byterover-cli 3.12.0 → 3.13.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/.env.production +2 -1
- package/dist/oclif/commands/curate/index.js +6 -0
- package/dist/oclif/commands/providers/connect.d.ts +26 -1
- package/dist/oclif/commands/providers/connect.js +95 -17
- package/dist/oclif/commands/providers/list.d.ts +10 -1
- package/dist/oclif/commands/providers/list.js +35 -3
- package/dist/oclif/commands/query.js +6 -0
- package/dist/oclif/commands/status.js +4 -0
- package/dist/oclif/lib/billing-line.d.ts +8 -0
- package/dist/oclif/lib/billing-line.js +45 -0
- package/dist/oclif/lib/format-billing-line.d.ts +2 -0
- package/dist/oclif/lib/format-billing-line.js +19 -0
- package/dist/oclif/lib/insufficient-credits.d.ts +11 -0
- package/dist/oclif/lib/insufficient-credits.js +36 -0
- package/dist/server/config/environment.d.ts +1 -0
- package/dist/server/config/environment.js +3 -0
- package/dist/server/core/domain/transport/schemas.d.ts +17 -0
- package/dist/server/core/domain/transport/schemas.js +3 -0
- package/dist/server/core/interfaces/services/i-billing-service.d.ts +26 -0
- package/dist/server/core/interfaces/services/i-billing-service.js +1 -0
- package/dist/server/core/interfaces/storage/i-billing-config-store.d.ts +4 -0
- package/dist/server/core/interfaces/storage/i-billing-config-store.js +1 -0
- package/dist/server/infra/billing/billing-state-endpoint.d.ts +4 -0
- package/dist/server/infra/billing/billing-state-endpoint.js +7 -0
- package/dist/server/infra/billing/build-status-billing.d.ts +9 -0
- package/dist/server/infra/billing/build-status-billing.js +36 -0
- package/dist/server/infra/billing/http-billing-service.d.ts +19 -0
- package/dist/server/infra/billing/http-billing-service.js +57 -0
- package/dist/server/infra/billing/paid-organizations-endpoint.d.ts +8 -0
- package/dist/server/infra/billing/paid-organizations-endpoint.js +18 -0
- package/dist/server/infra/billing/resolve-billing-source.d.ts +13 -0
- package/dist/server/infra/billing/resolve-billing-source.js +36 -0
- package/dist/server/infra/billing/resolve-billing-team.d.ts +5 -0
- package/dist/server/infra/billing/resolve-billing-team.js +8 -0
- package/dist/server/infra/connectors/rules/rules-connector.js +7 -2
- package/dist/server/infra/connectors/shared/constants.d.ts +9 -0
- package/dist/server/infra/connectors/shared/constants.js +31 -5
- package/dist/server/infra/daemon/agent-process.js +10 -8
- package/dist/server/infra/daemon/brv-server.js +5 -0
- package/dist/server/infra/http/provider-model-fetchers.js +10 -4
- package/dist/server/infra/process/feature-handlers.d.ts +3 -1
- package/dist/server/infra/process/feature-handlers.js +26 -2
- package/dist/server/infra/storage/file-billing-config-store.d.ts +13 -0
- package/dist/server/infra/storage/file-billing-config-store.js +55 -0
- package/dist/server/infra/transport/handlers/auth-handler.d.ts +4 -0
- package/dist/server/infra/transport/handlers/auth-handler.js +20 -2
- package/dist/server/infra/transport/handlers/billing-handler.d.ts +30 -0
- package/dist/server/infra/transport/handlers/billing-handler.js +132 -0
- package/dist/server/infra/transport/handlers/index.d.ts +4 -0
- package/dist/server/infra/transport/handlers/index.js +2 -0
- package/dist/server/infra/transport/handlers/init-handler.js +2 -0
- package/dist/server/infra/transport/handlers/status-handler.d.ts +14 -0
- package/dist/server/infra/transport/handlers/status-handler.js +16 -0
- package/dist/server/infra/transport/handlers/team-handler.d.ts +19 -0
- package/dist/server/infra/transport/handlers/team-handler.js +40 -0
- package/dist/shared/transport/events/auth-events.d.ts +3 -0
- package/dist/shared/transport/events/billing-events.d.ts +48 -0
- package/dist/shared/transport/events/billing-events.js +8 -0
- package/dist/shared/transport/events/index.d.ts +11 -0
- package/dist/shared/transport/events/index.js +6 -0
- package/dist/shared/transport/events/team-events.d.ts +8 -0
- package/dist/shared/transport/events/team-events.js +3 -0
- package/dist/shared/transport/types/dto.d.ts +80 -0
- package/dist/webui/assets/index-B9JmEFOK.js +130 -0
- package/dist/webui/assets/index-CMIKsBMr.css +1 -0
- package/dist/webui/index.html +2 -2
- package/dist/webui/sw.js +1 -1
- package/oclif.manifest.json +1280 -1272
- package/package.json +1 -1
- package/dist/webui/assets/index-DyVvFoM6.css +0 -1
- package/dist/webui/assets/index-lr0byHh9.js +0 -130
package/.env.production
CHANGED
|
@@ -4,4 +4,5 @@ BRV_COGIT_BASE_URL=https://v3-cgit.byterover.dev
|
|
|
4
4
|
BRV_GIT_REMOTE_BASE_URL=https://byterover.dev
|
|
5
5
|
BRV_LLM_BASE_URL=https://llm.byterover.dev
|
|
6
6
|
BRV_WEB_APP_URL=https://app.byterover.dev
|
|
7
|
-
BRV_UI_SOURCE=lib
|
|
7
|
+
BRV_UI_SOURCE=lib
|
|
8
|
+
BRV_BILLING_BASE_URL=https://v3-billing.byterover.dev
|
|
@@ -4,7 +4,9 @@ import { BRV_DIR, CONTEXT_TREE_DIR } from '../../../server/constants.js';
|
|
|
4
4
|
import { TransportStateEventNames } from '../../../server/core/domain/transport/index.js';
|
|
5
5
|
import { extractCurateOperations } from '../../../server/utils/curate-result-parser.js';
|
|
6
6
|
import { TaskEvents } from '../../../shared/transport/events/index.js';
|
|
7
|
+
import { printBillingLine } from '../../lib/billing-line.js';
|
|
7
8
|
import { formatConnectionError, hasLeakedHandles, providerMissingMessage, withDaemonRetry, } from '../../lib/daemon-client.js';
|
|
9
|
+
import { ensureBillingFunds } from '../../lib/insufficient-credits.js';
|
|
8
10
|
import { writeJsonResponse } from '../../lib/json-response.js';
|
|
9
11
|
import { DEFAULT_TIMEOUT_SECONDS, MAX_TIMEOUT_SECONDS, MIN_TIMEOUT_SECONDS, waitForTaskCompletion } from '../../lib/task-client.js';
|
|
10
12
|
export default class Curate extends Command {
|
|
@@ -104,6 +106,10 @@ Bad examples:
|
|
|
104
106
|
if (active.providerKeyMissing) {
|
|
105
107
|
throw new Error(providerMissingMessage(active.activeProvider, active.authMethod));
|
|
106
108
|
}
|
|
109
|
+
const billing = await printBillingLine({ client, format, log: (msg) => this.log(msg) });
|
|
110
|
+
if (billing) {
|
|
111
|
+
await ensureBillingFunds({ billing, client });
|
|
112
|
+
}
|
|
107
113
|
await this.submitTask({ client, content: resolvedContent, flags, format, projectRoot, taskType, worktreeRoot });
|
|
108
114
|
}, {
|
|
109
115
|
...this.getDaemonClientOptions(),
|
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
import { Command } from '@oclif/core';
|
|
2
|
-
import type { ProviderDTO } from '../../../shared/transport/types/dto.js';
|
|
2
|
+
import type { ProviderDTO, TeamDTO } from '../../../shared/transport/types/dto.js';
|
|
3
3
|
import { type ModelListResponse } from '../../../shared/transport/events/model-events.js';
|
|
4
4
|
import { type DaemonClientOptions } from '../../lib/daemon-client.js';
|
|
5
|
+
type ConnectInfo = {
|
|
6
|
+
kind: 'apikey';
|
|
7
|
+
model?: string;
|
|
8
|
+
providerId: string;
|
|
9
|
+
providerName: string;
|
|
10
|
+
} | {
|
|
11
|
+
kind: 'oauth';
|
|
12
|
+
providerName: string;
|
|
13
|
+
showInstructions: boolean;
|
|
14
|
+
};
|
|
5
15
|
export default class ProviderConnect extends Command {
|
|
6
16
|
static args: {
|
|
7
17
|
provider: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
|
|
@@ -15,7 +25,10 @@ export default class ProviderConnect extends Command {
|
|
|
15
25
|
format: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
16
26
|
model: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
17
27
|
oauth: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
28
|
+
team: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
18
29
|
};
|
|
30
|
+
protected applyTeamPin(team: string, options?: DaemonClientOptions): Promise<TeamDTO>;
|
|
31
|
+
protected buildPinPayload(team: TeamDTO | undefined): Record<string, unknown>;
|
|
19
32
|
protected connectProvider({ apiKey, baseUrl, model, providerId }: {
|
|
20
33
|
apiKey?: string;
|
|
21
34
|
baseUrl?: string;
|
|
@@ -36,6 +49,9 @@ export default class ProviderConnect extends Command {
|
|
|
36
49
|
protected disconnectProvider(providerId: string, options?: DaemonClientOptions): Promise<void>;
|
|
37
50
|
protected fetchModels(providerId: string, options?: DaemonClientOptions): Promise<ModelListResponse>;
|
|
38
51
|
protected fetchProviders(options?: DaemonClientOptions): Promise<ProviderDTO[]>;
|
|
52
|
+
protected fetchTeams(options?: DaemonClientOptions): Promise<TeamDTO[]>;
|
|
53
|
+
protected logPinResult(team: TeamDTO | undefined): void;
|
|
54
|
+
protected matchTeam(teams: readonly TeamDTO[], value: string): TeamDTO | undefined;
|
|
39
55
|
protected promptForApiKey(providerName: string, apiKeyUrl?: string, signal?: AbortSignal): Promise<string>;
|
|
40
56
|
protected promptForAuthMethod(provider: ProviderDTO, signal?: AbortSignal): Promise<'api-key' | 'oauth'>;
|
|
41
57
|
protected promptForBaseUrl(signal?: AbortSignal): Promise<string>;
|
|
@@ -46,6 +62,12 @@ export default class ProviderConnect extends Command {
|
|
|
46
62
|
}[], signal?: AbortSignal): Promise<string | undefined>;
|
|
47
63
|
protected promptForOptionalApiKey(providerName: string, signal?: AbortSignal): Promise<string | undefined>;
|
|
48
64
|
protected promptForProvider(providers: ProviderDTO[], signal?: AbortSignal): Promise<string>;
|
|
65
|
+
protected renderConnectSuccess(params: {
|
|
66
|
+
connectInfo: ConnectInfo;
|
|
67
|
+
format: 'json' | 'text';
|
|
68
|
+
pinnedTeam: TeamDTO | undefined;
|
|
69
|
+
providerId: string;
|
|
70
|
+
}): void;
|
|
49
71
|
run(): Promise<void>;
|
|
50
72
|
/**
|
|
51
73
|
* Interactive flow with cancel-to-go-back navigation.
|
|
@@ -58,8 +80,11 @@ export default class ProviderConnect extends Command {
|
|
|
58
80
|
code: string | undefined;
|
|
59
81
|
model: string | undefined;
|
|
60
82
|
oauth: boolean;
|
|
83
|
+
team: string | undefined;
|
|
61
84
|
}, format: 'json' | 'text'): Promise<void>;
|
|
85
|
+
protected setBillingPin(teamId: string | undefined, options?: DaemonClientOptions): Promise<void>;
|
|
62
86
|
/** Returns true when wizard should end (skip model step), false to continue to model step. */
|
|
63
87
|
private runAuthStep;
|
|
64
88
|
private runModelStep;
|
|
65
89
|
}
|
|
90
|
+
export {};
|
|
@@ -2,12 +2,15 @@ import { input, password, select, Separator } from '@inquirer/prompts';
|
|
|
2
2
|
import { Args, Command, Flags } from '@oclif/core';
|
|
3
3
|
import chalk from 'chalk';
|
|
4
4
|
import { OAUTH_CALLBACK_TIMEOUT_MS } from '../../../shared/constants/oauth.js';
|
|
5
|
+
import { BillingEvents, } from '../../../shared/transport/events/billing-events.js';
|
|
5
6
|
import { ModelEvents, } from '../../../shared/transport/events/model-events.js';
|
|
6
7
|
import { ProviderEvents, } from '../../../shared/transport/events/provider-events.js';
|
|
8
|
+
import { TeamEvents } from '../../../shared/transport/events/team-events.js';
|
|
7
9
|
import { withDaemonRetry } from '../../lib/daemon-client.js';
|
|
8
10
|
import { writeJsonResponse } from '../../lib/json-response.js';
|
|
9
11
|
import { createEscapeSignal, isEscBack, isPromptCancelled, validateUrl, wizardSelectTheme, } from '../../lib/prompt-utils.js';
|
|
10
12
|
import { createSpinner } from '../../lib/spinner.js';
|
|
13
|
+
const BYTEROVER_PROVIDER_ID = 'byterover';
|
|
11
14
|
export default class ProviderConnect extends Command {
|
|
12
15
|
static args = {
|
|
13
16
|
provider: Args.string({
|
|
@@ -21,6 +24,7 @@ export default class ProviderConnect extends Command {
|
|
|
21
24
|
'<%= config.bin %> providers connect anthropic --api-key sk-xxx',
|
|
22
25
|
'<%= config.bin %> providers connect openai --oauth',
|
|
23
26
|
'<%= config.bin %> providers connect byterover',
|
|
27
|
+
'<%= config.bin %> providers connect byterover --team acme',
|
|
24
28
|
'<%= config.bin %> providers connect openai-compatible --base-url http://localhost:11434/v1 --api-key sk-xxx',
|
|
25
29
|
];
|
|
26
30
|
static flags = {
|
|
@@ -51,7 +55,25 @@ export default class ProviderConnect extends Command {
|
|
|
51
55
|
default: false,
|
|
52
56
|
description: 'Connect via OAuth (browser-based)',
|
|
53
57
|
}),
|
|
58
|
+
team: Flags.string({
|
|
59
|
+
description: 'Pin this project to a billing team (byterover only). Accepts team name or slug.',
|
|
60
|
+
}),
|
|
54
61
|
};
|
|
62
|
+
async applyTeamPin(team, options) {
|
|
63
|
+
const teams = await this.fetchTeams(options);
|
|
64
|
+
const match = this.matchTeam(teams, team);
|
|
65
|
+
if (!match) {
|
|
66
|
+
const list = teams.length === 0 ? '' : ` Available: ${teams.map((t) => t.displayName).join(', ')}.`;
|
|
67
|
+
throw new Error(`No team matched "${team}".${list}`);
|
|
68
|
+
}
|
|
69
|
+
await this.setBillingPin(match.id, options);
|
|
70
|
+
return match;
|
|
71
|
+
}
|
|
72
|
+
buildPinPayload(team) {
|
|
73
|
+
if (!team)
|
|
74
|
+
return {};
|
|
75
|
+
return { team: { cleared: false, displayName: team.displayName, organizationId: team.id } };
|
|
76
|
+
}
|
|
55
77
|
async connectProvider({ apiKey, baseUrl, model, providerId }, options) {
|
|
56
78
|
return withDaemonRetry(async (client) => {
|
|
57
79
|
// 1. Verify provider exists
|
|
@@ -152,7 +174,24 @@ export default class ProviderConnect extends Command {
|
|
|
152
174
|
const { providers } = await withDaemonRetry(async (client) => client.requestWithAck(ProviderEvents.LIST), options);
|
|
153
175
|
return providers;
|
|
154
176
|
}
|
|
155
|
-
|
|
177
|
+
async fetchTeams(options) {
|
|
178
|
+
return withDaemonRetry(async (client) => {
|
|
179
|
+
const response = await client.requestWithAck(TeamEvents.LIST);
|
|
180
|
+
if (response.error)
|
|
181
|
+
throw new Error(response.error);
|
|
182
|
+
return response.teams ?? [];
|
|
183
|
+
}, options);
|
|
184
|
+
}
|
|
185
|
+
logPinResult(team) {
|
|
186
|
+
if (!team)
|
|
187
|
+
return;
|
|
188
|
+
this.log(`ByteRover usage on this project will be billed to ${team.displayName}.`);
|
|
189
|
+
}
|
|
190
|
+
matchTeam(teams, value) {
|
|
191
|
+
const lower = value.toLowerCase();
|
|
192
|
+
return (teams.find((t) => t.displayName.toLowerCase() === lower) ??
|
|
193
|
+
teams.find((t) => t.name.toLowerCase() === lower));
|
|
194
|
+
}
|
|
156
195
|
async promptForApiKey(providerName, apiKeyUrl, signal) {
|
|
157
196
|
this.log();
|
|
158
197
|
const hint = apiKeyUrl ? ` (get one at ${apiKeyUrl}):` : ':';
|
|
@@ -244,6 +283,28 @@ export default class ProviderConnect extends Command {
|
|
|
244
283
|
theme: wizardSelectTheme,
|
|
245
284
|
}, { signal });
|
|
246
285
|
}
|
|
286
|
+
renderConnectSuccess(params) {
|
|
287
|
+
const { connectInfo, format, pinnedTeam, providerId } = params;
|
|
288
|
+
if (format === 'json') {
|
|
289
|
+
const data = connectInfo.kind === 'oauth'
|
|
290
|
+
? { providerId }
|
|
291
|
+
: { model: connectInfo.model, providerId: connectInfo.providerId, providerName: connectInfo.providerName };
|
|
292
|
+
writeJsonResponse({ command: 'providers connect', data: { ...data, ...this.buildPinPayload(pinnedTeam) }, success: true });
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
if (connectInfo.kind === 'oauth') {
|
|
296
|
+
if (!connectInfo.showInstructions) {
|
|
297
|
+
this.log(`Connected to ${connectInfo.providerName} via OAuth`);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
else {
|
|
301
|
+
this.log(`Connected to ${connectInfo.providerName} (${connectInfo.providerId})`);
|
|
302
|
+
if (connectInfo.model) {
|
|
303
|
+
this.log(`Model set to: ${connectInfo.model}`);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
this.logPinResult(pinnedTeam);
|
|
307
|
+
}
|
|
247
308
|
async run() {
|
|
248
309
|
const { args, flags } = await this.parse(ProviderConnect);
|
|
249
310
|
const providerId = args.provider;
|
|
@@ -275,6 +336,7 @@ export default class ProviderConnect extends Command {
|
|
|
275
336
|
code: flags.code,
|
|
276
337
|
model: flags.model,
|
|
277
338
|
oauth: flags.oauth,
|
|
339
|
+
team: flags.team,
|
|
278
340
|
}, format);
|
|
279
341
|
}
|
|
280
342
|
/**
|
|
@@ -358,7 +420,7 @@ export default class ProviderConnect extends Command {
|
|
|
358
420
|
}
|
|
359
421
|
}
|
|
360
422
|
async runNonInteractive(providerId, flags, format) {
|
|
361
|
-
const { apiKey, baseUrl, code, model, oauth } = flags;
|
|
423
|
+
const { apiKey, baseUrl, code, model, oauth, team } = flags;
|
|
362
424
|
if (oauth && apiKey) {
|
|
363
425
|
const msg = 'Cannot use --oauth and --api-key together';
|
|
364
426
|
if (format === 'json') {
|
|
@@ -379,29 +441,34 @@ export default class ProviderConnect extends Command {
|
|
|
379
441
|
}
|
|
380
442
|
return;
|
|
381
443
|
}
|
|
444
|
+
if (team !== undefined && providerId !== BYTEROVER_PROVIDER_ID) {
|
|
445
|
+
const msg = `--team is only supported for the "${BYTEROVER_PROVIDER_ID}" provider.`;
|
|
446
|
+
if (format === 'json') {
|
|
447
|
+
writeJsonResponse({ command: 'providers connect', data: { error: msg }, success: false });
|
|
448
|
+
}
|
|
449
|
+
else {
|
|
450
|
+
this.log(msg);
|
|
451
|
+
}
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
382
454
|
try {
|
|
455
|
+
let connectInfo;
|
|
383
456
|
if (oauth) {
|
|
384
457
|
const onProgress = format === 'text' ? (msg) => this.log(msg) : undefined;
|
|
385
458
|
const result = await this.connectProviderOAuth({ code, providerId }, undefined, onProgress);
|
|
386
|
-
|
|
387
|
-
writeJsonResponse({ command: 'providers connect', data: { providerId }, success: true });
|
|
388
|
-
}
|
|
389
|
-
else if (!result.showInstructions) {
|
|
390
|
-
this.log(`Connected to ${result.providerName} via OAuth`);
|
|
391
|
-
}
|
|
459
|
+
connectInfo = { kind: 'oauth', providerName: result.providerName, showInstructions: result.showInstructions };
|
|
392
460
|
}
|
|
393
461
|
else {
|
|
394
462
|
const result = await this.connectProvider({ apiKey, baseUrl, model, providerId });
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
this.log(`Model set to: ${result.model}`);
|
|
402
|
-
}
|
|
403
|
-
}
|
|
463
|
+
connectInfo = {
|
|
464
|
+
kind: 'apikey',
|
|
465
|
+
model: result.model,
|
|
466
|
+
providerId: result.providerId,
|
|
467
|
+
providerName: result.providerName,
|
|
468
|
+
};
|
|
404
469
|
}
|
|
470
|
+
const pinnedTeam = team === undefined ? undefined : await this.applyTeamPin(team);
|
|
471
|
+
this.renderConnectSuccess({ connectInfo, format, pinnedTeam, providerId });
|
|
405
472
|
}
|
|
406
473
|
catch (error) {
|
|
407
474
|
const errorMessage = error instanceof Error
|
|
@@ -415,6 +482,17 @@ export default class ProviderConnect extends Command {
|
|
|
415
482
|
}
|
|
416
483
|
}
|
|
417
484
|
}
|
|
485
|
+
async setBillingPin(teamId, options) {
|
|
486
|
+
await withDaemonRetry(async (client, projectRoot) => {
|
|
487
|
+
if (!projectRoot)
|
|
488
|
+
throw new Error('Failed to resolve project path for billing pin.');
|
|
489
|
+
const request = teamId === undefined ? { projectPath: projectRoot } : { projectPath: projectRoot, teamId };
|
|
490
|
+
const response = await client.requestWithAck(BillingEvents.SET_PINNED_TEAM, request);
|
|
491
|
+
if (!response.success) {
|
|
492
|
+
throw new Error(response.error ?? 'Failed to update billing pin.');
|
|
493
|
+
}
|
|
494
|
+
}, options);
|
|
495
|
+
}
|
|
418
496
|
/* eslint-disable no-await-in-loop -- intentional retry loop for interactive auth */
|
|
419
497
|
/** Returns true when wizard should end (skip model step), false to continue to model step. */
|
|
420
498
|
async runAuthStep(providerId, provider, signal) {
|
|
@@ -1,12 +1,21 @@
|
|
|
1
1
|
import { Command } from '@oclif/core';
|
|
2
|
+
import type { StatusBillingDTO, TeamDTO } from '../../../shared/transport/types/dto.js';
|
|
2
3
|
import { type ProviderListResponse } from '../../../shared/transport/events/provider-events.js';
|
|
3
4
|
import { type DaemonClientOptions } from '../../lib/daemon-client.js';
|
|
5
|
+
interface ProvidersListData {
|
|
6
|
+
billing?: StatusBillingDTO;
|
|
7
|
+
providers: ProviderListResponse['providers'];
|
|
8
|
+
teams: TeamDTO[];
|
|
9
|
+
}
|
|
4
10
|
export default class ProviderList extends Command {
|
|
5
11
|
static description: string;
|
|
6
12
|
static examples: string[];
|
|
7
13
|
static flags: {
|
|
8
14
|
format: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
15
|
};
|
|
10
|
-
protected
|
|
16
|
+
protected billingMarker(teamId: string, billing?: StatusBillingDTO): string | undefined;
|
|
17
|
+
protected fetchAll(options?: DaemonClientOptions): Promise<ProvidersListData>;
|
|
18
|
+
protected printByteRoverTeams(teams: readonly TeamDTO[], billing?: StatusBillingDTO): void;
|
|
11
19
|
run(): Promise<void>;
|
|
12
20
|
}
|
|
21
|
+
export {};
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import { Command, Flags } from '@oclif/core';
|
|
2
2
|
import chalk from 'chalk';
|
|
3
|
+
import { BillingEvents } from '../../../shared/transport/events/billing-events.js';
|
|
3
4
|
import { ProviderEvents } from '../../../shared/transport/events/provider-events.js';
|
|
5
|
+
import { TeamEvents } from '../../../shared/transport/events/team-events.js';
|
|
4
6
|
import { formatConnectionError, withDaemonRetry } from '../../lib/daemon-client.js';
|
|
5
7
|
import { writeJsonResponse } from '../../lib/json-response.js';
|
|
8
|
+
const BYTEROVER_PROVIDER_ID = 'byterover';
|
|
9
|
+
const EMPTY_TEAMS = { teams: [] };
|
|
6
10
|
export default class ProviderList extends Command {
|
|
7
11
|
static description = 'List all available providers and their connection status';
|
|
8
12
|
static examples = ['<%= config.bin %> providers list', '<%= config.bin %> providers list --format json'];
|
|
@@ -13,14 +17,39 @@ export default class ProviderList extends Command {
|
|
|
13
17
|
options: ['text', 'json'],
|
|
14
18
|
}),
|
|
15
19
|
};
|
|
16
|
-
|
|
17
|
-
|
|
20
|
+
billingMarker(teamId, billing) {
|
|
21
|
+
if (billing?.source !== 'paid')
|
|
22
|
+
return undefined;
|
|
23
|
+
if (billing.organizationId !== teamId)
|
|
24
|
+
return undefined;
|
|
25
|
+
return 'billing';
|
|
26
|
+
}
|
|
27
|
+
async fetchAll(options) {
|
|
28
|
+
return withDaemonRetry(async (client) => {
|
|
29
|
+
const { providers } = await client.requestWithAck(ProviderEvents.LIST);
|
|
30
|
+
const byterover = providers.find((p) => p.id === BYTEROVER_PROVIDER_ID);
|
|
31
|
+
if (!byterover?.isConnected)
|
|
32
|
+
return { providers, teams: [] };
|
|
33
|
+
const [teamsResponse, billingResponse] = await Promise.all([
|
|
34
|
+
client.requestWithAck(TeamEvents.LIST).catch(() => EMPTY_TEAMS),
|
|
35
|
+
client.requestWithAck(BillingEvents.RESOLVE).catch(() => { }),
|
|
36
|
+
]);
|
|
37
|
+
return { billing: billingResponse?.billing, providers, teams: teamsResponse.teams ?? [] };
|
|
38
|
+
}, options);
|
|
39
|
+
}
|
|
40
|
+
printByteRoverTeams(teams, billing) {
|
|
41
|
+
this.log(` ${chalk.dim('Your teams:')}`);
|
|
42
|
+
for (const team of teams) {
|
|
43
|
+
const marker = this.billingMarker(team.id, billing);
|
|
44
|
+
const suffix = marker ? ` ${chalk.dim(`(${marker})`)}` : '';
|
|
45
|
+
this.log(` ${team.displayName}${suffix}`);
|
|
46
|
+
}
|
|
18
47
|
}
|
|
19
48
|
async run() {
|
|
20
49
|
const { flags } = await this.parse(ProviderList);
|
|
21
50
|
const format = flags.format;
|
|
22
51
|
try {
|
|
23
|
-
const { providers } = await this.
|
|
52
|
+
const { billing, providers, teams } = await this.fetchAll();
|
|
24
53
|
if (format === 'json') {
|
|
25
54
|
writeJsonResponse({ command: 'providers list', data: { providers }, success: true });
|
|
26
55
|
return;
|
|
@@ -32,6 +61,9 @@ export default class ProviderList extends Command {
|
|
|
32
61
|
if (p.description) {
|
|
33
62
|
this.log(` ${chalk.dim(p.description)}`);
|
|
34
63
|
}
|
|
64
|
+
if (p.id === BYTEROVER_PROVIDER_ID && p.isConnected && teams.length > 0) {
|
|
65
|
+
this.printByteRoverTeams(teams, billing);
|
|
66
|
+
}
|
|
35
67
|
}
|
|
36
68
|
}
|
|
37
69
|
catch (error) {
|
|
@@ -2,7 +2,9 @@ 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 { printBillingLine } from '../lib/billing-line.js';
|
|
5
6
|
import { formatConnectionError, hasLeakedHandles, providerMissingMessage, withDaemonRetry, } from '../lib/daemon-client.js';
|
|
7
|
+
import { ensureBillingFunds } from '../lib/insufficient-credits.js';
|
|
6
8
|
import { writeJsonResponse } from '../lib/json-response.js';
|
|
7
9
|
import { DEFAULT_TIMEOUT_SECONDS, MAX_TIMEOUT_SECONDS, MIN_TIMEOUT_SECONDS, waitForTaskCompletion } from '../lib/task-client.js';
|
|
8
10
|
export default class Query extends Command {
|
|
@@ -62,6 +64,10 @@ Bad:
|
|
|
62
64
|
if (active.providerKeyMissing) {
|
|
63
65
|
throw new Error(providerMissingMessage(active.activeProvider, active.authMethod));
|
|
64
66
|
}
|
|
67
|
+
const billing = await printBillingLine({ client, format, log: (msg) => this.log(msg) });
|
|
68
|
+
if (billing) {
|
|
69
|
+
await ensureBillingFunds({ billing, client });
|
|
70
|
+
}
|
|
65
71
|
await this.submitTask({
|
|
66
72
|
client,
|
|
67
73
|
format,
|
|
@@ -2,6 +2,7 @@ import { Command, Flags } from '@oclif/core';
|
|
|
2
2
|
import chalk from 'chalk';
|
|
3
3
|
import { StatusEvents, } from '../../shared/transport/events/status-events.js';
|
|
4
4
|
import { formatConnectionError, withDaemonRetry } from '../lib/daemon-client.js';
|
|
5
|
+
import { formatBillingLine } from '../lib/format-billing-line.js';
|
|
5
6
|
import { writeJsonResponse } from '../lib/json-response.js';
|
|
6
7
|
export default class Status extends Command {
|
|
7
8
|
static description = 'Show CLI status and project information. Display local context tree managed by ByteRover CLI';
|
|
@@ -114,6 +115,9 @@ export default class Status extends Command {
|
|
|
114
115
|
else {
|
|
115
116
|
this.log('Space: Not connected');
|
|
116
117
|
}
|
|
118
|
+
if (status.billing) {
|
|
119
|
+
this.log(formatBillingLine(status.billing));
|
|
120
|
+
}
|
|
117
121
|
// Context tree status
|
|
118
122
|
switch (status.contextTreeStatus) {
|
|
119
123
|
case 'git_vc': {
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ITransportClient } from '@campfirein/brv-transport-client';
|
|
2
|
+
import type { StatusBillingDTO } from '../../shared/transport/types/dto.js';
|
|
3
|
+
export interface PrintBillingLineDeps {
|
|
4
|
+
client: ITransportClient;
|
|
5
|
+
format: 'json' | 'text';
|
|
6
|
+
log: (msg: string) => void;
|
|
7
|
+
}
|
|
8
|
+
export declare function printBillingLine(deps: PrintBillingLineDeps): Promise<StatusBillingDTO | undefined>;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { BillingEvents, } from '../../shared/transport/events/billing-events.js';
|
|
3
|
+
import { formatBillingLine } from './format-billing-line.js';
|
|
4
|
+
const SKIP_SOURCES = new Set(['other-provider']);
|
|
5
|
+
const LOW_CREDIT_RATIO = 0.1;
|
|
6
|
+
function tone(billing) {
|
|
7
|
+
if (billing.source === 'other-provider')
|
|
8
|
+
return 'normal';
|
|
9
|
+
const { remaining, total } = billing;
|
|
10
|
+
if (remaining === undefined || total === undefined || total <= 0)
|
|
11
|
+
return 'normal';
|
|
12
|
+
if (remaining <= 0)
|
|
13
|
+
return 'danger';
|
|
14
|
+
if (remaining / total < LOW_CREDIT_RATIO)
|
|
15
|
+
return 'warn';
|
|
16
|
+
return 'normal';
|
|
17
|
+
}
|
|
18
|
+
function colorize(line, t) {
|
|
19
|
+
switch (t) {
|
|
20
|
+
case 'danger': {
|
|
21
|
+
return chalk.red(line);
|
|
22
|
+
}
|
|
23
|
+
case 'warn': {
|
|
24
|
+
return chalk.yellow(line);
|
|
25
|
+
}
|
|
26
|
+
default: {
|
|
27
|
+
return chalk.dim(line);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export async function printBillingLine(deps) {
|
|
32
|
+
try {
|
|
33
|
+
const response = await deps.client.requestWithAck(BillingEvents.RESOLVE);
|
|
34
|
+
const { billing } = response;
|
|
35
|
+
if (!billing)
|
|
36
|
+
return undefined;
|
|
37
|
+
if (deps.format === 'text' && !SKIP_SOURCES.has(billing.source)) {
|
|
38
|
+
deps.log(colorize(formatBillingLine(billing), tone(billing)));
|
|
39
|
+
}
|
|
40
|
+
return billing;
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export function formatBillingLine(billing) {
|
|
2
|
+
if (billing.source === 'other-provider') {
|
|
3
|
+
return `Using ${billing.activeProvider ?? 'another provider'}`;
|
|
4
|
+
}
|
|
5
|
+
if (billing.source === 'free') {
|
|
6
|
+
const { remaining, total } = billing;
|
|
7
|
+
if (remaining === undefined || total === undefined)
|
|
8
|
+
return 'Billing: Personal free credits';
|
|
9
|
+
return `Billing: Personal free credits (${formatNumber(remaining)} / ${formatNumber(total)})`;
|
|
10
|
+
}
|
|
11
|
+
const label = billing.organizationName ?? billing.organizationId;
|
|
12
|
+
if (billing.remaining === undefined || billing.tier === undefined) {
|
|
13
|
+
return `Billing: ${label} (usage unavailable)`;
|
|
14
|
+
}
|
|
15
|
+
return `Billing: ${label} (${formatNumber(billing.remaining)} credits, ${billing.tier})`;
|
|
16
|
+
}
|
|
17
|
+
function formatNumber(value) {
|
|
18
|
+
return value.toLocaleString('en-US');
|
|
19
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ITransportClient } from '@campfirein/brv-transport-client';
|
|
2
|
+
import type { StatusBillingDTO } from '../../shared/transport/types/dto.js';
|
|
3
|
+
export declare class InsufficientCreditsError extends Error {
|
|
4
|
+
constructor(message: string);
|
|
5
|
+
}
|
|
6
|
+
export declare function isBillingExhausted(billing: StatusBillingDTO): boolean;
|
|
7
|
+
export interface EnsureBillingFundsDeps {
|
|
8
|
+
billing: StatusBillingDTO;
|
|
9
|
+
client: ITransportClient;
|
|
10
|
+
}
|
|
11
|
+
export declare function ensureBillingFunds(deps: EnsureBillingFundsDeps): Promise<void>;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { BillingEvents } from '../../shared/transport/events/billing-events.js';
|
|
2
|
+
export class InsufficientCreditsError extends Error {
|
|
3
|
+
constructor(message) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.name = 'InsufficientCreditsError';
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
export function isBillingExhausted(billing) {
|
|
9
|
+
if (billing.source === 'other-provider')
|
|
10
|
+
return false;
|
|
11
|
+
return billing.remaining !== undefined && billing.remaining <= 0;
|
|
12
|
+
}
|
|
13
|
+
export async function ensureBillingFunds(deps) {
|
|
14
|
+
if (!isBillingExhausted(deps.billing))
|
|
15
|
+
return;
|
|
16
|
+
if (deps.billing.source === 'free') {
|
|
17
|
+
throw new InsufficientCreditsError('Your free monthly credits are exhausted. Upgrade to a paid team to continue using ByteRover provider.');
|
|
18
|
+
}
|
|
19
|
+
const currentTeamId = 'organizationId' in deps.billing ? deps.billing.organizationId : undefined;
|
|
20
|
+
const teams = await fetchOtherPaidTeamNames(deps.client, currentTeamId);
|
|
21
|
+
const suffix = teams.length > 0 ? ` Available teams: ${teams.join(', ')}.` : '';
|
|
22
|
+
throw new InsufficientCreditsError('ByteRover billing team is out of credits. Top up the team, or switch billing target with ' +
|
|
23
|
+
'`brv providers connect byterover --team <name>` before re-running.' +
|
|
24
|
+
suffix);
|
|
25
|
+
}
|
|
26
|
+
async function fetchOtherPaidTeamNames(client, excludeTeamId) {
|
|
27
|
+
try {
|
|
28
|
+
const response = await client.requestWithAck(BillingEvents.LIST_USAGE);
|
|
29
|
+
return Object.values(response.usage ?? {})
|
|
30
|
+
.filter((usage) => usage.tier !== 'FREE' && usage.organizationId !== excludeTeamId)
|
|
31
|
+
.map((usage) => usage.organizationName);
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -41,9 +41,12 @@ export const getCurrentConfig = () => {
|
|
|
41
41
|
assertRootDomain('BRV_IAM_BASE_URL', iamBaseUrl);
|
|
42
42
|
const cogitBaseUrl = readRequiredEnv('BRV_COGIT_BASE_URL');
|
|
43
43
|
assertRootDomain('BRV_COGIT_BASE_URL', cogitBaseUrl);
|
|
44
|
+
const billingBaseUrl = readRequiredEnv('BRV_BILLING_BASE_URL');
|
|
45
|
+
assertRootDomain('BRV_BILLING_BASE_URL', billingBaseUrl);
|
|
44
46
|
const oidcBase = `${iamBaseUrl}${API_V1_PATH}/oidc`;
|
|
45
47
|
return {
|
|
46
48
|
authorizationUrl: `${oidcBase}/authorize`,
|
|
49
|
+
billingBaseUrl,
|
|
47
50
|
clientId: DEFAULTS.clientId,
|
|
48
51
|
cogitBaseUrl,
|
|
49
52
|
gitRemoteBaseUrl: readRequiredEnv('BRV_GIT_REMOTE_BASE_URL'),
|
|
@@ -449,6 +449,8 @@ export declare const TransportAgentEventNames: {
|
|
|
449
449
|
*/
|
|
450
450
|
export declare const TransportStateEventNames: {
|
|
451
451
|
readonly GET_AUTH: "state:getAuth";
|
|
452
|
+
readonly GET_BILLING_CONFIG: "state:getBillingConfig";
|
|
453
|
+
readonly GET_PAID_ORGANIZATIONS: "state:getPaidOrganizations";
|
|
452
454
|
readonly GET_PROJECT_CONFIG: "state:getProjectConfig";
|
|
453
455
|
readonly GET_PROVIDER_CONFIG: "state:getProviderConfig";
|
|
454
456
|
};
|
|
@@ -457,8 +459,23 @@ export declare const TransportStateEventNames: {
|
|
|
457
459
|
* Used to notify agent child processes of global state changes.
|
|
458
460
|
*/
|
|
459
461
|
export declare const TransportDaemonEventNames: {
|
|
462
|
+
readonly BILLING_PIN_CHANGED: "billing:pinChanged";
|
|
460
463
|
readonly PROVIDER_UPDATED: "provider:updated";
|
|
461
464
|
};
|
|
465
|
+
export interface BillingStateRequest {
|
|
466
|
+
projectPath: string;
|
|
467
|
+
}
|
|
468
|
+
export interface BillingStateResponse {
|
|
469
|
+
pinnedTeamId?: string;
|
|
470
|
+
}
|
|
471
|
+
export interface BillingPinChangedPayload {
|
|
472
|
+
projectPath: string;
|
|
473
|
+
teamId?: string;
|
|
474
|
+
}
|
|
475
|
+
export interface PaidOrganizationsResponse {
|
|
476
|
+
error?: string;
|
|
477
|
+
organizationIds: string[];
|
|
478
|
+
}
|
|
462
479
|
/**
|
|
463
480
|
* Response payload for GET_PROVIDER_CONFIG — shared between daemon and agent process.
|
|
464
481
|
*
|
|
@@ -278,6 +278,8 @@ export const TransportAgentEventNames = {
|
|
|
278
278
|
*/
|
|
279
279
|
export const TransportStateEventNames = {
|
|
280
280
|
GET_AUTH: 'state:getAuth',
|
|
281
|
+
GET_BILLING_CONFIG: 'state:getBillingConfig',
|
|
282
|
+
GET_PAID_ORGANIZATIONS: 'state:getPaidOrganizations',
|
|
281
283
|
GET_PROJECT_CONFIG: 'state:getProjectConfig',
|
|
282
284
|
GET_PROVIDER_CONFIG: 'state:getProviderConfig',
|
|
283
285
|
};
|
|
@@ -286,6 +288,7 @@ export const TransportStateEventNames = {
|
|
|
286
288
|
* Used to notify agent child processes of global state changes.
|
|
287
289
|
*/
|
|
288
290
|
export const TransportDaemonEventNames = {
|
|
291
|
+
BILLING_PIN_CHANGED: 'billing:pinChanged',
|
|
289
292
|
PROVIDER_UPDATED: 'provider:updated',
|
|
290
293
|
};
|
|
291
294
|
/**
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { BillingFreeUserLimitDTO, BillingOrganizationTierDTO, BillingUsageDTO } from '../../../../shared/transport/types/dto.js';
|
|
2
|
+
/**
|
|
3
|
+
* Reads compute-unit usage from the ByteRover billing service.
|
|
4
|
+
* Implementations may be HTTP-based (production) or stubbed (tests).
|
|
5
|
+
*/
|
|
6
|
+
export interface IBillingService {
|
|
7
|
+
/**
|
|
8
|
+
* Returns the user's free-tier daily/monthly limits.
|
|
9
|
+
*
|
|
10
|
+
* @param sessionKey Authenticated session token (passed via x-byterover-session-id).
|
|
11
|
+
*/
|
|
12
|
+
getFreeUserLimit: (sessionKey: string) => Promise<BillingFreeUserLimitDTO>;
|
|
13
|
+
/**
|
|
14
|
+
* Returns the tier (FREE/PRO/ENTERPRISE) for every org the user belongs to.
|
|
15
|
+
*
|
|
16
|
+
* @param sessionKey Authenticated session token (passed via x-byterover-session-id).
|
|
17
|
+
*/
|
|
18
|
+
getTiers: (sessionKey: string) => Promise<BillingOrganizationTierDTO[]>;
|
|
19
|
+
/**
|
|
20
|
+
* Fetches usage for every organization the authenticated user belongs to in
|
|
21
|
+
* a single round trip. Replaces the old per-org `getUsageByProjects` fan-out.
|
|
22
|
+
*
|
|
23
|
+
* @param sessionKey Authenticated session token (passed via x-byterover-session-id).
|
|
24
|
+
*/
|
|
25
|
+
getUsages: (sessionKey: string) => Promise<BillingUsageDTO[]>;
|
|
26
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|