byterover-cli 2.4.0 → 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 (35) 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/constants.d.ts +1 -1
  4. package/dist/server/constants.js +1 -1
  5. package/dist/server/core/domain/entities/provider-config.js +1 -1
  6. package/dist/server/core/domain/entities/provider-registry.js +1 -1
  7. package/dist/server/core/domain/transport/schemas.d.ts +2 -0
  8. package/dist/server/infra/auth/oauth-service.js +2 -0
  9. package/dist/server/infra/auth/oidc-discovery-service.js +1 -0
  10. package/dist/server/infra/cogit/http-cogit-push-service.js +1 -0
  11. package/dist/server/infra/daemon/agent-process.js +4 -11
  12. package/dist/server/infra/daemon/brv-server.js +10 -4
  13. package/dist/server/infra/daemon/task-validation.d.ts +18 -0
  14. package/dist/server/infra/daemon/task-validation.js +42 -0
  15. package/dist/server/infra/http/authenticated-http-client.js +3 -0
  16. package/dist/server/infra/http/models-dev-client.js +1 -0
  17. package/dist/server/infra/http/openrouter-api-client.js +1 -0
  18. package/dist/server/infra/http/provider-model-fetchers.js +3 -0
  19. package/dist/server/infra/hub/hub-install-service.js +1 -0
  20. package/dist/server/infra/hub/hub-registry-service.js +1 -0
  21. package/dist/server/infra/memory/http-memory-storage-service.js +1 -0
  22. package/dist/server/infra/process/feature-handlers.js +1 -0
  23. package/dist/server/infra/provider/provider-config-resolver.d.ts +8 -1
  24. package/dist/server/infra/provider/provider-config-resolver.js +11 -12
  25. package/dist/server/infra/provider-oauth/refresh-token-exchange.js +1 -0
  26. package/dist/server/infra/provider-oauth/token-exchange.js +1 -0
  27. package/dist/server/infra/transport/handlers/provider-handler.d.ts +4 -0
  28. package/dist/server/infra/transport/handlers/provider-handler.js +14 -1
  29. package/dist/shared/transport/events/provider-events.d.ts +4 -0
  30. package/dist/tui/features/onboarding/hooks/use-app-view-mode.d.ts +23 -8
  31. package/dist/tui/features/onboarding/hooks/use-app-view-mode.js +29 -17
  32. package/dist/tui/features/provider/components/provider-flow.d.ts +1 -1
  33. package/dist/tui/features/provider/components/provider-flow.js +39 -4
  34. package/oclif.manifest.json +99 -99
  35. 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) {
@@ -55,7 +55,7 @@ export declare const SHUTDOWN_FORCE_EXIT_MS = 5000;
55
55
  export declare const AUTH_STATE_POLL_INTERVAL_MS = 5000;
56
56
  export declare const AGENT_MAX_CONCURRENT_TASKS = 5;
57
57
  export declare const AGENT_POOL_MAX_SIZE = 10;
58
- export declare const AGENT_PROCESS_READY_TIMEOUT_MS = 15000;
58
+ export declare const AGENT_PROCESS_READY_TIMEOUT_MS = 30000;
59
59
  export declare const AGENT_PROCESS_STOP_TIMEOUT_MS = 5000;
60
60
  export declare const CURATE_LOG_DIR = "curate-log";
61
61
  export declare const CURATE_LOG_ID_PREFIX = "cur";
@@ -75,7 +75,7 @@ export const AUTH_STATE_POLL_INTERVAL_MS = 5000; // Poll token store every 5s
75
75
  // Agent Pool (T6)
76
76
  export const AGENT_MAX_CONCURRENT_TASKS = 5; // Max parallel curate/query tasks per agent process
77
77
  export const AGENT_POOL_MAX_SIZE = 10;
78
- export const AGENT_PROCESS_READY_TIMEOUT_MS = 15_000; // 15s max wait for child process to register
78
+ export const AGENT_PROCESS_READY_TIMEOUT_MS = 30_000; // 30s max wait for child process to register
79
79
  export const AGENT_PROCESS_STOP_TIMEOUT_MS = 5000; // 5s max wait for child process to stop gracefully
80
80
  // Curate log
81
81
  export const CURATE_LOG_DIR = 'curate-log';
@@ -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++;
@@ -204,7 +204,11 @@ async function main() {
204
204
  agentPool = new AgentPool({
205
205
  agentIdleTimeoutPolicy,
206
206
  agentProcessFactory(projectPath) {
207
- return fork(agentProcessPath, [], {
207
+ // Prevent console window flash on Windows when forking agent processes.
208
+ // windowsHide is supported at runtime (fork delegates to spawn) but not in ForkOptions types,
209
+ // so we extract the options to a variable to bypass excess property checking.
210
+ const e2eStdio = ['ignore', 'inherit', 'inherit', 'ipc'];
211
+ const forkOptions = {
208
212
  cwd: projectPath,
209
213
  env: {
210
214
  ...process.env,
@@ -212,8 +216,10 @@ async function main() {
212
216
  BRV_AGENT_PROJECT_PATH: projectPath,
213
217
  },
214
218
  // In E2E mode, inherit stderr to see agent errors
215
- stdio: process.env.BRV_E2E_MODE === 'true' ? ['ignore', 'inherit', 'inherit', 'ipc'] : undefined,
216
- });
219
+ stdio: process.env.BRV_E2E_MODE === 'true' ? e2eStdio : undefined,
220
+ windowsHide: true,
221
+ };
222
+ return fork(agentProcessPath, [], forkOptions);
217
223
  },
218
224
  log,
219
225
  transportServer,
@@ -353,7 +359,7 @@ async function main() {
353
359
  // is returned to the onboarding flow rather than hitting a cryptic API key error mid-task.
354
360
  await clearStaleProviderConfig(providerConfigStore, providerKeychainStore, providerOAuthTokenStore);
355
361
  // State endpoint: provider config — agents request this on startup and after provider:updated
356
- transportServer.onRequest(TransportStateEventNames.GET_PROVIDER_CONFIG, async () => resolveProviderConfig(providerConfigStore, providerKeychainStore, tokenRefreshManager));
362
+ transportServer.onRequest(TransportStateEventNames.GET_PROVIDER_CONFIG, async () => resolveProviderConfig({ authStateStore, providerConfigStore, providerKeychainStore, tokenRefreshManager }));
357
363
  // Feature handlers (auth, init, status, push, pull, etc.) require async OIDC discovery.
358
364
  // Placed after daemon:getState so the debug endpoint is available immediately,
359
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
  }
@@ -1083,6 +1083,104 @@
1083
1083
  "switch.js"
1084
1084
  ]
1085
1085
  },
1086
+ "space:list": {
1087
+ "aliases": [],
1088
+ "args": {},
1089
+ "description": "List all teams and spaces",
1090
+ "examples": [
1091
+ "<%= config.bin %> space list",
1092
+ "<%= config.bin %> space list --format json"
1093
+ ],
1094
+ "flags": {
1095
+ "format": {
1096
+ "char": "f",
1097
+ "description": "Output format",
1098
+ "name": "format",
1099
+ "default": "text",
1100
+ "hasDynamicHelp": false,
1101
+ "multiple": false,
1102
+ "options": [
1103
+ "text",
1104
+ "json"
1105
+ ],
1106
+ "type": "option"
1107
+ }
1108
+ },
1109
+ "hasDynamicHelp": false,
1110
+ "hiddenAliases": [],
1111
+ "id": "space:list",
1112
+ "pluginAlias": "byterover-cli",
1113
+ "pluginName": "byterover-cli",
1114
+ "pluginType": "core",
1115
+ "strict": true,
1116
+ "enableJsonFlag": false,
1117
+ "isESM": true,
1118
+ "relativePath": [
1119
+ "dist",
1120
+ "oclif",
1121
+ "commands",
1122
+ "space",
1123
+ "list.js"
1124
+ ]
1125
+ },
1126
+ "space:switch": {
1127
+ "aliases": [],
1128
+ "args": {},
1129
+ "description": "Switch to a different space",
1130
+ "examples": [
1131
+ "<%= config.bin %> space switch --team acme --name my-space",
1132
+ "<%= config.bin %> space switch --team acme --name my-space --format json"
1133
+ ],
1134
+ "flags": {
1135
+ "format": {
1136
+ "char": "f",
1137
+ "description": "Output format",
1138
+ "name": "format",
1139
+ "default": "text",
1140
+ "hasDynamicHelp": false,
1141
+ "multiple": false,
1142
+ "options": [
1143
+ "text",
1144
+ "json"
1145
+ ],
1146
+ "type": "option"
1147
+ },
1148
+ "name": {
1149
+ "char": "n",
1150
+ "description": "Name of the space to switch to",
1151
+ "name": "name",
1152
+ "required": true,
1153
+ "hasDynamicHelp": false,
1154
+ "multiple": false,
1155
+ "type": "option"
1156
+ },
1157
+ "team": {
1158
+ "char": "t",
1159
+ "description": "Team name",
1160
+ "name": "team",
1161
+ "required": true,
1162
+ "hasDynamicHelp": false,
1163
+ "multiple": false,
1164
+ "type": "option"
1165
+ }
1166
+ },
1167
+ "hasDynamicHelp": false,
1168
+ "hiddenAliases": [],
1169
+ "id": "space:switch",
1170
+ "pluginAlias": "byterover-cli",
1171
+ "pluginName": "byterover-cli",
1172
+ "pluginType": "core",
1173
+ "strict": true,
1174
+ "enableJsonFlag": false,
1175
+ "isESM": true,
1176
+ "relativePath": [
1177
+ "dist",
1178
+ "oclif",
1179
+ "commands",
1180
+ "space",
1181
+ "switch.js"
1182
+ ]
1183
+ },
1086
1184
  "providers:connect": {
1087
1185
  "aliases": [],
1088
1186
  "args": {
@@ -1339,104 +1437,6 @@
1339
1437
  "switch.js"
1340
1438
  ]
1341
1439
  },
1342
- "space:list": {
1343
- "aliases": [],
1344
- "args": {},
1345
- "description": "List all teams and spaces",
1346
- "examples": [
1347
- "<%= config.bin %> space list",
1348
- "<%= config.bin %> space list --format json"
1349
- ],
1350
- "flags": {
1351
- "format": {
1352
- "char": "f",
1353
- "description": "Output format",
1354
- "name": "format",
1355
- "default": "text",
1356
- "hasDynamicHelp": false,
1357
- "multiple": false,
1358
- "options": [
1359
- "text",
1360
- "json"
1361
- ],
1362
- "type": "option"
1363
- }
1364
- },
1365
- "hasDynamicHelp": false,
1366
- "hiddenAliases": [],
1367
- "id": "space:list",
1368
- "pluginAlias": "byterover-cli",
1369
- "pluginName": "byterover-cli",
1370
- "pluginType": "core",
1371
- "strict": true,
1372
- "enableJsonFlag": false,
1373
- "isESM": true,
1374
- "relativePath": [
1375
- "dist",
1376
- "oclif",
1377
- "commands",
1378
- "space",
1379
- "list.js"
1380
- ]
1381
- },
1382
- "space:switch": {
1383
- "aliases": [],
1384
- "args": {},
1385
- "description": "Switch to a different space",
1386
- "examples": [
1387
- "<%= config.bin %> space switch --team acme --name my-space",
1388
- "<%= config.bin %> space switch --team acme --name my-space --format json"
1389
- ],
1390
- "flags": {
1391
- "format": {
1392
- "char": "f",
1393
- "description": "Output format",
1394
- "name": "format",
1395
- "default": "text",
1396
- "hasDynamicHelp": false,
1397
- "multiple": false,
1398
- "options": [
1399
- "text",
1400
- "json"
1401
- ],
1402
- "type": "option"
1403
- },
1404
- "name": {
1405
- "char": "n",
1406
- "description": "Name of the space to switch to",
1407
- "name": "name",
1408
- "required": true,
1409
- "hasDynamicHelp": false,
1410
- "multiple": false,
1411
- "type": "option"
1412
- },
1413
- "team": {
1414
- "char": "t",
1415
- "description": "Team name",
1416
- "name": "team",
1417
- "required": true,
1418
- "hasDynamicHelp": false,
1419
- "multiple": false,
1420
- "type": "option"
1421
- }
1422
- },
1423
- "hasDynamicHelp": false,
1424
- "hiddenAliases": [],
1425
- "id": "space:switch",
1426
- "pluginAlias": "byterover-cli",
1427
- "pluginName": "byterover-cli",
1428
- "pluginType": "core",
1429
- "strict": true,
1430
- "enableJsonFlag": false,
1431
- "isESM": true,
1432
- "relativePath": [
1433
- "dist",
1434
- "oclif",
1435
- "commands",
1436
- "space",
1437
- "switch.js"
1438
- ]
1439
- },
1440
1440
  "hub:registry:add": {
1441
1441
  "aliases": [],
1442
1442
  "args": {
@@ -1636,5 +1636,5 @@
1636
1636
  ]
1637
1637
  }
1638
1638
  },
1639
- "version": "2.4.0"
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.0",
4
+ "version": "2.5.0",
5
5
  "author": "ByteRover",
6
6
  "bin": {
7
7
  "brv": "./bin/run.js"