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.
Files changed (71) hide show
  1. package/.env.production +2 -1
  2. package/dist/oclif/commands/curate/index.js +6 -0
  3. package/dist/oclif/commands/providers/connect.d.ts +26 -1
  4. package/dist/oclif/commands/providers/connect.js +95 -17
  5. package/dist/oclif/commands/providers/list.d.ts +10 -1
  6. package/dist/oclif/commands/providers/list.js +35 -3
  7. package/dist/oclif/commands/query.js +6 -0
  8. package/dist/oclif/commands/status.js +4 -0
  9. package/dist/oclif/lib/billing-line.d.ts +8 -0
  10. package/dist/oclif/lib/billing-line.js +45 -0
  11. package/dist/oclif/lib/format-billing-line.d.ts +2 -0
  12. package/dist/oclif/lib/format-billing-line.js +19 -0
  13. package/dist/oclif/lib/insufficient-credits.d.ts +11 -0
  14. package/dist/oclif/lib/insufficient-credits.js +36 -0
  15. package/dist/server/config/environment.d.ts +1 -0
  16. package/dist/server/config/environment.js +3 -0
  17. package/dist/server/core/domain/transport/schemas.d.ts +17 -0
  18. package/dist/server/core/domain/transport/schemas.js +3 -0
  19. package/dist/server/core/interfaces/services/i-billing-service.d.ts +26 -0
  20. package/dist/server/core/interfaces/services/i-billing-service.js +1 -0
  21. package/dist/server/core/interfaces/storage/i-billing-config-store.d.ts +4 -0
  22. package/dist/server/core/interfaces/storage/i-billing-config-store.js +1 -0
  23. package/dist/server/infra/billing/billing-state-endpoint.d.ts +4 -0
  24. package/dist/server/infra/billing/billing-state-endpoint.js +7 -0
  25. package/dist/server/infra/billing/build-status-billing.d.ts +9 -0
  26. package/dist/server/infra/billing/build-status-billing.js +36 -0
  27. package/dist/server/infra/billing/http-billing-service.d.ts +19 -0
  28. package/dist/server/infra/billing/http-billing-service.js +57 -0
  29. package/dist/server/infra/billing/paid-organizations-endpoint.d.ts +8 -0
  30. package/dist/server/infra/billing/paid-organizations-endpoint.js +18 -0
  31. package/dist/server/infra/billing/resolve-billing-source.d.ts +13 -0
  32. package/dist/server/infra/billing/resolve-billing-source.js +36 -0
  33. package/dist/server/infra/billing/resolve-billing-team.d.ts +5 -0
  34. package/dist/server/infra/billing/resolve-billing-team.js +8 -0
  35. package/dist/server/infra/connectors/rules/rules-connector.js +7 -2
  36. package/dist/server/infra/connectors/shared/constants.d.ts +9 -0
  37. package/dist/server/infra/connectors/shared/constants.js +31 -5
  38. package/dist/server/infra/daemon/agent-process.js +10 -8
  39. package/dist/server/infra/daemon/brv-server.js +5 -0
  40. package/dist/server/infra/http/provider-model-fetchers.js +10 -4
  41. package/dist/server/infra/process/feature-handlers.d.ts +3 -1
  42. package/dist/server/infra/process/feature-handlers.js +26 -2
  43. package/dist/server/infra/storage/file-billing-config-store.d.ts +13 -0
  44. package/dist/server/infra/storage/file-billing-config-store.js +55 -0
  45. package/dist/server/infra/transport/handlers/auth-handler.d.ts +4 -0
  46. package/dist/server/infra/transport/handlers/auth-handler.js +20 -2
  47. package/dist/server/infra/transport/handlers/billing-handler.d.ts +30 -0
  48. package/dist/server/infra/transport/handlers/billing-handler.js +132 -0
  49. package/dist/server/infra/transport/handlers/index.d.ts +4 -0
  50. package/dist/server/infra/transport/handlers/index.js +2 -0
  51. package/dist/server/infra/transport/handlers/init-handler.js +2 -0
  52. package/dist/server/infra/transport/handlers/status-handler.d.ts +14 -0
  53. package/dist/server/infra/transport/handlers/status-handler.js +16 -0
  54. package/dist/server/infra/transport/handlers/team-handler.d.ts +19 -0
  55. package/dist/server/infra/transport/handlers/team-handler.js +40 -0
  56. package/dist/shared/transport/events/auth-events.d.ts +3 -0
  57. package/dist/shared/transport/events/billing-events.d.ts +48 -0
  58. package/dist/shared/transport/events/billing-events.js +8 -0
  59. package/dist/shared/transport/events/index.d.ts +11 -0
  60. package/dist/shared/transport/events/index.js +6 -0
  61. package/dist/shared/transport/events/team-events.d.ts +8 -0
  62. package/dist/shared/transport/events/team-events.js +3 -0
  63. package/dist/shared/transport/types/dto.d.ts +80 -0
  64. package/dist/webui/assets/index-B9JmEFOK.js +130 -0
  65. package/dist/webui/assets/index-CMIKsBMr.css +1 -0
  66. package/dist/webui/index.html +2 -2
  67. package/dist/webui/sw.js +1 -1
  68. package/oclif.manifest.json +1280 -1272
  69. package/package.json +1 -1
  70. package/dist/webui/assets/index-DyVvFoM6.css +0 -1
  71. 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,7 @@
1
+ export function createBillingStateHandler(storeFactory) {
2
+ return async (data) => {
3
+ const store = storeFactory(data.projectPath);
4
+ const pinnedTeamId = await store.getPinnedTeamId();
5
+ return pinnedTeamId === undefined ? {} : { pinnedTeamId };
6
+ };
7
+ }
@@ -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
+ }
@@ -0,0 +1,5 @@
1
+ export interface BillingTeamResolverInput {
2
+ paidOrganizationIds: readonly string[];
3
+ pinnedTeamId?: string;
4
+ }
5
+ export declare function resolveBillingTeamId(input: BillingTeamResolverInput): string | undefined;
@@ -0,0 +1,8 @@
1
+ export function resolveBillingTeamId(input) {
2
+ const { paidOrganizationIds, pinnedTeamId } = input;
3
+ if (pinnedTeamId)
4
+ return pinnedTeamId;
5
+ if (paidOrganizationIds.length === 1)
6
+ return paidOrganizationIds[0];
7
+ return undefined;
8
+ }
@@ -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 installed = hasMarkers && !hasMcpTools;
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 startIdx = content.indexOf(BRV_RULE_MARKERS.START);
19
- const endIdx = content.indexOf(BRV_RULE_MARKERS.END);
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 cachedTeamId = '';
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: () => cachedTeamId,
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 models = modelList.map((model) => {
364
+ const uniqueModels = new Map();
365
+ for (const model of modelList) {
365
366
  const id = String(model.id ?? model.name ?? '');
366
- return {
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
+ }