@cortexkit/opencode-anthropic-auth 1.21.0 → 1.23.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.
Files changed (68) hide show
  1. package/README.md +57 -12
  2. package/dist/billing-lineage.d.ts +51 -0
  3. package/dist/claustrum-enrollment-registry.d.ts +4 -0
  4. package/dist/cli.d.ts +2 -1
  5. package/dist/cli.js +89 -6
  6. package/dist/custody-dimensions.d.ts +36 -0
  7. package/dist/custody-enrollment.d.ts +14 -0
  8. package/dist/custody-live.d.ts +111 -0
  9. package/dist/custody-manifest-watcher.d.ts +16 -0
  10. package/dist/custody-mode.d.ts +186 -0
  11. package/dist/effort-history.d.ts +62 -0
  12. package/dist/fable-fallback.d.ts +6 -7
  13. package/dist/{index-w9d41yy9.js → index-0jfsxyn9.js} +1 -1
  14. package/dist/index-1fq7h6yd.js +430 -0
  15. package/dist/{index-y0khnbt6.js → index-1gr8v4fn.js} +1 -1
  16. package/dist/index-797pmamn.js +2 -0
  17. package/dist/{index-1db17bc2.js → index-8y2t4mwz.js} +2 -2
  18. package/dist/index-a5d6v3ba.js +5 -0
  19. package/dist/index-n96q8pks.js +2 -0
  20. package/dist/index-xxhyhp98.js +2 -0
  21. package/dist/index-xzbhzz0a.js +2 -0
  22. package/dist/index-z7gfdyv3.js +1 -0
  23. package/dist/index.js +28 -21
  24. package/dist/local-login.d.ts +64 -0
  25. package/dist/{logger-ftk4bg3p.js → logger-kk9vyem9.js} +1 -1
  26. package/dist/prime-manager-registry.d.ts +5 -1
  27. package/dist/rpc/notifications.d.ts +3 -3
  28. package/dist/rpc/port-file.js +1 -1
  29. package/dist/rpc/protocol.d.ts +23 -1
  30. package/dist/rpc/protocol.js +1 -1
  31. package/dist/rpc/rpc-client.d.ts +1 -1
  32. package/dist/rpc/rpc-client.js +1 -1
  33. package/dist/rpc/rpc-dir.js +1 -1
  34. package/dist/rpc/server-registry.d.ts +10 -0
  35. package/dist/setup/activation.d.ts +7 -0
  36. package/dist/setup/claustrum.d.ts +26 -0
  37. package/dist/setup/command-runner.d.ts +2 -0
  38. package/dist/setup/command.d.ts +8 -0
  39. package/dist/setup/detect.d.ts +5 -0
  40. package/dist/setup/jsonc.d.ts +9 -0
  41. package/dist/setup/opencode-config.d.ts +18 -0
  42. package/dist/setup/paths.d.ts +15 -0
  43. package/dist/setup/pi.d.ts +8 -0
  44. package/dist/setup/process-fence.d.ts +14 -0
  45. package/dist/setup/types.d.ts +37 -0
  46. package/dist/sidebar-state.d.ts +5 -2
  47. package/dist/sidebar-state.js +1 -1
  48. package/dist/transform.d.ts +17 -1
  49. package/dist/tui/command-dialogs.d.ts +28 -9
  50. package/dist/tui-preferences.js +2 -2
  51. package/package.json +10 -5
  52. package/src/rpc/notifications.ts +28 -24
  53. package/src/rpc/protocol.ts +34 -1
  54. package/src/rpc/rpc-client.ts +1 -1
  55. package/src/rpc/rpc-server.ts +18 -5
  56. package/src/rpc/server-registry.ts +61 -0
  57. package/src/sidebar-state.ts +42 -3
  58. package/src/tui/command-dialogs.tsx +301 -97
  59. package/src/tui-compiled/rpc/protocol.ts +34 -1
  60. package/src/tui-compiled/rpc/rpc-client.ts +1 -1
  61. package/src/tui-compiled/sidebar-state.ts +42 -3
  62. package/src/tui-compiled/tui/command-dialogs.tsx +240 -70
  63. package/src/tui-compiled/tui.tsx +8 -3
  64. package/src/tui.tsx +18 -3
  65. package/dist/index-4nz423bt.js +0 -4
  66. package/dist/index-4x20896x.js +0 -2
  67. package/dist/index-wezdyxst.js +0 -2
  68. package/dist/index-yskwj5e6.js +0 -428
@@ -3,52 +3,56 @@ import type { OpenDialogPayload, RpcNotification } from './protocol'
3
3
  const QUEUE_CAP = 100
4
4
  const TUI_CONNECTED_WINDOW_MS = 3_000
5
5
 
6
+ // One queue serves every RPC server in the process, and a process can hold one
7
+ // server per project directory. Session ids are globally unique, so both
8
+ // producers and drains require one: an unscoped drain could otherwise expose
9
+ // another project's pending command dialog.
6
10
  let queue: RpcNotification[] = []
7
11
  let nextId = 1
8
- let lastDrainAtAny = 0
9
12
  const lastDrainAtBySession = new Map<string, number>()
10
13
 
14
+ function assertSessionId(sessionId: string): void {
15
+ if (typeof sessionId !== 'string' || sessionId.length === 0) {
16
+ throw new TypeError('sessionId is required')
17
+ }
18
+ }
19
+
11
20
  export function pushNotification(
12
21
  payload: OpenDialogPayload,
13
- sessionId?: string,
22
+ sessionId: string,
14
23
  ): void {
24
+ assertSessionId(sessionId)
15
25
  queue.push({ id: nextId++, type: 'open-dialog', payload, sessionId })
16
26
  if (queue.length > QUEUE_CAP) queue = queue.slice(queue.length - QUEUE_CAP)
17
27
  }
18
28
 
19
29
  export function drainNotifications(
20
- lastReceivedId = 0,
21
- sessionId?: string,
30
+ lastReceivedId: number,
31
+ sessionId: string,
22
32
  ): RpcNotification[] {
23
- const now = Date.now()
24
- lastDrainAtAny = now
25
- if (sessionId !== undefined) lastDrainAtBySession.set(sessionId, now)
26
- const matches = (n: RpcNotification) =>
27
- sessionId === undefined ||
28
- n.sessionId === undefined ||
29
- n.sessionId === sessionId
33
+ assertSessionId(sessionId)
34
+ lastDrainAtBySession.set(sessionId, Date.now())
30
35
  if (lastReceivedId > 0) {
31
- queue = queue.filter((n) => {
32
- if (n.id > lastReceivedId) return true
33
- if (sessionId === undefined) return false
34
- return n.sessionId !== sessionId
35
- })
36
+ queue = queue.filter(
37
+ (notification) =>
38
+ notification.id > lastReceivedId ||
39
+ notification.sessionId !== sessionId,
40
+ )
36
41
  }
37
- return queue.filter((n) => n.id > lastReceivedId && matches(n))
42
+ return queue.filter(
43
+ (notification) =>
44
+ notification.id > lastReceivedId && notification.sessionId === sessionId,
45
+ )
38
46
  }
39
47
 
40
- export function isTuiConnected(sessionId?: string): boolean {
48
+ export function isTuiConnected(sessionId: string): boolean {
41
49
  const now = Date.now()
42
- if (sessionId !== undefined) {
43
- const at = lastDrainAtBySession.get(sessionId) ?? 0
44
- return at > 0 && now - at < TUI_CONNECTED_WINDOW_MS
45
- }
46
- return lastDrainAtAny > 0 && now - lastDrainAtAny < TUI_CONNECTED_WINDOW_MS
50
+ const at = lastDrainAtBySession.get(sessionId) ?? 0
51
+ return at > 0 && now - at < TUI_CONNECTED_WINDOW_MS
47
52
  }
48
53
 
49
54
  export function resetNotificationsForTest(): void {
50
55
  queue = []
51
56
  nextId = 1
52
- lastDrainAtAny = 0
53
57
  lastDrainAtBySession.clear()
54
58
  }
@@ -1,3 +1,5 @@
1
+ import type { ClaustrumEnrollmentStatus } from '@cortexkit/anthropic-auth-core'
2
+
1
3
  export const COMMAND_MODAL_NAMES = [
2
4
  'claude-account',
3
5
  'claude-cache',
@@ -14,6 +16,37 @@ export const COMMAND_MODAL_NAMES = [
14
16
 
15
17
  export type CommandModalName = (typeof COMMAND_MODAL_NAMES)[number]
16
18
 
19
+ export interface AccountDialogAccount {
20
+ id: string
21
+ label: string
22
+ role: 'main' | 'fallback'
23
+ enabled: boolean
24
+ quotaPercent: number | null
25
+ tierLabel?: string
26
+ claustrumGate: 'on' | 'off' | 'na'
27
+ vaultServed: boolean
28
+ vaultReauth: boolean
29
+ custodyState:
30
+ | 'na'
31
+ | 'off'
32
+ | 'on-vault-served'
33
+ | 'on-vault-reauth'
34
+ | 'on-cold'
35
+ | 'unknown-identity'
36
+ | 'on-identity-mismatch'
37
+ | 'on-corrupt-binding'
38
+ }
39
+
40
+ export interface AccountDialogKnobs {
41
+ accounts: AccountDialogAccount[]
42
+ claustrumDetection: string
43
+ custodyMode?: 'local' | 'claustrum'
44
+ custodyModeKnown?: boolean
45
+ enrollmentStatus?: string
46
+ claustrumEnrollment?: ClaustrumEnrollmentStatus
47
+ [key: string]: unknown
48
+ }
49
+
17
50
  export interface OpenDialogPayload {
18
51
  command: CommandModalName
19
52
  text: string
@@ -24,7 +57,7 @@ export interface RpcNotification {
24
57
  id: number
25
58
  type: 'open-dialog'
26
59
  payload: OpenDialogPayload
27
- sessionId?: string
60
+ sessionId: string
28
61
  }
29
62
 
30
63
  export interface ApplyRequest {
@@ -4,7 +4,7 @@ import type { ApplyRequest, ApplyResult, RpcNotification } from './protocol'
4
4
  export interface RpcClient {
5
5
  pending: (
6
6
  lastReceivedId: number,
7
- sessionId?: string,
7
+ sessionId: string,
8
8
  ) => Promise<RpcNotification[]>
9
9
  apply: (request: ApplyRequest) => Promise<ApplyResult>
10
10
  }
@@ -1,5 +1,5 @@
1
1
  import { randomBytes, timingSafeEqual } from 'node:crypto'
2
- import { unlink } from 'node:fs/promises'
2
+ import { readFile, unlink } from 'node:fs/promises'
3
3
  import {
4
4
  createServer,
5
5
  type IncomingMessage,
@@ -74,9 +74,15 @@ export async function startRpcServer(
74
74
  unknown
75
75
  >
76
76
  if (method === 'pending-notifications') {
77
+ if (
78
+ typeof params.sessionId !== 'string' ||
79
+ params.sessionId.length === 0
80
+ ) {
81
+ return json(400, { error: 'sessionId is required' })
82
+ }
77
83
  const messages = options.drain(
78
84
  Number(params.lastReceivedId ?? 0),
79
- typeof params.sessionId === 'string' ? params.sessionId : undefined,
85
+ params.sessionId,
80
86
  )
81
87
  return json(200, { messages })
82
88
  }
@@ -115,9 +121,16 @@ export async function startRpcServer(
115
121
  token,
116
122
  async stop() {
117
123
  await new Promise<void>((resolve) => server.close(() => resolve()))
118
- await unlink(join(options.dir, `port-${process.pid}.json`)).catch(
119
- () => {},
120
- )
124
+ try {
125
+ const portFile = join(options.dir, `port-${process.pid}.json`)
126
+ const current = JSON.parse(await readFile(portFile, 'utf8')) as {
127
+ port?: unknown
128
+ pid?: unknown
129
+ }
130
+ if (current.port === port && current.pid === process.pid) {
131
+ await unlink(portFile)
132
+ }
133
+ } catch {}
121
134
  },
122
135
  }
123
136
  }
@@ -0,0 +1,61 @@
1
+ import type { RpcServerHandle } from './rpc-server'
2
+
3
+ type RpcServerGlobal = typeof globalThis & {
4
+ __anthropicAuthRpcServers?: Map<string, RpcServerHandle>
5
+ __anthropicAuthRpcServersPending?: Map<string, Promise<void>>
6
+ }
7
+
8
+ export interface RpcServerAdoption {
9
+ server: RpcServerHandle
10
+ release: () => Promise<void>
11
+ }
12
+
13
+ /**
14
+ * Serializes same-directory replacement and returns an identity-fenced lease.
15
+ * Distinct project directories remain independent.
16
+ */
17
+ export async function adoptRpcServer(
18
+ rpcDir: string,
19
+ create: () => Promise<RpcServerHandle>,
20
+ ): Promise<RpcServerAdoption> {
21
+ const rpcGlobal = globalThis as RpcServerGlobal
22
+ const servers =
23
+ rpcGlobal.__anthropicAuthRpcServers ?? new Map<string, RpcServerHandle>()
24
+ const pending =
25
+ rpcGlobal.__anthropicAuthRpcServersPending ??
26
+ new Map<string, Promise<void>>()
27
+ rpcGlobal.__anthropicAuthRpcServers = servers
28
+ rpcGlobal.__anthropicAuthRpcServersPending = pending
29
+
30
+ const predecessor = pending.get(rpcDir) ?? Promise.resolve()
31
+ let server: RpcServerHandle | undefined
32
+ const start = predecessor
33
+ .catch(() => {})
34
+ .then(async () => {
35
+ const previous = servers.get(rpcDir)
36
+ if (previous) {
37
+ await previous.stop()
38
+ if (servers.get(rpcDir) === previous) servers.delete(rpcDir)
39
+ }
40
+ server = await create()
41
+ servers.set(rpcDir, server)
42
+ })
43
+ pending.set(rpcDir, start)
44
+
45
+ try {
46
+ await start
47
+ } finally {
48
+ if (pending.get(rpcDir) === start) pending.delete(rpcDir)
49
+ }
50
+ const adoptedServer = server
51
+ if (!adoptedServer) throw new Error('RPC server failed to start')
52
+
53
+ return {
54
+ server: adoptedServer,
55
+ release: async () => {
56
+ if (servers.get(rpcDir) !== adoptedServer) return
57
+ await adoptedServer.stop()
58
+ if (servers.get(rpcDir) === adoptedServer) servers.delete(rpcDir)
59
+ },
60
+ }
61
+ }
@@ -34,6 +34,18 @@ export interface SidebarAccountState {
34
34
  // True when the account's refresh token is permanently dead (400
35
35
  // invalid_grant) and it needs a re-login — distinct from a transient backoff.
36
36
  needsReauth: boolean
37
+ // True when the vault copy needs re-importing while the sidecar remains usable.
38
+ vaultReauth?: boolean
39
+ // A gate alone does not prove the sidecar can serve a request.
40
+ vaultServed?: boolean
41
+ custodyState?:
42
+ | 'off'
43
+ | 'on-vault-served'
44
+ | 'on-vault-reauth'
45
+ | 'on-cold'
46
+ | 'unknown-identity'
47
+ | 'on-identity-mismatch'
48
+ | 'on-corrupt-binding'
37
49
  tierLabel?: string
38
50
  }
39
51
 
@@ -57,7 +69,7 @@ export interface PrimeSidebarAccountState {
57
69
  label: string
58
70
  nextDueAt?: number | null
59
71
  lastPrimedAt?: number | null
60
- lastResult?: 'ok' | 'error'
72
+ lastResult?: 'ok' | 'error' | 'skipped'
61
73
  usage?: PrimeUsageCounters
62
74
  estimatedCostUsd?: number
63
75
  }
@@ -178,7 +190,11 @@ function normalizePrimeAccount(
178
190
  } else if (isFiniteNumber(value.lastPrimedAt)) {
179
191
  account.lastPrimedAt = value.lastPrimedAt
180
192
  }
181
- if (value.lastResult === 'ok' || value.lastResult === 'error') {
193
+ if (
194
+ value.lastResult === 'ok' ||
195
+ value.lastResult === 'error' ||
196
+ value.lastResult === 'skipped'
197
+ ) {
182
198
  account.lastResult = value.lastResult
183
199
  }
184
200
  const usage = normalizePrimeUsage(value.usage)
@@ -335,6 +351,12 @@ export function normalizeSidebarState(raw: unknown): SidebarState {
335
351
  enabled: typeof entry.enabled === 'boolean' ? entry.enabled : false,
336
352
  needsReauth:
337
353
  typeof entry.needsReauth === 'boolean' ? entry.needsReauth : false,
354
+ ...(entry.vaultReauth === true && { vaultReauth: true }),
355
+ ...(entry.vaultServed === true && { vaultServed: true }),
356
+ ...(typeof entry.custodyState === 'string' && {
357
+ custodyState:
358
+ entry.custodyState as SidebarAccountState['custodyState'],
359
+ }),
338
360
  tierLabel:
339
361
  typeof entry.tierLabel === 'string' && entry.tierLabel.trim()
340
362
  ? entry.tierLabel.trim()
@@ -715,6 +737,8 @@ export async function setSidebarState(
715
737
  lastUpdated: Math.max(current.lastUpdated, state.lastUpdated),
716
738
  }
717
739
  }
740
+ // The state file is a cross-process artifact; future account fields must fail closed.
741
+ stateToWrite = normalizeSidebarState(stateToWrite)
718
742
  result = await writeSidebarStateAtomic(
719
743
  stateFile,
720
744
  stateToWrite,
@@ -795,13 +819,25 @@ export function formatScopedQuotaLabel(title: string) {
795
819
  }
796
820
 
797
821
  /**
798
- * Pretty label for a recoverable-refusal source model (Fable 5 or Opus 5).
822
+ * Pretty label for a recoverable-refusal source model (Fable 5/5.1 or Opus 5).
799
823
  * Falls back to the raw id for unrecognized ids so the sidebar never goes
800
824
  * blank on a future model that has not yet been cataloged here.
801
825
  */
802
826
  export function formatFallbackModelLabel(modelId: string | undefined): string {
827
+ if (
828
+ modelId === 'claude-mythos-5-1' ||
829
+ modelId?.startsWith('claude-mythos-5-1-')
830
+ )
831
+ return 'Mythos 5.1'
832
+ if (
833
+ modelId === 'claude-fable-5-1' ||
834
+ modelId?.startsWith('claude-fable-5-1-')
835
+ )
836
+ return 'Fable 5.1'
803
837
  if (modelId === 'claude-fable-5' || modelId?.startsWith('claude-fable-5-'))
804
838
  return 'Fable 5'
839
+ if (modelId === 'claude-opus-5-5' || modelId?.startsWith('claude-opus-5-5-'))
840
+ return 'Opus 5.5'
805
841
  if (modelId === 'claude-opus-5' || modelId?.startsWith('claude-opus-5-'))
806
842
  return 'Opus 5'
807
843
  if (modelId === 'claude-opus-4-8' || modelId?.startsWith('claude-opus-4-8-'))
@@ -829,6 +865,9 @@ export function formatPrimeAccountValue(account: PrimeSidebarAccountState): {
829
865
  if (account.lastResult === 'error') {
830
866
  return { text: 'err', hasError: true }
831
867
  }
868
+ if (account.lastResult === 'skipped') {
869
+ return { text: 'skip', hasError: false }
870
+ }
832
871
  if (account.nextDueAt && account.nextDueAt > Date.now()) {
833
872
  return { text: formatPrimeTime(account.nextDueAt), hasError: false }
834
873
  }