ai-spend-agent 0.6.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -0
- package/dist/index.d.ts +10 -1
- package/dist/index.js +247 -31
- package/dist/statuslineInstaller.d.ts +80 -0
- package/dist/statuslineInstaller.js +1100 -0
- package/dist/statuslineRuntime.d.ts +125 -0
- package/dist/statuslineRuntime.js +917 -0
- package/package.json +3 -3
package/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
|
|
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
|
|
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
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
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
|
|
1064
|
-
//
|
|
1065
|
-
|
|
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" &&
|
|
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, {
|
|
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
|
-
|
|
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
|
-
|
|
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
|