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/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 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"), {
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-demo",
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 doctor",
1009
- `npx aibill scan --sample --path ${rootPath}`,
1010
- `npx aibill report --sample --out ai-spend-report --path ${rootPath}`
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
- 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([
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
- `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"));
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>] Initialize local state",
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; local state is unchanged.",
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
  };