byterover-cli 2.4.1 → 2.5.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/oclif/commands/providers/index.d.ts +1 -0
- package/dist/oclif/commands/providers/index.js +9 -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 +1 -1
- package/package.json +1 -1
|
@@ -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) {
|
|
@@ -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
|
+
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* ProviderFlow Component
|
|
3
3
|
*
|
|
4
4
|
* Multi-step React flow for the /providers command.
|
|
5
|
-
* State machine: loading → select → provider_actions → api_key → connecting → done
|
|
5
|
+
* State machine: loading → select → login_prompt → login → provider_actions → api_key → connecting → done
|
|
6
6
|
*
|
|
7
7
|
* Owns the UX flow — fetches providers, renders selection,
|
|
8
8
|
* handles API key input, and calls connect/setActive mutations.
|
|
@@ -3,7 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
* ProviderFlow Component
|
|
4
4
|
*
|
|
5
5
|
* Multi-step React flow for the /providers command.
|
|
6
|
-
* State machine: loading → select → provider_actions → api_key → connecting → done
|
|
6
|
+
* State machine: loading → select → login_prompt → login → provider_actions → api_key → connecting → done
|
|
7
7
|
*
|
|
8
8
|
* Owns the UX flow — fetches providers, renders selection,
|
|
9
9
|
* handles API key input, and calls connect/setActive mutations.
|
|
@@ -11,9 +11,12 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
11
11
|
*/
|
|
12
12
|
import { Box, Text } from 'ink';
|
|
13
13
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
14
|
+
import { InlineConfirm } from '../../../components/inline-prompts/inline-confirm.js';
|
|
14
15
|
import { SelectableList } from '../../../components/selectable-list.js';
|
|
15
16
|
import { useTheme } from '../../../hooks/index.js';
|
|
16
17
|
import { formatTransportError } from '../../../utils/index.js';
|
|
18
|
+
import { LoginFlow } from '../../auth/components/login-flow.js';
|
|
19
|
+
import { useAuthStore } from '../../auth/stores/auth-store.js';
|
|
17
20
|
import { useConnectProvider } from '../api/connect-provider.js';
|
|
18
21
|
import { useDisconnectProvider } from '../api/disconnect-provider.js';
|
|
19
22
|
import { useGetProviders } from '../api/get-providers.js';
|
|
@@ -36,6 +39,7 @@ export const ProviderFlow = ({ hideCancelButton = false, isActive = true, onCanc
|
|
|
36
39
|
const disconnectMutation = useDisconnectProvider();
|
|
37
40
|
const setActiveMutation = useSetActiveProvider();
|
|
38
41
|
const validateMutation = useValidateApiKey();
|
|
42
|
+
const isAuthorized = useAuthStore((s) => s.isAuthorized);
|
|
39
43
|
const providers = data?.providers ?? [];
|
|
40
44
|
// Exit gracefully when providers query fails — don't leave user stuck
|
|
41
45
|
useEffect(() => {
|
|
@@ -98,6 +102,11 @@ export const ProviderFlow = ({ hideCancelButton = false, isActive = true, onCanc
|
|
|
98
102
|
const handleSelect = useCallback(async (provider) => {
|
|
99
103
|
setSelectedProvider(provider);
|
|
100
104
|
setError(null);
|
|
105
|
+
// ByteRover requires authentication
|
|
106
|
+
if (provider.id === 'byterover' && !isAuthorized) {
|
|
107
|
+
setStep('login_prompt');
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
101
110
|
// ByteRover + already active → complete
|
|
102
111
|
if (provider.id === 'byterover' && provider.isCurrent) {
|
|
103
112
|
onComplete(`Connected to ${provider.name}`);
|
|
@@ -108,11 +117,12 @@ export const ProviderFlow = ({ hideCancelButton = false, isActive = true, onCanc
|
|
|
108
117
|
setStep('provider_actions');
|
|
109
118
|
return;
|
|
110
119
|
}
|
|
111
|
-
// ByteRover + not connected → connect directly, no model select
|
|
120
|
+
// ByteRover + not connected → connect + activate directly, no model select
|
|
112
121
|
if (provider.id === 'byterover') {
|
|
113
122
|
setStep('connecting');
|
|
114
123
|
try {
|
|
115
124
|
await connectMutation.mutateAsync({ providerId: provider.id });
|
|
125
|
+
await setActiveMutation.mutateAsync({ providerId: provider.id });
|
|
116
126
|
onComplete(`Connected to ${provider.name}`);
|
|
117
127
|
}
|
|
118
128
|
catch (error_) {
|
|
@@ -146,12 +156,16 @@ export const ProviderFlow = ({ hideCancelButton = false, isActive = true, onCanc
|
|
|
146
156
|
setError(formatTransportError(error_));
|
|
147
157
|
setStep('select');
|
|
148
158
|
}
|
|
149
|
-
}, [connectMutation, onComplete]);
|
|
159
|
+
}, [connectMutation, isAuthorized, onComplete, setActiveMutation]);
|
|
150
160
|
const handleAction = useCallback(async (action) => {
|
|
151
161
|
if (!selectedProvider)
|
|
152
162
|
return;
|
|
153
163
|
switch (action.id) {
|
|
154
164
|
case 'activate': {
|
|
165
|
+
if (selectedProvider.id === 'byterover' && !isAuthorized) {
|
|
166
|
+
setStep('login_prompt');
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
155
169
|
setStep('connecting');
|
|
156
170
|
try {
|
|
157
171
|
await setActiveMutation.mutateAsync({ providerId: selectedProvider.id });
|
|
@@ -199,7 +213,14 @@ export const ProviderFlow = ({ hideCancelButton = false, isActive = true, onCanc
|
|
|
199
213
|
break;
|
|
200
214
|
}
|
|
201
215
|
}
|
|
202
|
-
}, [disconnectMutation, onComplete, selectedProvider, setActiveMutation]);
|
|
216
|
+
}, [disconnectMutation, isAuthorized, onComplete, selectedProvider, setActiveMutation]);
|
|
217
|
+
const handleLoginComplete = useCallback((message) => {
|
|
218
|
+
const nowAuthorized = useAuthStore.getState().isAuthorized;
|
|
219
|
+
if (!nowAuthorized) {
|
|
220
|
+
setError(message);
|
|
221
|
+
}
|
|
222
|
+
setStep('select');
|
|
223
|
+
}, []);
|
|
203
224
|
const handleBaseUrlSubmit = useCallback((url) => {
|
|
204
225
|
setBaseUrl(url);
|
|
205
226
|
setStep('api_key');
|
|
@@ -285,6 +306,20 @@ export const ProviderFlow = ({ hideCancelButton = false, isActive = true, onCanc
|
|
|
285
306
|
case 'connecting': {
|
|
286
307
|
return (_jsx(Box, { children: _jsxs(Text, { color: colors.primary, children: ["Connecting to ", selectedProvider?.name, "..."] }) }));
|
|
287
308
|
}
|
|
309
|
+
case 'login': {
|
|
310
|
+
return (_jsx(LoginFlow, { onCancel: () => { }, onComplete: handleLoginComplete }));
|
|
311
|
+
}
|
|
312
|
+
case 'login_prompt': {
|
|
313
|
+
return (_jsx(InlineConfirm, { default: true, message: "ByteRover requires authentication. Sign in now", onConfirm: (confirmed) => {
|
|
314
|
+
if (confirmed) {
|
|
315
|
+
setStep('login');
|
|
316
|
+
}
|
|
317
|
+
else {
|
|
318
|
+
setStep('select');
|
|
319
|
+
setSelectedProvider(null);
|
|
320
|
+
}
|
|
321
|
+
} }));
|
|
322
|
+
}
|
|
288
323
|
case 'model_select': {
|
|
289
324
|
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;
|
|
290
325
|
}
|
package/oclif.manifest.json
CHANGED