byterover-cli 2.2.0 → 2.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) 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/commands/restart.d.ts +34 -50
  10. package/dist/oclif/commands/restart.js +122 -209
  11. package/dist/oclif/hooks/init/block-command-update-npm.d.ts +11 -0
  12. package/dist/oclif/hooks/init/block-command-update-npm.js +15 -0
  13. package/dist/oclif/hooks/init/update-notifier.d.ts +3 -0
  14. package/dist/oclif/hooks/init/update-notifier.js +17 -4
  15. package/dist/oclif/hooks/postrun/restart-after-update.d.ts +22 -0
  16. package/dist/oclif/hooks/postrun/restart-after-update.js +40 -0
  17. package/dist/oclif/lib/daemon-client.d.ts +4 -0
  18. package/dist/oclif/lib/daemon-client.js +13 -3
  19. package/dist/server/core/domain/entities/provider-config.d.ts +6 -0
  20. package/dist/server/core/domain/entities/provider-config.js +4 -3
  21. package/dist/server/core/domain/entities/provider-registry.d.ts +41 -1
  22. package/dist/server/core/domain/entities/provider-registry.js +24 -1
  23. package/dist/server/core/domain/errors/task-error.d.ts +2 -0
  24. package/dist/server/core/domain/errors/task-error.js +6 -1
  25. package/dist/server/core/domain/transport/schemas.d.ts +2 -0
  26. package/dist/server/core/interfaces/i-provider-config-store.d.ts +2 -0
  27. package/dist/server/core/interfaces/i-provider-model-fetcher.d.ts +12 -3
  28. package/dist/server/core/interfaces/i-provider-oauth-token-store.d.ts +24 -0
  29. package/dist/server/core/interfaces/i-provider-oauth-token-store.js +1 -0
  30. package/dist/server/core/interfaces/i-token-refresh-manager.d.ts +7 -0
  31. package/dist/server/core/interfaces/i-token-refresh-manager.js +1 -0
  32. package/dist/server/infra/daemon/agent-process.js +22 -4
  33. package/dist/server/infra/daemon/brv-server.js +13 -2
  34. package/dist/server/infra/http/models-dev-client.d.ts +29 -0
  35. package/dist/server/infra/http/models-dev-client.js +133 -0
  36. package/dist/server/infra/http/openrouter-api-client.js +1 -1
  37. package/dist/server/infra/http/provider-model-fetcher-registry.d.ts +2 -1
  38. package/dist/server/infra/http/provider-model-fetcher-registry.js +6 -1
  39. package/dist/server/infra/http/provider-model-fetchers.d.ts +33 -8
  40. package/dist/server/infra/http/provider-model-fetchers.js +88 -10
  41. package/dist/server/infra/process/feature-handlers.d.ts +3 -1
  42. package/dist/server/infra/process/feature-handlers.js +3 -1
  43. package/dist/server/infra/provider/provider-config-resolver.d.ts +4 -2
  44. package/dist/server/infra/provider/provider-config-resolver.js +59 -4
  45. package/dist/server/infra/provider-oauth/callback-server.d.ts +24 -0
  46. package/dist/server/infra/provider-oauth/callback-server.js +203 -0
  47. package/dist/server/infra/provider-oauth/errors.d.ts +39 -0
  48. package/dist/server/infra/provider-oauth/errors.js +76 -0
  49. package/dist/server/infra/provider-oauth/index.d.ts +9 -0
  50. package/dist/server/infra/provider-oauth/index.js +9 -0
  51. package/dist/server/infra/provider-oauth/jwt-utils.d.ts +17 -0
  52. package/dist/server/infra/provider-oauth/jwt-utils.js +51 -0
  53. package/dist/server/infra/provider-oauth/pkce-service.d.ts +22 -0
  54. package/dist/server/infra/provider-oauth/pkce-service.js +33 -0
  55. package/dist/server/infra/provider-oauth/provider-oauth-token-store.d.ts +48 -0
  56. package/dist/server/infra/provider-oauth/provider-oauth-token-store.js +155 -0
  57. package/dist/server/infra/provider-oauth/refresh-token-exchange.d.ts +8 -0
  58. package/dist/server/infra/provider-oauth/refresh-token-exchange.js +39 -0
  59. package/dist/server/infra/provider-oauth/token-exchange.d.ts +8 -0
  60. package/dist/server/infra/provider-oauth/token-exchange.js +44 -0
  61. package/dist/server/infra/provider-oauth/token-refresh-manager.d.ts +32 -0
  62. package/dist/server/infra/provider-oauth/token-refresh-manager.js +96 -0
  63. package/dist/server/infra/provider-oauth/types.d.ts +55 -0
  64. package/dist/server/infra/provider-oauth/types.js +22 -0
  65. package/dist/server/infra/storage/file-provider-config-store.d.ts +2 -0
  66. package/dist/server/infra/storage/file-provider-config-store.js +1 -3
  67. package/dist/server/infra/transport/handlers/model-handler.d.ts +3 -0
  68. package/dist/server/infra/transport/handlers/model-handler.js +53 -11
  69. package/dist/server/infra/transport/handlers/provider-handler.d.ts +26 -0
  70. package/dist/server/infra/transport/handlers/provider-handler.js +215 -13
  71. package/dist/shared/constants/oauth.d.ts +14 -0
  72. package/dist/shared/constants/oauth.js +14 -0
  73. package/dist/shared/transport/events/index.d.ts +5 -0
  74. package/dist/shared/transport/events/model-events.d.ts +2 -0
  75. package/dist/shared/transport/events/provider-events.d.ts +36 -0
  76. package/dist/shared/transport/events/provider-events.js +5 -0
  77. package/dist/shared/transport/types/dto.d.ts +4 -0
  78. package/dist/tui/features/model/api/set-active-model.d.ts +1 -1
  79. package/dist/tui/features/model/api/set-active-model.js +12 -4
  80. package/dist/tui/features/provider/api/await-oauth-callback.d.ts +11 -0
  81. package/dist/tui/features/provider/api/await-oauth-callback.js +25 -0
  82. package/dist/tui/features/provider/api/cancel-oauth.d.ts +5 -0
  83. package/dist/tui/features/provider/api/cancel-oauth.js +10 -0
  84. package/dist/tui/features/provider/api/start-oauth.d.ts +11 -0
  85. package/dist/tui/features/provider/api/start-oauth.js +15 -0
  86. package/dist/tui/features/provider/components/auth-method-dialog.d.ts +9 -0
  87. package/dist/tui/features/provider/components/auth-method-dialog.js +20 -0
  88. package/dist/tui/features/provider/components/oauth-dialog.d.ts +9 -0
  89. package/dist/tui/features/provider/components/oauth-dialog.js +96 -0
  90. package/dist/tui/features/provider/components/provider-dialog.js +1 -1
  91. package/dist/tui/features/provider/components/provider-flow.js +54 -4
  92. package/dist/tui/features/provider/components/provider-subscription-initializer.d.ts +1 -0
  93. package/dist/tui/features/provider/components/provider-subscription-initializer.js +5 -0
  94. package/dist/tui/features/provider/hooks/use-provider-subscriptions.d.ts +6 -0
  95. package/dist/tui/features/provider/hooks/use-provider-subscriptions.js +24 -0
  96. package/dist/tui/providers/app-providers.js +2 -1
  97. package/dist/tui/utils/error-messages.js +6 -1
  98. package/oclif.manifest.json +191 -168
  99. package/package.json +7 -5
@@ -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
  }, {
@@ -1,10 +1,13 @@
1
- import { type EnsureDaemonResult } from '@campfirein/brv-transport-client';
2
1
  import { Command } from '@oclif/core';
3
2
  export default class Restart extends Command {
4
3
  static description: string;
5
4
  static examples: string[];
5
+ /** Commands whose processes must not be killed (e.g. `brv update` calls `brv restart`). */
6
+ private static readonly PROTECTED_COMMANDS;
7
+ /** Server/agent patterns — cannot match CLI processes, no self-kill risk. */
8
+ private static readonly SERVER_AGENT_PATTERNS;
6
9
  /**
7
- * Builds the list of file-path patterns used to identify brv processes for pattern kill.
10
+ * Builds the list of CLI script patterns used to identify brv client processes.
8
11
  *
9
12
  * All patterns are absolute paths or specific filenames to avoid false-positive matches
10
13
  * against other oclif CLIs (which also use bin/run.js and bin/dev.js conventions).
@@ -17,32 +20,14 @@ export default class Restart extends Command {
17
20
  * nvm / system global: cmdline = node .../bin/brv ← caught by 'bin/brv' substring
18
21
  * curl install (/.brv-cli/): join(brvBinDir, 'run') — entry point named 'run' without .js
19
22
  *
20
- * Relative patterns (./bin/run.js, ./bin/dev.js) are intentionally excluded: they would
21
- * match any oclif CLI running in dev mode, not just brv.
22
- *
23
23
  * Set deduplicates when paths overlap (e.g. process.argv[1] is already run.js).
24
24
  */
25
- static buildKillPatterns(brvBinDir: string, argv1: string): string[];
26
- /**
27
- * Build a pid→cwd map from `lsof -d cwd -Fn` output.
28
- *
29
- * On macOS, `-p <pid>` is ignored and lsof returns ALL processes.
30
- * Output format per process: `p<pid>\nfcwd\nn<cwd_path>`.
31
- * Returns empty map if lsof is unavailable.
32
- */
33
- private static buildCwdByPid;
25
+ static buildCliPatterns(): string[];
34
26
  /**
35
- * Kill matching brv processes on macOS by scanning all processes via `ps`.
36
- *
37
- * For processes started with a relative path (e.g. `./bin/dev.js`), the literal
38
- * relative path is in the OS cmdline — absolute-path patterns won't match.
39
- * Resolves relative .js paths using buildCwdByPid() to avoid false positives
40
- * (e.g. `byterover-cli-clone/bin/dev.js` must not match `byterover-cli/bin/dev.js`).
41
- *
42
- * When cwd is resolved: check only absolute patterns (precise, no false positives).
43
- * When cwd is unavailable: also check relative fallback patterns (./bin/dev.js).
27
+ * Returns true if the cmdline contains a protected command as an argument.
28
+ * Handles both /proc null-byte delimiters (Linux) and space delimiters (macOS ps).
44
29
  */
45
- private static killByMacOsProcScan;
30
+ private static isProtectedCommand;
46
31
  /**
47
32
  * Kill a process by PID.
48
33
  * - Unix: SIGKILL via process.kill() — immediate, no graceful shutdown
@@ -51,46 +36,45 @@ export default class Restart extends Command {
51
36
  private static killByPid;
52
37
  /**
53
38
  * Kill matching brv processes on Linux by scanning /proc/<pid>/cmdline.
54
- *
55
- * For processes started with a relative path (e.g. `./bin/dev.js`), resolves
56
- * the path using /proc/<pid>/cwd so absolute-path patterns match correctly
57
- * without false positives.
58
- *
59
- * When cwd is resolved: check only absolute patterns (precise, no false positives).
60
- * When cwd is unavailable: also check relative fallback patterns (./bin/dev.js).
61
- * Mirrors the macOS killByMacOsProcScan behavior.
62
- *
63
- * Works on all Linux distros including Alpine — /proc is a kernel feature,
64
- * no userspace tools required.
39
+ * Simple substring match — no cwd resolution needed.
40
+ * Works on all Linux distros including Alpine /proc is a kernel feature.
65
41
  */
66
42
  private static killByProcScan;
67
43
  /**
68
- * Best-effort pattern kill for all brv processes (daemon, agents, TUI sessions, MCP servers,
69
- * headless commands). Errors are silently ignored.
44
+ * Kill matching brv processes on macOS by scanning all processes via `ps`.
45
+ * Simple substring match no cwd resolution needed because patterns
46
+ * are either unique filenames (brv-server.js) or absolute paths.
47
+ */
48
+ private static killByPsScan;
49
+ /**
50
+ * Pattern-kill brv processes matching the given patterns.
51
+ *
52
+ * Self-exclusion: own PID and parent PID are always filtered out.
53
+ * The parent PID exclusion protects the oclif bin/brv bash wrapper
54
+ * on bundled installs (it does not use exec, so bash remains as parent).
70
55
  *
71
- * Relative paths (e.g. `./bin/dev.js`) are resolved via cwd before pattern matching,
72
- * ensuring accuracy without false positives from other oclif CLIs.
56
+ * When skipProtected is true, processes running protected commands
57
+ * (e.g. `brv update`) are spared prevents `brv restart` from killing
58
+ * the `brv update` process that invoked it.
73
59
  *
74
60
  * OS dispatch:
75
- * Linux (incl. Alpine, WSL2): /proc scan + /proc/<pid>/cwd resolution
76
- * macOS: ps -A scan + lsof cwd resolution
61
+ * Linux (incl. Alpine, WSL2): /proc scan
62
+ * macOS: ps -A scan
77
63
  * Windows: PowerShell Get-CimInstance — available Windows 8+ / PS 3.0+
78
- *
79
- * Self-exclusion: own PID filtered on Unix; excluded explicitly in PowerShell query.
80
64
  */
81
65
  private static patternKill;
82
66
  private static sleep;
83
67
  /**
84
- * Polls until the process with the given PID is no longer alive.
68
+ * Polls until the process is dead, returning true if it exited within the timeout.
85
69
  * Uses `process.kill(pid, 0)` — sends no signal, just checks existence.
86
- * On ESRCH the PID is confirmed dead. Silently times out if the process
87
- * outlives timeoutMs (e.g. zombie held by parent).
88
- * Unix only — on Windows, taskkill /f is synchronous so no polling needed.
70
+ * On ESRCH the PID is confirmed dead.
89
71
  */
90
- private static waitForPidToDie;
72
+ private static waitForProcessExit;
91
73
  protected cleanupAllDaemonFiles(dataDir: string): void;
92
74
  protected exitProcess(code: number): void;
93
- protected killAllBrvProcesses(dataDir: string): Promise<void>;
75
+ protected loadDaemonInfo(dataDir: string): undefined | {
76
+ pid: number;
77
+ port: number;
78
+ };
94
79
  run(): Promise<void>;
95
- protected startDaemon(serverPath: string): Promise<EnsureDaemonResult>;
96
80
  }