ai-spend-agent 0.2.0 → 0.2.2

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 +60 -12
  2. package/package.json +3 -3
package/dist/index.js CHANGED
@@ -124,7 +124,10 @@ async function quickstartCommand(args) {
124
124
  mode,
125
125
  nextSteps,
126
126
  deadContext,
127
- detectedPlans
127
+ detectedPlans,
128
+ // An explicit --group-by is a drill-down question: answer with just the
129
+ // table + window instead of repeating the whole readout.
130
+ view: args.groupBy ? "breakdown" : "full"
128
131
  });
129
132
  const header = [` ${dataModeBanner(mode)}`, ...warnings.map((warning) => ` ! ${warning}`)].join("\n");
130
133
  return ok(`${header}\n${summaryText}`);
@@ -172,7 +175,10 @@ async function loadInstantReadData(args) {
172
175
  codexSessionsDir: process.env.AI_SPEND_CODEX_LOGS_DIR
173
176
  }).catch(() => undefined);
174
177
  if (logs && logs.records.length > 0) {
175
- if (persisted && persisted.records.length > 0 && persisted.mode !== "connected_provider") {
178
+ // Persisted local_logs state (written by report/apply-artifact) is the
179
+ // same data source we just re-read — superseding it silently is correct,
180
+ // not worth a scary "sample/legacy" warning.
181
+ if (persisted && persisted.records.length > 0 && persisted.mode !== "connected_provider" && persisted.mode !== "local_logs") {
176
182
  warnings.push("Ignored persisted sample/legacy state in .ai-spend-agent/spend.json — showing your real local agent logs. Run `ai-spend-agent reset` to clear it, or pass --ignore-state.");
177
183
  }
178
184
  return { records: logs.records, mode: "local-logs", warnings };
@@ -828,14 +834,19 @@ async function reportCommand(args) {
828
834
  `verification plan: ${artifactPaths.verificationPlan}`,
829
835
  `demo package: ${artifactPaths.demoPackage}`,
830
836
  `total spend: $${reportInput.summary.totalUsd.toFixed(2)}`,
831
- "privacy: local files only; no cloud upload performed"
837
+ "privacy: local files only; no cloud upload performed",
838
+ "",
839
+ "next:",
840
+ ` open ${htmlPath} view the full report in your browser`,
841
+ ` less ${markdownPath} read it in the terminal`,
842
+ " npx ai-spend-agent apply-artifact print the paste-ready coding-agent prompt"
832
843
  ].join("\n"));
833
844
  }
834
845
  catch (error) {
835
846
  return {
836
847
  exitCode: 1,
837
848
  stdout: "",
838
- stderr: `No local spend state found at ${stateDir}. Run scan --sample --path <dir> first. ${error instanceof Error ? error.message : ""}`
849
+ stderr: `Couldn't build a report: ${error instanceof Error ? error.message : String(error)}`
839
850
  };
840
851
  }
841
852
  }
@@ -882,30 +893,56 @@ async function applyArtifactCommand(args) {
882
893
  try {
883
894
  const reportInput = await buildReportInput(stateDir, rootPath);
884
895
  const artifactPaths = await writeApplyArtifacts(stateDir, reportInput);
896
+ // The prompt IS the product of this command — print it so a terminal
897
+ // user can copy it right here instead of hunting for a file path.
898
+ const codingPrompt = await readFile(artifactPaths.codingPrompt, "utf8");
885
899
  return ok([
886
900
  "AI Spend Analyst Agent apply-artifact",
887
901
  `path: ${rootPath}`,
888
- `coding prompt: ${artifactPaths.codingPrompt}`,
889
902
  `action plan: ${artifactPaths.actionPlan}`,
890
903
  `policy/config draft: ${artifactPaths.policyConfigDraft}`,
891
904
  `verification plan: ${artifactPaths.verificationPlan}`,
892
905
  `demo package: ${artifactPaths.demoPackage}`,
893
- "safety: generated artifacts only; no external systems changed"
906
+ "safety: generated artifacts only; no external systems changed",
907
+ "",
908
+ `──── copy everything below into Claude Code / Codex (also saved at ${artifactPaths.codingPrompt}) ────`,
909
+ "",
910
+ codingPrompt.trimEnd()
894
911
  ].join("\n"));
895
912
  }
896
913
  catch (error) {
897
914
  return {
898
915
  exitCode: 1,
899
916
  stdout: "",
900
- stderr: `No local spend state found at ${stateDir}. Run scan --sample --path <dir> first. ${error instanceof Error ? error.message : ""}`
917
+ stderr: `Couldn't build apply artifacts: ${error instanceof Error ? error.message : String(error)}`
901
918
  };
902
919
  }
903
920
  }
904
921
  async function buildReportInput(stateDir, rootPath) {
905
- const [spendState, discovery, mappings, sourceRegistry, missingSourcePrompts, confirmedMappings, providerRecordsState] = await Promise.all([
906
- readJson(join(stateDir, "spend.json")),
907
- readJson(join(stateDir, "discovery.json")),
908
- readJson(join(stateDir, "mappings.json")),
922
+ let spendState = await readOptionalJson(join(stateDir, "spend.json"), undefined);
923
+ let mappings = await readOptionalJson(join(stateDir, "mappings.json"), undefined);
924
+ // The quickstart intentionally writes nothing — so `report`/`apply-artifact`
925
+ // right after a first run must fall back to the SAME live local-log read
926
+ // (never sample data), then persist it so subsequent runs are consistent.
927
+ if (!spendState?.summary || !spendState.records || spendState.records.length === 0) {
928
+ const logs = await loadLocalAgentUsage({
929
+ claudeProjectsDir: process.env.AI_SPEND_CLAUDE_LOGS_DIR,
930
+ codexSessionsDir: process.env.AI_SPEND_CODEX_LOGS_DIR
931
+ }).catch(() => undefined);
932
+ if (!logs || logs.records.length === 0) {
933
+ throw new Error("no persisted spend state and no local Claude Code/Codex logs found. " +
934
+ "Run `npx ai-spend-agent` first (or `npx ai-spend-agent scan --sample --path <dir>` for a demo-data report).");
935
+ }
936
+ const records = logs.records;
937
+ const summary = analyzeSpend(records);
938
+ const liveMappings = attributeUsageRecords(records);
939
+ await mkdir(stateDir, { recursive: true });
940
+ await writeLocalSpendState(stateDir, records, summary, liveMappings, "local_logs");
941
+ spendState = { summary, records, mode: "local_logs" };
942
+ mappings = liveMappings;
943
+ }
944
+ const [discovery, sourceRegistry, missingSourcePrompts, confirmedMappings, providerRecordsState] = await Promise.all([
945
+ readOptionalJson(join(stateDir, "discovery.json"), emptyDiscovery(rootPath)),
909
946
  readSourceRegistry(stateDir, rootPath),
910
947
  readOptionalJson(join(stateDir, "missing-sources.json"), []),
911
948
  readConfirmedMappings(stateDir),
@@ -918,7 +955,7 @@ async function buildReportInput(stateDir, rootPath) {
918
955
  allRecords: spendState.records ?? [],
919
956
  dataMode: spendState.mode,
920
957
  discovery,
921
- mappings,
958
+ mappings: mappings ?? [],
922
959
  sourceRegistry,
923
960
  missingSourcePrompts,
924
961
  confirmedMappings,
@@ -926,6 +963,17 @@ async function buildReportInput(stateDir, rootPath) {
926
963
  providerQa: providerRecordsState.qa ? [providerRecordsState.qa] : []
927
964
  };
928
965
  }
966
+ function emptyDiscovery(rootPath) {
967
+ return {
968
+ rootPath,
969
+ scannedFiles: 0,
970
+ skippedDirectories: [],
971
+ unreadablePaths: [],
972
+ signals: [],
973
+ secretsDetected: [],
974
+ redactedEvidence: []
975
+ };
976
+ }
929
977
  async function writeApplyArtifacts(stateDir, reportInput) {
930
978
  const paths = {
931
979
  codingPrompt: join(stateDir, "ai-spend-coding-agent-prompt.md"),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-spend-agent",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
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.2.0",
53
- "@agent-finops/report": "0.2.0",
52
+ "@agent-finops/core": "0.2.2",
53
+ "@agent-finops/report": "0.2.2",
54
54
  "yocto-spinner": "^1.2.0"
55
55
  }
56
56
  }