@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.
- package/README.md +5 -3
- package/dist/actionPlanner.d.ts +140 -0
- package/dist/actionPlanner.js +938 -0
- package/dist/actionVerification.d.ts +1240 -0
- package/dist/actionVerification.js +1028 -0
- package/dist/activitySnapshot.d.ts +142 -50
- package/dist/activitySnapshot.js +145 -6
- package/dist/activitySnapshotCache.d.ts +8 -1
- package/dist/activitySnapshotCache.js +103 -7
- package/dist/agentEconomicsReceipt.d.ts +74 -74
- package/dist/glance.d.ts +27 -1
- package/dist/glance.js +151 -12
- package/dist/index.d.ts +11 -2
- package/dist/index.js +10 -1
- package/dist/localAgentFormats/gemini.js +2 -2
- package/dist/localAgentFormats/registry.js +6 -2
- package/dist/localAgentFormats/runtimeRegistry.js +5 -2
- package/dist/localAgentFormats/types.d.ts +2 -1
- package/dist/localAgentLogs.d.ts +362 -3
- package/dist/localAgentLogs.js +1964 -165
- package/dist/modelPricing.d.ts +1 -1
- package/dist/modelPricing.js +1 -1
- package/dist/projectEconomics.d.ts +617 -0
- package/dist/projectEconomics.js +620 -0
- package/dist/projectEconomicsBuilder.d.ts +89 -0
- package/dist/projectEconomicsBuilder.js +473 -0
- package/dist/projectIndexStore.d.ts +545 -0
- package/dist/projectIndexStore.js +606 -0
- package/dist/providerConnectors.d.ts +59 -1
- package/dist/providerConnectors.js +175 -11
- package/dist/qualitativeIndexCache.d.ts +494 -0
- package/dist/qualitativeIndexCache.js +930 -0
- package/dist/resultCard.d.ts +350 -0
- package/dist/resultCard.js +604 -0
- package/dist/runtimeCommands.d.ts +21 -0
- package/dist/runtimeCommands.js +27 -0
- package/dist/scanGuard.d.ts +3 -1
- package/dist/scanGuard.js +164 -4
- package/dist/schema.d.ts +31 -31
- package/dist/sessionVitals.d.ts +145 -0
- package/dist/sessionVitals.js +521 -0
- package/dist/toolInvocations.d.ts +40 -1
- package/dist/toolInvocations.js +101 -20
- 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
|
-
|
|
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:
|
|
481
|
+
costConfidence: confidence,
|
|
425
482
|
userId,
|
|
426
483
|
projectId: options.accountId,
|
|
427
484
|
providerCostType: "cursor_spend",
|
|
428
485
|
usageGranularity: "user_aggregate",
|
|
429
|
-
operation
|
|
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
|
|
575
|
-
|
|
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([
|
|
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
|
-
"
|
|
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."];
|