ai-spend-agent 0.1.5 → 0.2.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.
Files changed (2) hide show
  1. package/dist/index.js +55 -2
  2. 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";
@@ -927,6 +971,14 @@ function parseArgs(argv) {
927
971
  parsed.ignoreState = true;
928
972
  continue;
929
973
  }
974
+ if (arg === "--plan") {
975
+ const next = rest[index + 1];
976
+ if (next) {
977
+ parsed.plan = next;
978
+ index += 1;
979
+ }
980
+ continue;
981
+ }
930
982
  if (arg === "--group-by") {
931
983
  const next = rest[index + 1];
932
984
  if (isGroupByDimension(next)) {
@@ -1203,6 +1255,7 @@ function helpText() {
1203
1255
  "Run with no command for an instant, zero-key demo:",
1204
1256
  " ai-spend-agent Show where your AI money goes (sample/auto-detected data)",
1205
1257
  " ai-spend-agent --group-by agent Drill down by source|model|client|project|agent|user|workspace|apiKey",
1258
+ " 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
1259
  "",
1207
1260
  "Connect your real spend (cost data is ADMIN/owner-gated):",
1208
1261
  " 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.5",
3
+ "version": "0.2.0",
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.5",
53
- "@agent-finops/report": "0.1.5",
52
+ "@agent-finops/core": "0.2.0",
53
+ "@agent-finops/report": "0.2.0",
54
54
  "yocto-spinner": "^1.2.0"
55
55
  }
56
56
  }