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.
- package/dist/agent/infra/llm/providers/openai.d.ts +12 -0
- package/dist/agent/infra/llm/providers/openai.js +52 -1
- package/dist/oclif/commands/curate/index.js +2 -2
- package/dist/oclif/commands/model/switch.js +14 -3
- package/dist/oclif/commands/providers/connect.d.ts +9 -0
- package/dist/oclif/commands/providers/connect.js +110 -14
- package/dist/oclif/commands/providers/list.js +3 -5
- package/dist/oclif/commands/query.js +2 -2
- package/dist/oclif/lib/daemon-client.d.ts +4 -0
- package/dist/oclif/lib/daemon-client.js +13 -3
- package/dist/server/core/domain/entities/provider-config.d.ts +6 -0
- package/dist/server/core/domain/entities/provider-config.js +4 -3
- package/dist/server/core/domain/entities/provider-registry.d.ts +41 -1
- package/dist/server/core/domain/entities/provider-registry.js +24 -1
- package/dist/server/core/domain/errors/task-error.d.ts +2 -0
- package/dist/server/core/domain/errors/task-error.js +6 -1
- package/dist/server/core/domain/transport/schemas.d.ts +2 -0
- package/dist/server/core/interfaces/i-provider-config-store.d.ts +2 -0
- package/dist/server/core/interfaces/i-provider-model-fetcher.d.ts +12 -3
- package/dist/server/core/interfaces/i-provider-oauth-token-store.d.ts +24 -0
- package/dist/server/core/interfaces/i-provider-oauth-token-store.js +1 -0
- package/dist/server/core/interfaces/i-token-refresh-manager.d.ts +7 -0
- package/dist/server/core/interfaces/i-token-refresh-manager.js +1 -0
- package/dist/server/infra/daemon/agent-process.js +22 -4
- package/dist/server/infra/daemon/brv-server.js +13 -2
- package/dist/server/infra/http/models-dev-client.d.ts +29 -0
- package/dist/server/infra/http/models-dev-client.js +133 -0
- package/dist/server/infra/http/provider-model-fetcher-registry.d.ts +2 -1
- package/dist/server/infra/http/provider-model-fetcher-registry.js +6 -1
- package/dist/server/infra/http/provider-model-fetchers.d.ts +33 -8
- package/dist/server/infra/http/provider-model-fetchers.js +88 -10
- package/dist/server/infra/process/feature-handlers.d.ts +3 -1
- package/dist/server/infra/process/feature-handlers.js +3 -1
- package/dist/server/infra/provider/provider-config-resolver.d.ts +4 -2
- package/dist/server/infra/provider/provider-config-resolver.js +59 -4
- package/dist/server/infra/provider-oauth/callback-server.d.ts +24 -0
- package/dist/server/infra/provider-oauth/callback-server.js +203 -0
- package/dist/server/infra/provider-oauth/errors.d.ts +39 -0
- package/dist/server/infra/provider-oauth/errors.js +76 -0
- package/dist/server/infra/provider-oauth/index.d.ts +9 -0
- package/dist/server/infra/provider-oauth/index.js +9 -0
- package/dist/server/infra/provider-oauth/jwt-utils.d.ts +17 -0
- package/dist/server/infra/provider-oauth/jwt-utils.js +51 -0
- package/dist/server/infra/provider-oauth/pkce-service.d.ts +22 -0
- package/dist/server/infra/provider-oauth/pkce-service.js +33 -0
- package/dist/server/infra/provider-oauth/provider-oauth-token-store.d.ts +48 -0
- package/dist/server/infra/provider-oauth/provider-oauth-token-store.js +155 -0
- package/dist/server/infra/provider-oauth/refresh-token-exchange.d.ts +8 -0
- package/dist/server/infra/provider-oauth/refresh-token-exchange.js +39 -0
- package/dist/server/infra/provider-oauth/token-exchange.d.ts +8 -0
- package/dist/server/infra/provider-oauth/token-exchange.js +44 -0
- package/dist/server/infra/provider-oauth/token-refresh-manager.d.ts +32 -0
- package/dist/server/infra/provider-oauth/token-refresh-manager.js +96 -0
- package/dist/server/infra/provider-oauth/types.d.ts +55 -0
- package/dist/server/infra/provider-oauth/types.js +22 -0
- package/dist/server/infra/storage/file-provider-config-store.d.ts +2 -0
- package/dist/server/infra/storage/file-provider-config-store.js +1 -3
- package/dist/server/infra/transport/handlers/model-handler.d.ts +3 -0
- package/dist/server/infra/transport/handlers/model-handler.js +53 -11
- package/dist/server/infra/transport/handlers/provider-handler.d.ts +26 -0
- package/dist/server/infra/transport/handlers/provider-handler.js +215 -13
- package/dist/shared/constants/oauth.d.ts +14 -0
- package/dist/shared/constants/oauth.js +14 -0
- package/dist/shared/transport/events/index.d.ts +5 -0
- package/dist/shared/transport/events/model-events.d.ts +2 -0
- package/dist/shared/transport/events/provider-events.d.ts +36 -0
- package/dist/shared/transport/events/provider-events.js +5 -0
- package/dist/shared/transport/types/dto.d.ts +4 -0
- package/dist/tui/features/model/api/set-active-model.d.ts +1 -1
- package/dist/tui/features/model/api/set-active-model.js +12 -4
- package/dist/tui/features/provider/api/await-oauth-callback.d.ts +11 -0
- package/dist/tui/features/provider/api/await-oauth-callback.js +25 -0
- package/dist/tui/features/provider/api/cancel-oauth.d.ts +5 -0
- package/dist/tui/features/provider/api/cancel-oauth.js +10 -0
- package/dist/tui/features/provider/api/start-oauth.d.ts +11 -0
- package/dist/tui/features/provider/api/start-oauth.js +15 -0
- package/dist/tui/features/provider/components/auth-method-dialog.d.ts +9 -0
- package/dist/tui/features/provider/components/auth-method-dialog.js +20 -0
- package/dist/tui/features/provider/components/oauth-dialog.d.ts +9 -0
- package/dist/tui/features/provider/components/oauth-dialog.js +96 -0
- package/dist/tui/features/provider/components/provider-dialog.js +1 -1
- package/dist/tui/features/provider/components/provider-flow.js +54 -4
- package/dist/tui/features/provider/components/provider-subscription-initializer.d.ts +1 -0
- package/dist/tui/features/provider/components/provider-subscription-initializer.js +5 -0
- package/dist/tui/features/provider/hooks/use-provider-subscriptions.d.ts +6 -0
- package/dist/tui/features/provider/hooks/use-provider-subscriptions.js +24 -0
- package/dist/tui/providers/app-providers.js +2 -1
- package/dist/tui/utils/error-messages.js +6 -1
- package/oclif.manifest.json +132 -116
- 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.
|
|
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
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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,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
|
-
|
|
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
|
}
|