byterover-cli 2.4.1 → 2.5.1
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/oclif/commands/providers/connect.js +4 -1
- package/dist/oclif/commands/providers/index.d.ts +1 -0
- package/dist/oclif/commands/providers/index.js +9 -1
- package/dist/oclif/commands/providers/switch.js +4 -1
- package/dist/server/core/domain/entities/provider-config.js +1 -1
- package/dist/server/core/domain/entities/provider-registry.js +1 -1
- package/dist/server/core/domain/transport/schemas.d.ts +2 -0
- package/dist/server/infra/auth/oauth-service.js +2 -0
- package/dist/server/infra/auth/oidc-discovery-service.js +1 -0
- package/dist/server/infra/cogit/http-cogit-push-service.js +1 -0
- package/dist/server/infra/daemon/agent-process.js +4 -11
- package/dist/server/infra/daemon/brv-server.js +1 -1
- package/dist/server/infra/daemon/task-validation.d.ts +18 -0
- package/dist/server/infra/daemon/task-validation.js +42 -0
- package/dist/server/infra/http/authenticated-http-client.js +3 -0
- package/dist/server/infra/http/models-dev-client.js +1 -0
- package/dist/server/infra/http/openrouter-api-client.js +1 -0
- package/dist/server/infra/http/provider-model-fetchers.js +3 -0
- package/dist/server/infra/hub/hub-install-service.js +1 -0
- package/dist/server/infra/hub/hub-registry-service.js +1 -0
- package/dist/server/infra/memory/http-memory-storage-service.js +1 -0
- package/dist/server/infra/process/feature-handlers.js +1 -0
- package/dist/server/infra/provider/provider-config-resolver.d.ts +8 -1
- package/dist/server/infra/provider/provider-config-resolver.js +11 -12
- package/dist/server/infra/provider-oauth/refresh-token-exchange.js +1 -0
- package/dist/server/infra/provider-oauth/token-exchange.js +1 -0
- package/dist/server/infra/transport/handlers/provider-handler.d.ts +4 -0
- package/dist/server/infra/transport/handlers/provider-handler.js +14 -1
- package/dist/shared/transport/events/provider-events.d.ts +4 -0
- package/dist/tui/features/onboarding/hooks/use-app-view-mode.d.ts +23 -8
- package/dist/tui/features/onboarding/hooks/use-app-view-mode.js +29 -17
- package/dist/tui/features/provider/components/provider-flow.d.ts +1 -1
- package/dist/tui/features/provider/components/provider-flow.js +39 -4
- package/oclif.manifest.json +183 -183
- package/package.json +1 -1
|
@@ -89,9 +89,12 @@ export default class ProviderConnect extends Command {
|
|
|
89
89
|
}
|
|
90
90
|
// 4. Connect or switch active provider
|
|
91
91
|
const hasNewConfig = apiKey || baseUrl;
|
|
92
|
-
await (provider.isConnected && !hasNewConfig
|
|
92
|
+
const response = await (provider.isConnected && !hasNewConfig
|
|
93
93
|
? client.requestWithAck(ProviderEvents.SET_ACTIVE, { providerId })
|
|
94
94
|
: client.requestWithAck(ProviderEvents.CONNECT, { apiKey, baseUrl, providerId }));
|
|
95
|
+
if (!response.success) {
|
|
96
|
+
throw new Error(response.error ?? 'Failed to connect provider. Please try again.');
|
|
97
|
+
}
|
|
95
98
|
// 5. Set model if specified
|
|
96
99
|
if (model) {
|
|
97
100
|
await client.requestWithAck(ModelEvents.SET_ACTIVE, { modelId: model, providerId });
|
|
@@ -22,6 +22,7 @@ export default class Provider extends Command {
|
|
|
22
22
|
const provider = providers.find((p) => p.id === active.activeProviderId);
|
|
23
23
|
return {
|
|
24
24
|
activeModel: active.activeModel,
|
|
25
|
+
loginRequired: active.loginRequired,
|
|
25
26
|
providerId: active.activeProviderId,
|
|
26
27
|
providerName: provider?.name ?? active.activeProviderId,
|
|
27
28
|
};
|
|
@@ -33,7 +34,11 @@ export default class Provider extends Command {
|
|
|
33
34
|
try {
|
|
34
35
|
const info = await this.fetchActiveProvider();
|
|
35
36
|
if (format === 'json') {
|
|
36
|
-
|
|
37
|
+
const { loginRequired, ...rest } = info;
|
|
38
|
+
const data = loginRequired
|
|
39
|
+
? { ...rest, warning: "Not logged in. Run 'brv login' to authenticate." }
|
|
40
|
+
: rest;
|
|
41
|
+
writeJsonResponse({ command: 'providers', data, success: true });
|
|
37
42
|
}
|
|
38
43
|
else {
|
|
39
44
|
this.log(`Provider: ${info.providerName} (${info.providerId})`);
|
|
@@ -45,6 +50,9 @@ export default class Provider extends Command {
|
|
|
45
50
|
this.log('Model: Not set. Run "brv model list" to see available models, or "brv model switch <model>" to set one.');
|
|
46
51
|
}
|
|
47
52
|
}
|
|
53
|
+
if (info.loginRequired) {
|
|
54
|
+
this.log("Warning: Not logged in. Run 'brv login' to authenticate.");
|
|
55
|
+
}
|
|
48
56
|
}
|
|
49
57
|
}
|
|
50
58
|
catch (error) {
|
|
@@ -54,7 +54,10 @@ export default class ProviderSwitch extends Command {
|
|
|
54
54
|
if (!provider.isConnected) {
|
|
55
55
|
throw new Error(`Provider "${providerId}" is not connected. Use "brv providers connect ${providerId}" instead.`);
|
|
56
56
|
}
|
|
57
|
-
await client.requestWithAck(ProviderEvents.SET_ACTIVE, { providerId });
|
|
57
|
+
const response = await client.requestWithAck(ProviderEvents.SET_ACTIVE, { providerId });
|
|
58
|
+
if (!response.success) {
|
|
59
|
+
throw new Error(response.error ?? 'Failed to switch provider. Please try again.');
|
|
60
|
+
}
|
|
58
61
|
return { providerId, providerName: provider.name };
|
|
59
62
|
}, options);
|
|
60
63
|
}
|
|
@@ -184,7 +184,7 @@ export class ProviderConfig {
|
|
|
184
184
|
withProviderDisconnected(providerId) {
|
|
185
185
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
186
186
|
const { [providerId]: _removed, ...remainingProviders } = this.providers;
|
|
187
|
-
const newActiveProvider = this.activeProvider === providerId ? '
|
|
187
|
+
const newActiveProvider = this.activeProvider === providerId ? '' : this.activeProvider;
|
|
188
188
|
return new ProviderConfig({
|
|
189
189
|
activeProvider: newActiveProvider,
|
|
190
190
|
providers: remainingProviders,
|
|
@@ -26,7 +26,7 @@ export const PROVIDER_REGISTRY = {
|
|
|
26
26
|
byterover: {
|
|
27
27
|
baseUrl: '',
|
|
28
28
|
category: 'popular',
|
|
29
|
-
description: 'Built-in LLM,
|
|
29
|
+
description: 'Built-in LLM, logged-in ByteRover account required. Limited free usage.',
|
|
30
30
|
headers: {},
|
|
31
31
|
id: 'byterover',
|
|
32
32
|
modelsEndpoint: '',
|
|
@@ -465,6 +465,8 @@ export interface ProviderConfigResponse {
|
|
|
465
465
|
activeProvider: string;
|
|
466
466
|
/** How the provider was authenticated ('api-key' | 'oauth'). Undefined for internal providers. */
|
|
467
467
|
authMethod?: 'api-key' | 'oauth';
|
|
468
|
+
/** True when the active provider requires login but the user is not logged in. */
|
|
469
|
+
loginRequired?: boolean;
|
|
468
470
|
maxInputTokens?: number;
|
|
469
471
|
openRouterApiKey?: string;
|
|
470
472
|
provider?: string;
|
|
@@ -52,6 +52,7 @@ export class OAuthService {
|
|
|
52
52
|
},
|
|
53
53
|
httpAgent: ProxyConfig.getProxyAgent(),
|
|
54
54
|
httpsAgent: ProxyConfig.getProxyAgent(),
|
|
55
|
+
proxy: false,
|
|
55
56
|
});
|
|
56
57
|
return this.parseTokenResponse(response.data);
|
|
57
58
|
}
|
|
@@ -97,6 +98,7 @@ export class OAuthService {
|
|
|
97
98
|
}, {
|
|
98
99
|
httpAgent: ProxyConfig.getProxyAgent(),
|
|
99
100
|
httpsAgent: ProxyConfig.getProxyAgent(),
|
|
101
|
+
proxy: false,
|
|
100
102
|
});
|
|
101
103
|
return this.parseTokenResponse(response.data);
|
|
102
104
|
}
|
|
@@ -38,6 +38,7 @@ import { QueryExecutor } from '../executor/query-executor.js';
|
|
|
38
38
|
import { AgentInstanceDiscovery } from '../transport/agent-instance-discovery.js';
|
|
39
39
|
import { createAgentLogger } from './agent-logger.js';
|
|
40
40
|
import { resolveSessionId } from './session-resolver.js';
|
|
41
|
+
import { validateProviderForTask } from './task-validation.js';
|
|
41
42
|
// ============================================================================
|
|
42
43
|
// Environment
|
|
43
44
|
// ============================================================================
|
|
@@ -308,17 +309,9 @@ async function executeTask(task, curateExecutor, folderPackExecutor, queryExecut
|
|
|
308
309
|
if (!transport || !agent)
|
|
309
310
|
return;
|
|
310
311
|
const freshProviderConfig = await transport.requestWithAck(TransportStateEventNames.GET_PROVIDER_CONFIG);
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
transport.request(TransportTaskEventNames.ERROR, { clientId, error, taskId });
|
|
314
|
-
return;
|
|
315
|
-
}
|
|
316
|
-
if (freshProviderConfig.providerKeyMissing) {
|
|
317
|
-
const modelInfo = freshProviderConfig.activeModel ? ` (model: ${freshProviderConfig.activeModel})` : '';
|
|
318
|
-
const credentialType = freshProviderConfig.authMethod === 'oauth' ? 'authentication has expired' : 'API key is missing';
|
|
319
|
-
const errorMessage = `${freshProviderConfig.activeProvider} ${credentialType}${modelInfo}. Use /provider in the REPL to reconnect.`;
|
|
320
|
-
const error = serializeTaskError(new TaskError(errorMessage, TaskErrorCode.PROVIDER_NOT_CONFIGURED));
|
|
321
|
-
transport.request(TransportTaskEventNames.ERROR, { clientId, error, taskId });
|
|
312
|
+
const validationError = validateProviderForTask(freshProviderConfig);
|
|
313
|
+
if (validationError) {
|
|
314
|
+
transport.request(TransportTaskEventNames.ERROR, { clientId, error: validationError, taskId });
|
|
322
315
|
return;
|
|
323
316
|
}
|
|
324
317
|
activeTaskCount++;
|
|
@@ -359,7 +359,7 @@ async function main() {
|
|
|
359
359
|
// is returned to the onboarding flow rather than hitting a cryptic API key error mid-task.
|
|
360
360
|
await clearStaleProviderConfig(providerConfigStore, providerKeychainStore, providerOAuthTokenStore);
|
|
361
361
|
// State endpoint: provider config — agents request this on startup and after provider:updated
|
|
362
|
-
transportServer.onRequest(TransportStateEventNames.GET_PROVIDER_CONFIG, async () => resolveProviderConfig(providerConfigStore, providerKeychainStore, tokenRefreshManager));
|
|
362
|
+
transportServer.onRequest(TransportStateEventNames.GET_PROVIDER_CONFIG, async () => resolveProviderConfig({ authStateStore, providerConfigStore, providerKeychainStore, tokenRefreshManager }));
|
|
363
363
|
// Feature handlers (auth, init, status, push, pull, etc.) require async OIDC discovery.
|
|
364
364
|
// Placed after daemon:getState so the debug endpoint is available immediately,
|
|
365
365
|
// without waiting for OIDC discovery (~400ms).
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pre-task Provider Validation
|
|
3
|
+
*
|
|
4
|
+
* Pure validation logic extracted from agent-process.ts for testability.
|
|
5
|
+
* Checks that the provider config is ready for task execution.
|
|
6
|
+
*/
|
|
7
|
+
import type { TaskErrorData } from '../../core/domain/errors/task-error.js';
|
|
8
|
+
import type { ProviderConfigResponse } from '../../core/domain/transport/schemas.js';
|
|
9
|
+
/**
|
|
10
|
+
* Validate provider config before executing a task.
|
|
11
|
+
* Returns a TaskErrorData if validation fails, undefined if all checks pass.
|
|
12
|
+
*
|
|
13
|
+
* Check order matters — most fundamental issues first:
|
|
14
|
+
* 1. No provider connected
|
|
15
|
+
* 2. Provider credential missing (API key or expired OAuth token)
|
|
16
|
+
* 3. Provider requires authentication (ByteRover auth gate)
|
|
17
|
+
*/
|
|
18
|
+
export declare const validateProviderForTask: (config: ProviderConfigResponse) => TaskErrorData | undefined;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pre-task Provider Validation
|
|
3
|
+
*
|
|
4
|
+
* Pure validation logic extracted from agent-process.ts for testability.
|
|
5
|
+
* Checks that the provider config is ready for task execution.
|
|
6
|
+
*/
|
|
7
|
+
import { TaskErrorCode } from '../../core/domain/errors/task-error.js';
|
|
8
|
+
/**
|
|
9
|
+
* Validate provider config before executing a task.
|
|
10
|
+
* Returns a TaskErrorData if validation fails, undefined if all checks pass.
|
|
11
|
+
*
|
|
12
|
+
* Check order matters — most fundamental issues first:
|
|
13
|
+
* 1. No provider connected
|
|
14
|
+
* 2. Provider credential missing (API key or expired OAuth token)
|
|
15
|
+
* 3. Provider requires authentication (ByteRover auth gate)
|
|
16
|
+
*/
|
|
17
|
+
export const validateProviderForTask = (config) => {
|
|
18
|
+
if (!config.activeProvider) {
|
|
19
|
+
return {
|
|
20
|
+
code: TaskErrorCode.PROVIDER_NOT_CONFIGURED,
|
|
21
|
+
message: 'No provider connected. Use /provider in the REPL to configure a provider.',
|
|
22
|
+
name: 'TaskError',
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
if (config.providerKeyMissing) {
|
|
26
|
+
const modelInfo = config.activeModel ? ` (model: ${config.activeModel})` : '';
|
|
27
|
+
const credentialType = config.authMethod === 'oauth' ? 'authentication has expired' : 'API key is missing';
|
|
28
|
+
return {
|
|
29
|
+
code: TaskErrorCode.PROVIDER_NOT_CONFIGURED,
|
|
30
|
+
message: `${config.activeProvider} ${credentialType}${modelInfo}. Use /provider in the REPL to reconnect.`,
|
|
31
|
+
name: 'TaskError',
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
if (config.loginRequired) {
|
|
35
|
+
return {
|
|
36
|
+
code: TaskErrorCode.PROVIDER_NOT_CONFIGURED,
|
|
37
|
+
message: 'Provider requires authentication. Run /login or brv login to sign in.',
|
|
38
|
+
name: 'TaskError',
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
return undefined;
|
|
42
|
+
};
|
|
@@ -30,6 +30,7 @@ export class AuthenticatedHttpClient {
|
|
|
30
30
|
headers: this.buildHeaders(config?.headers),
|
|
31
31
|
httpAgent: ProxyConfig.getProxyAgent(),
|
|
32
32
|
httpsAgent: ProxyConfig.getProxyAgent(),
|
|
33
|
+
proxy: false,
|
|
33
34
|
timeout: config?.timeout,
|
|
34
35
|
};
|
|
35
36
|
const response = await axios.get(url, axiosConfig);
|
|
@@ -53,6 +54,7 @@ export class AuthenticatedHttpClient {
|
|
|
53
54
|
headers: this.buildHeaders(config?.headers),
|
|
54
55
|
httpAgent: ProxyConfig.getProxyAgent(),
|
|
55
56
|
httpsAgent: ProxyConfig.getProxyAgent(),
|
|
57
|
+
proxy: false,
|
|
56
58
|
timeout: config?.timeout,
|
|
57
59
|
};
|
|
58
60
|
const response = await axios.post(url, data, axiosConfig);
|
|
@@ -76,6 +78,7 @@ export class AuthenticatedHttpClient {
|
|
|
76
78
|
headers: this.buildHeaders(config?.headers),
|
|
77
79
|
httpAgent: ProxyConfig.getProxyAgent(),
|
|
78
80
|
httpsAgent: ProxyConfig.getProxyAgent(),
|
|
81
|
+
proxy: false,
|
|
79
82
|
timeout: config?.timeout,
|
|
80
83
|
};
|
|
81
84
|
const response = await axios.put(url, data, axiosConfig);
|
|
@@ -349,6 +349,7 @@ export class OpenAICompatibleModelFetcher {
|
|
|
349
349
|
headers: { Authorization: `Bearer ${apiKey}` },
|
|
350
350
|
httpAgent: ProxyConfig.getProxyAgent(),
|
|
351
351
|
httpsAgent: ProxyConfig.getProxyAgent(),
|
|
352
|
+
proxy: false,
|
|
352
353
|
timeout: 30_000,
|
|
353
354
|
});
|
|
354
355
|
// Handle different response formats:
|
|
@@ -381,6 +382,7 @@ export class OpenAICompatibleModelFetcher {
|
|
|
381
382
|
headers: { Authorization: `Bearer ${apiKey}` },
|
|
382
383
|
httpAgent: ProxyConfig.getProxyAgent(),
|
|
383
384
|
httpsAgent: ProxyConfig.getProxyAgent(),
|
|
385
|
+
proxy: false,
|
|
384
386
|
timeout: 15_000,
|
|
385
387
|
});
|
|
386
388
|
return { isValid: true };
|
|
@@ -438,6 +440,7 @@ export class ChatBasedModelFetcher {
|
|
|
438
440
|
},
|
|
439
441
|
httpAgent: ProxyConfig.getProxyAgent(),
|
|
440
442
|
httpsAgent: ProxyConfig.getProxyAgent(),
|
|
443
|
+
proxy: false,
|
|
441
444
|
timeout: 15_000,
|
|
442
445
|
});
|
|
443
446
|
return { isValid: true };
|
|
@@ -9,6 +9,7 @@ import type { IProviderConfigStore } from '../../core/interfaces/i-provider-conf
|
|
|
9
9
|
import type { IProviderKeychainStore } from '../../core/interfaces/i-provider-keychain-store.js';
|
|
10
10
|
import type { IProviderOAuthTokenStore } from '../../core/interfaces/i-provider-oauth-token-store.js';
|
|
11
11
|
import type { ITokenRefreshManager } from '../../core/interfaces/i-token-refresh-manager.js';
|
|
12
|
+
import type { IAuthStateStore } from '../../core/interfaces/state/i-auth-state-store.js';
|
|
12
13
|
import { type ProviderConfigResponse } from '../../core/domain/transport/schemas.js';
|
|
13
14
|
/**
|
|
14
15
|
* Validate provider config integrity at startup.
|
|
@@ -29,4 +30,10 @@ export declare function clearStaleProviderConfig(providerConfigStore: IProviderC
|
|
|
29
30
|
* the API key (keychain → env fallback), and maps provider-specific
|
|
30
31
|
* fields (base URL, headers, location, etc.).
|
|
31
32
|
*/
|
|
32
|
-
export
|
|
33
|
+
export type ResolveProviderConfigParams = {
|
|
34
|
+
authStateStore?: IAuthStateStore;
|
|
35
|
+
providerConfigStore: IProviderConfigStore;
|
|
36
|
+
providerKeychainStore: IProviderKeychainStore;
|
|
37
|
+
tokenRefreshManager?: ITokenRefreshManager;
|
|
38
|
+
};
|
|
39
|
+
export declare function resolveProviderConfig(params: ResolveProviderConfigParams): Promise<ProviderConfigResponse>;
|
|
@@ -49,9 +49,9 @@ export async function clearStaleProviderConfig(providerConfigStore, providerKeyc
|
|
|
49
49
|
for (const providerId of staleProviderIds) {
|
|
50
50
|
newConfig = newConfig.withProviderDisconnected(providerId);
|
|
51
51
|
}
|
|
52
|
-
// withProviderDisconnected()
|
|
53
|
-
// removed
|
|
54
|
-
//
|
|
52
|
+
// withProviderDisconnected() already sets activeProvider to '' when the active
|
|
53
|
+
// provider is removed. This call is a no-op but kept for readability — it makes
|
|
54
|
+
// the "clear active provider" intent explicit at the call site.
|
|
55
55
|
if (staleProviderIds.includes(config.activeProvider)) {
|
|
56
56
|
newConfig = newConfig.withActiveProvider('');
|
|
57
57
|
}
|
|
@@ -67,19 +67,18 @@ export async function clearStaleProviderConfig(providerConfigStore, providerKeyc
|
|
|
67
67
|
// The user will encounter a provider error when submitting a task instead.
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
*
|
|
73
|
-
* Reads the active provider/model from the config store, resolves
|
|
74
|
-
* the API key (keychain → env fallback), and maps provider-specific
|
|
75
|
-
* fields (base URL, headers, location, etc.).
|
|
76
|
-
*/
|
|
77
|
-
export async function resolveProviderConfig(providerConfigStore, providerKeychainStore, tokenRefreshManager) {
|
|
70
|
+
export async function resolveProviderConfig(params) {
|
|
71
|
+
const { authStateStore, providerConfigStore, providerKeychainStore, tokenRefreshManager } = params;
|
|
78
72
|
const config = await providerConfigStore.read();
|
|
79
73
|
const { activeProvider } = config;
|
|
80
74
|
const activeModel = config.getActiveModel(activeProvider);
|
|
81
75
|
const maxInputTokens = config.getActiveModelContextLength(activeProvider);
|
|
82
|
-
if (
|
|
76
|
+
if (activeProvider === 'byterover') {
|
|
77
|
+
const authToken = authStateStore?.getToken();
|
|
78
|
+
const loginRequired = authStateStore ? !authToken || !authToken.isValid() : undefined;
|
|
79
|
+
return { activeModel, activeProvider, loginRequired: loginRequired ? true : undefined, maxInputTokens };
|
|
80
|
+
}
|
|
81
|
+
if (!activeProvider) {
|
|
83
82
|
return { activeModel, activeProvider, maxInputTokens };
|
|
84
83
|
}
|
|
85
84
|
// Resolve API key: keychain first, then environment variable
|
|
@@ -2,10 +2,12 @@ import type { IProviderConfigStore } from '../../../core/interfaces/i-provider-c
|
|
|
2
2
|
import type { IProviderKeychainStore } from '../../../core/interfaces/i-provider-keychain-store.js';
|
|
3
3
|
import type { IProviderOAuthTokenStore } from '../../../core/interfaces/i-provider-oauth-token-store.js';
|
|
4
4
|
import type { IBrowserLauncher } from '../../../core/interfaces/services/i-browser-launcher.js';
|
|
5
|
+
import type { IAuthStateStore } from '../../../core/interfaces/state/i-auth-state-store.js';
|
|
5
6
|
import type { ITransportServer } from '../../../core/interfaces/transport/i-transport-server.js';
|
|
6
7
|
import type { PkceParameters, ProviderTokenResponse, TokenExchangeParams } from '../../provider-oauth/index.js';
|
|
7
8
|
import { ProviderCallbackServer } from '../../provider-oauth/index.js';
|
|
8
9
|
export interface ProviderHandlerDeps {
|
|
10
|
+
authStateStore: IAuthStateStore;
|
|
9
11
|
browserLauncher: IBrowserLauncher;
|
|
10
12
|
/** Factory for creating callback servers (injectable for testing) */
|
|
11
13
|
createCallbackServer?: (options: {
|
|
@@ -26,6 +28,7 @@ export interface ProviderHandlerDeps {
|
|
|
26
28
|
* Business logic for provider management — no terminal/UI calls.
|
|
27
29
|
*/
|
|
28
30
|
export declare class ProviderHandler {
|
|
31
|
+
private readonly authStateStore;
|
|
29
32
|
private readonly browserLauncher;
|
|
30
33
|
private readonly createCallbackServer;
|
|
31
34
|
private readonly exchangeCodeForTokens;
|
|
@@ -38,6 +41,7 @@ export declare class ProviderHandler {
|
|
|
38
41
|
constructor(deps: ProviderHandlerDeps);
|
|
39
42
|
setup(): void;
|
|
40
43
|
private cleanupFlowsForClient;
|
|
44
|
+
private isByteRoverAuthSatisfied;
|
|
41
45
|
private setupAwaitOAuthCallback;
|
|
42
46
|
private setupCancelOAuth;
|
|
43
47
|
private setupConnect;
|
|
@@ -10,6 +10,7 @@ import { computeExpiresAt, exchangeCodeForTokens as defaultExchangeCodeForTokens
|
|
|
10
10
|
* Business logic for provider management — no terminal/UI calls.
|
|
11
11
|
*/
|
|
12
12
|
export class ProviderHandler {
|
|
13
|
+
authStateStore;
|
|
13
14
|
browserLauncher;
|
|
14
15
|
createCallbackServer;
|
|
15
16
|
exchangeCodeForTokens;
|
|
@@ -20,6 +21,7 @@ export class ProviderHandler {
|
|
|
20
21
|
providerOAuthTokenStore;
|
|
21
22
|
transport;
|
|
22
23
|
constructor(deps) {
|
|
24
|
+
this.authStateStore = deps.authStateStore;
|
|
23
25
|
this.browserLauncher = deps.browserLauncher;
|
|
24
26
|
this.createCallbackServer = deps.createCallbackServer ?? ((options) => new ProviderCallbackServer(options));
|
|
25
27
|
this.exchangeCodeForTokens = deps.exchangeCodeForTokens ?? defaultExchangeCodeForTokens;
|
|
@@ -53,6 +55,10 @@ export class ProviderHandler {
|
|
|
53
55
|
}
|
|
54
56
|
}
|
|
55
57
|
}
|
|
58
|
+
isByteRoverAuthSatisfied() {
|
|
59
|
+
const token = this.authStateStore.getToken();
|
|
60
|
+
return token !== undefined && token.isValid();
|
|
61
|
+
}
|
|
56
62
|
setupAwaitOAuthCallback() {
|
|
57
63
|
this.transport.onRequest(ProviderEvents.AWAIT_OAUTH_CALLBACK, async (data) => {
|
|
58
64
|
const flow = this.oauthFlows.get(data.providerId);
|
|
@@ -133,6 +139,9 @@ export class ProviderHandler {
|
|
|
133
139
|
setupConnect() {
|
|
134
140
|
this.transport.onRequest(ProviderEvents.CONNECT, async (data) => {
|
|
135
141
|
const { apiKey, baseUrl, providerId } = data;
|
|
142
|
+
if (providerId === 'byterover' && !this.isByteRoverAuthSatisfied()) {
|
|
143
|
+
return { error: 'ByteRover Provider requires authentication. Run /login or brv login to sign in', success: false };
|
|
144
|
+
}
|
|
136
145
|
// Store API key if provided (supports optional keys for openai-compatible)
|
|
137
146
|
if (apiKey) {
|
|
138
147
|
await this.providerKeychainStore.setApiKey(providerId, apiKey);
|
|
@@ -161,7 +170,8 @@ export class ProviderHandler {
|
|
|
161
170
|
this.transport.onRequest(ProviderEvents.GET_ACTIVE, async () => {
|
|
162
171
|
const activeProviderId = await this.providerConfigStore.getActiveProvider();
|
|
163
172
|
const activeModel = await this.providerConfigStore.getActiveModel(activeProviderId);
|
|
164
|
-
|
|
173
|
+
const loginRequired = activeProviderId === 'byterover' && !this.isByteRoverAuthSatisfied();
|
|
174
|
+
return { activeModel, activeProviderId, loginRequired: loginRequired ? true : undefined };
|
|
165
175
|
});
|
|
166
176
|
}
|
|
167
177
|
setupList() {
|
|
@@ -198,6 +208,9 @@ export class ProviderHandler {
|
|
|
198
208
|
}
|
|
199
209
|
setupSetActive() {
|
|
200
210
|
this.transport.onRequest(ProviderEvents.SET_ACTIVE, async (data) => {
|
|
211
|
+
if (data.providerId === 'byterover' && !this.isByteRoverAuthSatisfied()) {
|
|
212
|
+
return { error: 'ByteRover Provider requires authentication. Run /login or brv login to sign in', success: false };
|
|
213
|
+
}
|
|
201
214
|
await this.providerConfigStore.setActiveProvider(data.providerId);
|
|
202
215
|
this.transport.broadcast(TransportDaemonEventNames.PROVIDER_UPDATED, {});
|
|
203
216
|
return { success: true };
|
|
@@ -21,6 +21,7 @@ export interface ProviderConnectRequest {
|
|
|
21
21
|
providerId: string;
|
|
22
22
|
}
|
|
23
23
|
export interface ProviderConnectResponse {
|
|
24
|
+
error?: string;
|
|
24
25
|
success: boolean;
|
|
25
26
|
}
|
|
26
27
|
export interface ProviderDisconnectRequest {
|
|
@@ -40,11 +41,14 @@ export interface ProviderValidateApiKeyResponse {
|
|
|
40
41
|
export interface ProviderGetActiveResponse {
|
|
41
42
|
activeModel?: string;
|
|
42
43
|
activeProviderId: string;
|
|
44
|
+
/** True when the active provider requires login but the user is not logged in. */
|
|
45
|
+
loginRequired?: boolean;
|
|
43
46
|
}
|
|
44
47
|
export interface ProviderSetActiveRequest {
|
|
45
48
|
providerId: string;
|
|
46
49
|
}
|
|
47
50
|
export interface ProviderSetActiveResponse {
|
|
51
|
+
error?: string;
|
|
48
52
|
success: boolean;
|
|
49
53
|
}
|
|
50
54
|
export interface ProviderCancelOAuthRequest {
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* This is the single source of truth for determining what UI to show.
|
|
6
6
|
*/
|
|
7
7
|
/**
|
|
8
|
-
* The
|
|
8
|
+
* The valid application view modes as a discriminated union.
|
|
9
9
|
*/
|
|
10
10
|
export type AppViewMode = {
|
|
11
11
|
type: 'config-provider';
|
|
@@ -15,13 +15,28 @@ export type AppViewMode = {
|
|
|
15
15
|
type: 'ready';
|
|
16
16
|
};
|
|
17
17
|
/**
|
|
18
|
-
*
|
|
19
|
-
|
|
18
|
+
* Parameters for the pure view mode derivation function.
|
|
19
|
+
*/
|
|
20
|
+
export type DeriveAppViewModeParams = {
|
|
21
|
+
activeModel?: string;
|
|
22
|
+
activeProviderId?: string;
|
|
23
|
+
isAuthorized: boolean;
|
|
24
|
+
isLoading: boolean;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Pure decision logic for determining the app view mode.
|
|
28
|
+
* Extracted from useAppViewMode for testability.
|
|
20
29
|
*
|
|
21
|
-
*
|
|
22
|
-
* 1. Loading
|
|
23
|
-
* 2.
|
|
24
|
-
* 3.
|
|
25
|
-
* 4.
|
|
30
|
+
* Decision tree:
|
|
31
|
+
* 1. Loading → 'loading'
|
|
32
|
+
* 2. ByteRover + unauthenticated → 'config-provider'
|
|
33
|
+
* 3. ByteRover + authenticated → 'ready'
|
|
34
|
+
* 4. Non-byterover + no active model → 'config-provider'
|
|
35
|
+
* 5. Otherwise → 'ready'
|
|
36
|
+
*/
|
|
37
|
+
export declare function deriveAppViewMode(params: DeriveAppViewModeParams): AppViewMode;
|
|
38
|
+
/**
|
|
39
|
+
* React hook that derives the current view mode from stored state.
|
|
40
|
+
* Thin wrapper around deriveAppViewMode — reads from stores, delegates logic.
|
|
26
41
|
*/
|
|
27
42
|
export declare function useAppViewMode(): AppViewMode;
|
|
@@ -7,30 +7,42 @@
|
|
|
7
7
|
import { useAuthStore } from '../../auth/stores/auth-store.js';
|
|
8
8
|
import { useGetActiveProviderConfig } from '../../provider/api/get-active-provider-config.js';
|
|
9
9
|
/**
|
|
10
|
-
*
|
|
11
|
-
*
|
|
10
|
+
* Pure decision logic for determining the app view mode.
|
|
11
|
+
* Extracted from useAppViewMode for testability.
|
|
12
12
|
*
|
|
13
|
-
*
|
|
14
|
-
* 1. Loading
|
|
15
|
-
* 2.
|
|
16
|
-
* 3.
|
|
17
|
-
* 4.
|
|
13
|
+
* Decision tree:
|
|
14
|
+
* 1. Loading → 'loading'
|
|
15
|
+
* 2. ByteRover + unauthenticated → 'config-provider'
|
|
16
|
+
* 3. ByteRover + authenticated → 'ready'
|
|
17
|
+
* 4. Non-byterover + no active model → 'config-provider'
|
|
18
|
+
* 5. Otherwise → 'ready'
|
|
18
19
|
*/
|
|
19
|
-
export function
|
|
20
|
-
|
|
21
|
-
const { data: activeData, isLoading: isLoadingActive } = useGetActiveProviderConfig();
|
|
22
|
-
// Still loading auth or active provider check
|
|
23
|
-
if (isLoadingAuth || isLoadingActive) {
|
|
20
|
+
export function deriveAppViewMode(params) {
|
|
21
|
+
if (params.isLoading) {
|
|
24
22
|
return { type: 'loading' };
|
|
25
23
|
}
|
|
26
|
-
|
|
27
|
-
|
|
24
|
+
if (params.activeProviderId === 'byterover' && !params.isAuthorized) {
|
|
25
|
+
return { type: 'config-provider' };
|
|
26
|
+
}
|
|
27
|
+
if (params.activeProviderId === 'byterover') {
|
|
28
28
|
return { type: 'ready' };
|
|
29
29
|
}
|
|
30
|
-
|
|
31
|
-
if (!activeData?.activeModel) {
|
|
30
|
+
if (!params.activeModel) {
|
|
32
31
|
return { type: 'config-provider' };
|
|
33
32
|
}
|
|
34
|
-
// Normal app state
|
|
35
33
|
return { type: 'ready' };
|
|
36
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* React hook that derives the current view mode from stored state.
|
|
37
|
+
* Thin wrapper around deriveAppViewMode — reads from stores, delegates logic.
|
|
38
|
+
*/
|
|
39
|
+
export function useAppViewMode() {
|
|
40
|
+
const { isAuthorized, isLoadingInitial: isLoadingAuth } = useAuthStore();
|
|
41
|
+
const { data: activeData, isLoading: isLoadingActive } = useGetActiveProviderConfig();
|
|
42
|
+
return deriveAppViewMode({
|
|
43
|
+
activeModel: activeData?.activeModel,
|
|
44
|
+
activeProviderId: activeData?.activeProviderId,
|
|
45
|
+
isAuthorized,
|
|
46
|
+
isLoading: isLoadingAuth || isLoadingActive,
|
|
47
|
+
});
|
|
48
|
+
}
|