ai-spend-agent 0.6.0 → 0.6.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.
Files changed (3) hide show
  1. package/README.md +7 -0
  2. package/dist/index.js +489 -34
  3. package/package.json +3 -3
package/README.md CHANGED
@@ -3,11 +3,18 @@
3
3
  The full [aibill](https://github.com/futurastudio/ai-spend-agent) CLI.
4
4
 
5
5
  ```bash
6
+ npx aibill init
6
7
  npx ai-spend-agent
7
8
  # short alias
8
9
  npx aibill
9
10
  ```
10
11
 
12
+ Run `npx aibill init` from a project to detect machine-wide local Claude Code
13
+ and Codex history, print the first evidence-labeled personal receipt, and seed a private
14
+ aggregate cache under `~/.aibill/cache/`. Init never replaces missing personal
15
+ evidence with the bundled sample and never overwrites existing connected
16
+ source or audit state.
17
+
11
18
  It reads local Claude Code and Codex metadata, labels API-equivalent estimates,
12
19
  and can optionally add official OpenAI or Anthropic provider-reported cost
13
20
  through an environment-variable reference. No product telemetry is sent.
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@ import { realpathSync } from "node:fs";
3
3
  import { mkdir, readFile, rm, stat } from "node:fs/promises";
4
4
  import { basename, dirname, extname, join, resolve } from "node:path";
5
5
  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";
6
+ 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";
7
7
  import { generateActionPlanMarkdown, generateApplyArtifactMarkdown, generateDemoPackageMarkdown, generateHtmlReport, generateMarkdownReport, generatePlainEnglishSummary, generatePolicyConfigDraftMarkdown, generateReportCardCaption, generateReportCardSvg, generateVerificationPlanMarkdown, groupByDimensions } from "@agent-finops/report";
8
8
  export async function runCli(argv = process.argv.slice(2)) {
9
9
  if (argv.includes("--version") || argv.includes("-v")) {
@@ -323,7 +323,7 @@ function quickstartNextSteps(mode, detected) {
323
323
  steps.push("Need team reconciliation, allocation, budgets, and approvals? Workspace design partners: https://ai-spend-agent.vercel.app");
324
324
  return steps;
325
325
  }
326
- async function readPersistedSpend(rootPath) {
326
+ async function readPersistedSpend(rootPath, options = {}) {
327
327
  const stateDir = join(rootPath, ".ai-spend-agent");
328
328
  try {
329
329
  const exactSpendContents = await readSafeStateText(stateDir, "spend.json");
@@ -340,6 +340,9 @@ async function readPersistedSpend(rootPath) {
340
340
  const records = mode === "sample" || mode === undefined
341
341
  ? downgradeSampleUsageEvidence(parsedRecords)
342
342
  : parsedRecords;
343
+ if (spend.checkedAt !== undefined && !validIsoString(spend.checkedAt)) {
344
+ throw new Error("persisted spend checkedAt must be an ISO timestamp");
345
+ }
343
346
  const providerCoverage = persistedProviderCoverage(spend.accounting);
344
347
  const connectedTrust = mode === "connected_provider"
345
348
  ? await verifyConnectedSpendTrustReceipt(rootPath, exactSpendContents)
@@ -347,12 +350,16 @@ async function readPersistedSpend(rootPath) {
347
350
  return {
348
351
  mode,
349
352
  records,
353
+ ...(typeof spend.checkedAt === "string" ? { checkedAt: spend.checkedAt } : {}),
350
354
  ...(providerCoverage ? { providerCoverage } : {}),
351
355
  ...(isPlainObject(spend.accounting) ? { accounting: spend.accounting } : {}),
352
356
  ...(connectedTrust ? { connectedTrust } : {})
353
357
  };
354
358
  }
355
- catch {
359
+ catch (error) {
360
+ if (options.strict && !isNodeError(error, "ENOENT")) {
361
+ throw new Error("Existing .ai-spend-agent/spend.json is invalid or unsafe; it was preserved and init stopped.");
362
+ }
356
363
  return undefined;
357
364
  }
358
365
  }
@@ -992,43 +999,474 @@ async function resetCommand(args) {
992
999
  ].join("\n"));
993
1000
  }
994
1001
  async function initCommand(args) {
995
- const rootPath = resolve(args.path);
996
- const stateDir = join(rootPath, ".ai-spend-agent");
997
- await mkdir(stateDir, { recursive: true });
998
- const registry = createLocalFolderSourceRegistry(rootPath);
999
- await writeJson(join(stateDir, "manifest.json"), {
1002
+ if (args.sample) {
1003
+ return {
1004
+ exitCode: 1,
1005
+ stdout: "",
1006
+ 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."
1007
+ };
1008
+ }
1009
+ let detectedPlanOverride;
1010
+ if (args.plan) {
1011
+ const override = planOverrideFromFlag(args.plan);
1012
+ if (!override) {
1013
+ return {
1014
+ exitCode: 1,
1015
+ stdout: "",
1016
+ stderr: `Unknown --plan "${args.plan}". Valid plans: ${subscriptionPlans.map((plan) => plan.id).join(", ")}`
1017
+ };
1018
+ }
1019
+ detectedPlanOverride = [override];
1020
+ }
1021
+ const rootPath = await resolveSafeScanRoot(args.path);
1022
+ const cacheDirectory = process.env.AIBILL_CACHE_DIR;
1023
+ await preflightInitCache(cacheDirectory);
1024
+ let stateDir;
1025
+ let stateDirectoryExists = true;
1026
+ try {
1027
+ stateDir = await resolveSafeStateDirectory(rootPath);
1028
+ }
1029
+ catch (error) {
1030
+ if (!isNodeError(error, "ENOENT"))
1031
+ throw error;
1032
+ stateDirectoryExists = false;
1033
+ stateDir = join(rootPath, ".ai-spend-agent");
1034
+ }
1035
+ const statePreparedAt = new Date();
1036
+ // Preflight every existing project file before any mutation. Valid files are
1037
+ // left byte-for-byte alone so a repeated init cannot erase connector
1038
+ // configuration, audit history, provider trust, spend state, or unknown
1039
+ // forward-compatible fields. The manifest remains the completion marker and
1040
+ // is written last.
1041
+ const existingManifest = stateDirectoryExists
1042
+ ? await readInitJsonObject(stateDir, "manifest.json", { allowMissing: true })
1043
+ : undefined;
1044
+ const existingRegistry = stateDirectoryExists
1045
+ ? await readInitJsonObject(stateDir, "sources.json", { allowMissing: true })
1046
+ : undefined;
1047
+ const existingAuditLog = stateDirectoryExists
1048
+ ? await readInitJsonObject(stateDir, "audit-log.json", { allowMissing: true })
1049
+ : undefined;
1050
+ if (existingRegistry)
1051
+ normalizeSourceRegistry(existingRegistry);
1052
+ validateInitAuditLog(existingAuditLog);
1053
+ // Capture one attempt anchor immediately before the concurrent detection /
1054
+ // backfill reads. Snapshot windows, refresh ordering, and the manifest all
1055
+ // refer to this same moment.
1056
+ const asOf = new Date();
1057
+ const planPromise = detectedPlanOverride
1058
+ ? Promise.resolve(detectedPlanOverride)
1059
+ : detectLocalPlans({
1060
+ claudeConfigPath: process.env.AI_SPEND_CLAUDE_CONFIG,
1061
+ codexAuthPath: process.env.AI_SPEND_CODEX_AUTH
1062
+ }).catch(() => []);
1063
+ // Attach the rejection handler now. A malformed spend file can fail before
1064
+ // the bounded transcript scan completes; leaving that promise unattended
1065
+ // during the scan would create an unhandled rejection.
1066
+ const persistedPromise = readPersistedSpend(rootPath, { strict: true }).then((persisted) => ({ persisted }), (error) => ({ error }));
1067
+ let logs;
1068
+ let scanError;
1069
+ try {
1070
+ logs = await loadLocalAgentFinancialUsage({
1071
+ claudeProjectsDir: process.env.AI_SPEND_CLAUDE_LOGS_DIR,
1072
+ codexSessionsDir: process.env.AI_SPEND_CODEX_LOGS_DIR,
1073
+ sinceIso: sinceIsoForDays(30, asOf)
1074
+ });
1075
+ }
1076
+ catch (error) {
1077
+ scanError = sanitizeSecretishError(error instanceof Error ? error.message : String(error));
1078
+ }
1079
+ const [detectedPlans, persistedResult] = await Promise.all([planPromise, persistedPromise]);
1080
+ if ("error" in persistedResult)
1081
+ throw persistedResult.error;
1082
+ const persisted = persistedResult.persisted;
1083
+ const trustedProviderRecords = persisted?.mode === "connected_provider" && persisted.connectedTrust?.trusted === true
1084
+ ? selectProviderFinancialHeadlineRecords(persisted.records)
1085
+ : [];
1086
+ let activitySnapshot;
1087
+ let cacheStatus;
1088
+ const structuredSourceFailure = logs !== undefined &&
1089
+ logs.sourceScans.some((scan) => scan.directoryStatus === "unreadable") &&
1090
+ !logs.sourceScans.some((scan) => scan.directoryStatus === "readable");
1091
+ if (logs && !structuredSourceFailure) {
1092
+ let refreshErrorCode = "invalid_evidence";
1093
+ try {
1094
+ const generatedAt = new Date().toISOString();
1095
+ activitySnapshot = buildActivitySnapshot({
1096
+ asOf: asOf.toISOString(),
1097
+ generatedAt,
1098
+ records: [...logs.records, ...trustedProviderRecords],
1099
+ calls: logs.calls,
1100
+ detectedPlans,
1101
+ sourceScans: logs.sourceScans,
1102
+ trustedProviderRecordIds: trustedProviderRecords.map((record) => record.id),
1103
+ billedOverageRecordIds: [],
1104
+ providerCoverage: initProviderCoverage(trustedProviderRecords, persisted?.mode === "connected_provider" && persisted.connectedTrust?.trusted === true
1105
+ ? trustedAccountingMap(persisted.accounting, "coverageByProvider")
1106
+ : {}, persisted?.mode === "connected_provider" && persisted.connectedTrust?.trusted === true
1107
+ ? trustedAccountingMap(persisted.accounting, "checkedAtByProvider")
1108
+ : {}, persisted?.mode === "connected_provider" && persisted.connectedTrust?.trusted === true
1109
+ ? trustedAccountingMap(persisted.accounting, "coverageIntervalsByProvider")
1110
+ : {}, persisted?.mode === "connected_provider" && persisted.connectedTrust?.trusted === true
1111
+ ? persisted.checkedAt ?? persisted.connectedTrust.trustedAt
1112
+ : undefined),
1113
+ sampleData: false
1114
+ });
1115
+ refreshErrorCode = "cache_write_failed";
1116
+ const written = await writeActivitySnapshot(activitySnapshot, { cacheDirectory });
1117
+ cacheStatus = written.status === "written" ? "refreshed" : "kept newer snapshot";
1118
+ }
1119
+ catch {
1120
+ scanError = refreshErrorCode === "invalid_evidence"
1121
+ ? "the observed evidence could not produce a valid activity snapshot"
1122
+ : "the private activity cache could not be updated";
1123
+ const failed = await recordActivitySnapshotRefreshFailure(asOf.toISOString(), refreshErrorCode, { cacheDirectory });
1124
+ activitySnapshot = failed.snapshot;
1125
+ cacheStatus = "refresh failed";
1126
+ }
1127
+ }
1128
+ else {
1129
+ const failed = await recordActivitySnapshotRefreshFailure(asOf.toISOString(), structuredSourceFailure ? "source_unreadable" : "scan_failed", { cacheDirectory });
1130
+ activitySnapshot = failed.snapshot;
1131
+ cacheStatus = "refresh failed";
1132
+ }
1133
+ if (!stateDirectoryExists) {
1134
+ stateDir = await resolveSafeStateDirectory(rootPath, { create: true });
1135
+ }
1136
+ await preserveOrCreateInitRegistry(stateDir, rootPath, statePreparedAt, existingRegistry);
1137
+ await preserveOrCreateInitAuditLog(stateDir, rootPath, statePreparedAt, existingAuditLog);
1138
+ const manifest = buildInitManifest(existingManifest, asOf);
1139
+ await writeSafeStateText(stateDir, "manifest.json", `${JSON.stringify(manifest, null, 2)}\n`);
1140
+ return ok(formatInitReceipt({
1141
+ rootPath,
1142
+ asOf,
1143
+ activitySnapshot,
1144
+ logs,
1145
+ detectedPlans,
1146
+ trustedProviderRecords,
1147
+ providerCoverage: persisted?.providerCoverage,
1148
+ trustedProviderState: persisted?.mode === "connected_provider" && persisted.connectedTrust?.trusted === true,
1149
+ untrustedProviderState: persisted?.mode === "connected_provider" && persisted.connectedTrust?.trusted !== true,
1150
+ scanError,
1151
+ cacheStatus
1152
+ }));
1153
+ }
1154
+ async function preflightInitCache(cacheDirectory) {
1155
+ const existing = await readActivitySnapshot({ cacheDirectory });
1156
+ if (existing.status !== "error")
1157
+ return;
1158
+ throw new Error(`Existing private activity cache is ${existing.code.replaceAll("_", " ")}; ` +
1159
+ "it was preserved and init stopped. Remove the cache explicitly before rebuilding it.");
1160
+ }
1161
+ async function preserveOrCreateInitRegistry(stateDir, rootPath, asOf, existing) {
1162
+ if (existing) {
1163
+ // Leave the valid contract byte-for-byte alone. Rewriting would drop
1164
+ // unknown future fields and could invalidate a connected-state receipt
1165
+ // bound to the exact source-registry bytes.
1166
+ return;
1167
+ }
1168
+ await writeSafeStateText(stateDir, "sources.json", `${JSON.stringify(createLocalFolderSourceRegistry(rootPath, asOf), null, 2)}\n`);
1169
+ }
1170
+ async function preserveOrCreateInitAuditLog(stateDir, rootPath, asOf, existing) {
1171
+ if (existing)
1172
+ return;
1173
+ const timestamp = asOf.toISOString();
1174
+ await writeSafeStateText(stateDir, "audit-log.json", `${JSON.stringify(createScanAuditLog([{
1175
+ timestamp,
1176
+ action: "source_registered",
1177
+ sourceId: "local-root",
1178
+ path: rootPath,
1179
+ detail: "Explicit local folder source approved during init."
1180
+ }]), null, 2)}\n`);
1181
+ }
1182
+ function validateInitAuditLog(existing) {
1183
+ if (existing && (existing.version !== 1 || existing.localOnly !== true || !Array.isArray(existing.events))) {
1184
+ throw new Error("Invalid local audit log: expected the canonical local-only audit shape.");
1185
+ }
1186
+ }
1187
+ async function readInitJsonObject(stateDir, fileName, options) {
1188
+ let contents;
1189
+ try {
1190
+ contents = await readSafeStateText(stateDir, fileName);
1191
+ }
1192
+ catch (error) {
1193
+ if (options.allowMissing && isNodeError(error, "ENOENT"))
1194
+ return undefined;
1195
+ throw error;
1196
+ }
1197
+ let parsed;
1198
+ try {
1199
+ parsed = JSON.parse(contents);
1200
+ }
1201
+ catch {
1202
+ throw new Error(`Invalid local ${fileName}: expected JSON; the existing file was preserved.`);
1203
+ }
1204
+ if (!isPlainObject(parsed)) {
1205
+ throw new Error(`Invalid local ${fileName}: expected a JSON object; the existing file was preserved.`);
1206
+ }
1207
+ return parsed;
1208
+ }
1209
+ function buildInitManifest(existing, asOf) {
1210
+ const timestamp = asOf.toISOString();
1211
+ return {
1212
+ ...(existing ?? {}),
1000
1213
  product: "aibill",
1001
- mode: "local-first-demo",
1214
+ mode: "local-first",
1002
1215
  cloudUpload: false,
1003
1216
  cronJobsEnabled: false,
1004
1217
  redactionPolicy: "secrets are never printed; detected values are written only as [REDACTED]",
1005
1218
  sourceRegistry: "sources.json",
1006
1219
  auditLog: "audit-log.json",
1220
+ backfillWindowDays: 30,
1221
+ statusSnapshot: {
1222
+ schema: "aibill.activity_snapshot/v1",
1223
+ storage: "private external cache",
1224
+ networkUploaded: false
1225
+ },
1226
+ initializedAt: typeof existing?.initializedAt === "string" ? existing.initializedAt : timestamp,
1227
+ lastInitializedAt: timestamp,
1007
1228
  nextCommands: [
1008
- "npx aibill doctor",
1009
- `npx aibill scan --sample --path ${rootPath}`,
1010
- `npx aibill report --sample --out ai-spend-report --path ${rootPath}`
1229
+ "npx aibill",
1230
+ "npx aibill doctor --sources",
1231
+ "npx aibill report"
1011
1232
  ]
1233
+ };
1234
+ }
1235
+ function initProviderCoverage(records, coverageByProvider, checkedAtByProvider, coverageIntervalsByProvider, fallbackCheckedAt) {
1236
+ const providerGroups = new Map();
1237
+ for (const record of records) {
1238
+ const rawProvider = record.source.provider;
1239
+ const provider = activitySnapshotProvider(rawProvider);
1240
+ const group = providerGroups.get(provider) ?? {
1241
+ rawProviders: new Set(),
1242
+ records: []
1243
+ };
1244
+ group.rawProviders.add(rawProvider);
1245
+ group.records.push(record);
1246
+ providerGroups.set(provider, group);
1247
+ }
1248
+ for (const rawProvider of new Set([
1249
+ ...Object.keys(coverageByProvider),
1250
+ ...Object.keys(checkedAtByProvider),
1251
+ ...Object.keys(coverageIntervalsByProvider)
1252
+ ])) {
1253
+ const provider = activitySnapshotProvider(rawProvider);
1254
+ const group = providerGroups.get(provider) ?? {
1255
+ rawProviders: new Set(),
1256
+ records: []
1257
+ };
1258
+ group.rawProviders.add(rawProvider);
1259
+ providerGroups.set(provider, group);
1260
+ }
1261
+ return [...providerGroups.entries()].map(([provider, group]) => {
1262
+ const coverages = [...group.rawProviders]
1263
+ .map((rawProvider) => coverageByProvider[rawProvider] ?? coverageByProvider[provider])
1264
+ .filter((coverage) => coverage !== undefined);
1265
+ const status = coverages.length === group.rawProviders.size && coverages.every((coverage) => coverage === "complete")
1266
+ ? "complete"
1267
+ : coverages.some((coverage) => coverage === "complete" || coverage === "partial")
1268
+ ? "partial"
1269
+ : "unavailable";
1270
+ const checkedValues = [...group.rawProviders]
1271
+ .map((rawProvider) => checkedAtByProvider[rawProvider] ?? checkedAtByProvider[provider])
1272
+ .filter((value) => validIsoString(value));
1273
+ const receiptBoundCheckedAt = checkedValues.length === group.rawProviders.size
1274
+ ? checkedValues.sort()[0]
1275
+ : providerGroups.size === 1 && validIsoString(fallbackCheckedAt)
1276
+ ? fallbackCheckedAt
1277
+ : undefined;
1278
+ const latestEvidenceAt = group.records
1279
+ .map((record) => record.timestamp)
1280
+ .filter(validIsoString)
1281
+ .sort()
1282
+ .at(-1);
1283
+ const intervals = [...group.rawProviders]
1284
+ .map((rawProvider) => coverageIntervalsByProvider[rawProvider] ?? coverageIntervalsByProvider[provider])
1285
+ .filter(validProviderCoverageInterval);
1286
+ const coverageStart = intervals.length === group.rawProviders.size
1287
+ ? intervals.map((interval) => interval.coverageStart).sort().at(-1)
1288
+ : undefined;
1289
+ const coverageEnd = intervals.length === group.rawProviders.size
1290
+ ? intervals.map((interval) => interval.coverageEnd).sort()[0]
1291
+ : undefined;
1292
+ return {
1293
+ provider,
1294
+ status,
1295
+ validationCoverage: sourceStatusDefinitions.find((definition) => definition.id === provider)?.validationCoverage
1296
+ ?? "untested",
1297
+ ...(receiptBoundCheckedAt ? { checkedAt: receiptBoundCheckedAt } : {}),
1298
+ ...(receiptBoundCheckedAt && latestEvidenceAt ? { latestEvidenceAt } : {}),
1299
+ ...(receiptBoundCheckedAt && coverageStart && coverageEnd && Date.parse(coverageStart) <= Date.parse(coverageEnd)
1300
+ ? { coverageStart, coverageEnd }
1301
+ : {})
1302
+ };
1303
+ }).sort((left, right) => left.provider.localeCompare(right.provider));
1304
+ }
1305
+ function validProviderCoverageInterval(value) {
1306
+ return isPlainObject(value) &&
1307
+ validIsoString(value.coverageStart) &&
1308
+ validIsoString(value.coverageEnd) &&
1309
+ Date.parse(value.coverageStart) <= Date.parse(value.coverageEnd);
1310
+ }
1311
+ function activitySnapshotProvider(provider) {
1312
+ if (provider === "openai" || provider === "anthropic" || provider === "cursor" || provider === "github-copilot") {
1313
+ return provider;
1314
+ }
1315
+ return "other";
1316
+ }
1317
+ function formatInitReceipt(input) {
1318
+ const records = input.logs?.records ?? [];
1319
+ const pricedRecords = records.filter((record) => typeof record.amountUsd === "number");
1320
+ const sourceFailures = (input.logs?.sourceScans ?? []).some((scan) => scan.directoryStatus === "unreadable") ||
1321
+ (input.logs?.diagnostics ?? []).some((diagnostic) => diagnostic.severity === "error");
1322
+ const receiptLines = input.scanError || sourceFailures && records.length === 0
1323
+ ? ["API-equivalent usage value: unavailable — the local scan could not prove an empty result"]
1324
+ : input.activitySnapshot
1325
+ ? initApiEquivalentWindowLines(input.activitySnapshot)
1326
+ : ["API-equivalent usage value: unavailable — no snapshot was produced"];
1327
+ const planLine = input.detectedPlans.length > 0
1328
+ ? input.detectedPlans.map((plan) => {
1329
+ const known = plan.planId ?? "unrecognized plan";
1330
+ return `${plan.agent}: ${known} (${plan.billing})`;
1331
+ }).join("; ")
1332
+ : "none detected (billing mode remains unresolved)";
1333
+ const sourceLines = (input.logs?.sourceScans ?? [
1334
+ emptyInitSourceScan("claude-code"),
1335
+ emptyInitSourceScan("codex")
1336
+ ]).map((scan) => {
1337
+ const agentRecords = records.filter((record) => record.agentId === scan.agent);
1338
+ const priced = agentRecords.filter((record) => typeof record.amountUsd === "number").length;
1339
+ const skipped = scan.filesSkippedBeforeWindow ?? 0;
1340
+ const validation = scan.jsonlValidationCoverage === "financial_events_only"
1341
+ ? "; financial-event JSONL validation only"
1342
+ : "";
1343
+ return ` ${scan.agent}: ${scan.directoryStatus}; ${scan.filesParsed}/${scan.filesDiscovered} files parsed; ${priced}/${agentRecords.length} rows priced${skipped > 0 ? `; ${skipped} old files skipped` : ""}${validation}`;
1012
1344
  });
1013
- await writeJson(join(stateDir, "sources.json"), registry);
1014
- await writeJson(join(stateDir, "audit-log.json"), createScanAuditLog([
1015
- {
1016
- timestamp: registry.updatedAt,
1017
- action: "source_registered",
1018
- sourceId: "local-root",
1019
- path: rootPath,
1020
- detail: "Explicit local folder source approved during init."
1021
- }
1022
- ]));
1023
- return ok([
1345
+ const diagnosticLines = (input.logs?.diagnostics ?? [])
1346
+ .filter((diagnostic) => diagnostic.code !== "directory_missing")
1347
+ .map((diagnostic) => ` ! ${sanitizeSecretishError(diagnostic.message)} (${diagnostic.count})`);
1348
+ const providerLines = formatInitProviderEvidence(input);
1349
+ return [
1024
1350
  "aibill init",
1025
- `path: ${rootPath}`,
1026
- "demo mode: local-first sample workflow",
1027
- "cloud upload: disabled",
1028
- "cron jobs: disabled in V0 demo",
1029
- `state directory: ${stateDir}`,
1030
- `next: npx aibill scan --sample --path ${rootPath}`
1031
- ].join("\n"));
1351
+ `state project: ${sanitizeSecretishError(basename(input.rootPath))}`,
1352
+ "local usage scope: all Claude Code + Codex activity on this machine (last 30 days)",
1353
+ "provider scope: trusted connected billing from this state project only (shown separately)",
1354
+ "",
1355
+ "FIRST RECEIPT · API-equivalent usage value · last 30 days",
1356
+ ...receiptLines,
1357
+ `observed records: ${records.length}; priced: ${pricedRecords.length}; unpriced: ${records.length - pricedRecords.length}`,
1358
+ ...providerLines,
1359
+ `plans: ${planLine}`,
1360
+ "",
1361
+ "source diagnostics (same backfill scan; no second scan):",
1362
+ ...sourceLines,
1363
+ ...diagnosticLines,
1364
+ input.scanError ? ` ! scan failed: ${input.scanError}` : "",
1365
+ "",
1366
+ `status cache: ${input.cacheStatus} · private local aggregate · nothing uploaded`,
1367
+ "state: .ai-spend-agent",
1368
+ "manifest: written last",
1369
+ "next: npx aibill doctor --sources"
1370
+ ].filter((line) => line !== "").join("\n");
1371
+ }
1372
+ function formatInitProviderEvidence(input) {
1373
+ const windowStart = input.asOf.getTime() - 30 * 24 * 60 * 60 * 1_000;
1374
+ const inWindow = input.trustedProviderRecords.filter((record) => {
1375
+ const timestamp = Date.parse(record.timestamp);
1376
+ return Number.isFinite(timestamp) && timestamp >= windowStart && timestamp <= input.asOf.getTime();
1377
+ });
1378
+ const priced = inWindow.filter((record) => typeof record.amountUsd === "number");
1379
+ const verified = priced.filter((record) => record.costConfidence === "verified");
1380
+ const estimatedApiEquivalent = priced.filter((record) => record.costConfidence === "estimated" &&
1381
+ record.providerCostType === "anthropic_claude_code_usage");
1382
+ const estimated = priced.filter((record) => record.costConfidence === "estimated" &&
1383
+ record.providerCostType !== "anthropic_claude_code_usage");
1384
+ const detected = priced.filter((record) => record.costConfidence === "detected_unverified");
1385
+ const unpriced = inWindow.length - priced.length;
1386
+ const lines = [];
1387
+ if (verified.length > 0) {
1388
+ const billedWindow = input.activitySnapshot?.metered?.providerBilled.thirtyDays;
1389
+ lines.push(billedWindow?.amountUsd !== null && billedWindow?.amountUsd !== undefined
1390
+ ? `provider-billed cost: ${formatOptionalUsd(billedWindow.amountUsd)} verified (kept separate)`
1391
+ : "provider-billed cost: unavailable — billed bucket boundaries do not prove an exact 30-day amount");
1392
+ }
1393
+ if (estimated.length > 0) {
1394
+ lines.push(`provider financial estimate: ${formatOptionalUsd(analyzeSpend(estimated).totalUsd)} estimated; not verified billed spend (kept separate)`);
1395
+ }
1396
+ if (estimatedApiEquivalent.length > 0) {
1397
+ lines.push(`provider API-equivalent estimate: ${formatOptionalUsd(analyzeSpend(estimatedApiEquivalent).totalUsd)} estimated value; not verified billed spend (kept separate)`);
1398
+ }
1399
+ if (detected.length > 0) {
1400
+ lines.push(`provider cost: ${formatOptionalUsd(analyzeSpend(detected).totalUsd)} detected_unverified; not billed spend (kept separate)`);
1401
+ }
1402
+ if (unpriced > 0) {
1403
+ lines.push(`provider cost: unavailable — ${unpriced} trusted row(s) lacked a supported cost amount`);
1404
+ }
1405
+ const provedEmptyBilledWindow = input.activitySnapshot?.metered?.providerBilled.thirtyDays;
1406
+ if (verified.length === 0 && provedEmptyBilledWindow?.amountUsd === 0 &&
1407
+ provedEmptyBilledWindow.financialEvidence === "verified") {
1408
+ lines.push("provider-billed cost: $0.00 verified for the receipt-bound 30-day interval");
1409
+ }
1410
+ if (lines.length === 0) {
1411
+ lines.push(input.untrustedProviderState
1412
+ ? "provider-billed cost: unavailable — untrusted repository state was ignored"
1413
+ : input.trustedProviderRecords.length > 0
1414
+ ? "provider cost: unavailable — connected evidence had no supported row in the last 30 days"
1415
+ : input.trustedProviderState
1416
+ ? "provider-billed cost: unavailable — trusted connected provider evidence exists, but no receipt-bound 30-day amount was proven"
1417
+ : "provider-billed cost: not connected");
1418
+ }
1419
+ if (input.providerCoverage)
1420
+ lines.push(`provider coverage: ${input.providerCoverage}`);
1421
+ return lines;
1422
+ }
1423
+ function initApiEquivalentWindowLines(snapshot) {
1424
+ if (snapshot.mode === "error") {
1425
+ return ["API-equivalent usage value: unavailable — refresh failed"];
1426
+ }
1427
+ if (snapshot.mode === "empty") {
1428
+ const completeZero = snapshot.coverage.recordsParsed === 0 &&
1429
+ snapshot.coverage.agents.length > 0 &&
1430
+ snapshot.coverage.agents.every((agent) => agent.directoryStatus === "readable" &&
1431
+ agent.malformedLines === 0 &&
1432
+ agent.unreadableFiles === 0 &&
1433
+ agent.unsupportedUsageSnapshots === 0 &&
1434
+ agent.jsonlValidationCoverage === "complete");
1435
+ return [completeZero
1436
+ ? "API-equivalent value: ~$0.00 1d · ~$0.00 7d · ~$0.00 30d (estimated value; not billed spend)"
1437
+ : "API-equivalent usage value: unavailable — local source coverage is incomplete; no zero total was inferred"];
1438
+ }
1439
+ if (snapshot.mode === "unresolved" && snapshot.unresolved) {
1440
+ return [formatInitApiWindows("billing unresolved", snapshot.unresolved.apiEquivalent)];
1441
+ }
1442
+ const lines = [];
1443
+ for (const agent of snapshot.subscription?.agents ?? []) {
1444
+ lines.push(formatInitApiWindows(`${agent.agent} subscription value`, agent.apiEquivalent));
1445
+ }
1446
+ if (snapshot.metered && snapshot.metered.apiEquivalent.thirtyDays.recordCount > 0) {
1447
+ lines.push(formatInitApiWindows("metered API-equivalent value", snapshot.metered.apiEquivalent));
1448
+ }
1449
+ if (snapshot.unresolved && snapshot.unresolved.apiEquivalent.thirtyDays.recordCount > 0) {
1450
+ lines.push(formatInitApiWindows("billing unresolved", snapshot.unresolved.apiEquivalent));
1451
+ }
1452
+ return lines.length > 0
1453
+ ? lines
1454
+ : ["API-equivalent usage value: unavailable — no priced local value was observed"];
1455
+ }
1456
+ function formatInitApiWindows(label, windows) {
1457
+ const amount = (value) => value === null ? "unavailable" : `~${formatOptionalUsd(value)}`;
1458
+ return `${label}: ${amount(windows.oneDay.amountUsd)} 1d · ${amount(windows.sevenDays.amountUsd)} 7d · ${amount(windows.thirtyDays.amountUsd)} 30d (API-equivalent; not billed spend)`;
1459
+ }
1460
+ function emptyInitSourceScan(agent) {
1461
+ return {
1462
+ agent,
1463
+ directoryStatus: "unreadable",
1464
+ filesDiscovered: 0,
1465
+ filesParsed: 0,
1466
+ malformedLines: 0,
1467
+ unreadableFiles: 0,
1468
+ unsupportedUsageSnapshots: 0
1469
+ };
1032
1470
  }
1033
1471
  async function scanCommand(args) {
1034
1472
  const rootPath = resolve(args.path);
@@ -1507,6 +1945,16 @@ async function syncProviderCommand(args) {
1507
1945
  ...trustedAccountingMap(trustedPrior?.accounting, "financialsByProvider"),
1508
1946
  [result.provider]: result.financials
1509
1947
  };
1948
+ const checkedAtByProvider = {
1949
+ ...trustedAccountingMap(trustedPrior?.accounting, "checkedAtByProvider"),
1950
+ [result.provider]: result.fetchedAt
1951
+ };
1952
+ const priorCoverageIntervals = trustedAccountingMap(trustedPrior?.accounting, "coverageIntervalsByProvider");
1953
+ const coverageIntervalsByProvider = Object.fromEntries(Object.entries(priorCoverageIntervals).filter(([provider]) => provider !== result.provider));
1954
+ const requestedCoverageInterval = result.coverageInterval;
1955
+ if (requestedCoverageInterval) {
1956
+ coverageIntervalsByProvider[result.provider] = requestedCoverageInterval;
1957
+ }
1510
1958
  // Invalidate any earlier receipt before the first mutation. If a later
1511
1959
  // local write fails, the partially updated repository state stays
1512
1960
  // untrusted rather than inheriting the previous sync's authority.
@@ -1523,7 +1971,11 @@ async function syncProviderCommand(args) {
1523
1971
  records,
1524
1972
  qa: result.qa,
1525
1973
  qaByProvider,
1974
+ checkedAtByProvider,
1526
1975
  coverageByProvider,
1976
+ ...(Object.keys(coverageIntervalsByProvider).length > 0
1977
+ ? { coverageIntervalsByProvider }
1978
+ : {}),
1527
1979
  financialsByProvider
1528
1980
  });
1529
1981
  await recordProviderSourceAttempt(stateDir, result.provider, result.fetchedAt, result.coverage === "partial"
@@ -1533,9 +1985,11 @@ async function syncProviderCommand(args) {
1533
1985
  policy: "provider_reported_billed_cost_preferred",
1534
1986
  note: "Official provider-reported billed costs are the spend headline. API-equivalent estimates remain separate evidence and are not added to that total.",
1535
1987
  coverageByProvider,
1988
+ checkedAtByProvider,
1989
+ coverageIntervalsByProvider,
1536
1990
  qaByProvider,
1537
1991
  financialsByProvider
1538
- });
1992
+ }, result.fetchedAt);
1539
1993
  await appendAuditEvent(stateDir, {
1540
1994
  timestamp: result.fetchedAt,
1541
1995
  action: "source_scanned",
@@ -2373,12 +2827,13 @@ function stripTerminalControlSequences(message) {
2373
2827
  function isPersistedDataMode(value) {
2374
2828
  return value === "sample" || value === "local_logs" || value === "connected_provider";
2375
2829
  }
2376
- async function writeLocalSpendState(stateDir, records, summary, mappings, mode, accounting) {
2830
+ async function writeLocalSpendState(stateDir, records, summary, mappings, mode, accounting, checkedAt) {
2377
2831
  if (mode !== "connected_provider") {
2378
2832
  await invalidateConnectedSpendTrustReceipt(dirname(stateDir));
2379
2833
  }
2380
2834
  await writeJson(join(stateDir, "spend.json"), {
2381
2835
  mode,
2836
+ ...(checkedAt && validIsoString(checkedAt) ? { checkedAt } : {}),
2382
2837
  records,
2383
2838
  summary,
2384
2839
  ...(accounting ? { accounting } : {})
@@ -2486,7 +2941,7 @@ function helpText() {
2486
2941
  "",
2487
2942
  "Other commands:",
2488
2943
  " --version, -v Print the package version without reading local data",
2489
- " init [--path <dir>] Initialize local state",
2944
+ " init [--path <dir>] Backfill 30 days machine-wide, print the first evidence-labeled receipt, and cache a private snapshot",
2490
2945
  " doctor [--sources] Launch diagnostics; --sources shows validation, evidence, freshness, and errors",
2491
2946
  " reset [--path <dir>] Clear persisted spend state (so sample state can't mask real logs)",
2492
2947
  " --ignore-state On the default/quickstart run, ignore persisted spend.json for this run",
@@ -2566,7 +3021,7 @@ export async function runMain() {
2566
3021
  stdout: "",
2567
3022
  stderr: [
2568
3023
  `aibill hit an unexpected error: ${message}`,
2569
- "Nothing was uploaded; local state is unchanged.",
3024
+ "Nothing was uploaded. The command stopped without completing; run diagnostics before retrying.",
2570
3025
  "Try `npx aibill doctor` for diagnostics, or open an issue: https://github.com/futurastudio/ai-spend-agent/issues"
2571
3026
  ].join("\n")
2572
3027
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-spend-agent",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "description": "Local-first financial accountability CLI for Claude Code and Codex work, cost evidence, attribution, runway, provenance, and Context Health.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -54,8 +54,8 @@
54
54
  "prepack": "npm run build"
55
55
  },
56
56
  "dependencies": {
57
- "@agent-finops/core": "0.6.0",
58
- "@agent-finops/report": "0.6.0",
57
+ "@agent-finops/core": "0.6.1",
58
+ "@agent-finops/report": "0.6.1",
59
59
  "yocto-spinner": "^1.2.0"
60
60
  }
61
61
  }