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
|
@@ -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
|
|
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(
|
|
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
|
}, {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Command } from '@oclif/core';
|
|
2
|
+
import type { ProjectLocationDTO } from '../../shared/transport/types/dto.js';
|
|
3
|
+
import { type DaemonClientOptions } from '../lib/daemon-client.js';
|
|
4
|
+
export default class Locations extends Command {
|
|
5
|
+
static description: string;
|
|
6
|
+
static examples: string[];
|
|
7
|
+
static flags: {
|
|
8
|
+
format: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
|
+
};
|
|
10
|
+
protected fetchLocations(options?: DaemonClientOptions): Promise<ProjectLocationDTO[]>;
|
|
11
|
+
run(): Promise<void>;
|
|
12
|
+
private formatLocationEntry;
|
|
13
|
+
private formatTextOutput;
|
|
14
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { Command, Flags } from '@oclif/core';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { LocationsEvents } from '../../shared/transport/events/locations-events.js';
|
|
4
|
+
import { formatConnectionError, withDaemonRetry } from '../lib/daemon-client.js';
|
|
5
|
+
import { writeJsonResponse } from '../lib/json-response.js';
|
|
6
|
+
export default class Locations extends Command {
|
|
7
|
+
static description = 'List all registered projects and their context tree status';
|
|
8
|
+
static examples = ['<%= config.bin %> <%= command.id %>', '<%= config.bin %> <%= command.id %> --format json'];
|
|
9
|
+
static flags = {
|
|
10
|
+
format: Flags.string({
|
|
11
|
+
char: 'f',
|
|
12
|
+
default: 'text',
|
|
13
|
+
description: 'Output format',
|
|
14
|
+
options: ['text', 'json'],
|
|
15
|
+
}),
|
|
16
|
+
};
|
|
17
|
+
async fetchLocations(options) {
|
|
18
|
+
return withDaemonRetry(async (client) => {
|
|
19
|
+
const response = await client.requestWithAck(LocationsEvents.GET);
|
|
20
|
+
return response.locations;
|
|
21
|
+
}, options);
|
|
22
|
+
}
|
|
23
|
+
async run() {
|
|
24
|
+
const { flags } = await this.parse(Locations);
|
|
25
|
+
const isJson = flags.format === 'json';
|
|
26
|
+
try {
|
|
27
|
+
const locations = await this.fetchLocations();
|
|
28
|
+
if (isJson) {
|
|
29
|
+
writeJsonResponse({ command: 'locations', data: { locations }, success: true });
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
this.formatTextOutput(locations);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
if (isJson) {
|
|
37
|
+
writeJsonResponse({ command: 'locations', data: { error: formatConnectionError(error) }, success: false });
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
this.log(formatConnectionError(error));
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
formatLocationEntry(loc) {
|
|
45
|
+
const label = loc.isCurrent ? ' ' + chalk.green('[current]') : loc.isActive ? ' ' + chalk.yellow('[active]') : '';
|
|
46
|
+
const path = loc.isCurrent || loc.isActive ? chalk.bold(loc.projectPath) : loc.projectPath;
|
|
47
|
+
this.log(` ${path}${label}`);
|
|
48
|
+
if (loc.isInitialized) {
|
|
49
|
+
this.log(chalk.dim(' └─ .brv/context-tree/'));
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
this.log(chalk.dim(' └─ .brv/context-tree/ (not initialized)'));
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
formatTextOutput(locations) {
|
|
56
|
+
if (locations.length > 0) {
|
|
57
|
+
this.log(`Registered Projects — ${locations.length} found`);
|
|
58
|
+
this.log(chalk.dim('──────────────────────────────────────────'));
|
|
59
|
+
for (const loc of locations) {
|
|
60
|
+
this.formatLocationEntry(loc);
|
|
61
|
+
this.log('');
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
this.log('Registered Projects — none found');
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Args, Command, Flags } from '@oclif/core';
|
|
2
|
-
import { ModelEvents
|
|
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
|
|
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, {
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
98
|
-
|
|
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:
|
|
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.
|
|
104
|
-
if (
|
|
105
|
-
|
|
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
|
|
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
|
-
|
|
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(
|
|
57
|
+
throw new Error(providerMissingMessage(active.activeProvider, active.authMethod));
|
|
58
58
|
}
|
|
59
59
|
await this.submitTask({ client, format, projectRoot, query: args.query });
|
|
60
60
|
}, {
|
|
@@ -22,10 +22,10 @@ export default class Status extends Command {
|
|
|
22
22
|
}
|
|
23
23
|
async run() {
|
|
24
24
|
const { flags } = await this.parse(Status);
|
|
25
|
-
const
|
|
25
|
+
const isJson = flags.format === 'json';
|
|
26
26
|
try {
|
|
27
27
|
const status = await this.fetchStatus();
|
|
28
|
-
if (
|
|
28
|
+
if (isJson) {
|
|
29
29
|
writeJsonResponse({
|
|
30
30
|
command: 'status',
|
|
31
31
|
data: { ...status, cliVersion: this.config.version },
|
|
@@ -37,7 +37,7 @@ export default class Status extends Command {
|
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
39
|
catch (error) {
|
|
40
|
-
if (
|
|
40
|
+
if (isJson) {
|
|
41
41
|
writeJsonResponse({
|
|
42
42
|
command: 'status',
|
|
43
43
|
data: { error: formatConnectionError(error) },
|
|
@@ -30,6 +30,10 @@ export declare function isRetryableError(error: unknown): boolean;
|
|
|
30
30
|
* Checks if an error left leaked Socket.IO handles that prevent Node.js from exiting.
|
|
31
31
|
*/
|
|
32
32
|
export declare function hasLeakedHandles(error: unknown): boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Builds a user-friendly message when provider credentials are missing from storage.
|
|
35
|
+
*/
|
|
36
|
+
export declare function providerMissingMessage(activeProvider: string, authMethod?: 'api-key' | 'oauth'): string;
|
|
33
37
|
export interface ProviderErrorContext {
|
|
34
38
|
activeModel?: string;
|
|
35
39
|
activeProvider?: string;
|
|
@@ -12,6 +12,8 @@ const USER_FRIENDLY_MESSAGES = {
|
|
|
12
12
|
[TaskErrorCode.CONTEXT_TREE_NOT_INITIALIZED]: 'Context tree not initialized.',
|
|
13
13
|
[TaskErrorCode.LOCAL_CHANGES_EXIST]: 'You have local changes. Run "brv push" to save your changes before pulling.',
|
|
14
14
|
[TaskErrorCode.NOT_AUTHENTICATED]: 'Not authenticated. Cloud sync features (push/pull/space) require login — local query and curate work without authentication.',
|
|
15
|
+
[TaskErrorCode.OAUTH_REFRESH_FAILED]: 'OAuth token refresh failed. Run "brv providers connect <provider> --oauth" to reconnect.',
|
|
16
|
+
[TaskErrorCode.OAUTH_TOKEN_EXPIRED]: 'OAuth token has expired. Run "brv providers connect <provider> --oauth" to reconnect.',
|
|
15
17
|
[TaskErrorCode.PROJECT_NOT_INIT]: 'Project not initialized. Run "brv restart" to reinitialize.',
|
|
16
18
|
[TaskErrorCode.PROVIDER_NOT_CONFIGURED]: 'No provider connected. Run "brv providers connect byterover" to use the free built-in provider, or connect another provider.',
|
|
17
19
|
[TaskErrorCode.SPACE_NOT_CONFIGURED]: 'No space configured. Run "brv space list" to see available spaces, then "brv space switch --team <team> --name <space>" to select one.',
|
|
@@ -83,6 +85,14 @@ export function hasLeakedHandles(error) {
|
|
|
83
85
|
return false;
|
|
84
86
|
return error.code === TaskErrorCode.AGENT_DISCONNECTED || error.code === TaskErrorCode.AGENT_NOT_AVAILABLE;
|
|
85
87
|
}
|
|
88
|
+
/**
|
|
89
|
+
* Builds a user-friendly message when provider credentials are missing from storage.
|
|
90
|
+
*/
|
|
91
|
+
export function providerMissingMessage(activeProvider, authMethod) {
|
|
92
|
+
return authMethod === 'oauth'
|
|
93
|
+
? `${activeProvider} authentication has expired.\nPlease reconnect: brv providers connect ${activeProvider} --oauth`
|
|
94
|
+
: `${activeProvider} API key is missing from storage.\nPlease reconnect: brv providers connect ${activeProvider} --api-key <your-key>`;
|
|
95
|
+
}
|
|
86
96
|
/**
|
|
87
97
|
* Formats a connection error into a user-friendly message.
|
|
88
98
|
*/
|
|
@@ -131,9 +141,9 @@ export function formatConnectionError(error, providerContext) {
|
|
|
131
141
|
const provider = providerContext?.activeProvider ?? '<provider>';
|
|
132
142
|
const model = providerContext?.activeModel;
|
|
133
143
|
const currentInfo = model ? `Provider: ${provider} Model: ${model}\n\n` : `Provider: ${provider}\n\n`;
|
|
134
|
-
return (`LLM provider
|
|
135
|
-
' Reconnect
|
|
136
|
-
` brv providers connect ${provider}
|
|
144
|
+
return (`LLM provider credentials are missing or invalid.\n${currentInfo}` +
|
|
145
|
+
' Reconnect your provider:\n' +
|
|
146
|
+
` brv providers connect ${provider}\n\n` +
|
|
137
147
|
' Switch to a different provider:\n' +
|
|
138
148
|
' brv providers switch <provider>\n\n' +
|
|
139
149
|
' See all options: brv providers --help');
|
|
@@ -12,12 +12,16 @@ export interface ConnectedProviderConfig {
|
|
|
12
12
|
readonly activeModel?: string;
|
|
13
13
|
/** Context window size of the active model (from provider API, e.g. OpenRouter) */
|
|
14
14
|
readonly activeModelContextLength?: number;
|
|
15
|
+
/** How this provider was authenticated */
|
|
16
|
+
readonly authMethod?: 'api-key' | 'oauth';
|
|
15
17
|
/** Custom API base URL (for openai-compatible provider) */
|
|
16
18
|
readonly baseUrl?: string;
|
|
17
19
|
/** When the provider was connected */
|
|
18
20
|
readonly connectedAt: string;
|
|
19
21
|
/** User's favorite models (for quick access) */
|
|
20
22
|
readonly favoriteModels: readonly string[];
|
|
23
|
+
/** OAuth account ID (e.g. ChatGPT-Account-Id for OpenAI) */
|
|
24
|
+
readonly oauthAccountId?: string;
|
|
21
25
|
/** Recently used models (last 10) */
|
|
22
26
|
readonly recentModels: readonly string[];
|
|
23
27
|
}
|
|
@@ -96,7 +100,9 @@ export declare class ProviderConfig {
|
|
|
96
100
|
*/
|
|
97
101
|
withProviderConnected(providerId: string, options?: {
|
|
98
102
|
activeModel?: string;
|
|
103
|
+
authMethod?: 'api-key' | 'oauth';
|
|
99
104
|
baseUrl?: string;
|
|
105
|
+
oauthAccountId?: string;
|
|
100
106
|
}): ProviderConfig;
|
|
101
107
|
/**
|
|
102
108
|
* Create a new config with a provider disconnected.
|
|
@@ -10,10 +10,9 @@
|
|
|
10
10
|
const isProviderConfigJson = (json) => {
|
|
11
11
|
if (typeof json !== 'object' || json === null)
|
|
12
12
|
return false;
|
|
13
|
-
|
|
14
|
-
if (typeof obj.activeProvider !== 'string')
|
|
13
|
+
if (!('activeProvider' in json) || typeof json.activeProvider !== 'string')
|
|
15
14
|
return false;
|
|
16
|
-
if (typeof
|
|
15
|
+
if (!('providers' in json) || typeof json.providers !== 'object' || json.providers === null)
|
|
17
16
|
return false;
|
|
18
17
|
return true;
|
|
19
18
|
};
|
|
@@ -164,9 +163,11 @@ export class ProviderConfig {
|
|
|
164
163
|
const existingConfig = this.providers[providerId];
|
|
165
164
|
const newProviderConfig = {
|
|
166
165
|
activeModel: options?.activeModel ?? existingConfig?.activeModel,
|
|
166
|
+
authMethod: options?.authMethod ?? existingConfig?.authMethod,
|
|
167
167
|
baseUrl: options?.baseUrl ?? existingConfig?.baseUrl,
|
|
168
168
|
connectedAt: existingConfig?.connectedAt ?? new Date().toISOString(),
|
|
169
169
|
favoriteModels: existingConfig?.favoriteModels ?? [],
|
|
170
|
+
oauthAccountId: options?.oauthAccountId ?? existingConfig?.oauthAccountId,
|
|
170
171
|
recentModels: existingConfig?.recentModels ?? [],
|
|
171
172
|
};
|
|
172
173
|
return new ProviderConfig({
|