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.
Files changed (33) hide show
  1. package/dist/oclif/commands/providers/index.d.ts +1 -0
  2. package/dist/oclif/commands/providers/index.js +9 -1
  3. package/dist/server/core/domain/entities/provider-config.js +1 -1
  4. package/dist/server/core/domain/entities/provider-registry.js +1 -1
  5. package/dist/server/core/domain/transport/schemas.d.ts +2 -0
  6. package/dist/server/infra/auth/oauth-service.js +2 -0
  7. package/dist/server/infra/auth/oidc-discovery-service.js +1 -0
  8. package/dist/server/infra/cogit/http-cogit-push-service.js +1 -0
  9. package/dist/server/infra/daemon/agent-process.js +4 -11
  10. package/dist/server/infra/daemon/brv-server.js +1 -1
  11. package/dist/server/infra/daemon/task-validation.d.ts +18 -0
  12. package/dist/server/infra/daemon/task-validation.js +42 -0
  13. package/dist/server/infra/http/authenticated-http-client.js +3 -0
  14. package/dist/server/infra/http/models-dev-client.js +1 -0
  15. package/dist/server/infra/http/openrouter-api-client.js +1 -0
  16. package/dist/server/infra/http/provider-model-fetchers.js +3 -0
  17. package/dist/server/infra/hub/hub-install-service.js +1 -0
  18. package/dist/server/infra/hub/hub-registry-service.js +1 -0
  19. package/dist/server/infra/memory/http-memory-storage-service.js +1 -0
  20. package/dist/server/infra/process/feature-handlers.js +1 -0
  21. package/dist/server/infra/provider/provider-config-resolver.d.ts +8 -1
  22. package/dist/server/infra/provider/provider-config-resolver.js +11 -12
  23. package/dist/server/infra/provider-oauth/refresh-token-exchange.js +1 -0
  24. package/dist/server/infra/provider-oauth/token-exchange.js +1 -0
  25. package/dist/server/infra/transport/handlers/provider-handler.d.ts +4 -0
  26. package/dist/server/infra/transport/handlers/provider-handler.js +14 -1
  27. package/dist/shared/transport/events/provider-events.d.ts +4 -0
  28. package/dist/tui/features/onboarding/hooks/use-app-view-mode.d.ts +23 -8
  29. package/dist/tui/features/onboarding/hooks/use-app-view-mode.js +29 -17
  30. package/dist/tui/features/provider/components/provider-flow.d.ts +1 -1
  31. package/dist/tui/features/provider/components/provider-flow.js +39 -4
  32. package/oclif.manifest.json +1 -1
  33. package/package.json +1 -1
@@ -8,6 +8,7 @@ export default class Provider extends Command {
8
8
  };
9
9
  protected fetchActiveProvider(options?: DaemonClientOptions): Promise<{
10
10
  activeModel: string | undefined;
11
+ loginRequired: boolean | undefined;
11
12
  providerId: string;
12
13
  providerName: string;
13
14
  }>;
@@ -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
- writeJsonResponse({ command: 'providers', data: info, success: true });
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 ? 'byterover' : this.activeProvider;
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, no API key required. Free tier available.',
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
  }
@@ -79,6 +79,7 @@ export class OidcDiscoveryService {
79
79
  const response = await axios.get(wellKnownUrl, {
80
80
  httpAgent: ProxyConfig.getProxyAgent(),
81
81
  httpsAgent: ProxyConfig.getProxyAgent(),
82
+ proxy: false,
82
83
  timeout: this.timeoutMs,
83
84
  });
84
85
  // Validate required fields
@@ -99,6 +99,7 @@ export class HttpCogitPushService {
99
99
  },
100
100
  httpAgent: ProxyConfig.getProxyAgent(),
101
101
  httpsAgent: ProxyConfig.getProxyAgent(),
102
+ proxy: false,
102
103
  timeout: this.config.timeout,
103
104
  });
104
105
  return CogitPushResponse.fromJson(response.data);
@@ -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
- if (!freshProviderConfig.activeProvider) {
312
- const error = serializeTaskError(new TaskError('No provider connected. Use /provider in the REPL to configure a provider.', TaskErrorCode.PROVIDER_NOT_CONFIGURED));
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);
@@ -60,6 +60,7 @@ export class ModelsDevClient {
60
60
  const response = await axios.get(MODELS_DEV_URL, {
61
61
  httpAgent: ProxyConfig.getProxyAgent(),
62
62
  httpsAgent: ProxyConfig.getProxyAgent(),
63
+ proxy: false,
63
64
  timeout: FETCH_TIMEOUT_MS,
64
65
  });
65
66
  const { data } = response;
@@ -103,6 +103,7 @@ export class OpenRouterApiClient {
103
103
  },
104
104
  httpAgent: ProxyConfig.getProxyAgent(),
105
105
  httpsAgent: ProxyConfig.getProxyAgent(),
106
+ proxy: false,
106
107
  timeout: 30_000,
107
108
  });
108
109
  return response.data.data.map((model) => this.normalizeModel(model));
@@ -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 };
@@ -33,6 +33,7 @@ export class HubInstallService {
33
33
  headers,
34
34
  httpAgent: ProxyConfig.getProxyAgent(),
35
35
  httpsAgent: ProxyConfig.getProxyAgent(),
36
+ proxy: false,
36
37
  responseType: 'text',
37
38
  timeout: 15_000,
38
39
  });
@@ -72,6 +72,7 @@ export class HubRegistryService {
72
72
  headers,
73
73
  httpAgent: ProxyConfig.getProxyAgent(),
74
74
  httpsAgent: ProxyConfig.getProxyAgent(),
75
+ proxy: false,
75
76
  timeout: this.timeoutMs,
76
77
  });
77
78
  const validated = this.parseRegistryResponse(response.data);
@@ -58,6 +58,7 @@ export class HttpMemoryStorageService {
58
58
  },
59
59
  httpAgent: ProxyConfig.getProxyAgent(),
60
60
  httpsAgent: ProxyConfig.getProxyAgent(),
61
+ proxy: false,
61
62
  timeout: this.config.timeout,
62
63
  });
63
64
  }
@@ -59,6 +59,7 @@ export async function setupFeatureHandlers({ authStateStore, broadcastToProject,
59
59
  userService,
60
60
  }).setup();
61
61
  new ProviderHandler({
62
+ authStateStore,
62
63
  browserLauncher: new SystemBrowserLauncher(),
63
64
  providerConfigStore,
64
65
  providerKeychainStore,
@@ -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 declare function resolveProviderConfig(providerConfigStore: IProviderConfigStore, providerKeychainStore: IProviderKeychainStore, tokenRefreshManager?: ITokenRefreshManager): Promise<ProviderConfigResponse>;
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() falls back to 'byterover' when the active provider is
53
- // removed, which causes the TUI to show 'ready' and skip provider setup. Explicitly
54
- // set activeProvider to '' so the user is returned to the provider setup flow.
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
- * Resolve the active provider's full configuration.
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 (!activeProvider || activeProvider === 'byterover') {
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
@@ -23,6 +23,7 @@ export async function exchangeRefreshToken(params) {
23
23
  },
24
24
  httpAgent: ProxyConfig.getProxyAgent(),
25
25
  httpsAgent: ProxyConfig.getProxyAgent(),
26
+ proxy: false,
26
27
  timeout: 30_000,
27
28
  });
28
29
  }
@@ -28,6 +28,7 @@ export async function exchangeCodeForTokens(params) {
28
28
  },
29
29
  httpAgent: ProxyConfig.getProxyAgent(),
30
30
  httpsAgent: ProxyConfig.getProxyAgent(),
31
+ proxy: false,
31
32
  timeout: 30_000,
32
33
  });
33
34
  }
@@ -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
- return { activeModel, activeProviderId };
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 5 valid application view modes as a discriminated union.
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
- * Selector that derives the current view mode from stored state.
19
- * This is the ONLY way to determine what UI to show.
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
- * View mode decision tree:
22
- * 1. Loading auth or onboarding check -> 'loading'
23
- * 2. New user (hasDismissed) -> 'onboarding'
24
- * 3. Existing user, no provider config -> provider flow
25
- * 4. Otherwise -> 'ready'
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
- * Selector that derives the current view mode from stored state.
11
- * This is the ONLY way to determine what UI to show.
10
+ * Pure decision logic for determining the app view mode.
11
+ * Extracted from useAppViewMode for testability.
12
12
  *
13
- * View mode decision tree:
14
- * 1. Loading auth or onboarding check -> 'loading'
15
- * 2. New user (hasDismissed) -> 'onboarding'
16
- * 3. Existing user, no provider config -> provider flow
17
- * 4. Otherwise -> 'ready'
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 useAppViewMode() {
20
- const { isLoadingInitial: isLoadingAuth } = useAuthStore();
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
- // ByteRover is the default provider and doesn't require model config
27
- if (activeData?.activeProviderId === 'byterover') {
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
- // No active model configured for non-byterover provider — need provider setup
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
  }
@@ -1636,5 +1636,5 @@
1636
1636
  ]
1637
1637
  }
1638
1638
  },
1639
- "version": "2.4.1"
1639
+ "version": "2.5.0"
1640
1640
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "byterover-cli",
3
3
  "description": "ByteRover's CLI",
4
- "version": "2.4.1",
4
+ "version": "2.5.0",
5
5
  "author": "ByteRover",
6
6
  "bin": {
7
7
  "brv": "./bin/run.js"