ai-spend-agent 0.7.2 → 0.7.3
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/dist/index.js +0 -0
- package/dist/statuslineRuntime.js +205 -9
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
File without changes
|
|
@@ -155,9 +155,18 @@ export function renderStatusline(result, options = {}) {
|
|
|
155
155
|
if (overage) {
|
|
156
156
|
// Billed overage is the sole compact paid-alert bridge and must survive
|
|
157
157
|
// narrow layouts. It never derives from plan pressure.
|
|
158
|
+
if (hasMultipleSubscribedAgents(snapshot)) {
|
|
159
|
+
return assembleMultiSubscriptionOverageLine(snapshot, segments, overage, freshness, tier, columns, now);
|
|
160
|
+
}
|
|
158
161
|
return assembleOverageLine(segments, overage, freshness, columns);
|
|
159
162
|
}
|
|
163
|
+
if (snapshot.mode === "subscription" && hasMultipleSubscribedAgents(snapshot)) {
|
|
164
|
+
return assembleMultiSubscriptionLine(snapshot, freshness, tier, columns, now, options.timeZone);
|
|
165
|
+
}
|
|
160
166
|
if (snapshot.mode === "mixed") {
|
|
167
|
+
if (hasMultipleSubscribedAgents(snapshot)) {
|
|
168
|
+
return assembleMultiSubscriptionMixedLine(snapshot, segments, freshness, tier, columns, now, options.timeZone);
|
|
169
|
+
}
|
|
161
170
|
return assembleMixedLine(snapshot, segments, freshness, tier, columns, now, options.timeZone);
|
|
162
171
|
}
|
|
163
172
|
return assembleLine(segments, freshness, columns, overage);
|
|
@@ -256,6 +265,9 @@ function renderMetered(snapshot, tier) {
|
|
|
256
265
|
return rendered;
|
|
257
266
|
}
|
|
258
267
|
function renderSubscription(snapshot, tier, now, timeZone) {
|
|
268
|
+
if (hasMultipleSubscribedAgents(snapshot)) {
|
|
269
|
+
return renderMultiSubscriptionSegments(snapshot, tier, now, timeZone);
|
|
270
|
+
}
|
|
259
271
|
const agent = selectSubscriptionAgent(snapshot, now);
|
|
260
272
|
if (!agent)
|
|
261
273
|
return ["subscription detected", "runway not reported"];
|
|
@@ -276,6 +288,11 @@ function renderSubscription(snapshot, tier, now, timeZone) {
|
|
|
276
288
|
return segments.length > 0 ? segments : ["subscription detected"];
|
|
277
289
|
}
|
|
278
290
|
function renderMixed(snapshot, tier, now, timeZone) {
|
|
291
|
+
if (hasMultipleSubscribedAgents(snapshot)) {
|
|
292
|
+
const segments = renderMultiSubscriptionSegments(snapshot, tier, now, timeZone);
|
|
293
|
+
appendMeteredSevenDaySegment(snapshot, segments);
|
|
294
|
+
return segments.length > 0 ? segments : ["mixed billing", "amounts unavailable"];
|
|
295
|
+
}
|
|
279
296
|
const agent = selectSubscriptionAgent(snapshot, now);
|
|
280
297
|
const limits = agent ? activeLimits(agent, now) : [];
|
|
281
298
|
const selectedLimits = tier === "full"
|
|
@@ -284,6 +301,42 @@ function renderMixed(snapshot, tier, now, timeZone) {
|
|
|
284
301
|
const segments = selectedLimits.map((limit) => formatLimit(limit, now, timeZone));
|
|
285
302
|
if (limits.length === 0 && tier !== "minimal")
|
|
286
303
|
segments.push("runway not reported");
|
|
304
|
+
appendMeteredSevenDaySegment(snapshot, segments);
|
|
305
|
+
const value = agent?.apiEquivalent.sevenDays;
|
|
306
|
+
if (value?.amountUsd !== null && value?.amountUsd !== undefined &&
|
|
307
|
+
value.financialEvidence === "estimated") {
|
|
308
|
+
segments.push(`sub ~${formatUsd(value.amountUsd)} 7d value`);
|
|
309
|
+
}
|
|
310
|
+
if (agent?.pressure === "extra_usage_credits_exhausted" && tier !== "minimal") {
|
|
311
|
+
segments.push("plan pressure");
|
|
312
|
+
}
|
|
313
|
+
return segments.length > 0 ? segments : ["mixed billing", "amounts unavailable"];
|
|
314
|
+
}
|
|
315
|
+
function renderMultiSubscriptionSegments(snapshot, tier, now, timeZone) {
|
|
316
|
+
const entries = orderedSubscriptionLimits(snapshot, now);
|
|
317
|
+
const selected = tier === "full" ? entries : entries.slice(0, 1);
|
|
318
|
+
const segments = selected.map(({ agent, limit }) => tier === "full"
|
|
319
|
+
? formatAttributedLimit(agent, limit, now, timeZone)
|
|
320
|
+
: formatAttributedLimitCompact(agent, limit));
|
|
321
|
+
if (entries.length === 0 && tier !== "minimal") {
|
|
322
|
+
segments.push("subscription detected", "runway not reported");
|
|
323
|
+
}
|
|
324
|
+
const agents = orderedSubscriptionAgents(snapshot, now);
|
|
325
|
+
const valueAgents = tier === "full" ? agents : agents.slice(0, 1);
|
|
326
|
+
for (const agent of valueAgents) {
|
|
327
|
+
const value = agent.apiEquivalent.sevenDays;
|
|
328
|
+
if (value.amountUsd !== null && value.financialEvidence === "estimated") {
|
|
329
|
+
segments.push(`${subscriptionAgentLabel(agent)} ~${formatUsd(value.amountUsd)} 7d value`);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
if (tier !== "minimal") {
|
|
333
|
+
for (const agent of agents.filter(({ pressure }) => pressure === "extra_usage_credits_exhausted")) {
|
|
334
|
+
segments.push(`${subscriptionAgentLabel(agent)} plan pressure`);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
return segments.length > 0 ? segments : ["subscription detected"];
|
|
338
|
+
}
|
|
339
|
+
function appendMeteredSevenDaySegment(snapshot, segments) {
|
|
287
340
|
const billed = snapshot.metered?.providerBilled.sevenDays;
|
|
288
341
|
const estimated = snapshot.metered?.apiEquivalent.sevenDays;
|
|
289
342
|
if (billed?.amountUsd !== null && billed?.amountUsd !== undefined &&
|
|
@@ -294,15 +347,6 @@ function renderMixed(snapshot, tier, now, timeZone) {
|
|
|
294
347
|
estimated.financialEvidence === "estimated") {
|
|
295
348
|
segments.push(`metered ~${formatUsd(estimated.amountUsd)} 7d`);
|
|
296
349
|
}
|
|
297
|
-
const value = agent?.apiEquivalent.sevenDays;
|
|
298
|
-
if (value?.amountUsd !== null && value?.amountUsd !== undefined &&
|
|
299
|
-
value.financialEvidence === "estimated") {
|
|
300
|
-
segments.push(`sub ~${formatUsd(value.amountUsd)} 7d value`);
|
|
301
|
-
}
|
|
302
|
-
if (agent?.pressure === "extra_usage_credits_exhausted" && tier !== "minimal") {
|
|
303
|
-
segments.push("plan pressure");
|
|
304
|
-
}
|
|
305
|
-
return segments.length > 0 ? segments : ["mixed billing", "amounts unavailable"];
|
|
306
350
|
}
|
|
307
351
|
function renderUnresolved(snapshot, tier) {
|
|
308
352
|
const value = snapshot.unresolved?.apiEquivalent.sevenDays;
|
|
@@ -324,6 +368,56 @@ function selectSubscriptionAgent(snapshot, now) {
|
|
|
324
368
|
return left.agent.localeCompare(right.agent);
|
|
325
369
|
})[0];
|
|
326
370
|
}
|
|
371
|
+
function hasMultipleSubscribedAgents(snapshot) {
|
|
372
|
+
return (snapshot.subscription?.agents.length ?? 0) > 1;
|
|
373
|
+
}
|
|
374
|
+
function orderedSubscriptionLimits(snapshot, now) {
|
|
375
|
+
return (snapshot.subscription?.agents ?? [])
|
|
376
|
+
.flatMap((agent) => activeLimits(agent, now).map((limit) => ({ agent, limit })))
|
|
377
|
+
.sort(compareSubscriptionLimitEntries);
|
|
378
|
+
}
|
|
379
|
+
function primarySubscriptionLimits(snapshot, now) {
|
|
380
|
+
return (snapshot.subscription?.agents ?? [])
|
|
381
|
+
.flatMap((agent) => {
|
|
382
|
+
const limit = [...activeLimits(agent, now)].sort(compareUrgency)[0];
|
|
383
|
+
return limit ? [{ agent, limit }] : [];
|
|
384
|
+
})
|
|
385
|
+
.sort(compareSubscriptionLimitEntries);
|
|
386
|
+
}
|
|
387
|
+
function compareSubscriptionLimitEntries(left, right) {
|
|
388
|
+
const urgency = compareUrgency(left.limit, right.limit);
|
|
389
|
+
if (urgency !== 0)
|
|
390
|
+
return urgency;
|
|
391
|
+
const observationDifference = Date.parse(right.limit.observedAt) - Date.parse(left.limit.observedAt);
|
|
392
|
+
if (observationDifference !== 0)
|
|
393
|
+
return observationDifference;
|
|
394
|
+
return left.agent.agent.localeCompare(right.agent.agent);
|
|
395
|
+
}
|
|
396
|
+
function orderedSubscriptionAgents(snapshot, now) {
|
|
397
|
+
return [...(snapshot.subscription?.agents ?? [])].sort((left, right) => {
|
|
398
|
+
const leftUrgent = [...activeLimits(left, now)].sort(compareUrgency)[0];
|
|
399
|
+
const rightUrgent = [...activeLimits(right, now)].sort(compareUrgency)[0];
|
|
400
|
+
if (leftUrgent && rightUrgent) {
|
|
401
|
+
const urgency = compareUrgency(leftUrgent, rightUrgent);
|
|
402
|
+
if (urgency !== 0)
|
|
403
|
+
return urgency;
|
|
404
|
+
}
|
|
405
|
+
else if (leftUrgent) {
|
|
406
|
+
return -1;
|
|
407
|
+
}
|
|
408
|
+
else if (rightUrgent) {
|
|
409
|
+
return 1;
|
|
410
|
+
}
|
|
411
|
+
const latestDifference = latestLimitObservation(right, now) - latestLimitObservation(left, now);
|
|
412
|
+
if (latestDifference !== 0)
|
|
413
|
+
return latestDifference;
|
|
414
|
+
const recordsDifference = right.apiEquivalent.sevenDays.recordCount -
|
|
415
|
+
left.apiEquivalent.sevenDays.recordCount;
|
|
416
|
+
if (recordsDifference !== 0)
|
|
417
|
+
return recordsDifference;
|
|
418
|
+
return left.agent.localeCompare(right.agent);
|
|
419
|
+
});
|
|
420
|
+
}
|
|
327
421
|
function latestLimitObservation(agent, now) {
|
|
328
422
|
return Math.max(0, ...activeLimits(agent, now).map((limit) => Date.parse(limit.observedAt)));
|
|
329
423
|
}
|
|
@@ -347,6 +441,24 @@ function formatLimit(limit, now, timeZone) {
|
|
|
347
441
|
const reset = formatReset(limit, now, timeZone);
|
|
348
442
|
return `${label} ${formatPercent(limit.remainingPercent)}% left ↻${reset}`;
|
|
349
443
|
}
|
|
444
|
+
function subscriptionAgentLabel(agent) {
|
|
445
|
+
return agent.agent === "claude-code" ? "claude" : "codex";
|
|
446
|
+
}
|
|
447
|
+
function formatAttributedLimit(agent, limit, now, timeZone) {
|
|
448
|
+
const label = limit.kind === "five-hour" ? "5h" : "week";
|
|
449
|
+
return `${subscriptionAgentLabel(agent)} ${label} ${formatPercent(limit.remainingPercent)}% ↻${formatReset(limit, now, timeZone)}`;
|
|
450
|
+
}
|
|
451
|
+
function formatAttributedLimitCompact(agent, limit) {
|
|
452
|
+
const label = limit.kind === "five-hour" ? "5h" : "wk";
|
|
453
|
+
return `${subscriptionAgentLabel(agent)} ${label} ${formatPercent(limit.remainingPercent)}%`;
|
|
454
|
+
}
|
|
455
|
+
function formatAttributedValue(agent, compact = false) {
|
|
456
|
+
const value = agent.apiEquivalent.sevenDays;
|
|
457
|
+
if (value.amountUsd === null || value.financialEvidence !== "estimated")
|
|
458
|
+
return undefined;
|
|
459
|
+
const window = compact ? "/7d" : " 7d";
|
|
460
|
+
return `${subscriptionAgentLabel(agent)} ~${formatUsd(value.amountUsd)}${window} value`;
|
|
461
|
+
}
|
|
350
462
|
function formatReset(limit, now, timeZone) {
|
|
351
463
|
const reset = new Date(limit.resetsAt);
|
|
352
464
|
const zone = validTimeZone(timeZone) ? timeZone : undefined;
|
|
@@ -444,6 +556,21 @@ function assembleOverageLine(segments, overage, freshness, columns) {
|
|
|
444
556
|
["billed"]
|
|
445
557
|
], columns);
|
|
446
558
|
}
|
|
559
|
+
function assembleMultiSubscriptionOverageLine(snapshot, fullSegments, overage, freshness, tier, columns, now) {
|
|
560
|
+
const primary = primarySubscriptionLimits(snapshot, now);
|
|
561
|
+
const compactPrimary = primary.map(({ agent, limit }) => formatAttributedLimitCompact(agent, limit));
|
|
562
|
+
const urgent = compactPrimary[0];
|
|
563
|
+
const candidates = [];
|
|
564
|
+
if (tier === "full")
|
|
565
|
+
candidates.push(["aibill", ...fullSegments, overage, freshness]);
|
|
566
|
+
if (tier !== "minimal") {
|
|
567
|
+
candidates.push(["aibill", ...compactPrimary, overage, freshness]);
|
|
568
|
+
}
|
|
569
|
+
if (urgent)
|
|
570
|
+
candidates.push(["aibill", urgent, overage, freshness]);
|
|
571
|
+
candidates.push(["aibill", overage, freshness], ["aibill", overage], [overage], ["OVERAGE billed"], ["billed"]);
|
|
572
|
+
return firstFittingLine(candidates, columns);
|
|
573
|
+
}
|
|
447
574
|
function assembleMixedLine(snapshot, fullSegments, freshness, tier, columns, now, timeZone) {
|
|
448
575
|
const agent = selectSubscriptionAgent(snapshot, now);
|
|
449
576
|
const urgent = agent ? [...activeLimits(agent, now)].sort(compareUrgency)[0] : undefined;
|
|
@@ -486,6 +613,75 @@ function assembleMixedLine(snapshot, fullSegments, freshness, tier, columns, now
|
|
|
486
613
|
candidates.push(["mix", runway, meteredClear, shortFreshness], ["mix", runway.replace("↻", ""), meteredClear], ["mix", meteredClear], ["mix", metered], ["mix"]);
|
|
487
614
|
return firstFittingLine(candidates, columns);
|
|
488
615
|
}
|
|
616
|
+
function assembleMultiSubscriptionLine(snapshot, freshness, tier, columns, now, timeZone) {
|
|
617
|
+
const entries = orderedSubscriptionLimits(snapshot, now);
|
|
618
|
+
const primary = primarySubscriptionLimits(snapshot, now);
|
|
619
|
+
const agents = orderedSubscriptionAgents(snapshot, now);
|
|
620
|
+
const fullLimits = entries.map(({ agent, limit }) => formatAttributedLimit(agent, limit, now, timeZone));
|
|
621
|
+
const compactLimits = entries.map(({ agent, limit }) => formatAttributedLimitCompact(agent, limit));
|
|
622
|
+
const compactPrimary = primary.map(({ agent, limit }) => formatAttributedLimitCompact(agent, limit));
|
|
623
|
+
const fullValues = agents.flatMap((agent) => formatAttributedValue(agent) ?? []);
|
|
624
|
+
const compactValues = agents.flatMap((agent) => formatAttributedValue(agent, true) ?? []);
|
|
625
|
+
const pressure = agents
|
|
626
|
+
.filter((agent) => agent.pressure === "extra_usage_credits_exhausted")
|
|
627
|
+
.map((agent) => `${subscriptionAgentLabel(agent)} plan pressure`);
|
|
628
|
+
const noRunway = entries.length === 0 ? ["subscription detected", "runway not reported"] : [];
|
|
629
|
+
const urgent = compactPrimary[0];
|
|
630
|
+
const urgentValue = agents[0] ? formatAttributedValue(agents[0], true) : undefined;
|
|
631
|
+
const candidates = [];
|
|
632
|
+
if (tier === "full") {
|
|
633
|
+
candidates.push(["aibill", ...noRunway, ...fullLimits, ...fullValues, ...pressure, freshness]);
|
|
634
|
+
}
|
|
635
|
+
if (tier !== "minimal") {
|
|
636
|
+
candidates.push(["aibill", ...noRunway, ...compactLimits, ...compactValues, ...pressure, freshness], ["aibill", ...noRunway, ...compactLimits, freshness], ["aibill", ...noRunway, ...compactPrimary, ...compactValues, freshness], ["aibill", ...noRunway, ...compactPrimary, freshness]);
|
|
637
|
+
}
|
|
638
|
+
if (urgent) {
|
|
639
|
+
if (urgentValue)
|
|
640
|
+
candidates.push(["aibill", urgent, urgentValue, freshness]);
|
|
641
|
+
candidates.push(["aibill", urgent, freshness], ["aibill", urgent], [urgent]);
|
|
642
|
+
}
|
|
643
|
+
else {
|
|
644
|
+
candidates.push(["aibill", "runway n/r", freshness], ["runway n/r", freshness], ["aibill", ...noRunway, ...compactValues, freshness], ["aibill", ...compactValues, freshness], ["aibill", "subscription detected", freshness], ["subscription detected"]);
|
|
645
|
+
}
|
|
646
|
+
return firstFittingLine(candidates, columns);
|
|
647
|
+
}
|
|
648
|
+
function assembleMultiSubscriptionMixedLine(snapshot, fullSegments, freshness, tier, columns, now, timeZone) {
|
|
649
|
+
const entries = orderedSubscriptionLimits(snapshot, now);
|
|
650
|
+
const primary = primarySubscriptionLimits(snapshot, now);
|
|
651
|
+
const agents = orderedSubscriptionAgents(snapshot, now);
|
|
652
|
+
const compactLimits = entries.map(({ agent, limit }) => formatAttributedLimitCompact(agent, limit));
|
|
653
|
+
const compactPrimary = primary.map(({ agent, limit }) => formatAttributedLimitCompact(agent, limit));
|
|
654
|
+
const compactValues = agents.flatMap((agent) => formatAttributedValue(agent, true) ?? []);
|
|
655
|
+
const noRunway = entries.length === 0 ? ["subscription detected", "runway not reported"] : [];
|
|
656
|
+
const urgent = compactPrimary[0];
|
|
657
|
+
const urgentValue = agents[0] ? formatAttributedValue(agents[0], true) : undefined;
|
|
658
|
+
const meteredBilled = snapshot.metered?.providerBilled.sevenDays;
|
|
659
|
+
const meteredEstimated = snapshot.metered?.apiEquivalent.sevenDays;
|
|
660
|
+
const metered = meteredBilled?.amountUsd !== null && meteredBilled?.amountUsd !== undefined &&
|
|
661
|
+
meteredBilled.financialEvidence === "verified"
|
|
662
|
+
? `metered ${formatBilledUsd(meteredBilled.amountUsd)}/7d billed`
|
|
663
|
+
: meteredEstimated?.amountUsd !== null && meteredEstimated?.amountUsd !== undefined &&
|
|
664
|
+
meteredEstimated.financialEvidence === "estimated"
|
|
665
|
+
? `metered ~${formatUsd(meteredEstimated.amountUsd)}/7d`
|
|
666
|
+
: "metered n/r";
|
|
667
|
+
const shortFreshness = compactFreshness(freshness);
|
|
668
|
+
const candidates = [];
|
|
669
|
+
if (tier === "full")
|
|
670
|
+
candidates.push(["aibill", ...fullSegments, freshness]);
|
|
671
|
+
if (tier !== "minimal") {
|
|
672
|
+
candidates.push(["aibill", "mix", ...noRunway, ...compactLimits, metered, ...compactValues, freshness], ["aibill", "mix", ...noRunway, ...compactPrimary, metered, freshness]);
|
|
673
|
+
}
|
|
674
|
+
if (urgent) {
|
|
675
|
+
if (urgentValue)
|
|
676
|
+
candidates.push(["aibill", "mix", urgent, metered, urgentValue, shortFreshness]);
|
|
677
|
+
candidates.push(["aibill", "mix", urgent, metered, shortFreshness], ["mix", urgent, metered], ["aibill", urgent, shortFreshness], ["aibill", urgent], [urgent]);
|
|
678
|
+
}
|
|
679
|
+
else {
|
|
680
|
+
candidates.push(["aibill", "mix", ...noRunway, metered, freshness], ["aibill", ...noRunway, freshness], ["aibill", "mix", "runway n/r", metered, shortFreshness], ["mix", "runway n/r", metered, shortFreshness], ["mix", "runway n/r", shortFreshness]);
|
|
681
|
+
}
|
|
682
|
+
candidates.push(["mix", metered, shortFreshness], ["mix", metered], ["mix"]);
|
|
683
|
+
return firstFittingLine(candidates, columns);
|
|
684
|
+
}
|
|
489
685
|
function formatLimitCompact(limit) {
|
|
490
686
|
const label = limit.kind === "five-hour" ? "5h" : "wk";
|
|
491
687
|
return `${label} ${formatPercent(limit.remainingPercent)}% left`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ai-spend-agent",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.3",
|
|
4
4
|
"description": "Local-first financial accountability CLI for Claude Code and Codex work, cost evidence, attribution, runway, provenance, and Context Health.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -54,8 +54,8 @@
|
|
|
54
54
|
"prepack": "npm run build"
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
|
-
"@agent-finops/core": "0.7.
|
|
58
|
-
"@agent-finops/report": "0.7.
|
|
57
|
+
"@agent-finops/core": "0.7.3",
|
|
58
|
+
"@agent-finops/report": "0.7.3",
|
|
59
59
|
"yocto-spinner": "^1.2.0"
|
|
60
60
|
}
|
|
61
61
|
}
|