ai-spend-agent 0.8.0 → 0.8.1
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 +356 -78
- package/dist/statuslineRuntime.js +58 -12
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -9,13 +9,33 @@ import { StatuslineInstallerError, installClaudeStatusline, uninstallClaudeStatu
|
|
|
9
9
|
import { readStatuslineCache, renderStatusline } from "./statuslineRuntime.js";
|
|
10
10
|
import { generateActionPlanMarkdown, generateApplyArtifactMarkdown, generateDemoPackageMarkdown, generateHtmlReport, generateMarkdownReport, generatePlainEnglishSummary, generatePolicyConfigDraftMarkdown, generateReportCardCaption, generateReportCardSvg, generateVerificationPlanMarkdown, groupByDimensions } from "@agent-finops/report";
|
|
11
11
|
export async function runCli(argv = process.argv.slice(2), runtime = {}) {
|
|
12
|
-
if (argv.
|
|
12
|
+
if (argv.length === 1 && (argv[0] === "--version" || argv[0] === "-v")) {
|
|
13
13
|
return ok(await cliVersion());
|
|
14
14
|
}
|
|
15
15
|
if (argv.includes("--help") || argv.includes("-h") || argv[0] === "help") {
|
|
16
16
|
return ok(helpText());
|
|
17
17
|
}
|
|
18
18
|
const args = parseArgs(argv);
|
|
19
|
+
if (args.parseErrors.length > 0) {
|
|
20
|
+
return {
|
|
21
|
+
exitCode: 1,
|
|
22
|
+
stdout: "",
|
|
23
|
+
stderr: [
|
|
24
|
+
...args.parseErrors.map((error) => `Invalid arguments: ${sanitizeSecretishError(error)}`),
|
|
25
|
+
"Run `npx aibill --help` to see supported commands and flags."
|
|
26
|
+
].join("\n")
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
if (args.json && args.command !== "context" && args.command !== "context-health" && args.command !== "glance") {
|
|
30
|
+
return {
|
|
31
|
+
exitCode: 1,
|
|
32
|
+
stdout: "",
|
|
33
|
+
stderr: [
|
|
34
|
+
"--json is not available for the main receipt yet; no text receipt was substituted.",
|
|
35
|
+
"Use `npx aibill context --json` for the canonical Context Health object or `npx aibill glance` for the canonical Glance snapshot."
|
|
36
|
+
].join("\n")
|
|
37
|
+
};
|
|
38
|
+
}
|
|
19
39
|
if (args.groupByInvalid) {
|
|
20
40
|
return {
|
|
21
41
|
exitCode: 1,
|
|
@@ -23,10 +43,8 @@ export async function runCli(argv = process.argv.slice(2), runtime = {}) {
|
|
|
23
43
|
stderr: `--group-by needs a dimension: ${groupByDimensions.join("|")}\nexample: npx aibill --group-by project`
|
|
24
44
|
};
|
|
25
45
|
}
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
// `--group-by agent`, lands the wow immediately on sample / auto-detected
|
|
29
|
-
// local data — no credential required.
|
|
46
|
+
// Running with no subcommand reads only evidence available on this machine.
|
|
47
|
+
// Illustrative records are reachable only through an explicit --sample.
|
|
30
48
|
if (!args.command || args.command.startsWith("--") || args.command === "quickstart" || args.command === "demo") {
|
|
31
49
|
return quickstartCommand(args);
|
|
32
50
|
}
|
|
@@ -84,14 +102,24 @@ export async function runCli(argv = process.argv.slice(2), runtime = {}) {
|
|
|
84
102
|
return {
|
|
85
103
|
exitCode: 1,
|
|
86
104
|
stdout: "",
|
|
87
|
-
stderr: `Unknown command: ${args.command}\n${helpText()}`
|
|
105
|
+
stderr: `Unknown command: ${sanitizeSecretishError(args.command)}\n${helpText()}`
|
|
88
106
|
};
|
|
89
107
|
}
|
|
90
108
|
async function quickstartCommand(args) {
|
|
91
109
|
const sinceDays = args.sinceDays ?? 30;
|
|
92
110
|
if (!validSinceDays(sinceDays))
|
|
93
111
|
return invalidSinceDaysResult();
|
|
112
|
+
if (args.plan && !planOverrideFromFlag(args.plan)) {
|
|
113
|
+
return {
|
|
114
|
+
exitCode: 1,
|
|
115
|
+
stdout: "",
|
|
116
|
+
stderr: `Unknown --plan "${sanitizeSecretishError(args.plan)}". Valid plans: ${subscriptionPlans.map((plan) => plan.id).join(", ")}`
|
|
117
|
+
};
|
|
118
|
+
}
|
|
94
119
|
const { records, mode, warnings, providerCoverage, codexInvocationFiles } = await loadInstantReadData(args);
|
|
120
|
+
if (records.length === 0) {
|
|
121
|
+
return noEvidenceResult("receipt", warnings, sinceDays);
|
|
122
|
+
}
|
|
95
123
|
const summaryRecords = mode === "connected"
|
|
96
124
|
? selectProviderFinancialHeadlineRecords(records)
|
|
97
125
|
: records;
|
|
@@ -100,6 +128,7 @@ async function quickstartCommand(args) {
|
|
|
100
128
|
// ("which project burns my plan"); demo/connected keep by-model.
|
|
101
129
|
const groupBy = args.groupBy ?? (mode === "local-logs" ? "project" : "model");
|
|
102
130
|
const color = args.noColor ? false : undefined;
|
|
131
|
+
const outputWidth = terminalOutputWidth();
|
|
103
132
|
// Persona: --plan override wins; otherwise read the plans the coding agents
|
|
104
133
|
// themselves persisted locally (read-only, whitelisted fields, no network).
|
|
105
134
|
let detectedPlans;
|
|
@@ -114,7 +143,7 @@ async function quickstartCommand(args) {
|
|
|
114
143
|
return {
|
|
115
144
|
exitCode: 1,
|
|
116
145
|
stdout: "",
|
|
117
|
-
stderr: `Unknown --plan "${args.plan}". Valid plans: ${subscriptionPlans.map((plan) => plan.id).join(", ")}`
|
|
146
|
+
stderr: `Unknown --plan "${sanitizeSecretishError(args.plan)}". Valid plans: ${subscriptionPlans.map((plan) => plan.id).join(", ")}`
|
|
118
147
|
};
|
|
119
148
|
}
|
|
120
149
|
detectedPlans = [override];
|
|
@@ -173,12 +202,17 @@ async function quickstartCommand(args) {
|
|
|
173
202
|
nextSteps,
|
|
174
203
|
deadContext,
|
|
175
204
|
detectedPlans,
|
|
205
|
+
width: outputWidth,
|
|
176
206
|
// An explicit --group-by is a drill-down question: answer with just the
|
|
177
207
|
// table + window instead of repeating the whole readout.
|
|
178
|
-
view: args.groupBy ? "breakdown" : "full"
|
|
208
|
+
view: args.groupBy ? "breakdown" : args.full ? "full" : "compact"
|
|
179
209
|
});
|
|
180
|
-
const
|
|
181
|
-
|
|
210
|
+
const detailedView = Boolean(args.groupBy || args.full);
|
|
211
|
+
const header = [
|
|
212
|
+
...(detailedView ? wrapCliHeader(dataModeBanner(mode, summaryRecords), " ", outputWidth) : []),
|
|
213
|
+
...warnings.flatMap((warning) => wrapCliHeader(warning, " ! ", outputWidth))
|
|
214
|
+
].join("\n");
|
|
215
|
+
return ok(header ? `${header}\n${summaryText}` : summaryText);
|
|
182
216
|
}
|
|
183
217
|
async function glanceCommand(args) {
|
|
184
218
|
const sinceDays = args.sinceDays ?? 30;
|
|
@@ -206,7 +240,7 @@ async function glanceCommand(args) {
|
|
|
206
240
|
return {
|
|
207
241
|
exitCode: 1,
|
|
208
242
|
stdout: "",
|
|
209
|
-
stderr: `Unknown --plan "${args.plan}". Valid plans: ${subscriptionPlans.map((plan) => plan.id).join(", ")}`
|
|
243
|
+
stderr: `Unknown --plan "${sanitizeSecretishError(args.plan)}". Valid plans: ${subscriptionPlans.map((plan) => plan.id).join(", ")}`
|
|
210
244
|
};
|
|
211
245
|
}
|
|
212
246
|
detectedPlans = [override];
|
|
@@ -326,7 +360,7 @@ function quickstartNextSteps(mode, detected) {
|
|
|
326
360
|
if (detected.length > 0) {
|
|
327
361
|
const names = detected.map((credential) => `${credential.provider} (${credential.hint})`).join(", ");
|
|
328
362
|
steps.push(`Found local key${detected.length === 1 ? "" : "s"}: ${names}`);
|
|
329
|
-
steps.push(`npx aibill connect ${detected[0].provider}
|
|
363
|
+
steps.push(`npx aibill connect ${detected[0].provider} set up the admin connector, then sync provider-reported cost`);
|
|
330
364
|
}
|
|
331
365
|
steps.push(mode === "demo"
|
|
332
366
|
? "npx aibill report --sample write a clearly labeled demo Markdown + HTML report"
|
|
@@ -429,14 +463,22 @@ async function loadInstantReadData(args) {
|
|
|
429
463
|
warnings.push(`${persisted.connectedTrust.message} CLI: run \`npx aibill connect <provider>\` or repeat the prior \`npx aibill sync-provider ...\` command. The repository-provided connected totals were ignored.`);
|
|
430
464
|
}
|
|
431
465
|
// Real local agent logs (Claude Code / Codex) beat any sample/legacy state.
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
466
|
+
let logs;
|
|
467
|
+
try {
|
|
468
|
+
logs = await loadLocalAgentUsage({
|
|
469
|
+
// Env overrides keep tests (and unusual installs) isolated from $HOME.
|
|
470
|
+
claudeProjectsDir: process.env.AI_SPEND_CLAUDE_LOGS_DIR,
|
|
471
|
+
codexSessionsDir: process.env.AI_SPEND_CODEX_LOGS_DIR,
|
|
472
|
+
geminiSessionsDir: process.env.AI_SPEND_GEMINI_LOGS_DIR,
|
|
473
|
+
sinceIso: sinceIsoForDays(args.sinceDays ?? 30),
|
|
474
|
+
collectCodexInvocationEvidence: true
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
catch {
|
|
478
|
+
// A failed read is not the same as an honest empty scan. Keep the compact
|
|
479
|
+
// result useful without leaking a local path or raw parser exception.
|
|
480
|
+
warnings.push("Some local agent evidence could not be read; coverage is incomplete.");
|
|
481
|
+
}
|
|
440
482
|
if (logs && logs.records.length > 0) {
|
|
441
483
|
// Persisted local_logs state (written by report/apply-artifact) is the
|
|
442
484
|
// same data source we just re-read — superseding it silently is correct,
|
|
@@ -461,20 +503,81 @@ async function loadInstantReadData(args) {
|
|
|
461
503
|
codexInvocationFiles: logs?.codexInvocationFiles
|
|
462
504
|
};
|
|
463
505
|
}
|
|
464
|
-
// No real logs.
|
|
465
|
-
//
|
|
506
|
+
// No real logs. Sample and legacy persisted state must never appear unless
|
|
507
|
+
// this invocation explicitly opted into --sample.
|
|
466
508
|
if (persisted && persisted.records.length > 0 && persisted.mode !== "connected_provider" && persisted.mode !== "local_logs") {
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
return { records: persisted.records, mode: "demo", warnings };
|
|
509
|
+
warnings.push(persisted.mode === "sample"
|
|
510
|
+
? "Sample state exists but was not displayed because this run did not include --sample."
|
|
511
|
+
: "Legacy state with no trustworthy data-mode label was ignored. Run `npx aibill reset`, then collect fresh evidence.");
|
|
471
512
|
}
|
|
472
513
|
if (persisted?.mode === "local_logs") {
|
|
473
514
|
warnings.push("Ignored persisted local-log cache because no current Claude Code/Codex source records were found. Re-run the local agent activity first; repository state alone cannot authorize an Apply action.");
|
|
474
515
|
}
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
516
|
+
return { records: [], mode: "local-logs", warnings };
|
|
517
|
+
}
|
|
518
|
+
function noEvidenceResult(surface, warnings, sinceDays) {
|
|
519
|
+
const surfaceLine = surface === "watch"
|
|
520
|
+
? "Watch has no financial baseline yet; no zero total or sample activity was recorded."
|
|
521
|
+
: surface === "report-card"
|
|
522
|
+
? "No receipt was written because there is no supported financial evidence to summarize."
|
|
523
|
+
: `No supported AI usage evidence was found in the last ${sinceDays} days.`;
|
|
524
|
+
return {
|
|
525
|
+
exitCode: surface === "receipt" ? 0 : 1,
|
|
526
|
+
stdout: surface === "receipt"
|
|
527
|
+
? [
|
|
528
|
+
"aibill · local only",
|
|
529
|
+
"",
|
|
530
|
+
surfaceLine,
|
|
531
|
+
"Looked for: Claude Code, Codex, and Gemini CLI local history.",
|
|
532
|
+
"Nothing was uploaded. No sample data was substituted.",
|
|
533
|
+
...warnings.map((warning) => `! ${warning}`),
|
|
534
|
+
"",
|
|
535
|
+
"Next",
|
|
536
|
+
" npx aibill doctor --sources see the exact evidence gap and setup paths"
|
|
537
|
+
].join("\n")
|
|
538
|
+
: "",
|
|
539
|
+
stderr: surface === "receipt"
|
|
540
|
+
? ""
|
|
541
|
+
: [
|
|
542
|
+
surfaceLine,
|
|
543
|
+
"Run `npx aibill doctor --sources` to see the exact evidence gap.",
|
|
544
|
+
"Local evidence: use Claude Code, Codex, or Gemini CLI normally, then retry.",
|
|
545
|
+
"Provider billing: connect openai, anthropic, cursor, or github-copilot with an admin credential reference.",
|
|
546
|
+
"For illustrative output only, rerun this command with --sample."
|
|
547
|
+
].join("\n")
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
function terminalOutputWidth() {
|
|
551
|
+
const envColumns = Number(process.env.COLUMNS);
|
|
552
|
+
const ttyColumns = process.stdout.columns;
|
|
553
|
+
const requested = Number.isFinite(envColumns) && envColumns > 0
|
|
554
|
+
? envColumns
|
|
555
|
+
: Number.isFinite(ttyColumns) && (ttyColumns ?? 0) > 0
|
|
556
|
+
? ttyColumns
|
|
557
|
+
: 72;
|
|
558
|
+
return Math.max(40, Math.min(120, Math.floor(requested)));
|
|
559
|
+
}
|
|
560
|
+
function wrapCliHeader(text, prefix, width) {
|
|
561
|
+
const available = Math.max(12, width - prefix.length);
|
|
562
|
+
const words = text.trim().split(/\s+/u);
|
|
563
|
+
const lines = [];
|
|
564
|
+
let current = "";
|
|
565
|
+
for (const word of words) {
|
|
566
|
+
if (!current) {
|
|
567
|
+
current = word;
|
|
568
|
+
continue;
|
|
569
|
+
}
|
|
570
|
+
if (`${current} ${word}`.length <= available) {
|
|
571
|
+
current += ` ${word}`;
|
|
572
|
+
continue;
|
|
573
|
+
}
|
|
574
|
+
lines.push(current);
|
|
575
|
+
current = word;
|
|
576
|
+
}
|
|
577
|
+
if (current)
|
|
578
|
+
lines.push(current);
|
|
579
|
+
const continuationPrefix = " ".repeat(prefix.length);
|
|
580
|
+
return lines.map((line, index) => `${index === 0 ? prefix : continuationPrefix}${line}`);
|
|
478
581
|
}
|
|
479
582
|
/** A one-line, unmissable banner telling the user which data they're seeing. */
|
|
480
583
|
function dataModeBanner(mode, records) {
|
|
@@ -538,16 +641,16 @@ async function doctorCommand(args) {
|
|
|
538
641
|
warnings.push("Gemini CLI detected, but no supported chats JSON/JSONL financial evidence was found. No financial rows were created; logs.json is presence-only evidence. Need this coverage? +1 or contribute a synthetic fixture: https://github.com/futurastudio/ai-spend-agent/issues/new?template=provider_or_agent.yml");
|
|
539
642
|
}
|
|
540
643
|
if (!hasLocalSource)
|
|
541
|
-
warnings.push("no supported Claude Code, Codex, or Gemini CLI session evidence found —
|
|
644
|
+
warnings.push("no supported Claude Code, Codex, or Gemini CLI session evidence found — the default receipt will remain empty; use --sample only for the labeled demo");
|
|
542
645
|
if (providerRefs.length === 0)
|
|
543
|
-
warnings.push("no provider admin keys detected —
|
|
646
|
+
warnings.push("no provider admin keys detected — set up and sync an OpenAI/Anthropic admin connector for provider-reported cost (local logs stay API-equivalent estimates)");
|
|
544
647
|
const predictedMode = connectedStateTrusted
|
|
545
648
|
? "connected provider billing"
|
|
546
649
|
: hasFinancialLogs
|
|
547
650
|
? "your local agent logs (estimated at API-equivalent rates)"
|
|
548
651
|
: geminiPresenceFound
|
|
549
652
|
? "local Gemini CLI presence only (financial evidence unavailable; no sample substituted)"
|
|
550
|
-
: "
|
|
653
|
+
: "no supported evidence yet (no sample substituted)";
|
|
551
654
|
const lines = [
|
|
552
655
|
"aibill doctor",
|
|
553
656
|
`node version: ${process.version}`,
|
|
@@ -1029,7 +1132,7 @@ async function resetCommand(args) {
|
|
|
1029
1132
|
"aibill reset",
|
|
1030
1133
|
`path: ${rootPath}`,
|
|
1031
1134
|
"nothing to clear (no persisted spend state found)",
|
|
1032
|
-
"next run will re-read your real local agent logs
|
|
1135
|
+
"next run will re-read your real local agent logs; no sample is substituted without --sample."
|
|
1033
1136
|
].join("\n"));
|
|
1034
1137
|
}
|
|
1035
1138
|
// Clear derived spend state so a prior `scan --sample` (or stale provider
|
|
@@ -1049,7 +1152,7 @@ async function resetCommand(args) {
|
|
|
1049
1152
|
"aibill reset",
|
|
1050
1153
|
`path: ${rootPath}`,
|
|
1051
1154
|
removed.length > 0 ? `cleared: ${removed.join(", ")}` : "nothing to clear (no persisted spend state found)",
|
|
1052
|
-
"next run will re-read your real local agent logs
|
|
1155
|
+
"next run will re-read your real local agent logs; no sample is substituted without --sample."
|
|
1053
1156
|
].join("\n"));
|
|
1054
1157
|
}
|
|
1055
1158
|
async function statuslineCommand(args, runtime) {
|
|
@@ -1165,7 +1268,7 @@ async function initCommand(args, runtime = {}) {
|
|
|
1165
1268
|
return {
|
|
1166
1269
|
exitCode: 1,
|
|
1167
1270
|
stdout: "",
|
|
1168
|
-
stderr: `Unknown --plan "${args.plan}". Valid plans: ${subscriptionPlans.map((plan) => plan.id).join(", ")}`
|
|
1271
|
+
stderr: `Unknown --plan "${sanitizeSecretishError(args.plan)}". Valid plans: ${subscriptionPlans.map((plan) => plan.id).join(", ")}`
|
|
1169
1272
|
};
|
|
1170
1273
|
}
|
|
1171
1274
|
detectedPlanOverride = [override];
|
|
@@ -1348,7 +1451,7 @@ async function refreshStatuslineCommand(args, runtime) {
|
|
|
1348
1451
|
return {
|
|
1349
1452
|
exitCode: 1,
|
|
1350
1453
|
stdout: "",
|
|
1351
|
-
stderr: `Unknown --plan "${args.plan}". Valid plans: ${subscriptionPlans.map((plan) => plan.id).join(", ")}`
|
|
1454
|
+
stderr: `Unknown --plan "${sanitizeSecretishError(args.plan)}". Valid plans: ${subscriptionPlans.map((plan) => plan.id).join(", ")}`
|
|
1352
1455
|
};
|
|
1353
1456
|
}
|
|
1354
1457
|
detectedPlanOverride = [override];
|
|
@@ -1706,7 +1809,7 @@ function initApiEquivalentWindowLines(snapshot) {
|
|
|
1706
1809
|
agent.unsupportedUsageSnapshots === 0 &&
|
|
1707
1810
|
agent.jsonlValidationCoverage === "complete");
|
|
1708
1811
|
return [completeZero
|
|
1709
|
-
? "API-equivalent value:
|
|
1812
|
+
? "API-equivalent usage value: unavailable — no priced evidence was observed in readable local sources"
|
|
1710
1813
|
: "API-equivalent usage value: unavailable — local source coverage is incomplete; no zero total was inferred"];
|
|
1711
1814
|
}
|
|
1712
1815
|
if (snapshot.mode === "unresolved" && snapshot.unresolved) {
|
|
@@ -1826,7 +1929,7 @@ async function scanCommand(args) {
|
|
|
1826
1929
|
const mappings = attributeUsageRecords(records);
|
|
1827
1930
|
await writeLocalSpendState(stateDir, records, summary, mappings, "sample");
|
|
1828
1931
|
lines.push(`sample records: ${records.length}`);
|
|
1829
|
-
lines.push(`total spend:
|
|
1932
|
+
lines.push(`total spend: ${formatOptionalUsd(summary.totalUsd)}`);
|
|
1830
1933
|
lines.push(`attribution mappings: ${mappings.length}`);
|
|
1831
1934
|
}
|
|
1832
1935
|
if (discovery.signals.length > 0) {
|
|
@@ -1858,6 +1961,9 @@ async function watchCommand(args) {
|
|
|
1858
1961
|
while (unbounded || iteration < cycles) {
|
|
1859
1962
|
const previous = await readOptionalJson(join(stateDir, "watch-latest.json"), null);
|
|
1860
1963
|
const { summary, snapshot, records, mode } = await runWatchCycle(stateDir, args);
|
|
1964
|
+
if (records.length === 0) {
|
|
1965
|
+
return noEvidenceResult("watch", [], args.sinceDays ?? 30);
|
|
1966
|
+
}
|
|
1861
1967
|
const deltaHeadline = buildDeltaHeadline(previous, snapshot);
|
|
1862
1968
|
// Watch's job is DELTAS: render the compact breakdown view per cycle, not
|
|
1863
1969
|
// the whole diagnose→verify readout again (the quickstart owns that).
|
|
@@ -1866,6 +1972,7 @@ async function watchCommand(args) {
|
|
|
1866
1972
|
groupBy: args.groupBy ?? "model",
|
|
1867
1973
|
color: args.noColor ? false : undefined,
|
|
1868
1974
|
mode: mode === "sample" ? "demo" : mode === "connected_provider" ? "connected" : "local-logs",
|
|
1975
|
+
width: terminalOutputWidth(),
|
|
1869
1976
|
view: "breakdown"
|
|
1870
1977
|
});
|
|
1871
1978
|
const stamped = [
|
|
@@ -1929,8 +2036,8 @@ async function runWatchCycle(stateDir, args) {
|
|
|
1929
2036
|
mode = "local_logs";
|
|
1930
2037
|
}
|
|
1931
2038
|
else {
|
|
1932
|
-
records =
|
|
1933
|
-
mode = "
|
|
2039
|
+
records = [];
|
|
2040
|
+
mode = "local_logs";
|
|
1934
2041
|
}
|
|
1935
2042
|
}
|
|
1936
2043
|
}
|
|
@@ -1961,7 +2068,7 @@ async function runWatchCycle(stateDir, args) {
|
|
|
1961
2068
|
sourceId: "watch",
|
|
1962
2069
|
detail: snapshot.totalUsd === null
|
|
1963
2070
|
? `Watch cycle captured ${snapshot.recordCount} records with no priced financial evidence; total unavailable.`
|
|
1964
|
-
: `Watch cycle captured ${snapshot.recordCount} records totaling
|
|
2071
|
+
: `Watch cycle captured ${snapshot.recordCount} records totaling ${formatOptionalUsd(snapshot.totalUsd)}.`
|
|
1965
2072
|
});
|
|
1966
2073
|
return { summary, snapshot, records: headlineRecords, mode };
|
|
1967
2074
|
}
|
|
@@ -1970,24 +2077,24 @@ function buildDeltaHeadline(previous, current) {
|
|
|
1970
2077
|
if (current.totalUsd === null) {
|
|
1971
2078
|
return `First watch snapshot. Financial baseline is unavailable across ${current.recordCount} records; missing/null is not zero. Future priced cycles will establish a numeric baseline.`;
|
|
1972
2079
|
}
|
|
1973
|
-
return `First watch snapshot. Baseline AI spend is
|
|
2080
|
+
return `First watch snapshot. Baseline AI spend is ${formatOptionalUsd(current.totalUsd)} across ${current.recordCount} charges. Future cycles will report what changed.`;
|
|
1974
2081
|
}
|
|
1975
2082
|
if (current.totalUsd === null) {
|
|
1976
2083
|
return `Financial evidence is unavailable across ${current.recordCount} records; missing/null is not zero and no numeric delta was calculated.`;
|
|
1977
2084
|
}
|
|
1978
2085
|
if (previous.totalUsd === null) {
|
|
1979
|
-
return `A priced financial baseline is now available at
|
|
2086
|
+
return `A priced financial baseline is now available at ${formatOptionalUsd(current.totalUsd)} across ${current.recordCount} records. The prior snapshot was unavailable, so no numeric delta was calculated.`;
|
|
1980
2087
|
}
|
|
1981
2088
|
const deltaUsd = roundMoneyCli(current.totalUsd - previous.totalUsd);
|
|
1982
2089
|
const lines = [];
|
|
1983
|
-
if (
|
|
1984
|
-
lines.push(`No change since the last check: AI spend is holding at
|
|
2090
|
+
if (deltaUsd === 0) {
|
|
2091
|
+
lines.push(`No change since the last check: AI spend is holding at ${formatOptionalUsd(current.totalUsd)}.`);
|
|
1985
2092
|
}
|
|
1986
2093
|
else {
|
|
1987
2094
|
const direction = deltaUsd > 0 ? "UP" : "DOWN";
|
|
1988
2095
|
const percent = previous.totalUsd > 0 ? Math.round((deltaUsd / previous.totalUsd) * 100) : 100;
|
|
1989
|
-
lines.push(`Spend is ${direction}
|
|
1990
|
-
`from
|
|
2096
|
+
lines.push(`Spend is ${direction} ${formatOptionalUsd(Math.abs(deltaUsd))} (${Math.abs(percent)}%) since the last check — ` +
|
|
2097
|
+
`from ${formatOptionalUsd(previous.totalUsd)} to ${formatOptionalUsd(current.totalUsd)}.`);
|
|
1991
2098
|
}
|
|
1992
2099
|
// New-model and per-model spike detection versus the previous snapshot.
|
|
1993
2100
|
const previousModels = new Map(previous.byModel.map((entry) => [entry.key, entry.amountUsd]));
|
|
@@ -1996,12 +2103,12 @@ function buildDeltaHeadline(previous, current) {
|
|
|
1996
2103
|
const before = previousModels.get(entry.key);
|
|
1997
2104
|
if (before === undefined) {
|
|
1998
2105
|
if (entry.amountUsd >= 1) {
|
|
1999
|
-
anomalies.push(`New model "${entry.key}" appeared, already at
|
|
2106
|
+
anomalies.push(`New model "${entry.key}" appeared, already at ${formatOptionalUsd(entry.amountUsd)}.`);
|
|
2000
2107
|
}
|
|
2001
2108
|
continue;
|
|
2002
2109
|
}
|
|
2003
2110
|
if (before > 0 && entry.amountUsd - before >= 5 && entry.amountUsd / before >= 1.5) {
|
|
2004
|
-
anomalies.push(`"${entry.key}" jumped from
|
|
2111
|
+
anomalies.push(`"${entry.key}" jumped from ${formatOptionalUsd(before)} to ${formatOptionalUsd(entry.amountUsd)}.`);
|
|
2005
2112
|
}
|
|
2006
2113
|
}
|
|
2007
2114
|
if (anomalies.length > 0) {
|
|
@@ -2013,7 +2120,7 @@ function sleep(milliseconds) {
|
|
|
2013
2120
|
return new Promise((resolveSleep) => setTimeout(resolveSleep, milliseconds));
|
|
2014
2121
|
}
|
|
2015
2122
|
function roundMoneyCli(value) {
|
|
2016
|
-
return Math.round(value *
|
|
2123
|
+
return Math.round(value * 10_000) / 10_000;
|
|
2017
2124
|
}
|
|
2018
2125
|
async function addSourceCommand(args) {
|
|
2019
2126
|
const rootPath = resolve(args.path);
|
|
@@ -2074,6 +2181,10 @@ const adminUpgradeProviders = {
|
|
|
2074
2181
|
"github-copilot": "requires a GitHub BILLING-ADMIN token (org/enterprise)",
|
|
2075
2182
|
copilot: "requires a GitHub BILLING-ADMIN token (org/enterprise)"
|
|
2076
2183
|
};
|
|
2184
|
+
const supportedAdminProviders = new Set(["openai", "anthropic", "cursor", "github-copilot"]);
|
|
2185
|
+
const providerAliases = {
|
|
2186
|
+
copilot: "github-copilot"
|
|
2187
|
+
};
|
|
2077
2188
|
const providerAdminEnvHint = {
|
|
2078
2189
|
openai: "env:OPENAI_ADMIN_KEY",
|
|
2079
2190
|
anthropic: "env:ANTHROPIC_ADMIN_KEY",
|
|
@@ -2081,10 +2192,20 @@ const providerAdminEnvHint = {
|
|
|
2081
2192
|
"github-copilot": "env:GITHUB_TOKEN",
|
|
2082
2193
|
copilot: "env:GITHUB_TOKEN"
|
|
2083
2194
|
};
|
|
2195
|
+
function providerSyncSetupCommand(provider, adminRef) {
|
|
2196
|
+
if (provider === "cursor") {
|
|
2197
|
+
return `npx aibill sync-provider --provider cursor --auth-reference ${adminRef} --account-id <team-label>`;
|
|
2198
|
+
}
|
|
2199
|
+
if (provider === "github-copilot") {
|
|
2200
|
+
return `npx aibill sync-provider --provider github-copilot --auth-reference ${adminRef} --org <organization>`;
|
|
2201
|
+
}
|
|
2202
|
+
return `npx aibill sync-provider --provider ${provider} --auth-reference ${adminRef} --start-time <unix>`;
|
|
2203
|
+
}
|
|
2084
2204
|
async function connectCommand(args) {
|
|
2085
2205
|
const rootPath = resolve(args.path);
|
|
2086
2206
|
const stateDir = join(rootPath, ".ai-spend-agent");
|
|
2087
|
-
const
|
|
2207
|
+
const requestedProvider = (args.provider ?? "unknown").trim().toLowerCase();
|
|
2208
|
+
const provider = providerAliases[requestedProvider] ?? requestedProvider;
|
|
2088
2209
|
if (!provider || provider === "unknown") {
|
|
2089
2210
|
return {
|
|
2090
2211
|
exitCode: 1,
|
|
@@ -2100,6 +2221,20 @@ async function connectCommand(args) {
|
|
|
2100
2221
|
};
|
|
2101
2222
|
}
|
|
2102
2223
|
const type = args.sourceType ?? "provider_api";
|
|
2224
|
+
if (!supportedAdminProviders.has(provider) || type !== "provider_api") {
|
|
2225
|
+
const reason = !supportedAdminProviders.has(provider)
|
|
2226
|
+
? `connect does not implement provider "${sanitizeSecretishError(requestedProvider)}".`
|
|
2227
|
+
: `connect supports provider_api only; received "${sanitizeSecretishError(type)}".`;
|
|
2228
|
+
return {
|
|
2229
|
+
exitCode: 1,
|
|
2230
|
+
stdout: "",
|
|
2231
|
+
stderr: [
|
|
2232
|
+
reason,
|
|
2233
|
+
"Supported admin connectors: openai, anthropic, cursor, github-copilot.",
|
|
2234
|
+
"For an approved file/export boundary, use `npx aibill add-source` instead."
|
|
2235
|
+
].join("\n")
|
|
2236
|
+
};
|
|
2237
|
+
}
|
|
2103
2238
|
const registry = await readSourceRegistry(stateDir, rootPath);
|
|
2104
2239
|
const source = createProviderConnectorStub(provider, type);
|
|
2105
2240
|
const nextRegistry = addApprovedSource(registry, source);
|
|
@@ -2126,7 +2261,7 @@ async function connectCommand(args) {
|
|
|
2126
2261
|
`boundary approval: ${source.boundaryApproval}`,
|
|
2127
2262
|
`validation coverage: ${source.validationCoverage}`,
|
|
2128
2263
|
`financial evidence: ${source.financialEvidence}`,
|
|
2129
|
-
|
|
2264
|
+
`secrets: no raw secrets stored; we only reference a local env var such as ${providerAdminEnvHint[provider] ?? "env:YOUR_ADMIN_KEY"}`
|
|
2130
2265
|
];
|
|
2131
2266
|
if (selfServeProviders.has(provider)) {
|
|
2132
2267
|
lines.push("tier: self-serve — an org owner can enable this in ~2 minutes");
|
|
@@ -2140,19 +2275,19 @@ async function connectCommand(args) {
|
|
|
2140
2275
|
lines.push(`auto-detected: a ${provider} key in ${detected.reference} (${detected.hint}) from ${describeOrigin(detected)}`);
|
|
2141
2276
|
if (detected.isLikelyAdminKey) {
|
|
2142
2277
|
const adminRef = providerAdminEnvHint[provider] ?? detected.reference;
|
|
2143
|
-
lines.push(`next:
|
|
2278
|
+
lines.push(`next: ${providerSyncSetupCommand(provider, adminRef)}`);
|
|
2144
2279
|
}
|
|
2145
2280
|
else {
|
|
2146
2281
|
const adminRef = providerAdminEnvHint[provider] ?? "env:YOUR_ADMIN_KEY";
|
|
2147
2282
|
lines.push(`this looks like a regular key — for COST data set an admin key in ${adminRef}, then:`);
|
|
2148
|
-
lines.push(`
|
|
2283
|
+
lines.push(` ${providerSyncSetupCommand(provider, adminRef)}`);
|
|
2149
2284
|
}
|
|
2150
2285
|
}
|
|
2151
2286
|
else {
|
|
2152
2287
|
const adminRef = providerAdminEnvHint[provider] ?? "env:YOUR_ADMIN_KEY";
|
|
2153
2288
|
lines.push("");
|
|
2154
2289
|
lines.push(`next: export an admin key reference, e.g. ${adminRef}, then run:`);
|
|
2155
|
-
lines.push(`
|
|
2290
|
+
lines.push(` ${providerSyncSetupCommand(provider, adminRef)}`);
|
|
2156
2291
|
}
|
|
2157
2292
|
lines.push(`missing: ${source.fieldsMissing.join(", ")}`);
|
|
2158
2293
|
return ok(lines.join("\n"));
|
|
@@ -2181,17 +2316,62 @@ function describeOrigin(credential) {
|
|
|
2181
2316
|
async function syncProviderCommand(args) {
|
|
2182
2317
|
const rootPath = resolve(args.path);
|
|
2183
2318
|
const stateDir = join(rootPath, ".ai-spend-agent");
|
|
2184
|
-
|
|
2319
|
+
const requestedProvider = (args.provider ?? "").trim().toLowerCase();
|
|
2320
|
+
const provider = providerAliases[requestedProvider] ?? requestedProvider;
|
|
2321
|
+
if (!provider) {
|
|
2322
|
+
return {
|
|
2323
|
+
exitCode: 1,
|
|
2324
|
+
stdout: "",
|
|
2325
|
+
stderr: [
|
|
2326
|
+
"sync-provider requires --provider.",
|
|
2327
|
+
"Supported admin connectors: openai, anthropic, cursor, github-copilot."
|
|
2328
|
+
].join("\n")
|
|
2329
|
+
};
|
|
2330
|
+
}
|
|
2331
|
+
if (!supportedAdminProviders.has(provider)) {
|
|
2185
2332
|
return {
|
|
2186
2333
|
exitCode: 1,
|
|
2187
2334
|
stdout: "",
|
|
2188
2335
|
stderr: [
|
|
2189
|
-
|
|
2190
|
-
"
|
|
2191
|
-
" npx aibill sync-provider --provider anthropic --auth-reference env:ANTHROPIC_ADMIN_KEY --start-time 1750000000"
|
|
2336
|
+
`sync-provider does not implement provider "${sanitizeSecretishError(requestedProvider)}".`,
|
|
2337
|
+
"Supported admin connectors: openai, anthropic, cursor, github-copilot."
|
|
2192
2338
|
].join("\n")
|
|
2193
2339
|
};
|
|
2194
2340
|
}
|
|
2341
|
+
if (!args.authReference) {
|
|
2342
|
+
return {
|
|
2343
|
+
exitCode: 1,
|
|
2344
|
+
stdout: "",
|
|
2345
|
+
stderr: `sync-provider ${provider} requires --auth-reference env:NAME; raw secrets are not accepted.`
|
|
2346
|
+
};
|
|
2347
|
+
}
|
|
2348
|
+
const usesRequestedTimeBounds = provider === "openai" || provider === "anthropic";
|
|
2349
|
+
if (usesRequestedTimeBounds && args.startTime === undefined) {
|
|
2350
|
+
return {
|
|
2351
|
+
exitCode: 1,
|
|
2352
|
+
stdout: "",
|
|
2353
|
+
stderr: [
|
|
2354
|
+
`sync-provider ${provider} requires --start-time <unix seconds>; --end-time is optional.`,
|
|
2355
|
+
`example: npx aibill sync-provider --provider ${provider} --auth-reference ${providerAdminEnvHint[provider]} --start-time 1750000000`
|
|
2356
|
+
].join("\n")
|
|
2357
|
+
};
|
|
2358
|
+
}
|
|
2359
|
+
if (!usesRequestedTimeBounds && (args.startTime !== undefined || args.endTime !== undefined)) {
|
|
2360
|
+
return {
|
|
2361
|
+
exitCode: 1,
|
|
2362
|
+
stdout: "",
|
|
2363
|
+
stderr: provider === "cursor"
|
|
2364
|
+
? "Cursor Admin spend is returned for the provider's current team subscription cycle; --start-time/--end-time are not accepted because the connector cannot enforce them."
|
|
2365
|
+
: "GitHub Copilot returns its latest metrics-report window plus current seat data; --start-time/--end-time are not accepted because the connector cannot enforce them."
|
|
2366
|
+
};
|
|
2367
|
+
}
|
|
2368
|
+
if (provider === "github-copilot" && Boolean(args.org) === Boolean(args.enterprise)) {
|
|
2369
|
+
return {
|
|
2370
|
+
exitCode: 1,
|
|
2371
|
+
stdout: "",
|
|
2372
|
+
stderr: "GitHub Copilot sync requires exactly one of --org <organization> or --enterprise <enterprise>."
|
|
2373
|
+
};
|
|
2374
|
+
}
|
|
2195
2375
|
try {
|
|
2196
2376
|
// A provider sync may merge only a prior connected snapshot whose exact
|
|
2197
2377
|
// repository bytes have a matching external machine receipt. A cloned
|
|
@@ -2203,10 +2383,13 @@ async function syncProviderCommand(args) {
|
|
|
2203
2383
|
? priorSpend
|
|
2204
2384
|
: undefined;
|
|
2205
2385
|
const result = await fetchProviderUsageRecords({
|
|
2206
|
-
provider
|
|
2207
|
-
sourceId: `${
|
|
2386
|
+
provider,
|
|
2387
|
+
sourceId: `${provider}-provider-api`,
|
|
2208
2388
|
authReference: args.authReference,
|
|
2209
|
-
|
|
2389
|
+
// Core keeps a single connector input contract. Non-time-bounded
|
|
2390
|
+
// adapters ignore this sentinel and the CLI rejects user-supplied bounds
|
|
2391
|
+
// above so no requested interval can be silently discarded.
|
|
2392
|
+
startTime: usesRequestedTimeBounds ? args.startTime : 0,
|
|
2210
2393
|
endTime: args.endTime,
|
|
2211
2394
|
org: args.org,
|
|
2212
2395
|
enterprise: args.enterprise,
|
|
@@ -2306,18 +2489,23 @@ async function syncProviderCommand(args) {
|
|
|
2306
2489
|
`financial evidence: ${syncedSource.financialEvidence}`,
|
|
2307
2490
|
`coverage: ${result.coverage}`,
|
|
2308
2491
|
`records fetched: ${result.records.length}`,
|
|
2492
|
+
provider === "cursor"
|
|
2493
|
+
? "source window: current team subscription cycle returned by Cursor"
|
|
2494
|
+
: provider === "github-copilot"
|
|
2495
|
+
? "source window: latest Copilot metrics-report window plus current seat data"
|
|
2496
|
+
: `source window: requested from ${new Date(args.startTime * 1_000).toISOString()}${args.endTime === undefined ? " through provider current time" : ` through ${new Date(args.endTime * 1_000).toISOString()}`}`,
|
|
2309
2497
|
`headline basis: ${syncedFinancials.headlineBasis}`,
|
|
2310
2498
|
`synced provider headline: ${formatOptionalUsd(syncedFinancials.headlineUsd)}`,
|
|
2311
2499
|
`combined headline spend: ${selectProviderFinancialHeadlineRecords(records).some((record) => typeof record.amountUsd === "number") ? formatOptionalUsd(summary.totalUsd) : "unavailable"}`,
|
|
2312
2500
|
...(syncedFinancials.apiEquivalentEstimatedUsd !== null
|
|
2313
|
-
? [`API-equivalent estimate (kept separate):
|
|
2501
|
+
? [`API-equivalent estimate (kept separate): ${formatOptionalUsd(syncedFinancials.apiEquivalentEstimatedUsd)}`]
|
|
2314
2502
|
: []),
|
|
2315
2503
|
"auth: reference-only; raw secrets were not persisted or printed"
|
|
2316
2504
|
].join("\n"));
|
|
2317
2505
|
}
|
|
2318
2506
|
catch (error) {
|
|
2319
2507
|
const sanitizedError = sanitizeSecretishError(error instanceof Error ? error.message : String(error), args.authReference);
|
|
2320
|
-
await recordProviderSourceAttempt(stateDir,
|
|
2508
|
+
await recordProviderSourceAttempt(stateDir, provider, new Date().toISOString(), sanitizedError).catch(() => {
|
|
2321
2509
|
// Source-status state is diagnostic only. Do not hide the provider's
|
|
2322
2510
|
// real error if derived-state persistence is unavailable.
|
|
2323
2511
|
});
|
|
@@ -2427,11 +2615,11 @@ async function reportCommand(args) {
|
|
|
2427
2615
|
`verification plan: ${artifactPaths.verificationPlan}`,
|
|
2428
2616
|
`demo package: ${artifactPaths.demoPackage}`,
|
|
2429
2617
|
reportInput.dataMode === "sample"
|
|
2430
|
-
? `DEMO SAMPLE · illustrative cost/value evidence total:
|
|
2618
|
+
? `DEMO SAMPLE · illustrative cost/value evidence total: ${formatOptionalUsd(reportInput.summary.totalUsd)} · not user data`
|
|
2431
2619
|
: reportInput.dataMode === "connected_provider" &&
|
|
2432
2620
|
!(reportInput.allRecords ?? reportInput.providerRecords ?? []).some((record) => typeof record.amountUsd === "number")
|
|
2433
2621
|
? "cost/value evidence total: Unavailable · no priced financial evidence; missing/null is not zero"
|
|
2434
|
-
: `cost/value evidence total:
|
|
2622
|
+
: `cost/value evidence total: ${formatOptionalUsd(reportInput.summary.totalUsd)}`,
|
|
2435
2623
|
"privacy: report rendered locally with no aibill telemetry; only explicit sync-provider contacts the selected provider",
|
|
2436
2624
|
"",
|
|
2437
2625
|
"next:",
|
|
@@ -2446,7 +2634,7 @@ async function reportCommand(args) {
|
|
|
2446
2634
|
return {
|
|
2447
2635
|
exitCode: 1,
|
|
2448
2636
|
stdout: "",
|
|
2449
|
-
stderr: `Couldn't build a report: ${error instanceof Error ? error.message : String(error)}`
|
|
2637
|
+
stderr: `Couldn't build a report: ${sanitizeSecretishError(error instanceof Error ? error.message : String(error))}`
|
|
2450
2638
|
};
|
|
2451
2639
|
}
|
|
2452
2640
|
}
|
|
@@ -2470,7 +2658,10 @@ async function reportCardCommand(args) {
|
|
|
2470
2658
|
// would reject a harmless receipt written from the user's home directory.
|
|
2471
2659
|
// Output still goes through the safe-write/symlink checks below.
|
|
2472
2660
|
const rootPath = args.sample ? resolve(args.path) : await resolveSafeScanRoot(args.path);
|
|
2473
|
-
const { records, mode, providerCoverage } = await loadInstantReadData(args);
|
|
2661
|
+
const { records, mode, providerCoverage, warnings } = await loadInstantReadData(args);
|
|
2662
|
+
if (records.length === 0) {
|
|
2663
|
+
return noEvidenceResult("report-card", warnings, args.sinceDays ?? 30);
|
|
2664
|
+
}
|
|
2474
2665
|
const headlineRecords = mode === "connected"
|
|
2475
2666
|
? selectProviderFinancialHeadlineRecords(records)
|
|
2476
2667
|
: records;
|
|
@@ -2510,7 +2701,7 @@ async function reportCardCommand(args) {
|
|
|
2510
2701
|
return {
|
|
2511
2702
|
exitCode: 1,
|
|
2512
2703
|
stdout: "",
|
|
2513
|
-
stderr: `Couldn't write the report card: ${error instanceof Error ? error.message : String(error)}`
|
|
2704
|
+
stderr: `Couldn't write the report card: ${sanitizeSecretishError(error instanceof Error ? error.message : String(error))}`
|
|
2514
2705
|
};
|
|
2515
2706
|
}
|
|
2516
2707
|
}
|
|
@@ -2544,6 +2735,13 @@ async function applyArtifactCommand(args) {
|
|
|
2544
2735
|
codingPrompt.trimEnd()
|
|
2545
2736
|
].join("\n"));
|
|
2546
2737
|
}
|
|
2738
|
+
const noCandidateGuidance = codingPrompt.includes("NO SCOPED CHANGE CANDIDATE")
|
|
2739
|
+
? [
|
|
2740
|
+
"",
|
|
2741
|
+
"No scoped change is supported yet. Collect the missing source evidence instead of guessing:",
|
|
2742
|
+
...applyEvidenceAcquisitionLines(reportInput)
|
|
2743
|
+
]
|
|
2744
|
+
: [];
|
|
2547
2745
|
return ok([
|
|
2548
2746
|
"aibill apply-artifact",
|
|
2549
2747
|
`path: ${rootPath}`,
|
|
@@ -2552,6 +2750,7 @@ async function applyArtifactCommand(args) {
|
|
|
2552
2750
|
`verification plan: ${artifactPaths.verificationPlan}`,
|
|
2553
2751
|
`demo package: ${artifactPaths.demoPackage}`,
|
|
2554
2752
|
"safety: generated artifacts only; no external systems changed",
|
|
2753
|
+
...noCandidateGuidance,
|
|
2555
2754
|
"",
|
|
2556
2755
|
`──── copy everything below into Claude Code / Codex (also saved at ${artifactPaths.codingPrompt}) ────`,
|
|
2557
2756
|
"",
|
|
@@ -2562,10 +2761,35 @@ async function applyArtifactCommand(args) {
|
|
|
2562
2761
|
return {
|
|
2563
2762
|
exitCode: 1,
|
|
2564
2763
|
stdout: "",
|
|
2565
|
-
stderr:
|
|
2764
|
+
stderr: [
|
|
2765
|
+
`Couldn't build apply artifacts: ${sanitizeSecretishError(error instanceof Error ? error.message : String(error))}`,
|
|
2766
|
+
"Collect evidence, then retry Apply:",
|
|
2767
|
+
"- Claude Code / Codex / Gemini CLI: use the agent normally so supported local history exists.",
|
|
2768
|
+
"- OpenAI / Anthropic / Cursor / GitHub Copilot: run `npx aibill connect <provider>` with an admin credential reference.",
|
|
2769
|
+
"- Diagnose the exact gap: `npx aibill doctor --sources`.",
|
|
2770
|
+
"- Demo only: `npx aibill apply --sample` (non-executable)."
|
|
2771
|
+
].join("\n")
|
|
2566
2772
|
};
|
|
2567
2773
|
}
|
|
2568
2774
|
}
|
|
2775
|
+
function applyEvidenceAcquisitionLines(input) {
|
|
2776
|
+
const records = input.allRecords ?? input.providerRecords ?? [];
|
|
2777
|
+
const providers = new Set(records.map((record) => record.source.provider));
|
|
2778
|
+
const lines = [];
|
|
2779
|
+
if (providers.has("anthropic") || providers.has("openai") || input.dataMode === "local_logs") {
|
|
2780
|
+
lines.push("- Local coding agents: run `npx aibill context --json` after comparable Claude Code/Codex sessions to inspect action-capable context evidence.");
|
|
2781
|
+
}
|
|
2782
|
+
if (providers.has("gemini") || providers.has("gemini-cli")) {
|
|
2783
|
+
lines.push("- Gemini CLI: current chat evidence is financial-only; unsupported context/action evidence remains missing.");
|
|
2784
|
+
}
|
|
2785
|
+
if (input.dataMode === "connected_provider") {
|
|
2786
|
+
lines.push("- Provider billing: sync explicit call/invocation-level workload evidence; aggregate owner or spend rows do not authorize a change.");
|
|
2787
|
+
}
|
|
2788
|
+
if (lines.length === 0) {
|
|
2789
|
+
lines.push("- Run `npx aibill doctor --sources` to see which local or provider evidence is missing.");
|
|
2790
|
+
}
|
|
2791
|
+
return lines;
|
|
2792
|
+
}
|
|
2569
2793
|
async function buildExplicitSampleReportInput(rootPath) {
|
|
2570
2794
|
const records = await loadSampleUsageData();
|
|
2571
2795
|
return {
|
|
@@ -2703,7 +2927,8 @@ async function buildReportInput(stateDir, rootPath, sinceDays = 30) {
|
|
|
2703
2927
|
"Re-run `npx aibill` while the local transcripts are available; no report or Apply action was generated from repository state alone.");
|
|
2704
2928
|
}
|
|
2705
2929
|
throw new Error("no persisted spend state and no supported local-agent financial evidence found. " +
|
|
2706
|
-
"
|
|
2930
|
+
"Use Claude Code, Codex, or Gemini CLI normally; connect an admin provider for billed cost; " +
|
|
2931
|
+
"or run `npx aibill doctor --sources` to inspect the exact gap.");
|
|
2707
2932
|
}
|
|
2708
2933
|
const [discovery, sourceRegistry, missingSourcePrompts, confirmedMappings, persistedProviderRecordsState] = await Promise.all([
|
|
2709
2934
|
readOptionalJson(join(stateDir, "discovery.json"), emptyDiscovery(rootPath)),
|
|
@@ -2851,7 +3076,8 @@ function parseArgs(argv) {
|
|
|
2851
3076
|
const parsed = {
|
|
2852
3077
|
command,
|
|
2853
3078
|
sample: false,
|
|
2854
|
-
path: process.cwd()
|
|
3079
|
+
path: process.cwd(),
|
|
3080
|
+
parseErrors: []
|
|
2855
3081
|
};
|
|
2856
3082
|
if (command === "statusline" && rest[0] && !rest[0].startsWith("--")) {
|
|
2857
3083
|
parsed.statuslineAction = rest.shift();
|
|
@@ -2860,8 +3086,26 @@ function parseArgs(argv) {
|
|
|
2860
3086
|
parsed.provider = rest[0];
|
|
2861
3087
|
rest.shift();
|
|
2862
3088
|
}
|
|
3089
|
+
const valueFlags = new Set([
|
|
3090
|
+
"--plan", "--since-days", "--path", "--out",
|
|
3091
|
+
"--source-path", "--type", "--provider", "--source-id", "--team",
|
|
3092
|
+
"--person", "--client", "--project", "--agent", "--workflow",
|
|
3093
|
+
"--evidence", "--confidence", "--label", "--auth-reference",
|
|
3094
|
+
"--start-time", "--end-time", "--org", "--enterprise", "--account-id",
|
|
3095
|
+
"--interval", "--cycles"
|
|
3096
|
+
]);
|
|
3097
|
+
const numericValueFlags = new Set([
|
|
3098
|
+
"--since-days", "--confidence", "--start-time", "--end-time", "--interval", "--cycles"
|
|
3099
|
+
]);
|
|
2863
3100
|
for (let index = 0; index < rest.length; index += 1) {
|
|
2864
3101
|
const arg = rest[index];
|
|
3102
|
+
const nextValue = rest[index + 1];
|
|
3103
|
+
const nextLooksLikeFlag = nextValue?.startsWith("--") ||
|
|
3104
|
+
(!numericValueFlags.has(arg) && nextValue?.startsWith("-"));
|
|
3105
|
+
if (valueFlags.has(arg) && (nextValue === undefined || nextLooksLikeFlag)) {
|
|
3106
|
+
parsed.parseErrors.push(`${arg} requires a value`);
|
|
3107
|
+
continue;
|
|
3108
|
+
}
|
|
2865
3109
|
if (arg === "--sample") {
|
|
2866
3110
|
parsed.sample = true;
|
|
2867
3111
|
continue;
|
|
@@ -2874,6 +3118,10 @@ function parseArgs(argv) {
|
|
|
2874
3118
|
parsed.json = true;
|
|
2875
3119
|
continue;
|
|
2876
3120
|
}
|
|
3121
|
+
if (arg === "--full") {
|
|
3122
|
+
parsed.full = true;
|
|
3123
|
+
continue;
|
|
3124
|
+
}
|
|
2877
3125
|
if (arg === "--sources") {
|
|
2878
3126
|
parsed.sources = true;
|
|
2879
3127
|
continue;
|
|
@@ -2957,6 +3205,10 @@ function parseArgs(argv) {
|
|
|
2957
3205
|
parsed.sourceType = next;
|
|
2958
3206
|
index += 1;
|
|
2959
3207
|
}
|
|
3208
|
+
else if (next) {
|
|
3209
|
+
parsed.parseErrors.push(`--type received unsupported source type "${next}"`);
|
|
3210
|
+
index += 1;
|
|
3211
|
+
}
|
|
2960
3212
|
continue;
|
|
2961
3213
|
}
|
|
2962
3214
|
if (arg === "--provider") {
|
|
@@ -3039,6 +3291,9 @@ function parseArgs(argv) {
|
|
|
3039
3291
|
if (Number.isFinite(value) && value >= 0 && value <= 1) {
|
|
3040
3292
|
parsed.confidence = value;
|
|
3041
3293
|
}
|
|
3294
|
+
else {
|
|
3295
|
+
parsed.parseErrors.push("--confidence must be a number between 0 and 1");
|
|
3296
|
+
}
|
|
3042
3297
|
index += 1;
|
|
3043
3298
|
}
|
|
3044
3299
|
continue;
|
|
@@ -3062,7 +3317,11 @@ function parseArgs(argv) {
|
|
|
3062
3317
|
if (arg === "--start-time") {
|
|
3063
3318
|
const next = rest[index + 1];
|
|
3064
3319
|
if (next) {
|
|
3065
|
-
|
|
3320
|
+
const value = Number(next);
|
|
3321
|
+
if (Number.isFinite(value))
|
|
3322
|
+
parsed.startTime = value;
|
|
3323
|
+
else
|
|
3324
|
+
parsed.parseErrors.push("--start-time must be a finite Unix timestamp");
|
|
3066
3325
|
index += 1;
|
|
3067
3326
|
}
|
|
3068
3327
|
continue;
|
|
@@ -3070,7 +3329,11 @@ function parseArgs(argv) {
|
|
|
3070
3329
|
if (arg === "--end-time") {
|
|
3071
3330
|
const next = rest[index + 1];
|
|
3072
3331
|
if (next) {
|
|
3073
|
-
|
|
3332
|
+
const value = Number(next);
|
|
3333
|
+
if (Number.isFinite(value))
|
|
3334
|
+
parsed.endTime = value;
|
|
3335
|
+
else
|
|
3336
|
+
parsed.parseErrors.push("--end-time must be a finite Unix timestamp");
|
|
3074
3337
|
index += 1;
|
|
3075
3338
|
}
|
|
3076
3339
|
continue;
|
|
@@ -3102,7 +3365,11 @@ function parseArgs(argv) {
|
|
|
3102
3365
|
if (arg === "--interval") {
|
|
3103
3366
|
const next = rest[index + 1];
|
|
3104
3367
|
if (next) {
|
|
3105
|
-
|
|
3368
|
+
const value = Number(next);
|
|
3369
|
+
if (Number.isFinite(value) && value > 0)
|
|
3370
|
+
parsed.interval = value;
|
|
3371
|
+
else
|
|
3372
|
+
parsed.parseErrors.push("--interval must be a positive number of seconds");
|
|
3106
3373
|
index += 1;
|
|
3107
3374
|
}
|
|
3108
3375
|
continue;
|
|
@@ -3110,11 +3377,18 @@ function parseArgs(argv) {
|
|
|
3110
3377
|
if (arg === "--cycles") {
|
|
3111
3378
|
const next = rest[index + 1];
|
|
3112
3379
|
if (next) {
|
|
3113
|
-
|
|
3380
|
+
const value = Number(next);
|
|
3381
|
+
if (Number.isInteger(value) && value >= 0)
|
|
3382
|
+
parsed.cycles = value;
|
|
3383
|
+
else
|
|
3384
|
+
parsed.parseErrors.push("--cycles must be a whole number of 0 or greater");
|
|
3114
3385
|
index += 1;
|
|
3115
3386
|
}
|
|
3116
3387
|
continue;
|
|
3117
3388
|
}
|
|
3389
|
+
parsed.parseErrors.push(arg.startsWith("-")
|
|
3390
|
+
? `unknown flag "${arg}"`
|
|
3391
|
+
: `unexpected argument "${arg}"`);
|
|
3118
3392
|
}
|
|
3119
3393
|
return parsed;
|
|
3120
3394
|
}
|
|
@@ -3242,7 +3516,9 @@ function helpText() {
|
|
|
3242
3516
|
"aibill — your AI cost and usage evidence in one private view",
|
|
3243
3517
|
"",
|
|
3244
3518
|
"Run with no command for an instant, zero-key local readout:",
|
|
3245
|
-
" npx aibill Show
|
|
3519
|
+
" npx aibill Show a compact receipt from available local/connected evidence",
|
|
3520
|
+
" npx aibill --full Show the complete diagnose → recommend → apply → verify audit",
|
|
3521
|
+
" npx aibill --sample Show the clearly labeled illustrative demo (never implicit)",
|
|
3246
3522
|
" npx aibill --group-by agent Drill down by source|model|client|project|agent|user|workspace|apiKey",
|
|
3247
3523
|
" npx aibill --plan <id> Declare your plan when auto-detection can't (claude-max-5x|claude-max-20x|claude-pro|chatgpt-plus|chatgpt-pro)",
|
|
3248
3524
|
"",
|
|
@@ -3272,12 +3548,14 @@ function helpText() {
|
|
|
3272
3548
|
" scan [--path <dir>] Scan a local workspace for AI usage signals",
|
|
3273
3549
|
" scan --sample Include deterministic sample spend analysis",
|
|
3274
3550
|
" quickstart [--sample] [--since-days N] Plain-English local readout (default 30 days)",
|
|
3551
|
+
" [--full] Render the complete audit; default is the compact receipt",
|
|
3275
3552
|
" [--group-by source|model|client|project|agent|user|workspace|apiKey] Default: project for local logs; model otherwise",
|
|
3276
3553
|
" report [--sample] [--out <name>] [--since-days N] Generate local Markdown and HTML reports from the same window",
|
|
3277
3554
|
" report-card [--out f.svg] Write your AI Receipt — a redacted, shareable SVG + caption",
|
|
3278
3555
|
" glance [--project <name>] [--plan <id>] [--since-days N] Emit the local, machine-readable Glance snapshot JSON",
|
|
3279
3556
|
" context [--project <name>] [--since-days N] Show hook-aware Context Health in the terminal",
|
|
3280
3557
|
" [--json] Emit the same canonical Context Health object used by MCP and Glance",
|
|
3558
|
+
" Main receipt JSON is not published yet; unsupported --json requests fail instead of returning text.",
|
|
3281
3559
|
" apply [--sample] [--since-days N] Print an evidence-constrained inspection/approval prompt + verification plans",
|
|
3282
3560
|
" apply-artifact Same as `apply` (long form)",
|
|
3283
3561
|
"",
|
|
@@ -3326,7 +3604,7 @@ export async function runMain() {
|
|
|
3326
3604
|
if (isInstantDemo && process.stdout.isTTY && !process.env.NO_COLOR) {
|
|
3327
3605
|
try {
|
|
3328
3606
|
const { default: yoctoSpinner } = await import("yocto-spinner");
|
|
3329
|
-
spinner = yoctoSpinner({ text: "
|
|
3607
|
+
spinner = yoctoSpinner({ text: "Reading local AI evidence…" }).start();
|
|
3330
3608
|
}
|
|
3331
3609
|
catch {
|
|
3332
3610
|
// Spinner is optional; never block the wow on it.
|
|
@@ -17,6 +17,11 @@ const CACHE_MAX_BYTES = 64 * 1_024;
|
|
|
17
17
|
const DEFAULT_COLUMNS = 100;
|
|
18
18
|
const MAX_COLUMNS = 240;
|
|
19
19
|
const STALE_AFTER_MS = 5 * 60 * 1_000;
|
|
20
|
+
// A freshly regenerated cache does not make an old transcript percentage current.
|
|
21
|
+
// Five-hour evidence expires with that window; weekly evidence must have been
|
|
22
|
+
// observed within the last day before it can be presented as live runway.
|
|
23
|
+
const FIVE_HOUR_LIMIT_FRESHNESS_MS = 5 * 60 * 60 * 1_000;
|
|
24
|
+
const WEEKLY_LIMIT_FRESHNESS_MS = 24 * 60 * 60 * 1_000;
|
|
20
25
|
const STDIN_MAX_BYTES = 64 * 1_024;
|
|
21
26
|
const STDIN_DRAIN_TIMEOUT_MS = 25;
|
|
22
27
|
const guardedOutputs = new WeakSet();
|
|
@@ -276,8 +281,9 @@ function renderSubscription(snapshot, tier, now, timeZone) {
|
|
|
276
281
|
? [...limits].sort((left, right) => limitKindOrder(left.kind) - limitKindOrder(right.kind))
|
|
277
282
|
: [...limits].sort(compareUrgency).slice(0, 1);
|
|
278
283
|
const segments = ordered.map((limit) => formatLimit(limit, now, timeZone));
|
|
279
|
-
if (limits.length === 0)
|
|
280
|
-
segments.push("subscription detected",
|
|
284
|
+
if (limits.length === 0) {
|
|
285
|
+
segments.push("subscription detected", missingRunwaySegment(snapshot, now));
|
|
286
|
+
}
|
|
281
287
|
const value = agent.apiEquivalent.sevenDays;
|
|
282
288
|
if (value.amountUsd !== null && value.financialEvidence === "estimated") {
|
|
283
289
|
segments.push(`~${formatUsd(value.amountUsd)} 7d value`);
|
|
@@ -299,8 +305,9 @@ function renderMixed(snapshot, tier, now, timeZone) {
|
|
|
299
305
|
? [...limits].sort((left, right) => limitKindOrder(left.kind) - limitKindOrder(right.kind))
|
|
300
306
|
: [...limits].sort(compareUrgency).slice(0, 1);
|
|
301
307
|
const segments = selectedLimits.map((limit) => formatLimit(limit, now, timeZone));
|
|
302
|
-
if (limits.length === 0 && tier !== "minimal")
|
|
303
|
-
segments.push(
|
|
308
|
+
if (limits.length === 0 && tier !== "minimal") {
|
|
309
|
+
segments.push(missingRunwaySegment(snapshot, now));
|
|
310
|
+
}
|
|
304
311
|
appendMeteredSevenDaySegment(snapshot, segments);
|
|
305
312
|
const value = agent?.apiEquivalent.sevenDays;
|
|
306
313
|
if (value?.amountUsd !== null && value?.amountUsd !== undefined &&
|
|
@@ -318,8 +325,12 @@ function renderMultiSubscriptionSegments(snapshot, tier, now, timeZone) {
|
|
|
318
325
|
const segments = selected.map(({ agent, limit }) => tier === "full"
|
|
319
326
|
? formatAttributedLimit(agent, limit, now, timeZone)
|
|
320
327
|
: formatAttributedLimitCompact(agent, limit));
|
|
321
|
-
|
|
322
|
-
|
|
328
|
+
const staleRunway = staleRunwaySegments(snapshot, now);
|
|
329
|
+
if (staleRunway.length > 0 && tier !== "minimal") {
|
|
330
|
+
segments.push(...staleRunway);
|
|
331
|
+
}
|
|
332
|
+
else if (entries.length === 0 && tier !== "minimal") {
|
|
333
|
+
segments.push("subscription detected", missingRunwaySegment(snapshot, now));
|
|
323
334
|
}
|
|
324
335
|
const agents = orderedSubscriptionAgents(snapshot, now);
|
|
325
336
|
const valueAgents = tier === "full" ? agents : agents.slice(0, 1);
|
|
@@ -422,7 +433,30 @@ function latestLimitObservation(agent, now) {
|
|
|
422
433
|
return Math.max(0, ...activeLimits(agent, now).map((limit) => Date.parse(limit.observedAt)));
|
|
423
434
|
}
|
|
424
435
|
function activeLimits(agent, now) {
|
|
425
|
-
return agent.limits.filter((limit) => Date.parse(limit.resetsAt) > now.getTime());
|
|
436
|
+
return agent.limits.filter((limit) => (Date.parse(limit.resetsAt) > now.getTime() && isFreshLimitEvidence(limit, now)));
|
|
437
|
+
}
|
|
438
|
+
function staleActiveLimits(agent, now) {
|
|
439
|
+
return agent.limits.filter((limit) => (Date.parse(limit.resetsAt) > now.getTime() && !isFreshLimitEvidence(limit, now)));
|
|
440
|
+
}
|
|
441
|
+
function isFreshLimitEvidence(limit, now) {
|
|
442
|
+
const observedMs = Date.parse(limit.observedAt);
|
|
443
|
+
const ageMs = now.getTime() - observedMs;
|
|
444
|
+
const maximumAgeMs = limit.kind === "five-hour"
|
|
445
|
+
? FIVE_HOUR_LIMIT_FRESHNESS_MS
|
|
446
|
+
: WEEKLY_LIMIT_FRESHNESS_MS;
|
|
447
|
+
return Number.isFinite(observedMs) && ageMs >= 0 && ageMs <= maximumAgeMs;
|
|
448
|
+
}
|
|
449
|
+
function hasStaleLimitEvidence(snapshot, now) {
|
|
450
|
+
return (snapshot.subscription?.agents ?? [])
|
|
451
|
+
.some((agent) => staleActiveLimits(agent, now).length > 0);
|
|
452
|
+
}
|
|
453
|
+
function missingRunwaySegment(snapshot, now) {
|
|
454
|
+
return hasStaleLimitEvidence(snapshot, now) ? "runway stale" : "runway not reported";
|
|
455
|
+
}
|
|
456
|
+
function staleRunwaySegments(snapshot, now) {
|
|
457
|
+
return orderedSubscriptionAgents(snapshot, now)
|
|
458
|
+
.filter((agent) => activeLimits(agent, now).length === 0 && staleActiveLimits(agent, now).length > 0)
|
|
459
|
+
.map((agent) => `${subscriptionAgentLabel(agent)} runway stale`);
|
|
426
460
|
}
|
|
427
461
|
function compareUrgency(left, right) {
|
|
428
462
|
const remainingDifference = left.remainingPercent - right.remainingPercent;
|
|
@@ -516,6 +550,8 @@ function formatAge(rawAgeMs) {
|
|
|
516
550
|
return `${Math.floor(hours / 24)}d`;
|
|
517
551
|
}
|
|
518
552
|
function formatUsd(amount) {
|
|
553
|
+
if (amount > 0 && amount < 0.01)
|
|
554
|
+
return "<$0.01";
|
|
519
555
|
if (amount >= 1_000_000_000)
|
|
520
556
|
return `$${compactNumber(amount / 1_000_000_000)}b`;
|
|
521
557
|
if (amount >= 1_000_000)
|
|
@@ -574,7 +610,9 @@ function assembleMultiSubscriptionOverageLine(snapshot, fullSegments, overage, f
|
|
|
574
610
|
function assembleMixedLine(snapshot, fullSegments, freshness, tier, columns, now, timeZone) {
|
|
575
611
|
const agent = selectSubscriptionAgent(snapshot, now);
|
|
576
612
|
const urgent = agent ? [...activeLimits(agent, now)].sort(compareUrgency)[0] : undefined;
|
|
577
|
-
const runway = urgent
|
|
613
|
+
const runway = urgent
|
|
614
|
+
? formatLimitCompact(urgent)
|
|
615
|
+
: hasStaleLimitEvidence(snapshot, now) ? "runway stale" : "runway n/r";
|
|
578
616
|
const runwayWithReset = urgent
|
|
579
617
|
? `${runway} ↻${formatReset(urgent, now, timeZone)}`
|
|
580
618
|
: runway;
|
|
@@ -625,7 +663,10 @@ function assembleMultiSubscriptionLine(snapshot, freshness, tier, columns, now,
|
|
|
625
663
|
const pressure = agents
|
|
626
664
|
.filter((agent) => agent.pressure === "extra_usage_credits_exhausted")
|
|
627
665
|
.map((agent) => `${subscriptionAgentLabel(agent)} plan pressure`);
|
|
628
|
-
const
|
|
666
|
+
const staleRunway = staleRunwaySegments(snapshot, now);
|
|
667
|
+
const noRunway = entries.length === 0 && staleRunway.length === 0
|
|
668
|
+
? ["subscription detected", "runway not reported"]
|
|
669
|
+
: staleRunway;
|
|
629
670
|
const urgent = compactPrimary[0];
|
|
630
671
|
const urgentValue = agents[0] ? formatAttributedValue(agents[0], true) : undefined;
|
|
631
672
|
const candidates = [];
|
|
@@ -641,7 +682,8 @@ function assembleMultiSubscriptionLine(snapshot, freshness, tier, columns, now,
|
|
|
641
682
|
candidates.push(["aibill", urgent, freshness], ["aibill", urgent], [urgent]);
|
|
642
683
|
}
|
|
643
684
|
else {
|
|
644
|
-
|
|
685
|
+
const compactRunway = hasStaleLimitEvidence(snapshot, now) ? "runway stale" : "runway n/r";
|
|
686
|
+
candidates.push(["aibill", compactRunway, freshness], [compactRunway, freshness], ["aibill", ...noRunway, ...compactValues, freshness], ["aibill", ...compactValues, freshness], ["aibill", "subscription detected", freshness], ["subscription detected"]);
|
|
645
687
|
}
|
|
646
688
|
return firstFittingLine(candidates, columns);
|
|
647
689
|
}
|
|
@@ -652,7 +694,10 @@ function assembleMultiSubscriptionMixedLine(snapshot, fullSegments, freshness, t
|
|
|
652
694
|
const compactLimits = entries.map(({ agent, limit }) => formatAttributedLimitCompact(agent, limit));
|
|
653
695
|
const compactPrimary = primary.map(({ agent, limit }) => formatAttributedLimitCompact(agent, limit));
|
|
654
696
|
const compactValues = agents.flatMap((agent) => formatAttributedValue(agent, true) ?? []);
|
|
655
|
-
const
|
|
697
|
+
const staleRunway = staleRunwaySegments(snapshot, now);
|
|
698
|
+
const noRunway = entries.length === 0 && staleRunway.length === 0
|
|
699
|
+
? ["subscription detected", "runway not reported"]
|
|
700
|
+
: staleRunway;
|
|
656
701
|
const urgent = compactPrimary[0];
|
|
657
702
|
const urgentValue = agents[0] ? formatAttributedValue(agents[0], true) : undefined;
|
|
658
703
|
const meteredBilled = snapshot.metered?.providerBilled.sevenDays;
|
|
@@ -665,6 +710,7 @@ function assembleMultiSubscriptionMixedLine(snapshot, fullSegments, freshness, t
|
|
|
665
710
|
? `metered ~${formatUsd(meteredEstimated.amountUsd)}/7d`
|
|
666
711
|
: "metered n/r";
|
|
667
712
|
const shortFreshness = compactFreshness(freshness);
|
|
713
|
+
const compactRunway = hasStaleLimitEvidence(snapshot, now) ? "runway stale" : "runway n/r";
|
|
668
714
|
const candidates = [];
|
|
669
715
|
if (tier === "full")
|
|
670
716
|
candidates.push(["aibill", ...fullSegments, freshness]);
|
|
@@ -677,7 +723,7 @@ function assembleMultiSubscriptionMixedLine(snapshot, fullSegments, freshness, t
|
|
|
677
723
|
candidates.push(["aibill", "mix", urgent, metered, shortFreshness], ["mix", urgent, metered], ["aibill", urgent, shortFreshness], ["aibill", urgent], [urgent]);
|
|
678
724
|
}
|
|
679
725
|
else {
|
|
680
|
-
candidates.push(["aibill", "mix", ...noRunway, metered, freshness], ["aibill", ...noRunway, freshness], ["aibill", "mix",
|
|
726
|
+
candidates.push(["aibill", "mix", ...noRunway, metered, freshness], ["aibill", ...noRunway, freshness], ["aibill", "mix", compactRunway, metered, shortFreshness], ["mix", compactRunway, metered, shortFreshness], ["mix", compactRunway, shortFreshness]);
|
|
681
727
|
}
|
|
682
728
|
candidates.push(["mix", metered, shortFreshness], ["mix", metered], ["mix"]);
|
|
683
729
|
return firstFittingLine(candidates, columns);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ai-spend-agent",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.1",
|
|
4
4
|
"description": "Local-first financial accountability CLI: Claude Code/Codex attribution, provenance, and next actions, plus experimental Gemini CLI cost evidence.",
|
|
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.8.
|
|
58
|
-
"@agent-finops/report": "0.8.
|
|
57
|
+
"@agent-finops/core": "0.8.1",
|
|
58
|
+
"@agent-finops/report": "0.8.1",
|
|
59
59
|
"yocto-spinner": "^1.2.0"
|
|
60
60
|
}
|
|
61
61
|
}
|