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.
- package/dist/agent/infra/llm/providers/openai.d.ts +12 -0
- package/dist/agent/infra/llm/providers/openai.js +52 -1
- package/dist/oclif/commands/curate/index.js +2 -2
- package/dist/oclif/commands/locations.d.ts +14 -0
- package/dist/oclif/commands/locations.js +68 -0
- package/dist/oclif/commands/model/switch.js +14 -3
- package/dist/oclif/commands/providers/connect.d.ts +9 -0
- package/dist/oclif/commands/providers/connect.js +110 -14
- package/dist/oclif/commands/providers/list.js +3 -5
- package/dist/oclif/commands/query.js +2 -2
- package/dist/oclif/commands/status.js +3 -3
- package/dist/oclif/lib/daemon-client.d.ts +4 -0
- package/dist/oclif/lib/daemon-client.js +13 -3
- package/dist/server/core/domain/entities/provider-config.d.ts +6 -0
- package/dist/server/core/domain/entities/provider-config.js +4 -3
- package/dist/server/core/domain/entities/provider-registry.d.ts +41 -1
- package/dist/server/core/domain/entities/provider-registry.js +24 -1
- package/dist/server/core/domain/errors/task-error.d.ts +2 -0
- package/dist/server/core/domain/errors/task-error.js +6 -1
- package/dist/server/core/domain/transport/schemas.d.ts +2 -0
- package/dist/server/core/interfaces/i-provider-config-store.d.ts +2 -0
- package/dist/server/core/interfaces/i-provider-model-fetcher.d.ts +12 -3
- package/dist/server/core/interfaces/i-provider-oauth-token-store.d.ts +24 -0
- package/dist/server/core/interfaces/i-provider-oauth-token-store.js +1 -0
- package/dist/server/core/interfaces/i-token-refresh-manager.d.ts +7 -0
- package/dist/server/core/interfaces/i-token-refresh-manager.js +1 -0
- package/dist/server/infra/daemon/agent-process.js +22 -4
- package/dist/server/infra/daemon/brv-server.js +15 -2
- package/dist/server/infra/http/models-dev-client.d.ts +29 -0
- package/dist/server/infra/http/models-dev-client.js +133 -0
- package/dist/server/infra/http/provider-model-fetcher-registry.d.ts +2 -1
- package/dist/server/infra/http/provider-model-fetcher-registry.js +6 -1
- package/dist/server/infra/http/provider-model-fetchers.d.ts +33 -8
- package/dist/server/infra/http/provider-model-fetchers.js +88 -10
- package/dist/server/infra/process/feature-handlers.d.ts +6 -1
- package/dist/server/infra/process/feature-handlers.js +11 -2
- package/dist/server/infra/provider/provider-config-resolver.d.ts +4 -2
- package/dist/server/infra/provider/provider-config-resolver.js +59 -4
- package/dist/server/infra/provider-oauth/callback-server.d.ts +24 -0
- package/dist/server/infra/provider-oauth/callback-server.js +203 -0
- package/dist/server/infra/provider-oauth/errors.d.ts +39 -0
- package/dist/server/infra/provider-oauth/errors.js +76 -0
- package/dist/server/infra/provider-oauth/index.d.ts +9 -0
- package/dist/server/infra/provider-oauth/index.js +9 -0
- package/dist/server/infra/provider-oauth/jwt-utils.d.ts +17 -0
- package/dist/server/infra/provider-oauth/jwt-utils.js +51 -0
- package/dist/server/infra/provider-oauth/pkce-service.d.ts +22 -0
- package/dist/server/infra/provider-oauth/pkce-service.js +33 -0
- package/dist/server/infra/provider-oauth/provider-oauth-token-store.d.ts +48 -0
- package/dist/server/infra/provider-oauth/provider-oauth-token-store.js +155 -0
- package/dist/server/infra/provider-oauth/refresh-token-exchange.d.ts +8 -0
- package/dist/server/infra/provider-oauth/refresh-token-exchange.js +39 -0
- package/dist/server/infra/provider-oauth/token-exchange.d.ts +8 -0
- package/dist/server/infra/provider-oauth/token-exchange.js +44 -0
- package/dist/server/infra/provider-oauth/token-refresh-manager.d.ts +32 -0
- package/dist/server/infra/provider-oauth/token-refresh-manager.js +96 -0
- package/dist/server/infra/provider-oauth/types.d.ts +55 -0
- package/dist/server/infra/provider-oauth/types.js +22 -0
- package/dist/server/infra/storage/file-provider-config-store.d.ts +2 -0
- package/dist/server/infra/storage/file-provider-config-store.js +1 -3
- package/dist/server/infra/transport/handlers/index.d.ts +2 -0
- package/dist/server/infra/transport/handlers/index.js +1 -0
- package/dist/server/infra/transport/handlers/locations-handler.d.ts +25 -0
- package/dist/server/infra/transport/handlers/locations-handler.js +64 -0
- package/dist/server/infra/transport/handlers/model-handler.d.ts +3 -0
- package/dist/server/infra/transport/handlers/model-handler.js +53 -11
- package/dist/server/infra/transport/handlers/provider-handler.d.ts +26 -0
- package/dist/server/infra/transport/handlers/provider-handler.js +215 -13
- package/dist/server/templates/skill/SKILL.md +19 -1
- package/dist/shared/constants/oauth.d.ts +14 -0
- package/dist/shared/constants/oauth.js +14 -0
- package/dist/shared/transport/events/index.d.ts +8 -0
- package/dist/shared/transport/events/index.js +3 -0
- package/dist/shared/transport/events/locations-events.d.ts +7 -0
- package/dist/shared/transport/events/locations-events.js +3 -0
- package/dist/shared/transport/events/model-events.d.ts +2 -0
- package/dist/shared/transport/events/provider-events.d.ts +36 -0
- package/dist/shared/transport/events/provider-events.js +5 -0
- package/dist/shared/transport/types/dto.d.ts +15 -0
- package/dist/tui/features/commands/definitions/index.js +2 -0
- package/dist/tui/features/commands/definitions/locations.d.ts +2 -0
- package/dist/tui/features/commands/definitions/locations.js +11 -0
- package/dist/tui/features/locations/api/get-locations.d.ts +16 -0
- package/dist/tui/features/locations/api/get-locations.js +17 -0
- package/dist/tui/features/locations/components/locations-view.d.ts +3 -0
- package/dist/tui/features/locations/components/locations-view.js +25 -0
- package/dist/tui/features/locations/utils/format-locations.d.ts +2 -0
- package/dist/tui/features/locations/utils/format-locations.js +26 -0
- package/dist/tui/features/model/api/set-active-model.d.ts +1 -1
- package/dist/tui/features/model/api/set-active-model.js +12 -4
- package/dist/tui/features/provider/api/await-oauth-callback.d.ts +11 -0
- package/dist/tui/features/provider/api/await-oauth-callback.js +25 -0
- package/dist/tui/features/provider/api/cancel-oauth.d.ts +5 -0
- package/dist/tui/features/provider/api/cancel-oauth.js +10 -0
- package/dist/tui/features/provider/api/start-oauth.d.ts +11 -0
- package/dist/tui/features/provider/api/start-oauth.js +15 -0
- package/dist/tui/features/provider/components/auth-method-dialog.d.ts +9 -0
- package/dist/tui/features/provider/components/auth-method-dialog.js +20 -0
- package/dist/tui/features/provider/components/oauth-dialog.d.ts +9 -0
- package/dist/tui/features/provider/components/oauth-dialog.js +96 -0
- package/dist/tui/features/provider/components/provider-dialog.js +1 -1
- package/dist/tui/features/provider/components/provider-flow.js +54 -4
- package/dist/tui/features/provider/components/provider-subscription-initializer.d.ts +1 -0
- package/dist/tui/features/provider/components/provider-subscription-initializer.js +5 -0
- package/dist/tui/features/provider/hooks/use-provider-subscriptions.d.ts +6 -0
- package/dist/tui/features/provider/hooks/use-provider-subscriptions.js +24 -0
- package/dist/tui/providers/app-providers.js +2 -1
- package/dist/tui/utils/error-messages.js +6 -1
- package/oclif.manifest.json +56 -1
- package/package.json +1 -1
|
@@ -15,6 +15,7 @@ import { GoogleGenAI } from '@google/genai';
|
|
|
15
15
|
import { APICallError, generateText } from 'ai';
|
|
16
16
|
import axios, { isAxiosError } from 'axios';
|
|
17
17
|
import OpenAI from 'openai';
|
|
18
|
+
import { getModelsDevClient as getModelsDevClientDefault } from './models-dev-client.js';
|
|
18
19
|
const DEFAULT_CACHE_TTL = 60 * 60 * 1000; // 1 hour
|
|
19
20
|
const ANTHROPIC_KNOWN_MODELS = {
|
|
20
21
|
'claude-3-5-haiku-20241022': { contextLength: 200_000, inputPerM: 0.8, outputPerM: 4 },
|
|
@@ -60,7 +61,8 @@ export class AnthropicModelFetcher {
|
|
|
60
61
|
constructor(cacheTtlMs = DEFAULT_CACHE_TTL) {
|
|
61
62
|
this.cacheTtlMs = cacheTtlMs;
|
|
62
63
|
}
|
|
63
|
-
async fetchModels(apiKey,
|
|
64
|
+
async fetchModels(apiKey, options) {
|
|
65
|
+
const forceRefresh = options?.forceRefresh ?? false;
|
|
64
66
|
if (!forceRefresh && this.cache && Date.now() - this.cache.timestamp < this.cacheTtlMs) {
|
|
65
67
|
return this.cache.models;
|
|
66
68
|
}
|
|
@@ -101,16 +103,64 @@ export class AnthropicModelFetcher {
|
|
|
101
103
|
// ============================================================================
|
|
102
104
|
// OpenAI Model Fetcher
|
|
103
105
|
// ============================================================================
|
|
106
|
+
/**
|
|
107
|
+
* Strict allowlist of model IDs permitted for OAuth-connected OpenAI (Codex).
|
|
108
|
+
* Only models verified to work with the ChatGPT Codex endpoint are included.
|
|
109
|
+
*
|
|
110
|
+
* NOT supported (Bad Request on chatgpt.com/backend-api/codex):
|
|
111
|
+
* - codex-mini-latest: standard API model, not a Codex endpoint model
|
|
112
|
+
* - o4-mini: reasoning model, not supported by the Codex endpoint
|
|
113
|
+
* - gpt-5.3-codex-spark: reported unsupported (GitHub openai/codex#13469)
|
|
114
|
+
*/
|
|
115
|
+
export const CODEX_ALLOWED_MODELS = new Set([
|
|
116
|
+
'gpt-5.1-codex',
|
|
117
|
+
'gpt-5.1-codex-max',
|
|
118
|
+
'gpt-5.1-codex-mini',
|
|
119
|
+
'gpt-5.2',
|
|
120
|
+
'gpt-5.2-codex',
|
|
121
|
+
'gpt-5.3-codex',
|
|
122
|
+
'gpt-5.4',
|
|
123
|
+
]);
|
|
124
|
+
/**
|
|
125
|
+
* Fallback Codex models used when models.dev is unreachable and no disk cache exists.
|
|
126
|
+
*/
|
|
127
|
+
export const CODEX_FALLBACK_MODELS = [
|
|
128
|
+
{
|
|
129
|
+
contextLength: 400_000,
|
|
130
|
+
id: 'gpt-5.3-codex',
|
|
131
|
+
isFree: false,
|
|
132
|
+
name: 'GPT-5.3 Codex',
|
|
133
|
+
pricing: { inputPerM: 0, outputPerM: 0 },
|
|
134
|
+
provider: 'OpenAI',
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
contextLength: 200_000,
|
|
138
|
+
id: 'gpt-5.1-codex-mini',
|
|
139
|
+
isFree: false,
|
|
140
|
+
name: 'GPT-5.1 Codex Mini',
|
|
141
|
+
pricing: { inputPerM: 0, outputPerM: 0 },
|
|
142
|
+
provider: 'OpenAI',
|
|
143
|
+
},
|
|
144
|
+
];
|
|
104
145
|
/**
|
|
105
146
|
* Fetches models from OpenAI using the official SDK.
|
|
147
|
+
* For OAuth-connected providers, fetches from models.dev and filters to Codex-allowed models.
|
|
106
148
|
*/
|
|
107
149
|
export class OpenAIModelFetcher {
|
|
108
150
|
cache;
|
|
109
151
|
cacheTtlMs;
|
|
110
|
-
|
|
111
|
-
|
|
152
|
+
getModelsDevClient;
|
|
153
|
+
constructor(options) {
|
|
154
|
+
this.cacheTtlMs = options?.cacheTtlMs ?? DEFAULT_CACHE_TTL;
|
|
155
|
+
const injectedClient = options?.modelsDevClient;
|
|
156
|
+
this.getModelsDevClient = injectedClient ? () => injectedClient : () => getModelsDevClientDefault();
|
|
112
157
|
}
|
|
113
|
-
async fetchModels(apiKey,
|
|
158
|
+
async fetchModels(apiKey, options) {
|
|
159
|
+
// OAuth-connected OpenAI: fetch from models.dev, filter to Codex models
|
|
160
|
+
if (options?.authMethod === 'oauth') {
|
|
161
|
+
return this.fetchCodexModels(options.forceRefresh ?? false);
|
|
162
|
+
}
|
|
163
|
+
const forceRefresh = options?.forceRefresh ?? false;
|
|
114
164
|
if (!forceRefresh && this.cache && Date.now() - this.cache.timestamp < this.cacheTtlMs) {
|
|
115
165
|
return this.cache.models;
|
|
116
166
|
}
|
|
@@ -198,6 +248,29 @@ export class OpenAIModelFetcher {
|
|
|
198
248
|
return { inputPerM: 15, outputPerM: 60 };
|
|
199
249
|
return { inputPerM: 0, outputPerM: 0 };
|
|
200
250
|
}
|
|
251
|
+
/**
|
|
252
|
+
* Fetch Codex models from models.dev, filtered by allowlist.
|
|
253
|
+
* Falls back to CODEX_FALLBACK_MODELS if models.dev is unavailable.
|
|
254
|
+
*/
|
|
255
|
+
async fetchCodexModels(forceRefresh) {
|
|
256
|
+
const client = this.getModelsDevClient();
|
|
257
|
+
const allModels = await client.getModelsForProvider('openai', forceRefresh);
|
|
258
|
+
if (allModels.length === 0) {
|
|
259
|
+
return [...CODEX_FALLBACK_MODELS];
|
|
260
|
+
}
|
|
261
|
+
// Strict allowlist only — dynamic "codex in name" matching is too broad
|
|
262
|
+
// (e.g. codex-mini-latest, gpt-5.3-codex-spark cause Bad Request)
|
|
263
|
+
const codexModels = allModels.filter((m) => CODEX_ALLOWED_MODELS.has(m.id));
|
|
264
|
+
if (codexModels.length === 0) {
|
|
265
|
+
return [...CODEX_FALLBACK_MODELS];
|
|
266
|
+
}
|
|
267
|
+
// Zero out costs (included in ChatGPT subscription)
|
|
268
|
+
return codexModels.map((m) => ({
|
|
269
|
+
...m,
|
|
270
|
+
isFree: false,
|
|
271
|
+
pricing: { inputPerM: 0, outputPerM: 0 },
|
|
272
|
+
}));
|
|
273
|
+
}
|
|
201
274
|
}
|
|
202
275
|
// ============================================================================
|
|
203
276
|
// Google Model Fetcher
|
|
@@ -211,7 +284,8 @@ export class GoogleModelFetcher {
|
|
|
211
284
|
constructor(cacheTtlMs = DEFAULT_CACHE_TTL) {
|
|
212
285
|
this.cacheTtlMs = cacheTtlMs;
|
|
213
286
|
}
|
|
214
|
-
async fetchModels(apiKey,
|
|
287
|
+
async fetchModels(apiKey, options) {
|
|
288
|
+
const forceRefresh = options?.forceRefresh ?? false;
|
|
215
289
|
if (!forceRefresh && this.cache && Date.now() - this.cache.timestamp < this.cacheTtlMs) {
|
|
216
290
|
return this.cache.models;
|
|
217
291
|
}
|
|
@@ -265,7 +339,8 @@ export class OpenAICompatibleModelFetcher {
|
|
|
265
339
|
this.providerName = providerName;
|
|
266
340
|
this.cacheTtlMs = cacheTtlMs;
|
|
267
341
|
}
|
|
268
|
-
async fetchModels(apiKey,
|
|
342
|
+
async fetchModels(apiKey, options) {
|
|
343
|
+
const forceRefresh = options?.forceRefresh ?? false;
|
|
269
344
|
if (!forceRefresh && this.cache && Date.now() - this.cache.timestamp < this.cacheTtlMs) {
|
|
270
345
|
return this.cache.models;
|
|
271
346
|
}
|
|
@@ -342,7 +417,7 @@ export class ChatBasedModelFetcher {
|
|
|
342
417
|
provider: providerName,
|
|
343
418
|
}));
|
|
344
419
|
}
|
|
345
|
-
async fetchModels(_apiKey,
|
|
420
|
+
async fetchModels(_apiKey, _options) {
|
|
346
421
|
return this.knownModels;
|
|
347
422
|
}
|
|
348
423
|
async validateApiKey(apiKey) {
|
|
@@ -384,9 +459,9 @@ import { getOpenRouterApiClient } from './openrouter-api-client.js';
|
|
|
384
459
|
* Adapts NormalizedModel to ProviderModelInfo.
|
|
385
460
|
*/
|
|
386
461
|
export class OpenRouterModelFetcher {
|
|
387
|
-
async fetchModels(apiKey,
|
|
462
|
+
async fetchModels(apiKey, options) {
|
|
388
463
|
const client = getOpenRouterApiClient();
|
|
389
|
-
const models = await client.fetchModels(apiKey, forceRefresh);
|
|
464
|
+
const models = await client.fetchModels(apiKey, options?.forceRefresh ?? false);
|
|
390
465
|
return models.map((m) => ({
|
|
391
466
|
contextLength: m.contextLength,
|
|
392
467
|
description: m.description,
|
|
@@ -438,7 +513,10 @@ function handleAiSdkValidationError(error) {
|
|
|
438
513
|
function handleSdkValidationError(error) {
|
|
439
514
|
if (error instanceof Error) {
|
|
440
515
|
const message = error.message.toLowerCase();
|
|
441
|
-
if (message.includes('401') ||
|
|
516
|
+
if (message.includes('401') ||
|
|
517
|
+
message.includes('unauthorized') ||
|
|
518
|
+
message.includes('invalid api key') ||
|
|
519
|
+
message.includes('authentication')) {
|
|
442
520
|
return { error: `Authentication failed: ${error.message}`, isValid: false };
|
|
443
521
|
}
|
|
444
522
|
if (message.includes('403') || message.includes('forbidden') || message.includes('permission')) {
|
|
@@ -6,15 +6,20 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import type { IProviderConfigStore } from '../../core/interfaces/i-provider-config-store.js';
|
|
8
8
|
import type { IProviderKeychainStore } from '../../core/interfaces/i-provider-keychain-store.js';
|
|
9
|
+
import type { IProviderOAuthTokenStore } from '../../core/interfaces/i-provider-oauth-token-store.js';
|
|
10
|
+
import type { IProjectRegistry } from '../../core/interfaces/project/i-project-registry.js';
|
|
9
11
|
import type { IAuthStateStore } from '../../core/interfaces/state/i-auth-state-store.js';
|
|
10
12
|
import type { ITransportServer } from '../../core/interfaces/transport/i-transport-server.js';
|
|
11
13
|
import type { ProjectBroadcaster, ProjectPathResolver } from '../transport/handlers/handler-types.js';
|
|
12
14
|
export interface FeatureHandlersOptions {
|
|
13
15
|
authStateStore: IAuthStateStore;
|
|
14
16
|
broadcastToProject: ProjectBroadcaster;
|
|
17
|
+
getActiveProjectPaths: () => string[];
|
|
15
18
|
log: (msg: string) => void;
|
|
19
|
+
projectRegistry: IProjectRegistry;
|
|
16
20
|
providerConfigStore: IProviderConfigStore;
|
|
17
21
|
providerKeychainStore: IProviderKeychainStore;
|
|
22
|
+
providerOAuthTokenStore: IProviderOAuthTokenStore;
|
|
18
23
|
resolveProjectPath: ProjectPathResolver;
|
|
19
24
|
transport: ITransportServer;
|
|
20
25
|
}
|
|
@@ -22,4 +27,4 @@ export interface FeatureHandlersOptions {
|
|
|
22
27
|
* Setup all feature handlers on the transport server.
|
|
23
28
|
* These handlers implement the TUI ↔ Server event contract (auth:*, config:*, status:*, etc.).
|
|
24
29
|
*/
|
|
25
|
-
export declare function setupFeatureHandlers({ authStateStore, broadcastToProject, log, providerConfigStore, providerKeychainStore, resolveProjectPath, transport, }: FeatureHandlersOptions): Promise<void>;
|
|
30
|
+
export declare function setupFeatureHandlers({ authStateStore, broadcastToProject, getActiveProjectPaths, log, projectRegistry, providerConfigStore, providerKeychainStore, providerOAuthTokenStore, resolveProjectPath, transport, }: FeatureHandlersOptions): Promise<void>;
|
|
@@ -29,13 +29,13 @@ import { HttpSpaceService } from '../space/http-space-service.js';
|
|
|
29
29
|
import { createTokenStore } from '../storage/token-store.js';
|
|
30
30
|
import { HttpTeamService } from '../team/http-team-service.js';
|
|
31
31
|
import { FsTemplateLoader } from '../template/fs-template-loader.js';
|
|
32
|
-
import { AuthHandler, ConfigHandler, ConnectorsHandler, HubHandler, InitHandler, ModelHandler, ProviderHandler, PullHandler, PushHandler, ResetHandler, SpaceHandler, StatusHandler, } from '../transport/handlers/index.js';
|
|
32
|
+
import { AuthHandler, ConfigHandler, ConnectorsHandler, HubHandler, InitHandler, LocationsHandler, ModelHandler, ProviderHandler, PullHandler, PushHandler, ResetHandler, SpaceHandler, StatusHandler, } from '../transport/handlers/index.js';
|
|
33
33
|
import { HttpUserService } from '../user/http-user-service.js';
|
|
34
34
|
/**
|
|
35
35
|
* Setup all feature handlers on the transport server.
|
|
36
36
|
* These handlers implement the TUI ↔ Server event contract (auth:*, config:*, status:*, etc.).
|
|
37
37
|
*/
|
|
38
|
-
export async function setupFeatureHandlers({ authStateStore, broadcastToProject, log, providerConfigStore, providerKeychainStore, resolveProjectPath, transport, }) {
|
|
38
|
+
export async function setupFeatureHandlers({ authStateStore, broadcastToProject, getActiveProjectPaths, log, projectRegistry, providerConfigStore, providerKeychainStore, providerOAuthTokenStore, resolveProjectPath, transport, }) {
|
|
39
39
|
const envConfig = getCurrentConfig();
|
|
40
40
|
const tokenStore = createTokenStore();
|
|
41
41
|
const projectConfigStore = new ProjectConfigStore();
|
|
@@ -59,8 +59,10 @@ export async function setupFeatureHandlers({ authStateStore, broadcastToProject,
|
|
|
59
59
|
userService,
|
|
60
60
|
}).setup();
|
|
61
61
|
new ProviderHandler({
|
|
62
|
+
browserLauncher: new SystemBrowserLauncher(),
|
|
62
63
|
providerConfigStore,
|
|
63
64
|
providerKeychainStore,
|
|
65
|
+
providerOAuthTokenStore,
|
|
64
66
|
transport,
|
|
65
67
|
}).setup();
|
|
66
68
|
new ModelHandler({
|
|
@@ -90,6 +92,13 @@ export async function setupFeatureHandlers({ authStateStore, broadcastToProject,
|
|
|
90
92
|
tokenStore,
|
|
91
93
|
transport,
|
|
92
94
|
}).setup();
|
|
95
|
+
new LocationsHandler({
|
|
96
|
+
contextTreeService,
|
|
97
|
+
getActiveProjectPaths,
|
|
98
|
+
projectRegistry,
|
|
99
|
+
resolveProjectPath,
|
|
100
|
+
transport,
|
|
101
|
+
}).setup();
|
|
93
102
|
new PushHandler({
|
|
94
103
|
broadcastToProject,
|
|
95
104
|
cogitPushService,
|
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import type { IProviderConfigStore } from '../../core/interfaces/i-provider-config-store.js';
|
|
9
9
|
import type { IProviderKeychainStore } from '../../core/interfaces/i-provider-keychain-store.js';
|
|
10
|
+
import type { IProviderOAuthTokenStore } from '../../core/interfaces/i-provider-oauth-token-store.js';
|
|
11
|
+
import type { ITokenRefreshManager } from '../../core/interfaces/i-token-refresh-manager.js';
|
|
10
12
|
import { type ProviderConfigResponse } from '../../core/domain/transport/schemas.js';
|
|
11
13
|
/**
|
|
12
14
|
* Validate provider config integrity at startup.
|
|
@@ -19,7 +21,7 @@ import { type ProviderConfigResponse } from '../../core/domain/transport/schemas
|
|
|
19
21
|
* empty string so the TUI routes to the provider setup flow — bypassing the
|
|
20
22
|
* 'byterover' fallback that withProviderDisconnected() would otherwise apply.
|
|
21
23
|
*/
|
|
22
|
-
export declare function clearStaleProviderConfig(providerConfigStore: IProviderConfigStore, providerKeychainStore: IProviderKeychainStore): Promise<void>;
|
|
24
|
+
export declare function clearStaleProviderConfig(providerConfigStore: IProviderConfigStore, providerKeychainStore: IProviderKeychainStore, providerOAuthTokenStore?: IProviderOAuthTokenStore): Promise<void>;
|
|
23
25
|
/**
|
|
24
26
|
* Resolve the active provider's full configuration.
|
|
25
27
|
*
|
|
@@ -27,4 +29,4 @@ export declare function clearStaleProviderConfig(providerConfigStore: IProviderC
|
|
|
27
29
|
* the API key (keychain → env fallback), and maps provider-specific
|
|
28
30
|
* fields (base URL, headers, location, etc.).
|
|
29
31
|
*/
|
|
30
|
-
export declare function resolveProviderConfig(providerConfigStore: IProviderConfigStore, providerKeychainStore: IProviderKeychainStore): Promise<ProviderConfigResponse>;
|
|
32
|
+
export declare function resolveProviderConfig(providerConfigStore: IProviderConfigStore, providerKeychainStore: IProviderKeychainStore, tokenRefreshManager?: ITokenRefreshManager): Promise<ProviderConfigResponse>;
|
|
@@ -5,8 +5,18 @@
|
|
|
5
5
|
* for the currently active provider. Used by the daemon state server to serve
|
|
6
6
|
* agent child processes on startup and after provider hot-swap.
|
|
7
7
|
*/
|
|
8
|
+
import { CHATGPT_OAUTH_BASE_URL, CHATGPT_OAUTH_ORIGINATOR } from '../../../shared/constants/oauth.js';
|
|
8
9
|
import { getProviderById, providerRequiresApiKey } from '../../core/domain/entities/provider-registry.js';
|
|
9
10
|
import { getProviderApiKeyFromEnv } from './env-provider-detector.js';
|
|
11
|
+
/**
|
|
12
|
+
* Check if a provider's credential (API key or OAuth access token) is accessible.
|
|
13
|
+
*
|
|
14
|
+
* Note: authMethod is intentionally NOT passed to providerRequiresApiKey() here.
|
|
15
|
+
* OAuth access tokens are stored in the keychain (as the provider's "API key"),
|
|
16
|
+
* so the keychain check on the next line correctly handles both auth methods.
|
|
17
|
+
* If an OAuth token expires and the refresh manager (Issue 5) deletes it from
|
|
18
|
+
* keychain, this function returns false — correctly marking the provider as stale.
|
|
19
|
+
*/
|
|
10
20
|
async function isProviderCredentialAccessible(providerId, providerKeychainStore) {
|
|
11
21
|
if (!providerRequiresApiKey(providerId))
|
|
12
22
|
return true;
|
|
@@ -23,7 +33,7 @@ async function isProviderCredentialAccessible(providerId, providerKeychainStore)
|
|
|
23
33
|
* empty string so the TUI routes to the provider setup flow — bypassing the
|
|
24
34
|
* 'byterover' fallback that withProviderDisconnected() would otherwise apply.
|
|
25
35
|
*/
|
|
26
|
-
export async function clearStaleProviderConfig(providerConfigStore, providerKeychainStore) {
|
|
36
|
+
export async function clearStaleProviderConfig(providerConfigStore, providerKeychainStore, providerOAuthTokenStore) {
|
|
27
37
|
try {
|
|
28
38
|
const config = await providerConfigStore.read();
|
|
29
39
|
const results = await Promise.all(Object.keys(config.providers).map(async (providerId) => ({
|
|
@@ -46,6 +56,11 @@ export async function clearStaleProviderConfig(providerConfigStore, providerKeyc
|
|
|
46
56
|
newConfig = newConfig.withActiveProvider('');
|
|
47
57
|
}
|
|
48
58
|
await providerConfigStore.write(newConfig);
|
|
59
|
+
// Clean up orphaned OAuth tokens for stale providers (consistent with
|
|
60
|
+
// the 3-store cleanup in TokenRefreshManager and ProviderHandler.setupDisconnect)
|
|
61
|
+
if (providerOAuthTokenStore) {
|
|
62
|
+
await Promise.all(staleProviderIds.map((id) => providerOAuthTokenStore.delete(id).catch(() => { })));
|
|
63
|
+
}
|
|
49
64
|
}
|
|
50
65
|
catch {
|
|
51
66
|
// Non-critical: if validation fails, daemon continues normally.
|
|
@@ -59,7 +74,7 @@ export async function clearStaleProviderConfig(providerConfigStore, providerKeyc
|
|
|
59
74
|
* the API key (keychain → env fallback), and maps provider-specific
|
|
60
75
|
* fields (base URL, headers, location, etc.).
|
|
61
76
|
*/
|
|
62
|
-
export async function resolveProviderConfig(providerConfigStore, providerKeychainStore) {
|
|
77
|
+
export async function resolveProviderConfig(providerConfigStore, providerKeychainStore, tokenRefreshManager) {
|
|
63
78
|
const config = await providerConfigStore.read();
|
|
64
79
|
const { activeProvider } = config;
|
|
65
80
|
const activeModel = config.getActiveModel(activeProvider);
|
|
@@ -96,16 +111,56 @@ export async function resolveProviderConfig(providerConfigStore, providerKeychai
|
|
|
96
111
|
}
|
|
97
112
|
default: {
|
|
98
113
|
const providerDef = getProviderById(activeProvider);
|
|
114
|
+
const providerConfig = config.providers[activeProvider];
|
|
115
|
+
if (!providerConfig) {
|
|
116
|
+
return { activeModel, activeProvider, maxInputTokens };
|
|
117
|
+
}
|
|
118
|
+
const { authMethod } = providerConfig;
|
|
119
|
+
// Attempt OAuth token refresh if provider is OAuth-connected
|
|
120
|
+
if (authMethod === 'oauth' && tokenRefreshManager) {
|
|
121
|
+
try {
|
|
122
|
+
const refreshed = await tokenRefreshManager.refreshIfNeeded(activeProvider);
|
|
123
|
+
if (!refreshed) {
|
|
124
|
+
return { activeModel, activeProvider, authMethod, maxInputTokens, providerKeyMissing: true };
|
|
125
|
+
}
|
|
126
|
+
// Re-read API key after potential refresh
|
|
127
|
+
apiKey = (await providerKeychainStore.getApiKey(activeProvider)) ?? apiKey;
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
return { activeModel, activeProvider, authMethod, maxInputTokens, providerKeyMissing: true };
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
// OAuth-connected OpenAI: use Codex endpoint + required headers
|
|
134
|
+
if (activeProvider === 'openai' && authMethod === 'oauth') {
|
|
135
|
+
const codexHeaders = {
|
|
136
|
+
originator: CHATGPT_OAUTH_ORIGINATOR,
|
|
137
|
+
};
|
|
138
|
+
if (providerConfig.oauthAccountId) {
|
|
139
|
+
codexHeaders['ChatGPT-Account-Id'] = providerConfig.oauthAccountId;
|
|
140
|
+
}
|
|
141
|
+
return {
|
|
142
|
+
activeModel,
|
|
143
|
+
activeProvider,
|
|
144
|
+
authMethod,
|
|
145
|
+
maxInputTokens,
|
|
146
|
+
provider: activeProvider,
|
|
147
|
+
providerApiKey: apiKey || undefined,
|
|
148
|
+
providerBaseUrl: CHATGPT_OAUTH_BASE_URL,
|
|
149
|
+
providerHeaders: codexHeaders,
|
|
150
|
+
providerKeyMissing: providerRequiresApiKey(activeProvider, authMethod) && !apiKey,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
99
153
|
const headers = providerDef?.headers;
|
|
100
154
|
return {
|
|
101
155
|
activeModel,
|
|
102
156
|
activeProvider,
|
|
157
|
+
authMethod,
|
|
103
158
|
maxInputTokens,
|
|
104
159
|
provider: activeProvider,
|
|
105
160
|
providerApiKey: apiKey || undefined,
|
|
106
|
-
providerBaseUrl: providerDef?.baseUrl || undefined,
|
|
161
|
+
providerBaseUrl: config.getBaseUrl(activeProvider) || providerDef?.baseUrl || undefined,
|
|
107
162
|
providerHeaders: headers && Object.keys(headers).length > 0 ? { ...headers } : undefined,
|
|
108
|
-
providerKeyMissing: providerRequiresApiKey(activeProvider) && !apiKey,
|
|
163
|
+
providerKeyMissing: providerRequiresApiKey(activeProvider, authMethod) && !apiKey,
|
|
109
164
|
};
|
|
110
165
|
}
|
|
111
166
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ProviderCallbackResult } from './types.js';
|
|
2
|
+
type ProviderCallbackServerOptions = {
|
|
3
|
+
callbackPath?: string;
|
|
4
|
+
port: number;
|
|
5
|
+
};
|
|
6
|
+
export declare class ProviderCallbackServer {
|
|
7
|
+
private readonly callbackPath;
|
|
8
|
+
private readonly connections;
|
|
9
|
+
private isStopping;
|
|
10
|
+
private onCallback;
|
|
11
|
+
private onError;
|
|
12
|
+
private pendingTimeout;
|
|
13
|
+
private readonly port;
|
|
14
|
+
private server;
|
|
15
|
+
constructor(options: ProviderCallbackServerOptions);
|
|
16
|
+
getAddress(): undefined | {
|
|
17
|
+
port: number;
|
|
18
|
+
};
|
|
19
|
+
start(): Promise<number>;
|
|
20
|
+
stop(): Promise<void>;
|
|
21
|
+
waitForCallback(expectedState: string, timeoutMs?: number): Promise<ProviderCallbackResult>;
|
|
22
|
+
private handleRequest;
|
|
23
|
+
}
|
|
24
|
+
export {};
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import http from 'node:http';
|
|
2
|
+
import { OAUTH_CALLBACK_TIMEOUT_MS } from '../../../shared/constants/oauth.js';
|
|
3
|
+
import { ProviderCallbackOAuthError, ProviderCallbackStateError, ProviderCallbackTimeoutError, ProviderOAuthError, } from './errors.js';
|
|
4
|
+
const DEFAULT_CALLBACK_PATH = '/auth/callback';
|
|
5
|
+
const SUCCESS_HTML = `<!DOCTYPE html>
|
|
6
|
+
<html>
|
|
7
|
+
<head>
|
|
8
|
+
<title>ByteRover - Authorization Successful</title>
|
|
9
|
+
<style>
|
|
10
|
+
body { font-family: 'Plus Jakarta Sans', system-ui, -apple-system, sans-serif; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; background: #0f0f0f; color: #e5e5e5; }
|
|
11
|
+
.container { text-align: center; padding: 2rem; }
|
|
12
|
+
h1 { color: #17b26a; margin-bottom: 1rem; }
|
|
13
|
+
p { color: #a3a3a3; }
|
|
14
|
+
</style>
|
|
15
|
+
</head>
|
|
16
|
+
<body>
|
|
17
|
+
<div class="container">
|
|
18
|
+
<h1>Authorization Successful</h1>
|
|
19
|
+
<p>You can close this window and return to ByteRover.</p>
|
|
20
|
+
</div>
|
|
21
|
+
<script>setTimeout(() => window.close(), 2000);</script>
|
|
22
|
+
</body>
|
|
23
|
+
</html>`;
|
|
24
|
+
function errorHtml(message) {
|
|
25
|
+
return `<!DOCTYPE html>
|
|
26
|
+
<html>
|
|
27
|
+
<head>
|
|
28
|
+
<title>ByteRover - Authorization Failed</title>
|
|
29
|
+
<style>
|
|
30
|
+
body { font-family: 'Plus Jakarta Sans', system-ui, -apple-system, sans-serif; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; background: #0f0f0f; color: #e5e5e5; }
|
|
31
|
+
.container { text-align: center; padding: 2rem; }
|
|
32
|
+
h1 { color: #f87171; margin-bottom: 1rem; }
|
|
33
|
+
p { color: #a3a3a3; }
|
|
34
|
+
.error { color: #fca5a5; font-family: monospace; margin-top: 1rem; padding: 1rem; background: rgba(248,113,113,0.1); border-radius: 0.5rem; }
|
|
35
|
+
</style>
|
|
36
|
+
</head>
|
|
37
|
+
<body>
|
|
38
|
+
<div class="container">
|
|
39
|
+
<h1>Authorization Failed</h1>
|
|
40
|
+
<p>An error occurred during authorization.</p>
|
|
41
|
+
<div class="error">${escapeHtml(message)}</div>
|
|
42
|
+
</div>
|
|
43
|
+
</body>
|
|
44
|
+
</html>`;
|
|
45
|
+
}
|
|
46
|
+
function escapeHtml(text) {
|
|
47
|
+
return text
|
|
48
|
+
.replaceAll('&', '&')
|
|
49
|
+
.replaceAll('<', '<')
|
|
50
|
+
.replaceAll('>', '>')
|
|
51
|
+
.replaceAll('"', '"')
|
|
52
|
+
.replaceAll("'", ''');
|
|
53
|
+
}
|
|
54
|
+
export class ProviderCallbackServer {
|
|
55
|
+
callbackPath;
|
|
56
|
+
connections = new Set();
|
|
57
|
+
isStopping = false;
|
|
58
|
+
onCallback;
|
|
59
|
+
onError;
|
|
60
|
+
pendingTimeout;
|
|
61
|
+
port;
|
|
62
|
+
server = undefined;
|
|
63
|
+
constructor(options) {
|
|
64
|
+
this.port = options.port;
|
|
65
|
+
this.callbackPath = options.callbackPath ?? DEFAULT_CALLBACK_PATH;
|
|
66
|
+
}
|
|
67
|
+
getAddress() {
|
|
68
|
+
const address = this.server?.address();
|
|
69
|
+
if (address !== null && address !== undefined && typeof address !== 'string') {
|
|
70
|
+
return { port: address.port };
|
|
71
|
+
}
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
async start() {
|
|
75
|
+
const address = this.getAddress();
|
|
76
|
+
if (address !== undefined) {
|
|
77
|
+
return address.port;
|
|
78
|
+
}
|
|
79
|
+
return new Promise((resolve, reject) => {
|
|
80
|
+
this.server = http.createServer((req, res) => {
|
|
81
|
+
this.handleRequest(req, res);
|
|
82
|
+
});
|
|
83
|
+
this.server.on('connection', (conn) => {
|
|
84
|
+
this.connections.add(conn);
|
|
85
|
+
conn.on('close', () => {
|
|
86
|
+
this.connections.delete(conn);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
this.server.on('error', reject);
|
|
90
|
+
this.server.listen(this.port, '127.0.0.1', () => {
|
|
91
|
+
const address = this.server?.address();
|
|
92
|
+
if (address !== null && address !== undefined && typeof address !== 'string') {
|
|
93
|
+
resolve(address.port);
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
reject(new Error('Failed to start provider callback server'));
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
async stop() {
|
|
102
|
+
if (this.isStopping)
|
|
103
|
+
return;
|
|
104
|
+
this.isStopping = true;
|
|
105
|
+
// Reject any pending waitForCallback promise so consumers don't hang
|
|
106
|
+
this.onError?.(new ProviderOAuthError('Callback server was stopped'));
|
|
107
|
+
this.onCallback = undefined;
|
|
108
|
+
this.onError = undefined;
|
|
109
|
+
if (this.pendingTimeout !== undefined) {
|
|
110
|
+
clearTimeout(this.pendingTimeout);
|
|
111
|
+
this.pendingTimeout = undefined;
|
|
112
|
+
}
|
|
113
|
+
return new Promise((resolve) => {
|
|
114
|
+
if (this.server === undefined) {
|
|
115
|
+
this.isStopping = false;
|
|
116
|
+
resolve();
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
for (const conn of this.connections) {
|
|
120
|
+
conn.destroy();
|
|
121
|
+
}
|
|
122
|
+
this.connections.clear();
|
|
123
|
+
this.server.close(() => {
|
|
124
|
+
this.server = undefined;
|
|
125
|
+
this.isStopping = false;
|
|
126
|
+
resolve();
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
waitForCallback(expectedState, timeoutMs = OAUTH_CALLBACK_TIMEOUT_MS) {
|
|
131
|
+
if (this.onCallback !== undefined) {
|
|
132
|
+
return Promise.reject(new ProviderOAuthError('A callback is already pending'));
|
|
133
|
+
}
|
|
134
|
+
const promise = new Promise((resolve, reject) => {
|
|
135
|
+
let settled = false;
|
|
136
|
+
this.pendingTimeout = setTimeout(() => {
|
|
137
|
+
if (!settled) {
|
|
138
|
+
settled = true;
|
|
139
|
+
this.pendingTimeout = undefined;
|
|
140
|
+
reject(new ProviderCallbackTimeoutError(timeoutMs));
|
|
141
|
+
}
|
|
142
|
+
}, timeoutMs);
|
|
143
|
+
this.onCallback = (code, state) => {
|
|
144
|
+
if (settled)
|
|
145
|
+
return;
|
|
146
|
+
clearTimeout(this.pendingTimeout);
|
|
147
|
+
this.pendingTimeout = undefined;
|
|
148
|
+
if (state !== expectedState) {
|
|
149
|
+
settled = true;
|
|
150
|
+
reject(new ProviderCallbackStateError());
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
settled = true;
|
|
154
|
+
resolve({ code, state });
|
|
155
|
+
};
|
|
156
|
+
this.onError = (error) => {
|
|
157
|
+
if (settled)
|
|
158
|
+
return;
|
|
159
|
+
clearTimeout(this.pendingTimeout);
|
|
160
|
+
this.pendingTimeout = undefined;
|
|
161
|
+
settled = true;
|
|
162
|
+
reject(error);
|
|
163
|
+
};
|
|
164
|
+
});
|
|
165
|
+
// Auto-close server after callback or timeout (ticket requirement)
|
|
166
|
+
const autoClose = () => this.stop();
|
|
167
|
+
promise.then(autoClose, autoClose);
|
|
168
|
+
return promise;
|
|
169
|
+
}
|
|
170
|
+
handleRequest(req, res) {
|
|
171
|
+
const url = new URL(req.url ?? '/', `http://localhost:${this.port}`);
|
|
172
|
+
if (url.pathname !== this.callbackPath) {
|
|
173
|
+
res.writeHead(404);
|
|
174
|
+
res.end('Not found');
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
const error = url.searchParams.get('error');
|
|
178
|
+
const errorDescription = url.searchParams.get('error_description');
|
|
179
|
+
const code = url.searchParams.get('code');
|
|
180
|
+
const state = url.searchParams.get('state');
|
|
181
|
+
if (error !== null) {
|
|
182
|
+
res.writeHead(400, { 'Content-Type': 'text/html' });
|
|
183
|
+
res.end(errorHtml(errorDescription ?? error));
|
|
184
|
+
this.onError?.(new ProviderCallbackOAuthError(error, errorDescription ?? undefined));
|
|
185
|
+
this.onCallback = undefined;
|
|
186
|
+
this.onError = undefined;
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
if (code === null || state === null) {
|
|
190
|
+
res.writeHead(400, { 'Content-Type': 'text/html' });
|
|
191
|
+
res.end(errorHtml('Missing code or state parameter'));
|
|
192
|
+
this.onError?.(new ProviderOAuthError('Missing code or state parameter'));
|
|
193
|
+
this.onCallback = undefined;
|
|
194
|
+
this.onError = undefined;
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
198
|
+
res.end(SUCCESS_HTML);
|
|
199
|
+
this.onCallback?.(code, state);
|
|
200
|
+
this.onCallback = undefined;
|
|
201
|
+
this.onError = undefined;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export declare class ProviderOAuthError extends Error {
|
|
2
|
+
constructor(message: string);
|
|
3
|
+
}
|
|
4
|
+
export declare class ProviderCallbackTimeoutError extends ProviderOAuthError {
|
|
5
|
+
readonly timeoutMs: number;
|
|
6
|
+
constructor(timeoutMs: number);
|
|
7
|
+
}
|
|
8
|
+
export declare class ProviderCallbackStateError extends ProviderOAuthError {
|
|
9
|
+
constructor();
|
|
10
|
+
}
|
|
11
|
+
export declare class ProviderCallbackOAuthError extends ProviderOAuthError {
|
|
12
|
+
readonly errorCode: string;
|
|
13
|
+
constructor(errorCode: string, errorDescription?: string);
|
|
14
|
+
}
|
|
15
|
+
export declare class ProviderTokenExchangeError extends ProviderOAuthError {
|
|
16
|
+
readonly errorCode?: string;
|
|
17
|
+
readonly statusCode?: number;
|
|
18
|
+
constructor(params: {
|
|
19
|
+
errorCode?: string;
|
|
20
|
+
message: string;
|
|
21
|
+
statusCode?: number;
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Extracts OAuth error fields from an unknown error response body.
|
|
26
|
+
* Shared by token-exchange and refresh-token-exchange.
|
|
27
|
+
*/
|
|
28
|
+
export declare function extractOAuthErrorFields(data: unknown): {
|
|
29
|
+
error?: string;
|
|
30
|
+
error_description?: string;
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Checks whether an OAuth token refresh error is permanent (token revoked, client invalid)
|
|
34
|
+
* vs. transient (network timeout, server error).
|
|
35
|
+
*
|
|
36
|
+
* Permanent errors require disconnecting the provider and re-authenticating.
|
|
37
|
+
* Transient errors should preserve credentials so the existing access token can still be used.
|
|
38
|
+
*/
|
|
39
|
+
export declare function isPermanentOAuthError(error: unknown): boolean;
|