byterover-cli 2.2.0 → 2.3.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 (90) hide show
  1. package/dist/agent/infra/llm/providers/openai.d.ts +12 -0
  2. package/dist/agent/infra/llm/providers/openai.js +52 -1
  3. package/dist/oclif/commands/curate/index.js +2 -2
  4. package/dist/oclif/commands/model/switch.js +14 -3
  5. package/dist/oclif/commands/providers/connect.d.ts +9 -0
  6. package/dist/oclif/commands/providers/connect.js +110 -14
  7. package/dist/oclif/commands/providers/list.js +3 -5
  8. package/dist/oclif/commands/query.js +2 -2
  9. package/dist/oclif/lib/daemon-client.d.ts +4 -0
  10. package/dist/oclif/lib/daemon-client.js +13 -3
  11. package/dist/server/core/domain/entities/provider-config.d.ts +6 -0
  12. package/dist/server/core/domain/entities/provider-config.js +4 -3
  13. package/dist/server/core/domain/entities/provider-registry.d.ts +41 -1
  14. package/dist/server/core/domain/entities/provider-registry.js +24 -1
  15. package/dist/server/core/domain/errors/task-error.d.ts +2 -0
  16. package/dist/server/core/domain/errors/task-error.js +6 -1
  17. package/dist/server/core/domain/transport/schemas.d.ts +2 -0
  18. package/dist/server/core/interfaces/i-provider-config-store.d.ts +2 -0
  19. package/dist/server/core/interfaces/i-provider-model-fetcher.d.ts +12 -3
  20. package/dist/server/core/interfaces/i-provider-oauth-token-store.d.ts +24 -0
  21. package/dist/server/core/interfaces/i-provider-oauth-token-store.js +1 -0
  22. package/dist/server/core/interfaces/i-token-refresh-manager.d.ts +7 -0
  23. package/dist/server/core/interfaces/i-token-refresh-manager.js +1 -0
  24. package/dist/server/infra/daemon/agent-process.js +22 -4
  25. package/dist/server/infra/daemon/brv-server.js +13 -2
  26. package/dist/server/infra/http/models-dev-client.d.ts +29 -0
  27. package/dist/server/infra/http/models-dev-client.js +133 -0
  28. package/dist/server/infra/http/provider-model-fetcher-registry.d.ts +2 -1
  29. package/dist/server/infra/http/provider-model-fetcher-registry.js +6 -1
  30. package/dist/server/infra/http/provider-model-fetchers.d.ts +33 -8
  31. package/dist/server/infra/http/provider-model-fetchers.js +88 -10
  32. package/dist/server/infra/process/feature-handlers.d.ts +3 -1
  33. package/dist/server/infra/process/feature-handlers.js +3 -1
  34. package/dist/server/infra/provider/provider-config-resolver.d.ts +4 -2
  35. package/dist/server/infra/provider/provider-config-resolver.js +59 -4
  36. package/dist/server/infra/provider-oauth/callback-server.d.ts +24 -0
  37. package/dist/server/infra/provider-oauth/callback-server.js +203 -0
  38. package/dist/server/infra/provider-oauth/errors.d.ts +39 -0
  39. package/dist/server/infra/provider-oauth/errors.js +76 -0
  40. package/dist/server/infra/provider-oauth/index.d.ts +9 -0
  41. package/dist/server/infra/provider-oauth/index.js +9 -0
  42. package/dist/server/infra/provider-oauth/jwt-utils.d.ts +17 -0
  43. package/dist/server/infra/provider-oauth/jwt-utils.js +51 -0
  44. package/dist/server/infra/provider-oauth/pkce-service.d.ts +22 -0
  45. package/dist/server/infra/provider-oauth/pkce-service.js +33 -0
  46. package/dist/server/infra/provider-oauth/provider-oauth-token-store.d.ts +48 -0
  47. package/dist/server/infra/provider-oauth/provider-oauth-token-store.js +155 -0
  48. package/dist/server/infra/provider-oauth/refresh-token-exchange.d.ts +8 -0
  49. package/dist/server/infra/provider-oauth/refresh-token-exchange.js +39 -0
  50. package/dist/server/infra/provider-oauth/token-exchange.d.ts +8 -0
  51. package/dist/server/infra/provider-oauth/token-exchange.js +44 -0
  52. package/dist/server/infra/provider-oauth/token-refresh-manager.d.ts +32 -0
  53. package/dist/server/infra/provider-oauth/token-refresh-manager.js +96 -0
  54. package/dist/server/infra/provider-oauth/types.d.ts +55 -0
  55. package/dist/server/infra/provider-oauth/types.js +22 -0
  56. package/dist/server/infra/storage/file-provider-config-store.d.ts +2 -0
  57. package/dist/server/infra/storage/file-provider-config-store.js +1 -3
  58. package/dist/server/infra/transport/handlers/model-handler.d.ts +3 -0
  59. package/dist/server/infra/transport/handlers/model-handler.js +53 -11
  60. package/dist/server/infra/transport/handlers/provider-handler.d.ts +26 -0
  61. package/dist/server/infra/transport/handlers/provider-handler.js +215 -13
  62. package/dist/shared/constants/oauth.d.ts +14 -0
  63. package/dist/shared/constants/oauth.js +14 -0
  64. package/dist/shared/transport/events/index.d.ts +5 -0
  65. package/dist/shared/transport/events/model-events.d.ts +2 -0
  66. package/dist/shared/transport/events/provider-events.d.ts +36 -0
  67. package/dist/shared/transport/events/provider-events.js +5 -0
  68. package/dist/shared/transport/types/dto.d.ts +4 -0
  69. package/dist/tui/features/model/api/set-active-model.d.ts +1 -1
  70. package/dist/tui/features/model/api/set-active-model.js +12 -4
  71. package/dist/tui/features/provider/api/await-oauth-callback.d.ts +11 -0
  72. package/dist/tui/features/provider/api/await-oauth-callback.js +25 -0
  73. package/dist/tui/features/provider/api/cancel-oauth.d.ts +5 -0
  74. package/dist/tui/features/provider/api/cancel-oauth.js +10 -0
  75. package/dist/tui/features/provider/api/start-oauth.d.ts +11 -0
  76. package/dist/tui/features/provider/api/start-oauth.js +15 -0
  77. package/dist/tui/features/provider/components/auth-method-dialog.d.ts +9 -0
  78. package/dist/tui/features/provider/components/auth-method-dialog.js +20 -0
  79. package/dist/tui/features/provider/components/oauth-dialog.d.ts +9 -0
  80. package/dist/tui/features/provider/components/oauth-dialog.js +96 -0
  81. package/dist/tui/features/provider/components/provider-dialog.js +1 -1
  82. package/dist/tui/features/provider/components/provider-flow.js +54 -4
  83. package/dist/tui/features/provider/components/provider-subscription-initializer.d.ts +1 -0
  84. package/dist/tui/features/provider/components/provider-subscription-initializer.js +5 -0
  85. package/dist/tui/features/provider/hooks/use-provider-subscriptions.d.ts +6 -0
  86. package/dist/tui/features/provider/hooks/use-provider-subscriptions.js +24 -0
  87. package/dist/tui/providers/app-providers.js +2 -1
  88. package/dist/tui/utils/error-messages.js +6 -1
  89. package/oclif.manifest.json +132 -116
  90. package/package.json +1 -1
@@ -0,0 +1,25 @@
1
+ import { useMutation, useQueryClient } from '@tanstack/react-query';
2
+ import { OAUTH_CALLBACK_TIMEOUT_MS } from '../../../../shared/constants/oauth.js';
3
+ import { ProviderEvents, } from '../../../../shared/transport/events/index.js';
4
+ import { useTransportStore } from '../../../stores/transport-store.js';
5
+ import { getProvidersQueryOptions } from './get-providers.js';
6
+ export const awaitOAuthCallback = ({ providerId, }) => {
7
+ const { apiClient } = useTransportStore.getState();
8
+ if (!apiClient)
9
+ return Promise.reject(new Error('Not connected'));
10
+ return apiClient.request(ProviderEvents.AWAIT_OAUTH_CALLBACK, { providerId }, { timeout: OAUTH_CALLBACK_TIMEOUT_MS });
11
+ };
12
+ export const useAwaitOAuthCallback = ({ mutationConfig } = {}) => {
13
+ const queryClient = useQueryClient();
14
+ const { onSuccess, ...restConfig } = mutationConfig ?? {};
15
+ return useMutation({
16
+ onSuccess(...args) {
17
+ queryClient.invalidateQueries({
18
+ queryKey: getProvidersQueryOptions().queryKey,
19
+ });
20
+ onSuccess?.(...args);
21
+ },
22
+ ...restConfig,
23
+ mutationFn: awaitOAuthCallback,
24
+ });
25
+ };
@@ -0,0 +1,5 @@
1
+ import { type ProviderCancelOAuthResponse } from '../../../../shared/transport/events/index.js';
2
+ export type CancelOAuthDTO = {
3
+ providerId: string;
4
+ };
5
+ export declare const cancelOAuth: ({ providerId }: CancelOAuthDTO) => Promise<ProviderCancelOAuthResponse | void>;
@@ -0,0 +1,10 @@
1
+ import { ProviderEvents, } from '../../../../shared/transport/events/index.js';
2
+ import { useTransportStore } from '../../../stores/transport-store.js';
3
+ export const cancelOAuth = ({ providerId }) => {
4
+ const { apiClient } = useTransportStore.getState();
5
+ if (!apiClient)
6
+ return Promise.resolve();
7
+ return apiClient.request(ProviderEvents.CANCEL_OAUTH, {
8
+ providerId,
9
+ });
10
+ };
@@ -0,0 +1,11 @@
1
+ import type { MutationConfig } from '../../../lib/react-query.js';
2
+ import { type ProviderStartOAuthResponse } from '../../../../shared/transport/events/index.js';
3
+ export type StartOAuthDTO = {
4
+ providerId: string;
5
+ };
6
+ export declare const startOAuth: ({ providerId }: StartOAuthDTO) => Promise<ProviderStartOAuthResponse>;
7
+ type UseStartOAuthOptions = {
8
+ mutationConfig?: MutationConfig<typeof startOAuth>;
9
+ };
10
+ export declare const useStartOAuth: ({ mutationConfig }?: UseStartOAuthOptions) => import("@tanstack/react-query").UseMutationResult<ProviderStartOAuthResponse, Error, StartOAuthDTO, unknown>;
11
+ export {};
@@ -0,0 +1,15 @@
1
+ import { useMutation } from '@tanstack/react-query';
2
+ import { ProviderEvents, } from '../../../../shared/transport/events/index.js';
3
+ import { useTransportStore } from '../../../stores/transport-store.js';
4
+ export const startOAuth = ({ providerId }) => {
5
+ const { apiClient } = useTransportStore.getState();
6
+ if (!apiClient)
7
+ return Promise.reject(new Error('Not connected'));
8
+ return apiClient.request(ProviderEvents.START_OAUTH, {
9
+ providerId,
10
+ });
11
+ };
12
+ export const useStartOAuth = ({ mutationConfig } = {}) => useMutation({
13
+ ...mutationConfig,
14
+ mutationFn: startOAuth,
15
+ });
@@ -0,0 +1,9 @@
1
+ import React from 'react';
2
+ import type { ProviderDTO } from '../../../../shared/transport/types/dto.js';
3
+ export interface AuthMethodDialogProps {
4
+ isActive?: boolean;
5
+ onCancel: () => void;
6
+ onSelect: (method: 'api-key' | 'oauth') => void;
7
+ provider: ProviderDTO;
8
+ }
9
+ export declare const AuthMethodDialog: React.FC<AuthMethodDialogProps>;
@@ -0,0 +1,20 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ import { SelectableList } from '../../../components/selectable-list.js';
4
+ import { useTheme } from '../../../hooks/index.js';
5
+ export const AuthMethodDialog = ({ isActive = true, onCancel, onSelect, provider, }) => {
6
+ const { theme: { colors } } = useTheme();
7
+ const items = [
8
+ {
9
+ description: 'Authenticate in your browser',
10
+ id: 'oauth',
11
+ name: provider.oauthLabel ?? 'Sign in with OAuth',
12
+ },
13
+ {
14
+ description: 'Enter your API key manually',
15
+ id: 'api-key',
16
+ name: 'API Key',
17
+ },
18
+ ];
19
+ return (_jsx(SelectableList, { filterKeys: (item) => [item.id, item.name], isActive: isActive, items: items, keyExtractor: (item) => item.id, onCancel: onCancel, onSelect: (item) => onSelect(item.id), renderItem: (item, isItemActive) => (_jsxs(Box, { gap: 2, children: [_jsx(Text, { backgroundColor: isItemActive ? colors.dimText : undefined, color: colors.text, children: item.name.padEnd(25) }), _jsx(Text, { color: colors.dimText, children: item.description })] })), title: `${provider.name} — Choose authentication method` }));
20
+ };
@@ -0,0 +1,9 @@
1
+ import React from 'react';
2
+ import type { ProviderDTO } from '../../../../shared/transport/types/dto.js';
3
+ export interface OAuthDialogProps {
4
+ isActive?: boolean;
5
+ onCancel: () => void;
6
+ onSuccess: () => void;
7
+ provider: ProviderDTO;
8
+ }
9
+ export declare const OAuthDialog: React.FC<OAuthDialogProps>;
@@ -0,0 +1,96 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text, useInput } from 'ink';
3
+ import { useCallback, useEffect, useRef, useState } from 'react';
4
+ import { SelectableList } from '../../../components/selectable-list.js';
5
+ import { useTheme } from '../../../hooks/index.js';
6
+ import { formatTransportError } from '../../../utils/index.js';
7
+ import { useAwaitOAuthCallback } from '../api/await-oauth-callback.js';
8
+ import { cancelOAuth } from '../api/cancel-oauth.js';
9
+ import { useStartOAuth } from '../api/start-oauth.js';
10
+ export const OAuthDialog = ({ isActive = true, onCancel, onSuccess, provider, }) => {
11
+ const { theme: { colors } } = useTheme();
12
+ const [step, setStep] = useState('starting');
13
+ const [authUrl, setAuthUrl] = useState(null);
14
+ const [error, setError] = useState(null);
15
+ const mounted = useRef(true);
16
+ const flowStarted = useRef(false);
17
+ const providerIdRef = useRef(provider.id);
18
+ providerIdRef.current = provider.id;
19
+ const startOAuthMutation = useStartOAuth();
20
+ const awaitCallbackMutation = useAwaitOAuthCallback();
21
+ const runOAuthFlow = useCallback(async () => {
22
+ if (!mounted.current)
23
+ return;
24
+ setStep('starting');
25
+ setError(null);
26
+ try {
27
+ const startResult = await startOAuthMutation.mutateAsync({ providerId: provider.id });
28
+ if (!mounted.current)
29
+ return;
30
+ if (!startResult.success) {
31
+ setError(startResult.error ?? 'Failed to start OAuth flow');
32
+ setStep('error');
33
+ return;
34
+ }
35
+ flowStarted.current = true;
36
+ setAuthUrl(startResult.authUrl);
37
+ setStep('waiting');
38
+ const callbackResult = await awaitCallbackMutation.mutateAsync({ providerId: provider.id });
39
+ if (!mounted.current)
40
+ return;
41
+ if (callbackResult.success) {
42
+ onSuccess();
43
+ }
44
+ else {
45
+ setError(callbackResult.error ?? 'OAuth authentication failed');
46
+ setStep('error');
47
+ }
48
+ }
49
+ catch (error_) {
50
+ if (!mounted.current)
51
+ return;
52
+ setError(formatTransportError(error_));
53
+ setStep('error');
54
+ }
55
+ }, [awaitCallbackMutation, onSuccess, provider.id, startOAuthMutation]);
56
+ useEffect(() => {
57
+ runOAuthFlow();
58
+ return () => {
59
+ mounted.current = false;
60
+ if (flowStarted.current) {
61
+ cancelOAuth({ providerId: providerIdRef.current }).catch(() => { });
62
+ }
63
+ };
64
+ }, []);
65
+ useInput((_input, key) => {
66
+ if (key.escape && isActive && step === 'waiting') {
67
+ onCancel();
68
+ }
69
+ });
70
+ const handleErrorAction = useCallback((action) => {
71
+ if (action.id === 'retry') {
72
+ runOAuthFlow();
73
+ }
74
+ else {
75
+ onCancel();
76
+ }
77
+ }, [onCancel, runOAuthFlow]);
78
+ const errorActions = [
79
+ { id: 'retry', name: 'Retry' },
80
+ { id: 'cancel', name: 'Cancel' },
81
+ ];
82
+ switch (step) {
83
+ case 'error': {
84
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: colors.warning, children: error }) }), _jsx(SelectableList, { filterKeys: (item) => [item.id, item.name], isActive: isActive, items: errorActions, keyExtractor: (item) => item.id, onCancel: onCancel, onSelect: handleErrorAction, renderItem: (item, isItemActive) => (_jsx(Text, { backgroundColor: isItemActive ? colors.dimText : undefined, color: colors.text, children: item.name })), title: "OAuth failed" })] }));
85
+ }
86
+ case 'starting': {
87
+ return (_jsx(Box, { children: _jsxs(Text, { color: colors.primary, children: ["Starting OAuth flow for ", provider.name, "..."] }) }));
88
+ }
89
+ case 'waiting': {
90
+ return (_jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsx(Text, { color: colors.primary, children: "Opening browser for authentication..." }), authUrl && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: colors.dimText, children: "If the browser did not open, visit this URL:" }), _jsx(Text, { color: colors.info, children: authUrl })] })), _jsx(Text, { color: colors.dimText, children: "Waiting for authorization... (press Esc to cancel)" })] }));
91
+ }
92
+ default: {
93
+ return null;
94
+ }
95
+ }
96
+ };
@@ -16,5 +16,5 @@ export const ProviderDialog = ({ hideCancelButton = false, isActive = true, onCa
16
16
  const { theme: { colors } } = useTheme();
17
17
  // Find current provider for the list
18
18
  const currentProvider = useMemo(() => providers.find((p) => p.isCurrent), [providers]);
19
- return (_jsx(SelectableList, { currentItem: currentProvider, filterKeys: (item) => [item.id, item.name, item.description], getCurrentKey: (item) => item.id, hideCancelButton: hideCancelButton, isActive: isActive, items: providers, keyExtractor: (item) => item.id, onCancel: onCancel, onSelect: (item) => onSelect(item), renderItem: (item, isActive, isCurrent) => (_jsxs(Box, { gap: 2, children: [_jsx(Text, { backgroundColor: isActive ? colors.dimText : undefined, color: isActive ? colors.text : colors.text, children: item.name.padEnd(15) }), _jsx(Text, { color: colors.dimText, children: item.description }), item.isConnected && !isCurrent && (_jsx(Text, { color: colors.primary, children: "[Connected]" })), isCurrent && (_jsx(Text, { color: colors.primary, children: "(Current)" }))] })), searchPlaceholder: "Search providers...", title: title }));
19
+ return (_jsx(SelectableList, { currentItem: currentProvider, filterKeys: (item) => [item.id, item.name, item.description], getCurrentKey: (item) => item.id, hideCancelButton: hideCancelButton, isActive: isActive, items: providers, keyExtractor: (item) => item.id, onCancel: onCancel, onSelect: (item) => onSelect(item), renderItem: (item, isActive, isCurrent) => (_jsxs(Box, { gap: 2, children: [_jsx(Text, { backgroundColor: isActive ? colors.dimText : undefined, color: isActive ? colors.text : colors.text, children: item.name.padEnd(15) }), _jsx(Text, { color: colors.dimText, children: item.description }), item.isConnected && !isCurrent && (_jsx(Text, { color: colors.primary, children: "[Connected]" })), isCurrent && (_jsx(Text, { color: colors.primary, children: "(Current)" })), item.isConnected && item.authMethod && (_jsx(Text, { color: colors.primary, children: item.authMethod === 'oauth' ? '[OAuth]' : '[API Key]' }))] })), searchPlaceholder: "Search providers...", title: title }));
20
20
  };
@@ -20,8 +20,10 @@ import { useGetProviders } from '../api/get-providers.js';
20
20
  import { useSetActiveProvider } from '../api/set-active-provider.js';
21
21
  import { useValidateApiKey } from '../api/validate-api-key.js';
22
22
  import { ApiKeyDialog } from './api-key-dialog.js';
23
+ import { AuthMethodDialog } from './auth-method-dialog.js';
23
24
  import { BaseUrlDialog } from './base-url-dialog.js';
24
25
  import { ModelSelectStep } from './model-select-step.js';
26
+ import { OAuthDialog } from './oauth-dialog.js';
25
27
  import { ProviderDialog } from './provider-dialog.js';
26
28
  export const ProviderFlow = ({ hideCancelButton = false, isActive = true, onCancel, onComplete, providerDialogTitle, }) => {
27
29
  const { theme: { colors } } = useTheme();
@@ -53,7 +55,18 @@ export const ProviderFlow = ({ hideCancelButton = false, isActive = true, onCanc
53
55
  name: 'Set as active',
54
56
  });
55
57
  }
56
- if (selectedProvider.requiresApiKey) {
58
+ if (selectedProvider.authMethod === 'oauth') {
59
+ actions.push({
60
+ description: 'Re-authenticate via browser',
61
+ id: 'reconnect_oauth',
62
+ name: 'Reconnect OAuth',
63
+ }, {
64
+ description: 'Remove OAuth connection',
65
+ id: 'disconnect',
66
+ name: 'Disconnect',
67
+ });
68
+ }
69
+ else if (selectedProvider.requiresApiKey) {
57
70
  actions.push({
58
71
  description: 'Enter a new API key',
59
72
  id: 'replace',
@@ -113,6 +126,11 @@ export const ProviderFlow = ({ hideCancelButton = false, isActive = true, onCanc
113
126
  setStep('base_url');
114
127
  return;
115
128
  }
129
+ // Supports OAuth → auth method selection
130
+ if (provider.supportsOAuth) {
131
+ setStep('auth_method');
132
+ return;
133
+ }
116
134
  // Requires API key → api_key step
117
135
  if (provider.requiresApiKey) {
118
136
  setStep('api_key');
@@ -166,6 +184,10 @@ export const ProviderFlow = ({ hideCancelButton = false, isActive = true, onCanc
166
184
  setStep('base_url');
167
185
  break;
168
186
  }
187
+ case 'reconnect_oauth': {
188
+ setStep('oauth');
189
+ break;
190
+ }
169
191
  case 'replace': {
170
192
  setStep('api_key');
171
193
  break;
@@ -200,9 +222,28 @@ export const ProviderFlow = ({ hideCancelButton = false, isActive = true, onCanc
200
222
  }
201
223
  }, [baseUrl, connectMutation, selectedProvider]);
202
224
  const handleApiKeyCancel = useCallback(() => {
203
- setStep('select');
204
- setSelectedProvider(null);
205
- setBaseUrl(null);
225
+ if (selectedProvider?.supportsOAuth) {
226
+ setStep('auth_method');
227
+ }
228
+ else {
229
+ setStep('select');
230
+ setSelectedProvider(null);
231
+ setBaseUrl(null);
232
+ }
233
+ }, [selectedProvider]);
234
+ const handleAuthMethodSelect = useCallback((method) => {
235
+ if (method === 'oauth') {
236
+ setStep('oauth');
237
+ }
238
+ else {
239
+ setStep('api_key');
240
+ }
241
+ }, []);
242
+ const handleOAuthCancel = useCallback(() => {
243
+ setStep('auth_method');
244
+ }, []);
245
+ const handleOAuthSuccess = useCallback(() => {
246
+ setStep('model_select');
206
247
  }, []);
207
248
  const handleValidateApiKey = useCallback(async (apiKey) => {
208
249
  if (!selectedProvider)
@@ -232,6 +273,12 @@ export const ProviderFlow = ({ hideCancelButton = false, isActive = true, onCanc
232
273
  case 'api_key': {
233
274
  return selectedProvider ? (_jsx(ApiKeyDialog, { isActive: isActive, isOptional: selectedProvider.id === 'openai-compatible', onCancel: handleApiKeyCancel, onSuccess: handleApiKeySuccess, provider: selectedProvider, validateApiKey: handleValidateApiKey })) : null;
234
275
  }
276
+ case 'auth_method': {
277
+ return selectedProvider ? (_jsx(AuthMethodDialog, { isActive: isActive, onCancel: () => {
278
+ setStep('select');
279
+ setSelectedProvider(null);
280
+ }, onSelect: handleAuthMethodSelect, provider: selectedProvider })) : null;
281
+ }
235
282
  case 'base_url': {
236
283
  return (_jsx(BaseUrlDialog, { description: "Enter the base URL of your OpenAI-compatible endpoint (Ollama, LM Studio, etc.)", isActive: isActive, onCancel: handleApiKeyCancel, onSubmit: handleBaseUrlSubmit, title: "Connect to OpenAI Compatible" }));
237
284
  }
@@ -241,6 +288,9 @@ export const ProviderFlow = ({ hideCancelButton = false, isActive = true, onCanc
241
288
  case 'model_select': {
242
289
  return selectedProvider ? (_jsx(ModelSelectStep, { isActive: isActive, onCancel: () => setStep('select'), onComplete: (modelName) => onComplete(`Connected to ${selectedProvider.name}, model set to ${modelName}`), providerId: selectedProvider.id, providerName: selectedProvider.name })) : null;
243
290
  }
291
+ case 'oauth': {
292
+ return selectedProvider ? (_jsx(OAuthDialog, { isActive: isActive, onCancel: handleOAuthCancel, onSuccess: handleOAuthSuccess, provider: selectedProvider })) : null;
293
+ }
244
294
  case 'provider_actions': {
245
295
  return selectedProvider ? (_jsxs(Box, { flexDirection: "column", children: [error && (_jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: colors.warning, children: error }) })), _jsx(SelectableList, { filterKeys: (item) => [item.id, item.name], isActive: isActive, items: providerActions, keyExtractor: (item) => item.id, onCancel: () => {
246
296
  setStep('select');
@@ -0,0 +1 @@
1
+ export declare function ProviderSubscriptionInitializer(): null;
@@ -0,0 +1,5 @@
1
+ import { useProviderSubscriptions } from '../hooks/use-provider-subscriptions.js';
2
+ export function ProviderSubscriptionInitializer() {
3
+ useProviderSubscriptions();
4
+ return null;
5
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Hook that subscribes to provider update broadcasts and invalidates React Query caches.
3
+ * Call this once from a top-level component to keep provider data fresh
4
+ * when changes happen via oclif commands or other clients.
5
+ */
6
+ export declare function useProviderSubscriptions(): void;
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Hook that subscribes to provider update broadcasts and invalidates React Query caches.
3
+ * Call this once from a top-level component to keep provider data fresh
4
+ * when changes happen via oclif commands or other clients.
5
+ */
6
+ import { useQueryClient } from '@tanstack/react-query';
7
+ import { useEffect } from 'react';
8
+ import { ProviderEvents } from '../../../../shared/transport/events/index.js';
9
+ import { useTransportStore } from '../../../stores/transport-store.js';
10
+ import { getActiveProviderConfigQueryOptions } from '../api/get-active-provider-config.js';
11
+ import { getProvidersQueryOptions } from '../api/get-providers.js';
12
+ export function useProviderSubscriptions() {
13
+ const client = useTransportStore((s) => s.client);
14
+ const queryClient = useQueryClient();
15
+ useEffect(() => {
16
+ if (!client)
17
+ return;
18
+ const unsub = client.on(ProviderEvents.UPDATED, () => {
19
+ queryClient.invalidateQueries({ queryKey: getProvidersQueryOptions().queryKey });
20
+ queryClient.invalidateQueries({ queryKey: getActiveProviderConfigQueryOptions().queryKey });
21
+ });
22
+ return unsub;
23
+ }, [client, queryClient]);
24
+ }
@@ -7,6 +7,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
7
7
  */
8
8
  import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
9
9
  import { AuthInitializer } from '../features/auth/components/auth-initializer.js';
10
+ import { ProviderSubscriptionInitializer } from '../features/provider/components/provider-subscription-initializer.js';
10
11
  import { TaskSubscriptionInitializer } from '../features/tasks/components/task-subscription-initializer.js';
11
12
  import { TransportInitializer } from '../features/transport/components/transport-initializer.js';
12
13
  const queryClient = new QueryClient({
@@ -23,5 +24,5 @@ const queryClient = new QueryClient({
23
24
  },
24
25
  });
25
26
  export function AppProviders({ children }) {
26
- return (_jsx(QueryClientProvider, { client: queryClient, children: _jsx(TransportInitializer, { children: _jsxs(AuthInitializer, { children: [_jsx(TaskSubscriptionInitializer, {}), children] }) }) }));
27
+ return (_jsx(QueryClientProvider, { client: queryClient, children: _jsx(TransportInitializer, { children: _jsxs(AuthInitializer, { children: [_jsx(TaskSubscriptionInitializer, {}), _jsx(ProviderSubscriptionInitializer, {}), children] }) }) }));
27
28
  }
@@ -11,6 +11,8 @@ const USER_FRIENDLY_MESSAGES = {
11
11
  ERR_CONTEXT_TREE_NOT_INIT: 'Context tree not initialized.',
12
12
  ERR_LOCAL_CHANGES_EXIST: 'You have local changes. Run /push to save your changes before pulling.',
13
13
  ERR_NOT_AUTHENTICATED: 'Not authenticated. This is required for cloud sync. Run /login to connect your account.',
14
+ ERR_OAUTH_REFRESH_FAILED: 'OAuth token refresh failed. Run /providers to reconnect your provider.',
15
+ ERR_OAUTH_TOKEN_EXPIRED: 'OAuth token has expired. Run /providers to reconnect your provider.',
14
16
  ERR_PROJECT_NOT_INIT: "Project not initialized. Run 'brv restart' to reinitialize.",
15
17
  ERR_PROVIDER_NOT_CONFIGURED: 'No provider connected. Run /providers connect byterover to use the free built-in provider, or connect another provider.',
16
18
  ERR_SPACE_NOT_CONFIGURED: 'No space configured. Run /space switch to select a space first.',
@@ -41,5 +43,8 @@ export function formatTransportError(error) {
41
43
  if (code && code in USER_FRIENDLY_MESSAGES) {
42
44
  return USER_FRIENDLY_MESSAGES[code];
43
45
  }
44
- return error.message.replace(/ for event '[^']+'$/, '');
46
+ if (error.name === 'TransportRequestTimeoutError') {
47
+ return 'Request timed out. Please try again.';
48
+ }
49
+ return error.message.replace(/ for event '[^']+'(?: after \d+ms)?$/, '');
45
50
  }