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.
Files changed (102) hide show
  1. package/.env.production +2 -1
  2. package/dist/agent/infra/tools/implementations/curate-tool.js +18 -8
  3. package/dist/oclif/commands/curate/index.js +6 -0
  4. package/dist/oclif/commands/providers/connect.d.ts +26 -1
  5. package/dist/oclif/commands/providers/connect.js +95 -17
  6. package/dist/oclif/commands/providers/list.d.ts +10 -1
  7. package/dist/oclif/commands/providers/list.js +35 -3
  8. package/dist/oclif/commands/query.js +6 -0
  9. package/dist/oclif/commands/status.js +4 -0
  10. package/dist/oclif/lib/billing-line.d.ts +8 -0
  11. package/dist/oclif/lib/billing-line.js +45 -0
  12. package/dist/oclif/lib/format-billing-line.d.ts +2 -0
  13. package/dist/oclif/lib/format-billing-line.js +19 -0
  14. package/dist/oclif/lib/insufficient-credits.d.ts +11 -0
  15. package/dist/oclif/lib/insufficient-credits.js +36 -0
  16. package/dist/server/config/environment.d.ts +1 -0
  17. package/dist/server/config/environment.js +3 -0
  18. package/dist/server/constants.d.ts +6 -0
  19. package/dist/server/constants.js +11 -0
  20. package/dist/server/core/domain/entities/task-history-entry.d.ts +775 -0
  21. package/dist/server/core/domain/entities/task-history-entry.js +88 -0
  22. package/dist/server/core/domain/transport/schemas.d.ts +1420 -11
  23. package/dist/server/core/domain/transport/schemas.js +160 -6
  24. package/dist/server/core/domain/transport/task-info.d.ts +18 -0
  25. package/dist/server/core/interfaces/process/i-task-lifecycle-hook.d.ts +7 -0
  26. package/dist/server/core/interfaces/services/i-billing-service.d.ts +26 -0
  27. package/dist/server/core/interfaces/services/i-billing-service.js +1 -0
  28. package/dist/server/core/interfaces/storage/i-billing-config-store.d.ts +4 -0
  29. package/dist/server/core/interfaces/storage/i-billing-config-store.js +1 -0
  30. package/dist/server/core/interfaces/storage/i-task-history-store.d.ts +62 -0
  31. package/dist/server/core/interfaces/storage/i-task-history-store.js +1 -0
  32. package/dist/server/infra/billing/billing-state-endpoint.d.ts +4 -0
  33. package/dist/server/infra/billing/billing-state-endpoint.js +7 -0
  34. package/dist/server/infra/billing/build-status-billing.d.ts +9 -0
  35. package/dist/server/infra/billing/build-status-billing.js +36 -0
  36. package/dist/server/infra/billing/http-billing-service.d.ts +19 -0
  37. package/dist/server/infra/billing/http-billing-service.js +57 -0
  38. package/dist/server/infra/billing/paid-organizations-endpoint.d.ts +8 -0
  39. package/dist/server/infra/billing/paid-organizations-endpoint.js +18 -0
  40. package/dist/server/infra/billing/resolve-billing-source.d.ts +13 -0
  41. package/dist/server/infra/billing/resolve-billing-source.js +36 -0
  42. package/dist/server/infra/billing/resolve-billing-team.d.ts +5 -0
  43. package/dist/server/infra/billing/resolve-billing-team.js +8 -0
  44. package/dist/server/infra/connectors/rules/rules-connector.js +7 -2
  45. package/dist/server/infra/connectors/shared/constants.d.ts +9 -0
  46. package/dist/server/infra/connectors/shared/constants.js +31 -5
  47. package/dist/server/infra/daemon/agent-process.js +10 -8
  48. package/dist/server/infra/daemon/brv-server.js +48 -18
  49. package/dist/server/infra/dream/dream-response-schemas.d.ts +24 -0
  50. package/dist/server/infra/dream/dream-response-schemas.js +7 -0
  51. package/dist/server/infra/dream/operations/consolidate.js +21 -8
  52. package/dist/server/infra/dream/operations/synthesize.js +35 -8
  53. package/dist/server/infra/http/provider-model-fetchers.js +10 -4
  54. package/dist/server/infra/process/feature-handlers.d.ts +3 -1
  55. package/dist/server/infra/process/feature-handlers.js +26 -2
  56. package/dist/server/infra/process/task-history-entry-builder.d.ts +36 -0
  57. package/dist/server/infra/process/task-history-entry-builder.js +101 -0
  58. package/dist/server/infra/process/task-history-hook.d.ts +37 -0
  59. package/dist/server/infra/process/task-history-hook.js +70 -0
  60. package/dist/server/infra/process/task-history-store-cache.d.ts +25 -0
  61. package/dist/server/infra/process/task-history-store-cache.js +106 -0
  62. package/dist/server/infra/process/task-router.d.ts +72 -0
  63. package/dist/server/infra/process/task-router.js +690 -15
  64. package/dist/server/infra/process/transport-handlers.d.ts +8 -0
  65. package/dist/server/infra/process/transport-handlers.js +2 -0
  66. package/dist/server/infra/storage/file-billing-config-store.d.ts +13 -0
  67. package/dist/server/infra/storage/file-billing-config-store.js +55 -0
  68. package/dist/server/infra/storage/file-task-history-store.d.ts +294 -0
  69. package/dist/server/infra/storage/file-task-history-store.js +912 -0
  70. package/dist/server/infra/transport/handlers/auth-handler.d.ts +4 -0
  71. package/dist/server/infra/transport/handlers/auth-handler.js +20 -2
  72. package/dist/server/infra/transport/handlers/billing-handler.d.ts +30 -0
  73. package/dist/server/infra/transport/handlers/billing-handler.js +132 -0
  74. package/dist/server/infra/transport/handlers/index.d.ts +4 -0
  75. package/dist/server/infra/transport/handlers/index.js +2 -0
  76. package/dist/server/infra/transport/handlers/init-handler.js +2 -0
  77. package/dist/server/infra/transport/handlers/status-handler.d.ts +14 -0
  78. package/dist/server/infra/transport/handlers/status-handler.js +16 -0
  79. package/dist/server/infra/transport/handlers/team-handler.d.ts +19 -0
  80. package/dist/server/infra/transport/handlers/team-handler.js +40 -0
  81. package/dist/shared/transport/events/auth-events.d.ts +3 -0
  82. package/dist/shared/transport/events/billing-events.d.ts +48 -0
  83. package/dist/shared/transport/events/billing-events.js +8 -0
  84. package/dist/shared/transport/events/index.d.ts +16 -0
  85. package/dist/shared/transport/events/index.js +6 -0
  86. package/dist/shared/transport/events/task-events.d.ts +204 -1
  87. package/dist/shared/transport/events/task-events.js +11 -0
  88. package/dist/shared/transport/events/team-events.d.ts +8 -0
  89. package/dist/shared/transport/events/team-events.js +3 -0
  90. package/dist/shared/transport/types/dto.d.ts +80 -0
  91. package/dist/tui/features/tasks/hooks/use-task-subscriptions.js +7 -0
  92. package/dist/tui/features/tasks/stores/tasks-store.d.ts +4 -16
  93. package/dist/tui/features/tasks/stores/tasks-store.js +7 -0
  94. package/dist/tui/types/messages.d.ts +2 -9
  95. package/dist/webui/assets/index-B9JmEFOK.js +130 -0
  96. package/dist/webui/assets/index-CMIKsBMr.css +1 -0
  97. package/dist/webui/index.html +2 -2
  98. package/dist/webui/sw.js +1 -1
  99. package/oclif.manifest.json +653 -645
  100. package/package.json +1 -1
  101. package/dist/webui/assets/index--sXE__bc.css +0 -1
  102. package/dist/webui/assets/index-Bkkx961b.js +0 -130
@@ -9,6 +9,7 @@
9
9
  */
10
10
  import { z } from 'zod';
11
11
  import { QUERY_LOG_TIERS } from '../../domain/entities/query-log-entry.js';
12
+ import { TaskHistoryEntrySchema } from '../entities/task-history-entry.js';
12
13
  // ============================================================================
13
14
  // Zod Schemas for Runtime Validation (mirrors domain types)
14
15
  // ============================================================================
@@ -211,12 +212,22 @@ export const TransportTaskEventNames = {
211
212
  CANCEL: 'task:cancel',
212
213
  // Task terminal states
213
214
  CANCELLED: 'task:cancelled',
215
+ // Bulk delete terminal entries (M2.09)
216
+ CLEAR_COMPLETED: 'task:clearCompleted',
214
217
  COMPLETED: 'task:completed',
215
218
  CREATE: 'task:create',
216
219
  CREATED: 'task:created',
220
+ // Single delete (M2.09)
221
+ DELETE: 'task:delete',
222
+ // Multi delete (M2.09)
223
+ DELETE_BULK: 'task:deleteBulk',
224
+ // Broadcast on successful removal (M2.09)
225
+ DELETED: 'task:deleted',
217
226
  ERROR: 'task:error',
218
227
  // Internal (Transport → Agent)
219
228
  EXECUTE: 'task:execute',
229
+ // Single-task detail fetch (M2.09)
230
+ GET: 'task:get',
220
231
  // Snapshot query (Client → Transport)
221
232
  LIST: 'task:list',
222
233
  // Query metadata (Agent → Daemon, before task:completed)
@@ -267,6 +278,8 @@ export const TransportAgentEventNames = {
267
278
  */
268
279
  export const TransportStateEventNames = {
269
280
  GET_AUTH: 'state:getAuth',
281
+ GET_BILLING_CONFIG: 'state:getBillingConfig',
282
+ GET_PAID_ORGANIZATIONS: 'state:getPaidOrganizations',
270
283
  GET_PROJECT_CONFIG: 'state:getProjectConfig',
271
284
  GET_PROVIDER_CONFIG: 'state:getProviderConfig',
272
285
  };
@@ -275,6 +288,7 @@ export const TransportStateEventNames = {
275
288
  * Used to notify agent child processes of global state changes.
276
289
  */
277
290
  export const TransportDaemonEventNames = {
291
+ BILLING_PIN_CHANGED: 'billing:pinChanged',
278
292
  PROVIDER_UPDATED: 'provider:updated',
279
293
  };
280
294
  /**
@@ -386,6 +400,10 @@ export const TaskCreatedSchema = z.object({
386
400
  files: z.array(z.string()).optional(),
387
401
  /** Folder path for curate-folder task type */
388
402
  folderPath: z.string().optional(),
403
+ /** Active model id at task creation time */
404
+ model: z.string().optional(),
405
+ /** Active provider id at task creation time */
406
+ provider: z.string().optional(),
389
407
  /** Unique task identifier */
390
408
  taskId: z.string(),
391
409
  /** Task type */
@@ -443,7 +461,9 @@ export const TaskQueryResultEventSchema = z.object({
443
461
  })
444
462
  .optional(),
445
463
  taskId: z.string(),
446
- tier: z.custom((val) => new Set(QUERY_LOG_TIERS).has(val), { message: 'Invalid query log tier' }),
464
+ tier: z.custom((val) => new Set(QUERY_LOG_TIERS).has(val), {
465
+ message: 'Invalid query log tier',
466
+ }),
447
467
  timing: z.object({ durationMs: z.number() }),
448
468
  });
449
469
  /**
@@ -563,11 +583,38 @@ export const TaskCancelResponseSchema = z.object({
563
583
  /**
564
584
  * task:list - Snapshot of active and recently-completed tasks for a project.
565
585
  * Used by the web UI Tasks tab to populate state without replaying history.
566
- */
567
- export const TaskListRequestSchema = z.object({
568
- /** Optional project filter defaults to caller's registered project */
586
+ *
587
+ * M2.16: cursor pagination dropped; numbered pagination (page/pageSize) +
588
+ * full filter dimensions (search/provider/model/time/duration).
589
+ */
590
+ export const TaskListRequestSchema = z
591
+ .object({
592
+ /** Created at >= this epoch ms (M2.16). */
593
+ createdAfter: z.number().optional(),
594
+ /** Created at <= this epoch ms (M2.16). */
595
+ createdBefore: z.number().optional(),
596
+ /** Maximum elapsed time (ms) for terminal tasks (M2.16). */
597
+ maxDurationMs: z.number().optional(),
598
+ /** Minimum elapsed time (ms) for terminal tasks; only matches startedAt+completedAt rows (M2.16). */
599
+ minDurationMs: z.number().optional(),
600
+ /** Optional model id filter (M2.16). */
601
+ model: z.array(z.string()).optional(),
602
+ /** 1-based page index — server clamps to >= 1; defaults to 1 (M2.16). */
603
+ page: z.number().int().min(1).optional(),
604
+ /** Page size — server clamps to 1..1000; defaults to 50 (M2.16). */
605
+ pageSize: z.number().int().min(1).max(1000).optional(),
606
+ /** Optional project filter — defaults to caller's registered project. */
569
607
  projectPath: z.string().optional(),
570
- });
608
+ /** Optional provider id filter (M2.16). */
609
+ provider: z.array(z.string()).optional(),
610
+ /** Case-insensitive substring search over content + result + error.message (M2.16). */
611
+ searchText: z.string().optional(),
612
+ /** Optional status filter — return only tasks whose status matches one of these. */
613
+ status: z.array(z.enum(['cancelled', 'completed', 'created', 'error', 'started'])).optional(),
614
+ /** Optional task-type filter — e.g. ['curate'], ['query']. */
615
+ type: z.array(z.string()).optional(),
616
+ })
617
+ .strict();
571
618
  export const TaskListItemStatusSchema = z.enum(['cancelled', 'completed', 'created', 'error', 'started']);
572
619
  export const TaskListItemSchema = z.object({
573
620
  completedAt: z.number().optional(),
@@ -578,15 +625,122 @@ export const TaskListItemSchema = z.object({
578
625
  files: z.array(z.string()).optional(),
579
626
  /** Folder path for `curate-folder` tasks */
580
627
  folderPath: z.string().optional(),
628
+ /** Active model id at task creation time */
629
+ model: z.string().optional(),
581
630
  projectPath: z.string().optional(),
631
+ /** Active provider id at task creation time */
632
+ provider: z.string().optional(),
633
+ /**
634
+ * Result string. Only present for in-memory completed tasks (toListItem
635
+ * populates from TaskInfo.result). Persisted entries from the index do not
636
+ * carry result by 2-tier design — detail panel uses task:get for full text.
637
+ */
582
638
  result: z.string().optional(),
583
639
  startedAt: z.number().optional(),
584
640
  status: TaskListItemStatusSchema,
585
641
  taskId: z.string(),
586
642
  type: z.string(),
587
643
  });
588
- export const TaskListResponseSchema = z.object({
644
+ /** Status histogram used by FE filter-bar breakdown (M2.16). */
645
+ export const TaskListCountsSchema = z.object({
646
+ all: z.number().int().nonnegative(),
647
+ cancelled: z.number().int().nonnegative(),
648
+ completed: z.number().int().nonnegative(),
649
+ /** Tasks with status === 'error'. */
650
+ failed: z.number().int().nonnegative(),
651
+ /** Tasks with status === 'created' || 'started'. */
652
+ running: z.number().int().nonnegative(),
653
+ });
654
+ /** (providerId, modelId) pair from history (M2.16). */
655
+ export const TaskListAvailableModelSchema = z.object({
656
+ modelId: z.string(),
657
+ providerId: z.string(),
658
+ });
659
+ export const TaskListResponseSchema = z
660
+ .object({
661
+ /** Distinct (providerId, modelId) pairs in candidate set. History-derived. */
662
+ availableModels: z.array(TaskListAvailableModelSchema),
663
+ /** Distinct providerId values in candidate set. History-derived (includes uninstalled). */
664
+ availableProviders: z.array(z.string()),
665
+ /**
666
+ * Status histogram matching current filter scope (Model A — post-filter,
667
+ * `counts.all === total` invariant). FE filter-bar chip count = visible
668
+ * row count.
669
+ */
670
+ counts: TaskListCountsSchema,
671
+ /**
672
+ * 1-based page index, echoed back as-sent. Server clamps lower bound only
673
+ * (page < 1 → 1). NOT clamped against `pageCount`: a request for `page=9999`
674
+ * against a 1-page result returns `{page: 9999, tasks: []}` so the caller
675
+ * can detect an out-of-range page and correct itself.
676
+ */
677
+ page: z.number().int().min(1),
678
+ /** Total page count = max(ceil(total/pageSize), 1). */
679
+ pageCount: z.number().int().min(1),
680
+ /** Page size echoed back, clamped to [1, 1000]. */
681
+ pageSize: z.number().int().min(1).max(1000),
682
+ /** Page slice of items after all filters. */
589
683
  tasks: z.array(TaskListItemSchema),
684
+ /** Total count of items matching ALL filters (incl. status). */
685
+ total: z.number().int().nonnegative(),
686
+ })
687
+ .strict();
688
+ /**
689
+ * task:get — fetch full Level 2 detail for a single persisted task.
690
+ * Returns null when the task is unknown or its data file is corrupt.
691
+ */
692
+ export const TaskGetRequestSchema = z.object({
693
+ taskId: z.string(),
694
+ });
695
+ export const TaskGetResponseSchema = z.object({
696
+ task: TaskHistoryEntrySchema.nullable(),
697
+ });
698
+ /**
699
+ * task:delete — remove a single task from the per-project history store.
700
+ * Idempotent: deleting a non-existent task returns success: true.
701
+ */
702
+ export const TaskDeleteRequestSchema = z.object({
703
+ taskId: z.string(),
704
+ });
705
+ export const TaskDeleteResponseSchema = z.object({
706
+ error: z.string().optional(),
707
+ /**
708
+ * `true` when the task was actually removed (was live in-memory or persisted),
709
+ * `false` when the call was a no-op (taskId unknown or already tombstoned).
710
+ * Idempotent semantics on `success` are preserved — `success: true` indicates
711
+ * the request was valid; `removed` distinguishes "actually removed" from
712
+ * "no-op". `task:deleteBulk` uses this to compute an accurate `deletedCount`.
713
+ */
714
+ removed: z.boolean().optional(),
715
+ success: z.boolean(),
716
+ });
717
+ /**
718
+ * task:deleteBulk — delete many tasks at once. `deletedCount` reports actual removals.
719
+ */
720
+ export const TaskDeleteBulkRequestSchema = z.object({
721
+ taskIds: z.array(z.string()),
722
+ });
723
+ export const TaskDeleteBulkResponseSchema = z.object({
724
+ deletedCount: z.number(),
725
+ error: z.string().optional(),
726
+ });
727
+ /**
728
+ * task:clearCompleted — remove all terminal-state tasks (completed/error/cancelled)
729
+ * from the project's history. Active tasks (created/started) are preserved.
730
+ */
731
+ export const TaskClearCompletedRequestSchema = z.object({
732
+ projectPath: z.string().optional(),
733
+ });
734
+ export const TaskClearCompletedResponseSchema = z.object({
735
+ deletedCount: z.number(),
736
+ error: z.string().optional(),
737
+ });
738
+ /**
739
+ * task:deleted — broadcast to project room when a task is removed from history.
740
+ * Lets other clients (TUI, other webui tabs) drop the row from their local view.
741
+ */
742
+ export const TaskDeletedEventSchema = z.object({
743
+ taskId: z.string(),
590
744
  });
591
745
  // ============================================================================
592
746
  // Session Schemas (client → server commands)
@@ -1,7 +1,13 @@
1
+ import type { ReasoningContentItem, ToolCallEvent } from '../../../../shared/transport/events/task-events.js';
1
2
  import type { TaskErrorData, TaskListItemStatus, TaskType } from './schemas.js';
2
3
  /**
3
4
  * Tracked task metadata used by TaskRouter for routing events
4
5
  * and by ConnectionCoordinator for agent disconnect cleanup.
6
+ *
7
+ * Level 2 fields (`responseContent`, `reasoningContents`, `toolCalls`,
8
+ * `sessionId`) are accumulated from `llmservice:*` events and persisted
9
+ * by `TaskHistoryHook` so a tab refresh mid-stream can render the in-flight
10
+ * state.
5
11
  */
6
12
  export type TaskInfo = {
7
13
  /** Client's working directory for file validation */
@@ -18,8 +24,16 @@ export type TaskInfo = {
18
24
  folderPath?: string;
19
25
  /** Log entry ID set by lifecycle hook after onTaskCreate */
20
26
  logId?: string;
27
+ /** Active model id at task creation time */
28
+ model?: string;
21
29
  /** Project path this task belongs to (for multi-project routing) */
22
30
  projectPath?: string;
31
+ /** Active provider id at task creation time */
32
+ provider?: string;
33
+ /** Accumulated reasoning/thinking entries from `llmservice:thinking` + `llmservice:chunk` (type=reasoning). */
34
+ reasoningContents?: ReasoningContentItem[];
35
+ /** Final assistant response set by `llmservice:response` (overwrites on multi-step). */
36
+ responseContent?: string;
23
37
  /** Set on successful completion */
24
38
  result?: string;
25
39
  /**
@@ -29,11 +43,15 @@ export type TaskInfo = {
29
43
  * the user toggles the flag mid-task.
30
44
  */
31
45
  reviewDisabled?: boolean;
46
+ /** LLM session id set alongside `responseContent` */
47
+ sessionId?: string;
32
48
  /** Set when agent picks up the task */
33
49
  startedAt?: number;
34
50
  /** Lifecycle status — defaults to 'created' on construction */
35
51
  status?: TaskListItemStatus;
36
52
  taskId: string;
53
+ /** Accumulated tool-call lifecycle entries from `llmservice:toolCall` + `:toolResult`. */
54
+ toolCalls?: ToolCallEvent[];
37
55
  type: TaskType;
38
56
  /** Workspace root (linked subdir or projectRoot if unlinked) */
39
57
  worktreeRoot?: string;
@@ -28,6 +28,13 @@ export interface ITaskLifecycleHook {
28
28
  }>;
29
29
  /** Called when a task fails with an error. */
30
30
  onTaskError?(taskId: string, errorMessage: string, task: TaskInfo): Promise<void>;
31
+ /**
32
+ * Called by the throttled flush (~100ms window) for in-flight task mutations
33
+ * — `task:started` transition + every `llmservice:*` accumulator update.
34
+ * Optional: existing handlers (CurateLogHandler, QueryLogHandler) don't
35
+ * implement it. Implementations must never throw.
36
+ */
37
+ onTaskUpdate?(task: TaskInfo): Promise<void>;
31
38
  /** Called when an LLM tool result event is received for an ACTIVE task (not grace-period). */
32
39
  onToolResult?(taskId: string, payload: LlmToolResultEvent): void;
33
40
  }
@@ -0,0 +1,26 @@
1
+ import type { BillingFreeUserLimitDTO, BillingOrganizationTierDTO, BillingUsageDTO } from '../../../../shared/transport/types/dto.js';
2
+ /**
3
+ * Reads compute-unit usage from the ByteRover billing service.
4
+ * Implementations may be HTTP-based (production) or stubbed (tests).
5
+ */
6
+ export interface IBillingService {
7
+ /**
8
+ * Returns the user's free-tier daily/monthly limits.
9
+ *
10
+ * @param sessionKey Authenticated session token (passed via x-byterover-session-id).
11
+ */
12
+ getFreeUserLimit: (sessionKey: string) => Promise<BillingFreeUserLimitDTO>;
13
+ /**
14
+ * Returns the tier (FREE/PRO/ENTERPRISE) for every org the user belongs to.
15
+ *
16
+ * @param sessionKey Authenticated session token (passed via x-byterover-session-id).
17
+ */
18
+ getTiers: (sessionKey: string) => Promise<BillingOrganizationTierDTO[]>;
19
+ /**
20
+ * Fetches usage for every organization the authenticated user belongs to in
21
+ * a single round trip. Replaces the old per-org `getUsageByProjects` fan-out.
22
+ *
23
+ * @param sessionKey Authenticated session token (passed via x-byterover-session-id).
24
+ */
25
+ getUsages: (sessionKey: string) => Promise<BillingUsageDTO[]>;
26
+ }
@@ -0,0 +1,4 @@
1
+ export interface IBillingConfigStore {
2
+ getPinnedTeamId: () => Promise<string | undefined>;
3
+ setPinnedTeamId: (teamId: string | undefined) => Promise<void>;
4
+ }
@@ -0,0 +1,62 @@
1
+ import type { TaskListItem } from '../../../../shared/transport/events/task-events.js';
2
+ import type { TaskHistoryEntry, TaskHistoryStatus } from '../../domain/entities/task-history-entry.js';
3
+ export type { TaskHistoryEntry, TaskHistoryStatus } from '../../domain/entities/task-history-entry.js';
4
+ export interface ITaskHistoryStore {
5
+ /**
6
+ * Tombstone all matching entries. Defaults to terminal statuses
7
+ * (`'cancelled' | 'completed' | 'error'`) when `statuses` is omitted,
8
+ * so active tasks are preserved.
9
+ * Returns the list of removed taskIds (caller broadcasts `task:deleted` per id).
10
+ */
11
+ clear(options?: {
12
+ projectPath?: string;
13
+ statuses?: TaskHistoryStatus[];
14
+ }): Promise<{
15
+ deletedCount: number;
16
+ taskIds: string[];
17
+ }>;
18
+ /** Remove a single entry by taskId. Idempotent — returns false on missing/already-deleted. */
19
+ delete(taskId: string): Promise<boolean>;
20
+ /**
21
+ * Bulk-delete by taskIds. Returns the subset of input ids that were live
22
+ * (and have now been tombstoned) — invalid, unknown, and already-tombstoned
23
+ * ids are dropped. Callers can rely on the returned array length as the
24
+ * `deletedCount` and on the array contents for per-id broadcasts.
25
+ */
26
+ deleteMany(taskIds: string[]): Promise<string[]>;
27
+ /** Retrieve an entry's full Level 2 detail by taskId. Returns undefined if not found or corrupt. */
28
+ getById(taskId: string): Promise<TaskHistoryEntry | undefined>;
29
+ /**
30
+ * List entries (summary shape) sorted newest-first.
31
+ *
32
+ * Returns the wire-friendly `TaskListItem` shape — no `responseContent`, `toolCalls`,
33
+ * `reasoningContents`, `sessionId`, or `result`. Callers fetch full detail via `getById`.
34
+ *
35
+ * M2.16: param names align with the wire schema (`createdAfter` / `createdBefore`
36
+ * instead of legacy `after` / `before`). Pagination moved to the handler — store
37
+ * returns ALL matches; no `limit`.
38
+ *
39
+ * Note on `provider` / `model` / `status` filters: `handleTaskList` does NOT push
40
+ * these down — pivot filters run at the handler so derivative sets (counts,
41
+ * availableProviders, availableModels) can apply their exclusion rules. The
42
+ * options remain on the interface for direct store callers (tests, future CLI
43
+ * commands like `brv query-log`-style scans) that don't need pivot semantics.
44
+ */
45
+ list(options?: {
46
+ /** Include only entries with createdAt >= this epoch ms. */
47
+ createdAfter?: number;
48
+ /** Include only entries with createdAt <= this epoch ms. */
49
+ createdBefore?: number;
50
+ /** Include only entries matching these model ids. Direct-caller use only. */
51
+ model?: string[];
52
+ projectPath?: string;
53
+ /** Include only entries matching these provider ids. Direct-caller use only. */
54
+ provider?: string[];
55
+ /** Include only entries matching these statuses. Direct-caller use only. */
56
+ status?: TaskHistoryStatus[];
57
+ /** Include only entries matching these task types. */
58
+ type?: string[];
59
+ }): Promise<TaskListItem[]>;
60
+ /** Persist (create or overwrite) a history entry. Validates with Zod — throws on invalid shape. */
61
+ save(entry: TaskHistoryEntry): Promise<void>;
62
+ }
@@ -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
+ }