@cortexkit/opencode-anthropic-auth 1.22.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 +37 -20
  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 +5 -1
  12. package/dist/fable-fallback.d.ts +1 -1
  13. package/dist/{index-926t2bzc.js → index-0jfsxyn9.js} +1 -1
  14. package/dist/index-1fq7h6yd.js +430 -0
  15. package/dist/{index-7f57ewkp.js → index-1gr8v4fn.js} +1 -1
  16. package/dist/index-797pmamn.js +2 -0
  17. package/dist/{index-bx5hsgf8.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 +27 -22
  24. package/dist/local-login.d.ts +64 -0
  25. package/dist/{logger-jrkranbz.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 +9 -2
  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 +3 -1
  47. package/dist/sidebar-state.js +1 -1
  48. package/dist/transform.d.ts +5 -1
  49. package/dist/tui/command-dialogs.d.ts +27 -2
  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 +18 -2
  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 +26 -2
  58. package/src/tui/command-dialogs.tsx +273 -68
  59. package/src/tui-compiled/rpc/protocol.ts +18 -2
  60. package/src/tui-compiled/rpc/rpc-client.ts +1 -1
  61. package/src/tui-compiled/sidebar-state.ts +26 -2
  62. package/src/tui-compiled/tui/command-dialogs.tsx +215 -61
  63. package/src/tui-compiled/tui.tsx +1 -0
  64. package/src/tui.tsx +1 -0
  65. package/dist/index-8aga2zwv.js +0 -4
  66. package/dist/index-cgez4m0h.js +0 -2
  67. package/dist/index-d3f8g6ve.js +0 -428
  68. package/dist/index-sh51yqxs.js +0 -2
@@ -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',
@@ -17,17 +19,31 @@ export type CommandModalName = (typeof COMMAND_MODAL_NAMES)[number]
17
19
  export interface AccountDialogAccount {
18
20
  id: string
19
21
  label: string
20
- role: string
22
+ role: 'main' | 'fallback'
21
23
  enabled: boolean
22
24
  quotaPercent: number | null
23
25
  tierLabel?: string
24
26
  claustrumGate: 'on' | 'off' | 'na'
25
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'
26
38
  }
27
39
 
28
40
  export interface AccountDialogKnobs {
29
41
  accounts: AccountDialogAccount[]
30
42
  claustrumDetection: string
43
+ custodyMode?: 'local' | 'claustrum'
44
+ custodyModeKnown?: boolean
45
+ enrollmentStatus?: string
46
+ claustrumEnrollment?: ClaustrumEnrollmentStatus
31
47
  [key: string]: unknown
32
48
  }
33
49
 
@@ -41,7 +57,7 @@ export interface RpcNotification {
41
57
  id: number
42
58
  type: 'open-dialog'
43
59
  payload: OpenDialogPayload
44
- sessionId?: string
60
+ sessionId: string
45
61
  }
46
62
 
47
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
+ }
@@ -36,6 +36,16 @@ export interface SidebarAccountState {
36
36
  needsReauth: boolean
37
37
  // True when the vault copy needs re-importing while the sidecar remains usable.
38
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'
39
49
  tierLabel?: string
40
50
  }
41
51
 
@@ -59,7 +69,7 @@ export interface PrimeSidebarAccountState {
59
69
  label: string
60
70
  nextDueAt?: number | null
61
71
  lastPrimedAt?: number | null
62
- lastResult?: 'ok' | 'error'
72
+ lastResult?: 'ok' | 'error' | 'skipped'
63
73
  usage?: PrimeUsageCounters
64
74
  estimatedCostUsd?: number
65
75
  }
@@ -180,7 +190,11 @@ function normalizePrimeAccount(
180
190
  } else if (isFiniteNumber(value.lastPrimedAt)) {
181
191
  account.lastPrimedAt = value.lastPrimedAt
182
192
  }
183
- if (value.lastResult === 'ok' || value.lastResult === 'error') {
193
+ if (
194
+ value.lastResult === 'ok' ||
195
+ value.lastResult === 'error' ||
196
+ value.lastResult === 'skipped'
197
+ ) {
184
198
  account.lastResult = value.lastResult
185
199
  }
186
200
  const usage = normalizePrimeUsage(value.usage)
@@ -338,6 +352,11 @@ export function normalizeSidebarState(raw: unknown): SidebarState {
338
352
  needsReauth:
339
353
  typeof entry.needsReauth === 'boolean' ? entry.needsReauth : false,
340
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
+ }),
341
360
  tierLabel:
342
361
  typeof entry.tierLabel === 'string' && entry.tierLabel.trim()
343
362
  ? entry.tierLabel.trim()
@@ -817,6 +836,8 @@ export function formatFallbackModelLabel(modelId: string | undefined): string {
817
836
  return 'Fable 5.1'
818
837
  if (modelId === 'claude-fable-5' || modelId?.startsWith('claude-fable-5-'))
819
838
  return 'Fable 5'
839
+ if (modelId === 'claude-opus-5-5' || modelId?.startsWith('claude-opus-5-5-'))
840
+ return 'Opus 5.5'
820
841
  if (modelId === 'claude-opus-5' || modelId?.startsWith('claude-opus-5-'))
821
842
  return 'Opus 5'
822
843
  if (modelId === 'claude-opus-4-8' || modelId?.startsWith('claude-opus-4-8-'))
@@ -844,6 +865,9 @@ export function formatPrimeAccountValue(account: PrimeSidebarAccountState): {
844
865
  if (account.lastResult === 'error') {
845
866
  return { text: 'err', hasError: true }
846
867
  }
868
+ if (account.lastResult === 'skipped') {
869
+ return { text: 'skip', hasError: false }
870
+ }
847
871
  if (account.nextDueAt && account.nextDueAt > Date.now()) {
848
872
  return { text: formatPrimeTime(account.nextDueAt), hasError: false }
849
873
  }
@@ -1,5 +1,8 @@
1
1
  /** @jsxImportSource @opentui/solid */
2
- import type { PrimeAccountStatus } from '@cortexkit/anthropic-auth-core'
2
+ import {
3
+ custodyStatusLabel,
4
+ type PrimeAccountStatus,
5
+ } from '@cortexkit/anthropic-auth-core'
3
6
  import type { TuiPluginApi } from '@opencode-ai/plugin/tui'
4
7
  import type { AccountDialogAccount } from '../rpc/protocol'
5
8
  import type { OpenDialogPayload } from '../rpc/protocol.js'
@@ -16,6 +19,27 @@ type KillswitchDialogConfig = {
16
19
  accounts?: Record<string, Record<string, number>>
17
20
  }
18
21
 
22
+ type TuiAccountDialogAccount = Omit<
23
+ AccountDialogAccount,
24
+ 'vaultReauth' | 'custodyState'
25
+ > &
26
+ Partial<Pick<AccountDialogAccount, 'vaultReauth' | 'custodyState'>>
27
+
28
+ type TuiAccountDialogPayload = {
29
+ accounts: TuiAccountDialogAccount[]
30
+ claustrumDetection: string
31
+ custodyMode?: TuiCustodyMode
32
+ enrollmentStatus?: string
33
+ }
34
+
35
+ type TuiCustodyMode = 'local' | 'claustrum' | `mismatch: ${string}`
36
+
37
+ type AccountDialogOption = {
38
+ title: string
39
+ value: string
40
+ description?: string
41
+ }
42
+
19
43
  export const PRIME_DIALOG_OPTIONS = [
20
44
  { title: 'Enable', value: 'on' },
21
45
  { title: 'Disable', value: 'off' },
@@ -49,24 +73,189 @@ export function buildKillswitchThresholdSeed(
49
73
  return seedParts.join(' ')
50
74
  }
51
75
 
52
- export function buildAccountDialogOption(account: AccountDialogAccount) {
76
+ function normalizeTuiCustodyMode(value: string): TuiCustodyMode {
77
+ return value === 'local' || value === 'claustrum'
78
+ ? value
79
+ : `mismatch: ${value}`
80
+ }
81
+
82
+ export function normalizeAccountDialogPayload(
83
+ value: unknown,
84
+ ): TuiAccountDialogPayload {
85
+ const payload =
86
+ value && typeof value === 'object' ? (value as Record<string, unknown>) : {}
87
+ const rawAccounts = Array.isArray(payload.accounts) ? payload.accounts : []
88
+ const accounts = rawAccounts.flatMap((value) => {
89
+ if (!value || typeof value !== 'object') return []
90
+ const account = value as Record<string, unknown>
91
+ const quotaPercent = account.quotaPercent
92
+ if (
93
+ typeof account.id !== 'string' ||
94
+ typeof account.label !== 'string' ||
95
+ (account.role !== 'main' && account.role !== 'fallback') ||
96
+ typeof account.enabled !== 'boolean' ||
97
+ (quotaPercent !== null &&
98
+ (typeof quotaPercent !== 'number' || !Number.isFinite(quotaPercent))) ||
99
+ (account.claustrumGate !== 'on' &&
100
+ account.claustrumGate !== 'off' &&
101
+ account.claustrumGate !== 'na') ||
102
+ typeof account.vaultServed !== 'boolean'
103
+ ) {
104
+ return []
105
+ }
106
+ const normalized = {
107
+ id: account.id,
108
+ label: account.label,
109
+ role: account.role,
110
+ enabled: account.enabled,
111
+ quotaPercent,
112
+ ...(typeof account.tierLabel === 'string' && {
113
+ tierLabel: account.tierLabel,
114
+ }),
115
+ claustrumGate: account.claustrumGate,
116
+ vaultServed: account.vaultServed,
117
+ } satisfies TuiAccountDialogAccount
118
+ const custodyState = account.custodyState
119
+ if (
120
+ (custodyState === 'na' ||
121
+ custodyState === 'off' ||
122
+ custodyState === 'on-vault-served' ||
123
+ custodyState === 'on-vault-reauth' ||
124
+ custodyState === 'on-cold') &&
125
+ typeof account.vaultReauth === 'boolean'
126
+ ) {
127
+ return [
128
+ {
129
+ ...normalized,
130
+ custodyState,
131
+ vaultReauth: account.vaultReauth,
132
+ },
133
+ ]
134
+ }
135
+ return [normalized]
136
+ })
137
+ const custodyMode: TuiCustodyMode | undefined =
138
+ payload.custodyModeKnown === true && typeof payload.custodyMode === 'string'
139
+ ? normalizeTuiCustodyMode(payload.custodyMode)
140
+ : undefined
141
+ return {
142
+ accounts,
143
+ claustrumDetection:
144
+ typeof payload.claustrumDetection === 'string'
145
+ ? payload.claustrumDetection
146
+ : 'unknown',
147
+ ...(custodyMode && { custodyMode }),
148
+ ...(typeof payload.enrollmentStatus === 'string' &&
149
+ payload.enrollmentStatus.length <= 2_000 && {
150
+ enrollmentStatus: payload.enrollmentStatus,
151
+ }),
152
+ }
153
+ }
154
+
155
+ export function buildAccountDialogOption(account: TuiAccountDialogAccount) {
53
156
  const pct =
54
157
  account.quotaPercent != null
55
158
  ? ` ${Math.round(account.quotaPercent)}%`
56
159
  : ' \u2013%'
57
160
  const status = !account.enabled ? ' (disabled)' : ''
58
- const gate = ` · gate ${account.claustrumGate === 'na' ? 'n/a' : account.claustrumGate}`
59
- const vault =
60
- account.role === 'main'
61
- ? ' · vault n/a'
62
- : ` · vault ${account.vaultServed ? 'served' : 'cold'}`
161
+ const custody = account.custodyState
162
+ ? account.custodyState === 'na'
163
+ ? undefined
164
+ : custodyStatusLabel(account.custodyState).replace(
165
+ ' (OpenCode managed)',
166
+ '',
167
+ )
168
+ : undefined
63
169
  return {
64
- title: `${account.label} [${account.role}]${status}${pct}${gate}${vault}`,
170
+ title: `${account.label} [${account.role}]${status}${pct}${custody ? ` · custody ${custody}` : ''}`,
65
171
  value: account.id,
66
172
  ...(account.tierLabel && { description: account.tierLabel }),
67
173
  }
68
174
  }
69
175
 
176
+ export function buildManageAccountOptions(account: TuiAccountDialogAccount) {
177
+ const options: AccountDialogOption[] = []
178
+ const toggleLabel = account.enabled ? 'Disable' : 'Enable'
179
+ options.push({
180
+ title: toggleLabel,
181
+ value: account.enabled ? 'disable' : 'enable',
182
+ description: account.enabled
183
+ ? 'Stop using this fallback account'
184
+ : 'Allow this fallback account to be used',
185
+ })
186
+ options.push({
187
+ title: 'Move up',
188
+ value: 'move-up',
189
+ description: 'Higher priority in fallback order',
190
+ })
191
+ options.push({
192
+ title: 'Move down',
193
+ value: 'move-down',
194
+ description: 'Lower priority in fallback order',
195
+ })
196
+ options.push({
197
+ title: 'Remove\u2026',
198
+ value: 'remove',
199
+ description: 'Delete this account permanently',
200
+ })
201
+ options.push({ title: 'Back', value: 'back' })
202
+ return options
203
+ }
204
+
205
+ export function buildAccountDialogL1(value: unknown): {
206
+ header: string
207
+ options: AccountDialogOption[]
208
+ enrollmentStatus?: string
209
+ modeAction?: { command: 'claude-account'; arguments: 'local' | 'claustrum' }
210
+ } {
211
+ const payload = normalizeAccountDialogPayload(value)
212
+ const modeAction =
213
+ payload.custodyMode === 'local' || payload.custodyMode === 'claustrum'
214
+ ? {
215
+ command: 'claude-account' as const,
216
+ arguments:
217
+ payload.custodyMode === 'local'
218
+ ? ('claustrum' as const)
219
+ : ('local' as const),
220
+ }
221
+ : undefined
222
+ return {
223
+ header: payload.custodyMode
224
+ ? `Custody mode: ${payload.custodyMode}`
225
+ : 'Custody mode: unavailable from older server',
226
+ options: [
227
+ ...(modeAction
228
+ ? [
229
+ {
230
+ title:
231
+ payload.custodyMode === 'local'
232
+ ? 'Use Claustrum custody'
233
+ : 'Use local custody',
234
+ value: '__custody-mode__',
235
+ },
236
+ ]
237
+ : []),
238
+ {
239
+ title: 'Add account…',
240
+ value: '__add__',
241
+ description: 'Add an API key or OAuth fallback account',
242
+ },
243
+ ...payload.accounts.map(buildAccountDialogOption),
244
+ ],
245
+ ...(payload.enrollmentStatus && {
246
+ enrollmentStatus: payload.enrollmentStatus,
247
+ }),
248
+ ...(modeAction && { modeAction }),
249
+ }
250
+ }
251
+
252
+ export function retainAccountDialogProjection(
253
+ current: Record<string, unknown>,
254
+ incoming: Record<string, unknown>,
255
+ ): Record<string, unknown> {
256
+ return Array.isArray(incoming.accounts) ? incoming : current
257
+ }
258
+
70
259
  function showText(api: TuiPluginApi, text: string) {
71
260
  api.ui.dialog.setSize('xlarge')
72
261
  api.ui.dialog.replace(() => (
@@ -99,6 +288,8 @@ export function buildPrimeStatusRows(accounts: PrimeAccountStatus[]): string[] {
99
288
  const time = formatPrimeTime(account.lastPrimedAt)
100
289
  if (account.lastResult === 'error') {
101
290
  rows.push(`${account.label} \u00b7 primed ${time} err`)
291
+ } else if (account.lastResult === 'skipped') {
292
+ rows.push(`${account.label} \u00b7 primed ${time} skipped`)
102
293
  } else {
103
294
  rows.push(`${account.label} \u00b7 primed ${time} \u2713`)
104
295
  }
@@ -377,17 +568,17 @@ export function openCommandDialog(
377
568
  }
378
569
 
379
570
  if (payload.command === 'claude-account') {
380
- const accounts =
381
- (payload.knobs.accounts as AccountDialogAccount[] | undefined) ?? []
382
- const claustrumDetection =
383
- (payload.knobs.claustrumDetection as string | undefined) ?? 'unknown'
571
+ let accountKnobs: Record<string, unknown> = payload.knobs
572
+ const accounts = normalizeAccountDialogPayload(accountKnobs).accounts
573
+ let statusMessage: { text: string; error?: boolean } | undefined
384
574
 
385
575
  const updateAccounts = (r: {
386
576
  text: string
387
577
  knobs: Record<string, unknown>
388
578
  }) => {
389
- const updated = r.knobs.accounts as typeof accounts
390
- if (updated && updated.length > 0) {
579
+ accountKnobs = retainAccountDialogProjection(accountKnobs, r.knobs)
580
+ const updated = normalizeAccountDialogPayload(accountKnobs).accounts
581
+ if (updated.length > 0) {
391
582
  accounts.length = 0
392
583
  accounts.push(...updated)
393
584
  }
@@ -395,31 +586,61 @@ export function openCommandDialog(
395
586
 
396
587
  const buildL1 = () => {
397
588
  const DialogSelect = api.ui.DialogSelect<string>
398
- const l1Options: Array<{
399
- title: string
400
- value: string
401
- description?: string
402
- }> = [
403
- {
404
- title: 'Add account\u2026',
405
- value: '__add__',
406
- description: 'Add an API key or OAuth fallback account',
407
- },
408
- ...accounts.map(buildAccountDialogOption),
409
- ]
589
+ const l1 = buildAccountDialogL1({ ...accountKnobs, accounts })
410
590
  api.ui.dialog.setSize('xlarge')
411
591
  api.ui.dialog.replace(() => (
412
592
  <box flexDirection='column' padding={1} width='100%'>
413
- <text>{`Claustrum: ${claustrumDetection}`}</text>
593
+ <text>{`Claustrum: ${normalizeAccountDialogPayload(accountKnobs).claustrumDetection}`}</text>
594
+ <text>{l1.header}</text>
595
+ {l1.enrollmentStatus && (
596
+ <box marginTop={1}>
597
+ <text>{l1.enrollmentStatus}</text>
598
+ </box>
599
+ )}
600
+ {statusMessage && (
601
+ <box marginTop={1}>
602
+ <text fg={statusMessage.error ? '#ef4444' : '#22c55e'}>
603
+ {statusMessage.text}
604
+ </text>
605
+ </box>
606
+ )}
414
607
  <box marginTop={1}>
415
608
  <DialogSelect
416
609
  title='Claude accounts'
417
- options={l1Options}
610
+ options={l1.options}
418
611
  onSelect={(option) => {
419
612
  if (option.value === '__add__') {
613
+ statusMessage = undefined
420
614
  openAddType()
421
615
  return
422
616
  }
617
+ if (option.value === '__custody-mode__' && l1.modeAction) {
618
+ void apply(l1.modeAction.command, l1.modeAction.arguments)
619
+ .then((r) => {
620
+ const isRefusalOrError =
621
+ r.text.includes('refused') ||
622
+ r.text.includes('unavailable') ||
623
+ r.text.includes('failed') ||
624
+ r.text.includes('Error') ||
625
+ r.text.includes('error')
626
+ if (isRefusalOrError) {
627
+ statusMessage = { text: r.text, error: true }
628
+ } else {
629
+ statusMessage = { text: r.text, error: false }
630
+ api.ui.toast({ message: r.text })
631
+ }
632
+ updateAccounts(r)
633
+ buildL1()
634
+ })
635
+ .catch((err) => {
636
+ const msg =
637
+ err instanceof Error ? err.message : String(err)
638
+ statusMessage = { text: `Error: ${msg}`, error: true }
639
+ buildL1()
640
+ })
641
+ return
642
+ }
643
+ statusMessage = undefined
423
644
  const account = accounts.find((a) => a.id === option.value)
424
645
  if (!account) return
425
646
  if (account.role === 'main') {
@@ -737,37 +958,9 @@ export function openCommandDialog(
737
958
  const DialogConfirm = api.ui.DialogConfirm
738
959
  api.ui.dialog.setSize('xlarge')
739
960
 
740
- const options: Array<{
741
- title: string
742
- value: string
743
- description?: string
744
- }> = []
745
- if (!isMain) {
746
- const toggleLabel = account.enabled ? 'Disable' : 'Enable'
747
- options.push({
748
- title: toggleLabel,
749
- value: account.enabled ? 'disable' : 'enable',
750
- description: account.enabled
751
- ? 'Stop using this fallback account'
752
- : 'Allow this fallback account to be used',
753
- })
754
- options.push({
755
- title: 'Move up',
756
- value: 'move-up',
757
- description: 'Higher priority in fallback order',
758
- })
759
- options.push({
760
- title: 'Move down',
761
- value: 'move-down',
762
- description: 'Lower priority in fallback order',
763
- })
764
- options.push({
765
- title: 'Remove\u2026',
766
- value: 'remove',
767
- description: 'Delete this account permanently',
768
- })
769
- }
770
- options.push({ title: 'Back', value: 'back' })
961
+ const options = isMain
962
+ ? [{ title: 'Back', value: 'back' }]
963
+ : buildManageAccountOptions(account)
771
964
 
772
965
  api.ui.dialog.replace(() => (
773
966
  <DialogSelect
@@ -785,13 +978,18 @@ export function openCommandDialog(
785
978
  title={`Remove ${account.label}?`}
786
979
  message={`Are you sure you want to remove the fallback account "${account.label}"?`}
787
980
  onConfirm={() => {
788
- void apply('claude-account', `remove ${account.id}`).then(
789
- (r) => {
981
+ void apply('claude-account', `remove ${account.id}`)
982
+ .then((r) => {
790
983
  api.ui.toast({ message: r.text })
791
984
  updateAccounts(r)
792
985
  buildL1()
793
- },
794
- )
986
+ })
987
+ .catch((err) => {
988
+ const msg =
989
+ err instanceof Error ? err.message : String(err)
990
+ statusMessage = { text: `Error: ${msg}`, error: true }
991
+ buildL1()
992
+ })
795
993
  }}
796
994
  onCancel={() => openManage(account, isMain)}
797
995
  />
@@ -799,18 +997,25 @@ export function openCommandDialog(
799
997
  return
800
998
  }
801
999
 
802
- void apply('claude-account', `${option.value} ${account.id}`).then(
803
- (r) => {
1000
+ const args = `${option.value} ${account.id}`
1001
+ void apply('claude-account', args)
1002
+ .then((r) => {
804
1003
  api.ui.toast({ message: r.text })
805
1004
  updateAccounts(r)
806
- const updatedList = r.knobs.accounts as typeof accounts
1005
+ const updatedList = normalizeAccountDialogPayload(
1006
+ r.knobs,
1007
+ ).accounts
807
1008
  const refreshed =
808
1009
  (updatedList && updatedList.length > 0
809
1010
  ? updatedList.find((a) => a.id === account.id)
810
1011
  : undefined) ?? account
811
1012
  openManage(refreshed, isMain)
812
- },
813
- )
1013
+ })
1014
+ .catch((err) => {
1015
+ const msg = err instanceof Error ? err.message : String(err)
1016
+ statusMessage = { text: `Error: ${msg}`, error: true }
1017
+ buildL1()
1018
+ })
814
1019
  }}
815
1020
  />
816
1021
  ))