@cat-factory/node-server 0.80.5 → 0.81.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.
@@ -2,7 +2,7 @@ import { LLM_WARNING_FINISH_REASONS } from '@cat-factory/kernel';
2
2
  import { agentRunKindSchema, decodeInitiativeRow, isWebSearchProvider, } from '@cat-factory/contracts';
3
3
  import { decodeEnum, tryDecodeRows, blockInsertValues, blockPatchToColumns, parseIssueIntakeColumn, serializeIssueIntakeColumn, rowToBlock, rowToExecution, executionToDetail, rowToPipeline, rowToSandboxExperiment, rowToSandboxFixture, rowToSandboxGrade, rowToSandboxPromptVersion, rowToSandboxRun, rowToWorkspace, } from '@cat-factory/server';
4
4
  import { and, asc, count, desc, eq, gte, inArray, isNull, lt, ne, notInArray, or, sql, } from 'drizzle-orm';
5
- import { accountInvitations, passwordResetTokens, accountSettings, localSettings, accounts, agentContextSnapshots, agentSearchQueries, agentRuns, blocks, consensusSessions, incidentEnrichmentConnections, observabilityConnections, packageRegistryConnections, emailConnections, llmCallMetrics, provisioningLog, sharedStacks, memberships, mergeThresholdPresets, releaseHealthConfigs, pipelineScheduleRuns, pipelineSchedules, pipelines, requirementReviews, docInterviewSessions, kaizenGradings, kaizenVerifiedCombos, clarityReviews, binaryArtifacts, brainstormSessions, initiatives, sandboxPromptVersions, sandboxFixtures, sandboxExperiments, sandboxRuns, sandboxGrades, services, tokenUsage, trackerSettings, modelPresets, userIdentities, users, workspaceFragmentDefaults, workspaceServices, workspaceSettings, workspaces, } from '../db/schema.js';
5
+ import { accountInvitations, passwordResetTokens, accountSettings, localSettings, accounts, agentContextSnapshots, agentSearchQueries, agentRuns, blocks, consensusSessions, incidentEnrichmentConnections, observabilityConnections, packageRegistryConnections, emailConnections, llmCallMetrics, provisioningLog, sharedStacks, memberships, mergeThresholdPresets, releaseHealthConfigs, pipelineScheduleRuns, pipelineSchedules, pipelines, requirementReviews, docInterviewSessions, kaizenGradings, kaizenVerifiedCombos, clarityReviews, binaryArtifacts, brainstormSessions, initiatives, sandboxPromptVersions, sandboxFixtures, sandboxExperiments, sandboxRuns, sandboxGrades, services, tokenUsage, trackerSettings, modelPresets, userIdentities, users, userSettings, workspaceFragmentDefaults, workspaceServices, workspaceSettings, workspaces, } from '../db/schema.js';
6
6
  // Drizzle/Postgres implementations of the core kernel repository ports. The
7
7
  // row<->domain mapping is the SAME shared mapping the Cloudflare D1 repos use
8
8
  // (@cat-factory/server), so behaviour matches across stores; this layer only owns
@@ -532,6 +532,7 @@ function rowToAccount(row) {
532
532
  ...(row.default_cloud_provider
533
533
  ? { defaultCloudProvider: row.default_cloud_provider }
534
534
  : {}),
535
+ ...(row.spend_monthly_limit != null ? { spendMonthlyLimit: row.spend_monthly_limit } : {}),
535
536
  };
536
537
  }
537
538
  class DrizzleAccountRepository {
@@ -567,18 +568,23 @@ class DrizzleAccountRepository {
567
568
  owner_user_id: account.ownerUserId,
568
569
  created_at: account.createdAt,
569
570
  default_cloud_provider: account.defaultCloudProvider ?? null,
571
+ spend_monthly_limit: account.spendMonthlyLimit ?? null,
570
572
  });
571
573
  }
572
574
  async rename(id, name) {
573
575
  await this.db.update(accounts).set({ name }).where(eq(accounts.id, id));
574
576
  }
575
577
  async updateSettings(id, patch) {
576
- if (!('defaultCloudProvider' in patch))
578
+ const set = {};
579
+ if ('defaultCloudProvider' in patch) {
580
+ set.default_cloud_provider = patch.defaultCloudProvider ?? null;
581
+ }
582
+ if ('spendMonthlyLimit' in patch) {
583
+ set.spend_monthly_limit = patch.spendMonthlyLimit ?? null;
584
+ }
585
+ if (Object.keys(set).length === 0)
577
586
  return;
578
- await this.db
579
- .update(accounts)
580
- .set({ default_cloud_provider: patch.defaultCloudProvider ?? null })
581
- .where(eq(accounts.id, id));
587
+ await this.db.update(accounts).set(set).where(eq(accounts.id, id));
582
588
  }
583
589
  async findPersonalByUser(userId) {
584
590
  const [row] = await this.db
@@ -930,6 +936,8 @@ class DrizzleTokenUsageRepository {
930
936
  await this.db.insert(tokenUsage).values({
931
937
  id: usage.id,
932
938
  workspace_id: usage.workspaceId,
939
+ account_id: usage.accountId,
940
+ user_id: usage.userId,
933
941
  execution_id: usage.executionId,
934
942
  agent_kind: usage.agentKind,
935
943
  provider: usage.provider,
@@ -974,6 +982,36 @@ class DrizzleTokenUsageRepository {
974
982
  costEstimate: row?.cost ?? 0,
975
983
  };
976
984
  }
985
+ async totalsSinceForAccount(accountId, epochMs) {
986
+ const [row] = await this.db
987
+ .select({
988
+ input: sql `coalesce(sum(${tokenUsage.input_tokens}), 0)::bigint`,
989
+ output: sql `coalesce(sum(${tokenUsage.output_tokens}), 0)::bigint`,
990
+ cost: sql `coalesce(sum(${tokenUsage.cost_estimate}), 0)::float8`,
991
+ })
992
+ .from(tokenUsage)
993
+ .where(and(eq(tokenUsage.account_id, accountId), gte(tokenUsage.created_at, epochMs)));
994
+ return {
995
+ inputTokens: Number(row?.input ?? 0),
996
+ outputTokens: Number(row?.output ?? 0),
997
+ costEstimate: row?.cost ?? 0,
998
+ };
999
+ }
1000
+ async totalsSinceForUser(userId, epochMs) {
1001
+ const [row] = await this.db
1002
+ .select({
1003
+ input: sql `coalesce(sum(${tokenUsage.input_tokens}), 0)::bigint`,
1004
+ output: sql `coalesce(sum(${tokenUsage.output_tokens}), 0)::bigint`,
1005
+ cost: sql `coalesce(sum(${tokenUsage.cost_estimate}), 0)::float8`,
1006
+ })
1007
+ .from(tokenUsage)
1008
+ .where(and(eq(tokenUsage.user_id, userId), gte(tokenUsage.created_at, epochMs)));
1009
+ return {
1010
+ inputTokens: Number(row?.input ?? 0),
1011
+ outputTokens: Number(row?.output ?? 0),
1012
+ costEstimate: row?.cost ?? 0,
1013
+ };
1014
+ }
977
1015
  async deleteOlderThan(epochMs) {
978
1016
  const deleted = await this.db
979
1017
  .delete(tokenUsage)
@@ -982,6 +1020,29 @@ class DrizzleTokenUsageRepository {
982
1020
  return deleted.length;
983
1021
  }
984
1022
  }
1023
+ class DrizzleUserSettingsRepository {
1024
+ db;
1025
+ constructor(db) {
1026
+ this.db = db;
1027
+ }
1028
+ async get(userId) {
1029
+ const [row] = await this.db.select().from(userSettings).where(eq(userSettings.user_id, userId));
1030
+ return row ? { spendMonthlyLimit: row.spend_monthly_limit } : null;
1031
+ }
1032
+ async upsert(userId, settings) {
1033
+ await this.db
1034
+ .insert(userSettings)
1035
+ .values({
1036
+ user_id: userId,
1037
+ spend_monthly_limit: settings.spendMonthlyLimit ?? null,
1038
+ updated_at: Date.now(),
1039
+ })
1040
+ .onConflictDoUpdate({
1041
+ target: userSettings.user_id,
1042
+ set: { spend_monthly_limit: settings.spendMonthlyLimit ?? null, updated_at: Date.now() },
1043
+ });
1044
+ }
1045
+ }
985
1046
  function rowToLlmMetric(row) {
986
1047
  return {
987
1048
  id: row.id,
@@ -3770,6 +3831,7 @@ export function createDrizzleRepositories(db, clock) {
3770
3831
  mergePresetRepository: new DrizzleMergePresetRepository(db),
3771
3832
  sharedStackRepository: new DrizzleSharedStackRepository(db),
3772
3833
  workspaceSettingsRepository: new DrizzleWorkspaceSettingsRepository(db),
3834
+ userSettingsRepository: new DrizzleUserSettingsRepository(db),
3773
3835
  observabilityConnectionRepository: new DrizzleObservabilityConnectionRepository(db),
3774
3836
  packageRegistryConnectionRepository: new DrizzlePackageRegistryConnectionRepository(db),
3775
3837
  incidentEnrichmentConnectionRepository: new DrizzleIncidentEnrichmentConnectionRepository(db),