@hasna/terminal 2.3.0 → 2.3.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 (267) hide show
  1. package/dist/App.js +404 -0
  2. package/dist/Browse.js +79 -0
  3. package/dist/FuzzyPicker.js +47 -0
  4. package/dist/Onboarding.js +51 -0
  5. package/dist/Spinner.js +12 -0
  6. package/dist/StatusBar.js +49 -0
  7. package/dist/ai.js +322 -0
  8. package/dist/cache.js +41 -0
  9. package/dist/cli.js +64 -16
  10. package/dist/command-rewriter.js +64 -0
  11. package/dist/command-validator.js +86 -0
  12. package/dist/compression.js +107 -0
  13. package/dist/context-hints.js +275 -0
  14. package/dist/diff-cache.js +107 -0
  15. package/dist/discover.js +212 -0
  16. package/dist/economy.js +123 -0
  17. package/dist/expand-store.js +38 -0
  18. package/dist/file-cache.js +72 -0
  19. package/dist/file-index.js +62 -0
  20. package/dist/history.js +62 -0
  21. package/dist/lazy-executor.js +54 -0
  22. package/dist/line-dedup.js +59 -0
  23. package/dist/loop-detector.js +75 -0
  24. package/dist/mcp/install.js +98 -0
  25. package/dist/mcp/server.js +569 -0
  26. package/dist/noise-filter.js +86 -0
  27. package/dist/output-processor.js +129 -0
  28. package/dist/output-router.js +41 -0
  29. package/dist/output-store.js +111 -0
  30. package/dist/parsers/base.js +2 -0
  31. package/dist/parsers/build.js +64 -0
  32. package/dist/parsers/errors.js +101 -0
  33. package/dist/parsers/files.js +78 -0
  34. package/dist/parsers/git.js +99 -0
  35. package/dist/parsers/index.js +48 -0
  36. package/dist/parsers/tests.js +89 -0
  37. package/dist/providers/anthropic.js +39 -0
  38. package/dist/providers/base.js +4 -0
  39. package/dist/providers/cerebras.js +95 -0
  40. package/dist/providers/groq.js +95 -0
  41. package/dist/providers/index.js +73 -0
  42. package/dist/providers/xai.js +95 -0
  43. package/dist/recipes/model.js +20 -0
  44. package/dist/recipes/storage.js +136 -0
  45. package/dist/search/content-search.js +68 -0
  46. package/dist/search/file-search.js +61 -0
  47. package/dist/search/filters.js +34 -0
  48. package/dist/search/index.js +5 -0
  49. package/dist/search/semantic.js +320 -0
  50. package/dist/session-boot.js +59 -0
  51. package/dist/session-context.js +55 -0
  52. package/dist/sessions-db.js +173 -0
  53. package/dist/smart-display.js +286 -0
  54. package/dist/snapshots.js +51 -0
  55. package/dist/supervisor.js +112 -0
  56. package/dist/test-watchlist.js +131 -0
  57. package/dist/tool-profiles.js +122 -0
  58. package/dist/tree.js +94 -0
  59. package/dist/usage-cache.js +65 -0
  60. package/package.json +8 -1
  61. package/src/ai.ts +8 -0
  62. package/src/cli.tsx +57 -18
  63. package/src/output-processor.ts +6 -1
  64. package/src/output-store.ts +58 -12
  65. package/src/tool-profiles.ts +139 -0
  66. package/.claude/scheduled_tasks.lock +0 -1
  67. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -20
  68. package/.github/ISSUE_TEMPLATE/feature_request.md +0 -14
  69. package/CONTRIBUTING.md +0 -80
  70. package/benchmarks/benchmark.mjs +0 -115
  71. package/imported_modules.txt +0 -0
  72. package/temp/rtk/.claude/agents/code-reviewer.md +0 -221
  73. package/temp/rtk/.claude/agents/debugger.md +0 -519
  74. package/temp/rtk/.claude/agents/rtk-testing-specialist.md +0 -461
  75. package/temp/rtk/.claude/agents/rust-rtk.md +0 -511
  76. package/temp/rtk/.claude/agents/technical-writer.md +0 -355
  77. package/temp/rtk/.claude/commands/diagnose.md +0 -352
  78. package/temp/rtk/.claude/commands/test-routing.md +0 -362
  79. package/temp/rtk/.claude/hooks/bash/pre-commit-format.sh +0 -16
  80. package/temp/rtk/.claude/hooks/rtk-rewrite.sh +0 -70
  81. package/temp/rtk/.claude/hooks/rtk-suggest.sh +0 -152
  82. package/temp/rtk/.claude/rules/cli-testing.md +0 -526
  83. package/temp/rtk/.claude/skills/issue-triage/SKILL.md +0 -348
  84. package/temp/rtk/.claude/skills/issue-triage/templates/issue-comment.md +0 -134
  85. package/temp/rtk/.claude/skills/performance.md +0 -435
  86. package/temp/rtk/.claude/skills/pr-triage/SKILL.md +0 -315
  87. package/temp/rtk/.claude/skills/pr-triage/templates/review-comment.md +0 -71
  88. package/temp/rtk/.claude/skills/repo-recap.md +0 -206
  89. package/temp/rtk/.claude/skills/rtk-tdd/SKILL.md +0 -78
  90. package/temp/rtk/.claude/skills/rtk-tdd/references/testing-patterns.md +0 -124
  91. package/temp/rtk/.claude/skills/security-guardian.md +0 -503
  92. package/temp/rtk/.claude/skills/ship.md +0 -404
  93. package/temp/rtk/.github/workflows/benchmark.yml +0 -34
  94. package/temp/rtk/.github/workflows/dco-check.yaml +0 -12
  95. package/temp/rtk/.github/workflows/release-please.yml +0 -51
  96. package/temp/rtk/.github/workflows/release.yml +0 -343
  97. package/temp/rtk/.github/workflows/security-check.yml +0 -135
  98. package/temp/rtk/.github/workflows/validate-docs.yml +0 -78
  99. package/temp/rtk/.release-please-manifest.json +0 -3
  100. package/temp/rtk/ARCHITECTURE.md +0 -1491
  101. package/temp/rtk/CHANGELOG.md +0 -640
  102. package/temp/rtk/CLAUDE.md +0 -605
  103. package/temp/rtk/CONTRIBUTING.md +0 -199
  104. package/temp/rtk/Cargo.lock +0 -1668
  105. package/temp/rtk/Cargo.toml +0 -64
  106. package/temp/rtk/Formula/rtk.rb +0 -43
  107. package/temp/rtk/INSTALL.md +0 -390
  108. package/temp/rtk/LICENSE +0 -21
  109. package/temp/rtk/README.md +0 -386
  110. package/temp/rtk/README_es.md +0 -159
  111. package/temp/rtk/README_fr.md +0 -197
  112. package/temp/rtk/README_ja.md +0 -159
  113. package/temp/rtk/README_ko.md +0 -159
  114. package/temp/rtk/README_zh.md +0 -167
  115. package/temp/rtk/ROADMAP.md +0 -15
  116. package/temp/rtk/SECURITY.md +0 -217
  117. package/temp/rtk/TEST_EXEC_TIME.md +0 -102
  118. package/temp/rtk/build.rs +0 -57
  119. package/temp/rtk/docs/AUDIT_GUIDE.md +0 -432
  120. package/temp/rtk/docs/FEATURES.md +0 -1410
  121. package/temp/rtk/docs/TROUBLESHOOTING.md +0 -309
  122. package/temp/rtk/docs/filter-workflow.md +0 -102
  123. package/temp/rtk/docs/images/gain-dashboard.jpg +0 -0
  124. package/temp/rtk/docs/tracking.md +0 -583
  125. package/temp/rtk/hooks/opencode-rtk.ts +0 -39
  126. package/temp/rtk/hooks/rtk-awareness.md +0 -29
  127. package/temp/rtk/hooks/rtk-rewrite.sh +0 -61
  128. package/temp/rtk/hooks/test-rtk-rewrite.sh +0 -442
  129. package/temp/rtk/install.sh +0 -124
  130. package/temp/rtk/release-please-config.json +0 -10
  131. package/temp/rtk/scripts/benchmark.sh +0 -592
  132. package/temp/rtk/scripts/check-installation.sh +0 -162
  133. package/temp/rtk/scripts/install-local.sh +0 -37
  134. package/temp/rtk/scripts/rtk-economics.sh +0 -137
  135. package/temp/rtk/scripts/test-all.sh +0 -561
  136. package/temp/rtk/scripts/test-aristote.sh +0 -227
  137. package/temp/rtk/scripts/test-tracking.sh +0 -79
  138. package/temp/rtk/scripts/update-readme-metrics.sh +0 -32
  139. package/temp/rtk/scripts/validate-docs.sh +0 -73
  140. package/temp/rtk/src/aws_cmd.rs +0 -880
  141. package/temp/rtk/src/binlog.rs +0 -1645
  142. package/temp/rtk/src/cargo_cmd.rs +0 -1727
  143. package/temp/rtk/src/cc_economics.rs +0 -1157
  144. package/temp/rtk/src/ccusage.rs +0 -340
  145. package/temp/rtk/src/config.rs +0 -187
  146. package/temp/rtk/src/container.rs +0 -855
  147. package/temp/rtk/src/curl_cmd.rs +0 -134
  148. package/temp/rtk/src/deps.rs +0 -268
  149. package/temp/rtk/src/diff_cmd.rs +0 -367
  150. package/temp/rtk/src/discover/mod.rs +0 -274
  151. package/temp/rtk/src/discover/provider.rs +0 -388
  152. package/temp/rtk/src/discover/registry.rs +0 -2022
  153. package/temp/rtk/src/discover/report.rs +0 -202
  154. package/temp/rtk/src/discover/rules.rs +0 -667
  155. package/temp/rtk/src/display_helpers.rs +0 -402
  156. package/temp/rtk/src/dotnet_cmd.rs +0 -1771
  157. package/temp/rtk/src/dotnet_format_report.rs +0 -133
  158. package/temp/rtk/src/dotnet_trx.rs +0 -593
  159. package/temp/rtk/src/env_cmd.rs +0 -204
  160. package/temp/rtk/src/filter.rs +0 -462
  161. package/temp/rtk/src/filters/README.md +0 -52
  162. package/temp/rtk/src/filters/ansible-playbook.toml +0 -34
  163. package/temp/rtk/src/filters/basedpyright.toml +0 -47
  164. package/temp/rtk/src/filters/biome.toml +0 -45
  165. package/temp/rtk/src/filters/brew-install.toml +0 -37
  166. package/temp/rtk/src/filters/composer-install.toml +0 -40
  167. package/temp/rtk/src/filters/df.toml +0 -16
  168. package/temp/rtk/src/filters/dotnet-build.toml +0 -64
  169. package/temp/rtk/src/filters/du.toml +0 -16
  170. package/temp/rtk/src/filters/fail2ban-client.toml +0 -15
  171. package/temp/rtk/src/filters/gcc.toml +0 -49
  172. package/temp/rtk/src/filters/gcloud.toml +0 -22
  173. package/temp/rtk/src/filters/hadolint.toml +0 -24
  174. package/temp/rtk/src/filters/helm.toml +0 -29
  175. package/temp/rtk/src/filters/iptables.toml +0 -27
  176. package/temp/rtk/src/filters/jj.toml +0 -28
  177. package/temp/rtk/src/filters/jq.toml +0 -24
  178. package/temp/rtk/src/filters/make.toml +0 -41
  179. package/temp/rtk/src/filters/markdownlint.toml +0 -24
  180. package/temp/rtk/src/filters/mix-compile.toml +0 -27
  181. package/temp/rtk/src/filters/mix-format.toml +0 -15
  182. package/temp/rtk/src/filters/mvn-build.toml +0 -44
  183. package/temp/rtk/src/filters/oxlint.toml +0 -43
  184. package/temp/rtk/src/filters/ping.toml +0 -63
  185. package/temp/rtk/src/filters/pio-run.toml +0 -40
  186. package/temp/rtk/src/filters/poetry-install.toml +0 -50
  187. package/temp/rtk/src/filters/pre-commit.toml +0 -35
  188. package/temp/rtk/src/filters/ps.toml +0 -16
  189. package/temp/rtk/src/filters/quarto-render.toml +0 -41
  190. package/temp/rtk/src/filters/rsync.toml +0 -48
  191. package/temp/rtk/src/filters/shellcheck.toml +0 -27
  192. package/temp/rtk/src/filters/shopify-theme.toml +0 -29
  193. package/temp/rtk/src/filters/skopeo.toml +0 -45
  194. package/temp/rtk/src/filters/sops.toml +0 -16
  195. package/temp/rtk/src/filters/ssh.toml +0 -44
  196. package/temp/rtk/src/filters/stat.toml +0 -34
  197. package/temp/rtk/src/filters/swift-build.toml +0 -41
  198. package/temp/rtk/src/filters/systemctl-status.toml +0 -33
  199. package/temp/rtk/src/filters/terraform-plan.toml +0 -35
  200. package/temp/rtk/src/filters/tofu-fmt.toml +0 -16
  201. package/temp/rtk/src/filters/tofu-init.toml +0 -38
  202. package/temp/rtk/src/filters/tofu-plan.toml +0 -35
  203. package/temp/rtk/src/filters/tofu-validate.toml +0 -17
  204. package/temp/rtk/src/filters/trunk-build.toml +0 -39
  205. package/temp/rtk/src/filters/ty.toml +0 -50
  206. package/temp/rtk/src/filters/uv-sync.toml +0 -37
  207. package/temp/rtk/src/filters/xcodebuild.toml +0 -99
  208. package/temp/rtk/src/filters/yamllint.toml +0 -25
  209. package/temp/rtk/src/find_cmd.rs +0 -598
  210. package/temp/rtk/src/format_cmd.rs +0 -386
  211. package/temp/rtk/src/gain.rs +0 -723
  212. package/temp/rtk/src/gh_cmd.rs +0 -1651
  213. package/temp/rtk/src/git.rs +0 -2012
  214. package/temp/rtk/src/go_cmd.rs +0 -592
  215. package/temp/rtk/src/golangci_cmd.rs +0 -254
  216. package/temp/rtk/src/grep_cmd.rs +0 -288
  217. package/temp/rtk/src/gt_cmd.rs +0 -810
  218. package/temp/rtk/src/hook_audit_cmd.rs +0 -283
  219. package/temp/rtk/src/hook_check.rs +0 -171
  220. package/temp/rtk/src/init.rs +0 -1859
  221. package/temp/rtk/src/integrity.rs +0 -537
  222. package/temp/rtk/src/json_cmd.rs +0 -231
  223. package/temp/rtk/src/learn/detector.rs +0 -628
  224. package/temp/rtk/src/learn/mod.rs +0 -119
  225. package/temp/rtk/src/learn/report.rs +0 -184
  226. package/temp/rtk/src/lint_cmd.rs +0 -694
  227. package/temp/rtk/src/local_llm.rs +0 -316
  228. package/temp/rtk/src/log_cmd.rs +0 -248
  229. package/temp/rtk/src/ls.rs +0 -324
  230. package/temp/rtk/src/main.rs +0 -2482
  231. package/temp/rtk/src/mypy_cmd.rs +0 -389
  232. package/temp/rtk/src/next_cmd.rs +0 -241
  233. package/temp/rtk/src/npm_cmd.rs +0 -236
  234. package/temp/rtk/src/parser/README.md +0 -267
  235. package/temp/rtk/src/parser/error.rs +0 -46
  236. package/temp/rtk/src/parser/formatter.rs +0 -336
  237. package/temp/rtk/src/parser/mod.rs +0 -311
  238. package/temp/rtk/src/parser/types.rs +0 -119
  239. package/temp/rtk/src/pip_cmd.rs +0 -302
  240. package/temp/rtk/src/playwright_cmd.rs +0 -479
  241. package/temp/rtk/src/pnpm_cmd.rs +0 -573
  242. package/temp/rtk/src/prettier_cmd.rs +0 -221
  243. package/temp/rtk/src/prisma_cmd.rs +0 -482
  244. package/temp/rtk/src/psql_cmd.rs +0 -382
  245. package/temp/rtk/src/pytest_cmd.rs +0 -384
  246. package/temp/rtk/src/read.rs +0 -217
  247. package/temp/rtk/src/rewrite_cmd.rs +0 -50
  248. package/temp/rtk/src/ruff_cmd.rs +0 -402
  249. package/temp/rtk/src/runner.rs +0 -271
  250. package/temp/rtk/src/summary.rs +0 -297
  251. package/temp/rtk/src/tee.rs +0 -405
  252. package/temp/rtk/src/telemetry.rs +0 -248
  253. package/temp/rtk/src/toml_filter.rs +0 -1655
  254. package/temp/rtk/src/tracking.rs +0 -1416
  255. package/temp/rtk/src/tree.rs +0 -209
  256. package/temp/rtk/src/tsc_cmd.rs +0 -259
  257. package/temp/rtk/src/utils.rs +0 -432
  258. package/temp/rtk/src/verify_cmd.rs +0 -47
  259. package/temp/rtk/src/vitest_cmd.rs +0 -385
  260. package/temp/rtk/src/wc_cmd.rs +0 -401
  261. package/temp/rtk/src/wget_cmd.rs +0 -260
  262. package/temp/rtk/tests/fixtures/dotnet/build_failed.txt +0 -11
  263. package/temp/rtk/tests/fixtures/dotnet/format_changes.json +0 -31
  264. package/temp/rtk/tests/fixtures/dotnet/format_empty.json +0 -1
  265. package/temp/rtk/tests/fixtures/dotnet/format_success.json +0 -12
  266. package/temp/rtk/tests/fixtures/dotnet/test_failed.txt +0 -18
  267. package/tsconfig.json +0 -15
@@ -1,1157 +0,0 @@
1
- //! Claude Code Economics: Spending vs Savings Analysis
2
- //!
3
- //! Combines ccusage (tokens spent) with rtk tracking (tokens saved) to provide
4
- //! dual-metric economic impact reporting with blended and active cost-per-token.
5
-
6
- use anyhow::{Context, Result};
7
- use chrono::NaiveDate;
8
- use serde::Serialize;
9
- use std::collections::HashMap;
10
-
11
- use crate::ccusage::{self, CcusagePeriod, Granularity};
12
- use crate::tracking::{DayStats, MonthStats, Tracker, WeekStats};
13
- use crate::utils::{format_cpt, format_tokens, format_usd};
14
-
15
- // ── Constants ──
16
-
17
- const BILLION: f64 = 1e9;
18
-
19
- // API pricing ratios (verified Feb 2026, consistent across Claude models <=200K context)
20
- // Source: https://docs.anthropic.com/en/docs/about-claude/models
21
- const WEIGHT_OUTPUT: f64 = 5.0; // Output = 5x input
22
- const WEIGHT_CACHE_CREATE: f64 = 1.25; // Cache write = 1.25x input
23
- const WEIGHT_CACHE_READ: f64 = 0.1; // Cache read = 0.1x input
24
-
25
- // ── Types ──
26
-
27
- #[derive(Debug, Serialize)]
28
- pub struct PeriodEconomics {
29
- pub label: String,
30
- // ccusage metrics (Option for graceful degradation)
31
- pub cc_cost: Option<f64>,
32
- pub cc_total_tokens: Option<u64>,
33
- pub cc_active_tokens: Option<u64>, // input + output only (excluding cache)
34
- // Per-type token breakdown
35
- pub cc_input_tokens: Option<u64>,
36
- pub cc_output_tokens: Option<u64>,
37
- pub cc_cache_create_tokens: Option<u64>,
38
- pub cc_cache_read_tokens: Option<u64>,
39
- // rtk metrics
40
- pub rtk_commands: Option<usize>,
41
- pub rtk_saved_tokens: Option<usize>,
42
- pub rtk_savings_pct: Option<f64>,
43
- // Primary metric (weighted input CPT)
44
- pub weighted_input_cpt: Option<f64>, // Derived input CPT using API ratios
45
- pub savings_weighted: Option<f64>, // saved * weighted_input_cpt (PRIMARY)
46
- // Legacy metrics (verbose mode only)
47
- pub blended_cpt: Option<f64>, // cost / total_tokens (diluted by cache)
48
- pub active_cpt: Option<f64>, // cost / active_tokens (OVERESTIMATES)
49
- pub savings_blended: Option<f64>, // saved * blended_cpt (UNDERESTIMATES)
50
- pub savings_active: Option<f64>, // saved * active_cpt (OVERESTIMATES)
51
- }
52
-
53
- impl PeriodEconomics {
54
- fn new(label: &str) -> Self {
55
- Self {
56
- label: label.to_string(),
57
- cc_cost: None,
58
- cc_total_tokens: None,
59
- cc_active_tokens: None,
60
- cc_input_tokens: None,
61
- cc_output_tokens: None,
62
- cc_cache_create_tokens: None,
63
- cc_cache_read_tokens: None,
64
- rtk_commands: None,
65
- rtk_saved_tokens: None,
66
- rtk_savings_pct: None,
67
- weighted_input_cpt: None,
68
- savings_weighted: None,
69
- blended_cpt: None,
70
- active_cpt: None,
71
- savings_blended: None,
72
- savings_active: None,
73
- }
74
- }
75
-
76
- fn set_ccusage(&mut self, metrics: &ccusage::CcusageMetrics) {
77
- self.cc_cost = Some(metrics.total_cost);
78
- self.cc_total_tokens = Some(metrics.total_tokens);
79
-
80
- // Store per-type tokens
81
- self.cc_input_tokens = Some(metrics.input_tokens);
82
- self.cc_output_tokens = Some(metrics.output_tokens);
83
- self.cc_cache_create_tokens = Some(metrics.cache_creation_tokens);
84
- self.cc_cache_read_tokens = Some(metrics.cache_read_tokens);
85
-
86
- // Active tokens (legacy)
87
- let active = metrics.input_tokens + metrics.output_tokens;
88
- self.cc_active_tokens = Some(active);
89
- }
90
-
91
- fn set_rtk_from_day(&mut self, stats: &DayStats) {
92
- self.rtk_commands = Some(stats.commands);
93
- self.rtk_saved_tokens = Some(stats.saved_tokens);
94
- self.rtk_savings_pct = Some(stats.savings_pct);
95
- }
96
-
97
- fn set_rtk_from_week(&mut self, stats: &WeekStats) {
98
- self.rtk_commands = Some(stats.commands);
99
- self.rtk_saved_tokens = Some(stats.saved_tokens);
100
- self.rtk_savings_pct = Some(stats.savings_pct);
101
- }
102
-
103
- fn set_rtk_from_month(&mut self, stats: &MonthStats) {
104
- self.rtk_commands = Some(stats.commands);
105
- self.rtk_saved_tokens = Some(stats.saved_tokens);
106
- self.rtk_savings_pct = Some(if stats.input_tokens + stats.output_tokens > 0 {
107
- stats.saved_tokens as f64
108
- / (stats.saved_tokens + stats.input_tokens + stats.output_tokens) as f64
109
- * 100.0
110
- } else {
111
- 0.0
112
- });
113
- }
114
-
115
- fn compute_weighted_metrics(&mut self) {
116
- // Weighted input CPT derivation using API price ratios
117
- if let (Some(cost), Some(saved)) = (self.cc_cost, self.rtk_saved_tokens) {
118
- if let (Some(input), Some(output), Some(cache_create), Some(cache_read)) = (
119
- self.cc_input_tokens,
120
- self.cc_output_tokens,
121
- self.cc_cache_create_tokens,
122
- self.cc_cache_read_tokens,
123
- ) {
124
- // Weighted units = input + 5*output + 1.25*cache_create + 0.1*cache_read
125
- let weighted_units = input as f64
126
- + WEIGHT_OUTPUT * output as f64
127
- + WEIGHT_CACHE_CREATE * cache_create as f64
128
- + WEIGHT_CACHE_READ * cache_read as f64;
129
-
130
- if weighted_units > 0.0 {
131
- let input_cpt = cost / weighted_units;
132
- let savings = saved as f64 * input_cpt;
133
-
134
- self.weighted_input_cpt = Some(input_cpt);
135
- self.savings_weighted = Some(savings);
136
- }
137
- }
138
- }
139
- }
140
-
141
- fn compute_dual_metrics(&mut self) {
142
- if let (Some(cost), Some(saved)) = (self.cc_cost, self.rtk_saved_tokens) {
143
- // Blended CPT (cost / total_tokens including cache)
144
- if let Some(total) = self.cc_total_tokens {
145
- if total > 0 {
146
- self.blended_cpt = Some(cost / total as f64);
147
- self.savings_blended = Some(saved as f64 * (cost / total as f64));
148
- }
149
- }
150
-
151
- // Active CPT (cost / active_tokens = input+output only)
152
- if let Some(active) = self.cc_active_tokens {
153
- if active > 0 {
154
- self.active_cpt = Some(cost / active as f64);
155
- self.savings_active = Some(saved as f64 * (cost / active as f64));
156
- }
157
- }
158
- }
159
- }
160
- }
161
-
162
- #[derive(Debug, Serialize)]
163
- struct Totals {
164
- cc_cost: f64,
165
- cc_total_tokens: u64,
166
- cc_active_tokens: u64,
167
- cc_input_tokens: u64,
168
- cc_output_tokens: u64,
169
- cc_cache_create_tokens: u64,
170
- cc_cache_read_tokens: u64,
171
- rtk_commands: usize,
172
- rtk_saved_tokens: usize,
173
- rtk_avg_savings_pct: f64,
174
- weighted_input_cpt: Option<f64>,
175
- savings_weighted: Option<f64>,
176
- blended_cpt: Option<f64>,
177
- active_cpt: Option<f64>,
178
- savings_blended: Option<f64>,
179
- savings_active: Option<f64>,
180
- }
181
-
182
- // ── Public API ──
183
-
184
- pub fn run(
185
- daily: bool,
186
- weekly: bool,
187
- monthly: bool,
188
- all: bool,
189
- format: &str,
190
- verbose: u8,
191
- ) -> Result<()> {
192
- let tracker = Tracker::new().context("Failed to initialize tracking database")?;
193
-
194
- match format {
195
- "json" => export_json(&tracker, daily, weekly, monthly, all),
196
- "csv" => export_csv(&tracker, daily, weekly, monthly, all),
197
- _ => display_text(&tracker, daily, weekly, monthly, all, verbose),
198
- }
199
- }
200
-
201
- // ── Merge Logic ──
202
-
203
- fn merge_daily(cc: Option<Vec<CcusagePeriod>>, rtk: Vec<DayStats>) -> Vec<PeriodEconomics> {
204
- let mut map: HashMap<String, PeriodEconomics> = HashMap::new();
205
-
206
- // Insert ccusage data
207
- if let Some(cc_data) = cc {
208
- for entry in cc_data {
209
- let crate::ccusage::CcusagePeriod { key, metrics } = entry;
210
- map.entry(key)
211
- .or_insert_with_key(|k| PeriodEconomics::new(k))
212
- .set_ccusage(&metrics);
213
- }
214
- }
215
-
216
- // Merge rtk data
217
- for entry in rtk {
218
- map.entry(entry.date.clone())
219
- .or_insert_with_key(|k| PeriodEconomics::new(k))
220
- .set_rtk_from_day(&entry);
221
- }
222
-
223
- // Compute dual metrics and sort
224
- let mut result: Vec<_> = map.into_values().collect();
225
- for period in &mut result {
226
- period.compute_weighted_metrics();
227
- period.compute_dual_metrics();
228
- }
229
- result.sort_by(|a, b| a.label.cmp(&b.label));
230
- result
231
- }
232
-
233
- fn merge_weekly(cc: Option<Vec<CcusagePeriod>>, rtk: Vec<WeekStats>) -> Vec<PeriodEconomics> {
234
- let mut map: HashMap<String, PeriodEconomics> = HashMap::new();
235
-
236
- // Insert ccusage data (key = ISO Monday "2026-01-20")
237
- if let Some(cc_data) = cc {
238
- for entry in cc_data {
239
- let crate::ccusage::CcusagePeriod { key, metrics } = entry;
240
- map.entry(key)
241
- .or_insert_with_key(|k| PeriodEconomics::new(k))
242
- .set_ccusage(&metrics);
243
- }
244
- }
245
-
246
- // Merge rtk data (week_start = legacy Saturday "2026-01-18")
247
- // Convert Saturday to Monday for alignment
248
- for entry in rtk {
249
- let monday_key = match convert_saturday_to_monday(&entry.week_start) {
250
- Some(m) => m,
251
- None => {
252
- eprintln!("⚠️ Invalid week_start format: {}", entry.week_start);
253
- continue;
254
- }
255
- };
256
-
257
- map.entry(monday_key)
258
- .or_insert_with_key(|key| PeriodEconomics::new(key))
259
- .set_rtk_from_week(&entry);
260
- }
261
-
262
- let mut result: Vec<_> = map.into_values().collect();
263
- for period in &mut result {
264
- period.compute_weighted_metrics();
265
- period.compute_dual_metrics();
266
- }
267
- result.sort_by(|a, b| a.label.cmp(&b.label));
268
- result
269
- }
270
-
271
- fn merge_monthly(cc: Option<Vec<CcusagePeriod>>, rtk: Vec<MonthStats>) -> Vec<PeriodEconomics> {
272
- let mut map: HashMap<String, PeriodEconomics> = HashMap::new();
273
-
274
- // Insert ccusage data
275
- if let Some(cc_data) = cc {
276
- for entry in cc_data {
277
- let crate::ccusage::CcusagePeriod { key, metrics } = entry;
278
- map.entry(key)
279
- .or_insert_with_key(|k| PeriodEconomics::new(k))
280
- .set_ccusage(&metrics);
281
- }
282
- }
283
-
284
- // Merge rtk data
285
- for entry in rtk {
286
- map.entry(entry.month.clone())
287
- .or_insert_with_key(|k| PeriodEconomics::new(k))
288
- .set_rtk_from_month(&entry);
289
- }
290
-
291
- let mut result: Vec<_> = map.into_values().collect();
292
- for period in &mut result {
293
- period.compute_weighted_metrics();
294
- period.compute_dual_metrics();
295
- }
296
- result.sort_by(|a, b| a.label.cmp(&b.label));
297
- result
298
- }
299
-
300
- // ── Helpers ──
301
-
302
- /// Convert Saturday week_start (legacy rtk) to ISO Monday
303
- /// Example: "2026-01-18" (Sat) -> "2026-01-20" (Mon)
304
- fn convert_saturday_to_monday(saturday: &str) -> Option<String> {
305
- let sat_date = NaiveDate::parse_from_str(saturday, "%Y-%m-%d").ok()?;
306
-
307
- // rtk uses Saturday as week start, ISO uses Monday
308
- // Saturday + 2 days = Monday
309
- let monday = sat_date + chrono::TimeDelta::try_days(2)?;
310
-
311
- Some(monday.format("%Y-%m-%d").to_string())
312
- }
313
-
314
- fn compute_totals(periods: &[PeriodEconomics]) -> Totals {
315
- let mut totals = Totals {
316
- cc_cost: 0.0,
317
- cc_total_tokens: 0,
318
- cc_active_tokens: 0,
319
- cc_input_tokens: 0,
320
- cc_output_tokens: 0,
321
- cc_cache_create_tokens: 0,
322
- cc_cache_read_tokens: 0,
323
- rtk_commands: 0,
324
- rtk_saved_tokens: 0,
325
- rtk_avg_savings_pct: 0.0,
326
- weighted_input_cpt: None,
327
- savings_weighted: None,
328
- blended_cpt: None,
329
- active_cpt: None,
330
- savings_blended: None,
331
- savings_active: None,
332
- };
333
-
334
- let mut pct_sum = 0.0;
335
- let mut pct_count = 0;
336
-
337
- for p in periods {
338
- if let Some(cost) = p.cc_cost {
339
- totals.cc_cost += cost;
340
- }
341
- if let Some(total) = p.cc_total_tokens {
342
- totals.cc_total_tokens += total;
343
- }
344
- if let Some(active) = p.cc_active_tokens {
345
- totals.cc_active_tokens += active;
346
- }
347
- if let Some(input) = p.cc_input_tokens {
348
- totals.cc_input_tokens += input;
349
- }
350
- if let Some(output) = p.cc_output_tokens {
351
- totals.cc_output_tokens += output;
352
- }
353
- if let Some(cache_create) = p.cc_cache_create_tokens {
354
- totals.cc_cache_create_tokens += cache_create;
355
- }
356
- if let Some(cache_read) = p.cc_cache_read_tokens {
357
- totals.cc_cache_read_tokens += cache_read;
358
- }
359
- if let Some(cmds) = p.rtk_commands {
360
- totals.rtk_commands += cmds;
361
- }
362
- if let Some(saved) = p.rtk_saved_tokens {
363
- totals.rtk_saved_tokens += saved;
364
- }
365
- if let Some(pct) = p.rtk_savings_pct {
366
- pct_sum += pct;
367
- pct_count += 1;
368
- }
369
- }
370
-
371
- if pct_count > 0 {
372
- totals.rtk_avg_savings_pct = pct_sum / pct_count as f64;
373
- }
374
-
375
- // Compute global weighted metrics
376
- let weighted_units = totals.cc_input_tokens as f64
377
- + WEIGHT_OUTPUT * totals.cc_output_tokens as f64
378
- + WEIGHT_CACHE_CREATE * totals.cc_cache_create_tokens as f64
379
- + WEIGHT_CACHE_READ * totals.cc_cache_read_tokens as f64;
380
-
381
- if weighted_units > 0.0 {
382
- let input_cpt = totals.cc_cost / weighted_units;
383
- totals.weighted_input_cpt = Some(input_cpt);
384
- totals.savings_weighted = Some(totals.rtk_saved_tokens as f64 * input_cpt);
385
- }
386
-
387
- // Compute global dual metrics (legacy)
388
- if totals.cc_total_tokens > 0 {
389
- totals.blended_cpt = Some(totals.cc_cost / totals.cc_total_tokens as f64);
390
- totals.savings_blended = Some(totals.rtk_saved_tokens as f64 * totals.blended_cpt.unwrap());
391
- }
392
- if totals.cc_active_tokens > 0 {
393
- totals.active_cpt = Some(totals.cc_cost / totals.cc_active_tokens as f64);
394
- totals.savings_active = Some(totals.rtk_saved_tokens as f64 * totals.active_cpt.unwrap());
395
- }
396
-
397
- totals
398
- }
399
-
400
- // ── Display ──
401
-
402
- fn display_text(
403
- tracker: &Tracker,
404
- daily: bool,
405
- weekly: bool,
406
- monthly: bool,
407
- all: bool,
408
- verbose: u8,
409
- ) -> Result<()> {
410
- // Default: summary view
411
- if !daily && !weekly && !monthly && !all {
412
- display_summary(tracker, verbose)?;
413
- return Ok(());
414
- }
415
-
416
- if all || daily {
417
- display_daily(tracker, verbose)?;
418
- }
419
- if all || weekly {
420
- display_weekly(tracker, verbose)?;
421
- }
422
- if all || monthly {
423
- display_monthly(tracker, verbose)?;
424
- }
425
-
426
- Ok(())
427
- }
428
-
429
- fn display_summary(tracker: &Tracker, verbose: u8) -> Result<()> {
430
- let cc_monthly =
431
- ccusage::fetch(Granularity::Monthly).context("Failed to fetch ccusage monthly data")?;
432
- let rtk_monthly = tracker
433
- .get_by_month()
434
- .context("Failed to load monthly token savings from database")?;
435
- let periods = merge_monthly(cc_monthly, rtk_monthly);
436
-
437
- if periods.is_empty() {
438
- println!("No data available. Run some rtk commands to start tracking.");
439
- return Ok(());
440
- }
441
-
442
- let totals = compute_totals(&periods);
443
-
444
- println!("💰 Claude Code Economics");
445
- println!("════════════════════════════════════════════════════");
446
- println!();
447
-
448
- println!(
449
- " Spent (ccusage): {}",
450
- format_usd(totals.cc_cost)
451
- );
452
- println!(" Token breakdown:");
453
- println!(
454
- " Input: {}",
455
- format_tokens(totals.cc_input_tokens as usize)
456
- );
457
- println!(
458
- " Output: {}",
459
- format_tokens(totals.cc_output_tokens as usize)
460
- );
461
- println!(
462
- " Cache writes: {}",
463
- format_tokens(totals.cc_cache_create_tokens as usize)
464
- );
465
- println!(
466
- " Cache reads: {}",
467
- format_tokens(totals.cc_cache_read_tokens as usize)
468
- );
469
- println!();
470
-
471
- println!(" RTK commands: {}", totals.rtk_commands);
472
- println!(
473
- " Tokens saved: {}",
474
- format_tokens(totals.rtk_saved_tokens)
475
- );
476
- println!();
477
-
478
- println!(" Estimated Savings:");
479
- println!(" ┌─────────────────────────────────────────────────┐");
480
-
481
- if let Some(weighted_savings) = totals.savings_weighted {
482
- let weighted_pct = if totals.cc_cost > 0.0 {
483
- (weighted_savings / totals.cc_cost) * 100.0
484
- } else {
485
- 0.0
486
- };
487
- println!(
488
- " │ Input token pricing: {} ({:.1}%) │",
489
- format_usd(weighted_savings).trim_end(),
490
- weighted_pct
491
- );
492
- if let Some(input_cpt) = totals.weighted_input_cpt {
493
- println!(
494
- " │ Derived input CPT: {} │",
495
- format_cpt(input_cpt)
496
- );
497
- }
498
- } else {
499
- println!(" │ Input token pricing: — │");
500
- }
501
-
502
- println!(" └─────────────────────────────────────────────────┘");
503
- println!();
504
-
505
- println!(" How it works:");
506
- println!(" RTK compresses CLI outputs before they enter Claude's context.");
507
- println!(" Savings derived using API price ratios (out=5x, cache_w=1.25x, cache_r=0.1x).");
508
- println!();
509
-
510
- // Verbose mode: legacy metrics
511
- if verbose > 0 {
512
- println!(" Legacy metrics (reference only):");
513
- if let Some(active_savings) = totals.savings_active {
514
- let active_pct = if totals.cc_cost > 0.0 {
515
- (active_savings / totals.cc_cost) * 100.0
516
- } else {
517
- 0.0
518
- };
519
- println!(
520
- " Active (OVERESTIMATES): {} ({:.1}%)",
521
- format_usd(active_savings),
522
- active_pct
523
- );
524
- }
525
- if let Some(blended_savings) = totals.savings_blended {
526
- let blended_pct = if totals.cc_cost > 0.0 {
527
- (blended_savings / totals.cc_cost) * 100.0
528
- } else {
529
- 0.0
530
- };
531
- println!(
532
- " Blended (UNDERESTIMATES): {} ({:.2}%)",
533
- format_usd(blended_savings),
534
- blended_pct
535
- );
536
- }
537
- println!(" Note: Saved tokens estimated via chars/4 heuristic, not exact tokenizer.");
538
- println!();
539
- }
540
-
541
- Ok(())
542
- }
543
-
544
- fn display_daily(tracker: &Tracker, verbose: u8) -> Result<()> {
545
- let cc_daily =
546
- ccusage::fetch(Granularity::Daily).context("Failed to fetch ccusage daily data")?;
547
- let rtk_daily = tracker
548
- .get_all_days()
549
- .context("Failed to load daily token savings from database")?;
550
- let periods = merge_daily(cc_daily, rtk_daily);
551
-
552
- println!("📅 Daily Economics");
553
- println!("════════════════════════════════════════════════════");
554
- print_period_table(&periods, verbose);
555
- Ok(())
556
- }
557
-
558
- fn display_weekly(tracker: &Tracker, verbose: u8) -> Result<()> {
559
- let cc_weekly =
560
- ccusage::fetch(Granularity::Weekly).context("Failed to fetch ccusage weekly data")?;
561
- let rtk_weekly = tracker
562
- .get_by_week()
563
- .context("Failed to load weekly token savings from database")?;
564
- let periods = merge_weekly(cc_weekly, rtk_weekly);
565
-
566
- println!("📅 Weekly Economics");
567
- println!("════════════════════════════════════════════════════");
568
- print_period_table(&periods, verbose);
569
- Ok(())
570
- }
571
-
572
- fn display_monthly(tracker: &Tracker, verbose: u8) -> Result<()> {
573
- let cc_monthly =
574
- ccusage::fetch(Granularity::Monthly).context("Failed to fetch ccusage monthly data")?;
575
- let rtk_monthly = tracker
576
- .get_by_month()
577
- .context("Failed to load monthly token savings from database")?;
578
- let periods = merge_monthly(cc_monthly, rtk_monthly);
579
-
580
- println!("📅 Monthly Economics");
581
- println!("════════════════════════════════════════════════════");
582
- print_period_table(&periods, verbose);
583
- Ok(())
584
- }
585
-
586
- fn print_period_table(periods: &[PeriodEconomics], verbose: u8) {
587
- println!();
588
-
589
- if verbose > 0 {
590
- // Verbose: include legacy metrics
591
- println!(
592
- "{:<12} {:>10} {:>10} {:>10} {:>10} {:>12} {:>12}",
593
- "Period", "Spent", "Saved", "Savings", "Active$", "Blended$", "RTK Cmds"
594
- );
595
- println!(
596
- "{:-<12} {:-<10} {:-<10} {:-<10} {:-<10} {:-<12} {:-<12}",
597
- "", "", "", "", "", "", ""
598
- );
599
-
600
- for p in periods {
601
- let spent = p.cc_cost.map(format_usd).unwrap_or_else(|| "—".to_string());
602
- let saved = p
603
- .rtk_saved_tokens
604
- .map(format_tokens)
605
- .unwrap_or_else(|| "—".to_string());
606
- let weighted = p
607
- .savings_weighted
608
- .map(format_usd)
609
- .unwrap_or_else(|| "—".to_string());
610
- let active = p
611
- .savings_active
612
- .map(format_usd)
613
- .unwrap_or_else(|| "—".to_string());
614
- let blended = p
615
- .savings_blended
616
- .map(format_usd)
617
- .unwrap_or_else(|| "—".to_string());
618
- let cmds = p
619
- .rtk_commands
620
- .map(|c| c.to_string())
621
- .unwrap_or_else(|| "—".to_string());
622
-
623
- println!(
624
- "{:<12} {:>10} {:>10} {:>10} {:>10} {:>12} {:>12}",
625
- p.label, spent, saved, weighted, active, blended, cmds
626
- );
627
- }
628
- } else {
629
- // Default: single Savings column
630
- println!(
631
- "{:<12} {:>10} {:>10} {:>10} {:>12}",
632
- "Period", "Spent", "Saved", "Savings", "RTK Cmds"
633
- );
634
- println!(
635
- "{:-<12} {:-<10} {:-<10} {:-<10} {:-<12}",
636
- "", "", "", "", ""
637
- );
638
-
639
- for p in periods {
640
- let spent = p.cc_cost.map(format_usd).unwrap_or_else(|| "—".to_string());
641
- let saved = p
642
- .rtk_saved_tokens
643
- .map(format_tokens)
644
- .unwrap_or_else(|| "—".to_string());
645
- let weighted = p
646
- .savings_weighted
647
- .map(format_usd)
648
- .unwrap_or_else(|| "—".to_string());
649
- let cmds = p
650
- .rtk_commands
651
- .map(|c| c.to_string())
652
- .unwrap_or_else(|| "—".to_string());
653
-
654
- println!(
655
- "{:<12} {:>10} {:>10} {:>10} {:>12}",
656
- p.label, spent, saved, weighted, cmds
657
- );
658
- }
659
- }
660
- println!();
661
- }
662
-
663
- // ── Export ──
664
-
665
- fn export_json(
666
- tracker: &Tracker,
667
- daily: bool,
668
- weekly: bool,
669
- monthly: bool,
670
- all: bool,
671
- ) -> Result<()> {
672
- #[derive(Serialize)]
673
- struct Export {
674
- daily: Option<Vec<PeriodEconomics>>,
675
- weekly: Option<Vec<PeriodEconomics>>,
676
- monthly: Option<Vec<PeriodEconomics>>,
677
- totals: Option<Totals>,
678
- }
679
-
680
- let mut export = Export {
681
- daily: None,
682
- weekly: None,
683
- monthly: None,
684
- totals: None,
685
- };
686
-
687
- if all || daily {
688
- let cc = ccusage::fetch(Granularity::Daily)
689
- .context("Failed to fetch ccusage daily data for JSON export")?;
690
- let rtk = tracker
691
- .get_all_days()
692
- .context("Failed to load daily token savings for JSON export")?;
693
- export.daily = Some(merge_daily(cc, rtk));
694
- }
695
-
696
- if all || weekly {
697
- let cc = ccusage::fetch(Granularity::Weekly)
698
- .context("Failed to fetch ccusage weekly data for export")?;
699
- let rtk = tracker
700
- .get_by_week()
701
- .context("Failed to load weekly token savings for export")?;
702
- export.weekly = Some(merge_weekly(cc, rtk));
703
- }
704
-
705
- if all || monthly {
706
- let cc = ccusage::fetch(Granularity::Monthly)
707
- .context("Failed to fetch ccusage monthly data for export")?;
708
- let rtk = tracker
709
- .get_by_month()
710
- .context("Failed to load monthly token savings for export")?;
711
- let periods = merge_monthly(cc, rtk);
712
- export.totals = Some(compute_totals(&periods));
713
- export.monthly = Some(periods);
714
- }
715
-
716
- println!(
717
- "{}",
718
- serde_json::to_string_pretty(&export)
719
- .context("Failed to serialize economics data to JSON")?
720
- );
721
- Ok(())
722
- }
723
-
724
- fn export_csv(
725
- tracker: &Tracker,
726
- daily: bool,
727
- weekly: bool,
728
- monthly: bool,
729
- all: bool,
730
- ) -> Result<()> {
731
- // Header (new columns: input_tokens, output_tokens, cache_create, cache_read, weighted_savings)
732
- println!("period,spent,input_tokens,output_tokens,cache_create,cache_read,active_tokens,total_tokens,saved_tokens,weighted_savings,active_savings,blended_savings,rtk_commands");
733
-
734
- if all || daily {
735
- let cc = ccusage::fetch(Granularity::Daily)
736
- .context("Failed to fetch ccusage daily data for JSON export")?;
737
- let rtk = tracker
738
- .get_all_days()
739
- .context("Failed to load daily token savings for JSON export")?;
740
- let periods = merge_daily(cc, rtk);
741
- for p in periods {
742
- print_csv_row(&p);
743
- }
744
- }
745
-
746
- if all || weekly {
747
- let cc = ccusage::fetch(Granularity::Weekly)
748
- .context("Failed to fetch ccusage weekly data for export")?;
749
- let rtk = tracker
750
- .get_by_week()
751
- .context("Failed to load weekly token savings for export")?;
752
- let periods = merge_weekly(cc, rtk);
753
- for p in periods {
754
- print_csv_row(&p);
755
- }
756
- }
757
-
758
- if all || monthly {
759
- let cc = ccusage::fetch(Granularity::Monthly)
760
- .context("Failed to fetch ccusage monthly data for export")?;
761
- let rtk = tracker
762
- .get_by_month()
763
- .context("Failed to load monthly token savings for export")?;
764
- let periods = merge_monthly(cc, rtk);
765
- for p in periods {
766
- print_csv_row(&p);
767
- }
768
- }
769
-
770
- Ok(())
771
- }
772
-
773
- fn print_csv_row(p: &PeriodEconomics) {
774
- let spent = p.cc_cost.map(|c| format!("{:.4}", c)).unwrap_or_default();
775
- let input_tokens = p.cc_input_tokens.map(|t| t.to_string()).unwrap_or_default();
776
- let output_tokens = p
777
- .cc_output_tokens
778
- .map(|t| t.to_string())
779
- .unwrap_or_default();
780
- let cache_create = p
781
- .cc_cache_create_tokens
782
- .map(|t| t.to_string())
783
- .unwrap_or_default();
784
- let cache_read = p
785
- .cc_cache_read_tokens
786
- .map(|t| t.to_string())
787
- .unwrap_or_default();
788
- let active_tokens = p
789
- .cc_active_tokens
790
- .map(|t| t.to_string())
791
- .unwrap_or_default();
792
- let total_tokens = p.cc_total_tokens.map(|t| t.to_string()).unwrap_or_default();
793
- let saved_tokens = p
794
- .rtk_saved_tokens
795
- .map(|t| t.to_string())
796
- .unwrap_or_default();
797
- let weighted_savings = p
798
- .savings_weighted
799
- .map(|s| format!("{:.4}", s))
800
- .unwrap_or_default();
801
- let active_savings = p
802
- .savings_active
803
- .map(|s| format!("{:.4}", s))
804
- .unwrap_or_default();
805
- let blended_savings = p
806
- .savings_blended
807
- .map(|s| format!("{:.4}", s))
808
- .unwrap_or_default();
809
- let cmds = p.rtk_commands.map(|c| c.to_string()).unwrap_or_default();
810
-
811
- println!(
812
- "{},{},{},{},{},{},{},{},{},{},{},{},{}",
813
- p.label,
814
- spent,
815
- input_tokens,
816
- output_tokens,
817
- cache_create,
818
- cache_read,
819
- active_tokens,
820
- total_tokens,
821
- saved_tokens,
822
- weighted_savings,
823
- active_savings,
824
- blended_savings,
825
- cmds
826
- );
827
- }
828
-
829
- #[cfg(test)]
830
- mod tests {
831
- use super::*;
832
-
833
- #[test]
834
- fn test_convert_saturday_to_monday() {
835
- // Saturday Jan 18 -> Monday Jan 20
836
- assert_eq!(
837
- convert_saturday_to_monday("2026-01-18"),
838
- Some("2026-01-20".to_string())
839
- );
840
-
841
- // Invalid format
842
- assert_eq!(convert_saturday_to_monday("invalid"), None);
843
- }
844
-
845
- #[test]
846
- fn test_period_economics_new() {
847
- let p = PeriodEconomics::new("2026-01");
848
- assert_eq!(p.label, "2026-01");
849
- assert!(p.cc_cost.is_none());
850
- assert!(p.rtk_commands.is_none());
851
- }
852
-
853
- #[test]
854
- fn test_compute_dual_metrics_with_data() {
855
- let mut p = PeriodEconomics {
856
- label: "2026-01".to_string(),
857
- cc_cost: Some(100.0),
858
- cc_total_tokens: Some(1_000_000),
859
- cc_active_tokens: Some(10_000),
860
- rtk_saved_tokens: Some(5_000),
861
- ..PeriodEconomics::new("2026-01")
862
- };
863
-
864
- p.compute_dual_metrics();
865
-
866
- assert!(p.blended_cpt.is_some());
867
- assert_eq!(p.blended_cpt.unwrap(), 100.0 / 1_000_000.0);
868
-
869
- assert!(p.active_cpt.is_some());
870
- assert_eq!(p.active_cpt.unwrap(), 100.0 / 10_000.0);
871
-
872
- assert!(p.savings_blended.is_some());
873
- assert!(p.savings_active.is_some());
874
- }
875
-
876
- #[test]
877
- fn test_compute_dual_metrics_zero_tokens() {
878
- let mut p = PeriodEconomics {
879
- label: "2026-01".to_string(),
880
- cc_cost: Some(100.0),
881
- cc_total_tokens: Some(0),
882
- cc_active_tokens: Some(0),
883
- rtk_saved_tokens: Some(5_000),
884
- ..PeriodEconomics::new("2026-01")
885
- };
886
-
887
- p.compute_dual_metrics();
888
-
889
- assert!(p.blended_cpt.is_none());
890
- assert!(p.active_cpt.is_none());
891
- assert!(p.savings_blended.is_none());
892
- assert!(p.savings_active.is_none());
893
- }
894
-
895
- #[test]
896
- fn test_compute_dual_metrics_no_ccusage_data() {
897
- let mut p = PeriodEconomics {
898
- label: "2026-01".to_string(),
899
- rtk_saved_tokens: Some(5_000),
900
- ..PeriodEconomics::new("2026-01")
901
- };
902
-
903
- p.compute_dual_metrics();
904
-
905
- assert!(p.blended_cpt.is_none());
906
- assert!(p.active_cpt.is_none());
907
- }
908
-
909
- #[test]
910
- fn test_merge_monthly_both_present() {
911
- let cc = vec![CcusagePeriod {
912
- key: "2026-01".to_string(),
913
- metrics: ccusage::CcusageMetrics {
914
- input_tokens: 1000,
915
- output_tokens: 500,
916
- cache_creation_tokens: 100,
917
- cache_read_tokens: 200,
918
- total_tokens: 1800,
919
- total_cost: 12.34,
920
- },
921
- }];
922
-
923
- let rtk = vec![MonthStats {
924
- month: "2026-01".to_string(),
925
- commands: 10,
926
- input_tokens: 800,
927
- output_tokens: 400,
928
- saved_tokens: 5000,
929
- savings_pct: 50.0,
930
- total_time_ms: 0,
931
- avg_time_ms: 0,
932
- }];
933
-
934
- let merged = merge_monthly(Some(cc), rtk);
935
- assert_eq!(merged.len(), 1);
936
- assert_eq!(merged[0].label, "2026-01");
937
- assert_eq!(merged[0].cc_cost, Some(12.34));
938
- assert_eq!(merged[0].rtk_commands, Some(10));
939
- }
940
-
941
- #[test]
942
- fn test_merge_monthly_only_ccusage() {
943
- let cc = vec![CcusagePeriod {
944
- key: "2026-01".to_string(),
945
- metrics: ccusage::CcusageMetrics {
946
- input_tokens: 1000,
947
- output_tokens: 500,
948
- cache_creation_tokens: 100,
949
- cache_read_tokens: 200,
950
- total_tokens: 1800,
951
- total_cost: 12.34,
952
- },
953
- }];
954
-
955
- let merged = merge_monthly(Some(cc), vec![]);
956
- assert_eq!(merged.len(), 1);
957
- assert_eq!(merged[0].cc_cost, Some(12.34));
958
- assert!(merged[0].rtk_commands.is_none());
959
- }
960
-
961
- #[test]
962
- fn test_merge_monthly_only_rtk() {
963
- let rtk = vec![MonthStats {
964
- month: "2026-01".to_string(),
965
- commands: 10,
966
- input_tokens: 800,
967
- output_tokens: 400,
968
- saved_tokens: 5000,
969
- savings_pct: 50.0,
970
- total_time_ms: 0,
971
- avg_time_ms: 0,
972
- }];
973
-
974
- let merged = merge_monthly(None, rtk);
975
- assert_eq!(merged.len(), 1);
976
- assert!(merged[0].cc_cost.is_none());
977
- assert_eq!(merged[0].rtk_commands, Some(10));
978
- }
979
-
980
- #[test]
981
- fn test_merge_monthly_sorted() {
982
- let rtk = vec![
983
- MonthStats {
984
- month: "2026-03".to_string(),
985
- commands: 5,
986
- input_tokens: 100,
987
- output_tokens: 50,
988
- saved_tokens: 1000,
989
- savings_pct: 40.0,
990
- total_time_ms: 0,
991
- avg_time_ms: 0,
992
- },
993
- MonthStats {
994
- month: "2026-01".to_string(),
995
- commands: 10,
996
- input_tokens: 200,
997
- output_tokens: 100,
998
- saved_tokens: 2000,
999
- savings_pct: 60.0,
1000
- total_time_ms: 0,
1001
- avg_time_ms: 0,
1002
- },
1003
- ];
1004
-
1005
- let merged = merge_monthly(None, rtk);
1006
- assert_eq!(merged.len(), 2);
1007
- assert_eq!(merged[0].label, "2026-01");
1008
- assert_eq!(merged[1].label, "2026-03");
1009
- }
1010
-
1011
- #[test]
1012
- fn test_compute_weighted_input_cpt() {
1013
- let mut p = PeriodEconomics::new("2026-01");
1014
- p.cc_cost = Some(100.0);
1015
- p.cc_input_tokens = Some(1000);
1016
- p.cc_output_tokens = Some(500);
1017
- p.cc_cache_create_tokens = Some(200);
1018
- p.cc_cache_read_tokens = Some(5000);
1019
- p.rtk_saved_tokens = Some(10_000);
1020
-
1021
- p.compute_weighted_metrics();
1022
-
1023
- // weighted_units = 1000 + 5*500 + 1.25*200 + 0.1*5000 = 1000 + 2500 + 250 + 500 = 4250
1024
- // input_cpt = 100 / 4250 = 0.0235294...
1025
- // savings = 10000 * 0.0235294... = 235.29...
1026
-
1027
- assert!(p.weighted_input_cpt.is_some());
1028
- let cpt = p.weighted_input_cpt.unwrap();
1029
- assert!((cpt - (100.0 / 4250.0)).abs() < 1e-6);
1030
-
1031
- assert!(p.savings_weighted.is_some());
1032
- let savings = p.savings_weighted.unwrap();
1033
- assert!((savings - 235.294).abs() < 0.01);
1034
- }
1035
-
1036
- #[test]
1037
- fn test_compute_weighted_metrics_zero_tokens() {
1038
- let mut p = PeriodEconomics::new("2026-01");
1039
- p.cc_cost = Some(100.0);
1040
- p.cc_input_tokens = Some(0);
1041
- p.cc_output_tokens = Some(0);
1042
- p.cc_cache_create_tokens = Some(0);
1043
- p.cc_cache_read_tokens = Some(0);
1044
- p.rtk_saved_tokens = Some(5000);
1045
-
1046
- p.compute_weighted_metrics();
1047
-
1048
- assert!(p.weighted_input_cpt.is_none());
1049
- assert!(p.savings_weighted.is_none());
1050
- }
1051
-
1052
- #[test]
1053
- fn test_compute_weighted_metrics_no_cache() {
1054
- let mut p = PeriodEconomics::new("2026-01");
1055
- p.cc_cost = Some(60.0);
1056
- p.cc_input_tokens = Some(1000);
1057
- p.cc_output_tokens = Some(1000);
1058
- p.cc_cache_create_tokens = Some(0);
1059
- p.cc_cache_read_tokens = Some(0);
1060
- p.rtk_saved_tokens = Some(3000);
1061
-
1062
- p.compute_weighted_metrics();
1063
-
1064
- // weighted_units = 1000 + 5*1000 = 6000
1065
- // input_cpt = 60 / 6000 = 0.01
1066
- // savings = 3000 * 0.01 = 30
1067
-
1068
- assert!(p.weighted_input_cpt.is_some());
1069
- let cpt = p.weighted_input_cpt.unwrap();
1070
- assert!((cpt - 0.01).abs() < 1e-6);
1071
-
1072
- assert!(p.savings_weighted.is_some());
1073
- let savings = p.savings_weighted.unwrap();
1074
- assert!((savings - 30.0).abs() < 0.01);
1075
- }
1076
-
1077
- #[test]
1078
- fn test_set_ccusage_stores_per_type_tokens() {
1079
- let mut p = PeriodEconomics::new("2026-01");
1080
- let metrics = ccusage::CcusageMetrics {
1081
- input_tokens: 1000,
1082
- output_tokens: 500,
1083
- cache_creation_tokens: 200,
1084
- cache_read_tokens: 3000,
1085
- total_tokens: 4700,
1086
- total_cost: 50.0,
1087
- };
1088
-
1089
- p.set_ccusage(&metrics);
1090
-
1091
- assert_eq!(p.cc_input_tokens, Some(1000));
1092
- assert_eq!(p.cc_output_tokens, Some(500));
1093
- assert_eq!(p.cc_cache_create_tokens, Some(200));
1094
- assert_eq!(p.cc_cache_read_tokens, Some(3000));
1095
- assert_eq!(p.cc_total_tokens, Some(4700));
1096
- assert_eq!(p.cc_cost, Some(50.0));
1097
- }
1098
-
1099
- #[test]
1100
- fn test_compute_totals() {
1101
- let periods = vec![
1102
- PeriodEconomics {
1103
- label: "2026-01".to_string(),
1104
- cc_cost: Some(100.0),
1105
- cc_total_tokens: Some(1_000_000),
1106
- cc_active_tokens: Some(10_000),
1107
- cc_input_tokens: Some(5000),
1108
- cc_output_tokens: Some(5000),
1109
- cc_cache_create_tokens: Some(100),
1110
- cc_cache_read_tokens: Some(984_900),
1111
- rtk_commands: Some(5),
1112
- rtk_saved_tokens: Some(2000),
1113
- rtk_savings_pct: Some(50.0),
1114
- weighted_input_cpt: None,
1115
- savings_weighted: None,
1116
- blended_cpt: None,
1117
- active_cpt: None,
1118
- savings_blended: None,
1119
- savings_active: None,
1120
- },
1121
- PeriodEconomics {
1122
- label: "2026-02".to_string(),
1123
- cc_cost: Some(200.0),
1124
- cc_total_tokens: Some(2_000_000),
1125
- cc_active_tokens: Some(20_000),
1126
- cc_input_tokens: Some(10_000),
1127
- cc_output_tokens: Some(10_000),
1128
- cc_cache_create_tokens: Some(200),
1129
- cc_cache_read_tokens: Some(1_979_800),
1130
- rtk_commands: Some(10),
1131
- rtk_saved_tokens: Some(3000),
1132
- rtk_savings_pct: Some(60.0),
1133
- weighted_input_cpt: None,
1134
- savings_weighted: None,
1135
- blended_cpt: None,
1136
- active_cpt: None,
1137
- savings_blended: None,
1138
- savings_active: None,
1139
- },
1140
- ];
1141
-
1142
- let totals = compute_totals(&periods);
1143
- assert_eq!(totals.cc_cost, 300.0);
1144
- assert_eq!(totals.cc_total_tokens, 3_000_000);
1145
- assert_eq!(totals.cc_active_tokens, 30_000);
1146
- assert_eq!(totals.cc_input_tokens, 15_000);
1147
- assert_eq!(totals.cc_output_tokens, 15_000);
1148
- assert_eq!(totals.rtk_commands, 15);
1149
- assert_eq!(totals.rtk_saved_tokens, 5000);
1150
- assert_eq!(totals.rtk_avg_savings_pct, 55.0);
1151
-
1152
- assert!(totals.weighted_input_cpt.is_some());
1153
- assert!(totals.savings_weighted.is_some());
1154
- assert!(totals.blended_cpt.is_some());
1155
- assert!(totals.active_cpt.is_some());
1156
- }
1157
- }