@agent-finops/core 0.8.1 → 0.9.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 (44) hide show
  1. package/README.md +5 -3
  2. package/dist/actionPlanner.d.ts +140 -0
  3. package/dist/actionPlanner.js +938 -0
  4. package/dist/actionVerification.d.ts +1240 -0
  5. package/dist/actionVerification.js +1028 -0
  6. package/dist/activitySnapshot.d.ts +142 -50
  7. package/dist/activitySnapshot.js +145 -6
  8. package/dist/activitySnapshotCache.d.ts +8 -1
  9. package/dist/activitySnapshotCache.js +103 -7
  10. package/dist/agentEconomicsReceipt.d.ts +74 -74
  11. package/dist/glance.d.ts +27 -1
  12. package/dist/glance.js +151 -12
  13. package/dist/index.d.ts +11 -2
  14. package/dist/index.js +10 -1
  15. package/dist/localAgentFormats/gemini.js +2 -2
  16. package/dist/localAgentFormats/registry.js +6 -2
  17. package/dist/localAgentFormats/runtimeRegistry.js +5 -2
  18. package/dist/localAgentFormats/types.d.ts +2 -1
  19. package/dist/localAgentLogs.d.ts +362 -3
  20. package/dist/localAgentLogs.js +1964 -165
  21. package/dist/modelPricing.d.ts +1 -1
  22. package/dist/modelPricing.js +1 -1
  23. package/dist/projectEconomics.d.ts +617 -0
  24. package/dist/projectEconomics.js +620 -0
  25. package/dist/projectEconomicsBuilder.d.ts +89 -0
  26. package/dist/projectEconomicsBuilder.js +473 -0
  27. package/dist/projectIndexStore.d.ts +545 -0
  28. package/dist/projectIndexStore.js +606 -0
  29. package/dist/providerConnectors.d.ts +59 -1
  30. package/dist/providerConnectors.js +175 -11
  31. package/dist/qualitativeIndexCache.d.ts +494 -0
  32. package/dist/qualitativeIndexCache.js +930 -0
  33. package/dist/resultCard.d.ts +350 -0
  34. package/dist/resultCard.js +604 -0
  35. package/dist/runtimeCommands.d.ts +21 -0
  36. package/dist/runtimeCommands.js +27 -0
  37. package/dist/scanGuard.d.ts +3 -1
  38. package/dist/scanGuard.js +164 -4
  39. package/dist/schema.d.ts +31 -31
  40. package/dist/sessionVitals.d.ts +145 -0
  41. package/dist/sessionVitals.js +521 -0
  42. package/dist/toolInvocations.d.ts +40 -1
  43. package/dist/toolInvocations.js +101 -20
  44. package/package.json +1 -1
@@ -19,6 +19,55 @@ export function isProviderAuthenticationError(error) {
19
19
  return error instanceof ProviderConnectorError &&
20
20
  error.code === "authentication_error";
21
21
  }
22
+ /** Environment variables the Cursor connector reads for a reconciliation run. */
23
+ export const cursorReconciliationEnvVars = {
24
+ expectedUsd: "AI_SPEND_CURSOR_RECONCILE_EXPECTED_USD",
25
+ cycleStart: "AI_SPEND_CURSOR_RECONCILE_CYCLE_START",
26
+ toleranceUsd: "AI_SPEND_CURSOR_RECONCILE_TOLERANCE_USD"
27
+ };
28
+ /**
29
+ * Read an operator-declared Cursor reconciliation anchor from the local
30
+ * environment. Absent variables mean "no reconciliation requested"; present
31
+ * but invalid variables fail closed with a reason (records stay estimated)
32
+ * instead of throwing, so a typo can never abort or silently verify a sync.
33
+ * Raw variable values are never echoed into the reason.
34
+ */
35
+ export function parseCursorReconciliationEnv(env = process.env) {
36
+ const rawExpected = env[cursorReconciliationEnvVars.expectedUsd];
37
+ const rawCycleStart = env[cursorReconciliationEnvVars.cycleStart];
38
+ const rawTolerance = env[cursorReconciliationEnvVars.toleranceUsd];
39
+ if (rawExpected === undefined && rawCycleStart === undefined && rawTolerance === undefined) {
40
+ return {};
41
+ }
42
+ if (rawExpected === undefined || rawCycleStart === undefined) {
43
+ return {
44
+ invalidReason: `both ${cursorReconciliationEnvVars.expectedUsd} and ${cursorReconciliationEnvVars.cycleStart} are required to request a reconciliation`
45
+ };
46
+ }
47
+ const expectedOnDemandUsd = Number(rawExpected.trim());
48
+ const toleranceUsd = rawTolerance === undefined ? undefined : Number(rawTolerance.trim());
49
+ const expectation = {
50
+ expectedOnDemandUsd,
51
+ expectedCycleStartDate: rawCycleStart.trim(),
52
+ ...(toleranceUsd === undefined ? {} : { toleranceUsd })
53
+ };
54
+ const invalidReason = invalidCursorReconciliationExpectationReason(expectation);
55
+ return invalidReason ? { invalidReason } : { expectation };
56
+ }
57
+ function invalidCursorReconciliationExpectationReason(expectation) {
58
+ if (!Number.isFinite(expectation.expectedOnDemandUsd) || expectation.expectedOnDemandUsd <= 0) {
59
+ return `${cursorReconciliationEnvVars.expectedUsd} must be a positive USD amount read off the Cursor dashboard or invoice`;
60
+ }
61
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(expectation.expectedCycleStartDate) ||
62
+ !Number.isFinite(Date.parse(`${expectation.expectedCycleStartDate}T00:00:00Z`))) {
63
+ return `${cursorReconciliationEnvVars.cycleStart} must be the cycle start date shown on the dashboard, formatted YYYY-MM-DD`;
64
+ }
65
+ if (expectation.toleranceUsd !== undefined &&
66
+ (!Number.isFinite(expectation.toleranceUsd) || expectation.toleranceUsd < 0)) {
67
+ return `${cursorReconciliationEnvVars.toleranceUsd} must be a non-negative USD amount when set`;
68
+ }
69
+ return undefined;
70
+ }
22
71
  export function normalizeOpenAiCostResponse(response, options) {
23
72
  const data = isObject(response) && Array.isArray(response.data) ? response.data : [];
24
73
  const records = [];
@@ -399,10 +448,21 @@ export function normalizeGitHubCopilotMetricsResponse(response, options) {
399
448
  }
400
449
  return records;
401
450
  }
402
- export function normalizeCursorSpendResponse(response, options) {
451
+ export function normalizeCursorSpendResponse(response, options, reconciliation) {
403
452
  const users = extractArray(response, "teamMemberSpend");
404
453
  const cycleStart = isRecord(response) ? numberValue(response.subscriptionCycleStart) : undefined;
405
454
  const timestamp = typeof cycleStart === "number" ? new Date(cycleStart).toISOString() : new Date().toISOString();
455
+ // The Cursor connector's dollars are labeled estimated until an in-run
456
+ // reconciliation proves the connector total against a human-read dashboard
457
+ // or invoice figure for the same cycle. Only that evidence — never a
458
+ // hardcoded flip — can stamp these records "verified", and a mismatched or
459
+ // unprovable reconciliation fails closed back to estimated.
460
+ const reconciled = reconciliation?.status === "verified";
461
+ const confidence = reconciled ? "verified" : "estimated";
462
+ // Documented semantics: spendCents is "On-demand spend in cents for the
463
+ // current billing cycle" — seat fees and included-pool usage are excluded.
464
+ const baseOperation = "Cursor on-demand team spend (current billing cycle; excludes seat fees and included-pool usage)";
465
+ const operation = reconciled ? `${baseOperation}; ${reconciliation.note}` : baseOperation;
406
466
  return users.flatMap((user) => {
407
467
  if (!isRecord(user))
408
468
  return [];
@@ -413,20 +473,17 @@ export function normalizeCursorSpendResponse(response, options) {
413
473
  return [{
414
474
  id: slugifySourceId(["cursor-spend", options.accountId, userId].filter(Boolean).join("-")),
415
475
  timestamp,
416
- // The Cursor connector is spec-built and not yet live-verified (beta),
417
- // so its dollars are labeled estimated until reconciled against a real
418
- // team's invoice. Never stamp "verified" on data we haven't verified.
419
- source: { id: options.sourceId, name: "Cursor Admin API", provider: "cursor", confidence: "estimated", observedFrom: options.observedFrom },
476
+ source: { id: options.sourceId, name: "Cursor Admin API", provider: "cursor", confidence, observedFrom: options.observedFrom },
420
477
  model: "cursor-team-usage",
421
478
  inputTokens: 0,
422
479
  outputTokens: 0,
423
480
  amountUsd: cents / 100,
424
- costConfidence: "estimated",
481
+ costConfidence: confidence,
425
482
  userId,
426
483
  projectId: options.accountId,
427
484
  providerCostType: "cursor_spend",
428
485
  usageGranularity: "user_aggregate",
429
- operation: "Cursor team spend"
486
+ operation
430
487
  }];
431
488
  });
432
489
  }
@@ -571,8 +628,102 @@ async function fetchGitHubCopilot(input, token, fetcher, sourceId) {
571
628
  async function fetchCursor(input, token, fetcher, sourceId) {
572
629
  const accountId = input.accountId ?? input.org ?? "cursor-team";
573
630
  const spendFetch = await fetchCursorSpendPages(fetcher, token);
574
- const records = spendFetch.pages.flatMap((page) => normalizeCursorSpendResponse(page, { sourceId, observedFrom: "Cursor Admin API", accountId }));
575
- return providerResult("cursor", sourceId, input.authReference, records, qaSummary("cursor", [spendFetch]));
631
+ const requested = input.reconciliation
632
+ ? { expectation: input.reconciliation, invalidReason: invalidCursorReconciliationExpectationReason(input.reconciliation) }
633
+ : parseCursorReconciliationEnv();
634
+ const reconciliation = assessCursorReconciliation(spendFetch, requested.expectation, requested.invalidReason);
635
+ const records = spendFetch.pages.flatMap((page) => normalizeCursorSpendResponse(page, { sourceId, observedFrom: "Cursor Admin API", accountId }, reconciliation));
636
+ const qa = qaSummary("cursor", [spendFetch]);
637
+ if (reconciliation) {
638
+ // The outcome must survive the persisted-QA round trip, so it rides in
639
+ // instructions (kept verbatim) and, on failure, in responseDrift.
640
+ qa.instructions = [...qa.instructions, `Reconciliation ${reconciliation.status}: ${reconciliation.note}`];
641
+ if (reconciliation.status !== "verified") {
642
+ qa.responseDrift.push({
643
+ label: "Cursor Admin API spend",
644
+ field: "teamMemberSpend[].spendCents (cycle total)",
645
+ issue: reconciliation.note
646
+ });
647
+ }
648
+ }
649
+ return providerResult("cursor", sourceId, input.authReference, records, qa);
650
+ }
651
+ /**
652
+ * Compare the connector's summed current-cycle on-demand total against the
653
+ * operator-read dashboard/invoice figure. Every exit that is not an exact
654
+ * window-proven match inside the clamped tolerance fails closed: the records
655
+ * stay estimated and the note says exactly why. Returns undefined when no
656
+ * reconciliation was requested.
657
+ */
658
+ function assessCursorReconciliation(spendFetch, expectation, invalidReason) {
659
+ if (!expectation && !invalidReason)
660
+ return undefined;
661
+ if (invalidReason || !expectation) {
662
+ return {
663
+ status: "not_provable",
664
+ note: `Cursor reconciliation input was rejected (${invalidReason ?? "missing expectation"}); records remain estimated.`
665
+ };
666
+ }
667
+ if (spendFetch.pagination.stoppedBecause !== "complete" || spendFetch.coverageIncomplete === true) {
668
+ return {
669
+ status: "not_provable",
670
+ note: `Cursor reconciliation requires a complete spend window; pagination stopped because "${spendFetch.pagination.stoppedBecause}" so a partial window cannot verify billed dollars. Records remain estimated.`
671
+ };
672
+ }
673
+ const cycleStarts = spendFetch.pages.map((page) => isRecord(page) ? numberValue(page.subscriptionCycleStart) : undefined);
674
+ const cycleStart = cycleStarts[0];
675
+ if (typeof cycleStart !== "number" || cycleStarts.some((value) => value !== cycleStart)) {
676
+ return {
677
+ status: "not_provable",
678
+ note: "Cursor did not report one consistent subscriptionCycleStart across spend pages; the reconciliation window cannot be proven. Records remain estimated."
679
+ };
680
+ }
681
+ const cycleStartIso = new Date(cycleStart).toISOString();
682
+ const apiCycleDate = cycleStartIso.slice(0, 10);
683
+ const declaredDateMs = Date.parse(`${expectation.expectedCycleStartDate}T00:00:00Z`);
684
+ const dayMs = 24 * 60 * 60 * 1000;
685
+ if (!Number.isFinite(declaredDateMs) || Math.abs(Date.parse(`${apiCycleDate}T00:00:00Z`) - declaredDateMs) > dayMs) {
686
+ return {
687
+ status: "not_provable",
688
+ cycleStartIso,
689
+ note: `The declared cycle start ${expectation.expectedCycleStartDate} does not match the provider-reported cycle start ${apiCycleDate} (UTC); the dashboard figure and the connector read different windows. Records remain estimated.`
690
+ };
691
+ }
692
+ const connectorTotalCents = spendFetch.pages.reduce((sum, page) => sum + extractArray(page, "teamMemberSpend").reduce((pageSum, member) => pageSum + (isRecord(member) ? numberValue(member.spendCents) ?? 0 : 0), 0), 0);
693
+ const connectorTotalUsd = connectorTotalCents / 100;
694
+ if (!(connectorTotalUsd > 0)) {
695
+ return {
696
+ status: "not_provable",
697
+ cycleStartIso,
698
+ connectorTotalUsd,
699
+ expectedOnDemandUsd: expectation.expectedOnDemandUsd,
700
+ note: "Cursor reconciliation needs a non-zero connector total; matching $0.00 against a dashboard figure proves nothing. Records remain estimated."
701
+ };
702
+ }
703
+ // Default $0.01 (dashboards round to cents); clamp to at most 1% of the
704
+ // expected figure so an oversized tolerance cannot manufacture a match.
705
+ const requestedTolerance = Math.max(expectation.toleranceUsd ?? 0.01, 0.01);
706
+ const toleranceUsd = Math.min(requestedTolerance, Math.max(0.01, expectation.expectedOnDemandUsd * 0.01));
707
+ const differenceUsd = Math.abs(connectorTotalUsd - expectation.expectedOnDemandUsd);
708
+ const shared = {
709
+ connectorTotalUsd,
710
+ expectedOnDemandUsd: expectation.expectedOnDemandUsd,
711
+ differenceUsd,
712
+ toleranceUsd,
713
+ cycleStartIso
714
+ };
715
+ if (differenceUsd <= toleranceUsd + 1e-9) {
716
+ return {
717
+ status: "verified",
718
+ ...shared,
719
+ note: `reconciled to the operator-read dashboard/invoice on-demand total $${expectation.expectedOnDemandUsd.toFixed(2)} for the cycle starting ${apiCycleDate}: connector total $${connectorTotalUsd.toFixed(2)}, difference $${differenceUsd.toFixed(2)} within tolerance $${toleranceUsd.toFixed(2)}`
720
+ };
721
+ }
722
+ return {
723
+ status: "mismatch",
724
+ ...shared,
725
+ note: `Cursor reconciliation mismatch: connector on-demand total $${connectorTotalUsd.toFixed(2)} vs operator-read $${expectation.expectedOnDemandUsd.toFixed(2)} for the cycle starting ${apiCycleDate}; difference $${differenceUsd.toFixed(2)} exceeds tolerance $${toleranceUsd.toFixed(2)}. Records remain estimated until the totals agree.`
726
+ };
576
727
  }
577
728
  async function fetchCursorSpendPages(fetcher, token) {
578
729
  const label = "Cursor Admin API spend";
@@ -1392,7 +1543,18 @@ function knownProviderFields(provider, label) {
1392
1543
  return new Set([...common, "total_seats", "seats", "seats[]", "seats[].created_at", "seats[].updated_at", "seats[].pending_cancellation_date", "seats[].last_activity_at", "seats[].last_activity_editor", "seats[].last_authenticated_at", "seats[].plan_type", "seats[].login", "seats[].id", "seats[].assignee", "seats[].assignee.login", "seats[].assignee.email", "seats[].assignee.id", "seats[].assignee.node_id", "seats[].assignee.avatar_url", "seats[].assignee.html_url", "seats[].assignee.type", "seats[].assignee.site_admin", "seats[].assigning_team", "seats[].organization"]);
1393
1544
  }
1394
1545
  if (provider === "cursor") {
1395
- return new Set([...common, "teamMemberSpend", "teamMemberSpend[]", "teamMemberSpend[].userId", "teamMemberSpend[].email", "teamMemberSpend[].name", "teamMemberSpend[].role", "teamMemberSpend[].spendCents", "teamMemberSpend[].fastPremiumRequests", "teamMemberSpend[].hardLimitOverrideDollars", "subscriptionCycleStart", "totalMembers", "totalPages"]);
1546
+ return new Set([
1547
+ ...common,
1548
+ "teamMemberSpend", "teamMemberSpend[]", "teamMemberSpend[].userId", "teamMemberSpend[].email", "teamMemberSpend[].name", "teamMemberSpend[].role", "teamMemberSpend[].spendCents", "teamMemberSpend[].fastPremiumRequests", "teamMemberSpend[].hardLimitOverrideDollars",
1549
+ // Documented in the 2026 Admin API reference alongside spendCents.
1550
+ "teamMemberSpend[].overallSpendCents", "teamMemberSpend[].monthlyLimitDollars", "teamMemberSpend[].effectivePerUserLimitDollars",
1551
+ // Present in live responses and staff-acknowledged as a docs lag
1552
+ // (forum.cursor.com thread 162742, "docs for Get Spending Data are
1553
+ // behind the current API schema"). billingTier and the percent fields
1554
+ // are tiered-team-only and may be undefined elsewhere.
1555
+ "teamMemberSpend[].includedSpendCents", "teamMemberSpend[].profilePictureUrl", "teamMemberSpend[].billingTier", "teamMemberSpend[].autoPercentUsed", "teamMemberSpend[].apiPercentUsed", "teamMemberSpend[].totalPercentUsed",
1556
+ "subscriptionCycleStart", "totalMembers", "totalPages"
1557
+ ]);
1396
1558
  }
1397
1559
  return new Set([...common]);
1398
1560
  }
@@ -1429,7 +1591,9 @@ function providerInstructions(provider) {
1429
1591
  if (provider === "cursor") {
1430
1592
  return [
1431
1593
  "Use a Cursor team admin API key reference, or fall back to Browser Account UI/manual export when API access is unavailable.",
1432
- "Validate user-level spend against invoices before treating the source as finance-grade."
1594
+ "Cursor's 2026 docs list the Admin API under Enterprise teams; individual Pro/Ultra plans expose no billing API. Standard Admin API endpoints are rate-limited to 20 requests/minute per team.",
1595
+ "spendCents is on-demand spend for the current billing cycle; seat fees and included-pool usage are not in this total.",
1596
+ "Validate user-level spend against invoices before treating the source as finance-grade; set AI_SPEND_CURSOR_RECONCILE_EXPECTED_USD and AI_SPEND_CURSOR_RECONCILE_CYCLE_START to run an in-sync reconciliation."
1433
1597
  ];
1434
1598
  }
1435
1599
  return ["Use a local token reference only; never paste raw provider secrets into commands or reports."];