ai-spend-agent 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -0
- package/dist/index.d.ts +10 -1
- package/dist/index.js +708 -37
- package/dist/statuslineInstaller.d.ts +80 -0
- package/dist/statuslineInstaller.js +1100 -0
- package/dist/statuslineRuntime.d.ts +125 -0
- package/dist/statuslineRuntime.js +917 -0
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { realpathSync } from "node:fs";
|
|
3
3
|
import { mkdir, readFile, rm, stat } from "node:fs/promises";
|
|
4
|
+
import { homedir } from "node:os";
|
|
4
5
|
import { basename, dirname, extname, join, resolve } from "node:path";
|
|
5
6
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
6
|
-
import { analyzeSpend, attributeUsageRecords, buildUsageGlance, loadContextHealth, detectLocalCredentials, detectLocalPlans, redactSecrets, readSafeStateText, invalidateConnectedSpendTrustReceipt, resolveSafeScanRoot, resolveSafeStateDirectory, subscriptionPlans, unsafeScanRootReason, selectProviderFinancialHeadlineRecords, writeSafeStateText, verifyConnectedSpendTrustReceipt, verifyConnectedSourceRegistryTrustReceipt, writeConnectedSpendTrustReceipt, loadDeadContext, sampleDeadContext, latestObservedWorkingDirectory, downgradeSampleUsageEvidence, isBundledSampleUsage, loadLocalAgentUsage, loadSampleUsageData, parseUsageRecord, scanLocalUsageSignals, buildMissingSourcePrompts, confirmMapping, createProviderConnectorStub, createLocalFolderSourceRegistry, createScanAuditLog, fetchProviderUsageRecords, addApprovedSource, normalizeSourceRegistry, downgradeUntrustedSourceRegistryClaims, buildSourceStatuses, slugifySourceId, financialEvidenceForRecords, formatSourceStatuses } from "@agent-finops/core";
|
|
7
|
+
import { analyzeSpend, attributeUsageRecords, buildUsageGlance, buildActivitySnapshot, loadContextHealth, detectLocalCredentials, detectLocalPlans, redactSecrets, readSafeStateText, invalidateConnectedSpendTrustReceipt, resolveSafeScanRoot, resolveSafeStateDirectory, subscriptionPlans, unsafeScanRootReason, selectProviderFinancialHeadlineRecords, writeSafeStateText, verifyConnectedSpendTrustReceipt, verifyConnectedSourceRegistryTrustReceipt, writeConnectedSpendTrustReceipt, loadDeadContext, sampleDeadContext, latestObservedWorkingDirectory, downgradeSampleUsageEvidence, isBundledSampleUsage, loadLocalAgentUsage, loadLocalAgentFinancialUsage, loadSampleUsageData, parseUsageRecord, scanLocalUsageSignals, buildMissingSourcePrompts, confirmMapping, createProviderConnectorStub, createLocalFolderSourceRegistry, createScanAuditLog, fetchProviderUsageRecords, addApprovedSource, normalizeSourceRegistry, downgradeUntrustedSourceRegistryClaims, buildSourceStatuses, slugifySourceId, financialEvidenceForRecords, formatSourceStatuses, readActivitySnapshot, recordActivitySnapshotRefreshFailure, sourceStatusDefinitions, writeActivitySnapshot } from "@agent-finops/core";
|
|
8
|
+
import { StatuslineInstallerError, installClaudeStatusline, uninstallClaudeStatusline } from "./statuslineInstaller.js";
|
|
9
|
+
import { readStatuslineCache, renderStatusline } from "./statuslineRuntime.js";
|
|
7
10
|
import { generateActionPlanMarkdown, generateApplyArtifactMarkdown, generateDemoPackageMarkdown, generateHtmlReport, generateMarkdownReport, generatePlainEnglishSummary, generatePolicyConfigDraftMarkdown, generateReportCardCaption, generateReportCardSvg, generateVerificationPlanMarkdown, groupByDimensions } from "@agent-finops/report";
|
|
8
|
-
export async function runCli(argv = process.argv.slice(2)) {
|
|
11
|
+
export async function runCli(argv = process.argv.slice(2), runtime = {}) {
|
|
9
12
|
if (argv.includes("--version") || argv.includes("-v")) {
|
|
10
13
|
return ok(await cliVersion());
|
|
11
14
|
}
|
|
@@ -34,7 +37,10 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
34
37
|
return resetCommand(args);
|
|
35
38
|
}
|
|
36
39
|
if (args.command === "init") {
|
|
37
|
-
return initCommand(args);
|
|
40
|
+
return initCommand(args, runtime);
|
|
41
|
+
}
|
|
42
|
+
if (args.command === "statusline") {
|
|
43
|
+
return statuslineCommand(args, runtime);
|
|
38
44
|
}
|
|
39
45
|
if (args.command === "scan") {
|
|
40
46
|
return scanCommand(args);
|
|
@@ -323,7 +329,7 @@ function quickstartNextSteps(mode, detected) {
|
|
|
323
329
|
steps.push("Need team reconciliation, allocation, budgets, and approvals? Workspace design partners: https://ai-spend-agent.vercel.app");
|
|
324
330
|
return steps;
|
|
325
331
|
}
|
|
326
|
-
async function readPersistedSpend(rootPath) {
|
|
332
|
+
async function readPersistedSpend(rootPath, options = {}) {
|
|
327
333
|
const stateDir = join(rootPath, ".ai-spend-agent");
|
|
328
334
|
try {
|
|
329
335
|
const exactSpendContents = await readSafeStateText(stateDir, "spend.json");
|
|
@@ -340,6 +346,9 @@ async function readPersistedSpend(rootPath) {
|
|
|
340
346
|
const records = mode === "sample" || mode === undefined
|
|
341
347
|
? downgradeSampleUsageEvidence(parsedRecords)
|
|
342
348
|
: parsedRecords;
|
|
349
|
+
if (spend.checkedAt !== undefined && !validIsoString(spend.checkedAt)) {
|
|
350
|
+
throw new Error("persisted spend checkedAt must be an ISO timestamp");
|
|
351
|
+
}
|
|
343
352
|
const providerCoverage = persistedProviderCoverage(spend.accounting);
|
|
344
353
|
const connectedTrust = mode === "connected_provider"
|
|
345
354
|
? await verifyConnectedSpendTrustReceipt(rootPath, exactSpendContents)
|
|
@@ -347,12 +356,16 @@ async function readPersistedSpend(rootPath) {
|
|
|
347
356
|
return {
|
|
348
357
|
mode,
|
|
349
358
|
records,
|
|
359
|
+
...(typeof spend.checkedAt === "string" ? { checkedAt: spend.checkedAt } : {}),
|
|
350
360
|
...(providerCoverage ? { providerCoverage } : {}),
|
|
351
361
|
...(isPlainObject(spend.accounting) ? { accounting: spend.accounting } : {}),
|
|
352
362
|
...(connectedTrust ? { connectedTrust } : {})
|
|
353
363
|
};
|
|
354
364
|
}
|
|
355
|
-
catch {
|
|
365
|
+
catch (error) {
|
|
366
|
+
if (options.strict && !isNodeError(error, "ENOENT")) {
|
|
367
|
+
throw new Error("Existing .ai-spend-agent/spend.json is invalid or unsafe; it was preserved and init stopped.");
|
|
368
|
+
}
|
|
356
369
|
return undefined;
|
|
357
370
|
}
|
|
358
371
|
}
|
|
@@ -991,44 +1004,668 @@ async function resetCommand(args) {
|
|
|
991
1004
|
"next run will re-read your real local agent logs (or demo sample if none)."
|
|
992
1005
|
].join("\n"));
|
|
993
1006
|
}
|
|
994
|
-
async function
|
|
995
|
-
const
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1007
|
+
async function statuslineCommand(args, runtime) {
|
|
1008
|
+
const action = args.statuslineAction;
|
|
1009
|
+
if (action === undefined) {
|
|
1010
|
+
const cache = await readStatuslineCache({
|
|
1011
|
+
cacheDirectory: process.env.AIBILL_CACHE_DIR,
|
|
1012
|
+
homeDirectory: runtime.homeDirectory
|
|
1013
|
+
});
|
|
1014
|
+
return ok(renderStatusline(cache, {
|
|
1015
|
+
now: runtime.statuslineNow,
|
|
1016
|
+
columns: runtime.statuslineColumns,
|
|
1017
|
+
timeZone: runtime.statuslineTimeZone
|
|
1018
|
+
}));
|
|
1019
|
+
}
|
|
1020
|
+
if (action === "install")
|
|
1021
|
+
return installStatuslineCommand(args, runtime);
|
|
1022
|
+
if (action === "uninstall")
|
|
1023
|
+
return uninstallStatuslineCommand(runtime);
|
|
1024
|
+
if (action === "refresh")
|
|
1025
|
+
return refreshStatuslineCommand(args, runtime);
|
|
1026
|
+
return {
|
|
1027
|
+
exitCode: 1,
|
|
1028
|
+
stdout: "",
|
|
1029
|
+
stderr: `Unknown statusline action: ${sanitizeSecretishError(action)}\n` +
|
|
1030
|
+
"Use: aibill statusline [refresh|install|uninstall]"
|
|
1031
|
+
};
|
|
1032
|
+
}
|
|
1033
|
+
async function packagedStatuslineRunner(runtime) {
|
|
1034
|
+
if (runtime.statuslineRunnerContents !== undefined) {
|
|
1035
|
+
return runtime.statuslineRunnerContents;
|
|
1036
|
+
}
|
|
1037
|
+
return readFile(fileURLToPath(new URL("./statuslineRuntime.js", import.meta.url)));
|
|
1038
|
+
}
|
|
1039
|
+
async function installStatuslineCommand(args, runtime) {
|
|
1040
|
+
try {
|
|
1041
|
+
const result = await installClaudeStatusline({
|
|
1042
|
+
homeDir: runtime.homeDirectory ?? homedir(),
|
|
1043
|
+
cwd: resolve(args.path),
|
|
1044
|
+
runnerContents: await packagedStatuslineRunner(runtime),
|
|
1045
|
+
replace: args.replaceStatusline
|
|
1046
|
+
});
|
|
1047
|
+
return ok([
|
|
1048
|
+
result.action === "unchanged"
|
|
1049
|
+
? "aibill statusline is already installed in Claude user settings."
|
|
1050
|
+
: "aibill statusline installed in Claude user settings.",
|
|
1051
|
+
"Claude Code: run /status to verify the active setting and every managed source.",
|
|
1052
|
+
"The renderer reads only the private aibill cache; it never reads Claude's session stdin as financial evidence."
|
|
1053
|
+
].join("\n"));
|
|
1054
|
+
}
|
|
1055
|
+
catch (error) {
|
|
1056
|
+
return statuslineInstallerFailure("install", error);
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
async function uninstallStatuslineCommand(runtime) {
|
|
1060
|
+
try {
|
|
1061
|
+
const result = await uninstallClaudeStatusline({
|
|
1062
|
+
homeDir: runtime.homeDirectory ?? homedir(),
|
|
1063
|
+
cwd: process.cwd()
|
|
1064
|
+
});
|
|
1065
|
+
return ok([
|
|
1066
|
+
result.statusLineAction === "restored-prior"
|
|
1067
|
+
? "aibill statusline was removed and the prior Claude user statusLine was restored."
|
|
1068
|
+
: "aibill statusline was removed from Claude user settings.",
|
|
1069
|
+
{
|
|
1070
|
+
removed: "The owned standalone runner was removed.",
|
|
1071
|
+
restored: "The exact pre-installation runner was restored.",
|
|
1072
|
+
"preserved-modified": "The modified runner was preserved because it was no longer owned.",
|
|
1073
|
+
"already-missing": "The owned runner was already missing; no runner file was removed."
|
|
1074
|
+
}[result.runnerAction],
|
|
1075
|
+
...result.warnings
|
|
1076
|
+
].join("\n"));
|
|
1077
|
+
}
|
|
1078
|
+
catch (error) {
|
|
1079
|
+
return statuslineInstallerFailure("uninstall", error);
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
function statuslineInstallerFailure(action, error) {
|
|
1083
|
+
const installerError = error instanceof StatuslineInstallerError
|
|
1084
|
+
? error
|
|
1085
|
+
: new StatuslineInstallerError("unsafe-settings-file", `The local filesystem operation failed safely${safeFileSystemErrorCode(error)}; no successful settings change was claimed.`);
|
|
1086
|
+
const replacement = installerError.code === "statusline-conflict"
|
|
1087
|
+
? "\nTo replace an existing status line explicitly: aibill statusline install --replace"
|
|
1088
|
+
: "";
|
|
1089
|
+
return {
|
|
1090
|
+
exitCode: 1,
|
|
1091
|
+
stdout: "",
|
|
1092
|
+
stderr: [
|
|
1093
|
+
`aibill statusline ${action} stopped safely: ${sanitizeSecretishError(installerError.message)}`,
|
|
1094
|
+
"No successful settings change was claimed. Resolve the conflict, then verify active sources with /status in Claude Code.",
|
|
1095
|
+
replacement.trim()
|
|
1096
|
+
].filter(Boolean).join("\n")
|
|
1097
|
+
};
|
|
1098
|
+
}
|
|
1099
|
+
function safeFileSystemErrorCode(error) {
|
|
1100
|
+
if (typeof error !== "object" || error === null || !("code" in error))
|
|
1101
|
+
return "";
|
|
1102
|
+
const code = String(error.code ?? "");
|
|
1103
|
+
return /^[A-Z][A-Z0-9_]{1,31}$/.test(code) ? ` (${code})` : "";
|
|
1104
|
+
}
|
|
1105
|
+
async function initCommand(args, runtime = {}) {
|
|
1106
|
+
if (args.sample) {
|
|
1107
|
+
return {
|
|
1108
|
+
exitCode: 1,
|
|
1109
|
+
stdout: "",
|
|
1110
|
+
stderr: "aibill init only initializes from real local evidence; --sample was not used and no state or cache was changed. Run `npx aibill --sample` for the illustrative demo."
|
|
1111
|
+
};
|
|
1112
|
+
}
|
|
1113
|
+
let detectedPlanOverride;
|
|
1114
|
+
if (args.plan) {
|
|
1115
|
+
const override = planOverrideFromFlag(args.plan);
|
|
1116
|
+
if (!override) {
|
|
1117
|
+
return {
|
|
1118
|
+
exitCode: 1,
|
|
1119
|
+
stdout: "",
|
|
1120
|
+
stderr: `Unknown --plan "${args.plan}". Valid plans: ${subscriptionPlans.map((plan) => plan.id).join(", ")}`
|
|
1121
|
+
};
|
|
1122
|
+
}
|
|
1123
|
+
detectedPlanOverride = [override];
|
|
1124
|
+
}
|
|
1125
|
+
const rootPath = await resolveSafeScanRoot(args.path);
|
|
1126
|
+
const cacheDirectory = process.env.AIBILL_CACHE_DIR;
|
|
1127
|
+
await preflightInitCache(cacheDirectory);
|
|
1128
|
+
let stateDir;
|
|
1129
|
+
let stateDirectoryExists = true;
|
|
1130
|
+
try {
|
|
1131
|
+
stateDir = await resolveSafeStateDirectory(rootPath);
|
|
1132
|
+
}
|
|
1133
|
+
catch (error) {
|
|
1134
|
+
if (!isNodeError(error, "ENOENT"))
|
|
1135
|
+
throw error;
|
|
1136
|
+
stateDirectoryExists = false;
|
|
1137
|
+
stateDir = join(rootPath, ".ai-spend-agent");
|
|
1138
|
+
}
|
|
1139
|
+
const statePreparedAt = new Date();
|
|
1140
|
+
// Preflight every existing project file before any mutation. Valid files are
|
|
1141
|
+
// left byte-for-byte alone so a repeated init cannot erase connector
|
|
1142
|
+
// configuration, audit history, provider trust, spend state, or unknown
|
|
1143
|
+
// forward-compatible fields. The manifest remains the completion marker and
|
|
1144
|
+
// is written last.
|
|
1145
|
+
const existingManifest = stateDirectoryExists
|
|
1146
|
+
? await readInitJsonObject(stateDir, "manifest.json", { allowMissing: true })
|
|
1147
|
+
: undefined;
|
|
1148
|
+
const existingRegistry = stateDirectoryExists
|
|
1149
|
+
? await readInitJsonObject(stateDir, "sources.json", { allowMissing: true })
|
|
1150
|
+
: undefined;
|
|
1151
|
+
const existingAuditLog = stateDirectoryExists
|
|
1152
|
+
? await readInitJsonObject(stateDir, "audit-log.json", { allowMissing: true })
|
|
1153
|
+
: undefined;
|
|
1154
|
+
if (existingRegistry)
|
|
1155
|
+
normalizeSourceRegistry(existingRegistry);
|
|
1156
|
+
validateInitAuditLog(existingAuditLog);
|
|
1157
|
+
const refresh = await collectAndPublishActivitySnapshot({
|
|
1158
|
+
rootPath,
|
|
1159
|
+
cacheDirectory,
|
|
1160
|
+
detectedPlanOverride
|
|
1161
|
+
});
|
|
1162
|
+
const { asOf, activitySnapshot, logs, detectedPlans, trustedProviderRecords, persisted, scanError, cacheStatus } = refresh;
|
|
1163
|
+
if (!stateDirectoryExists) {
|
|
1164
|
+
stateDir = await resolveSafeStateDirectory(rootPath, { create: true });
|
|
1165
|
+
}
|
|
1166
|
+
await preserveOrCreateInitRegistry(stateDir, rootPath, statePreparedAt, existingRegistry);
|
|
1167
|
+
await preserveOrCreateInitAuditLog(stateDir, rootPath, statePreparedAt, existingAuditLog);
|
|
1168
|
+
const manifest = buildInitManifest(existingManifest, asOf);
|
|
1169
|
+
await writeSafeStateText(stateDir, "manifest.json", `${JSON.stringify(manifest, null, 2)}\n`);
|
|
1170
|
+
const receipt = formatInitReceipt({
|
|
1171
|
+
rootPath,
|
|
1172
|
+
asOf,
|
|
1173
|
+
activitySnapshot,
|
|
1174
|
+
logs,
|
|
1175
|
+
detectedPlans,
|
|
1176
|
+
trustedProviderRecords,
|
|
1177
|
+
providerCoverage: persisted?.providerCoverage,
|
|
1178
|
+
trustedProviderState: persisted?.mode === "connected_provider" && persisted.connectedTrust?.trusted === true,
|
|
1179
|
+
untrustedProviderState: persisted?.mode === "connected_provider" && persisted.connectedTrust?.trusted !== true,
|
|
1180
|
+
scanError,
|
|
1181
|
+
cacheStatus
|
|
1182
|
+
});
|
|
1183
|
+
if (!args.statusline) {
|
|
1184
|
+
return ok(`${receipt}\noptional Claude Code status line: npx aibill statusline install`);
|
|
1185
|
+
}
|
|
1186
|
+
const installation = await installStatuslineCommand(args, runtime);
|
|
1187
|
+
return installation.exitCode === 0
|
|
1188
|
+
? ok(`${receipt}\n${installation.stdout}`)
|
|
1189
|
+
: {
|
|
1190
|
+
exitCode: installation.exitCode,
|
|
1191
|
+
stdout: receipt,
|
|
1192
|
+
stderr: installation.stderr
|
|
1193
|
+
};
|
|
1194
|
+
}
|
|
1195
|
+
async function collectAndPublishActivitySnapshot(input) {
|
|
1196
|
+
// One attempt anchor binds every rolling window and cache ordering decision.
|
|
1197
|
+
const asOf = new Date();
|
|
1198
|
+
const planPromise = input.detectedPlanOverride
|
|
1199
|
+
? Promise.resolve(input.detectedPlanOverride)
|
|
1200
|
+
: detectLocalPlans({
|
|
1201
|
+
claudeConfigPath: process.env.AI_SPEND_CLAUDE_CONFIG,
|
|
1202
|
+
codexAuthPath: process.env.AI_SPEND_CODEX_AUTH
|
|
1203
|
+
}).catch(() => []);
|
|
1204
|
+
// Attach the rejection handler immediately so a failed transcript scan can
|
|
1205
|
+
// never leave an unhandled persisted-state rejection behind.
|
|
1206
|
+
const persistedPromise = readPersistedSpend(input.rootPath, { strict: true }).then((persisted) => ({ persisted }), (error) => ({ error }));
|
|
1207
|
+
let logs;
|
|
1208
|
+
let scanError;
|
|
1209
|
+
try {
|
|
1210
|
+
logs = await loadLocalAgentFinancialUsage({
|
|
1211
|
+
claudeProjectsDir: process.env.AI_SPEND_CLAUDE_LOGS_DIR,
|
|
1212
|
+
codexSessionsDir: process.env.AI_SPEND_CODEX_LOGS_DIR,
|
|
1213
|
+
sinceIso: sinceIsoForDays(30, asOf)
|
|
1214
|
+
});
|
|
1215
|
+
}
|
|
1216
|
+
catch (error) {
|
|
1217
|
+
scanError = sanitizeSecretishError(error instanceof Error ? error.message : String(error));
|
|
1218
|
+
}
|
|
1219
|
+
const [detectedPlans, persistedResult] = await Promise.all([planPromise, persistedPromise]);
|
|
1220
|
+
if ("error" in persistedResult)
|
|
1221
|
+
throw persistedResult.error;
|
|
1222
|
+
const persisted = persistedResult.persisted;
|
|
1223
|
+
const trustedProviderRecords = persisted?.mode === "connected_provider" &&
|
|
1224
|
+
persisted.connectedTrust?.trusted === true
|
|
1225
|
+
? selectProviderFinancialHeadlineRecords(persisted.records)
|
|
1226
|
+
: [];
|
|
1227
|
+
let activitySnapshot;
|
|
1228
|
+
let cacheStatus;
|
|
1229
|
+
const structuredSourceFailure = logs !== undefined &&
|
|
1230
|
+
logs.sourceScans.some((scan) => scan.directoryStatus === "unreadable") &&
|
|
1231
|
+
!logs.sourceScans.some((scan) => scan.directoryStatus === "readable");
|
|
1232
|
+
if (logs && !structuredSourceFailure) {
|
|
1233
|
+
let refreshErrorCode = "invalid_evidence";
|
|
1234
|
+
try {
|
|
1235
|
+
activitySnapshot = buildActivitySnapshot({
|
|
1236
|
+
asOf: asOf.toISOString(),
|
|
1237
|
+
generatedAt: new Date().toISOString(),
|
|
1238
|
+
records: [...logs.records, ...trustedProviderRecords],
|
|
1239
|
+
calls: logs.calls,
|
|
1240
|
+
detectedPlans,
|
|
1241
|
+
sourceScans: logs.sourceScans,
|
|
1242
|
+
trustedProviderRecordIds: trustedProviderRecords.map((record) => record.id),
|
|
1243
|
+
billedOverageRecordIds: [],
|
|
1244
|
+
providerCoverage: initProviderCoverage(trustedProviderRecords, persisted?.mode === "connected_provider" && persisted.connectedTrust?.trusted === true
|
|
1245
|
+
? trustedAccountingMap(persisted.accounting, "coverageByProvider")
|
|
1246
|
+
: {}, persisted?.mode === "connected_provider" && persisted.connectedTrust?.trusted === true
|
|
1247
|
+
? trustedAccountingMap(persisted.accounting, "checkedAtByProvider")
|
|
1248
|
+
: {}, persisted?.mode === "connected_provider" && persisted.connectedTrust?.trusted === true
|
|
1249
|
+
? trustedAccountingMap(persisted.accounting, "coverageIntervalsByProvider")
|
|
1250
|
+
: {}, persisted?.mode === "connected_provider" && persisted.connectedTrust?.trusted === true
|
|
1251
|
+
? persisted.checkedAt ?? persisted.connectedTrust.trustedAt
|
|
1252
|
+
: undefined),
|
|
1253
|
+
sampleData: false
|
|
1254
|
+
});
|
|
1255
|
+
refreshErrorCode = "cache_write_failed";
|
|
1256
|
+
const written = await writeActivitySnapshot(activitySnapshot, {
|
|
1257
|
+
cacheDirectory: input.cacheDirectory
|
|
1258
|
+
});
|
|
1259
|
+
cacheStatus = written.status === "written" ? "refreshed" : "kept newer snapshot";
|
|
1260
|
+
}
|
|
1261
|
+
catch {
|
|
1262
|
+
scanError = refreshErrorCode === "invalid_evidence"
|
|
1263
|
+
? "the observed evidence could not produce a valid activity snapshot"
|
|
1264
|
+
: "the private activity cache could not be updated";
|
|
1265
|
+
const failed = await recordActivitySnapshotRefreshFailure(asOf.toISOString(), refreshErrorCode, { cacheDirectory: input.cacheDirectory });
|
|
1266
|
+
activitySnapshot = failed.snapshot;
|
|
1267
|
+
cacheStatus = "refresh failed";
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
else {
|
|
1271
|
+
const failed = await recordActivitySnapshotRefreshFailure(asOf.toISOString(), structuredSourceFailure ? "source_unreadable" : "scan_failed", { cacheDirectory: input.cacheDirectory });
|
|
1272
|
+
activitySnapshot = failed.snapshot;
|
|
1273
|
+
cacheStatus = "refresh failed";
|
|
1274
|
+
}
|
|
1275
|
+
return {
|
|
1276
|
+
asOf,
|
|
1277
|
+
activitySnapshot,
|
|
1278
|
+
logs,
|
|
1279
|
+
detectedPlans,
|
|
1280
|
+
trustedProviderRecords,
|
|
1281
|
+
persisted,
|
|
1282
|
+
scanError,
|
|
1283
|
+
cacheStatus
|
|
1284
|
+
};
|
|
1285
|
+
}
|
|
1286
|
+
async function refreshStatuslineCommand(args, runtime) {
|
|
1287
|
+
if (args.sample) {
|
|
1288
|
+
return {
|
|
1289
|
+
exitCode: 1,
|
|
1290
|
+
stdout: "",
|
|
1291
|
+
stderr: "aibill statusline refresh only uses real local evidence; --sample was rejected and the cache was not changed."
|
|
1292
|
+
};
|
|
1293
|
+
}
|
|
1294
|
+
let detectedPlanOverride;
|
|
1295
|
+
if (args.plan) {
|
|
1296
|
+
const override = planOverrideFromFlag(args.plan);
|
|
1297
|
+
if (!override) {
|
|
1298
|
+
return {
|
|
1299
|
+
exitCode: 1,
|
|
1300
|
+
stdout: "",
|
|
1301
|
+
stderr: `Unknown --plan "${args.plan}". Valid plans: ${subscriptionPlans.map((plan) => plan.id).join(", ")}`
|
|
1302
|
+
};
|
|
1303
|
+
}
|
|
1304
|
+
detectedPlanOverride = [override];
|
|
1305
|
+
}
|
|
1306
|
+
const rootPath = await resolveSafeScanRoot(args.path);
|
|
1307
|
+
const cacheDirectory = process.env.AIBILL_CACHE_DIR;
|
|
1308
|
+
await preflightInitCache(cacheDirectory);
|
|
1309
|
+
let refresh;
|
|
1310
|
+
try {
|
|
1311
|
+
refresh = await collectAndPublishActivitySnapshot({
|
|
1312
|
+
rootPath,
|
|
1313
|
+
cacheDirectory,
|
|
1314
|
+
detectedPlanOverride
|
|
1315
|
+
});
|
|
1316
|
+
}
|
|
1317
|
+
catch (error) {
|
|
1318
|
+
const attemptedAt = new Date().toISOString();
|
|
1319
|
+
await recordActivitySnapshotRefreshFailure(attemptedAt, "invalid_evidence", {
|
|
1320
|
+
cacheDirectory
|
|
1321
|
+
}).catch(() => undefined);
|
|
1322
|
+
const cache = await readStatuslineCache({
|
|
1323
|
+
cacheDirectory,
|
|
1324
|
+
homeDirectory: runtime.homeDirectory
|
|
1325
|
+
});
|
|
1326
|
+
return {
|
|
1327
|
+
exitCode: 1,
|
|
1328
|
+
stdout: renderStatusline(cache, {
|
|
1329
|
+
now: runtime.statuslineNow,
|
|
1330
|
+
columns: runtime.statuslineColumns,
|
|
1331
|
+
timeZone: runtime.statuslineTimeZone
|
|
1332
|
+
}),
|
|
1333
|
+
stderr: `aibill statusline refresh failed safely: ${sanitizeSecretishError(error instanceof Error ? error.message : String(error))}`
|
|
1334
|
+
};
|
|
1335
|
+
}
|
|
1336
|
+
const cache = await readStatuslineCache({
|
|
1337
|
+
cacheDirectory,
|
|
1338
|
+
homeDirectory: runtime.homeDirectory
|
|
1339
|
+
});
|
|
1340
|
+
const line = renderStatusline(cache, {
|
|
1341
|
+
now: runtime.statuslineNow,
|
|
1342
|
+
columns: runtime.statuslineColumns,
|
|
1343
|
+
timeZone: runtime.statuslineTimeZone
|
|
1344
|
+
});
|
|
1345
|
+
return refresh.cacheStatus === "refresh failed"
|
|
1346
|
+
? {
|
|
1347
|
+
exitCode: 1,
|
|
1348
|
+
stdout: line,
|
|
1349
|
+
stderr: `aibill statusline refresh failed safely: ${refresh.scanError ?? "local evidence was unavailable"}`
|
|
1350
|
+
}
|
|
1351
|
+
: ok(line);
|
|
1352
|
+
}
|
|
1353
|
+
async function preflightInitCache(cacheDirectory) {
|
|
1354
|
+
const existing = await readActivitySnapshot({ cacheDirectory });
|
|
1355
|
+
if (existing.status !== "error")
|
|
1356
|
+
return;
|
|
1357
|
+
throw new Error(`Existing private activity cache is ${existing.code.replaceAll("_", " ")}; ` +
|
|
1358
|
+
"it was preserved and init stopped. Remove the cache explicitly before rebuilding it.");
|
|
1359
|
+
}
|
|
1360
|
+
async function preserveOrCreateInitRegistry(stateDir, rootPath, asOf, existing) {
|
|
1361
|
+
if (existing) {
|
|
1362
|
+
// Leave the valid contract byte-for-byte alone. Rewriting would drop
|
|
1363
|
+
// unknown future fields and could invalidate a connected-state receipt
|
|
1364
|
+
// bound to the exact source-registry bytes.
|
|
1365
|
+
return;
|
|
1366
|
+
}
|
|
1367
|
+
await writeSafeStateText(stateDir, "sources.json", `${JSON.stringify(createLocalFolderSourceRegistry(rootPath, asOf), null, 2)}\n`);
|
|
1368
|
+
}
|
|
1369
|
+
async function preserveOrCreateInitAuditLog(stateDir, rootPath, asOf, existing) {
|
|
1370
|
+
if (existing)
|
|
1371
|
+
return;
|
|
1372
|
+
const timestamp = asOf.toISOString();
|
|
1373
|
+
await writeSafeStateText(stateDir, "audit-log.json", `${JSON.stringify(createScanAuditLog([{
|
|
1374
|
+
timestamp,
|
|
1375
|
+
action: "source_registered",
|
|
1376
|
+
sourceId: "local-root",
|
|
1377
|
+
path: rootPath,
|
|
1378
|
+
detail: "Explicit local folder source approved during init."
|
|
1379
|
+
}]), null, 2)}\n`);
|
|
1380
|
+
}
|
|
1381
|
+
function validateInitAuditLog(existing) {
|
|
1382
|
+
if (existing && (existing.version !== 1 || existing.localOnly !== true || !Array.isArray(existing.events))) {
|
|
1383
|
+
throw new Error("Invalid local audit log: expected the canonical local-only audit shape.");
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
async function readInitJsonObject(stateDir, fileName, options) {
|
|
1387
|
+
let contents;
|
|
1388
|
+
try {
|
|
1389
|
+
contents = await readSafeStateText(stateDir, fileName);
|
|
1390
|
+
}
|
|
1391
|
+
catch (error) {
|
|
1392
|
+
if (options.allowMissing && isNodeError(error, "ENOENT"))
|
|
1393
|
+
return undefined;
|
|
1394
|
+
throw error;
|
|
1395
|
+
}
|
|
1396
|
+
let parsed;
|
|
1397
|
+
try {
|
|
1398
|
+
parsed = JSON.parse(contents);
|
|
1399
|
+
}
|
|
1400
|
+
catch {
|
|
1401
|
+
throw new Error(`Invalid local ${fileName}: expected JSON; the existing file was preserved.`);
|
|
1402
|
+
}
|
|
1403
|
+
if (!isPlainObject(parsed)) {
|
|
1404
|
+
throw new Error(`Invalid local ${fileName}: expected a JSON object; the existing file was preserved.`);
|
|
1405
|
+
}
|
|
1406
|
+
return parsed;
|
|
1407
|
+
}
|
|
1408
|
+
function buildInitManifest(existing, asOf) {
|
|
1409
|
+
const timestamp = asOf.toISOString();
|
|
1410
|
+
return {
|
|
1411
|
+
...(existing ?? {}),
|
|
1000
1412
|
product: "aibill",
|
|
1001
|
-
mode: "local-first
|
|
1413
|
+
mode: "local-first",
|
|
1002
1414
|
cloudUpload: false,
|
|
1003
1415
|
cronJobsEnabled: false,
|
|
1004
1416
|
redactionPolicy: "secrets are never printed; detected values are written only as [REDACTED]",
|
|
1005
1417
|
sourceRegistry: "sources.json",
|
|
1006
1418
|
auditLog: "audit-log.json",
|
|
1419
|
+
backfillWindowDays: 30,
|
|
1420
|
+
statusSnapshot: {
|
|
1421
|
+
schema: "aibill.activity_snapshot/v1",
|
|
1422
|
+
storage: "private external cache",
|
|
1423
|
+
networkUploaded: false
|
|
1424
|
+
},
|
|
1425
|
+
initializedAt: typeof existing?.initializedAt === "string" ? existing.initializedAt : timestamp,
|
|
1426
|
+
lastInitializedAt: timestamp,
|
|
1007
1427
|
nextCommands: [
|
|
1008
|
-
"npx aibill
|
|
1009
|
-
|
|
1010
|
-
|
|
1428
|
+
"npx aibill",
|
|
1429
|
+
"npx aibill doctor --sources",
|
|
1430
|
+
"npx aibill report"
|
|
1011
1431
|
]
|
|
1432
|
+
};
|
|
1433
|
+
}
|
|
1434
|
+
function initProviderCoverage(records, coverageByProvider, checkedAtByProvider, coverageIntervalsByProvider, fallbackCheckedAt) {
|
|
1435
|
+
const providerGroups = new Map();
|
|
1436
|
+
for (const record of records) {
|
|
1437
|
+
const rawProvider = record.source.provider;
|
|
1438
|
+
const provider = activitySnapshotProvider(rawProvider);
|
|
1439
|
+
const group = providerGroups.get(provider) ?? {
|
|
1440
|
+
rawProviders: new Set(),
|
|
1441
|
+
records: []
|
|
1442
|
+
};
|
|
1443
|
+
group.rawProviders.add(rawProvider);
|
|
1444
|
+
group.records.push(record);
|
|
1445
|
+
providerGroups.set(provider, group);
|
|
1446
|
+
}
|
|
1447
|
+
for (const rawProvider of new Set([
|
|
1448
|
+
...Object.keys(coverageByProvider),
|
|
1449
|
+
...Object.keys(checkedAtByProvider),
|
|
1450
|
+
...Object.keys(coverageIntervalsByProvider)
|
|
1451
|
+
])) {
|
|
1452
|
+
const provider = activitySnapshotProvider(rawProvider);
|
|
1453
|
+
const group = providerGroups.get(provider) ?? {
|
|
1454
|
+
rawProviders: new Set(),
|
|
1455
|
+
records: []
|
|
1456
|
+
};
|
|
1457
|
+
group.rawProviders.add(rawProvider);
|
|
1458
|
+
providerGroups.set(provider, group);
|
|
1459
|
+
}
|
|
1460
|
+
return [...providerGroups.entries()].map(([provider, group]) => {
|
|
1461
|
+
const coverages = [...group.rawProviders]
|
|
1462
|
+
.map((rawProvider) => coverageByProvider[rawProvider] ?? coverageByProvider[provider])
|
|
1463
|
+
.filter((coverage) => coverage !== undefined);
|
|
1464
|
+
const status = coverages.length === group.rawProviders.size && coverages.every((coverage) => coverage === "complete")
|
|
1465
|
+
? "complete"
|
|
1466
|
+
: coverages.some((coverage) => coverage === "complete" || coverage === "partial")
|
|
1467
|
+
? "partial"
|
|
1468
|
+
: "unavailable";
|
|
1469
|
+
const checkedValues = [...group.rawProviders]
|
|
1470
|
+
.map((rawProvider) => checkedAtByProvider[rawProvider] ?? checkedAtByProvider[provider])
|
|
1471
|
+
.filter((value) => validIsoString(value));
|
|
1472
|
+
const receiptBoundCheckedAt = checkedValues.length === group.rawProviders.size
|
|
1473
|
+
? checkedValues.sort()[0]
|
|
1474
|
+
: providerGroups.size === 1 && validIsoString(fallbackCheckedAt)
|
|
1475
|
+
? fallbackCheckedAt
|
|
1476
|
+
: undefined;
|
|
1477
|
+
const latestEvidenceAt = group.records
|
|
1478
|
+
.map((record) => record.timestamp)
|
|
1479
|
+
.filter(validIsoString)
|
|
1480
|
+
.sort()
|
|
1481
|
+
.at(-1);
|
|
1482
|
+
const intervals = [...group.rawProviders]
|
|
1483
|
+
.map((rawProvider) => coverageIntervalsByProvider[rawProvider] ?? coverageIntervalsByProvider[provider])
|
|
1484
|
+
.filter(validProviderCoverageInterval);
|
|
1485
|
+
const coverageStart = intervals.length === group.rawProviders.size
|
|
1486
|
+
? intervals.map((interval) => interval.coverageStart).sort().at(-1)
|
|
1487
|
+
: undefined;
|
|
1488
|
+
const coverageEnd = intervals.length === group.rawProviders.size
|
|
1489
|
+
? intervals.map((interval) => interval.coverageEnd).sort()[0]
|
|
1490
|
+
: undefined;
|
|
1491
|
+
return {
|
|
1492
|
+
provider,
|
|
1493
|
+
status,
|
|
1494
|
+
validationCoverage: sourceStatusDefinitions.find((definition) => definition.id === provider)?.validationCoverage
|
|
1495
|
+
?? "untested",
|
|
1496
|
+
...(receiptBoundCheckedAt ? { checkedAt: receiptBoundCheckedAt } : {}),
|
|
1497
|
+
...(receiptBoundCheckedAt && latestEvidenceAt ? { latestEvidenceAt } : {}),
|
|
1498
|
+
...(receiptBoundCheckedAt && coverageStart && coverageEnd && Date.parse(coverageStart) <= Date.parse(coverageEnd)
|
|
1499
|
+
? { coverageStart, coverageEnd }
|
|
1500
|
+
: {})
|
|
1501
|
+
};
|
|
1502
|
+
}).sort((left, right) => left.provider.localeCompare(right.provider));
|
|
1503
|
+
}
|
|
1504
|
+
function validProviderCoverageInterval(value) {
|
|
1505
|
+
return isPlainObject(value) &&
|
|
1506
|
+
validIsoString(value.coverageStart) &&
|
|
1507
|
+
validIsoString(value.coverageEnd) &&
|
|
1508
|
+
Date.parse(value.coverageStart) <= Date.parse(value.coverageEnd);
|
|
1509
|
+
}
|
|
1510
|
+
function activitySnapshotProvider(provider) {
|
|
1511
|
+
if (provider === "openai" || provider === "anthropic" || provider === "cursor" || provider === "github-copilot") {
|
|
1512
|
+
return provider;
|
|
1513
|
+
}
|
|
1514
|
+
return "other";
|
|
1515
|
+
}
|
|
1516
|
+
function formatInitReceipt(input) {
|
|
1517
|
+
const records = input.logs?.records ?? [];
|
|
1518
|
+
const pricedRecords = records.filter((record) => typeof record.amountUsd === "number");
|
|
1519
|
+
const sourceFailures = (input.logs?.sourceScans ?? []).some((scan) => scan.directoryStatus === "unreadable") ||
|
|
1520
|
+
(input.logs?.diagnostics ?? []).some((diagnostic) => diagnostic.severity === "error");
|
|
1521
|
+
const receiptLines = input.scanError || sourceFailures && records.length === 0
|
|
1522
|
+
? ["API-equivalent usage value: unavailable — the local scan could not prove an empty result"]
|
|
1523
|
+
: input.activitySnapshot
|
|
1524
|
+
? initApiEquivalentWindowLines(input.activitySnapshot)
|
|
1525
|
+
: ["API-equivalent usage value: unavailable — no snapshot was produced"];
|
|
1526
|
+
const planLine = input.detectedPlans.length > 0
|
|
1527
|
+
? input.detectedPlans.map((plan) => {
|
|
1528
|
+
const known = plan.planId ?? "unrecognized plan";
|
|
1529
|
+
return `${plan.agent}: ${known} (${plan.billing})`;
|
|
1530
|
+
}).join("; ")
|
|
1531
|
+
: "none detected (billing mode remains unresolved)";
|
|
1532
|
+
const sourceLines = (input.logs?.sourceScans ?? [
|
|
1533
|
+
emptyInitSourceScan("claude-code"),
|
|
1534
|
+
emptyInitSourceScan("codex")
|
|
1535
|
+
]).map((scan) => {
|
|
1536
|
+
const agentRecords = records.filter((record) => record.agentId === scan.agent);
|
|
1537
|
+
const priced = agentRecords.filter((record) => typeof record.amountUsd === "number").length;
|
|
1538
|
+
const skipped = scan.filesSkippedBeforeWindow ?? 0;
|
|
1539
|
+
const validation = scan.jsonlValidationCoverage === "financial_events_only"
|
|
1540
|
+
? "; financial-event JSONL validation only"
|
|
1541
|
+
: "";
|
|
1542
|
+
return ` ${scan.agent}: ${scan.directoryStatus}; ${scan.filesParsed}/${scan.filesDiscovered} files parsed; ${priced}/${agentRecords.length} rows priced${skipped > 0 ? `; ${skipped} old files skipped` : ""}${validation}`;
|
|
1012
1543
|
});
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
{
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
sourceId: "local-root",
|
|
1019
|
-
path: rootPath,
|
|
1020
|
-
detail: "Explicit local folder source approved during init."
|
|
1021
|
-
}
|
|
1022
|
-
]));
|
|
1023
|
-
return ok([
|
|
1544
|
+
const diagnosticLines = (input.logs?.diagnostics ?? [])
|
|
1545
|
+
.filter((diagnostic) => diagnostic.code !== "directory_missing")
|
|
1546
|
+
.map((diagnostic) => ` ! ${sanitizeSecretishError(diagnostic.message)} (${diagnostic.count})`);
|
|
1547
|
+
const providerLines = formatInitProviderEvidence(input);
|
|
1548
|
+
return [
|
|
1024
1549
|
"aibill init",
|
|
1025
|
-
`
|
|
1026
|
-
"
|
|
1027
|
-
"
|
|
1028
|
-
"
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1550
|
+
`state project: ${sanitizeSecretishError(basename(input.rootPath))}`,
|
|
1551
|
+
"local usage scope: all Claude Code + Codex activity on this machine (last 30 days)",
|
|
1552
|
+
"provider scope: trusted connected billing from this state project only (shown separately)",
|
|
1553
|
+
"",
|
|
1554
|
+
"FIRST RECEIPT · API-equivalent usage value · last 30 days",
|
|
1555
|
+
...receiptLines,
|
|
1556
|
+
`observed records: ${records.length}; priced: ${pricedRecords.length}; unpriced: ${records.length - pricedRecords.length}`,
|
|
1557
|
+
...providerLines,
|
|
1558
|
+
`plans: ${planLine}`,
|
|
1559
|
+
"",
|
|
1560
|
+
"source diagnostics (same backfill scan; no second scan):",
|
|
1561
|
+
...sourceLines,
|
|
1562
|
+
...diagnosticLines,
|
|
1563
|
+
input.scanError ? ` ! scan failed: ${input.scanError}` : "",
|
|
1564
|
+
"",
|
|
1565
|
+
`status cache: ${input.cacheStatus} · private local aggregate · nothing uploaded`,
|
|
1566
|
+
"state: .ai-spend-agent",
|
|
1567
|
+
"manifest: written last",
|
|
1568
|
+
"next: npx aibill doctor --sources"
|
|
1569
|
+
].filter((line) => line !== "").join("\n");
|
|
1570
|
+
}
|
|
1571
|
+
function formatInitProviderEvidence(input) {
|
|
1572
|
+
const windowStart = input.asOf.getTime() - 30 * 24 * 60 * 60 * 1_000;
|
|
1573
|
+
const inWindow = input.trustedProviderRecords.filter((record) => {
|
|
1574
|
+
const timestamp = Date.parse(record.timestamp);
|
|
1575
|
+
return Number.isFinite(timestamp) && timestamp >= windowStart && timestamp <= input.asOf.getTime();
|
|
1576
|
+
});
|
|
1577
|
+
const priced = inWindow.filter((record) => typeof record.amountUsd === "number");
|
|
1578
|
+
const verified = priced.filter((record) => record.costConfidence === "verified");
|
|
1579
|
+
const estimatedApiEquivalent = priced.filter((record) => record.costConfidence === "estimated" &&
|
|
1580
|
+
record.providerCostType === "anthropic_claude_code_usage");
|
|
1581
|
+
const estimated = priced.filter((record) => record.costConfidence === "estimated" &&
|
|
1582
|
+
record.providerCostType !== "anthropic_claude_code_usage");
|
|
1583
|
+
const detected = priced.filter((record) => record.costConfidence === "detected_unverified");
|
|
1584
|
+
const unpriced = inWindow.length - priced.length;
|
|
1585
|
+
const lines = [];
|
|
1586
|
+
if (verified.length > 0) {
|
|
1587
|
+
const billedWindow = input.activitySnapshot?.metered?.providerBilled.thirtyDays;
|
|
1588
|
+
lines.push(billedWindow?.amountUsd !== null && billedWindow?.amountUsd !== undefined
|
|
1589
|
+
? `provider-billed cost: ${formatOptionalUsd(billedWindow.amountUsd)} verified (kept separate)`
|
|
1590
|
+
: "provider-billed cost: unavailable — billed bucket boundaries do not prove an exact 30-day amount");
|
|
1591
|
+
}
|
|
1592
|
+
if (estimated.length > 0) {
|
|
1593
|
+
lines.push(`provider financial estimate: ${formatOptionalUsd(analyzeSpend(estimated).totalUsd)} estimated; not verified billed spend (kept separate)`);
|
|
1594
|
+
}
|
|
1595
|
+
if (estimatedApiEquivalent.length > 0) {
|
|
1596
|
+
lines.push(`provider API-equivalent estimate: ${formatOptionalUsd(analyzeSpend(estimatedApiEquivalent).totalUsd)} estimated value; not verified billed spend (kept separate)`);
|
|
1597
|
+
}
|
|
1598
|
+
if (detected.length > 0) {
|
|
1599
|
+
lines.push(`provider cost: ${formatOptionalUsd(analyzeSpend(detected).totalUsd)} detected_unverified; not billed spend (kept separate)`);
|
|
1600
|
+
}
|
|
1601
|
+
if (unpriced > 0) {
|
|
1602
|
+
lines.push(`provider cost: unavailable — ${unpriced} trusted row(s) lacked a supported cost amount`);
|
|
1603
|
+
}
|
|
1604
|
+
const provedEmptyBilledWindow = input.activitySnapshot?.metered?.providerBilled.thirtyDays;
|
|
1605
|
+
if (verified.length === 0 && provedEmptyBilledWindow?.amountUsd === 0 &&
|
|
1606
|
+
provedEmptyBilledWindow.financialEvidence === "verified") {
|
|
1607
|
+
lines.push("provider-billed cost: $0.00 verified for the receipt-bound 30-day interval");
|
|
1608
|
+
}
|
|
1609
|
+
if (lines.length === 0) {
|
|
1610
|
+
lines.push(input.untrustedProviderState
|
|
1611
|
+
? "provider-billed cost: unavailable — untrusted repository state was ignored"
|
|
1612
|
+
: input.trustedProviderRecords.length > 0
|
|
1613
|
+
? "provider cost: unavailable — connected evidence had no supported row in the last 30 days"
|
|
1614
|
+
: input.trustedProviderState
|
|
1615
|
+
? "provider-billed cost: unavailable — trusted connected provider evidence exists, but no receipt-bound 30-day amount was proven"
|
|
1616
|
+
: "provider-billed cost: not connected");
|
|
1617
|
+
}
|
|
1618
|
+
if (input.providerCoverage)
|
|
1619
|
+
lines.push(`provider coverage: ${input.providerCoverage}`);
|
|
1620
|
+
return lines;
|
|
1621
|
+
}
|
|
1622
|
+
function initApiEquivalentWindowLines(snapshot) {
|
|
1623
|
+
if (snapshot.mode === "error") {
|
|
1624
|
+
return ["API-equivalent usage value: unavailable — refresh failed"];
|
|
1625
|
+
}
|
|
1626
|
+
if (snapshot.mode === "empty") {
|
|
1627
|
+
const completeZero = snapshot.coverage.recordsParsed === 0 &&
|
|
1628
|
+
snapshot.coverage.agents.length > 0 &&
|
|
1629
|
+
snapshot.coverage.agents.every((agent) => agent.directoryStatus === "readable" &&
|
|
1630
|
+
agent.malformedLines === 0 &&
|
|
1631
|
+
agent.unreadableFiles === 0 &&
|
|
1632
|
+
agent.unsupportedUsageSnapshots === 0 &&
|
|
1633
|
+
agent.jsonlValidationCoverage === "complete");
|
|
1634
|
+
return [completeZero
|
|
1635
|
+
? "API-equivalent value: ~$0.00 1d · ~$0.00 7d · ~$0.00 30d (estimated value; not billed spend)"
|
|
1636
|
+
: "API-equivalent usage value: unavailable — local source coverage is incomplete; no zero total was inferred"];
|
|
1637
|
+
}
|
|
1638
|
+
if (snapshot.mode === "unresolved" && snapshot.unresolved) {
|
|
1639
|
+
return [formatInitApiWindows("billing unresolved", snapshot.unresolved.apiEquivalent)];
|
|
1640
|
+
}
|
|
1641
|
+
const lines = [];
|
|
1642
|
+
for (const agent of snapshot.subscription?.agents ?? []) {
|
|
1643
|
+
lines.push(formatInitApiWindows(`${agent.agent} subscription value`, agent.apiEquivalent));
|
|
1644
|
+
}
|
|
1645
|
+
if (snapshot.metered && snapshot.metered.apiEquivalent.thirtyDays.recordCount > 0) {
|
|
1646
|
+
lines.push(formatInitApiWindows("metered API-equivalent value", snapshot.metered.apiEquivalent));
|
|
1647
|
+
}
|
|
1648
|
+
if (snapshot.unresolved && snapshot.unresolved.apiEquivalent.thirtyDays.recordCount > 0) {
|
|
1649
|
+
lines.push(formatInitApiWindows("billing unresolved", snapshot.unresolved.apiEquivalent));
|
|
1650
|
+
}
|
|
1651
|
+
return lines.length > 0
|
|
1652
|
+
? lines
|
|
1653
|
+
: ["API-equivalent usage value: unavailable — no priced local value was observed"];
|
|
1654
|
+
}
|
|
1655
|
+
function formatInitApiWindows(label, windows) {
|
|
1656
|
+
const amount = (value) => value === null ? "unavailable" : `~${formatOptionalUsd(value)}`;
|
|
1657
|
+
return `${label}: ${amount(windows.oneDay.amountUsd)} 1d · ${amount(windows.sevenDays.amountUsd)} 7d · ${amount(windows.thirtyDays.amountUsd)} 30d (API-equivalent; not billed spend)`;
|
|
1658
|
+
}
|
|
1659
|
+
function emptyInitSourceScan(agent) {
|
|
1660
|
+
return {
|
|
1661
|
+
agent,
|
|
1662
|
+
directoryStatus: "unreadable",
|
|
1663
|
+
filesDiscovered: 0,
|
|
1664
|
+
filesParsed: 0,
|
|
1665
|
+
malformedLines: 0,
|
|
1666
|
+
unreadableFiles: 0,
|
|
1667
|
+
unsupportedUsageSnapshots: 0
|
|
1668
|
+
};
|
|
1032
1669
|
}
|
|
1033
1670
|
async function scanCommand(args) {
|
|
1034
1671
|
const rootPath = resolve(args.path);
|
|
@@ -1507,6 +2144,16 @@ async function syncProviderCommand(args) {
|
|
|
1507
2144
|
...trustedAccountingMap(trustedPrior?.accounting, "financialsByProvider"),
|
|
1508
2145
|
[result.provider]: result.financials
|
|
1509
2146
|
};
|
|
2147
|
+
const checkedAtByProvider = {
|
|
2148
|
+
...trustedAccountingMap(trustedPrior?.accounting, "checkedAtByProvider"),
|
|
2149
|
+
[result.provider]: result.fetchedAt
|
|
2150
|
+
};
|
|
2151
|
+
const priorCoverageIntervals = trustedAccountingMap(trustedPrior?.accounting, "coverageIntervalsByProvider");
|
|
2152
|
+
const coverageIntervalsByProvider = Object.fromEntries(Object.entries(priorCoverageIntervals).filter(([provider]) => provider !== result.provider));
|
|
2153
|
+
const requestedCoverageInterval = result.coverageInterval;
|
|
2154
|
+
if (requestedCoverageInterval) {
|
|
2155
|
+
coverageIntervalsByProvider[result.provider] = requestedCoverageInterval;
|
|
2156
|
+
}
|
|
1510
2157
|
// Invalidate any earlier receipt before the first mutation. If a later
|
|
1511
2158
|
// local write fails, the partially updated repository state stays
|
|
1512
2159
|
// untrusted rather than inheriting the previous sync's authority.
|
|
@@ -1523,7 +2170,11 @@ async function syncProviderCommand(args) {
|
|
|
1523
2170
|
records,
|
|
1524
2171
|
qa: result.qa,
|
|
1525
2172
|
qaByProvider,
|
|
2173
|
+
checkedAtByProvider,
|
|
1526
2174
|
coverageByProvider,
|
|
2175
|
+
...(Object.keys(coverageIntervalsByProvider).length > 0
|
|
2176
|
+
? { coverageIntervalsByProvider }
|
|
2177
|
+
: {}),
|
|
1527
2178
|
financialsByProvider
|
|
1528
2179
|
});
|
|
1529
2180
|
await recordProviderSourceAttempt(stateDir, result.provider, result.fetchedAt, result.coverage === "partial"
|
|
@@ -1533,9 +2184,11 @@ async function syncProviderCommand(args) {
|
|
|
1533
2184
|
policy: "provider_reported_billed_cost_preferred",
|
|
1534
2185
|
note: "Official provider-reported billed costs are the spend headline. API-equivalent estimates remain separate evidence and are not added to that total.",
|
|
1535
2186
|
coverageByProvider,
|
|
2187
|
+
checkedAtByProvider,
|
|
2188
|
+
coverageIntervalsByProvider,
|
|
1536
2189
|
qaByProvider,
|
|
1537
2190
|
financialsByProvider
|
|
1538
|
-
});
|
|
2191
|
+
}, result.fetchedAt);
|
|
1539
2192
|
await appendAuditEvent(stateDir, {
|
|
1540
2193
|
timestamp: result.fetchedAt,
|
|
1541
2194
|
action: "source_scanned",
|
|
@@ -2092,6 +2745,9 @@ function parseArgs(argv) {
|
|
|
2092
2745
|
sample: false,
|
|
2093
2746
|
path: process.cwd()
|
|
2094
2747
|
};
|
|
2748
|
+
if (command === "statusline" && rest[0] && !rest[0].startsWith("--")) {
|
|
2749
|
+
parsed.statuslineAction = rest.shift();
|
|
2750
|
+
}
|
|
2095
2751
|
if (command === "connect" && rest[0] && !rest[0].startsWith("--")) {
|
|
2096
2752
|
parsed.provider = rest[0];
|
|
2097
2753
|
rest.shift();
|
|
@@ -2114,6 +2770,14 @@ function parseArgs(argv) {
|
|
|
2114
2770
|
parsed.sources = true;
|
|
2115
2771
|
continue;
|
|
2116
2772
|
}
|
|
2773
|
+
if (arg === "--statusline") {
|
|
2774
|
+
parsed.statusline = true;
|
|
2775
|
+
continue;
|
|
2776
|
+
}
|
|
2777
|
+
if (arg === "--replace") {
|
|
2778
|
+
parsed.replaceStatusline = true;
|
|
2779
|
+
continue;
|
|
2780
|
+
}
|
|
2117
2781
|
if (arg === "--ignore-state") {
|
|
2118
2782
|
parsed.ignoreState = true;
|
|
2119
2783
|
continue;
|
|
@@ -2373,12 +3037,13 @@ function stripTerminalControlSequences(message) {
|
|
|
2373
3037
|
function isPersistedDataMode(value) {
|
|
2374
3038
|
return value === "sample" || value === "local_logs" || value === "connected_provider";
|
|
2375
3039
|
}
|
|
2376
|
-
async function writeLocalSpendState(stateDir, records, summary, mappings, mode, accounting) {
|
|
3040
|
+
async function writeLocalSpendState(stateDir, records, summary, mappings, mode, accounting, checkedAt) {
|
|
2377
3041
|
if (mode !== "connected_provider") {
|
|
2378
3042
|
await invalidateConnectedSpendTrustReceipt(dirname(stateDir));
|
|
2379
3043
|
}
|
|
2380
3044
|
await writeJson(join(stateDir, "spend.json"), {
|
|
2381
3045
|
mode,
|
|
3046
|
+
...(checkedAt && validIsoString(checkedAt) ? { checkedAt } : {}),
|
|
2382
3047
|
records,
|
|
2383
3048
|
summary,
|
|
2384
3049
|
...(accounting ? { accounting } : {})
|
|
@@ -2486,7 +3151,13 @@ function helpText() {
|
|
|
2486
3151
|
"",
|
|
2487
3152
|
"Other commands:",
|
|
2488
3153
|
" --version, -v Print the package version without reading local data",
|
|
2489
|
-
" init [--path <dir>]
|
|
3154
|
+
" init [--path <dir>] Backfill 30 days machine-wide, print the first evidence-labeled receipt, and cache a private snapshot",
|
|
3155
|
+
" [--statusline] Explicitly install the Claude Code status line after a successful init",
|
|
3156
|
+
" statusline Render one plan-aware line from the private cache (no scan or network)",
|
|
3157
|
+
" statusline refresh Foreground refresh from real local evidence, then render the cache",
|
|
3158
|
+
" statusline install Reversibly add the standalone runner to Claude user settings",
|
|
3159
|
+
" [--replace] Explicitly replace an existing statusLine while preserving it for uninstall",
|
|
3160
|
+
" statusline uninstall Remove only the owned setting and restore its preserved predecessor",
|
|
2490
3161
|
" doctor [--sources] Launch diagnostics; --sources shows validation, evidence, freshness, and errors",
|
|
2491
3162
|
" reset [--path <dir>] Clear persisted spend state (so sample state can't mask real logs)",
|
|
2492
3163
|
" --ignore-state On the default/quickstart run, ignore persisted spend.json for this run",
|
|
@@ -2566,7 +3237,7 @@ export async function runMain() {
|
|
|
2566
3237
|
stdout: "",
|
|
2567
3238
|
stderr: [
|
|
2568
3239
|
`aibill hit an unexpected error: ${message}`,
|
|
2569
|
-
"Nothing was uploaded;
|
|
3240
|
+
"Nothing was uploaded. The command stopped without completing; run diagnostics before retrying.",
|
|
2570
3241
|
"Try `npx aibill doctor` for diagnostics, or open an issue: https://github.com/futurastudio/ai-spend-agent/issues"
|
|
2571
3242
|
].join("\n")
|
|
2572
3243
|
};
|