opencode-auth-load-balancer 0.5.1 → 0.7.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 (49) hide show
  1. package/README.md +152 -12
  2. package/dist/accounts.d.ts +22 -2
  3. package/dist/index.d.ts +7 -12
  4. package/dist/index.js +46026 -14563
  5. package/dist/legacy.d.ts +25 -0
  6. package/dist/login-health.d.ts +12 -0
  7. package/dist/notify.d.ts +6 -0
  8. package/dist/pairing.d.ts +22 -0
  9. package/dist/pending/coordinator.d.ts +5 -1
  10. package/dist/pending/v2.d.ts +3 -0
  11. package/dist/pool/paths.d.ts +8 -0
  12. package/dist/providers/anthropic/constants.d.ts +21 -0
  13. package/dist/providers/anthropic/oauth.d.ts +9 -0
  14. package/dist/providers/anthropic/setup-token.d.ts +14 -0
  15. package/dist/providers/anthropic/usage.d.ts +13 -0
  16. package/dist/providers/types.d.ts +27 -0
  17. package/dist/refresh.d.ts +12 -0
  18. package/dist/status.d.ts +6 -0
  19. package/dist/sync/apply.d.ts +66 -0
  20. package/dist/sync/crypto.d.ts +19 -0
  21. package/dist/sync/cycle.d.ts +19 -0
  22. package/dist/sync/engine.d.ts +44 -0
  23. package/dist/sync/errors.d.ts +9 -0
  24. package/dist/sync/gist.d.ts +33 -0
  25. package/dist/sync/github-auth.d.ts +15 -0
  26. package/dist/sync/intent.d.ts +29 -0
  27. package/dist/sync/loop.d.ts +40 -0
  28. package/dist/sync/merge.d.ts +49 -0
  29. package/dist/sync/origin.d.ts +2 -0
  30. package/dist/sync/own.d.ts +31 -0
  31. package/dist/sync/payload.d.ts +50 -0
  32. package/dist/sync/start.d.ts +5 -0
  33. package/dist/sync/state.d.ts +79 -0
  34. package/dist/sync/timing.d.ts +4 -0
  35. package/dist/token-mint.d.ts +25 -0
  36. package/dist/types.d.ts +75 -0
  37. package/dist/util.d.ts +7 -0
  38. package/dist/v1.d.ts +1 -0
  39. package/dist/v1.js +18393 -0
  40. package/dist/v2-rpc.d.ts +62 -0
  41. package/dist/v2.d.ts +2 -0
  42. package/index.js +2 -0
  43. package/package.json +46 -8
  44. package/tui/auth-load-balancer-rpc.ts +60 -0
  45. package/tui/auth-load-balancer-tui.logic.ts +363 -7
  46. package/tui/auth-load-balancer-tui.view.tsx +299 -107
  47. package/tui/auth-load-balancer-v2.ts +10 -0
  48. package/tui/auth-load-balancer-v2.view.tsx +242 -0
  49. package/tui.ts +2 -0
@@ -0,0 +1,242 @@
1
+ /** @jsxImportSource @opentui/solid */
2
+ import type { Plugin } from '@opencode/plugin/tui'
3
+ import { createSignal, For, onCleanup, Show } from 'solid-js'
4
+
5
+ import { loadBalancerRpc, type ProviderStatus } from './auth-load-balancer-rpc'
6
+
7
+ const names: Record<string, string> = {
8
+ anthropic: 'Claude',
9
+ openai: 'Codex',
10
+ 'kimi-code-plan-cn': 'Kimi',
11
+ 'kimi-code-plan-global': 'Kimi (kimi.ai)',
12
+ }
13
+ const pct = (value: number | null) =>
14
+ value === null ? '?' : `${Math.round(value * 100)}%`
15
+
16
+ function StatusView(props: {
17
+ ctx: Plugin.Context
18
+ sessionID?: string
19
+ compact?: boolean
20
+ }) {
21
+ const rpc = props.ctx.client.rpc(loadBalancerRpc)
22
+ const [providers, setProviders] = createSignal<ProviderStatus[]>([])
23
+ const [error, setError] = createSignal('')
24
+ let disposed = false
25
+ const [refreshing, setRefreshing] = createSignal(false)
26
+ const refresh = async () => {
27
+ if (refreshing()) return
28
+ setRefreshing(true)
29
+ try {
30
+ const result = await rpc.status(
31
+ { sessionID: props.sessionID },
32
+ { location: props.ctx.location },
33
+ )
34
+ if (!disposed) {
35
+ setProviders(result)
36
+ setError('')
37
+ }
38
+ } catch {
39
+ if (!disposed) setError('Auth Load Balancer unavailable')
40
+ } finally {
41
+ setRefreshing(false)
42
+ }
43
+ }
44
+ const timer = setInterval(() => void refresh(), 3000)
45
+ onCleanup(() => {
46
+ disposed = true
47
+ clearInterval(timer)
48
+ })
49
+ void refresh()
50
+
51
+ const login = async (
52
+ provider: ProviderStatus,
53
+ account: ProviderStatus['accounts'][number],
54
+ ) => {
55
+ const result = await props.ctx.client.integration.get({
56
+ integrationID: provider.providerID,
57
+ location: props.ctx.location,
58
+ })
59
+ const methods = result.data.methods.flatMap((method) =>
60
+ method.type === 'oauth' && method.id.startsWith('auth-lb-')
61
+ ? [method]
62
+ : [],
63
+ )
64
+ const methodID = await props.ctx.ui.dialog.select({
65
+ title: `Sign in to ${names[provider.providerID] ?? provider.providerID}`,
66
+ options: methods.map((method) => ({
67
+ title: method.label,
68
+ value: method.id,
69
+ })),
70
+ })
71
+ if (!methodID) return
72
+ await rpc.manage(
73
+ { account: account.id, action: 'relogin' },
74
+ { location: props.ctx.location },
75
+ )
76
+ let attemptID: string | undefined
77
+ try {
78
+ const { data } = await props.ctx.client.integration.oauth.connect({
79
+ integrationID: provider.providerID,
80
+ methodID,
81
+ location: props.ctx.location,
82
+ })
83
+ attemptID = data.attemptID
84
+ const code =
85
+ data.mode === 'code'
86
+ ? await props.ctx.ui.dialog.prompt({
87
+ title: 'Authorize account',
88
+ description: `${data.url}\n${data.instructions}`,
89
+ })
90
+ : await props.ctx.ui.dialog.confirm({
91
+ title: 'Authorize account',
92
+ message: `${data.url}\n${data.instructions}\nContinue after approving in your browser.`,
93
+ })
94
+ if (code === undefined || code === false) return
95
+ await props.ctx.client.integration.oauth.complete({
96
+ integrationID: provider.providerID,
97
+ attemptID,
98
+ ...(typeof code === 'string' ? { code } : {}),
99
+ location: props.ctx.location,
100
+ })
101
+ attemptID = undefined
102
+ props.ctx.ui.toast.show({
103
+ message: 'Account added to load balancer',
104
+ variant: 'success',
105
+ })
106
+ } finally {
107
+ if (attemptID)
108
+ await props.ctx.client.integration.oauth
109
+ .cancel({
110
+ integrationID: provider.providerID,
111
+ attemptID,
112
+ location: props.ctx.location,
113
+ })
114
+ .catch(() => {})
115
+ await rpc
116
+ .manage(
117
+ { account: account.id, action: 'clear-relogin' },
118
+ { location: props.ctx.location },
119
+ )
120
+ .catch(() => {})
121
+ }
122
+ }
123
+ const manage = async (
124
+ provider: ProviderStatus,
125
+ account: ProviderStatus['accounts'][number],
126
+ ) => {
127
+ try {
128
+ const disabled = account.disabledReason === 'manually disabled'
129
+ const action = await props.ctx.ui.dialog.select({
130
+ title: account.label,
131
+ options: [
132
+ { title: 'Rename', value: 'rename' as const },
133
+ {
134
+ title: disabled ? 'Enable' : 'Disable',
135
+ value: disabled ? ('enable' as const) : ('disable' as const),
136
+ },
137
+ { title: 'Re-login / pair credential', value: 'relogin' as const },
138
+ { title: 'Delete', value: 'delete' as const },
139
+ ],
140
+ })
141
+ if (!action) return
142
+ if (action === 'relogin') {
143
+ await login(provider, account)
144
+ return
145
+ }
146
+ const name =
147
+ action === 'rename'
148
+ ? await props.ctx.ui.dialog.prompt({
149
+ title: 'Rename account',
150
+ value: account.label,
151
+ })
152
+ : undefined
153
+ if (action === 'rename' && name === undefined) return
154
+ if (
155
+ action === 'delete' &&
156
+ !(await props.ctx.ui.dialog.confirm({
157
+ title: 'Delete account',
158
+ message: `Remove ${account.label} from the pool?`,
159
+ }))
160
+ )
161
+ return
162
+ await rpc.manage(
163
+ { account: account.id, action, name },
164
+ { location: props.ctx.location },
165
+ )
166
+ } catch (cause) {
167
+ props.ctx.ui.toast.show({
168
+ message:
169
+ cause instanceof Error ? cause.message : 'Account update failed',
170
+ variant: 'error',
171
+ })
172
+ } finally {
173
+ await refresh()
174
+ }
175
+ }
176
+
177
+ return (
178
+ <box flexDirection={props.compact ? 'row' : 'column'} gap={1}>
179
+ <Show when={error()}>
180
+ <text fg={props.ctx.theme.text.muted}>{error()}</text>
181
+ </Show>
182
+ <For each={providers()}>
183
+ {(provider) => (
184
+ <box flexDirection="column">
185
+ <Show when={!props.compact}>
186
+ <text fg={props.ctx.theme.text.base}>
187
+ <b>{names[provider.providerID] ?? provider.providerID}</b>
188
+ </text>
189
+ </Show>
190
+ <For
191
+ each={
192
+ props.compact
193
+ ? provider.accounts.filter((account) => account.current)
194
+ : provider.accounts
195
+ }
196
+ >
197
+ {(account) => (
198
+ <box
199
+ flexDirection="column"
200
+ onMouseDown={() => void manage(provider, account)}
201
+ >
202
+ <text fg={props.ctx.theme.text.base} wrapMode="word">
203
+ {`${account.current ? '●' : `#${account.rank}`} ${props.compact ? `${names[provider.providerID] ?? provider.providerID}: ` : ''}${account.label} · weekly ${pct(account.weeklyUtil)} · 5h ${pct(account.hourlyUtil)}${account.disabledReason ? ' · disabled' : account.available ? '' : ' · waiting'}`}
204
+ </text>
205
+ <Show when={!props.compact && account.warnings.length > 0}>
206
+ <text fg={props.ctx.theme.text.muted}>
207
+ {account.warnings.join(' · ')}
208
+ </text>
209
+ </Show>
210
+ </box>
211
+ )}
212
+ </For>
213
+ </box>
214
+ )}
215
+ </For>
216
+ </box>
217
+ )
218
+ }
219
+
220
+ export function registerV2Slots(ctx: Plugin.Context): () => void {
221
+ const stopToast = ctx.client
222
+ .rpc(loadBalancerRpc)
223
+ .events.on('toast', (event) => {
224
+ if (event.location?.directory === ctx.location?.directory)
225
+ ctx.ui.toast.show(event.data)
226
+ })
227
+ const stopFooter = ctx.ui.slot({
228
+ after: 'prompt.footer',
229
+ render: (props) => (
230
+ <StatusView compact ctx={ctx} sessionID={props.sessionID} />
231
+ ),
232
+ })
233
+ const stopSidebar = ctx.ui.slot({
234
+ append: 'sidebar.content',
235
+ render: (props) => <StatusView ctx={ctx} sessionID={props.sessionID} />,
236
+ })
237
+ return () => {
238
+ stopToast()
239
+ stopFooter()
240
+ stopSidebar()
241
+ }
242
+ }
package/tui.ts ADDED
@@ -0,0 +1,2 @@
1
+ // Local directory discovery and the published ./tui export share this entry.
2
+ export { default } from './tui/auth-load-balancer-v2'