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
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { BillingStateRequest, BillingStateResponse } from '../../core/domain/transport/schemas.js';
|
|
2
|
+
import type { IBillingConfigStore } from '../../core/interfaces/storage/i-billing-config-store.js';
|
|
3
|
+
export type BillingConfigStoreFactory = (projectPath: string) => IBillingConfigStore;
|
|
4
|
+
export declare function createBillingStateHandler(storeFactory: BillingConfigStoreFactory): (data: BillingStateRequest) => Promise<BillingStateResponse>;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { BillingFreeUserLimitDTO, BillingUsageDTO, StatusBillingDTO } from '../../../shared/transport/types/dto.js';
|
|
2
|
+
export interface BuildStatusBillingInput {
|
|
3
|
+
activeProvider: string;
|
|
4
|
+
freeUserLimit?: BillingFreeUserLimitDTO;
|
|
5
|
+
isAuthenticated: boolean;
|
|
6
|
+
paidUsages: readonly BillingUsageDTO[];
|
|
7
|
+
pinnedTeamId?: string;
|
|
8
|
+
}
|
|
9
|
+
export declare function buildStatusBilling(input: BuildStatusBillingInput): StatusBillingDTO | undefined;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { resolveBillingTeamId } from './resolve-billing-team.js';
|
|
2
|
+
const BYTEROVER_PROVIDER_ID = 'byterover';
|
|
3
|
+
export function buildStatusBilling(input) {
|
|
4
|
+
if (!input.isAuthenticated)
|
|
5
|
+
return undefined;
|
|
6
|
+
if (input.activeProvider !== BYTEROVER_PROVIDER_ID) {
|
|
7
|
+
return { activeProvider: input.activeProvider, source: 'other-provider' };
|
|
8
|
+
}
|
|
9
|
+
const paidIds = input.paidUsages.map((u) => u.organizationId);
|
|
10
|
+
const resolved = resolveBillingTeamId({
|
|
11
|
+
paidOrganizationIds: paidIds,
|
|
12
|
+
pinnedTeamId: input.pinnedTeamId,
|
|
13
|
+
});
|
|
14
|
+
if (resolved === undefined)
|
|
15
|
+
return freeSource(input.freeUserLimit);
|
|
16
|
+
const usage = input.paidUsages.find((u) => u.organizationId === resolved);
|
|
17
|
+
if (!usage)
|
|
18
|
+
return { organizationId: resolved, source: 'paid' };
|
|
19
|
+
return {
|
|
20
|
+
organizationId: usage.organizationId,
|
|
21
|
+
organizationName: usage.organizationName,
|
|
22
|
+
remaining: usage.remaining,
|
|
23
|
+
source: 'paid',
|
|
24
|
+
tier: usage.tier,
|
|
25
|
+
total: usage.totalLimit,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function freeSource(freeLimit) {
|
|
29
|
+
if (!freeLimit)
|
|
30
|
+
return { source: 'free' };
|
|
31
|
+
return {
|
|
32
|
+
remaining: freeLimit.monthly.remaining,
|
|
33
|
+
source: 'free',
|
|
34
|
+
total: freeLimit.monthly.limit,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { BillingFreeUserLimitDTO, BillingOrganizationTierDTO, BillingUsageDTO } from '../../../shared/transport/types/dto.js';
|
|
2
|
+
import type { IBillingService } from '../../core/interfaces/services/i-billing-service.js';
|
|
3
|
+
export type BillingServiceConfig = {
|
|
4
|
+
apiBaseUrl: string;
|
|
5
|
+
timeout?: number;
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* HTTP-backed billing service. `getUsages` joins `/billing/usages` and
|
|
9
|
+
* `/billing/organizations/tiers` in parallel so consumers see tier alongside
|
|
10
|
+
* credit usage in a single DTO. `getFreeUserLimit` mirrors its endpoint 1:1.
|
|
11
|
+
*/
|
|
12
|
+
export declare class HttpBillingService implements IBillingService {
|
|
13
|
+
private readonly config;
|
|
14
|
+
constructor(config: BillingServiceConfig);
|
|
15
|
+
getFreeUserLimit(sessionKey: string): Promise<BillingFreeUserLimitDTO>;
|
|
16
|
+
getTiers(sessionKey: string): Promise<BillingOrganizationTierDTO[]>;
|
|
17
|
+
getUsages(sessionKey: string): Promise<BillingUsageDTO[]>;
|
|
18
|
+
private fetchRawUsages;
|
|
19
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { getErrorMessage } from '../../utils/error-helpers.js';
|
|
2
|
+
import { AuthenticatedHttpClient } from '../http/authenticated-http-client.js';
|
|
3
|
+
const DEFAULT_TIMEOUT_MS = 10_000;
|
|
4
|
+
/**
|
|
5
|
+
* HTTP-backed billing service. `getUsages` joins `/billing/usages` and
|
|
6
|
+
* `/billing/organizations/tiers` in parallel so consumers see tier alongside
|
|
7
|
+
* credit usage in a single DTO. `getFreeUserLimit` mirrors its endpoint 1:1.
|
|
8
|
+
*/
|
|
9
|
+
export class HttpBillingService {
|
|
10
|
+
config;
|
|
11
|
+
constructor(config) {
|
|
12
|
+
this.config = {
|
|
13
|
+
apiBaseUrl: config.apiBaseUrl,
|
|
14
|
+
timeout: config.timeout ?? DEFAULT_TIMEOUT_MS,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
async getFreeUserLimit(sessionKey) {
|
|
18
|
+
try {
|
|
19
|
+
const httpClient = new AuthenticatedHttpClient(sessionKey);
|
|
20
|
+
const url = `${this.config.apiBaseUrl}/billing/usage/free-user/limit`;
|
|
21
|
+
return await httpClient.get(url, { timeout: this.config.timeout });
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
throw new Error(`Failed to fetch free-user limit: ${getErrorMessage(error)}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
async getTiers(sessionKey) {
|
|
28
|
+
try {
|
|
29
|
+
const httpClient = new AuthenticatedHttpClient(sessionKey);
|
|
30
|
+
const url = `${this.config.apiBaseUrl}/billing/organizations/tiers`;
|
|
31
|
+
const response = await httpClient.get(url, { timeout: this.config.timeout });
|
|
32
|
+
return response.organizations;
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
throw new Error(`Failed to fetch billing tiers: ${getErrorMessage(error)}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
async getUsages(sessionKey) {
|
|
39
|
+
const [rawUsages, tiers] = await Promise.all([this.fetchRawUsages(sessionKey), this.getTiers(sessionKey)]);
|
|
40
|
+
const tierByOrg = new Map(tiers.map((t) => [t.organizationId, t]));
|
|
41
|
+
return rawUsages.map((usage) => {
|
|
42
|
+
const tier = tierByOrg.get(usage.organizationId);
|
|
43
|
+
return { ...usage, isTrialing: tier?.isTrialing ?? false, tier: tier?.tier ?? 'FREE' };
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
async fetchRawUsages(sessionKey) {
|
|
47
|
+
try {
|
|
48
|
+
const httpClient = new AuthenticatedHttpClient(sessionKey);
|
|
49
|
+
const url = `${this.config.apiBaseUrl}/billing/usages`;
|
|
50
|
+
const response = await httpClient.get(url, { timeout: this.config.timeout });
|
|
51
|
+
return response.organizations;
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
throw new Error(`Failed to fetch billing usages: ${getErrorMessage(error)}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { PaidOrganizationsResponse } from '../../core/domain/transport/schemas.js';
|
|
2
|
+
import type { IBillingService } from '../../core/interfaces/services/i-billing-service.js';
|
|
3
|
+
import type { IAuthStateStore } from '../../core/interfaces/state/i-auth-state-store.js';
|
|
4
|
+
export interface PaidOrganizationsHandlerDeps {
|
|
5
|
+
authStateStore: IAuthStateStore;
|
|
6
|
+
billingService: IBillingService;
|
|
7
|
+
}
|
|
8
|
+
export declare function createPaidOrganizationsHandler(deps: PaidOrganizationsHandlerDeps): () => Promise<PaidOrganizationsResponse>;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { getErrorMessage } from '../../utils/error-helpers.js';
|
|
2
|
+
export function createPaidOrganizationsHandler(deps) {
|
|
3
|
+
return async () => {
|
|
4
|
+
const token = deps.authStateStore.getToken();
|
|
5
|
+
if (!token?.isValid())
|
|
6
|
+
return { organizationIds: [] };
|
|
7
|
+
try {
|
|
8
|
+
const tiers = await deps.billingService.getTiers(token.sessionKey);
|
|
9
|
+
const organizationIds = tiers
|
|
10
|
+
.filter((tier) => tier.tier !== 'FREE')
|
|
11
|
+
.map((tier) => tier.organizationId);
|
|
12
|
+
return { organizationIds };
|
|
13
|
+
}
|
|
14
|
+
catch (error) {
|
|
15
|
+
return { error: getErrorMessage(error), organizationIds: [] };
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { StatusBillingDTO } from '../../../shared/transport/types/dto.js';
|
|
2
|
+
import type { IProviderConfigStore } from '../../core/interfaces/i-provider-config-store.js';
|
|
3
|
+
import type { IBillingService } from '../../core/interfaces/services/i-billing-service.js';
|
|
4
|
+
import type { IAuthStateStore } from '../../core/interfaces/state/i-auth-state-store.js';
|
|
5
|
+
import type { IBillingConfigStore } from '../../core/interfaces/storage/i-billing-config-store.js';
|
|
6
|
+
export interface ResolveBillingDeps {
|
|
7
|
+
authStateStore: IAuthStateStore;
|
|
8
|
+
billingConfigStoreFactory: (projectPath: string) => IBillingConfigStore;
|
|
9
|
+
billingService: IBillingService;
|
|
10
|
+
projectPath: string;
|
|
11
|
+
providerConfigStore: IProviderConfigStore;
|
|
12
|
+
}
|
|
13
|
+
export declare function resolveBillingForProject(deps: ResolveBillingDeps): Promise<StatusBillingDTO | undefined>;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { buildStatusBilling } from './build-status-billing.js';
|
|
2
|
+
const BYTEROVER_PROVIDER_ID = 'byterover';
|
|
3
|
+
export async function resolveBillingForProject(deps) {
|
|
4
|
+
const activeProvider = await deps.providerConfigStore.getActiveProvider().catch(() => '');
|
|
5
|
+
const token = deps.authStateStore.getToken();
|
|
6
|
+
if (!token?.isValid()) {
|
|
7
|
+
return buildStatusBilling({ activeProvider, isAuthenticated: false, paidUsages: [] });
|
|
8
|
+
}
|
|
9
|
+
if (activeProvider !== BYTEROVER_PROVIDER_ID) {
|
|
10
|
+
return buildStatusBilling({ activeProvider, isAuthenticated: true, paidUsages: [] });
|
|
11
|
+
}
|
|
12
|
+
const { sessionKey } = token;
|
|
13
|
+
const [pinnedTeamId, usagesResult] = await Promise.all([
|
|
14
|
+
deps
|
|
15
|
+
.billingConfigStoreFactory(deps.projectPath)
|
|
16
|
+
.getPinnedTeamId()
|
|
17
|
+
.catch(() => undefined),
|
|
18
|
+
deps.billingService
|
|
19
|
+
.getUsages(sessionKey)
|
|
20
|
+
.then((usages) => ({ ok: true, usages }))
|
|
21
|
+
.catch(() => ({ ok: false })),
|
|
22
|
+
]);
|
|
23
|
+
if (!usagesResult.ok)
|
|
24
|
+
return undefined;
|
|
25
|
+
const paidUsages = usagesResult.usages.filter((u) => u.tier !== 'FREE');
|
|
26
|
+
const freeUserLimit = paidUsages.length === 0
|
|
27
|
+
? await deps.billingService.getFreeUserLimit(sessionKey).catch(() => undefined)
|
|
28
|
+
: undefined;
|
|
29
|
+
return buildStatusBilling({
|
|
30
|
+
activeProvider,
|
|
31
|
+
freeUserLimit,
|
|
32
|
+
isAuthenticated: true,
|
|
33
|
+
paidUsages,
|
|
34
|
+
pinnedTeamId,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { AGENT_CONNECTOR_CONFIG } from '../../../core/domain/entities/agent.js';
|
|
3
|
-
import { hasMcpToolsInBrvSection } from '../shared/constants.js';
|
|
3
|
+
import { extractInstalledAgentFromBrvSection, hasMcpToolsInBrvSection } from '../shared/constants.js';
|
|
4
4
|
import { RuleFileManager } from '../shared/rule-file-manager.js';
|
|
5
5
|
import { RULES_CONNECTOR_CONFIGS } from './rules-connector-config.js';
|
|
6
6
|
/**
|
|
@@ -99,7 +99,12 @@ export class RulesConnector {
|
|
|
99
99
|
}
|
|
100
100
|
const content = await this.fileService.read(fullPath);
|
|
101
101
|
const hasMcpTools = hasMcpToolsInBrvSection(content);
|
|
102
|
-
const
|
|
102
|
+
const footerAgent = extractInstalledAgentFromBrvSection(content);
|
|
103
|
+
// Footer present: only the agent named in the footer owns this rule file.
|
|
104
|
+
// Footer absent (legacy file pre-footer): fall back to marker presence so
|
|
105
|
+
// existing installs keep reporting installed until the next reinstall.
|
|
106
|
+
const matchesFooter = footerAgent === undefined ? true : footerAgent === agent;
|
|
107
|
+
const installed = hasMarkers && !hasMcpTools && matchesFooter;
|
|
103
108
|
return {
|
|
104
109
|
configExists: true,
|
|
105
110
|
configPath: config.filePath,
|
|
@@ -15,3 +15,12 @@ export declare const BRV_RULE_MARKERS: {
|
|
|
15
15
|
* Only checks within the markers section to avoid false positives from user content.
|
|
16
16
|
*/
|
|
17
17
|
export declare const hasMcpToolsInBrvSection: (content: string) => boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Extracts the agent name from a `Generated by ByteRover CLI for X` footer
|
|
20
|
+
* inside the BRV markers section. Used to disambiguate which agent owns a
|
|
21
|
+
* shared rule file (Amp / Codex / OpenCode all map to AGENTS.md).
|
|
22
|
+
*
|
|
23
|
+
* Returns undefined when markers are missing, when the footer is absent
|
|
24
|
+
* (legacy pre-footer installs), or when the footer line is empty.
|
|
25
|
+
*/
|
|
26
|
+
export declare const extractInstalledAgentFromBrvSection: (content: string) => string | undefined;
|
|
@@ -10,16 +10,42 @@ export const BRV_RULE_MARKERS = {
|
|
|
10
10
|
END: '<!-- END BYTEROVER RULES -->',
|
|
11
11
|
START: '<!-- BEGIN BYTEROVER RULES -->',
|
|
12
12
|
};
|
|
13
|
+
const sliceBrvSection = (content) => {
|
|
14
|
+
const startIdx = content.indexOf(BRV_RULE_MARKERS.START);
|
|
15
|
+
const endIdx = content.indexOf(BRV_RULE_MARKERS.END);
|
|
16
|
+
if (startIdx === -1 || endIdx === -1 || endIdx < startIdx)
|
|
17
|
+
return undefined;
|
|
18
|
+
return content.slice(startIdx, endIdx);
|
|
19
|
+
};
|
|
13
20
|
/**
|
|
14
21
|
* Checks if the BRV markers section contains MCP tool references (brv-query/brv-curate).
|
|
15
22
|
* Only checks within the markers section to avoid false positives from user content.
|
|
16
23
|
*/
|
|
17
24
|
export const hasMcpToolsInBrvSection = (content) => {
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
if (startIdx === -1 || endIdx === -1)
|
|
25
|
+
const brvSection = sliceBrvSection(content);
|
|
26
|
+
if (brvSection === undefined)
|
|
21
27
|
return false;
|
|
22
|
-
// eslint-disable-next-line unicorn/prefer-set-has
|
|
23
|
-
const brvSection = content.slice(startIdx, endIdx);
|
|
24
28
|
return brvSection.includes('brv-query') || brvSection.includes('brv-curate');
|
|
25
29
|
};
|
|
30
|
+
/**
|
|
31
|
+
* Extracts the agent name from a `Generated by ByteRover CLI for X` footer
|
|
32
|
+
* inside the BRV markers section. Used to disambiguate which agent owns a
|
|
33
|
+
* shared rule file (Amp / Codex / OpenCode all map to AGENTS.md).
|
|
34
|
+
*
|
|
35
|
+
* Returns undefined when markers are missing, when the footer is absent
|
|
36
|
+
* (legacy pre-footer installs), or when the footer line is empty.
|
|
37
|
+
*/
|
|
38
|
+
export const extractInstalledAgentFromBrvSection = (content) => {
|
|
39
|
+
const brvSection = sliceBrvSection(content);
|
|
40
|
+
if (brvSection === undefined)
|
|
41
|
+
return undefined;
|
|
42
|
+
const tagWithDelimiter = `${BRV_RULE_TAG} `;
|
|
43
|
+
const tagIdx = brvSection.indexOf(tagWithDelimiter);
|
|
44
|
+
if (tagIdx === -1)
|
|
45
|
+
return undefined;
|
|
46
|
+
const afterTag = brvSection.slice(tagIdx + tagWithDelimiter.length);
|
|
47
|
+
const newlineIdx = afterTag.indexOf('\n');
|
|
48
|
+
const agentLine = newlineIdx === -1 ? afterTag : afterTag.slice(0, newlineIdx);
|
|
49
|
+
const agent = agentLine.trim();
|
|
50
|
+
return agent.length === 0 ? undefined : agent;
|
|
51
|
+
};
|
|
@@ -133,7 +133,7 @@ async function activateExistingSession(sessionId, providerId) {
|
|
|
133
133
|
*/
|
|
134
134
|
let cachedSessionKey = '';
|
|
135
135
|
let cachedBrvConfig;
|
|
136
|
-
let
|
|
136
|
+
let cachedPinnedOrgId;
|
|
137
137
|
let cachedSpaceId = '';
|
|
138
138
|
let cachedActiveProvider = '';
|
|
139
139
|
let cachedActiveModel = '';
|
|
@@ -177,15 +177,16 @@ async function start() {
|
|
|
177
177
|
transport.on('connect_error', (err) => {
|
|
178
178
|
agentLog(`Transport connect_error: ${err?.message ?? 'unknown'}`);
|
|
179
179
|
});
|
|
180
|
-
const [configResult, authResult, providerResult] = await Promise.all([
|
|
180
|
+
const [configResult, authResult, providerResult, billingResult] = await Promise.all([
|
|
181
181
|
transport.requestWithAck(TransportStateEventNames.GET_PROJECT_CONFIG, { projectPath }),
|
|
182
182
|
transport.requestWithAck(TransportStateEventNames.GET_AUTH),
|
|
183
183
|
transport.requestWithAck(TransportStateEventNames.GET_PROVIDER_CONFIG),
|
|
184
|
+
transport.requestWithAck(TransportStateEventNames.GET_BILLING_CONFIG, { projectPath }),
|
|
184
185
|
]);
|
|
185
186
|
cachedBrvConfig = configResult.brvConfig;
|
|
186
|
-
cachedTeamId = configResult.teamId ?? '';
|
|
187
187
|
cachedSpaceId = configResult.spaceId ?? '';
|
|
188
188
|
cachedSessionKey = authResult.sessionKey ?? '';
|
|
189
|
+
cachedPinnedOrgId = billingResult.pinnedTeamId;
|
|
189
190
|
agentLog('Initial config loaded from state server');
|
|
190
191
|
// 3. Listen for config/auth/provider updates from daemon
|
|
191
192
|
transport.on('config:updated', (data) => {
|
|
@@ -193,8 +194,6 @@ async function start() {
|
|
|
193
194
|
return;
|
|
194
195
|
if (data.brvConfig)
|
|
195
196
|
cachedBrvConfig = data.brvConfig;
|
|
196
|
-
if (data.teamId !== undefined)
|
|
197
|
-
cachedTeamId = data.teamId;
|
|
198
197
|
if (data.spaceId !== undefined)
|
|
199
198
|
cachedSpaceId = data.spaceId;
|
|
200
199
|
});
|
|
@@ -206,6 +205,11 @@ async function start() {
|
|
|
206
205
|
providerConfigDirty = true;
|
|
207
206
|
providerFetchRetries = 0;
|
|
208
207
|
});
|
|
208
|
+
transport.on(TransportDaemonEventNames.BILLING_PIN_CHANGED, (data) => {
|
|
209
|
+
if (data.projectPath !== projectPath)
|
|
210
|
+
return;
|
|
211
|
+
cachedPinnedOrgId = data.teamId;
|
|
212
|
+
});
|
|
209
213
|
// 4. Provider config resolved by daemon (API key, base URL, headers, etc.)
|
|
210
214
|
const { activeModel, activeProvider } = providerResult;
|
|
211
215
|
cachedActiveProvider = activeProvider;
|
|
@@ -244,7 +248,7 @@ async function start() {
|
|
|
244
248
|
projectIdProvider: () => PROJECT,
|
|
245
249
|
sessionKeyProvider: () => cachedSessionKey,
|
|
246
250
|
spaceIdProvider: () => cachedSpaceId,
|
|
247
|
-
teamIdProvider: () =>
|
|
251
|
+
teamIdProvider: () => cachedPinnedOrgId ?? '',
|
|
248
252
|
transportClient: transport,
|
|
249
253
|
});
|
|
250
254
|
await agent.start();
|
|
@@ -393,8 +397,6 @@ async function executeTask(task, curateExecutor, folderPackExecutor, queryExecut
|
|
|
393
397
|
const configResult = await transport.requestWithAck(TransportStateEventNames.GET_PROJECT_CONFIG, { projectPath });
|
|
394
398
|
if (configResult.brvConfig)
|
|
395
399
|
cachedBrvConfig = configResult.brvConfig;
|
|
396
|
-
if (configResult.teamId !== undefined)
|
|
397
|
-
cachedTeamId = configResult.teamId;
|
|
398
400
|
if (configResult.spaceId !== undefined)
|
|
399
401
|
cachedSpaceId = configResult.spaceId;
|
|
400
402
|
}
|
|
@@ -34,6 +34,7 @@ import { TransportStateEventNames, TransportTaskEventNames, } from '../../core/d
|
|
|
34
34
|
import { getGlobalDataDir } from '../../utils/global-data-path.js';
|
|
35
35
|
import { getProjectDataDir } from '../../utils/path-utils.js';
|
|
36
36
|
import { crashLog, processLog } from '../../utils/process-logger.js';
|
|
37
|
+
import { createBillingStateHandler } from '../billing/billing-state-endpoint.js';
|
|
37
38
|
import { ClientManager } from '../client/client-manager.js';
|
|
38
39
|
import { ProjectConfigStore } from '../config/file-config-store.js';
|
|
39
40
|
import { readContextTreeRemoteUrl } from '../context-tree/read-context-tree-remote.js';
|
|
@@ -54,6 +55,7 @@ import { clearStaleProviderConfig, resolveProviderConfig } from '../provider/pro
|
|
|
54
55
|
import { ProjectRouter } from '../routing/project-router.js';
|
|
55
56
|
import { AuthStateStore } from '../state/auth-state-store.js';
|
|
56
57
|
import { ProjectStateLoader } from '../state/project-state-loader.js';
|
|
58
|
+
import { FileBillingConfigStore } from '../storage/file-billing-config-store.js';
|
|
57
59
|
import { FileCurateLogStore } from '../storage/file-curate-log-store.js';
|
|
58
60
|
import { FileProviderConfigStore } from '../storage/file-provider-config-store.js';
|
|
59
61
|
import { FileReviewBackupStore } from '../storage/file-review-backup-store.js';
|
|
@@ -360,6 +362,8 @@ async function main() {
|
|
|
360
362
|
await clearStaleProviderConfig(providerConfigStore, providerKeychainStore, providerOAuthTokenStore);
|
|
361
363
|
// State endpoint: provider config — agents request this on startup and after provider:updated
|
|
362
364
|
transportServer.onRequest(TransportStateEventNames.GET_PROVIDER_CONFIG, async () => resolveProviderConfig({ authStateStore, providerConfigStore, providerKeychainStore, tokenRefreshManager }));
|
|
365
|
+
const billingConfigStoreFactory = (projectPath) => new FileBillingConfigStore({ baseDir: join(projectPath, BRV_DIR) });
|
|
366
|
+
transportServer.onRequest(TransportStateEventNames.GET_BILLING_CONFIG, createBillingStateHandler(billingConfigStoreFactory));
|
|
363
367
|
const transportHandlers = new TransportHandlers({
|
|
364
368
|
agentPool,
|
|
365
369
|
clientManager,
|
|
@@ -573,6 +577,7 @@ async function main() {
|
|
|
573
577
|
// without waiting for OIDC discovery (~400ms).
|
|
574
578
|
await setupFeatureHandlers({
|
|
575
579
|
authStateStore,
|
|
580
|
+
billingConfigStoreFactory,
|
|
576
581
|
broadcastToProject(projectPath, event, data) {
|
|
577
582
|
broadcastToProjectRoom(projectRegistry, projectRouter, projectPath, event, data);
|
|
578
583
|
},
|
|
@@ -361,17 +361,23 @@ export class OpenAICompatibleModelFetcher {
|
|
|
361
361
|
const modelList = Array.isArray(responseData)
|
|
362
362
|
? responseData
|
|
363
363
|
: (responseData.data ?? responseData.models ?? []);
|
|
364
|
-
const
|
|
364
|
+
const uniqueModels = new Map();
|
|
365
|
+
for (const model of modelList) {
|
|
365
366
|
const id = String(model.id ?? model.name ?? '');
|
|
366
|
-
|
|
367
|
+
if (!id)
|
|
368
|
+
continue;
|
|
369
|
+
if (uniqueModels.has(id))
|
|
370
|
+
continue;
|
|
371
|
+
uniqueModels.set(id, {
|
|
367
372
|
contextLength: typeof model.context_length === 'number' ? model.context_length : 128_000,
|
|
368
373
|
id,
|
|
369
374
|
isFree: false,
|
|
370
375
|
name: id,
|
|
371
376
|
pricing: { inputPerM: 0, outputPerM: 0 },
|
|
372
377
|
provider: this.providerName,
|
|
373
|
-
};
|
|
374
|
-
}
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
const models = [...uniqueModels.values()];
|
|
375
381
|
// Sort by ID
|
|
376
382
|
models.sort((a, b) => a.id.localeCompare(b.id));
|
|
377
383
|
this.cache = { models, timestamp: Date.now() };
|
|
@@ -9,10 +9,12 @@ import type { IProviderKeychainStore } from '../../core/interfaces/i-provider-ke
|
|
|
9
9
|
import type { IProviderOAuthTokenStore } from '../../core/interfaces/i-provider-oauth-token-store.js';
|
|
10
10
|
import type { IProjectRegistry } from '../../core/interfaces/project/i-project-registry.js';
|
|
11
11
|
import type { IAuthStateStore } from '../../core/interfaces/state/i-auth-state-store.js';
|
|
12
|
+
import type { IBillingConfigStore } from '../../core/interfaces/storage/i-billing-config-store.js';
|
|
12
13
|
import type { ITransportServer } from '../../core/interfaces/transport/i-transport-server.js';
|
|
13
14
|
import type { ProjectBroadcaster, ProjectPathResolver } from '../transport/handlers/handler-types.js';
|
|
14
15
|
export interface FeatureHandlersOptions {
|
|
15
16
|
authStateStore: IAuthStateStore;
|
|
17
|
+
billingConfigStoreFactory: (projectPath: string) => IBillingConfigStore;
|
|
16
18
|
broadcastToProject: ProjectBroadcaster;
|
|
17
19
|
getActiveProjectPaths: () => string[];
|
|
18
20
|
log: (msg: string) => void;
|
|
@@ -28,4 +30,4 @@ export interface FeatureHandlersOptions {
|
|
|
28
30
|
* Setup all feature handlers on the transport server.
|
|
29
31
|
* These handlers implement the TUI ↔ Server event contract (auth:*, config:*, status:*, etc.).
|
|
30
32
|
*/
|
|
31
|
-
export declare function setupFeatureHandlers({ authStateStore, broadcastToProject, getActiveProjectPaths, log, projectRegistry, providerConfigStore, providerKeychainStore, providerOAuthTokenStore, resolveProjectPath, transport, webuiPort, }: FeatureHandlersOptions): Promise<void>;
|
|
33
|
+
export declare function setupFeatureHandlers({ authStateStore, billingConfigStoreFactory, broadcastToProject, getActiveProjectPaths, log, projectRegistry, providerConfigStore, providerKeychainStore, providerOAuthTokenStore, resolveProjectPath, transport, webuiPort, }: FeatureHandlersOptions): Promise<void>;
|
|
@@ -10,9 +10,12 @@ import { ReviewEvents } from '../../../shared/transport/events/review-events.js'
|
|
|
10
10
|
import { getAuthConfig } from '../../config/auth.config.js';
|
|
11
11
|
import { getCurrentConfig } from '../../config/environment.js';
|
|
12
12
|
import { API_V1_PATH, BRV_DIR } from '../../constants.js';
|
|
13
|
+
import { TransportStateEventNames } from '../../core/domain/transport/schemas.js';
|
|
13
14
|
import { getProjectDataDir } from '../../utils/path-utils.js';
|
|
14
15
|
import { OAuthService } from '../auth/oauth-service.js';
|
|
15
16
|
import { OidcDiscoveryService } from '../auth/oidc-discovery-service.js';
|
|
17
|
+
import { HttpBillingService } from '../billing/http-billing-service.js';
|
|
18
|
+
import { createPaidOrganizationsHandler } from '../billing/paid-organizations-endpoint.js';
|
|
16
19
|
import { SystemBrowserLauncher } from '../browser/system-browser-launcher.js';
|
|
17
20
|
import { HttpCogitPullService } from '../cogit/http-cogit-pull-service.js';
|
|
18
21
|
import { HttpCogitPushService } from '../cogit/http-cogit-push-service.js';
|
|
@@ -37,23 +40,25 @@ import { FileReviewBackupStore } from '../storage/file-review-backup-store.js';
|
|
|
37
40
|
import { createTokenStore } from '../storage/token-store.js';
|
|
38
41
|
import { HttpTeamService } from '../team/http-team-service.js';
|
|
39
42
|
import { FsTemplateLoader } from '../template/fs-template-loader.js';
|
|
40
|
-
import { AuthHandler, ConfigHandler, ConnectorsHandler, ContextTreeHandler, HubHandler, InitHandler, LocationsHandler, ModelHandler, ProviderHandler, PullHandler, PushHandler, ResetHandler, ReviewHandler, SourceHandler, SpaceHandler, StatusHandler, VcHandler, WorktreeHandler, } from '../transport/handlers/index.js';
|
|
43
|
+
import { AuthHandler, BillingHandler, ConfigHandler, ConnectorsHandler, ContextTreeHandler, HubHandler, InitHandler, LocationsHandler, ModelHandler, ProviderHandler, PullHandler, PushHandler, ResetHandler, ReviewHandler, SourceHandler, SpaceHandler, StatusHandler, TeamHandler, VcHandler, WorktreeHandler, } from '../transport/handlers/index.js';
|
|
41
44
|
import { HttpUserService } from '../user/http-user-service.js';
|
|
42
45
|
import { FileVcGitConfigStore } from '../vc/file-vc-git-config-store.js';
|
|
43
46
|
/**
|
|
44
47
|
* Setup all feature handlers on the transport server.
|
|
45
48
|
* These handlers implement the TUI ↔ Server event contract (auth:*, config:*, status:*, etc.).
|
|
46
49
|
*/
|
|
47
|
-
export async function setupFeatureHandlers({ authStateStore, broadcastToProject, getActiveProjectPaths, log, projectRegistry, providerConfigStore, providerKeychainStore, providerOAuthTokenStore, resolveProjectPath, transport, webuiPort, }) {
|
|
50
|
+
export async function setupFeatureHandlers({ authStateStore, billingConfigStoreFactory, broadcastToProject, getActiveProjectPaths, log, projectRegistry, providerConfigStore, providerKeychainStore, providerOAuthTokenStore, resolveProjectPath, transport, webuiPort, }) {
|
|
48
51
|
const envConfig = getCurrentConfig();
|
|
49
52
|
const tokenStore = createTokenStore();
|
|
50
53
|
const projectConfigStore = new ProjectConfigStore();
|
|
51
54
|
// API version paths appended at point of use.
|
|
52
55
|
// Note: IAM and Cogit currently share this version path, but may version independently in the future.
|
|
53
56
|
const iamApiV1 = `${envConfig.iamBaseUrl}${API_V1_PATH}`;
|
|
57
|
+
const billingApiV1 = `${envConfig.billingBaseUrl}${API_V1_PATH}`;
|
|
54
58
|
const userService = new HttpUserService({ apiBaseUrl: iamApiV1 });
|
|
55
59
|
const teamService = new HttpTeamService({ apiBaseUrl: iamApiV1 });
|
|
56
60
|
const spaceService = new HttpSpaceService({ apiBaseUrl: iamApiV1 });
|
|
61
|
+
const billingService = new HttpBillingService({ apiBaseUrl: billingApiV1 });
|
|
57
62
|
// Auth handler requires async OIDC discovery
|
|
58
63
|
const discoveryService = new OidcDiscoveryService();
|
|
59
64
|
const authConfig = await getAuthConfig(discoveryService);
|
|
@@ -65,6 +70,7 @@ export async function setupFeatureHandlers({ authStateStore, broadcastToProject,
|
|
|
65
70
|
browserLauncher: new SystemBrowserLauncher(),
|
|
66
71
|
callbackHandler: new CallbackHandler(),
|
|
67
72
|
projectConfigStore,
|
|
73
|
+
providerConfigStore,
|
|
68
74
|
resolveProjectPath,
|
|
69
75
|
tokenStore,
|
|
70
76
|
transport,
|
|
@@ -78,6 +84,20 @@ export async function setupFeatureHandlers({ authStateStore, broadcastToProject,
|
|
|
78
84
|
providerOAuthTokenStore,
|
|
79
85
|
transport,
|
|
80
86
|
}).setup();
|
|
87
|
+
new BillingHandler({
|
|
88
|
+
authStateStore,
|
|
89
|
+
billingConfigStoreFactory,
|
|
90
|
+
billingService,
|
|
91
|
+
providerConfigStore,
|
|
92
|
+
resolveProjectPath,
|
|
93
|
+
transport,
|
|
94
|
+
}).setup();
|
|
95
|
+
transport.onRequest(TransportStateEventNames.GET_PAID_ORGANIZATIONS, createPaidOrganizationsHandler({ authStateStore, billingService }));
|
|
96
|
+
new TeamHandler({
|
|
97
|
+
authStateStore,
|
|
98
|
+
teamService,
|
|
99
|
+
transport,
|
|
100
|
+
}).setup();
|
|
81
101
|
new ModelHandler({
|
|
82
102
|
providerConfigStore,
|
|
83
103
|
providerKeychainStore,
|
|
@@ -100,10 +120,14 @@ export async function setupFeatureHandlers({ authStateStore, broadcastToProject,
|
|
|
100
120
|
// Project-scoped handlers (receive resolveProjectPath for client → project resolution)
|
|
101
121
|
const gitService = new IsomorphicGitService(authStateStore);
|
|
102
122
|
new StatusHandler({
|
|
123
|
+
authStateStore,
|
|
124
|
+
billingConfigStoreFactory,
|
|
125
|
+
billingService,
|
|
103
126
|
contextTreeService,
|
|
104
127
|
contextTreeSnapshotService,
|
|
105
128
|
curateLogStoreFactory: (projectPath) => new FileCurateLogStore({ baseDir: getProjectDataDir(projectPath) }),
|
|
106
129
|
projectConfigStore,
|
|
130
|
+
providerConfigStore,
|
|
107
131
|
resolveProjectPath,
|
|
108
132
|
tokenStore,
|
|
109
133
|
transport,
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { IBillingConfigStore } from '../../core/interfaces/storage/i-billing-config-store.js';
|
|
2
|
+
export interface FileBillingConfigStoreOptions {
|
|
3
|
+
baseDir: string;
|
|
4
|
+
}
|
|
5
|
+
export declare class FileBillingConfigStore implements IBillingConfigStore {
|
|
6
|
+
private readonly baseDir;
|
|
7
|
+
private readonly configPath;
|
|
8
|
+
constructor(options: FileBillingConfigStoreOptions);
|
|
9
|
+
getPinnedTeamId(): Promise<string | undefined>;
|
|
10
|
+
setPinnedTeamId(teamId: string | undefined): Promise<void>;
|
|
11
|
+
private readJson;
|
|
12
|
+
private writeJson;
|
|
13
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
const PROVIDER_CONFIG_FILE = 'brv-provider.json';
|
|
4
|
+
export class FileBillingConfigStore {
|
|
5
|
+
baseDir;
|
|
6
|
+
configPath;
|
|
7
|
+
constructor(options) {
|
|
8
|
+
this.baseDir = options.baseDir;
|
|
9
|
+
this.configPath = join(options.baseDir, PROVIDER_CONFIG_FILE);
|
|
10
|
+
}
|
|
11
|
+
async getPinnedTeamId() {
|
|
12
|
+
const json = await this.readJson();
|
|
13
|
+
return json.billing?.pinnedTeamId;
|
|
14
|
+
}
|
|
15
|
+
async setPinnedTeamId(teamId) {
|
|
16
|
+
const json = await this.readJson();
|
|
17
|
+
const billing = { ...json.billing };
|
|
18
|
+
if (teamId === undefined) {
|
|
19
|
+
delete billing.pinnedTeamId;
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
billing.pinnedTeamId = teamId;
|
|
23
|
+
}
|
|
24
|
+
const next = { ...json, billing };
|
|
25
|
+
if (Object.keys(billing).length === 0)
|
|
26
|
+
delete next.billing;
|
|
27
|
+
await this.writeJson(next);
|
|
28
|
+
}
|
|
29
|
+
async readJson() {
|
|
30
|
+
try {
|
|
31
|
+
const content = await readFile(this.configPath, 'utf8');
|
|
32
|
+
const parsed = JSON.parse(content);
|
|
33
|
+
if (!isRecord(parsed))
|
|
34
|
+
return {};
|
|
35
|
+
const result = { ...parsed };
|
|
36
|
+
if (isRecord(parsed.billing) && typeof parsed.billing.pinnedTeamId === 'string') {
|
|
37
|
+
result.billing = { pinnedTeamId: parsed.billing.pinnedTeamId };
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
delete result.billing;
|
|
41
|
+
}
|
|
42
|
+
return result;
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return {};
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
async writeJson(json) {
|
|
49
|
+
await mkdir(this.baseDir, { recursive: true });
|
|
50
|
+
await writeFile(this.configPath, JSON.stringify(json, null, 2), 'utf8');
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function isRecord(value) {
|
|
54
|
+
return typeof value === 'object' && value !== null;
|
|
55
|
+
}
|