byterover-cli 2.1.5 → 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 (110) 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/locations.d.ts +14 -0
  5. package/dist/oclif/commands/locations.js +68 -0
  6. package/dist/oclif/commands/model/switch.js +14 -3
  7. package/dist/oclif/commands/providers/connect.d.ts +9 -0
  8. package/dist/oclif/commands/providers/connect.js +110 -14
  9. package/dist/oclif/commands/providers/list.js +3 -5
  10. package/dist/oclif/commands/query.js +2 -2
  11. package/dist/oclif/commands/status.js +3 -3
  12. package/dist/oclif/lib/daemon-client.d.ts +4 -0
  13. package/dist/oclif/lib/daemon-client.js +13 -3
  14. package/dist/server/core/domain/entities/provider-config.d.ts +6 -0
  15. package/dist/server/core/domain/entities/provider-config.js +4 -3
  16. package/dist/server/core/domain/entities/provider-registry.d.ts +41 -1
  17. package/dist/server/core/domain/entities/provider-registry.js +24 -1
  18. package/dist/server/core/domain/errors/task-error.d.ts +2 -0
  19. package/dist/server/core/domain/errors/task-error.js +6 -1
  20. package/dist/server/core/domain/transport/schemas.d.ts +2 -0
  21. package/dist/server/core/interfaces/i-provider-config-store.d.ts +2 -0
  22. package/dist/server/core/interfaces/i-provider-model-fetcher.d.ts +12 -3
  23. package/dist/server/core/interfaces/i-provider-oauth-token-store.d.ts +24 -0
  24. package/dist/server/core/interfaces/i-provider-oauth-token-store.js +1 -0
  25. package/dist/server/core/interfaces/i-token-refresh-manager.d.ts +7 -0
  26. package/dist/server/core/interfaces/i-token-refresh-manager.js +1 -0
  27. package/dist/server/infra/daemon/agent-process.js +22 -4
  28. package/dist/server/infra/daemon/brv-server.js +15 -2
  29. package/dist/server/infra/http/models-dev-client.d.ts +29 -0
  30. package/dist/server/infra/http/models-dev-client.js +133 -0
  31. package/dist/server/infra/http/provider-model-fetcher-registry.d.ts +2 -1
  32. package/dist/server/infra/http/provider-model-fetcher-registry.js +6 -1
  33. package/dist/server/infra/http/provider-model-fetchers.d.ts +33 -8
  34. package/dist/server/infra/http/provider-model-fetchers.js +88 -10
  35. package/dist/server/infra/process/feature-handlers.d.ts +6 -1
  36. package/dist/server/infra/process/feature-handlers.js +11 -2
  37. package/dist/server/infra/provider/provider-config-resolver.d.ts +4 -2
  38. package/dist/server/infra/provider/provider-config-resolver.js +59 -4
  39. package/dist/server/infra/provider-oauth/callback-server.d.ts +24 -0
  40. package/dist/server/infra/provider-oauth/callback-server.js +203 -0
  41. package/dist/server/infra/provider-oauth/errors.d.ts +39 -0
  42. package/dist/server/infra/provider-oauth/errors.js +76 -0
  43. package/dist/server/infra/provider-oauth/index.d.ts +9 -0
  44. package/dist/server/infra/provider-oauth/index.js +9 -0
  45. package/dist/server/infra/provider-oauth/jwt-utils.d.ts +17 -0
  46. package/dist/server/infra/provider-oauth/jwt-utils.js +51 -0
  47. package/dist/server/infra/provider-oauth/pkce-service.d.ts +22 -0
  48. package/dist/server/infra/provider-oauth/pkce-service.js +33 -0
  49. package/dist/server/infra/provider-oauth/provider-oauth-token-store.d.ts +48 -0
  50. package/dist/server/infra/provider-oauth/provider-oauth-token-store.js +155 -0
  51. package/dist/server/infra/provider-oauth/refresh-token-exchange.d.ts +8 -0
  52. package/dist/server/infra/provider-oauth/refresh-token-exchange.js +39 -0
  53. package/dist/server/infra/provider-oauth/token-exchange.d.ts +8 -0
  54. package/dist/server/infra/provider-oauth/token-exchange.js +44 -0
  55. package/dist/server/infra/provider-oauth/token-refresh-manager.d.ts +32 -0
  56. package/dist/server/infra/provider-oauth/token-refresh-manager.js +96 -0
  57. package/dist/server/infra/provider-oauth/types.d.ts +55 -0
  58. package/dist/server/infra/provider-oauth/types.js +22 -0
  59. package/dist/server/infra/storage/file-provider-config-store.d.ts +2 -0
  60. package/dist/server/infra/storage/file-provider-config-store.js +1 -3
  61. package/dist/server/infra/transport/handlers/index.d.ts +2 -0
  62. package/dist/server/infra/transport/handlers/index.js +1 -0
  63. package/dist/server/infra/transport/handlers/locations-handler.d.ts +25 -0
  64. package/dist/server/infra/transport/handlers/locations-handler.js +64 -0
  65. package/dist/server/infra/transport/handlers/model-handler.d.ts +3 -0
  66. package/dist/server/infra/transport/handlers/model-handler.js +53 -11
  67. package/dist/server/infra/transport/handlers/provider-handler.d.ts +26 -0
  68. package/dist/server/infra/transport/handlers/provider-handler.js +215 -13
  69. package/dist/server/templates/skill/SKILL.md +19 -1
  70. package/dist/shared/constants/oauth.d.ts +14 -0
  71. package/dist/shared/constants/oauth.js +14 -0
  72. package/dist/shared/transport/events/index.d.ts +8 -0
  73. package/dist/shared/transport/events/index.js +3 -0
  74. package/dist/shared/transport/events/locations-events.d.ts +7 -0
  75. package/dist/shared/transport/events/locations-events.js +3 -0
  76. package/dist/shared/transport/events/model-events.d.ts +2 -0
  77. package/dist/shared/transport/events/provider-events.d.ts +36 -0
  78. package/dist/shared/transport/events/provider-events.js +5 -0
  79. package/dist/shared/transport/types/dto.d.ts +15 -0
  80. package/dist/tui/features/commands/definitions/index.js +2 -0
  81. package/dist/tui/features/commands/definitions/locations.d.ts +2 -0
  82. package/dist/tui/features/commands/definitions/locations.js +11 -0
  83. package/dist/tui/features/locations/api/get-locations.d.ts +16 -0
  84. package/dist/tui/features/locations/api/get-locations.js +17 -0
  85. package/dist/tui/features/locations/components/locations-view.d.ts +3 -0
  86. package/dist/tui/features/locations/components/locations-view.js +25 -0
  87. package/dist/tui/features/locations/utils/format-locations.d.ts +2 -0
  88. package/dist/tui/features/locations/utils/format-locations.js +26 -0
  89. package/dist/tui/features/model/api/set-active-model.d.ts +1 -1
  90. package/dist/tui/features/model/api/set-active-model.js +12 -4
  91. package/dist/tui/features/provider/api/await-oauth-callback.d.ts +11 -0
  92. package/dist/tui/features/provider/api/await-oauth-callback.js +25 -0
  93. package/dist/tui/features/provider/api/cancel-oauth.d.ts +5 -0
  94. package/dist/tui/features/provider/api/cancel-oauth.js +10 -0
  95. package/dist/tui/features/provider/api/start-oauth.d.ts +11 -0
  96. package/dist/tui/features/provider/api/start-oauth.js +15 -0
  97. package/dist/tui/features/provider/components/auth-method-dialog.d.ts +9 -0
  98. package/dist/tui/features/provider/components/auth-method-dialog.js +20 -0
  99. package/dist/tui/features/provider/components/oauth-dialog.d.ts +9 -0
  100. package/dist/tui/features/provider/components/oauth-dialog.js +96 -0
  101. package/dist/tui/features/provider/components/provider-dialog.js +1 -1
  102. package/dist/tui/features/provider/components/provider-flow.js +54 -4
  103. package/dist/tui/features/provider/components/provider-subscription-initializer.d.ts +1 -0
  104. package/dist/tui/features/provider/components/provider-subscription-initializer.js +5 -0
  105. package/dist/tui/features/provider/hooks/use-provider-subscriptions.d.ts +6 -0
  106. package/dist/tui/features/provider/hooks/use-provider-subscriptions.js +24 -0
  107. package/dist/tui/providers/app-providers.js +2 -1
  108. package/dist/tui/utils/error-messages.js +6 -1
  109. package/oclif.manifest.json +56 -1
  110. package/package.json +1 -1
@@ -5,7 +5,7 @@ export type SetActiveModelDTO = {
5
5
  modelId: string;
6
6
  providerId: string;
7
7
  };
8
- export declare const setActiveModel: ({ contextLength, modelId, providerId }: SetActiveModelDTO) => Promise<ModelSetActiveResponse>;
8
+ export declare const setActiveModel: ({ contextLength, modelId, providerId, }: SetActiveModelDTO) => Promise<ModelSetActiveResponse>;
9
9
  type UseSetActiveModelOptions = {
10
10
  mutationConfig?: MutationConfig<typeof setActiveModel>;
11
11
  };
@@ -1,11 +1,19 @@
1
1
  import { useMutation } from '@tanstack/react-query';
2
- import { ModelEvents } from '../../../../shared/transport/events/index.js';
2
+ import { ModelEvents, } from '../../../../shared/transport/events/index.js';
3
3
  import { useTransportStore } from '../../../stores/transport-store.js';
4
- export const setActiveModel = ({ contextLength, modelId, providerId }) => {
4
+ export const setActiveModel = async ({ contextLength, modelId, providerId, }) => {
5
5
  const { apiClient } = useTransportStore.getState();
6
6
  if (!apiClient)
7
- return Promise.reject(new Error('Not connected'));
8
- return apiClient.request(ModelEvents.SET_ACTIVE, { contextLength, modelId, providerId });
7
+ throw new Error('Not connected');
8
+ const response = await apiClient.request(ModelEvents.SET_ACTIVE, {
9
+ contextLength,
10
+ modelId,
11
+ providerId,
12
+ });
13
+ if (!response.success && response.error) {
14
+ throw new Error(response.error);
15
+ }
16
+ return response;
9
17
  };
10
18
  export const useSetActiveModel = ({ mutationConfig } = {}) => useMutation({
11
19
  ...mutationConfig,
@@ -0,0 +1,11 @@
1
+ import type { MutationConfig } from '../../../lib/react-query.js';
2
+ import { type ProviderAwaitOAuthCallbackResponse } from '../../../../shared/transport/events/index.js';
3
+ export type AwaitOAuthCallbackDTO = {
4
+ providerId: string;
5
+ };
6
+ export declare const awaitOAuthCallback: ({ providerId, }: AwaitOAuthCallbackDTO) => Promise<ProviderAwaitOAuthCallbackResponse>;
7
+ type UseAwaitOAuthCallbackOptions = {
8
+ mutationConfig?: MutationConfig<typeof awaitOAuthCallback>;
9
+ };
10
+ export declare const useAwaitOAuthCallback: ({ mutationConfig }?: UseAwaitOAuthCallbackOptions) => import("@tanstack/react-query").UseMutationResult<ProviderAwaitOAuthCallbackResponse, Error, AwaitOAuthCallbackDTO, unknown>;
11
+ export {};
@@ -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
  }
@@ -75,6 +75,45 @@
75
75
  "hook-prompt-submit.js"
76
76
  ]
77
77
  },
78
+ "locations": {
79
+ "aliases": [],
80
+ "args": {},
81
+ "description": "List all registered projects and their context tree status",
82
+ "examples": [
83
+ "<%= config.bin %> <%= command.id %>",
84
+ "<%= config.bin %> <%= command.id %> --format json"
85
+ ],
86
+ "flags": {
87
+ "format": {
88
+ "char": "f",
89
+ "description": "Output format",
90
+ "name": "format",
91
+ "default": "text",
92
+ "hasDynamicHelp": false,
93
+ "multiple": false,
94
+ "options": [
95
+ "text",
96
+ "json"
97
+ ],
98
+ "type": "option"
99
+ }
100
+ },
101
+ "hasDynamicHelp": false,
102
+ "hiddenAliases": [],
103
+ "id": "locations",
104
+ "pluginAlias": "byterover-cli",
105
+ "pluginName": "byterover-cli",
106
+ "pluginType": "core",
107
+ "strict": true,
108
+ "enableJsonFlag": false,
109
+ "isESM": true,
110
+ "relativePath": [
111
+ "dist",
112
+ "oclif",
113
+ "commands",
114
+ "locations.js"
115
+ ]
116
+ },
78
117
  "login": {
79
118
  "aliases": [],
80
119
  "args": {},
@@ -1050,6 +1089,7 @@
1050
1089
  "examples": [
1051
1090
  "<%= config.bin %> providers connect anthropic --api-key sk-xxx",
1052
1091
  "<%= config.bin %> providers connect openai --api-key sk-xxx --model gpt-4.1",
1092
+ "<%= config.bin %> providers connect openai --oauth",
1053
1093
  "<%= config.bin %> providers connect byterover",
1054
1094
  "<%= config.bin %> providers connect openai-compatible --base-url http://localhost:11434/v1",
1055
1095
  "<%= config.bin %> providers connect openai-compatible --base-url http://localhost:11434/v1 --api-key sk-xxx --model llama3"
@@ -1071,6 +1111,15 @@
1071
1111
  "multiple": false,
1072
1112
  "type": "option"
1073
1113
  },
1114
+ "code": {
1115
+ "char": "c",
1116
+ "description": "Authorization code for code-paste OAuth providers (e.g., Anthropic). Not applicable to browser-callback providers like OpenAI — use --oauth without --code instead.",
1117
+ "hidden": true,
1118
+ "name": "code",
1119
+ "hasDynamicHelp": false,
1120
+ "multiple": false,
1121
+ "type": "option"
1122
+ },
1074
1123
  "format": {
1075
1124
  "description": "Output format (text or json)",
1076
1125
  "name": "format",
@@ -1090,6 +1139,12 @@
1090
1139
  "hasDynamicHelp": false,
1091
1140
  "multiple": false,
1092
1141
  "type": "option"
1142
+ },
1143
+ "oauth": {
1144
+ "description": "Connect via OAuth (browser-based)",
1145
+ "name": "oauth",
1146
+ "allowNo": false,
1147
+ "type": "boolean"
1093
1148
  }
1094
1149
  },
1095
1150
  "hasDynamicHelp": false,
@@ -1574,5 +1629,5 @@
1574
1629
  ]
1575
1630
  }
1576
1631
  },
1577
- "version": "2.1.5"
1632
+ "version": "2.3.0"
1578
1633
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "byterover-cli",
3
3
  "description": "ByteRover's CLI",
4
- "version": "2.1.5",
4
+ "version": "2.3.0",
5
5
  "author": "ByteRover",
6
6
  "bin": {
7
7
  "brv": "./bin/run.js"