byterover-cli 2.1.5 → 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 (110) 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/locations.d.ts +14 -0
  5. package/dist/oclif/commands/locations.js +68 -0
  6. package/dist/oclif/commands/model/switch.js +14 -3
  7. package/dist/oclif/commands/providers/connect.d.ts +9 -0
  8. package/dist/oclif/commands/providers/connect.js +110 -14
  9. package/dist/oclif/commands/providers/list.js +3 -5
  10. package/dist/oclif/commands/query.js +2 -2
  11. package/dist/oclif/commands/status.js +3 -3
  12. package/dist/oclif/lib/daemon-client.d.ts +4 -0
  13. package/dist/oclif/lib/daemon-client.js +13 -3
  14. package/dist/server/core/domain/entities/provider-config.d.ts +6 -0
  15. package/dist/server/core/domain/entities/provider-config.js +4 -3
  16. package/dist/server/core/domain/entities/provider-registry.d.ts +41 -1
  17. package/dist/server/core/domain/entities/provider-registry.js +24 -1
  18. package/dist/server/core/domain/errors/task-error.d.ts +2 -0
  19. package/dist/server/core/domain/errors/task-error.js +6 -1
  20. package/dist/server/core/domain/transport/schemas.d.ts +2 -0
  21. package/dist/server/core/interfaces/i-provider-config-store.d.ts +2 -0
  22. package/dist/server/core/interfaces/i-provider-model-fetcher.d.ts +12 -3
  23. package/dist/server/core/interfaces/i-provider-oauth-token-store.d.ts +24 -0
  24. package/dist/server/core/interfaces/i-provider-oauth-token-store.js +1 -0
  25. package/dist/server/core/interfaces/i-token-refresh-manager.d.ts +7 -0
  26. package/dist/server/core/interfaces/i-token-refresh-manager.js +1 -0
  27. package/dist/server/infra/daemon/agent-process.js +22 -4
  28. package/dist/server/infra/daemon/brv-server.js +15 -2
  29. package/dist/server/infra/http/models-dev-client.d.ts +29 -0
  30. package/dist/server/infra/http/models-dev-client.js +133 -0
  31. package/dist/server/infra/http/provider-model-fetcher-registry.d.ts +2 -1
  32. package/dist/server/infra/http/provider-model-fetcher-registry.js +6 -1
  33. package/dist/server/infra/http/provider-model-fetchers.d.ts +33 -8
  34. package/dist/server/infra/http/provider-model-fetchers.js +88 -10
  35. package/dist/server/infra/process/feature-handlers.d.ts +6 -1
  36. package/dist/server/infra/process/feature-handlers.js +11 -2
  37. package/dist/server/infra/provider/provider-config-resolver.d.ts +4 -2
  38. package/dist/server/infra/provider/provider-config-resolver.js +59 -4
  39. package/dist/server/infra/provider-oauth/callback-server.d.ts +24 -0
  40. package/dist/server/infra/provider-oauth/callback-server.js +203 -0
  41. package/dist/server/infra/provider-oauth/errors.d.ts +39 -0
  42. package/dist/server/infra/provider-oauth/errors.js +76 -0
  43. package/dist/server/infra/provider-oauth/index.d.ts +9 -0
  44. package/dist/server/infra/provider-oauth/index.js +9 -0
  45. package/dist/server/infra/provider-oauth/jwt-utils.d.ts +17 -0
  46. package/dist/server/infra/provider-oauth/jwt-utils.js +51 -0
  47. package/dist/server/infra/provider-oauth/pkce-service.d.ts +22 -0
  48. package/dist/server/infra/provider-oauth/pkce-service.js +33 -0
  49. package/dist/server/infra/provider-oauth/provider-oauth-token-store.d.ts +48 -0
  50. package/dist/server/infra/provider-oauth/provider-oauth-token-store.js +155 -0
  51. package/dist/server/infra/provider-oauth/refresh-token-exchange.d.ts +8 -0
  52. package/dist/server/infra/provider-oauth/refresh-token-exchange.js +39 -0
  53. package/dist/server/infra/provider-oauth/token-exchange.d.ts +8 -0
  54. package/dist/server/infra/provider-oauth/token-exchange.js +44 -0
  55. package/dist/server/infra/provider-oauth/token-refresh-manager.d.ts +32 -0
  56. package/dist/server/infra/provider-oauth/token-refresh-manager.js +96 -0
  57. package/dist/server/infra/provider-oauth/types.d.ts +55 -0
  58. package/dist/server/infra/provider-oauth/types.js +22 -0
  59. package/dist/server/infra/storage/file-provider-config-store.d.ts +2 -0
  60. package/dist/server/infra/storage/file-provider-config-store.js +1 -3
  61. package/dist/server/infra/transport/handlers/index.d.ts +2 -0
  62. package/dist/server/infra/transport/handlers/index.js +1 -0
  63. package/dist/server/infra/transport/handlers/locations-handler.d.ts +25 -0
  64. package/dist/server/infra/transport/handlers/locations-handler.js +64 -0
  65. package/dist/server/infra/transport/handlers/model-handler.d.ts +3 -0
  66. package/dist/server/infra/transport/handlers/model-handler.js +53 -11
  67. package/dist/server/infra/transport/handlers/provider-handler.d.ts +26 -0
  68. package/dist/server/infra/transport/handlers/provider-handler.js +215 -13
  69. package/dist/server/templates/skill/SKILL.md +19 -1
  70. package/dist/shared/constants/oauth.d.ts +14 -0
  71. package/dist/shared/constants/oauth.js +14 -0
  72. package/dist/shared/transport/events/index.d.ts +8 -0
  73. package/dist/shared/transport/events/index.js +3 -0
  74. package/dist/shared/transport/events/locations-events.d.ts +7 -0
  75. package/dist/shared/transport/events/locations-events.js +3 -0
  76. package/dist/shared/transport/events/model-events.d.ts +2 -0
  77. package/dist/shared/transport/events/provider-events.d.ts +36 -0
  78. package/dist/shared/transport/events/provider-events.js +5 -0
  79. package/dist/shared/transport/types/dto.d.ts +15 -0
  80. package/dist/tui/features/commands/definitions/index.js +2 -0
  81. package/dist/tui/features/commands/definitions/locations.d.ts +2 -0
  82. package/dist/tui/features/commands/definitions/locations.js +11 -0
  83. package/dist/tui/features/locations/api/get-locations.d.ts +16 -0
  84. package/dist/tui/features/locations/api/get-locations.js +17 -0
  85. package/dist/tui/features/locations/components/locations-view.d.ts +3 -0
  86. package/dist/tui/features/locations/components/locations-view.js +25 -0
  87. package/dist/tui/features/locations/utils/format-locations.d.ts +2 -0
  88. package/dist/tui/features/locations/utils/format-locations.js +26 -0
  89. package/dist/tui/features/model/api/set-active-model.d.ts +1 -1
  90. package/dist/tui/features/model/api/set-active-model.js +12 -4
  91. package/dist/tui/features/provider/api/await-oauth-callback.d.ts +11 -0
  92. package/dist/tui/features/provider/api/await-oauth-callback.js +25 -0
  93. package/dist/tui/features/provider/api/cancel-oauth.d.ts +5 -0
  94. package/dist/tui/features/provider/api/cancel-oauth.js +10 -0
  95. package/dist/tui/features/provider/api/start-oauth.d.ts +11 -0
  96. package/dist/tui/features/provider/api/start-oauth.js +15 -0
  97. package/dist/tui/features/provider/components/auth-method-dialog.d.ts +9 -0
  98. package/dist/tui/features/provider/components/auth-method-dialog.js +20 -0
  99. package/dist/tui/features/provider/components/oauth-dialog.d.ts +9 -0
  100. package/dist/tui/features/provider/components/oauth-dialog.js +96 -0
  101. package/dist/tui/features/provider/components/provider-dialog.js +1 -1
  102. package/dist/tui/features/provider/components/provider-flow.js +54 -4
  103. package/dist/tui/features/provider/components/provider-subscription-initializer.d.ts +1 -0
  104. package/dist/tui/features/provider/components/provider-subscription-initializer.js +5 -0
  105. package/dist/tui/features/provider/hooks/use-provider-subscriptions.d.ts +6 -0
  106. package/dist/tui/features/provider/hooks/use-provider-subscriptions.js +24 -0
  107. package/dist/tui/providers/app-providers.js +2 -1
  108. package/dist/tui/utils/error-messages.js +6 -1
  109. package/oclif.manifest.json +56 -1
  110. package/package.json +1 -1
@@ -4,17 +4,29 @@ import { TransportDaemonEventNames } from '../../../core/domain/transport/schema
4
4
  import { getErrorMessage } from '../../../utils/error-helpers.js';
5
5
  import { processLog } from '../../../utils/process-logger.js';
6
6
  import { validateApiKey as validateApiKeyViaFetcher } from '../../http/provider-model-fetcher-registry.js';
7
+ import { computeExpiresAt, exchangeCodeForTokens as defaultExchangeCodeForTokens, generatePkce as defaultGeneratePkce, parseAccountIdFromIdToken, ProviderCallbackServer, ProviderCallbackTimeoutError, } from '../../provider-oauth/index.js';
7
8
  /**
8
9
  * Handles provider:* events.
9
10
  * Business logic for provider management — no terminal/UI calls.
10
11
  */
11
12
  export class ProviderHandler {
13
+ browserLauncher;
14
+ createCallbackServer;
15
+ exchangeCodeForTokens;
16
+ generatePkce;
17
+ oauthFlows = new Map();
12
18
  providerConfigStore;
13
19
  providerKeychainStore;
20
+ providerOAuthTokenStore;
14
21
  transport;
15
22
  constructor(deps) {
23
+ this.browserLauncher = deps.browserLauncher;
24
+ this.createCallbackServer = deps.createCallbackServer ?? ((options) => new ProviderCallbackServer(options));
25
+ this.exchangeCodeForTokens = deps.exchangeCodeForTokens ?? defaultExchangeCodeForTokens;
26
+ this.generatePkce = deps.generatePkce ?? defaultGeneratePkce;
16
27
  this.providerConfigStore = deps.providerConfigStore;
17
28
  this.providerKeychainStore = deps.providerKeychainStore;
29
+ this.providerOAuthTokenStore = deps.providerOAuthTokenStore;
18
30
  this.transport = deps.transport;
19
31
  }
20
32
  setup() {
@@ -24,6 +36,99 @@ export class ProviderHandler {
24
36
  this.setupList();
25
37
  this.setupSetActive();
26
38
  this.setupValidateApiKey();
39
+ this.setupStartOAuth();
40
+ this.setupAwaitOAuthCallback();
41
+ this.setupCancelOAuth();
42
+ this.setupSubmitOAuthCode();
43
+ // Clean up OAuth flows when a client disconnects (prevents callback server port leaks)
44
+ this.transport.onDisconnection((clientId) => {
45
+ this.cleanupFlowsForClient(clientId);
46
+ });
47
+ }
48
+ cleanupFlowsForClient(clientId) {
49
+ for (const [providerId, flow] of this.oauthFlows.entries()) {
50
+ if (flow.clientId === clientId) {
51
+ flow.callbackServer?.stop().catch(() => { });
52
+ this.oauthFlows.delete(providerId);
53
+ }
54
+ }
55
+ }
56
+ setupAwaitOAuthCallback() {
57
+ this.transport.onRequest(ProviderEvents.AWAIT_OAUTH_CALLBACK, async (data) => {
58
+ const flow = this.oauthFlows.get(data.providerId);
59
+ if (!flow?.callbackServer) {
60
+ return { error: 'No active OAuth flow for this provider', success: false };
61
+ }
62
+ if (flow.awaitInProgress) {
63
+ return { error: 'OAuth callback is already being awaited for this provider', success: false };
64
+ }
65
+ flow.awaitInProgress = true;
66
+ try {
67
+ // Block until callback or timeout (5 min default in ProviderCallbackServer)
68
+ const callbackResult = await flow.callbackServer.waitForCallback(flow.state);
69
+ // Exchange code for tokens
70
+ const providerDef = getProviderById(data.providerId);
71
+ if (!providerDef?.oauth) {
72
+ return { error: 'Provider does not support OAuth', success: false };
73
+ }
74
+ const oauthConfig = providerDef.oauth;
75
+ const contentType = oauthConfig.tokenContentType === 'form' ? 'application/x-www-form-urlencoded' : 'application/json';
76
+ const tokens = await this.exchangeCodeForTokens({
77
+ clientId: oauthConfig.clientId,
78
+ code: callbackResult.code,
79
+ codeVerifier: flow.codeVerifier,
80
+ contentType,
81
+ redirectUri: oauthConfig.redirectUri,
82
+ tokenUrl: oauthConfig.tokenUrl,
83
+ });
84
+ // Parse JWT id_token for account ID
85
+ const oauthAccountId = tokens.id_token ? parseAccountIdFromIdToken(tokens.id_token) : undefined;
86
+ // Store access token as the "API key" in keychain
87
+ await this.providerKeychainStore.setApiKey(data.providerId, tokens.access_token);
88
+ // Store refresh token + expiry in encrypted OAuth token store
89
+ if (tokens.refresh_token) {
90
+ const expiresAt = tokens.expires_in ? computeExpiresAt(tokens.expires_in) : computeExpiresAt(3600); // 1-hour default when provider omits expires_in
91
+ await this.providerOAuthTokenStore.set(data.providerId, {
92
+ expiresAt,
93
+ refreshToken: tokens.refresh_token,
94
+ });
95
+ }
96
+ // Connect provider — secrets stored in keychain + encrypted token store, not config
97
+ // OAuth providers may define their own default model (e.g., Codex for OpenAI OAuth)
98
+ const defaultModel = oauthConfig.defaultModel ?? providerDef.defaultModel;
99
+ await this.providerConfigStore.connectProvider(data.providerId, {
100
+ activeModel: defaultModel,
101
+ authMethod: 'oauth',
102
+ oauthAccountId,
103
+ });
104
+ // Broadcast update
105
+ this.transport.broadcast(TransportDaemonEventNames.PROVIDER_UPDATED, {});
106
+ return { success: true };
107
+ }
108
+ catch (error) {
109
+ if (error instanceof ProviderCallbackTimeoutError) {
110
+ return { error: 'Authentication timed out. Please try again.', success: false };
111
+ }
112
+ return { error: getErrorMessage(error), success: false };
113
+ }
114
+ finally {
115
+ // Only clean up if this is still the same flow (guard against concurrent START_OAUTH)
116
+ if (this.oauthFlows.get(data.providerId) === flow) {
117
+ await flow.callbackServer?.stop().catch(() => { });
118
+ this.oauthFlows.delete(data.providerId);
119
+ }
120
+ }
121
+ });
122
+ }
123
+ setupCancelOAuth() {
124
+ this.transport.onRequest(ProviderEvents.CANCEL_OAUTH, async (data) => {
125
+ const flow = this.oauthFlows.get(data.providerId);
126
+ if (flow?.callbackServer) {
127
+ await flow.callbackServer.stop().catch(() => { });
128
+ }
129
+ this.oauthFlows.delete(data.providerId);
130
+ return { success: true };
131
+ });
27
132
  }
28
133
  setupConnect() {
29
134
  this.transport.onRequest(ProviderEvents.CONNECT, async (data) => {
@@ -35,6 +140,7 @@ export class ProviderHandler {
35
140
  const provider = getProviderById(providerId);
36
141
  await this.providerConfigStore.connectProvider(providerId, {
37
142
  activeModel: provider?.defaultModel,
143
+ authMethod: apiKey ? 'api-key' : undefined,
38
144
  baseUrl,
39
145
  });
40
146
  this.transport.broadcast(TransportDaemonEventNames.PROVIDER_UPDATED, {});
@@ -46,6 +152,7 @@ export class ProviderHandler {
46
152
  const { providerId } = data;
47
153
  await this.providerConfigStore.disconnectProvider(providerId);
48
154
  await this.providerKeychainStore.deleteApiKey(providerId);
155
+ await this.providerOAuthTokenStore.delete(providerId);
49
156
  this.transport.broadcast(TransportDaemonEventNames.PROVIDER_UPDATED, {});
50
157
  return { success: true };
51
158
  });
@@ -64,19 +171,28 @@ export class ProviderHandler {
64
171
  processLog(`[ProviderHandler] getActiveProvider failed: ${error instanceof Error ? error.message : String(error)}`);
65
172
  return '';
66
173
  });
67
- const providers = await Promise.all(definitions.map(async (def) => ({
68
- apiKeyUrl: def.apiKeyUrl,
69
- category: def.category,
70
- description: def.description,
71
- id: def.id,
72
- isConnected: await this.providerConfigStore.isProviderConnected(def.id).catch((error) => {
73
- processLog(`[ProviderHandler] isProviderConnected failed for ${def.id}: ${error instanceof Error ? error.message : String(error)}`);
74
- return false;
75
- }),
76
- isCurrent: def.id === activeProviderId,
77
- name: def.name,
78
- requiresApiKey: providerRequiresApiKey(def.id),
79
- })));
174
+ const config = await this.providerConfigStore.read().catch(() => null);
175
+ const providers = await Promise.all(definitions.map(async (def) => {
176
+ const providerConfig = config?.providers[def.id];
177
+ const authMethod = providerConfig?.authMethod;
178
+ return {
179
+ apiKeyUrl: def.apiKeyUrl,
180
+ authMethod,
181
+ category: def.category,
182
+ description: def.description,
183
+ id: def.id,
184
+ isConnected: await this.providerConfigStore.isProviderConnected(def.id).catch((error) => {
185
+ processLog(`[ProviderHandler] isProviderConnected failed for ${def.id}: ${error instanceof Error ? error.message : String(error)}`);
186
+ return false;
187
+ }),
188
+ isCurrent: def.id === activeProviderId,
189
+ name: def.name,
190
+ oauthCallbackMode: def.oauth?.callbackMode,
191
+ oauthLabel: def.oauth?.modes[0]?.label,
192
+ requiresApiKey: providerRequiresApiKey(def.id, authMethod),
193
+ supportsOAuth: Boolean(def.oauth),
194
+ };
195
+ }));
80
196
  return { providers };
81
197
  });
82
198
  }
@@ -87,6 +203,92 @@ export class ProviderHandler {
87
203
  return { success: true };
88
204
  });
89
205
  }
206
+ /* eslint-disable camelcase -- OAuth query params follow RFC 6749 naming */
207
+ setupStartOAuth() {
208
+ this.transport.onRequest(ProviderEvents.START_OAUTH, async (data, clientId) => {
209
+ const providerDef = getProviderById(data.providerId);
210
+ if (!providerDef?.oauth) {
211
+ const errorResponse = {
212
+ authUrl: '',
213
+ callbackMode: 'auto',
214
+ error: 'Provider does not support OAuth',
215
+ success: false,
216
+ };
217
+ return errorResponse;
218
+ }
219
+ try {
220
+ const oauthConfig = providerDef.oauth;
221
+ // Clean up any existing flow for this provider (race condition guard)
222
+ const existingFlow = this.oauthFlows.get(data.providerId);
223
+ if (existingFlow?.callbackServer) {
224
+ await existingFlow.callbackServer.stop().catch(() => { });
225
+ }
226
+ this.oauthFlows.delete(data.providerId);
227
+ // Generate PKCE parameters
228
+ const pkce = this.generatePkce();
229
+ // Build auth URL
230
+ const mode = oauthConfig.modes.find((m) => m.id === (data.mode ?? 'default')) ?? oauthConfig.modes[0];
231
+ const params = new URLSearchParams({
232
+ client_id: oauthConfig.clientId,
233
+ code_challenge: pkce.codeChallenge,
234
+ code_challenge_method: 'S256',
235
+ redirect_uri: oauthConfig.redirectUri,
236
+ response_type: 'code',
237
+ scope: oauthConfig.scopes,
238
+ state: pkce.state,
239
+ });
240
+ // Provider-specific extra params (e.g. OpenAI's codex_cli_simplified_flow)
241
+ if (oauthConfig.extraParams) {
242
+ for (const [key, value] of Object.entries(oauthConfig.extraParams)) {
243
+ params.set(key, value);
244
+ }
245
+ }
246
+ const authUrl = `${mode.authUrl}?${params.toString()}`;
247
+ // Start callback server for auto mode
248
+ let callbackServer;
249
+ if (oauthConfig.callbackMode === 'auto' && oauthConfig.callbackPort) {
250
+ callbackServer = this.createCallbackServer({ port: oauthConfig.callbackPort });
251
+ await callbackServer.start();
252
+ }
253
+ // Store flow state
254
+ this.oauthFlows.set(data.providerId, {
255
+ callbackServer,
256
+ clientId,
257
+ codeVerifier: pkce.codeVerifier,
258
+ state: pkce.state,
259
+ });
260
+ // Open browser (non-fatal on failure)
261
+ try {
262
+ await this.browserLauncher.open(authUrl);
263
+ }
264
+ catch {
265
+ processLog(`[ProviderHandler] Browser launch failed for OAuth — user can copy the URL`);
266
+ }
267
+ return { authUrl, callbackMode: oauthConfig.callbackMode, success: true };
268
+ }
269
+ catch (error) {
270
+ // Clean up callback server if it was started but flow setup failed
271
+ const partialFlow = this.oauthFlows.get(data.providerId);
272
+ if (partialFlow?.callbackServer) {
273
+ await partialFlow.callbackServer.stop().catch(() => { });
274
+ }
275
+ this.oauthFlows.delete(data.providerId);
276
+ const errorResponse = {
277
+ authUrl: '',
278
+ callbackMode: 'auto',
279
+ error: getErrorMessage(error),
280
+ success: false,
281
+ };
282
+ return errorResponse;
283
+ }
284
+ });
285
+ }
286
+ /* eslint-enable camelcase */
287
+ setupSubmitOAuthCode() {
288
+ this.transport.onRequest(ProviderEvents.SUBMIT_OAUTH_CODE,
289
+ // Stub for M2 (Anthropic code-paste flow)
290
+ async () => ({ error: 'Code submission is not yet supported for this provider', success: false }));
291
+ }
90
292
  setupValidateApiKey() {
91
293
  this.transport.onRequest(ProviderEvents.VALIDATE_API_KEY, async (data) => {
92
294
  try {
@@ -93,7 +93,25 @@ brv providers list
93
93
  brv providers connect openai --api-key sk-xxx --model gpt-4.1
94
94
  ```
95
95
 
96
- ### 4. Cloud Sync (Optional)
96
+ ### 4. Project Locations
97
+ **Overview:** List registered projects and their context tree paths. Returns project metadata including initialization status and active state. Use `-f json` for machine-readable output.
98
+
99
+ **Use this when:**
100
+ - You need to find a project's context tree path
101
+ - You need to check which projects are registered
102
+ - You need to verify if a project is initialized
103
+
104
+ **Do NOT use this when:**
105
+ - You already know the project path from your current context
106
+ - You need project content rather than metadata — use `brv query` instead
107
+
108
+ ```bash
109
+ brv locations -f json
110
+ ```
111
+
112
+ JSON fields: `projectPath`, `contextTreePath`, `isCurrent`, `isActive`, `isInitialized`.
113
+
114
+ ### 5. Cloud Sync (Optional)
97
115
  **Overview:** Sync your local knowledge with a team via ByteRover's cloud service. Requires ByteRover authentication.
98
116
 
99
117
  **Setup steps:**
@@ -0,0 +1,14 @@
1
+ /**
2
+ * ChatGPT OAuth (Codex) API base URL — single source of truth.
3
+ * Used by the agent's OpenAI provider module and the server's provider config resolver.
4
+ */
5
+ export declare const CHATGPT_OAUTH_BASE_URL = "https://chatgpt.com/backend-api/codex";
6
+ /**
7
+ * Originator header/param value sent to OpenAI in OAuth flows.
8
+ */
9
+ export declare const CHATGPT_OAUTH_ORIGINATOR = "byterover";
10
+ /**
11
+ * OAuth callback timeout in milliseconds (5 minutes).
12
+ * Used by the callback server, TUI await-oauth-callback mutation, and CLI connect command.
13
+ */
14
+ export declare const OAUTH_CALLBACK_TIMEOUT_MS = 300000;
@@ -0,0 +1,14 @@
1
+ /**
2
+ * ChatGPT OAuth (Codex) API base URL — single source of truth.
3
+ * Used by the agent's OpenAI provider module and the server's provider config resolver.
4
+ */
5
+ export const CHATGPT_OAUTH_BASE_URL = 'https://chatgpt.com/backend-api/codex';
6
+ /**
7
+ * Originator header/param value sent to OpenAI in OAuth flows.
8
+ */
9
+ export const CHATGPT_OAUTH_ORIGINATOR = 'byterover';
10
+ /**
11
+ * OAuth callback timeout in milliseconds (5 minutes).
12
+ * Used by the callback server, TUI await-oauth-callback mutation, and CLI connect command.
13
+ */
14
+ export const OAUTH_CALLBACK_TIMEOUT_MS = 300_000;
@@ -6,6 +6,7 @@ export * from './connector-events.js';
6
6
  export * from './hub-events.js';
7
7
  export * from './init-events.js';
8
8
  export * from './llm-events.js';
9
+ export * from './locations-events.js';
9
10
  export * from './model-events.js';
10
11
  export * from './onboarding-events.js';
11
12
  export * from './provider-events.js';
@@ -85,11 +86,16 @@ export declare const AllEventGroups: readonly [{
85
86
  readonly COMPLETE: "onboarding:complete";
86
87
  readonly GET_STATE: "onboarding:getState";
87
88
  }, {
89
+ readonly AWAIT_OAUTH_CALLBACK: "provider:awaitOAuthCallback";
90
+ readonly CANCEL_OAUTH: "provider:cancelOAuth";
88
91
  readonly CONNECT: "provider:connect";
89
92
  readonly DISCONNECT: "provider:disconnect";
90
93
  readonly GET_ACTIVE: "provider:getActive";
91
94
  readonly LIST: "provider:list";
92
95
  readonly SET_ACTIVE: "provider:setActive";
96
+ readonly START_OAUTH: "provider:startOAuth";
97
+ readonly SUBMIT_OAUTH_CODE: "provider:submitOAuthCode";
98
+ readonly UPDATED: "provider:updated";
93
99
  readonly VALIDATE_API_KEY: "provider:validateApiKey";
94
100
  }, {
95
101
  readonly EXECUTE: "pull:execute";
@@ -103,6 +109,8 @@ export declare const AllEventGroups: readonly [{
103
109
  readonly EXECUTE: "reset:execute";
104
110
  }, {
105
111
  readonly SWITCHED: "session:switched";
112
+ }, {
113
+ readonly GET: "locations:get";
106
114
  }, {
107
115
  readonly LIST: "space:list";
108
116
  readonly SWITCH: "space:switch";
@@ -8,6 +8,7 @@ export * from './connector-events.js';
8
8
  export * from './hub-events.js';
9
9
  export * from './init-events.js';
10
10
  export * from './llm-events.js';
11
+ export * from './locations-events.js';
11
12
  export * from './model-events.js';
12
13
  export * from './onboarding-events.js';
13
14
  export * from './provider-events.js';
@@ -26,6 +27,7 @@ import { ConnectorEvents } from './connector-events.js';
26
27
  import { HubEvents } from './hub-events.js';
27
28
  import { InitEvents } from './init-events.js';
28
29
  import { LlmEvents } from './llm-events.js';
30
+ import { LocationsEvents } from './locations-events.js';
29
31
  import { ModelEvents } from './model-events.js';
30
32
  import { OnboardingEvents } from './onboarding-events.js';
31
33
  import { ProviderEvents } from './provider-events.js';
@@ -55,6 +57,7 @@ export const AllEventGroups = [
55
57
  PushEvents,
56
58
  ResetEvents,
57
59
  SessionEvents,
60
+ LocationsEvents,
58
61
  SpaceEvents,
59
62
  StatusEvents,
60
63
  TaskEvents,
@@ -0,0 +1,7 @@
1
+ import type { ProjectLocationDTO } from '../types/dto.js';
2
+ export declare const LocationsEvents: {
3
+ readonly GET: "locations:get";
4
+ };
5
+ export interface LocationsGetResponse {
6
+ locations: ProjectLocationDTO[];
7
+ }
@@ -0,0 +1,3 @@
1
+ export const LocationsEvents = {
2
+ GET: 'locations:get',
3
+ };
@@ -9,6 +9,7 @@ export interface ModelListRequest {
9
9
  }
10
10
  export interface ModelListResponse {
11
11
  activeModel?: string;
12
+ error?: string;
12
13
  favorites: string[];
13
14
  models: ModelDTO[];
14
15
  recent: string[];
@@ -26,5 +27,6 @@ export interface ModelSetActiveRequest {
26
27
  providerId: string;
27
28
  }
28
29
  export interface ModelSetActiveResponse {
30
+ error?: string;
29
31
  success: boolean;
30
32
  }
@@ -1,10 +1,15 @@
1
1
  import type { ProviderDTO } from '../types/dto.js';
2
2
  export declare const ProviderEvents: {
3
+ readonly AWAIT_OAUTH_CALLBACK: "provider:awaitOAuthCallback";
4
+ readonly CANCEL_OAUTH: "provider:cancelOAuth";
3
5
  readonly CONNECT: "provider:connect";
4
6
  readonly DISCONNECT: "provider:disconnect";
5
7
  readonly GET_ACTIVE: "provider:getActive";
6
8
  readonly LIST: "provider:list";
7
9
  readonly SET_ACTIVE: "provider:setActive";
10
+ readonly START_OAUTH: "provider:startOAuth";
11
+ readonly SUBMIT_OAUTH_CODE: "provider:submitOAuthCode";
12
+ readonly UPDATED: "provider:updated";
8
13
  readonly VALIDATE_API_KEY: "provider:validateApiKey";
9
14
  };
10
15
  export interface ProviderListResponse {
@@ -42,3 +47,34 @@ export interface ProviderSetActiveRequest {
42
47
  export interface ProviderSetActiveResponse {
43
48
  success: boolean;
44
49
  }
50
+ export interface ProviderCancelOAuthRequest {
51
+ providerId: string;
52
+ }
53
+ export interface ProviderCancelOAuthResponse {
54
+ success: boolean;
55
+ }
56
+ export interface ProviderStartOAuthRequest {
57
+ mode?: string;
58
+ providerId: string;
59
+ }
60
+ export interface ProviderStartOAuthResponse {
61
+ authUrl: string;
62
+ callbackMode: 'auto' | 'code-paste';
63
+ error?: string;
64
+ success: boolean;
65
+ }
66
+ export interface ProviderAwaitOAuthCallbackRequest {
67
+ providerId: string;
68
+ }
69
+ export interface ProviderAwaitOAuthCallbackResponse {
70
+ error?: string;
71
+ success: boolean;
72
+ }
73
+ export interface ProviderSubmitOAuthCodeRequest {
74
+ code: string;
75
+ providerId: string;
76
+ }
77
+ export interface ProviderSubmitOAuthCodeResponse {
78
+ error?: string;
79
+ success: boolean;
80
+ }
@@ -1,8 +1,13 @@
1
1
  export const ProviderEvents = {
2
+ AWAIT_OAUTH_CALLBACK: 'provider:awaitOAuthCallback',
3
+ CANCEL_OAUTH: 'provider:cancelOAuth',
2
4
  CONNECT: 'provider:connect',
3
5
  DISCONNECT: 'provider:disconnect',
4
6
  GET_ACTIVE: 'provider:getActive',
5
7
  LIST: 'provider:list',
6
8
  SET_ACTIVE: 'provider:setActive',
9
+ START_OAUTH: 'provider:startOAuth',
10
+ SUBMIT_OAUTH_CODE: 'provider:submitOAuthCode',
11
+ UPDATED: 'provider:updated',
7
12
  VALIDATE_API_KEY: 'provider:validateApiKey',
8
13
  };
@@ -49,13 +49,17 @@ export interface ConnectorDTO {
49
49
  }
50
50
  export interface ProviderDTO {
51
51
  apiKeyUrl?: string;
52
+ authMethod?: 'api-key' | 'oauth';
52
53
  category: 'other' | 'popular';
53
54
  description: string;
54
55
  id: string;
55
56
  isConnected: boolean;
56
57
  isCurrent: boolean;
57
58
  name: string;
59
+ oauthCallbackMode?: 'auto' | 'code-paste';
60
+ oauthLabel?: string;
58
61
  requiresApiKey: boolean;
62
+ supportsOAuth: boolean;
59
63
  }
60
64
  export interface ModelDTO {
61
65
  contextLength: number;
@@ -97,6 +101,17 @@ export interface HubEntryDTO {
97
101
  type: 'agent-skill' | 'bundle';
98
102
  version: string;
99
103
  }
104
+ export interface ProjectLocationDTO {
105
+ /** Absolute path to the context tree directory (e.g., '/Users/foo/project/.brv/context-tree') */
106
+ contextTreePath: string;
107
+ /** True if this project has connected clients/agents or is the current project */
108
+ isActive: boolean;
109
+ /** True if this is the project the client is currently running from */
110
+ isCurrent: boolean;
111
+ /** True if .brv/context-tree exists */
112
+ isInitialized: boolean;
113
+ projectPath: string;
114
+ }
100
115
  export interface StatusDTO {
101
116
  authStatus: 'expired' | 'logged_in' | 'not_logged_in' | 'unknown';
102
117
  contextTreeChanges?: ContextTreeChanges;
@@ -2,6 +2,7 @@ import { connectorsCommand } from './connectors.js';
2
2
  import { curateCommand } from './curate.js';
3
3
  import { exitCommand } from './exit.js';
4
4
  import { hubCommand } from './hub.js';
5
+ import { locationsCommand } from './locations.js';
5
6
  import { loginCommand } from './login.js';
6
7
  import { logoutCommand } from './logout.js';
7
8
  import { modelCommand } from './model.js';
@@ -22,6 +23,7 @@ import { statusCommand } from './status.js';
22
23
  export const load = () => [
23
24
  // Core workflow - most frequently used
24
25
  statusCommand,
26
+ locationsCommand,
25
27
  curateCommand,
26
28
  queryCommand,
27
29
  // Connectors management
@@ -0,0 +1,2 @@
1
+ import type { SlashCommand } from '../../../types/commands.js';
2
+ export declare const locationsCommand: SlashCommand;
@@ -0,0 +1,11 @@
1
+ import React from 'react';
2
+ import { LocationsView } from '../../locations/components/locations-view.js';
3
+ export const locationsCommand = {
4
+ action() {
5
+ return {
6
+ render: ({ onCancel, onComplete }) => React.createElement(LocationsView, { onCancel, onComplete }),
7
+ };
8
+ },
9
+ description: 'List all registered projects and their context tree status',
10
+ name: 'locations',
11
+ };
@@ -0,0 +1,16 @@
1
+ import type { QueryConfig } from '../../../lib/react-query.js';
2
+ import { type LocationsGetResponse } from '../../../../shared/transport/events/index.js';
3
+ export declare const getLocations: () => Promise<LocationsGetResponse>;
4
+ export declare const getLocationsQueryOptions: () => import("@tanstack/react-query").OmitKeyof<import("@tanstack/react-query").UseQueryOptions<LocationsGetResponse, Error, LocationsGetResponse, string[]>, "queryFn"> & {
5
+ queryFn?: import("@tanstack/react-query").QueryFunction<LocationsGetResponse, string[], never> | undefined;
6
+ } & {
7
+ queryKey: string[] & {
8
+ [dataTagSymbol]: LocationsGetResponse;
9
+ [dataTagErrorSymbol]: Error;
10
+ };
11
+ };
12
+ type UseGetLocationsOptions = {
13
+ queryConfig?: QueryConfig<typeof getLocationsQueryOptions>;
14
+ };
15
+ export declare const useGetLocations: ({ queryConfig }?: UseGetLocationsOptions) => import("@tanstack/react-query").UseQueryResult<LocationsGetResponse, Error>;
16
+ export {};
@@ -0,0 +1,17 @@
1
+ import { queryOptions, useQuery } from '@tanstack/react-query';
2
+ import { LocationsEvents } from '../../../../shared/transport/events/index.js';
3
+ import { useTransportStore } from '../../../stores/transport-store.js';
4
+ export const getLocations = () => {
5
+ const { apiClient } = useTransportStore.getState();
6
+ if (!apiClient)
7
+ return Promise.reject(new Error('Not connected'));
8
+ return apiClient.request(LocationsEvents.GET);
9
+ };
10
+ export const getLocationsQueryOptions = () => queryOptions({
11
+ queryFn: getLocations,
12
+ queryKey: ['locations'],
13
+ });
14
+ export const useGetLocations = ({ queryConfig } = {}) => useQuery({
15
+ ...getLocationsQueryOptions(),
16
+ ...queryConfig,
17
+ });
@@ -0,0 +1,3 @@
1
+ import React from 'react';
2
+ import type { CustomDialogCallbacks } from '../../../types/commands.js';
3
+ export declare function LocationsView({ onCancel, onComplete }: CustomDialogCallbacks): React.ReactNode;
@@ -0,0 +1,25 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Text, useInput } from 'ink';
3
+ import Spinner from 'ink-spinner';
4
+ import { useEffect } from 'react';
5
+ import { useGetLocations } from '../api/get-locations.js';
6
+ import { formatLocations } from '../utils/format-locations.js';
7
+ export function LocationsView({ onCancel, onComplete }) {
8
+ const { data, error, isLoading } = useGetLocations();
9
+ useInput((_input, key) => {
10
+ if (key.escape)
11
+ onCancel();
12
+ });
13
+ useEffect(() => {
14
+ if (data) {
15
+ onComplete(formatLocations(data.locations));
16
+ }
17
+ if (error) {
18
+ onComplete(`Failed to get locations: ${error.message}`);
19
+ }
20
+ }, [data, error, onComplete]);
21
+ if (isLoading) {
22
+ return (_jsxs(Text, { children: [_jsx(Spinner, { type: "dots" }), " Loading locations..."] }));
23
+ }
24
+ return null;
25
+ }
@@ -0,0 +1,2 @@
1
+ import type { ProjectLocationDTO } from '../../../../shared/transport/types/dto.js';
2
+ export declare function formatLocations(locations: ProjectLocationDTO[]): string;
@@ -0,0 +1,26 @@
1
+ import chalk from 'chalk';
2
+ function formatLocationEntry(loc) {
3
+ const label = loc.isCurrent ? ' ' + chalk.green('[current]') : loc.isActive ? ' ' + chalk.yellow('[active]') : '';
4
+ const path = loc.isCurrent || loc.isActive ? chalk.bold(loc.projectPath) : loc.projectPath;
5
+ const lines = [` ${path}${label}`];
6
+ if (loc.isInitialized) {
7
+ lines.push(chalk.dim(' └─ .brv/context-tree/'));
8
+ }
9
+ else {
10
+ lines.push(chalk.dim(' └─ .brv/context-tree/ (not initialized)'));
11
+ }
12
+ return lines;
13
+ }
14
+ export function formatLocations(locations) {
15
+ const lines = [];
16
+ if (locations.length > 0) {
17
+ lines.push(`Registered Projects — ${locations.length} found`, chalk.dim('──────────────────────────────────────────'));
18
+ for (const loc of locations) {
19
+ lines.push(...formatLocationEntry(loc), '');
20
+ }
21
+ }
22
+ else {
23
+ lines.push('Registered Projects — none found');
24
+ }
25
+ return lines.join('\n');
26
+ }