ai-spend-agent 0.9.3 → 0.9.4
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 +157 -58
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -618,9 +618,11 @@ function quickstartNextSteps(mode, detected) {
|
|
|
618
618
|
steps.push(`npx aibill connect ${detected[0].provider} set up the admin connector, then sync provider-reported cost`);
|
|
619
619
|
}
|
|
620
620
|
steps.push(mode === "demo"
|
|
621
|
-
//
|
|
622
|
-
//
|
|
623
|
-
|
|
621
|
+
// 0.9.4: report --sample runs as printed from ANY directory — broad
|
|
622
|
+
// roots write ./ai-spend-report.{md,html} machine-wide-style, project
|
|
623
|
+
// folders keep .ai-spend-agent/report.* (the old mkdir demo-workspace
|
|
624
|
+
// preamble is no longer needed for the command to run as printed).
|
|
625
|
+
? "npx aibill report --sample write a clearly labeled demo report right here"
|
|
624
626
|
: "npx aibill report write a shareable Markdown + HTML report");
|
|
625
627
|
steps.push("npx aibill --group-by project see which project has the most observed activity");
|
|
626
628
|
steps.push("Need team reconciliation, allocation, budgets, and approvals? Workspace design partners: https://asktilden.com");
|
|
@@ -1154,7 +1156,9 @@ function noEvidenceResult(surface, warnings, sinceDays, telemetryDisclosure) {
|
|
|
1154
1156
|
? "Watch has no financial baseline yet; no zero total or sample activity was recorded."
|
|
1155
1157
|
: surface === "report-card"
|
|
1156
1158
|
? "No receipt was written because there is no supported financial evidence to summarize."
|
|
1157
|
-
:
|
|
1159
|
+
: surface === "report"
|
|
1160
|
+
? "No report was written because there is no supported financial evidence to summarize."
|
|
1161
|
+
: `No supported AI usage evidence was found in the last ${sinceDays} days.`;
|
|
1158
1162
|
return {
|
|
1159
1163
|
exitCode: surface === "receipt" ? 0 : 1,
|
|
1160
1164
|
stdout: surface === "receipt"
|
|
@@ -3758,27 +3762,46 @@ async function confirmMappingCommand(args) {
|
|
|
3758
3762
|
].join("\n"));
|
|
3759
3763
|
}
|
|
3760
3764
|
async function reportCommand(args, runtime = {}) {
|
|
3761
|
-
//
|
|
3762
|
-
//
|
|
3763
|
-
//
|
|
3764
|
-
//
|
|
3765
|
-
//
|
|
3766
|
-
//
|
|
3767
|
-
|
|
3768
|
-
|
|
3769
|
-
|
|
3765
|
+
// 0.9.4: a broad root (home, /) runs MACHINE-WIDE — the same read-only
|
|
3766
|
+
// transcript scanning as the bare receipt, no project state created, both
|
|
3767
|
+
// report files written to the current directory. The report renders
|
|
3768
|
+
// machine-wide content anyway (it lists every project), so the
|
|
3769
|
+
// exact-project requirement was incoherent here: the receipt's own Next
|
|
3770
|
+
// pointer led from home straight into a refusal. Only a bogus --path
|
|
3771
|
+
// still gets the friendly guard; project folders behave exactly as
|
|
3772
|
+
// before.
|
|
3773
|
+
const machineWide = isBroadScanRoot(args.path);
|
|
3774
|
+
if (!machineWide) {
|
|
3775
|
+
const rootGuard = await guardExactProjectRoot("report", args.path);
|
|
3776
|
+
if (rootGuard)
|
|
3777
|
+
return rootGuard;
|
|
3778
|
+
}
|
|
3770
3779
|
const rootPath = resolve(args.path);
|
|
3771
3780
|
try {
|
|
3772
3781
|
const sinceDays = args.sinceDays ?? 30;
|
|
3773
3782
|
if (!validSinceDays(sinceDays))
|
|
3774
3783
|
return invalidSinceDaysResult();
|
|
3775
|
-
|
|
3784
|
+
// Machine-wide mode NEVER creates project state at the broad root — the
|
|
3785
|
+
// only writes are the two report files below (plus ~/.aibill home state
|
|
3786
|
+
// owned by other subsystems).
|
|
3787
|
+
const stateDir = machineWide ? undefined : await resolveSafeStateDirectory(rootPath, { create: true });
|
|
3776
3788
|
// Like Apply, an explicit sample report is a strict privacy boundary. It
|
|
3777
3789
|
// must not inspect local transcripts, account metadata, or persisted state.
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
|
|
3781
|
-
|
|
3790
|
+
let reportInput;
|
|
3791
|
+
if (args.sample) {
|
|
3792
|
+
reportInput = await buildExplicitSampleReportInput(rootPath);
|
|
3793
|
+
}
|
|
3794
|
+
else if (machineWide) {
|
|
3795
|
+
const machineWideInput = await buildMachineWideReportInput(args, sinceDays);
|
|
3796
|
+
if (machineWideInput.kind === "no_evidence") {
|
|
3797
|
+
return noEvidenceResult("report", machineWideInput.warnings, sinceDays, runtime.telemetryDisclosure);
|
|
3798
|
+
}
|
|
3799
|
+
reportInput = machineWideInput.input;
|
|
3800
|
+
}
|
|
3801
|
+
else {
|
|
3802
|
+
reportInput = await buildReportInput(stateDir, rootPath, sinceDays);
|
|
3803
|
+
}
|
|
3804
|
+
const persistedPreferredExperiment = args.sample || machineWide
|
|
3782
3805
|
? undefined
|
|
3783
3806
|
: chooseLatestTokenReductionExperiment((await loadTokenVerificationState(rootPath)).experiments);
|
|
3784
3807
|
const preferredExperiment = persistedPreferredExperiment &&
|
|
@@ -3820,48 +3843,60 @@ async function reportCommand(args, runtime = {}) {
|
|
|
3820
3843
|
: { ...reportInput, telemetryDisclosure };
|
|
3821
3844
|
const qualitativeActionsSuppressed = reportInput.dataMode !== "sample" &&
|
|
3822
3845
|
reportInput.qualitativeCoverage?.status !== "complete";
|
|
3823
|
-
|
|
3846
|
+
// Machine-wide artifacts land in the CURRENT directory under the
|
|
3847
|
+
// ai-spend-* family name; project mode keeps .ai-spend-agent/report.*.
|
|
3848
|
+
const outBase = args.out
|
|
3849
|
+
? resolve(rootPath, args.out)
|
|
3850
|
+
: machineWide
|
|
3851
|
+
? join(rootPath, "ai-spend-report")
|
|
3852
|
+
: join(stateDir, "report");
|
|
3824
3853
|
const markdownPath = `${outBase}.md`;
|
|
3825
3854
|
const htmlPath = `${outBase}.html`;
|
|
3826
|
-
await writeLocalReportFile(markdownPath, generateMarkdownReport(reportRenderInput), stateDir);
|
|
3827
|
-
await writeLocalReportFile(htmlPath, generateHtmlReport(reportRenderInput), stateDir);
|
|
3855
|
+
await writeLocalReportFile(markdownPath, generateMarkdownReport(reportRenderInput), stateDir ?? rootPath);
|
|
3856
|
+
await writeLocalReportFile(htmlPath, generateHtmlReport(reportRenderInput), stateDir ?? rootPath);
|
|
3828
3857
|
// A preferred canonical experiment owns this project's action/result
|
|
3829
3858
|
// lineage even after completion or rollback. A report may refresh its
|
|
3830
3859
|
// read-only projection, but never overwrite the frozen handoff with a
|
|
3831
3860
|
// fresh or contradictory candidate. Coverage gaps receive only explicit
|
|
3832
3861
|
// non-executable gap artifacts from the report package.
|
|
3833
|
-
|
|
3862
|
+
// Apply artifacts are project-scoped handoffs — machine-wide runs skip
|
|
3863
|
+
// them (apply itself still requires one exact project folder).
|
|
3864
|
+
const artifactPaths = reportableExperiment || machineWide
|
|
3834
3865
|
? undefined
|
|
3835
3866
|
: await writeApplyArtifacts(stateDir, reportInput);
|
|
3836
3867
|
return ok([
|
|
3837
3868
|
"aibill report",
|
|
3838
|
-
|
|
3869
|
+
machineWide
|
|
3870
|
+
? `scope: machine-wide · all supported local agent evidence on this machine (last ${sinceDays} days) · artifacts in ${rootPath}`
|
|
3871
|
+
: `path: ${rootPath}`,
|
|
3839
3872
|
`markdown: ${markdownPath}`,
|
|
3840
3873
|
`html: ${htmlPath}`,
|
|
3841
|
-
...(
|
|
3842
|
-
? [
|
|
3843
|
-
|
|
3844
|
-
`token result: status=${reportableExperiment.evaluation.status}; reductionPercent=${reportExperimentProjection.reductionPercent ?? "unavailable"}; metricEvidence=${reportExperimentProjection.evidenceLabel}; quality=${reportExperimentProjection.qualityLabel}; qualityEvidence=${reportExperimentProjection.qualityEvidence}; matchingEvidence=${reportableExperiment.evaluation.matchingEvidence}`,
|
|
3845
|
-
`token test: ${improveRuntimeCommand}`
|
|
3846
|
-
]
|
|
3847
|
-
: qualitativeActionsSuppressed
|
|
3874
|
+
...(machineWide
|
|
3875
|
+
? []
|
|
3876
|
+
: reportableExperiment
|
|
3848
3877
|
? [
|
|
3849
|
-
`action artifacts:
|
|
3850
|
-
`
|
|
3851
|
-
`
|
|
3852
|
-
`coverage policy/config: ${artifactPaths.policyConfigDraft}`,
|
|
3853
|
-
`coverage verification: ${artifactPaths.verificationPlan}`,
|
|
3854
|
-
`coverage package: ${artifactPaths.demoPackage}`
|
|
3878
|
+
`action artifacts: preserved · canonical token test ${reportableExperiment.id} (${reportableExperiment.lifecycle})`,
|
|
3879
|
+
`token result: status=${reportableExperiment.evaluation.status}; reductionPercent=${reportExperimentProjection.reductionPercent ?? "unavailable"}; metricEvidence=${reportExperimentProjection.evidenceLabel}; quality=${reportExperimentProjection.qualityLabel}; qualityEvidence=${reportExperimentProjection.qualityEvidence}; matchingEvidence=${reportableExperiment.evaluation.matchingEvidence}`,
|
|
3880
|
+
`token test: ${improveRuntimeCommand}`
|
|
3855
3881
|
]
|
|
3856
|
-
:
|
|
3882
|
+
: qualitativeActionsSuppressed
|
|
3857
3883
|
? [
|
|
3858
|
-
`
|
|
3859
|
-
`
|
|
3860
|
-
`
|
|
3861
|
-
`
|
|
3862
|
-
`
|
|
3884
|
+
`action artifacts: suppressed · qualitative index ${reportInput.qualitativeCoverage?.status ?? "unknown"}`,
|
|
3885
|
+
`coverage artifact: ${artifactPaths.codingPrompt}`,
|
|
3886
|
+
`coverage action plan: ${artifactPaths.actionPlan}`,
|
|
3887
|
+
`coverage policy/config: ${artifactPaths.policyConfigDraft}`,
|
|
3888
|
+
`coverage verification: ${artifactPaths.verificationPlan}`,
|
|
3889
|
+
`coverage package: ${artifactPaths.demoPackage}`
|
|
3863
3890
|
]
|
|
3864
|
-
:
|
|
3891
|
+
: artifactPaths
|
|
3892
|
+
? [
|
|
3893
|
+
`apply artifact: ${artifactPaths.codingPrompt}`,
|
|
3894
|
+
`action plan: ${artifactPaths.actionPlan}`,
|
|
3895
|
+
`policy/config draft: ${artifactPaths.policyConfigDraft}`,
|
|
3896
|
+
`verification plan: ${artifactPaths.verificationPlan}`,
|
|
3897
|
+
`demo package: ${artifactPaths.demoPackage}`
|
|
3898
|
+
]
|
|
3899
|
+
: []),
|
|
3865
3900
|
reportInput.dataMode === "sample"
|
|
3866
3901
|
? `DEMO SAMPLE · illustrative cost/value evidence total: ${formatOptionalUsd(reportInput.summary.totalUsd)} · not user data`
|
|
3867
3902
|
: reportInput.dataMode === "connected_provider" &&
|
|
@@ -3875,13 +3910,20 @@ async function reportCommand(args, runtime = {}) {
|
|
|
3875
3910
|
"next:",
|
|
3876
3911
|
` open ${htmlPath} view the full report in your browser`,
|
|
3877
3912
|
` less ${markdownPath} read it in the terminal`,
|
|
3878
|
-
|
|
3879
|
-
|
|
3880
|
-
|
|
3881
|
-
|
|
3882
|
-
|
|
3883
|
-
|
|
3884
|
-
|
|
3913
|
+
machineWide
|
|
3914
|
+
// apply/improve need one exact project folder — a machine-wide
|
|
3915
|
+
// report must never point at a command that then refuses (the exact
|
|
3916
|
+
// trap this mode removes).
|
|
3917
|
+
? reportInput.dataMode === "sample"
|
|
3918
|
+
? ` cd <project> && ${actionRuntimeCommand("apply --sample")} print the non-executable demo boundary from one exact project folder`
|
|
3919
|
+
: ` cd <project> && ${actionRuntimeCommand(`apply --since-days ${sinceDays}`)} per-project action plan from one exact project folder`
|
|
3920
|
+
: reportableExperiment
|
|
3921
|
+
? ` ${improveRuntimeCommand} review canonical token test ${reportableExperiment.id}`
|
|
3922
|
+
: qualitativeActionsSuppressed
|
|
3923
|
+
? ` ${actionRuntimeCommand(`context --json --since-days ${sinceDays}`)} complete bounded qualitative evidence before any action`
|
|
3924
|
+
: reportInput.dataMode === "sample"
|
|
3925
|
+
? ` ${actionRuntimeCommand("apply --sample")} print the non-executable demo boundary`
|
|
3926
|
+
: ` ${actionRuntimeCommand(`apply --since-days ${sinceDays}`)} print the paste-ready coding-agent prompt from this exact evidence window`
|
|
3885
3927
|
].join("\n"));
|
|
3886
3928
|
}
|
|
3887
3929
|
catch (error) {
|
|
@@ -3907,19 +3949,25 @@ async function resolveReceiptPath(rootPath, out) {
|
|
|
3907
3949
|
return extname(resolved) ? resolved : `${resolved}.svg`;
|
|
3908
3950
|
}
|
|
3909
3951
|
async function reportCardCommand(args) {
|
|
3910
|
-
|
|
3911
|
-
|
|
3912
|
-
|
|
3913
|
-
|
|
3952
|
+
// 0.9.4: a broad root (home, /) runs MACHINE-WIDE — identical read-only
|
|
3953
|
+
// scanning to the bare receipt (loadInstantReadData below), SVG written to
|
|
3954
|
+
// the current directory. The card renders machine-wide content anyway, so
|
|
3955
|
+
// an exact-project requirement was incoherent here; only a bogus --path
|
|
3956
|
+
// still gets the friendly guard.
|
|
3957
|
+
const machineWide = isBroadScanRoot(args.path);
|
|
3958
|
+
if (!args.sample && !machineWide) {
|
|
3914
3959
|
const rootGuard = await guardExactProjectRoot("report-card", args.path);
|
|
3915
3960
|
if (rootGuard)
|
|
3916
3961
|
return rootGuard;
|
|
3917
3962
|
}
|
|
3918
3963
|
try {
|
|
3919
|
-
//
|
|
3920
|
-
//
|
|
3921
|
-
//
|
|
3922
|
-
|
|
3964
|
+
// Sample mode reads no workspace data and machine-wide mode reads only
|
|
3965
|
+
// the agent transcript dirs — neither scans the current directory, so
|
|
3966
|
+
// both write the receipt from wherever the user stands. Output still
|
|
3967
|
+
// goes through the safe-write/symlink checks below.
|
|
3968
|
+
const rootPath = args.sample || machineWide
|
|
3969
|
+
? resolve(args.path)
|
|
3970
|
+
: await resolveSafeScanRoot(args.path);
|
|
3923
3971
|
const { records, mode, providerCoverage, warnings } = await loadInstantReadData(args);
|
|
3924
3972
|
if (records.length === 0) {
|
|
3925
3973
|
return noEvidenceResult("report-card", warnings, args.sinceDays ?? 30);
|
|
@@ -3974,6 +4022,23 @@ async function reportCardCommand(args) {
|
|
|
3974
4022
|
* exist (a location problem is not a breadth problem, but the fix is the
|
|
3975
4023
|
* same). Returns undefined when the root is an acceptable exact project.
|
|
3976
4024
|
*/
|
|
4025
|
+
/**
|
|
4026
|
+
* True when the requested root is a machine-wide location (home, filesystem
|
|
4027
|
+
* root, a system directory, or anything containing home). report/report-card
|
|
4028
|
+
* treat this as MACHINE-WIDE MODE — the same read-only, transcript-dir
|
|
4029
|
+
* scanning the bare receipt performs — instead of refusing (0.9.4 founder
|
|
4030
|
+
* fix: the receipt's own Next pointer led from home into a refusal that
|
|
4031
|
+
* read as "the commands don't work"). Genuinely project-scoped commands
|
|
4032
|
+
* (improve, apply, verify, watch, connect, reset, …) keep the guard.
|
|
4033
|
+
*/
|
|
4034
|
+
function isBroadScanRoot(requestedPath) {
|
|
4035
|
+
const rootPath = resolve(requestedPath);
|
|
4036
|
+
const home = homedir();
|
|
4037
|
+
const guardHome = home && home.trim().length > 0
|
|
4038
|
+
? home
|
|
4039
|
+
: join(rootPath, "aibill-impossible-home-sentinel");
|
|
4040
|
+
return unsafeScanRootReason(rootPath, guardHome) !== undefined;
|
|
4041
|
+
}
|
|
3977
4042
|
async function guardExactProjectRoot(commandName, requestedPath) {
|
|
3978
4043
|
const rootPath = resolve(requestedPath);
|
|
3979
4044
|
// An unset/empty $HOME (containers) must never make the current directory
|
|
@@ -5890,6 +5955,40 @@ function applyEvidenceAcquisitionLines(input) {
|
|
|
5890
5955
|
}
|
|
5891
5956
|
return lines;
|
|
5892
5957
|
}
|
|
5958
|
+
/**
|
|
5959
|
+
* Machine-wide report input (0.9.4): the bare receipt's own data path —
|
|
5960
|
+
* loadInstantReadData over the agent transcript dirs (read-only) — rendered
|
|
5961
|
+
* through the report package. No project state is read or created; plan
|
|
5962
|
+
* detection is home-scoped metadata, exactly as the receipt reads it.
|
|
5963
|
+
*/
|
|
5964
|
+
async function buildMachineWideReportInput(args, sinceDays) {
|
|
5965
|
+
const { records, mode, providerCoverage, warnings } = await loadInstantReadData(args);
|
|
5966
|
+
if (records.length === 0) {
|
|
5967
|
+
// Same honest empty-state voice the receipt/report-card use — an empty
|
|
5968
|
+
// report file would just look broken.
|
|
5969
|
+
return { kind: "no_evidence", warnings };
|
|
5970
|
+
}
|
|
5971
|
+
const detectedPlans = await detectLocalPlans({
|
|
5972
|
+
claudeConfigPath: process.env.AI_SPEND_CLAUDE_CONFIG,
|
|
5973
|
+
codexAuthPath: process.env.AI_SPEND_CODEX_AUTH
|
|
5974
|
+
}).catch(() => []);
|
|
5975
|
+
const headlineRecords = mode === "connected"
|
|
5976
|
+
? selectProviderFinancialHeadlineRecords(records)
|
|
5977
|
+
: records;
|
|
5978
|
+
return {
|
|
5979
|
+
kind: "input",
|
|
5980
|
+
input: {
|
|
5981
|
+
generatedAt: new Date().toISOString(),
|
|
5982
|
+
summary: analyzeSpend(headlineRecords),
|
|
5983
|
+
allRecords: records,
|
|
5984
|
+
dataMode: mode === "connected" ? "connected_provider" : "local_logs",
|
|
5985
|
+
evidenceWindowDays: sinceDays,
|
|
5986
|
+
detectedPlans,
|
|
5987
|
+
...(mode === "connected" ? { providerRecords: records } : {}),
|
|
5988
|
+
...(providerCoverage ? { providerCoverage } : {})
|
|
5989
|
+
}
|
|
5990
|
+
};
|
|
5991
|
+
}
|
|
5893
5992
|
async function buildExplicitSampleReportInput(rootPath) {
|
|
5894
5993
|
const records = await loadSampleUsageData();
|
|
5895
5994
|
return {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ai-spend-agent",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.4",
|
|
4
4
|
"funding": "https://asktilden.com",
|
|
5
5
|
"description": "Local-first financial accountability CLI: Claude Code/Codex attribution, provenance, and next actions, plus experimental Gemini CLI cost evidence.",
|
|
6
6
|
"type": "module",
|
|
@@ -55,8 +55,8 @@
|
|
|
55
55
|
"prepack": "npm run build"
|
|
56
56
|
},
|
|
57
57
|
"dependencies": {
|
|
58
|
-
"@agent-finops/core": "0.9.
|
|
59
|
-
"@agent-finops/report": "0.9.
|
|
58
|
+
"@agent-finops/core": "0.9.4",
|
|
59
|
+
"@agent-finops/report": "0.9.4",
|
|
60
60
|
"yocto-spinner": "^1.2.0"
|
|
61
61
|
}
|
|
62
62
|
}
|