ai-spend-agent 0.6.1 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,6 +4,7 @@ The full [aibill](https://github.com/futurastudio/ai-spend-agent) CLI.
4
4
 
5
5
  ```bash
6
6
  npx aibill init
7
+ npx aibill statusline install # optional Claude Code cache-only status line
7
8
  npx ai-spend-agent
8
9
  # short alias
9
10
  npx aibill
@@ -15,6 +16,13 @@ aggregate cache under `~/.aibill/cache/`. Init never replaces missing personal
15
16
  evidence with the bundled sample and never overwrites existing connected
16
17
  source or audit state.
17
18
 
19
+ The optional status line is explicit and reversible. It installs a standalone
20
+ Node-builtins-only runner at Claude user scope, rereads only the private
21
+ aggregate cache, and never scans transcripts or contacts a provider from the
22
+ hook. Subscription runway appears only when it was transcript-reported; `~`
23
+ means API-equivalent value, and untilded `billed` money requires verified
24
+ provider evidence. Remove it with `npx aibill statusline uninstall`.
25
+
18
26
  It reads local Claude Code and Codex metadata, labels API-equivalent estimates,
19
27
  and can optionally add official OpenAI or Anthropic provider-reported cost
20
28
  through an environment-variable reference. No product telemetry is sent.
package/dist/index.d.ts CHANGED
@@ -4,7 +4,16 @@ export type CliResult = {
4
4
  stdout: string;
5
5
  stderr: string;
6
6
  };
7
- export declare function runCli(argv?: string[]): Promise<CliResult>;
7
+ export type CliRuntimeOptions = {
8
+ /** Test/embedding override. Production always defaults to the OS home. */
9
+ homeDirectory?: string;
10
+ /** Test/embedding override. Packed production reads the built runtime asset. */
11
+ statuslineRunnerContents?: string | Uint8Array;
12
+ statuslineNow?: Date;
13
+ statuslineColumns?: number;
14
+ statuslineTimeZone?: string;
15
+ };
16
+ export declare function runCli(argv?: string[], runtime?: CliRuntimeOptions): Promise<CliResult>;
8
17
  /**
9
18
  * Full bin entrypoint (node guard, spinner, error voice, exit code).
10
19
  * Exported so the thin alias packages (`aispend`, `aireceipt`) run the EXACT
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
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);
@@ -998,7 +1004,105 @@ async function resetCommand(args) {
998
1004
  "next run will re-read your real local agent logs (or demo sample if none)."
999
1005
  ].join("\n"));
1000
1006
  }
1001
- async function initCommand(args) {
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 = {}) {
1002
1106
  if (args.sample) {
1003
1107
  return {
1004
1108
  exitCode: 1,
@@ -1050,20 +1154,56 @@ async function initCommand(args) {
1050
1154
  if (existingRegistry)
1051
1155
  normalizeSourceRegistry(existingRegistry);
1052
1156
  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.
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.
1056
1197
  const asOf = new Date();
1057
- const planPromise = detectedPlanOverride
1058
- ? Promise.resolve(detectedPlanOverride)
1198
+ const planPromise = input.detectedPlanOverride
1199
+ ? Promise.resolve(input.detectedPlanOverride)
1059
1200
  : detectLocalPlans({
1060
1201
  claudeConfigPath: process.env.AI_SPEND_CLAUDE_CONFIG,
1061
1202
  codexAuthPath: process.env.AI_SPEND_CODEX_AUTH
1062
1203
  }).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 }));
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 }));
1067
1207
  let logs;
1068
1208
  let scanError;
1069
1209
  try {
@@ -1080,7 +1220,8 @@ async function initCommand(args) {
1080
1220
  if ("error" in persistedResult)
1081
1221
  throw persistedResult.error;
1082
1222
  const persisted = persistedResult.persisted;
1083
- const trustedProviderRecords = persisted?.mode === "connected_provider" && persisted.connectedTrust?.trusted === true
1223
+ const trustedProviderRecords = persisted?.mode === "connected_provider" &&
1224
+ persisted.connectedTrust?.trusted === true
1084
1225
  ? selectProviderFinancialHeadlineRecords(persisted.records)
1085
1226
  : [];
1086
1227
  let activitySnapshot;
@@ -1091,10 +1232,9 @@ async function initCommand(args) {
1091
1232
  if (logs && !structuredSourceFailure) {
1092
1233
  let refreshErrorCode = "invalid_evidence";
1093
1234
  try {
1094
- const generatedAt = new Date().toISOString();
1095
1235
  activitySnapshot = buildActivitySnapshot({
1096
1236
  asOf: asOf.toISOString(),
1097
- generatedAt,
1237
+ generatedAt: new Date().toISOString(),
1098
1238
  records: [...logs.records, ...trustedProviderRecords],
1099
1239
  calls: logs.calls,
1100
1240
  detectedPlans,
@@ -1113,43 +1253,102 @@ async function initCommand(args) {
1113
1253
  sampleData: false
1114
1254
  });
1115
1255
  refreshErrorCode = "cache_write_failed";
1116
- const written = await writeActivitySnapshot(activitySnapshot, { cacheDirectory });
1256
+ const written = await writeActivitySnapshot(activitySnapshot, {
1257
+ cacheDirectory: input.cacheDirectory
1258
+ });
1117
1259
  cacheStatus = written.status === "written" ? "refreshed" : "kept newer snapshot";
1118
1260
  }
1119
1261
  catch {
1120
1262
  scanError = refreshErrorCode === "invalid_evidence"
1121
1263
  ? "the observed evidence could not produce a valid activity snapshot"
1122
1264
  : "the private activity cache could not be updated";
1123
- const failed = await recordActivitySnapshotRefreshFailure(asOf.toISOString(), refreshErrorCode, { cacheDirectory });
1265
+ const failed = await recordActivitySnapshotRefreshFailure(asOf.toISOString(), refreshErrorCode, { cacheDirectory: input.cacheDirectory });
1124
1266
  activitySnapshot = failed.snapshot;
1125
1267
  cacheStatus = "refresh failed";
1126
1268
  }
1127
1269
  }
1128
1270
  else {
1129
- const failed = await recordActivitySnapshotRefreshFailure(asOf.toISOString(), structuredSourceFailure ? "source_unreadable" : "scan_failed", { cacheDirectory });
1271
+ const failed = await recordActivitySnapshotRefreshFailure(asOf.toISOString(), structuredSourceFailure ? "source_unreadable" : "scan_failed", { cacheDirectory: input.cacheDirectory });
1130
1272
  activitySnapshot = failed.snapshot;
1131
1273
  cacheStatus = "refresh failed";
1132
1274
  }
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,
1275
+ return {
1142
1276
  asOf,
1143
1277
  activitySnapshot,
1144
1278
  logs,
1145
1279
  detectedPlans,
1146
1280
  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,
1281
+ persisted,
1150
1282
  scanError,
1151
1283
  cacheStatus
1152
- }));
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);
1153
1352
  }
1154
1353
  async function preflightInitCache(cacheDirectory) {
1155
1354
  const existing = await readActivitySnapshot({ cacheDirectory });
@@ -2546,6 +2745,9 @@ function parseArgs(argv) {
2546
2745
  sample: false,
2547
2746
  path: process.cwd()
2548
2747
  };
2748
+ if (command === "statusline" && rest[0] && !rest[0].startsWith("--")) {
2749
+ parsed.statuslineAction = rest.shift();
2750
+ }
2549
2751
  if (command === "connect" && rest[0] && !rest[0].startsWith("--")) {
2550
2752
  parsed.provider = rest[0];
2551
2753
  rest.shift();
@@ -2568,6 +2770,14 @@ function parseArgs(argv) {
2568
2770
  parsed.sources = true;
2569
2771
  continue;
2570
2772
  }
2773
+ if (arg === "--statusline") {
2774
+ parsed.statusline = true;
2775
+ continue;
2776
+ }
2777
+ if (arg === "--replace") {
2778
+ parsed.replaceStatusline = true;
2779
+ continue;
2780
+ }
2571
2781
  if (arg === "--ignore-state") {
2572
2782
  parsed.ignoreState = true;
2573
2783
  continue;
@@ -2942,6 +3152,12 @@ function helpText() {
2942
3152
  "Other commands:",
2943
3153
  " --version, -v Print the package version without reading local data",
2944
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",
2945
3161
  " doctor [--sources] Launch diagnostics; --sources shows validation, evidence, freshness, and errors",
2946
3162
  " reset [--path <dir>] Clear persisted spend state (so sample state can't mask real logs)",
2947
3163
  " --ignore-state On the default/quickstart run, ignore persisted spend.json for this run",
@@ -0,0 +1,80 @@
1
+ export declare const aibillStatuslineRefreshIntervalSeconds = 30;
2
+ export type StatuslinePlatform = "darwin" | "linux" | "win32";
3
+ export type StatuslinePaths = {
4
+ settingsPath: string;
5
+ runnerPath: string;
6
+ receiptPath: string;
7
+ backupDirectory: string;
8
+ lockPath: string;
9
+ projectSettingsPath: string;
10
+ localSettingsPath: string;
11
+ managedSettingsPath: string;
12
+ managedDropInDirectory: string;
13
+ };
14
+ export type AibillStatusLineSetting = {
15
+ type: "command";
16
+ command: string;
17
+ refreshInterval: 30;
18
+ };
19
+ type InstallerTestHooks = {
20
+ /** Runs only after every replacement file has been fully prepared and checked. */
21
+ afterPrepare?: () => void | Promise<void>;
22
+ /** Backward-compatible name used by the first installer fixtures. */
23
+ beforeSettingsCommit?: () => void | Promise<void>;
24
+ /** Tests can exercise a race between ordered transaction commit points. */
25
+ afterMutationCommit?: (path: string, index: number) => void | Promise<void>;
26
+ };
27
+ export type InstallStatuslineOptions = InstallerTestHooks & {
28
+ homeDir: string;
29
+ cwd: string;
30
+ /** Integration can pass a packaged asset path without importing renderer code. */
31
+ runnerSourcePath?: string;
32
+ /** Or pass the exact standalone runner bytes produced by the renderer lane. */
33
+ runnerContents?: string | Uint8Array;
34
+ platform?: StatuslinePlatform;
35
+ replace?: boolean;
36
+ now?: Date;
37
+ /** Windows only: the locally visible Program Files root. */
38
+ programFilesDir?: string;
39
+ /** Narrow fixture/embedding overrides; unspecified paths keep platform defaults. */
40
+ pathOverrides?: Partial<StatuslinePaths>;
41
+ /** Tests and embedding callers can supply explicit locally visible managed files. */
42
+ managedSettingsPaths?: string[];
43
+ };
44
+ export type UninstallStatuslineOptions = InstallerTestHooks & {
45
+ homeDir: string;
46
+ cwd: string;
47
+ platform?: StatuslinePlatform;
48
+ programFilesDir?: string;
49
+ pathOverrides?: Partial<StatuslinePaths>;
50
+ };
51
+ export type StatuslineInstallResult = {
52
+ action: "installed" | "unchanged";
53
+ settingsPath: string;
54
+ runnerPath: string;
55
+ receiptPath: string;
56
+ backupPath?: string;
57
+ };
58
+ export type StatuslineUninstallResult = {
59
+ action: "uninstalled";
60
+ settingsPath: string;
61
+ runnerPath: string;
62
+ runnerRemoved: boolean;
63
+ runnerAction: "removed" | "restored" | "preserved-modified" | "already-missing";
64
+ statusLineAction: "removed" | "restored-prior";
65
+ warnings: string[];
66
+ };
67
+ export type StatuslineInstallerErrorCode = "concurrent-edit" | "hooks-disabled" | "installer-busy" | "invalid-receipt" | "invalid-settings-json" | "missing-ownership" | "ownership-mismatch" | "settings-shadowed" | "statusline-conflict" | "unsafe-runner-source" | "unsafe-settings-file";
68
+ export declare class StatuslineInstallerError extends Error {
69
+ readonly code: StatuslineInstallerErrorCode;
70
+ constructor(code: StatuslineInstallerErrorCode, message: string);
71
+ }
72
+ export declare function resolveStatuslinePaths(homeDir: string, cwd: string, platform?: StatuslinePlatform, options?: {
73
+ programFilesDir?: string;
74
+ }): StatuslinePaths;
75
+ export declare function buildAibillStatusLineSetting(_platform?: StatuslinePlatform): AibillStatusLineSetting;
76
+ export declare function manualStatuslineConfigSnippet(platform?: StatuslinePlatform): string;
77
+ export declare function installClaudeStatusline(options: InstallStatuslineOptions): Promise<StatuslineInstallResult>;
78
+ export declare function uninstallClaudeStatusline(options: UninstallStatuslineOptions): Promise<StatuslineUninstallResult>;
79
+ export {};
80
+ //# sourceMappingURL=statuslineInstaller.d.ts.map