@agent-finops/core 0.8.0 → 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 +101 -9
- package/dist/activitySnapshot.js +145 -6
- package/dist/activitySnapshotCache.d.ts +8 -1
- package/dist/activitySnapshotCache.js +103 -7
- package/dist/agentEconomicsReceipt.d.ts +58 -58
- package/dist/analyze.js +3 -1
- package/dist/cutList.js +1 -1
- package/dist/glance.d.ts +30 -2
- package/dist/glance.js +265 -84
- package/dist/index.d.ts +11 -2
- package/dist/index.js +10 -1
- package/dist/insights.js +3 -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 +4 -1
- package/dist/planMath.js +12 -7
- 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 +192 -12
- 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/sourceRegistry.js +90 -52
- package/dist/toolInvocations.d.ts +40 -1
- package/dist/toolInvocations.js +101 -20
- package/package.json +1 -1
|
@@ -41,6 +41,18 @@ export type ActivitySnapshotBuildInput = {
|
|
|
41
41
|
/** Explicit provider-billed overage records; must also be trusted. */
|
|
42
42
|
billedOverageRecordIds?: readonly string[];
|
|
43
43
|
providerCoverage?: readonly ActivitySnapshotProviderCoverageInput[];
|
|
44
|
+
/**
|
|
45
|
+
* C-lane §2.1: provider-billed subscriptions with no local agent (cursor).
|
|
46
|
+
* The builder includes a billed30d amount only when the supplied window is
|
|
47
|
+
* "verified"; anything else is degraded to a missing window (writer-side
|
|
48
|
+
* lock — the renderer re-checks independently).
|
|
49
|
+
*/
|
|
50
|
+
providerSubscriptions?: readonly {
|
|
51
|
+
provider: ActivitySnapshotProvider;
|
|
52
|
+
planLabel: string | null;
|
|
53
|
+
committedUsdPerMonth: number | null;
|
|
54
|
+
billed30d?: ActivitySnapshotBilledWindow;
|
|
55
|
+
}[];
|
|
44
56
|
pricingAsOf?: string;
|
|
45
57
|
/** Deliberately accepts only false; a runtime true is rejected as defense in depth. */
|
|
46
58
|
sampleData?: false;
|
|
@@ -187,6 +199,7 @@ export declare const activitySnapshotSubscriptionAgentSchema: z.ZodObject<{
|
|
|
187
199
|
"chatgpt-plus": "chatgpt-plus";
|
|
188
200
|
"chatgpt-pro": "chatgpt-pro";
|
|
189
201
|
}>>;
|
|
202
|
+
committedUsdPerMonth: z.ZodNullable<z.ZodNumber>;
|
|
190
203
|
apiEquivalent: z.ZodObject<{
|
|
191
204
|
oneDay: z.ZodObject<{
|
|
192
205
|
amountUsd: z.ZodNullable<z.ZodNumber>;
|
|
@@ -247,6 +260,47 @@ export declare const activitySnapshotSubscriptionAgentSchema: z.ZodObject<{
|
|
|
247
260
|
}>>;
|
|
248
261
|
}, z.core.$strict>;
|
|
249
262
|
export type ActivitySnapshotSubscriptionAgent = z.infer<typeof activitySnapshotSubscriptionAgentSchema>;
|
|
263
|
+
/**
|
|
264
|
+
* C-lane §2.1: a provider-billed subscription with no local agent (cursor
|
|
265
|
+
* today). The writer includes a billed30d amount ONLY when its financial
|
|
266
|
+
* evidence is "verified"; the renderer independently drops anything else
|
|
267
|
+
* (double lock: estimated/unverified provider dollars can never reach a
|
|
268
|
+
* statusline segment).
|
|
269
|
+
*/
|
|
270
|
+
export declare const activitySnapshotProviderSubscriptionSchema: z.ZodObject<{
|
|
271
|
+
provider: z.ZodEnum<{
|
|
272
|
+
anthropic: "anthropic";
|
|
273
|
+
openai: "openai";
|
|
274
|
+
cursor: "cursor";
|
|
275
|
+
other: "other";
|
|
276
|
+
"github-copilot": "github-copilot";
|
|
277
|
+
}>;
|
|
278
|
+
billing: z.ZodLiteral<"subscription">;
|
|
279
|
+
planLabel: z.ZodNullable<z.ZodString>;
|
|
280
|
+
committedUsdPerMonth: z.ZodNullable<z.ZodNumber>;
|
|
281
|
+
billed30d: z.ZodObject<{
|
|
282
|
+
amountUsd: z.ZodNullable<z.ZodNumber>;
|
|
283
|
+
recordCount: z.ZodNumber;
|
|
284
|
+
basis: z.ZodLiteral<"provider_billed">;
|
|
285
|
+
financialEvidence: z.ZodEnum<{
|
|
286
|
+
verified: "verified";
|
|
287
|
+
missing: "missing";
|
|
288
|
+
}>;
|
|
289
|
+
coverage: z.ZodEnum<{
|
|
290
|
+
missing: "missing";
|
|
291
|
+
complete: "complete";
|
|
292
|
+
partial: "partial";
|
|
293
|
+
}>;
|
|
294
|
+
}, z.core.$strict>;
|
|
295
|
+
}, z.core.$strict>;
|
|
296
|
+
export type ActivitySnapshotProviderSubscription = z.infer<typeof activitySnapshotProviderSubscriptionSchema>;
|
|
297
|
+
/** C-lane §2.1: the committed $/mo total across every detected subscription. */
|
|
298
|
+
export declare const activitySnapshotCommittedTotalSchema: z.ZodObject<{
|
|
299
|
+
amountUsd: z.ZodNullable<z.ZodNumber>;
|
|
300
|
+
pricedSubs: z.ZodNumber;
|
|
301
|
+
totalSubs: z.ZodNumber;
|
|
302
|
+
}, z.core.$strict>;
|
|
303
|
+
export type ActivitySnapshotCommittedTotal = z.infer<typeof activitySnapshotCommittedTotalSchema>;
|
|
250
304
|
export declare const activitySnapshotOverageSchema: z.ZodObject<{
|
|
251
305
|
amountUsd: z.ZodNumber;
|
|
252
306
|
currency: z.ZodLiteral<"USD">;
|
|
@@ -276,8 +330,8 @@ export declare const activitySnapshotCoverageSchema: z.ZodObject<{
|
|
|
276
330
|
nonFinancialBytesPrefiltered: z.ZodNumber;
|
|
277
331
|
jsonlValidationCoverage: z.ZodEnum<{
|
|
278
332
|
complete: "complete";
|
|
279
|
-
financial_events_only: "financial_events_only";
|
|
280
333
|
not_reported: "not_reported";
|
|
334
|
+
financial_events_only: "financial_events_only";
|
|
281
335
|
}>;
|
|
282
336
|
}, z.core.$strict>>;
|
|
283
337
|
providers: z.ZodArray<z.ZodObject<{
|
|
@@ -309,10 +363,10 @@ export declare const activitySnapshotCoverageSchema: z.ZodObject<{
|
|
|
309
363
|
recordsPriced: z.ZodNumber;
|
|
310
364
|
recordsUnpriced: z.ZodNumber;
|
|
311
365
|
validationStatus: z.ZodEnum<{
|
|
312
|
-
failed: "failed";
|
|
313
366
|
complete: "complete";
|
|
314
|
-
not_checked: "not_checked";
|
|
315
367
|
partial: "partial";
|
|
368
|
+
failed: "failed";
|
|
369
|
+
not_checked: "not_checked";
|
|
316
370
|
}>;
|
|
317
371
|
pricingAsOf: z.ZodString;
|
|
318
372
|
networkUploaded: z.ZodLiteral<false>;
|
|
@@ -320,7 +374,7 @@ export declare const activitySnapshotCoverageSchema: z.ZodObject<{
|
|
|
320
374
|
export type ActivitySnapshotCoverage = z.infer<typeof activitySnapshotCoverageSchema>;
|
|
321
375
|
export declare const activitySnapshotSchema: z.ZodObject<{
|
|
322
376
|
kind: z.ZodLiteral<"aibill.activity_snapshot">;
|
|
323
|
-
schemaVersion: z.ZodLiteral<
|
|
377
|
+
schemaVersion: z.ZodLiteral<2>;
|
|
324
378
|
currency: z.ZodLiteral<"USD">;
|
|
325
379
|
asOf: z.ZodString;
|
|
326
380
|
generatedAt: z.ZodString;
|
|
@@ -332,20 +386,20 @@ export declare const activitySnapshotSchema: z.ZodObject<{
|
|
|
332
386
|
status: z.ZodLiteral<"error">;
|
|
333
387
|
errorCode: z.ZodEnum<{
|
|
334
388
|
unknown: "unknown";
|
|
389
|
+
timeout: "timeout";
|
|
335
390
|
scan_failed: "scan_failed";
|
|
336
391
|
source_unreadable: "source_unreadable";
|
|
337
392
|
invalid_evidence: "invalid_evidence";
|
|
338
|
-
timeout: "timeout";
|
|
339
393
|
cache_write_failed: "cache_write_failed";
|
|
340
394
|
}>;
|
|
341
395
|
}, z.core.$strict>], "status">;
|
|
342
396
|
mode: z.ZodEnum<{
|
|
343
397
|
error: "error";
|
|
398
|
+
empty: "empty";
|
|
344
399
|
subscription: "subscription";
|
|
345
400
|
metered: "metered";
|
|
346
401
|
mixed: "mixed";
|
|
347
402
|
unresolved: "unresolved";
|
|
348
|
-
empty: "empty";
|
|
349
403
|
}>;
|
|
350
404
|
subscription: z.ZodNullable<z.ZodObject<{
|
|
351
405
|
agents: z.ZodArray<z.ZodObject<{
|
|
@@ -358,6 +412,7 @@ export declare const activitySnapshotSchema: z.ZodObject<{
|
|
|
358
412
|
"chatgpt-plus": "chatgpt-plus";
|
|
359
413
|
"chatgpt-pro": "chatgpt-pro";
|
|
360
414
|
}>>;
|
|
415
|
+
committedUsdPerMonth: z.ZodNullable<z.ZodNumber>;
|
|
361
416
|
apiEquivalent: z.ZodObject<{
|
|
362
417
|
oneDay: z.ZodObject<{
|
|
363
418
|
amountUsd: z.ZodNullable<z.ZodNumber>;
|
|
@@ -576,6 +631,37 @@ export declare const activitySnapshotSchema: z.ZodObject<{
|
|
|
576
631
|
}, z.core.$strict>;
|
|
577
632
|
}, z.core.$strict>;
|
|
578
633
|
}, z.core.$strict>>;
|
|
634
|
+
providers: z.ZodNullable<z.ZodArray<z.ZodObject<{
|
|
635
|
+
provider: z.ZodEnum<{
|
|
636
|
+
anthropic: "anthropic";
|
|
637
|
+
openai: "openai";
|
|
638
|
+
cursor: "cursor";
|
|
639
|
+
other: "other";
|
|
640
|
+
"github-copilot": "github-copilot";
|
|
641
|
+
}>;
|
|
642
|
+
billing: z.ZodLiteral<"subscription">;
|
|
643
|
+
planLabel: z.ZodNullable<z.ZodString>;
|
|
644
|
+
committedUsdPerMonth: z.ZodNullable<z.ZodNumber>;
|
|
645
|
+
billed30d: z.ZodObject<{
|
|
646
|
+
amountUsd: z.ZodNullable<z.ZodNumber>;
|
|
647
|
+
recordCount: z.ZodNumber;
|
|
648
|
+
basis: z.ZodLiteral<"provider_billed">;
|
|
649
|
+
financialEvidence: z.ZodEnum<{
|
|
650
|
+
verified: "verified";
|
|
651
|
+
missing: "missing";
|
|
652
|
+
}>;
|
|
653
|
+
coverage: z.ZodEnum<{
|
|
654
|
+
missing: "missing";
|
|
655
|
+
complete: "complete";
|
|
656
|
+
partial: "partial";
|
|
657
|
+
}>;
|
|
658
|
+
}, z.core.$strict>;
|
|
659
|
+
}, z.core.$strict>>>;
|
|
660
|
+
committedTotal: z.ZodObject<{
|
|
661
|
+
amountUsd: z.ZodNullable<z.ZodNumber>;
|
|
662
|
+
pricedSubs: z.ZodNumber;
|
|
663
|
+
totalSubs: z.ZodNumber;
|
|
664
|
+
}, z.core.$strict>;
|
|
579
665
|
overage: z.ZodNullable<z.ZodObject<{
|
|
580
666
|
amountUsd: z.ZodNumber;
|
|
581
667
|
currency: z.ZodLiteral<"USD">;
|
|
@@ -604,8 +690,8 @@ export declare const activitySnapshotSchema: z.ZodObject<{
|
|
|
604
690
|
nonFinancialBytesPrefiltered: z.ZodNumber;
|
|
605
691
|
jsonlValidationCoverage: z.ZodEnum<{
|
|
606
692
|
complete: "complete";
|
|
607
|
-
financial_events_only: "financial_events_only";
|
|
608
693
|
not_reported: "not_reported";
|
|
694
|
+
financial_events_only: "financial_events_only";
|
|
609
695
|
}>;
|
|
610
696
|
}, z.core.$strict>>;
|
|
611
697
|
providers: z.ZodArray<z.ZodObject<{
|
|
@@ -637,10 +723,10 @@ export declare const activitySnapshotSchema: z.ZodObject<{
|
|
|
637
723
|
recordsPriced: z.ZodNumber;
|
|
638
724
|
recordsUnpriced: z.ZodNumber;
|
|
639
725
|
validationStatus: z.ZodEnum<{
|
|
640
|
-
failed: "failed";
|
|
641
726
|
complete: "complete";
|
|
642
|
-
not_checked: "not_checked";
|
|
643
727
|
partial: "partial";
|
|
728
|
+
failed: "failed";
|
|
729
|
+
not_checked: "not_checked";
|
|
644
730
|
}>;
|
|
645
731
|
pricingAsOf: z.ZodString;
|
|
646
732
|
networkUploaded: z.ZodLiteral<false>;
|
|
@@ -654,6 +740,12 @@ export type ActivitySnapshot = z.infer<typeof activitySnapshotSchema>;
|
|
|
654
740
|
* IDs already validated by the external connected-state trust receipt.
|
|
655
741
|
*/
|
|
656
742
|
export declare function buildActivitySnapshot(input: ActivitySnapshotBuildInput): ActivitySnapshot;
|
|
743
|
+
/**
|
|
744
|
+
* The v1 dual-write payload (C-lane §2.1 fleet back-compat): today's fields
|
|
745
|
+
* only, so an already-installed v1 runner keeps rendering fresh data instead
|
|
746
|
+
* of decaying into permanent staleness. Exact v1 key set; no v2 fields.
|
|
747
|
+
*/
|
|
748
|
+
export declare function activitySnapshotV1Payload(snapshot: ActivitySnapshot): Record<string, unknown>;
|
|
657
749
|
/** A bounded no-evidence state for an initial failed refresh. */
|
|
658
750
|
export declare function createActivitySnapshotError(attemptedAt: string, errorCode: ActivitySnapshotRefreshErrorCode): ActivitySnapshot;
|
|
659
751
|
//# sourceMappingURL=activitySnapshot.d.ts.map
|
package/dist/activitySnapshot.js
CHANGED
|
@@ -2,6 +2,7 @@ import { z } from "zod";
|
|
|
2
2
|
import { aggregateCalls, dedupeCumulativeSessionCalls } from "./localAgentLogs.js";
|
|
3
3
|
import { localAgentFormatDescriptors } from "./localAgentFormats/registry.js";
|
|
4
4
|
import { canPriceTokenUsageAtScope, estimateTokenCostUsd, PRICING_TABLE_AS_OF } from "./modelPricing.js";
|
|
5
|
+
import { subscriptionPlans } from "./planMath.js";
|
|
5
6
|
import { isBundledSampleUsage } from "./schema.js";
|
|
6
7
|
import { sourceValidationCoverageValues } from "./sourceStatus.js";
|
|
7
8
|
const DAY_MS = 24 * 60 * 60 * 1_000;
|
|
@@ -163,6 +164,8 @@ export const activitySnapshotSubscriptionAgentSchema = z.object({
|
|
|
163
164
|
agent: agentSchema,
|
|
164
165
|
billing: z.literal("subscription"),
|
|
165
166
|
planId: planIdSchema,
|
|
167
|
+
/** C-lane §2.1: detected-plan list price; null when the plan is unpriced. */
|
|
168
|
+
committedUsdPerMonth: usdSchema.nullable(),
|
|
166
169
|
apiEquivalent: activitySnapshotApiEquivalentWindowsSchema,
|
|
167
170
|
limits: z.array(activitySnapshotLimitSchema).max(2),
|
|
168
171
|
pressure: z.enum(["extra_usage_credits_exhausted"]).nullable()
|
|
@@ -215,6 +218,41 @@ const activitySnapshotUnresolvedSchema = z.object({
|
|
|
215
218
|
});
|
|
216
219
|
}
|
|
217
220
|
});
|
|
221
|
+
/**
|
|
222
|
+
* C-lane §2.1: a provider-billed subscription with no local agent (cursor
|
|
223
|
+
* today). The writer includes a billed30d amount ONLY when its financial
|
|
224
|
+
* evidence is "verified"; the renderer independently drops anything else
|
|
225
|
+
* (double lock: estimated/unverified provider dollars can never reach a
|
|
226
|
+
* statusline segment).
|
|
227
|
+
*/
|
|
228
|
+
export const activitySnapshotProviderSubscriptionSchema = z.object({
|
|
229
|
+
provider: z.enum(activitySnapshotProviderValues),
|
|
230
|
+
billing: z.literal("subscription"),
|
|
231
|
+
planLabel: z.string().min(1).max(64).nullable(),
|
|
232
|
+
committedUsdPerMonth: usdSchema.nullable(),
|
|
233
|
+
billed30d: activitySnapshotBilledWindowSchema
|
|
234
|
+
}).strict();
|
|
235
|
+
/** C-lane §2.1: the committed $/mo total across every detected subscription. */
|
|
236
|
+
export const activitySnapshotCommittedTotalSchema = z.object({
|
|
237
|
+
amountUsd: usdSchema.nullable(),
|
|
238
|
+
pricedSubs: countSchema,
|
|
239
|
+
totalSubs: countSchema
|
|
240
|
+
}).strict().superRefine((total, context) => {
|
|
241
|
+
if (total.pricedSubs > total.totalSubs) {
|
|
242
|
+
context.addIssue({
|
|
243
|
+
code: "custom",
|
|
244
|
+
path: ["pricedSubs"],
|
|
245
|
+
message: "Priced subscriptions cannot exceed total subscriptions."
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
if ((total.amountUsd === null) !== (total.pricedSubs === 0)) {
|
|
249
|
+
context.addIssue({
|
|
250
|
+
code: "custom",
|
|
251
|
+
path: ["amountUsd"],
|
|
252
|
+
message: "A committed total exists exactly when at least one subscription is priced."
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
});
|
|
218
256
|
export const activitySnapshotOverageSchema = z.object({
|
|
219
257
|
amountUsd: z.number().finite().positive(),
|
|
220
258
|
currency: z.literal("USD"),
|
|
@@ -330,7 +368,10 @@ export const activitySnapshotCoverageSchema = z.object({
|
|
|
330
368
|
});
|
|
331
369
|
export const activitySnapshotSchema = z.object({
|
|
332
370
|
kind: z.literal("aibill.activity_snapshot"),
|
|
333
|
-
|
|
371
|
+
// v2 (C-lane §2.1): adds per-agent committedUsdPerMonth, provider-billed
|
|
372
|
+
// subscriptions, and the committed total. The writer dual-writes a v1
|
|
373
|
+
// payload for already-installed v1 runners during the deprecation window.
|
|
374
|
+
schemaVersion: z.literal(2),
|
|
334
375
|
currency: z.literal("USD"),
|
|
335
376
|
asOf: isoTimestampSchema,
|
|
336
377
|
generatedAt: isoTimestampSchema,
|
|
@@ -347,6 +388,8 @@ export const activitySnapshotSchema = z.object({
|
|
|
347
388
|
subscription: activitySnapshotSubscriptionSchema.nullable(),
|
|
348
389
|
metered: activitySnapshotMeteredSchema.nullable(),
|
|
349
390
|
unresolved: activitySnapshotUnresolvedSchema.nullable(),
|
|
391
|
+
providers: z.array(activitySnapshotProviderSubscriptionSchema).max(5).nullable(),
|
|
392
|
+
committedTotal: activitySnapshotCommittedTotalSchema,
|
|
350
393
|
overage: activitySnapshotOverageSchema.nullable(),
|
|
351
394
|
coverage: activitySnapshotCoverageSchema,
|
|
352
395
|
networkUploaded: z.literal(false)
|
|
@@ -372,6 +415,20 @@ export const activitySnapshotSchema = z.object({
|
|
|
372
415
|
(snapshot.subscription || snapshot.metered || snapshot.unresolved || snapshot.overage)) {
|
|
373
416
|
invalid("Empty and error snapshots cannot carry financial cohorts.", ["mode"]);
|
|
374
417
|
}
|
|
418
|
+
if ((snapshot.mode === "empty" || snapshot.mode === "error") &&
|
|
419
|
+
(snapshot.providers || snapshot.committedTotal.amountUsd !== null ||
|
|
420
|
+
snapshot.committedTotal.totalSubs !== 0)) {
|
|
421
|
+
invalid("Empty and error snapshots cannot carry subscription pricing.", ["committedTotal"]);
|
|
422
|
+
}
|
|
423
|
+
if (snapshot.providers &&
|
|
424
|
+
new Set(snapshot.providers.map((provider) => provider.provider)).size !== snapshot.providers.length) {
|
|
425
|
+
invalid("A provider subscription may appear only once.", ["providers"]);
|
|
426
|
+
}
|
|
427
|
+
const expectedTotalSubs = (snapshot.subscription?.agents.length ?? 0) +
|
|
428
|
+
(snapshot.providers?.length ?? 0);
|
|
429
|
+
if (snapshot.committedTotal.totalSubs !== expectedTotalSubs) {
|
|
430
|
+
invalid("The committed total must count every subscription row exactly once.", ["committedTotal"]);
|
|
431
|
+
}
|
|
375
432
|
if (snapshot.mode === "error" && snapshot.refresh.status !== "error") {
|
|
376
433
|
invalid("Error mode requires an error refresh state.", ["refresh"]);
|
|
377
434
|
}
|
|
@@ -559,9 +616,32 @@ export function buildActivitySnapshot(input) {
|
|
|
559
616
|
} : null;
|
|
560
617
|
const overage = buildOverage(meteredBilledRecords, overageIds, asOfMs);
|
|
561
618
|
const coverage = buildCoverage(classified.map((entry) => entry.record), scans, providers, input.pricingAsOf ?? PRICING_TABLE_AS_OF, deduplicated.conflictingIds);
|
|
619
|
+
const finalSubscription = mode === "metered" || mode === "unresolved" || mode === "empty"
|
|
620
|
+
? null
|
|
621
|
+
: subscription;
|
|
622
|
+
// Writer-side lock (C-lane §2.1): a provider-billed amount survives only
|
|
623
|
+
// when its supplied window is verified; anything else degrades to missing.
|
|
624
|
+
const providerSubscriptionRows = mode === "empty"
|
|
625
|
+
? []
|
|
626
|
+
: (input.providerSubscriptions ?? []).map((row) => ({
|
|
627
|
+
provider: row.provider,
|
|
628
|
+
billing: "subscription",
|
|
629
|
+
planLabel: row.planLabel,
|
|
630
|
+
committedUsdPerMonth: row.committedUsdPerMonth,
|
|
631
|
+
billed30d: row.billed30d &&
|
|
632
|
+
row.billed30d.financialEvidence === "verified" &&
|
|
633
|
+
row.billed30d.amountUsd !== null
|
|
634
|
+
? row.billed30d
|
|
635
|
+
: missingBilledWindow()
|
|
636
|
+
}));
|
|
637
|
+
const committedRows = [
|
|
638
|
+
...(finalSubscription?.agents ?? []).map((agent) => agent.committedUsdPerMonth),
|
|
639
|
+
...providerSubscriptionRows.map((row) => row.committedUsdPerMonth)
|
|
640
|
+
];
|
|
641
|
+
const pricedRows = committedRows.filter((amount) => amount !== null);
|
|
562
642
|
return activitySnapshotSchema.parse({
|
|
563
643
|
kind: "aibill.activity_snapshot",
|
|
564
|
-
schemaVersion:
|
|
644
|
+
schemaVersion: 2,
|
|
565
645
|
currency: "USD",
|
|
566
646
|
asOf: new Date(asOfMs).toISOString(),
|
|
567
647
|
generatedAt,
|
|
@@ -569,24 +649,80 @@ export function buildActivitySnapshot(input) {
|
|
|
569
649
|
lastSuccessAt: generatedAt,
|
|
570
650
|
refresh: { status: "ok" },
|
|
571
651
|
mode,
|
|
572
|
-
subscription:
|
|
573
|
-
? null
|
|
574
|
-
: subscription,
|
|
652
|
+
subscription: finalSubscription,
|
|
575
653
|
metered: mode === "subscription" || mode === "unresolved" || mode === "empty"
|
|
576
654
|
? null
|
|
577
655
|
: metered,
|
|
578
656
|
unresolved,
|
|
657
|
+
providers: providerSubscriptionRows.length > 0 ? providerSubscriptionRows : null,
|
|
658
|
+
committedTotal: {
|
|
659
|
+
amountUsd: pricedRows.length > 0
|
|
660
|
+
? roundUsd(pricedRows.reduce((total, amount) => total + amount, 0))
|
|
661
|
+
: null,
|
|
662
|
+
pricedSubs: pricedRows.length,
|
|
663
|
+
totalSubs: committedRows.length
|
|
664
|
+
},
|
|
579
665
|
overage: mode === "metered" || mode === "mixed" ? overage : null,
|
|
580
666
|
coverage,
|
|
581
667
|
networkUploaded: false
|
|
582
668
|
});
|
|
583
669
|
}
|
|
670
|
+
/** Detected-plan list price (subscriptionPlans); null when unpriced. */
|
|
671
|
+
function committedPriceForPlanId(planId) {
|
|
672
|
+
if (!planId)
|
|
673
|
+
return null;
|
|
674
|
+
return subscriptionPlans.find((plan) => plan.id === planId)?.monthlyUsd ?? null;
|
|
675
|
+
}
|
|
676
|
+
function missingBilledWindow() {
|
|
677
|
+
return {
|
|
678
|
+
amountUsd: null,
|
|
679
|
+
recordCount: 0,
|
|
680
|
+
basis: "provider_billed",
|
|
681
|
+
financialEvidence: "missing",
|
|
682
|
+
coverage: "missing"
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
/**
|
|
686
|
+
* The v1 dual-write payload (C-lane §2.1 fleet back-compat): today's fields
|
|
687
|
+
* only, so an already-installed v1 runner keeps rendering fresh data instead
|
|
688
|
+
* of decaying into permanent staleness. Exact v1 key set; no v2 fields.
|
|
689
|
+
*/
|
|
690
|
+
export function activitySnapshotV1Payload(snapshot) {
|
|
691
|
+
return {
|
|
692
|
+
kind: snapshot.kind,
|
|
693
|
+
schemaVersion: 1,
|
|
694
|
+
currency: snapshot.currency,
|
|
695
|
+
asOf: snapshot.asOf,
|
|
696
|
+
generatedAt: snapshot.generatedAt,
|
|
697
|
+
lastAttemptAt: snapshot.lastAttemptAt,
|
|
698
|
+
lastSuccessAt: snapshot.lastSuccessAt,
|
|
699
|
+
refresh: snapshot.refresh,
|
|
700
|
+
mode: snapshot.mode,
|
|
701
|
+
subscription: snapshot.subscription
|
|
702
|
+
? {
|
|
703
|
+
agents: snapshot.subscription.agents.map((agent) => ({
|
|
704
|
+
agent: agent.agent,
|
|
705
|
+
billing: agent.billing,
|
|
706
|
+
planId: agent.planId,
|
|
707
|
+
apiEquivalent: agent.apiEquivalent,
|
|
708
|
+
limits: agent.limits,
|
|
709
|
+
pressure: agent.pressure
|
|
710
|
+
}))
|
|
711
|
+
}
|
|
712
|
+
: null,
|
|
713
|
+
metered: snapshot.metered,
|
|
714
|
+
unresolved: snapshot.unresolved,
|
|
715
|
+
overage: snapshot.overage,
|
|
716
|
+
coverage: snapshot.coverage,
|
|
717
|
+
networkUploaded: snapshot.networkUploaded
|
|
718
|
+
};
|
|
719
|
+
}
|
|
584
720
|
/** A bounded no-evidence state for an initial failed refresh. */
|
|
585
721
|
export function createActivitySnapshotError(attemptedAt, errorCode) {
|
|
586
722
|
const timestamp = new Date(parseTimestamp(attemptedAt, "attemptedAt")).toISOString();
|
|
587
723
|
return activitySnapshotSchema.parse({
|
|
588
724
|
kind: "aibill.activity_snapshot",
|
|
589
|
-
schemaVersion:
|
|
725
|
+
schemaVersion: 2,
|
|
590
726
|
currency: "USD",
|
|
591
727
|
asOf: timestamp,
|
|
592
728
|
generatedAt: timestamp,
|
|
@@ -597,6 +733,8 @@ export function createActivitySnapshotError(attemptedAt, errorCode) {
|
|
|
597
733
|
subscription: null,
|
|
598
734
|
metered: null,
|
|
599
735
|
unresolved: null,
|
|
736
|
+
providers: null,
|
|
737
|
+
committedTotal: { amountUsd: null, pricedSubs: 0, totalSubs: 0 },
|
|
600
738
|
overage: null,
|
|
601
739
|
coverage: {
|
|
602
740
|
agents: [],
|
|
@@ -650,6 +788,7 @@ function activitySubscriptionAgents(records, calls, allCalls, plans, scans, trus
|
|
|
650
788
|
agent,
|
|
651
789
|
billing: "subscription",
|
|
652
790
|
planId: isKnownPlanId(plan.planId) ? plan.planId : null,
|
|
791
|
+
committedUsdPerMonth: committedPriceForPlanId(plan.planId),
|
|
653
792
|
apiEquivalent: buildApiWindows(agentRecords, allCalls.filter((call) => call.agent === agent), trustedProviderIds, asOfMs, localCoverageForAgent(agent, scans)),
|
|
654
793
|
limits: latestReportedLimits(agentCalls, asOfMs),
|
|
655
794
|
pressure: plan.limitSignal === "extra-usage credits exhausted"
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import { type ActivitySnapshot, type ActivitySnapshotRefreshErrorCode } from "./activitySnapshot.js";
|
|
2
2
|
export declare const activitySnapshotCacheEnvironmentVariable = "AIBILL_CACHE_DIR";
|
|
3
|
-
|
|
3
|
+
/** The v2 snapshot cache (C-lane §2.1). */
|
|
4
|
+
export declare const activitySnapshotCacheFileName = "statusline-v2.json";
|
|
5
|
+
/**
|
|
6
|
+
* Deprecation-window dual-write target: installed v1 runners are frozen
|
|
7
|
+
* copies that read only this file, so every v2 write also refreshes a v1
|
|
8
|
+
* payload here (today's fields only) instead of stranding them stale.
|
|
9
|
+
*/
|
|
10
|
+
export declare const activitySnapshotLegacyCacheFileName = "statusline-v1.json";
|
|
4
11
|
export declare const activitySnapshotCacheMaxBytes: number;
|
|
5
12
|
export type ActivitySnapshotCacheOptions = {
|
|
6
13
|
/** Test/embedding override. Production defaults to ~/.aibill/cache. */
|
|
@@ -1,18 +1,30 @@
|
|
|
1
1
|
import { constants } from "node:fs";
|
|
2
|
+
import { execFile as execFileCallback } from "node:child_process";
|
|
2
3
|
import { open, lstat, mkdir, chmod, realpath, rename, unlink } from "node:fs/promises";
|
|
3
4
|
import { homedir } from "node:os";
|
|
4
|
-
import { join, resolve } from "node:path";
|
|
5
|
+
import { dirname, join, relative, resolve } from "node:path";
|
|
5
6
|
import { randomUUID } from "node:crypto";
|
|
6
7
|
import { setTimeout as delay } from "node:timers/promises";
|
|
7
|
-
import {
|
|
8
|
+
import { promisify } from "node:util";
|
|
9
|
+
import { activitySnapshotSchema, activitySnapshotV1Payload, createActivitySnapshotError } from "./activitySnapshot.js";
|
|
8
10
|
export const activitySnapshotCacheEnvironmentVariable = "AIBILL_CACHE_DIR";
|
|
9
|
-
|
|
11
|
+
/** The v2 snapshot cache (C-lane §2.1). */
|
|
12
|
+
export const activitySnapshotCacheFileName = "statusline-v2.json";
|
|
13
|
+
/**
|
|
14
|
+
* Deprecation-window dual-write target: installed v1 runners are frozen
|
|
15
|
+
* copies that read only this file, so every v2 write also refreshes a v1
|
|
16
|
+
* payload here (today's fields only) instead of stranding them stale.
|
|
17
|
+
*/
|
|
18
|
+
export const activitySnapshotLegacyCacheFileName = "statusline-v1.json";
|
|
10
19
|
export const activitySnapshotCacheMaxBytes = 64 * 1_024;
|
|
20
|
+
// The lock name is shared with pre-v2 writers on purpose: during the fleet
|
|
21
|
+
// deprecation window both CLI generations serialize through one lock.
|
|
11
22
|
const lockFileName = ".statusline-v1.lock";
|
|
12
23
|
const defaultLockTimeoutMs = 2_000;
|
|
13
24
|
const staleLockMs = 15_000;
|
|
14
25
|
const lockPollMs = 20;
|
|
15
26
|
const lockMetadataMaxBytes = 512;
|
|
27
|
+
const execFile = promisify(execFileCallback);
|
|
16
28
|
export class ActivitySnapshotCacheError extends Error {
|
|
17
29
|
code;
|
|
18
30
|
constructor(code, message) {
|
|
@@ -207,7 +219,7 @@ async function readSnapshotFile(directory) {
|
|
|
207
219
|
catch {
|
|
208
220
|
return { status: "error", code: "malformed" };
|
|
209
221
|
}
|
|
210
|
-
if (isRecord(value) && value.schemaVersion !== undefined && value.schemaVersion !==
|
|
222
|
+
if (isRecord(value) && value.schemaVersion !== undefined && value.schemaVersion !== 2) {
|
|
211
223
|
return { status: "error", code: "unsupported_version" };
|
|
212
224
|
}
|
|
213
225
|
const parsed = activitySnapshotSchema.safeParse(value);
|
|
@@ -225,11 +237,17 @@ async function readSnapshotFile(directory) {
|
|
|
225
237
|
}
|
|
226
238
|
}
|
|
227
239
|
async function atomicWriteSnapshot(directory, snapshot) {
|
|
228
|
-
|
|
240
|
+
await atomicWriteCacheFile(directory, activitySnapshotCacheFileName, `${JSON.stringify(snapshot)}\n`);
|
|
241
|
+
// C-lane §2.1 fleet back-compat: dual-write the v1 payload so an
|
|
242
|
+
// already-installed v1 runner keeps rendering fresh data during the
|
|
243
|
+
// deprecation window instead of decaying into permanent staleness.
|
|
244
|
+
await atomicWriteCacheFile(directory, activitySnapshotLegacyCacheFileName, `${JSON.stringify(activitySnapshotV1Payload(snapshot))}\n`);
|
|
245
|
+
}
|
|
246
|
+
async function atomicWriteCacheFile(directory, fileName, contents) {
|
|
229
247
|
if (Buffer.byteLength(contents, "utf8") > activitySnapshotCacheMaxBytes) {
|
|
230
248
|
throw new ActivitySnapshotCacheError("invalid_snapshot", "Activity snapshot exceeds the 64 KiB cache limit.");
|
|
231
249
|
}
|
|
232
|
-
const filePath = join(directory,
|
|
250
|
+
const filePath = join(directory, fileName);
|
|
233
251
|
const existing = await lstat(filePath).catch((error) => {
|
|
234
252
|
if (isNodeError(error, "ENOENT"))
|
|
235
253
|
return undefined;
|
|
@@ -238,7 +256,7 @@ async function atomicWriteSnapshot(directory, snapshot) {
|
|
|
238
256
|
if (existing?.isSymbolicLink() || (existing && !existing.isFile())) {
|
|
239
257
|
throw new ActivitySnapshotCacheError("unsafe_file", "Activity snapshot cache path is not a regular file.");
|
|
240
258
|
}
|
|
241
|
-
const temporaryPath = join(directory, `.${
|
|
259
|
+
const temporaryPath = join(directory, `.${fileName}.${process.pid}.${randomUUID()}.tmp`);
|
|
242
260
|
let handle;
|
|
243
261
|
try {
|
|
244
262
|
handle = await open(temporaryPath, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | noFollowFlag(), 0o600);
|
|
@@ -329,6 +347,84 @@ async function ensureDefaultParent(homeDirectory, create) {
|
|
|
329
347
|
}
|
|
330
348
|
if (create)
|
|
331
349
|
await chmod(parent, 0o700);
|
|
350
|
+
await ensureDefaultCacheGitPrivacy(parent, create);
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* When a synthetic or real HOME is itself inside a Git worktree, protect the
|
|
354
|
+
* complete top-level private state directory before any cache child is
|
|
355
|
+
* created. Explicit AIBILL_CACHE_DIR/cacheDirectory overrides intentionally
|
|
356
|
+
* remain caller-owned and never receive repository files from this helper.
|
|
357
|
+
*/
|
|
358
|
+
async function ensureDefaultCacheGitPrivacy(aibillDirectory, create) {
|
|
359
|
+
const gitRoot = await findEnclosingGitRoot(aibillDirectory);
|
|
360
|
+
if (!gitRoot)
|
|
361
|
+
return;
|
|
362
|
+
const marker = join(aibillDirectory, ".gitignore");
|
|
363
|
+
let handle;
|
|
364
|
+
try {
|
|
365
|
+
handle = await open(marker, constants.O_RDONLY | noFollowFlag());
|
|
366
|
+
}
|
|
367
|
+
catch (error) {
|
|
368
|
+
if (!isNodeError(error, "ENOENT"))
|
|
369
|
+
throw error;
|
|
370
|
+
if (!create) {
|
|
371
|
+
const missing = new Error("Private aibill Git privacy marker does not exist.");
|
|
372
|
+
missing.code = "ENOENT";
|
|
373
|
+
throw missing;
|
|
374
|
+
}
|
|
375
|
+
handle = await open(marker, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | noFollowFlag(), 0o600);
|
|
376
|
+
await handle.writeFile("*\n", "utf8");
|
|
377
|
+
await handle.sync();
|
|
378
|
+
await handle.close();
|
|
379
|
+
handle = await open(marker, constants.O_RDONLY | noFollowFlag());
|
|
380
|
+
}
|
|
381
|
+
try {
|
|
382
|
+
const info = await handle.stat();
|
|
383
|
+
if (!info.isFile() || !hasPrivatePermissions(info.mode) || info.size !== 2) {
|
|
384
|
+
throw new ActivitySnapshotCacheError("unsafe_directory", "Private aibill Git privacy marker is unsafe.");
|
|
385
|
+
}
|
|
386
|
+
const buffer = Buffer.alloc(2);
|
|
387
|
+
const { bytesRead } = await handle.read(buffer, 0, 2, 0);
|
|
388
|
+
if (bytesRead !== 2 || buffer.toString("utf8") !== "*\n") {
|
|
389
|
+
throw new ActivitySnapshotCacheError("unsafe_directory", "Private aibill Git privacy marker is invalid.");
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
finally {
|
|
393
|
+
await handle.close().catch(() => undefined);
|
|
394
|
+
}
|
|
395
|
+
const relativeDirectory = relative(gitRoot, aibillDirectory);
|
|
396
|
+
const tracked = await execFile("git", ["-C", gitRoot, "ls-files", "--", relativeDirectory], {
|
|
397
|
+
encoding: "utf8",
|
|
398
|
+
maxBuffer: 64 * 1024
|
|
399
|
+
}).then(({ stdout }) => stdout.trim()).catch(() => {
|
|
400
|
+
throw new ActivitySnapshotCacheError("unsafe_directory", "Private aibill cache tracking status could not be verified.");
|
|
401
|
+
});
|
|
402
|
+
if (tracked) {
|
|
403
|
+
throw new ActivitySnapshotCacheError("unsafe_directory", "Private aibill cache is already tracked by Git.");
|
|
404
|
+
}
|
|
405
|
+
const ignored = await execFile("git", [
|
|
406
|
+
"-C", gitRoot, "check-ignore", "--quiet", "--no-index", "--",
|
|
407
|
+
join(relativeDirectory, "cache", "privacy-probe.json")
|
|
408
|
+
]).then(() => true).catch(() => false);
|
|
409
|
+
if (!ignored) {
|
|
410
|
+
throw new ActivitySnapshotCacheError("unsafe_directory", "Private aibill cache is not proven ignored by Git.");
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
async function findEnclosingGitRoot(path) {
|
|
414
|
+
let current = resolve(path);
|
|
415
|
+
while (true) {
|
|
416
|
+
const gitEntry = await lstat(join(current, ".git")).catch((error) => {
|
|
417
|
+
if (isNodeError(error, "ENOENT") || isNodeError(error, "ENOTDIR"))
|
|
418
|
+
return undefined;
|
|
419
|
+
throw error;
|
|
420
|
+
});
|
|
421
|
+
if (gitEntry)
|
|
422
|
+
return current;
|
|
423
|
+
const parent = dirname(current);
|
|
424
|
+
if (parent === current)
|
|
425
|
+
return undefined;
|
|
426
|
+
current = parent;
|
|
427
|
+
}
|
|
332
428
|
}
|
|
333
429
|
function configuredCacheDirectory(options) {
|
|
334
430
|
const configured = options.cacheDirectory?.trim() ||
|