byterover-cli 3.11.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/agent/infra/tools/implementations/curate-tool.js +18 -8
- 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/constants.d.ts +6 -0
- package/dist/server/constants.js +11 -0
- package/dist/server/core/domain/entities/task-history-entry.d.ts +775 -0
- package/dist/server/core/domain/entities/task-history-entry.js +88 -0
- package/dist/server/core/domain/transport/schemas.d.ts +1420 -11
- package/dist/server/core/domain/transport/schemas.js +160 -6
- package/dist/server/core/domain/transport/task-info.d.ts +18 -0
- package/dist/server/core/interfaces/process/i-task-lifecycle-hook.d.ts +7 -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/core/interfaces/storage/i-task-history-store.d.ts +62 -0
- package/dist/server/core/interfaces/storage/i-task-history-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 +48 -18
- package/dist/server/infra/dream/dream-response-schemas.d.ts +24 -0
- package/dist/server/infra/dream/dream-response-schemas.js +7 -0
- package/dist/server/infra/dream/operations/consolidate.js +21 -8
- package/dist/server/infra/dream/operations/synthesize.js +35 -8
- 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/process/task-history-entry-builder.d.ts +36 -0
- package/dist/server/infra/process/task-history-entry-builder.js +101 -0
- package/dist/server/infra/process/task-history-hook.d.ts +37 -0
- package/dist/server/infra/process/task-history-hook.js +70 -0
- package/dist/server/infra/process/task-history-store-cache.d.ts +25 -0
- package/dist/server/infra/process/task-history-store-cache.js +106 -0
- package/dist/server/infra/process/task-router.d.ts +72 -0
- package/dist/server/infra/process/task-router.js +690 -15
- package/dist/server/infra/process/transport-handlers.d.ts +8 -0
- package/dist/server/infra/process/transport-handlers.js +2 -0
- 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/storage/file-task-history-store.d.ts +294 -0
- package/dist/server/infra/storage/file-task-history-store.js +912 -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 +16 -0
- package/dist/shared/transport/events/index.js +6 -0
- package/dist/shared/transport/events/task-events.d.ts +204 -1
- package/dist/shared/transport/events/task-events.js +11 -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/tui/features/tasks/hooks/use-task-subscriptions.js +7 -0
- package/dist/tui/features/tasks/stores/tasks-store.d.ts +4 -16
- package/dist/tui/features/tasks/stores/tasks-store.js +7 -0
- package/dist/tui/types/messages.d.ts +2 -9
- 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 +653 -645
- package/package.json +1 -1
- package/dist/webui/assets/index--sXE__bc.css +0 -1
- package/dist/webui/assets/index-Bkkx961b.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
|
|
@@ -6,6 +6,7 @@ import { MarkdownWriter, parseCreatedAt } from '../../../../server/core/domain/k
|
|
|
6
6
|
import { determineTier, mergeScoring, recordCurateUpdate, } from '../../../../server/core/domain/knowledge/memory-scoring.js';
|
|
7
7
|
import { createDefaultRuntimeSignals, } from '../../../../server/core/domain/knowledge/runtime-signals-schema.js';
|
|
8
8
|
import { warnSidecarFailure } from '../../../../server/core/domain/knowledge/sidecar-logging.js';
|
|
9
|
+
import { isExcludedFromSync } from '../../../../server/infra/context-tree/derived-artifact.js';
|
|
9
10
|
import { toSnakeCase } from '../../../../server/utils/file-helpers.js';
|
|
10
11
|
import { deriveImpactFromLoss, detectStructuralLoss } from '../../../core/domain/knowledge/conflict-detector.js';
|
|
11
12
|
import { resolveStructuralLoss } from '../../../core/domain/knowledge/conflict-resolver.js';
|
|
@@ -268,14 +269,19 @@ const OperationSchema = z.object({
|
|
|
268
269
|
type: OperationType.describe('Operation type: ADD, UPDATE, MERGE, or DELETE'),
|
|
269
270
|
});
|
|
270
271
|
/**
|
|
271
|
-
* Filter out non-existent files from rawConcept.files
|
|
272
|
-
*
|
|
272
|
+
* Filter out non-existent files from rawConcept.files and derived-artifact
|
|
273
|
+
* paths from relations.
|
|
273
274
|
*/
|
|
274
275
|
async function filterValidFiles(content) {
|
|
275
|
-
|
|
276
|
-
|
|
276
|
+
// Drop relations that won't be pushed — they'd be dangling refs on remote.
|
|
277
|
+
const cleanedRelations = content.relations?.filter((r) => !isExcludedFromSync(r));
|
|
278
|
+
const withCleanRelations = cleanedRelations === content.relations
|
|
279
|
+
? content
|
|
280
|
+
: { ...content, relations: cleanedRelations };
|
|
281
|
+
if (!withCleanRelations.rawConcept?.files || withCleanRelations.rawConcept.files.length === 0) {
|
|
282
|
+
return withCleanRelations;
|
|
277
283
|
}
|
|
278
|
-
const checks = await Promise.all(
|
|
284
|
+
const checks = await Promise.all(withCleanRelations.rawConcept.files.map(async (filePath) => {
|
|
279
285
|
// Skip filesystem validation for URLs and document references
|
|
280
286
|
if (filePath.includes('://'))
|
|
281
287
|
return true;
|
|
@@ -284,12 +290,12 @@ async function filterValidFiles(content) {
|
|
|
284
290
|
return true;
|
|
285
291
|
return DirectoryManager.fileExists(filePath);
|
|
286
292
|
}));
|
|
287
|
-
const validFiles =
|
|
293
|
+
const validFiles = withCleanRelations.rawConcept.files.filter((_, i) => checks[i]);
|
|
288
294
|
// Return content with filtered files (empty array if none exist)
|
|
289
295
|
return {
|
|
290
|
-
...
|
|
296
|
+
...withCleanRelations,
|
|
291
297
|
rawConcept: {
|
|
292
|
-
...
|
|
298
|
+
...withCleanRelations.rawConcept,
|
|
293
299
|
files: validFiles.length > 0 ? validFiles : undefined,
|
|
294
300
|
},
|
|
295
301
|
};
|
|
@@ -730,6 +736,10 @@ async function executeUpdate(basePath, operation, reviewDisabled, onAfterWrite,
|
|
|
730
736
|
const filteredContent = await filterValidFiles(content);
|
|
731
737
|
// Extract previous summary from existing file's frontmatter (for review UI)
|
|
732
738
|
const existingParsed = existingContent ? MarkdownWriter.parseContent(existingContent, title) : null;
|
|
739
|
+
if (existingParsed?.relations?.length) {
|
|
740
|
+
// Drop legacy dangling refs before conflict-detection; otherwise resolver unions them back.
|
|
741
|
+
existingParsed.relations = existingParsed.relations.filter((r) => !isExcludedFromSync(r));
|
|
742
|
+
}
|
|
733
743
|
const previousSummary = existingParsed?.summary;
|
|
734
744
|
// Detect structural loss and auto-resolve: merge back anything the LLM dropped
|
|
735
745
|
const proposedContextData = {
|
|
@@ -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'),
|
|
@@ -63,6 +63,12 @@ export declare const QUERY_LOG_DIR = "query-log";
|
|
|
63
63
|
export declare const QUERY_LOG_ID_PREFIX = "qry";
|
|
64
64
|
export declare const DREAM_LOG_DIR = "dream-log";
|
|
65
65
|
export declare const DREAM_LOG_ID_PREFIX = "drm";
|
|
66
|
+
export declare const TASK_HISTORY_DIR = "task-history";
|
|
67
|
+
export declare const TASK_HISTORY_ID_PREFIX = "tsk";
|
|
68
|
+
export declare const TASK_HISTORY_DEFAULT_MAX_AGE_DAYS = 0;
|
|
69
|
+
export declare const TASK_HISTORY_DEFAULT_MAX_ENTRIES = 1000;
|
|
70
|
+
export declare const TASK_HISTORY_DEFAULT_MAX_INDEX_BLOAT_RATIO = 2;
|
|
71
|
+
export declare const TASK_HISTORY_STALE_THRESHOLD_MS = 600000;
|
|
66
72
|
export declare const REVIEW_BACKUPS_DIR = "review-backups";
|
|
67
73
|
export declare const SUMMARY_INDEX_FILE = "_index.md";
|
|
68
74
|
export declare const ARCHIVE_DIR = "_archived";
|