byterover-cli 2.2.0 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/dist/agent/infra/llm/providers/openai.d.ts +12 -0
  2. package/dist/agent/infra/llm/providers/openai.js +52 -1
  3. package/dist/oclif/commands/curate/index.js +2 -2
  4. package/dist/oclif/commands/model/switch.js +14 -3
  5. package/dist/oclif/commands/providers/connect.d.ts +9 -0
  6. package/dist/oclif/commands/providers/connect.js +110 -14
  7. package/dist/oclif/commands/providers/list.js +3 -5
  8. package/dist/oclif/commands/query.js +2 -2
  9. package/dist/oclif/lib/daemon-client.d.ts +4 -0
  10. package/dist/oclif/lib/daemon-client.js +13 -3
  11. package/dist/server/core/domain/entities/provider-config.d.ts +6 -0
  12. package/dist/server/core/domain/entities/provider-config.js +4 -3
  13. package/dist/server/core/domain/entities/provider-registry.d.ts +41 -1
  14. package/dist/server/core/domain/entities/provider-registry.js +24 -1
  15. package/dist/server/core/domain/errors/task-error.d.ts +2 -0
  16. package/dist/server/core/domain/errors/task-error.js +6 -1
  17. package/dist/server/core/domain/transport/schemas.d.ts +2 -0
  18. package/dist/server/core/interfaces/i-provider-config-store.d.ts +2 -0
  19. package/dist/server/core/interfaces/i-provider-model-fetcher.d.ts +12 -3
  20. package/dist/server/core/interfaces/i-provider-oauth-token-store.d.ts +24 -0
  21. package/dist/server/core/interfaces/i-provider-oauth-token-store.js +1 -0
  22. package/dist/server/core/interfaces/i-token-refresh-manager.d.ts +7 -0
  23. package/dist/server/core/interfaces/i-token-refresh-manager.js +1 -0
  24. package/dist/server/infra/daemon/agent-process.js +22 -4
  25. package/dist/server/infra/daemon/brv-server.js +13 -2
  26. package/dist/server/infra/http/models-dev-client.d.ts +29 -0
  27. package/dist/server/infra/http/models-dev-client.js +133 -0
  28. package/dist/server/infra/http/provider-model-fetcher-registry.d.ts +2 -1
  29. package/dist/server/infra/http/provider-model-fetcher-registry.js +6 -1
  30. package/dist/server/infra/http/provider-model-fetchers.d.ts +33 -8
  31. package/dist/server/infra/http/provider-model-fetchers.js +88 -10
  32. package/dist/server/infra/process/feature-handlers.d.ts +3 -1
  33. package/dist/server/infra/process/feature-handlers.js +3 -1
  34. package/dist/server/infra/provider/provider-config-resolver.d.ts +4 -2
  35. package/dist/server/infra/provider/provider-config-resolver.js +59 -4
  36. package/dist/server/infra/provider-oauth/callback-server.d.ts +24 -0
  37. package/dist/server/infra/provider-oauth/callback-server.js +203 -0
  38. package/dist/server/infra/provider-oauth/errors.d.ts +39 -0
  39. package/dist/server/infra/provider-oauth/errors.js +76 -0
  40. package/dist/server/infra/provider-oauth/index.d.ts +9 -0
  41. package/dist/server/infra/provider-oauth/index.js +9 -0
  42. package/dist/server/infra/provider-oauth/jwt-utils.d.ts +17 -0
  43. package/dist/server/infra/provider-oauth/jwt-utils.js +51 -0
  44. package/dist/server/infra/provider-oauth/pkce-service.d.ts +22 -0
  45. package/dist/server/infra/provider-oauth/pkce-service.js +33 -0
  46. package/dist/server/infra/provider-oauth/provider-oauth-token-store.d.ts +48 -0
  47. package/dist/server/infra/provider-oauth/provider-oauth-token-store.js +155 -0
  48. package/dist/server/infra/provider-oauth/refresh-token-exchange.d.ts +8 -0
  49. package/dist/server/infra/provider-oauth/refresh-token-exchange.js +39 -0
  50. package/dist/server/infra/provider-oauth/token-exchange.d.ts +8 -0
  51. package/dist/server/infra/provider-oauth/token-exchange.js +44 -0
  52. package/dist/server/infra/provider-oauth/token-refresh-manager.d.ts +32 -0
  53. package/dist/server/infra/provider-oauth/token-refresh-manager.js +96 -0
  54. package/dist/server/infra/provider-oauth/types.d.ts +55 -0
  55. package/dist/server/infra/provider-oauth/types.js +22 -0
  56. package/dist/server/infra/storage/file-provider-config-store.d.ts +2 -0
  57. package/dist/server/infra/storage/file-provider-config-store.js +1 -3
  58. package/dist/server/infra/transport/handlers/model-handler.d.ts +3 -0
  59. package/dist/server/infra/transport/handlers/model-handler.js +53 -11
  60. package/dist/server/infra/transport/handlers/provider-handler.d.ts +26 -0
  61. package/dist/server/infra/transport/handlers/provider-handler.js +215 -13
  62. package/dist/shared/constants/oauth.d.ts +14 -0
  63. package/dist/shared/constants/oauth.js +14 -0
  64. package/dist/shared/transport/events/index.d.ts +5 -0
  65. package/dist/shared/transport/events/model-events.d.ts +2 -0
  66. package/dist/shared/transport/events/provider-events.d.ts +36 -0
  67. package/dist/shared/transport/events/provider-events.js +5 -0
  68. package/dist/shared/transport/types/dto.d.ts +4 -0
  69. package/dist/tui/features/model/api/set-active-model.d.ts +1 -1
  70. package/dist/tui/features/model/api/set-active-model.js +12 -4
  71. package/dist/tui/features/provider/api/await-oauth-callback.d.ts +11 -0
  72. package/dist/tui/features/provider/api/await-oauth-callback.js +25 -0
  73. package/dist/tui/features/provider/api/cancel-oauth.d.ts +5 -0
  74. package/dist/tui/features/provider/api/cancel-oauth.js +10 -0
  75. package/dist/tui/features/provider/api/start-oauth.d.ts +11 -0
  76. package/dist/tui/features/provider/api/start-oauth.js +15 -0
  77. package/dist/tui/features/provider/components/auth-method-dialog.d.ts +9 -0
  78. package/dist/tui/features/provider/components/auth-method-dialog.js +20 -0
  79. package/dist/tui/features/provider/components/oauth-dialog.d.ts +9 -0
  80. package/dist/tui/features/provider/components/oauth-dialog.js +96 -0
  81. package/dist/tui/features/provider/components/provider-dialog.js +1 -1
  82. package/dist/tui/features/provider/components/provider-flow.js +54 -4
  83. package/dist/tui/features/provider/components/provider-subscription-initializer.d.ts +1 -0
  84. package/dist/tui/features/provider/components/provider-subscription-initializer.js +5 -0
  85. package/dist/tui/features/provider/hooks/use-provider-subscriptions.d.ts +6 -0
  86. package/dist/tui/features/provider/hooks/use-provider-subscriptions.js +24 -0
  87. package/dist/tui/providers/app-providers.js +2 -1
  88. package/dist/tui/utils/error-messages.js +6 -1
  89. package/oclif.manifest.json +132 -116
  90. package/package.json +1 -1
@@ -2,6 +2,18 @@
2
2
  * OpenAI Provider Module
3
3
  *
4
4
  * Direct access to GPT models via @ai-sdk/openai.
5
+ * Supports both standard OpenAI API and ChatGPT OAuth (Codex) endpoint.
5
6
  */
6
7
  import type { ProviderModule } from './types.js';
8
+ /**
9
+ * Custom fetch wrapper for the ChatGPT OAuth endpoint.
10
+ *
11
+ * The ChatGPT OAuth Responses API has stricter requirements than the standard
12
+ * OpenAI API — certain fields are required and others are rejected:
13
+ * - `instructions` is required (system prompt — defaults to empty)
14
+ * - `store` must be false
15
+ * - `max_output_tokens` is not supported (must be omitted)
16
+ * - `id` fields on input items are rejected
17
+ */
18
+ export declare function createChatGptOAuthFetch(): typeof globalThis.fetch;
7
19
  export declare const openaiProvider: ProviderModule;
@@ -2,16 +2,67 @@
2
2
  * OpenAI Provider Module
3
3
  *
4
4
  * Direct access to GPT models via @ai-sdk/openai.
5
+ * Supports both standard OpenAI API and ChatGPT OAuth (Codex) endpoint.
5
6
  */
6
7
  import { createOpenAI } from '@ai-sdk/openai';
8
+ import { CHATGPT_OAUTH_BASE_URL } from '../../../../shared/constants/oauth.js';
7
9
  import { AiSdkContentGenerator } from '../generators/ai-sdk-content-generator.js';
10
+ /**
11
+ * Custom fetch wrapper for the ChatGPT OAuth endpoint.
12
+ *
13
+ * The ChatGPT OAuth Responses API has stricter requirements than the standard
14
+ * OpenAI API — certain fields are required and others are rejected:
15
+ * - `instructions` is required (system prompt — defaults to empty)
16
+ * - `store` must be false
17
+ * - `max_output_tokens` is not supported (must be omitted)
18
+ * - `id` fields on input items are rejected
19
+ */
20
+ /* eslint-disable n/no-unsupported-features/node-builtins */
21
+ export function createChatGptOAuthFetch() {
22
+ return async (input, init) => {
23
+ if (init?.method === 'POST' && init.body) {
24
+ if (typeof init.body !== 'string') {
25
+ return globalThis.fetch(input, init);
26
+ }
27
+ let body;
28
+ try {
29
+ body = JSON.parse(init.body);
30
+ }
31
+ catch {
32
+ return globalThis.fetch(input, init);
33
+ }
34
+ if (!body.instructions) {
35
+ body.instructions = '';
36
+ }
37
+ body.store = false;
38
+ delete body.max_output_tokens;
39
+ if (Array.isArray(body.input)) {
40
+ for (const item of body.input) {
41
+ if (typeof item === 'object' && item !== null && 'id' in item) {
42
+ const record = item;
43
+ delete record.id;
44
+ }
45
+ }
46
+ }
47
+ init = { ...init, body: JSON.stringify(body) };
48
+ }
49
+ return globalThis.fetch(input, init);
50
+ };
51
+ }
52
+ /* eslint-enable n/no-unsupported-features/node-builtins */
8
53
  export const openaiProvider = {
9
54
  apiKeyUrl: 'https://platform.openai.com/api-keys',
10
55
  authType: 'api-key',
11
56
  baseUrl: 'https://api.openai.com/v1',
12
57
  category: 'popular',
13
58
  createGenerator(config) {
14
- const provider = createOpenAI({ apiKey: config.apiKey });
59
+ const useChatGptOAuth = config.baseUrl === CHATGPT_OAUTH_BASE_URL;
60
+ const provider = createOpenAI({
61
+ apiKey: config.apiKey ?? '',
62
+ baseURL: config.baseUrl,
63
+ fetch: useChatGptOAuth ? createChatGptOAuthFetch() : undefined,
64
+ headers: config.headers,
65
+ });
15
66
  return new AiSdkContentGenerator({
16
67
  model: provider.responses(config.model),
17
68
  });
@@ -3,7 +3,7 @@ import { randomUUID } from 'node:crypto';
3
3
  import { TransportStateEventNames } from '../../../server/core/domain/transport/index.js';
4
4
  import { extractCurateOperations } from '../../../server/utils/curate-result-parser.js';
5
5
  import { TaskEvents } from '../../../shared/transport/events/index.js';
6
- import { formatConnectionError, hasLeakedHandles, withDaemonRetry, } from '../../lib/daemon-client.js';
6
+ import { formatConnectionError, hasLeakedHandles, providerMissingMessage, withDaemonRetry, } from '../../lib/daemon-client.js';
7
7
  import { writeJsonResponse } from '../../lib/json-response.js';
8
8
  import { waitForTaskCompletion } from '../../lib/task-client.js';
9
9
  export default class Curate extends Command {
@@ -91,7 +91,7 @@ Bad examples:
91
91
  throw new Error('No provider connected. Run "brv providers connect byterover" to use the free built-in provider, or connect another provider.');
92
92
  }
93
93
  if (active.providerKeyMissing) {
94
- throw new Error(`${active.activeProvider} API key is missing from storage.\nPlease reconnect: brv providers connect ${active.activeProvider} --api-key <your-key>`);
94
+ throw new Error(providerMissingMessage(active.activeProvider, active.authMethod));
95
95
  }
96
96
  await this.submitTask({ client, content: resolvedContent, flags, format, projectRoot, taskType });
97
97
  }, {
@@ -1,5 +1,5 @@
1
1
  import { Args, Command, Flags } from '@oclif/core';
2
- import { ModelEvents, } from '../../../shared/transport/events/model-events.js';
2
+ import { ModelEvents } from '../../../shared/transport/events/model-events.js';
3
3
  import { ProviderEvents, } from '../../../shared/transport/events/provider-events.js';
4
4
  import { withDaemonRetry } from '../../lib/daemon-client.js';
5
5
  import { writeJsonResponse } from '../../lib/json-response.js';
@@ -42,7 +42,9 @@ export default class ModelSwitch extends Command {
42
42
  }
43
43
  }
44
44
  catch (error) {
45
- const errorMessage = error instanceof Error ? error.message : 'An unexpected error occurred while switching the model. Please try again.';
45
+ const errorMessage = error instanceof Error
46
+ ? error.message
47
+ : 'An unexpected error occurred while switching the model. Please try again.';
46
48
  if (format === 'json') {
47
49
  writeJsonResponse({ command: 'model switch', data: { error: errorMessage }, success: false });
48
50
  }
@@ -68,13 +70,22 @@ export default class ModelSwitch extends Command {
68
70
  }
69
71
  else {
70
72
  const active = await client.requestWithAck(ProviderEvents.GET_ACTIVE);
73
+ if (!active.activeProviderId) {
74
+ throw new Error('No active provider configured. Run "brv providers connect <provider>" first.');
75
+ }
71
76
  providerId = active.activeProviderId;
72
77
  }
73
78
  if (providerId === 'byterover') {
74
79
  throw new Error('ByteRover provider uses its own internal LLM and does not support model switching. Run "brv providers switch <provider>" to switch to a different provider first.');
75
80
  }
76
81
  // 2. Switch active model
77
- await client.requestWithAck(ModelEvents.SET_ACTIVE, { modelId, providerId });
82
+ const response = await client.requestWithAck(ModelEvents.SET_ACTIVE, {
83
+ modelId,
84
+ providerId,
85
+ });
86
+ if (!response.success) {
87
+ throw new Error(response.error ?? 'Failed to switch model');
88
+ }
78
89
  return { modelId, providerId };
79
90
  }, options);
80
91
  }
@@ -9,8 +9,10 @@ export default class ProviderConnect extends Command {
9
9
  static flags: {
10
10
  'api-key': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
11
  'base-url': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
+ code: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
13
  format: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
13
14
  model: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
15
+ oauth: import("@oclif/core/interfaces").BooleanFlag<boolean>;
14
16
  };
15
17
  protected connectProvider({ apiKey, baseUrl, model, providerId }: {
16
18
  apiKey?: string;
@@ -22,5 +24,12 @@ export default class ProviderConnect extends Command {
22
24
  providerId: string;
23
25
  providerName: string;
24
26
  }>;
27
+ protected connectProviderOAuth({ code, providerId }: {
28
+ code?: string;
29
+ providerId: string;
30
+ }, options?: DaemonClientOptions, onProgress?: (msg: string) => void): Promise<{
31
+ providerName: string;
32
+ showInstructions: boolean;
33
+ }>;
25
34
  run(): Promise<void>;
26
35
  }
@@ -1,5 +1,6 @@
1
1
  import { Args, Command, Flags } from '@oclif/core';
2
- import { ModelEvents, } from '../../../shared/transport/events/model-events.js';
2
+ import { OAUTH_CALLBACK_TIMEOUT_MS } from '../../../shared/constants/oauth.js';
3
+ import { ModelEvents } from '../../../shared/transport/events/model-events.js';
3
4
  import { ProviderEvents, } from '../../../shared/transport/events/provider-events.js';
4
5
  import { withDaemonRetry } from '../../lib/daemon-client.js';
5
6
  import { writeJsonResponse } from '../../lib/json-response.js';
@@ -14,6 +15,7 @@ export default class ProviderConnect extends Command {
14
15
  static examples = [
15
16
  '<%= config.bin %> providers connect anthropic --api-key sk-xxx',
16
17
  '<%= config.bin %> providers connect openai --api-key sk-xxx --model gpt-4.1',
18
+ '<%= config.bin %> providers connect openai --oauth',
17
19
  '<%= config.bin %> providers connect byterover',
18
20
  '<%= config.bin %> providers connect openai-compatible --base-url http://localhost:11434/v1',
19
21
  '<%= config.bin %> providers connect openai-compatible --base-url http://localhost:11434/v1 --api-key sk-xxx --model llama3',
@@ -27,6 +29,12 @@ export default class ProviderConnect extends Command {
27
29
  char: 'b',
28
30
  description: 'Base URL for OpenAI-compatible providers (e.g., http://localhost:11434/v1)',
29
31
  }),
32
+ code: Flags.string({
33
+ char: 'c',
34
+ description: 'Authorization code for code-paste OAuth providers (e.g., Anthropic). ' +
35
+ 'Not applicable to browser-callback providers like OpenAI — use --oauth without --code instead.',
36
+ hidden: true,
37
+ }),
30
38
  format: Flags.string({
31
39
  default: 'text',
32
40
  description: 'Output format (text or json)',
@@ -36,6 +44,10 @@ export default class ProviderConnect extends Command {
36
44
  char: 'm',
37
45
  description: 'Model to set as active after connecting',
38
46
  }),
47
+ oauth: Flags.boolean({
48
+ default: false,
49
+ description: 'Connect via OAuth (browser-based)',
50
+ }),
39
51
  };
40
52
  async connectProvider({ apiKey, baseUrl, model, providerId }, options) {
41
53
  return withDaemonRetry(async (client) => {
@@ -48,8 +60,8 @@ export default class ProviderConnect extends Command {
48
60
  // 2. Validate base URL for openai-compatible
49
61
  if (providerId === 'openai-compatible') {
50
62
  if (!baseUrl && !provider.isConnected) {
51
- throw new Error('Provider "openai-compatible" requires a base URL. Use the --base-url flag to provide one.'
52
- + '\nExample: brv providers connect openai-compatible --base-url http://localhost:11434/v1');
63
+ throw new Error('Provider "openai-compatible" requires a base URL. Use the --base-url flag to provide one.' +
64
+ '\nExample: brv providers connect openai-compatible --base-url http://localhost:11434/v1');
53
65
  }
54
66
  if (baseUrl) {
55
67
  let parsed;
@@ -72,8 +84,8 @@ export default class ProviderConnect extends Command {
72
84
  }
73
85
  }
74
86
  else if (!apiKey && provider.requiresApiKey && !provider.isConnected) {
75
- throw new Error(`Provider "${providerId}" requires an API key. Use the --api-key flag to provide one.`
76
- + (provider.apiKeyUrl ? `\nDon't have one? Get your API key at: ${provider.apiKeyUrl}` : ''));
87
+ throw new Error(`Provider "${providerId}" requires an API key. Use the --api-key flag to provide one.` +
88
+ (provider.apiKeyUrl ? `\nDon't have one? Get your API key at: ${provider.apiKeyUrl}` : ''));
77
89
  }
78
90
  // 4. Connect or switch active provider
79
91
  const hasNewConfig = apiKey || baseUrl;
@@ -87,27 +99,111 @@ export default class ProviderConnect extends Command {
87
99
  return { model, providerId, providerName: provider.name };
88
100
  }, options);
89
101
  }
102
+ async connectProviderOAuth({ code, providerId }, options, onProgress) {
103
+ return withDaemonRetry(async (client) => {
104
+ // 1. Verify provider exists and supports OAuth
105
+ const { providers } = await client.requestWithAck(ProviderEvents.LIST);
106
+ const provider = providers.find((p) => p.id === providerId);
107
+ if (!provider) {
108
+ throw new Error(`Unknown provider "${providerId}". Run "brv providers list" to see available providers.`);
109
+ }
110
+ if (!provider.supportsOAuth) {
111
+ throw new Error(`Provider "${providerId}" does not support OAuth. Use --api-key instead.`);
112
+ }
113
+ // --code is only valid for code-paste providers (e.g., Anthropic).
114
+ // Browser-callback providers like OpenAI handle the code exchange automatically.
115
+ if (code && provider.oauthCallbackMode !== 'code-paste') {
116
+ throw new Error(`Provider "${providerId}" uses browser-based OAuth and does not accept --code.\n` +
117
+ `Run: brv providers connect ${providerId} --oauth`);
118
+ }
119
+ // If --code is provided, submit it directly (code-paste providers)
120
+ if (code) {
121
+ const response = await client.requestWithAck(ProviderEvents.SUBMIT_OAUTH_CODE, { code, providerId });
122
+ if (!response.success) {
123
+ throw new Error(response.error ?? 'OAuth code submission failed');
124
+ }
125
+ return { providerName: provider.name, showInstructions: false };
126
+ }
127
+ // 2. Start OAuth flow — returns immediately with auth URL
128
+ const startResponse = await client.requestWithAck(ProviderEvents.START_OAUTH, {
129
+ providerId,
130
+ });
131
+ if (!startResponse.success) {
132
+ throw new Error(startResponse.error ?? 'Failed to start OAuth flow');
133
+ }
134
+ // Always print auth URL (user's machine may not support browser launch)
135
+ onProgress?.(`\nOpen this URL to authenticate:\n ${startResponse.authUrl}\n`);
136
+ // 3. Handle based on callback mode
137
+ if (startResponse.callbackMode === 'auto') {
138
+ onProgress?.('Waiting for authentication in browser...');
139
+ const awaitResponse = await client.requestWithAck(ProviderEvents.AWAIT_OAUTH_CALLBACK, { providerId }, { timeout: OAUTH_CALLBACK_TIMEOUT_MS });
140
+ if (!awaitResponse.success) {
141
+ throw new Error(awaitResponse.error ?? 'OAuth authentication failed');
142
+ }
143
+ return { providerName: provider.name, showInstructions: false };
144
+ }
145
+ // code-paste mode: print instructions and exit
146
+ onProgress?.('Copy the authorization code from the browser and run:');
147
+ onProgress?.(` brv providers connect ${providerId} --oauth --code <code>`);
148
+ return { providerName: provider.name, showInstructions: true };
149
+ }, options);
150
+ }
90
151
  async run() {
91
152
  const { args, flags } = await this.parse(ProviderConnect);
92
153
  const providerId = args.provider;
93
154
  const apiKey = flags['api-key'];
94
155
  const baseUrl = flags['base-url'];
95
- const { model } = flags;
96
- const format = flags.format;
97
- try {
98
- const result = await this.connectProvider({ apiKey, baseUrl, model, providerId });
156
+ const { code, model, oauth } = flags;
157
+ const format = flags.format === 'json' ? 'json' : 'text';
158
+ // Validate flag combinations
159
+ if (oauth && apiKey) {
160
+ const msg = 'Cannot use --oauth and --api-key together';
161
+ if (format === 'json') {
162
+ writeJsonResponse({ command: 'providers connect', data: { error: msg }, success: false });
163
+ }
164
+ else {
165
+ this.log(msg);
166
+ }
167
+ return;
168
+ }
169
+ if (code && !oauth) {
170
+ const msg = '--code requires the --oauth flag';
99
171
  if (format === 'json') {
100
- writeJsonResponse({ command: 'providers connect', data: result, success: true });
172
+ writeJsonResponse({ command: 'providers connect', data: { error: msg }, success: false });
173
+ }
174
+ else {
175
+ this.log(msg);
176
+ }
177
+ return;
178
+ }
179
+ try {
180
+ if (oauth) {
181
+ const onProgress = format === 'text' ? (msg) => this.log(msg) : undefined;
182
+ const result = await this.connectProviderOAuth({ code, providerId }, undefined, onProgress);
183
+ if (format === 'json') {
184
+ writeJsonResponse({ command: 'providers connect', data: { providerId }, success: true });
185
+ }
186
+ else if (!result.showInstructions) {
187
+ this.log(`Connected to ${result.providerName} via OAuth`);
188
+ }
101
189
  }
102
190
  else {
103
- this.log(`Connected to ${result.providerName} (${result.providerId})`);
104
- if (result.model) {
105
- this.log(`Model set to: ${result.model}`);
191
+ const result = await this.connectProvider({ apiKey, baseUrl, model, providerId });
192
+ if (format === 'json') {
193
+ writeJsonResponse({ command: 'providers connect', data: result, success: true });
194
+ }
195
+ else {
196
+ this.log(`Connected to ${result.providerName} (${result.providerId})`);
197
+ if (result.model) {
198
+ this.log(`Model set to: ${result.model}`);
199
+ }
106
200
  }
107
201
  }
108
202
  }
109
203
  catch (error) {
110
- const errorMessage = error instanceof Error ? error.message : 'An unexpected error occurred while connecting the provider. Please try again.';
204
+ const errorMessage = error instanceof Error
205
+ ? error.message
206
+ : 'An unexpected error occurred while connecting the provider. Please try again.';
111
207
  if (format === 'json') {
112
208
  writeJsonResponse({ command: 'providers connect', data: { error: errorMessage }, success: false });
113
209
  }
@@ -5,10 +5,7 @@ import { formatConnectionError, withDaemonRetry } from '../../lib/daemon-client.
5
5
  import { writeJsonResponse } from '../../lib/json-response.js';
6
6
  export default class ProviderList extends Command {
7
7
  static description = 'List all available providers and their connection status';
8
- static examples = [
9
- '<%= config.bin %> providers list',
10
- '<%= config.bin %> providers list --format json',
11
- ];
8
+ static examples = ['<%= config.bin %> providers list', '<%= config.bin %> providers list --format json'];
12
9
  static flags = {
13
10
  format: Flags.string({
14
11
  default: 'text',
@@ -30,7 +27,8 @@ export default class ProviderList extends Command {
30
27
  }
31
28
  for (const p of providers) {
32
29
  const status = p.isCurrent ? chalk.green('(current)') : p.isConnected ? chalk.yellow('(connected)') : '';
33
- this.log(` ${p.name} [${p.id}] ${status}`.trimEnd());
30
+ const authBadge = p.authMethod === 'oauth' ? chalk.cyan('[OAuth]') : p.authMethod === 'api-key' ? chalk.dim('[API Key]') : '';
31
+ this.log(` ${p.name} [${p.id}] ${status} ${authBadge}`.trimEnd());
34
32
  }
35
33
  }
36
34
  catch (error) {
@@ -2,7 +2,7 @@ import { Args, Command, Flags } from '@oclif/core';
2
2
  import { randomUUID } from 'node:crypto';
3
3
  import { TransportStateEventNames } from '../../server/core/domain/transport/schemas.js';
4
4
  import { TaskEvents } from '../../shared/transport/events/index.js';
5
- import { formatConnectionError, hasLeakedHandles, withDaemonRetry, } from '../lib/daemon-client.js';
5
+ import { formatConnectionError, hasLeakedHandles, providerMissingMessage, withDaemonRetry, } from '../lib/daemon-client.js';
6
6
  import { writeJsonResponse } from '../lib/json-response.js';
7
7
  import { waitForTaskCompletion } from '../lib/task-client.js';
8
8
  export default class Query extends Command {
@@ -54,7 +54,7 @@ Bad:
54
54
  throw new Error('No provider connected. Run "brv providers connect byterover" to use the free built-in provider, or connect another provider.');
55
55
  }
56
56
  if (active.providerKeyMissing) {
57
- throw new Error(`${active.activeProvider} API key is missing from storage.\nPlease reconnect: brv providers connect ${active.activeProvider} --api-key <your-key>`);
57
+ throw new Error(providerMissingMessage(active.activeProvider, active.authMethod));
58
58
  }
59
59
  await this.submitTask({ client, format, projectRoot, query: args.query });
60
60
  }, {
@@ -30,6 +30,10 @@ export declare function isRetryableError(error: unknown): boolean;
30
30
  * Checks if an error left leaked Socket.IO handles that prevent Node.js from exiting.
31
31
  */
32
32
  export declare function hasLeakedHandles(error: unknown): boolean;
33
+ /**
34
+ * Builds a user-friendly message when provider credentials are missing from storage.
35
+ */
36
+ export declare function providerMissingMessage(activeProvider: string, authMethod?: 'api-key' | 'oauth'): string;
33
37
  export interface ProviderErrorContext {
34
38
  activeModel?: string;
35
39
  activeProvider?: string;
@@ -12,6 +12,8 @@ const USER_FRIENDLY_MESSAGES = {
12
12
  [TaskErrorCode.CONTEXT_TREE_NOT_INITIALIZED]: 'Context tree not initialized.',
13
13
  [TaskErrorCode.LOCAL_CHANGES_EXIST]: 'You have local changes. Run "brv push" to save your changes before pulling.',
14
14
  [TaskErrorCode.NOT_AUTHENTICATED]: 'Not authenticated. Cloud sync features (push/pull/space) require login — local query and curate work without authentication.',
15
+ [TaskErrorCode.OAUTH_REFRESH_FAILED]: 'OAuth token refresh failed. Run "brv providers connect <provider> --oauth" to reconnect.',
16
+ [TaskErrorCode.OAUTH_TOKEN_EXPIRED]: 'OAuth token has expired. Run "brv providers connect <provider> --oauth" to reconnect.',
15
17
  [TaskErrorCode.PROJECT_NOT_INIT]: 'Project not initialized. Run "brv restart" to reinitialize.',
16
18
  [TaskErrorCode.PROVIDER_NOT_CONFIGURED]: 'No provider connected. Run "brv providers connect byterover" to use the free built-in provider, or connect another provider.',
17
19
  [TaskErrorCode.SPACE_NOT_CONFIGURED]: 'No space configured. Run "brv space list" to see available spaces, then "brv space switch --team <team> --name <space>" to select one.',
@@ -83,6 +85,14 @@ export function hasLeakedHandles(error) {
83
85
  return false;
84
86
  return error.code === TaskErrorCode.AGENT_DISCONNECTED || error.code === TaskErrorCode.AGENT_NOT_AVAILABLE;
85
87
  }
88
+ /**
89
+ * Builds a user-friendly message when provider credentials are missing from storage.
90
+ */
91
+ export function providerMissingMessage(activeProvider, authMethod) {
92
+ return authMethod === 'oauth'
93
+ ? `${activeProvider} authentication has expired.\nPlease reconnect: brv providers connect ${activeProvider} --oauth`
94
+ : `${activeProvider} API key is missing from storage.\nPlease reconnect: brv providers connect ${activeProvider} --api-key <your-key>`;
95
+ }
86
96
  /**
87
97
  * Formats a connection error into a user-friendly message.
88
98
  */
@@ -131,9 +141,9 @@ export function formatConnectionError(error, providerContext) {
131
141
  const provider = providerContext?.activeProvider ?? '<provider>';
132
142
  const model = providerContext?.activeModel;
133
143
  const currentInfo = model ? `Provider: ${provider} Model: ${model}\n\n` : `Provider: ${provider}\n\n`;
134
- return (`LLM provider API key is missing or invalid.\n${currentInfo}` +
135
- ' Reconnect with your API key:\n' +
136
- ` brv providers connect ${provider} --api-key <key>\n\n` +
144
+ return (`LLM provider credentials are missing or invalid.\n${currentInfo}` +
145
+ ' Reconnect your provider:\n' +
146
+ ` brv providers connect ${provider}\n\n` +
137
147
  ' Switch to a different provider:\n' +
138
148
  ' brv providers switch <provider>\n\n' +
139
149
  ' See all options: brv providers --help');
@@ -12,12 +12,16 @@ export interface ConnectedProviderConfig {
12
12
  readonly activeModel?: string;
13
13
  /** Context window size of the active model (from provider API, e.g. OpenRouter) */
14
14
  readonly activeModelContextLength?: number;
15
+ /** How this provider was authenticated */
16
+ readonly authMethod?: 'api-key' | 'oauth';
15
17
  /** Custom API base URL (for openai-compatible provider) */
16
18
  readonly baseUrl?: string;
17
19
  /** When the provider was connected */
18
20
  readonly connectedAt: string;
19
21
  /** User's favorite models (for quick access) */
20
22
  readonly favoriteModels: readonly string[];
23
+ /** OAuth account ID (e.g. ChatGPT-Account-Id for OpenAI) */
24
+ readonly oauthAccountId?: string;
21
25
  /** Recently used models (last 10) */
22
26
  readonly recentModels: readonly string[];
23
27
  }
@@ -96,7 +100,9 @@ export declare class ProviderConfig {
96
100
  */
97
101
  withProviderConnected(providerId: string, options?: {
98
102
  activeModel?: string;
103
+ authMethod?: 'api-key' | 'oauth';
99
104
  baseUrl?: string;
105
+ oauthAccountId?: string;
100
106
  }): ProviderConfig;
101
107
  /**
102
108
  * Create a new config with a provider disconnected.
@@ -10,10 +10,9 @@
10
10
  const isProviderConfigJson = (json) => {
11
11
  if (typeof json !== 'object' || json === null)
12
12
  return false;
13
- const obj = json;
14
- if (typeof obj.activeProvider !== 'string')
13
+ if (!('activeProvider' in json) || typeof json.activeProvider !== 'string')
15
14
  return false;
16
- if (typeof obj.providers !== 'object' || obj.providers === null)
15
+ if (!('providers' in json) || typeof json.providers !== 'object' || json.providers === null)
17
16
  return false;
18
17
  return true;
19
18
  };
@@ -164,9 +163,11 @@ export class ProviderConfig {
164
163
  const existingConfig = this.providers[providerId];
165
164
  const newProviderConfig = {
166
165
  activeModel: options?.activeModel ?? existingConfig?.activeModel,
166
+ authMethod: options?.authMethod ?? existingConfig?.authMethod,
167
167
  baseUrl: options?.baseUrl ?? existingConfig?.baseUrl,
168
168
  connectedAt: existingConfig?.connectedAt ?? new Date().toISOString(),
169
169
  favoriteModels: existingConfig?.favoriteModels ?? [],
170
+ oauthAccountId: options?.oauthAccountId ?? existingConfig?.oauthAccountId,
170
171
  recentModels: existingConfig?.recentModels ?? [],
171
172
  };
172
173
  return new ProviderConfig({
@@ -4,6 +4,44 @@
4
4
  * Defines available LLM providers that can be connected to byterover-cli.
5
5
  * Inspired by OpenCode's provider system.
6
6
  */
7
+ /**
8
+ * Configuration for a single OAuth authentication mode.
9
+ */
10
+ export interface OAuthModeConfig {
11
+ /** Auth URL for this mode */
12
+ readonly authUrl: string;
13
+ /** Mode identifier (e.g. 'default', 'pro-max') */
14
+ readonly id: string;
15
+ /** Display label (e.g. "Sign in with OpenAI") */
16
+ readonly label: string;
17
+ }
18
+ /**
19
+ * OAuth configuration for a provider.
20
+ */
21
+ export interface ProviderOAuthConfig {
22
+ /** How the callback is received: local server ('auto') or user pastes code ('code-paste') */
23
+ readonly callbackMode: 'auto' | 'code-paste';
24
+ /** Port for local callback server (auto mode only) */
25
+ readonly callbackPort?: number;
26
+ /** OAuth client ID */
27
+ readonly clientId: string;
28
+ /** Whether to add `code=true` query param to auth URL (code-paste mode only — tells server to display paste-able code) */
29
+ readonly codeDisplay?: boolean;
30
+ /** Default model when connected via OAuth (overrides ProviderDefinition.defaultModel) */
31
+ readonly defaultModel?: string;
32
+ /** Extra query params appended to the authorization URL (provider-specific) */
33
+ readonly extraParams?: Readonly<Record<string, string>>;
34
+ /** Supported OAuth modes (some providers have multiple) */
35
+ readonly modes: readonly OAuthModeConfig[];
36
+ /** OAuth redirect URI */
37
+ readonly redirectUri: string;
38
+ /** OAuth scopes */
39
+ readonly scopes: string;
40
+ /** Token endpoint content type: OpenAI = 'form', Anthropic = 'json' */
41
+ readonly tokenContentType: 'form' | 'json';
42
+ /** Token exchange endpoint */
43
+ readonly tokenUrl: string;
44
+ }
7
45
  /**
8
46
  * Definition for an LLM provider.
9
47
  */
@@ -28,6 +66,8 @@ export interface ProviderDefinition {
28
66
  readonly modelsEndpoint: string;
29
67
  /** Display name */
30
68
  readonly name: string;
69
+ /** OAuth configuration (only for OAuth-capable providers) */
70
+ readonly oauth?: ProviderOAuthConfig;
31
71
  /** Priority for display order (lower = higher priority) */
32
72
  readonly priority: number;
33
73
  }
@@ -54,4 +94,4 @@ export declare function getProviderById(id: string): ProviderDefinition | undefi
54
94
  /**
55
95
  * Check if a provider requires an API key.
56
96
  */
57
- export declare function providerRequiresApiKey(id: string): boolean;
97
+ export declare function providerRequiresApiKey(id: string, authMethod?: 'api-key' | 'oauth'): boolean;
@@ -4,6 +4,7 @@
4
4
  * Defines available LLM providers that can be connected to byterover-cli.
5
5
  * Inspired by OpenCode's provider system.
6
6
  */
7
+ import { CHATGPT_OAUTH_ORIGINATOR } from '../../../../shared/constants/oauth.js';
7
8
  /**
8
9
  * Registry of all available providers.
9
10
  * Order by priority for consistent display.
@@ -160,6 +161,26 @@ export const PROVIDER_REGISTRY = {
160
161
  id: 'openai',
161
162
  modelsEndpoint: '/models',
162
163
  name: 'OpenAI',
164
+ oauth: {
165
+ callbackMode: 'auto',
166
+ callbackPort: 1455,
167
+ // Public OAuth client ID (safe to commit — native app public client, no client secret)
168
+ clientId: 'app_EMoamEEZ73f0CkXaXp7hrann',
169
+ // OpenAI Codex model used for the ChatGPT OAuth (Codex CLI) flow
170
+ defaultModel: 'gpt-5.1-codex-mini',
171
+ /* eslint-disable camelcase -- OAuth query params follow RFC 6749 naming */
172
+ extraParams: {
173
+ codex_cli_simplified_flow: 'true',
174
+ id_token_add_organizations: 'true',
175
+ originator: CHATGPT_OAUTH_ORIGINATOR,
176
+ },
177
+ /* eslint-enable camelcase */
178
+ modes: [{ authUrl: 'https://auth.openai.com/oauth/authorize', id: 'default', label: 'Sign in with OpenAI' }],
179
+ redirectUri: 'http://localhost:1455/auth/callback',
180
+ scopes: 'openid profile email offline_access',
181
+ tokenContentType: 'form',
182
+ tokenUrl: 'https://auth.openai.com/oauth/token',
183
+ },
163
184
  priority: 3,
164
185
  },
165
186
  'openai-compatible': {
@@ -268,7 +289,9 @@ export function getProviderById(id) {
268
289
  /**
269
290
  * Check if a provider requires an API key.
270
291
  */
271
- export function providerRequiresApiKey(id) {
292
+ export function providerRequiresApiKey(id, authMethod) {
293
+ if (authMethod === 'oauth')
294
+ return false;
272
295
  const provider = getProviderById(id);
273
296
  if (!provider)
274
297
  return false;
@@ -11,6 +11,8 @@ export declare const TaskErrorCode: {
11
11
  readonly LLM_RATE_LIMIT: "ERR_LLM_RATE_LIMIT";
12
12
  readonly LOCAL_CHANGES_EXIST: "ERR_LOCAL_CHANGES_EXIST";
13
13
  readonly NOT_AUTHENTICATED: "ERR_NOT_AUTHENTICATED";
14
+ readonly OAUTH_REFRESH_FAILED: "ERR_OAUTH_REFRESH_FAILED";
15
+ readonly OAUTH_TOKEN_EXPIRED: "ERR_OAUTH_TOKEN_EXPIRED";
14
16
  readonly PROJECT_NOT_INIT: "ERR_PROJECT_NOT_INIT";
15
17
  readonly PROVIDER_NOT_CONFIGURED: "ERR_PROVIDER_NOT_CONFIGURED";
16
18
  readonly SPACE_NOT_CONFIGURED: "ERR_SPACE_NOT_CONFIGURED";
@@ -15,6 +15,9 @@ export const TaskErrorCode = {
15
15
  LOCAL_CHANGES_EXIST: 'ERR_LOCAL_CHANGES_EXIST',
16
16
  // Auth/Init errors
17
17
  NOT_AUTHENTICATED: 'ERR_NOT_AUTHENTICATED',
18
+ // OAuth errors
19
+ OAUTH_REFRESH_FAILED: 'ERR_OAUTH_REFRESH_FAILED',
20
+ OAUTH_TOKEN_EXPIRED: 'ERR_OAUTH_TOKEN_EXPIRED',
18
21
  // Execution errors
19
22
  PROJECT_NOT_INIT: 'ERR_PROJECT_NOT_INIT',
20
23
  PROVIDER_NOT_CONFIGURED: 'ERR_PROVIDER_NOT_CONFIGURED',
@@ -100,7 +103,9 @@ export class AgentDisconnectedError extends TaskError {
100
103
  }
101
104
  export class AgentNotInitializedError extends TaskError {
102
105
  constructor(reason) {
103
- super(reason ? `Agent failed to initialize: ${reason}` : "Agent failed to initialize. Run 'brv restart' to force a clean restart.", TaskErrorCode.AGENT_NOT_INITIALIZED);
106
+ super(reason
107
+ ? `Agent failed to initialize: ${reason}`
108
+ : "Agent failed to initialize. Run 'brv restart' to force a clean restart.", TaskErrorCode.AGENT_NOT_INITIALIZED);
104
109
  this.name = 'AgentNotInitializedError';
105
110
  }
106
111
  }
@@ -463,6 +463,8 @@ export declare const TransportDaemonEventNames: {
463
463
  export interface ProviderConfigResponse {
464
464
  activeModel?: string;
465
465
  activeProvider: string;
466
+ /** How the provider was authenticated ('api-key' | 'oauth'). Undefined for internal providers. */
467
+ authMethod?: 'api-key' | 'oauth';
466
468
  maxInputTokens?: number;
467
469
  openRouterApiKey?: string;
468
470
  provider?: string;