ai-spend-agent 0.1.5 → 0.2.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/dist/index.js +93 -9
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -3,7 +3,7 @@ import { realpathSync } from "node:fs";
|
|
|
3
3
|
import { mkdir, readFile, rm, stat, writeFile } from "node:fs/promises";
|
|
4
4
|
import { dirname, extname, join, resolve } from "node:path";
|
|
5
5
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
6
|
-
import { analyzeSpend, attributeUsageRecords, detectLocalCredentials, redactSecrets, unsafeScanRootReason, loadDeadContext, sampleDeadContext, loadLocalAgentUsage, loadSampleUsageData, scanLocalUsageSignals, buildMissingSourcePrompts, confirmMapping, createProviderConnectorStub, createLocalFolderSourceRegistry, createScanAuditLog, fetchProviderUsageRecords, addApprovedSource, slugifySourceId } from "@agent-finops/core";
|
|
6
|
+
import { analyzeSpend, attributeUsageRecords, detectLocalCredentials, detectLocalPlans, redactSecrets, subscriptionPlans, unsafeScanRootReason, loadDeadContext, sampleDeadContext, loadLocalAgentUsage, loadSampleUsageData, scanLocalUsageSignals, buildMissingSourcePrompts, confirmMapping, createProviderConnectorStub, createLocalFolderSourceRegistry, createScanAuditLog, fetchProviderUsageRecords, addApprovedSource, slugifySourceId } from "@agent-finops/core";
|
|
7
7
|
import { generateActionPlanMarkdown, generateApplyArtifactMarkdown, generateDemoPackageMarkdown, generateHtmlReport, generateMarkdownReport, generatePlainEnglishSummary, generatePolicyConfigDraftMarkdown, generateReportCardCaption, generateReportCardSvg, generateVerificationPlanMarkdown, groupByDimensions } from "@agent-finops/report";
|
|
8
8
|
export async function runCli(argv = process.argv.slice(2)) {
|
|
9
9
|
if (argv.includes("--help") || argv.includes("-h") || argv[0] === "help") {
|
|
@@ -70,6 +70,27 @@ async function quickstartCommand(args) {
|
|
|
70
70
|
const summary = analyzeSpend(records);
|
|
71
71
|
const groupBy = args.groupBy ?? "model";
|
|
72
72
|
const color = args.noColor ? false : undefined;
|
|
73
|
+
// Persona: --plan override wins; otherwise read the plans the coding agents
|
|
74
|
+
// themselves persisted locally (read-only, whitelisted fields, no network).
|
|
75
|
+
let detectedPlans;
|
|
76
|
+
if (args.plan) {
|
|
77
|
+
const override = planOverrideFromFlag(args.plan);
|
|
78
|
+
if (!override) {
|
|
79
|
+
return {
|
|
80
|
+
exitCode: 1,
|
|
81
|
+
stdout: "",
|
|
82
|
+
stderr: `Unknown --plan "${args.plan}". Valid plans: ${subscriptionPlans.map((plan) => plan.id).join(", ")}`
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
detectedPlans = [override];
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
detectedPlans = await detectLocalPlans({
|
|
89
|
+
// Env overrides keep tests (and unusual installs) isolated from $HOME.
|
|
90
|
+
claudeConfigPath: process.env.AI_SPEND_CLAUDE_CONFIG,
|
|
91
|
+
codexAuthPath: process.env.AI_SPEND_CODEX_AUTH
|
|
92
|
+
}).catch(() => []);
|
|
93
|
+
}
|
|
73
94
|
// Surface auto-detected credentials so the user knows their next 2-min step,
|
|
74
95
|
// without ever printing a raw secret.
|
|
75
96
|
const detection = await detectLocalCredentials({ cwd: resolve(args.path) });
|
|
@@ -102,7 +123,8 @@ async function quickstartCommand(args) {
|
|
|
102
123
|
color,
|
|
103
124
|
mode,
|
|
104
125
|
nextSteps,
|
|
105
|
-
deadContext
|
|
126
|
+
deadContext,
|
|
127
|
+
detectedPlans
|
|
106
128
|
});
|
|
107
129
|
const header = [` ${dataModeBanner(mode)}`, ...warnings.map((warning) => ` ! ${warning}`)].join("\n");
|
|
108
130
|
return ok(`${header}\n${summaryText}`);
|
|
@@ -190,6 +212,13 @@ async function doctorCommand(args) {
|
|
|
190
212
|
const hasLogs = claudeFound || codexFound;
|
|
191
213
|
const detection = await detectLocalCredentials({ cwd: rootPath }).catch(() => ({ credentials: [] }));
|
|
192
214
|
const providerRefs = detection.credentials.map((credential) => `${credential.provider} (${credential.hint})`);
|
|
215
|
+
const plans = await detectLocalPlans({
|
|
216
|
+
claudeConfigPath: process.env.AI_SPEND_CLAUDE_CONFIG,
|
|
217
|
+
codexAuthPath: process.env.AI_SPEND_CODEX_AUTH
|
|
218
|
+
}).catch(() => []);
|
|
219
|
+
const planLine = plans.length > 0
|
|
220
|
+
? plans.map((plan) => `${plan.planLabel} (${plan.agent}, ${plan.billing === "api_key" ? "pay per token" : plan.billing})`).join(", ")
|
|
221
|
+
: "none detected (use --plan to declare one)";
|
|
193
222
|
const warnings = [];
|
|
194
223
|
if (stateMode === "sample")
|
|
195
224
|
warnings.push("sample state present — it will be shown as DEMO and cannot mask real logs; run `ai-spend-agent reset` to clear it");
|
|
@@ -215,6 +244,7 @@ async function doctorCommand(args) {
|
|
|
215
244
|
`Claude Code logs: ${claudeFound ? "found" : "not found"}`,
|
|
216
245
|
`Codex logs: ${codexFound ? "found" : "not found"}`,
|
|
217
246
|
`provider env references: ${providerRefs.length > 0 ? providerRefs.join(", ") : "none detected"}`,
|
|
247
|
+
`subscription plans: ${planLine}`,
|
|
218
248
|
"redaction policy: secrets are never printed or persisted",
|
|
219
249
|
"plan check: available (subscription vs API-rate math)",
|
|
220
250
|
`data mode you'll get now: ${predictedMode}`,
|
|
@@ -663,6 +693,20 @@ async function connectCommand(args) {
|
|
|
663
693
|
lines.push(`missing: ${source.fieldsMissing.join(", ")}`);
|
|
664
694
|
return ok(lines.join("\n"));
|
|
665
695
|
}
|
|
696
|
+
/** Map a --plan id to a synthetic DetectedPlan (explicit user override). */
|
|
697
|
+
function planOverrideFromFlag(planId) {
|
|
698
|
+
const plan = subscriptionPlans.find((candidate) => candidate.id === planId);
|
|
699
|
+
if (!plan)
|
|
700
|
+
return undefined;
|
|
701
|
+
return {
|
|
702
|
+
agent: plan.agent,
|
|
703
|
+
provider: plan.provider,
|
|
704
|
+
planId: plan.id,
|
|
705
|
+
planLabel: plan.name,
|
|
706
|
+
billing: "subscription",
|
|
707
|
+
source: "--plan override"
|
|
708
|
+
};
|
|
709
|
+
}
|
|
666
710
|
function describeOrigin(credential) {
|
|
667
711
|
if (credential.origin === "process_env")
|
|
668
712
|
return "your shell environment";
|
|
@@ -791,7 +835,7 @@ async function reportCommand(args) {
|
|
|
791
835
|
return {
|
|
792
836
|
exitCode: 1,
|
|
793
837
|
stdout: "",
|
|
794
|
-
stderr: `
|
|
838
|
+
stderr: `Couldn't build a report: ${error instanceof Error ? error.message : String(error)}`
|
|
795
839
|
};
|
|
796
840
|
}
|
|
797
841
|
}
|
|
@@ -853,15 +897,35 @@ async function applyArtifactCommand(args) {
|
|
|
853
897
|
return {
|
|
854
898
|
exitCode: 1,
|
|
855
899
|
stdout: "",
|
|
856
|
-
stderr: `
|
|
900
|
+
stderr: `Couldn't build apply artifacts: ${error instanceof Error ? error.message : String(error)}`
|
|
857
901
|
};
|
|
858
902
|
}
|
|
859
903
|
}
|
|
860
904
|
async function buildReportInput(stateDir, rootPath) {
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
905
|
+
let spendState = await readOptionalJson(join(stateDir, "spend.json"), undefined);
|
|
906
|
+
let mappings = await readOptionalJson(join(stateDir, "mappings.json"), undefined);
|
|
907
|
+
// The quickstart intentionally writes nothing — so `report`/`apply-artifact`
|
|
908
|
+
// right after a first run must fall back to the SAME live local-log read
|
|
909
|
+
// (never sample data), then persist it so subsequent runs are consistent.
|
|
910
|
+
if (!spendState?.summary || !spendState.records || spendState.records.length === 0) {
|
|
911
|
+
const logs = await loadLocalAgentUsage({
|
|
912
|
+
claudeProjectsDir: process.env.AI_SPEND_CLAUDE_LOGS_DIR,
|
|
913
|
+
codexSessionsDir: process.env.AI_SPEND_CODEX_LOGS_DIR
|
|
914
|
+
}).catch(() => undefined);
|
|
915
|
+
if (!logs || logs.records.length === 0) {
|
|
916
|
+
throw new Error("no persisted spend state and no local Claude Code/Codex logs found. " +
|
|
917
|
+
"Run `npx ai-spend-agent` first (or `npx ai-spend-agent scan --sample --path <dir>` for a demo-data report).");
|
|
918
|
+
}
|
|
919
|
+
const records = logs.records;
|
|
920
|
+
const summary = analyzeSpend(records);
|
|
921
|
+
const liveMappings = attributeUsageRecords(records);
|
|
922
|
+
await mkdir(stateDir, { recursive: true });
|
|
923
|
+
await writeLocalSpendState(stateDir, records, summary, liveMappings, "local_logs");
|
|
924
|
+
spendState = { summary, records, mode: "local_logs" };
|
|
925
|
+
mappings = liveMappings;
|
|
926
|
+
}
|
|
927
|
+
const [discovery, sourceRegistry, missingSourcePrompts, confirmedMappings, providerRecordsState] = await Promise.all([
|
|
928
|
+
readOptionalJson(join(stateDir, "discovery.json"), emptyDiscovery(rootPath)),
|
|
865
929
|
readSourceRegistry(stateDir, rootPath),
|
|
866
930
|
readOptionalJson(join(stateDir, "missing-sources.json"), []),
|
|
867
931
|
readConfirmedMappings(stateDir),
|
|
@@ -874,7 +938,7 @@ async function buildReportInput(stateDir, rootPath) {
|
|
|
874
938
|
allRecords: spendState.records ?? [],
|
|
875
939
|
dataMode: spendState.mode,
|
|
876
940
|
discovery,
|
|
877
|
-
mappings,
|
|
941
|
+
mappings: mappings ?? [],
|
|
878
942
|
sourceRegistry,
|
|
879
943
|
missingSourcePrompts,
|
|
880
944
|
confirmedMappings,
|
|
@@ -882,6 +946,17 @@ async function buildReportInput(stateDir, rootPath) {
|
|
|
882
946
|
providerQa: providerRecordsState.qa ? [providerRecordsState.qa] : []
|
|
883
947
|
};
|
|
884
948
|
}
|
|
949
|
+
function emptyDiscovery(rootPath) {
|
|
950
|
+
return {
|
|
951
|
+
rootPath,
|
|
952
|
+
scannedFiles: 0,
|
|
953
|
+
skippedDirectories: [],
|
|
954
|
+
unreadablePaths: [],
|
|
955
|
+
signals: [],
|
|
956
|
+
secretsDetected: [],
|
|
957
|
+
redactedEvidence: []
|
|
958
|
+
};
|
|
959
|
+
}
|
|
885
960
|
async function writeApplyArtifacts(stateDir, reportInput) {
|
|
886
961
|
const paths = {
|
|
887
962
|
codingPrompt: join(stateDir, "ai-spend-coding-agent-prompt.md"),
|
|
@@ -927,6 +1002,14 @@ function parseArgs(argv) {
|
|
|
927
1002
|
parsed.ignoreState = true;
|
|
928
1003
|
continue;
|
|
929
1004
|
}
|
|
1005
|
+
if (arg === "--plan") {
|
|
1006
|
+
const next = rest[index + 1];
|
|
1007
|
+
if (next) {
|
|
1008
|
+
parsed.plan = next;
|
|
1009
|
+
index += 1;
|
|
1010
|
+
}
|
|
1011
|
+
continue;
|
|
1012
|
+
}
|
|
930
1013
|
if (arg === "--group-by") {
|
|
931
1014
|
const next = rest[index + 1];
|
|
932
1015
|
if (isGroupByDimension(next)) {
|
|
@@ -1203,6 +1286,7 @@ function helpText() {
|
|
|
1203
1286
|
"Run with no command for an instant, zero-key demo:",
|
|
1204
1287
|
" ai-spend-agent Show where your AI money goes (sample/auto-detected data)",
|
|
1205
1288
|
" ai-spend-agent --group-by agent Drill down by source|model|client|project|agent|user|workspace|apiKey",
|
|
1289
|
+
" ai-spend-agent --plan <id> Declare your plan when auto-detection can't (claude-max-5x|claude-max-20x|claude-pro|chatgpt-plus|chatgpt-pro)",
|
|
1206
1290
|
"",
|
|
1207
1291
|
"Connect your real spend (cost data is ADMIN/owner-gated):",
|
|
1208
1292
|
" ai-spend-agent connect openai Self-serve in ~2 min with an org-owner Admin key",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ai-spend-agent",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Your AI spend in one view, in 90 seconds — local-first CLI for OpenAI, Anthropic, Cursor, Copilot + Claude Code/Codex session logs, with a ranked savings cut list, and flags the tools your agent loads but never uses (dead context).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -49,8 +49,8 @@
|
|
|
49
49
|
"prepack": "npm run build"
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
|
-
"@agent-finops/core": "0.1
|
|
53
|
-
"@agent-finops/report": "0.1
|
|
52
|
+
"@agent-finops/core": "0.2.1",
|
|
53
|
+
"@agent-finops/report": "0.2.1",
|
|
54
54
|
"yocto-spinner": "^1.2.0"
|
|
55
55
|
}
|
|
56
56
|
}
|