@archeagle087/costcrew 0.4.1 → 0.4.3

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 (54) hide show
  1. package/README.md +1 -0
  2. package/bin.js +10 -6
  3. package/package.json +8 -5
  4. package/.github/workflows/ci.yml +0 -21
  5. package/.github/workflows/npm-release.yml +0 -123
  6. package/.github/workflows/prices-monthly.yml +0 -58
  7. package/.github/workflows/release.yml +0 -45
  8. package/Cargo.lock +0 -980
  9. package/Cargo.toml +0 -22
  10. package/assets/advise.png +0 -0
  11. package/assets/board.png +0 -0
  12. package/assets/catalog.png +0 -0
  13. package/assets/demo.png +0 -0
  14. package/assets/help.png +0 -0
  15. package/assets/mascot.png +0 -0
  16. package/assets/prices.png +0 -0
  17. package/assets/scan.png +0 -0
  18. package/assets/spend.png +0 -0
  19. package/assets/ui.png +0 -0
  20. package/data/prices.json +0 -1
  21. package/dist/costcrew-linux-x86_64.tar.gz +0 -0
  22. package/dist/costcrew-macos-aarch64.tar.gz +0 -0
  23. package/dist/costcrew-macos-x86_64.tar.gz +0 -0
  24. package/dist/costcrew-windows-x86_64.zip +0 -0
  25. package/install.ps1 +0 -38
  26. package/install.sh +0 -56
  27. package/packaging/npm/bin.js +0 -46
  28. package/packaging/npm/package.json +0 -20
  29. package/packaging/winget/0.4.0/Arch087146.costcrew.installer.yaml +0 -15
  30. package/packaging/winget/0.4.0/Arch087146.costcrew.locale.en-US.yaml +0 -28
  31. package/packaging/winget/0.4.0/Arch087146.costcrew.yaml +0 -5
  32. package/packaging/winget/README.md +0 -31
  33. package/scripts/render_screen.py +0 -221
  34. package/src/advise.rs +0 -154
  35. package/src/args.rs +0 -239
  36. package/src/board.rs +0 -227
  37. package/src/catalog.rs +0 -136
  38. package/src/demo.rs +0 -79
  39. package/src/fetch.rs +0 -155
  40. package/src/main.rs +0 -371
  41. package/src/price.rs +0 -218
  42. package/src/prices.rs +0 -182
  43. package/src/report.rs +0 -194
  44. package/src/scan.rs +0 -593
  45. package/src/spend.rs +0 -452
  46. package/src/ui.rs +0 -367
  47. package/work/costcrew-darwin-arm64/costcrew +0 -0
  48. package/work/costcrew-darwin-arm64/package.json +0 -9
  49. package/work/costcrew-darwin-x64/costcrew +0 -0
  50. package/work/costcrew-darwin-x64/package.json +0 -9
  51. package/work/costcrew-linux-x64/costcrew +0 -0
  52. package/work/costcrew-linux-x64/package.json +0 -9
  53. package/work/costcrew-win32-x64/costcrew.exe +0 -0
  54. package/work/costcrew-win32-x64/package.json +0 -9
package/src/prices.rs DELETED
@@ -1,182 +0,0 @@
1
- //! `costcrew prices` — browse the bundled price database.
2
- //!
3
- //! The database ships with the binary: a curated vendor table plus the full
4
- //! embedded OpenRouter catalog (data/prices.json), optionally enriched by the
5
- //! on-disk cache (`fetch-prices`) and custom overrides (`--prices`). No
6
- //! network, no log scanning — it's just a database you can read.
7
-
8
- use crate::price::PriceDB;
9
- use crate::report::{BOLD, CYAN, DIM, GREEN, RESET, YELLOW};
10
-
11
- pub fn print_prices(db: &PriceDB, query: Option<&str>, sort: &str, top: usize, json: bool) {
12
- // filter + dedupe: the same model can appear in curated + embedded +
13
- // cache + custom; the LATEST entry is authoritative (matches lookup order)
14
- let mut latest: std::collections::HashMap<String, &crate::price::PriceEntry> =
15
- std::collections::HashMap::new();
16
- for e in db.entries() {
17
- if let Some(q) = query {
18
- if !e.prefix.to_lowercase().contains(&q.to_lowercase()) {
19
- continue;
20
- }
21
- }
22
- latest.insert(e.prefix.clone(), e);
23
- }
24
- let mut rows: Vec<&crate::price::PriceEntry> = latest.into_values().collect();
25
-
26
- match sort {
27
- "price" | "cost" => rows.sort_by(|a, b| {
28
- let ka = a.price.input + a.price.output;
29
- let kb = b.price.input + b.price.output;
30
- kb.partial_cmp(&ka).unwrap_or(std::cmp::Ordering::Equal) // priciest first
31
- }),
32
- "cheap" => rows.sort_by(|a, b| {
33
- let ka = a.price.input + a.price.output;
34
- let kb = b.price.input + b.price.output;
35
- ka.partial_cmp(&kb).unwrap_or(std::cmp::Ordering::Equal) // cheapest first
36
- }),
37
- _ => rows.sort_by(|a, b| a.prefix.cmp(&b.prefix)), // name
38
- }
39
-
40
- if json {
41
- use serde_json::json;
42
- let arr: Vec<_> = rows
43
- .iter()
44
- .take(top.max(1))
45
- .map(|e| {
46
- json!({
47
- "model": e.prefix,
48
- "input_per_1m": e.price.input,
49
- "output_per_1m": e.price.output,
50
- "cache_read_per_1m": e.price.cache_read,
51
- "cache_write_per_1m": e.price.cache_write,
52
- "per_hour": if e.price.per_hour > 0.0 { serde_json::Value::from(e.price.per_hour) } else { serde_json::Value::Null },
53
- "source": e.src,
54
- })
55
- })
56
- .collect();
57
- println!(
58
- "{}",
59
- json!({
60
- "total_models": db.entries().len(),
61
- "matches": rows.len(),
62
- "rows": arr,
63
- })
64
- );
65
- return;
66
- }
67
-
68
- println!();
69
- println!(
70
- " {BOLD}costcrew price database{RESET} {DIM}· bundled, {} models{RESET}",
71
- db.entries().len()
72
- );
73
- println!(
74
- " {DIM}{} curated table · {} embedded snapshot · {} live cache{} · {} custom{RESET}",
75
- db.entries().iter().filter(|e| e.src == "curated").count(),
76
- db.embedded_count,
77
- db.cache_count,
78
- if db.embedded_fetched_at_ms > 0 {
79
- format!(" (snapshot {})", date_of(db.embedded_fetched_at_ms))
80
- } else {
81
- String::new()
82
- },
83
- db.custom_count,
84
- );
85
- println!(" {DIM}refresh anytime: {RESET}{CYAN}costcrew fetch-prices{RESET}");
86
- println!();
87
- if let Some(q) = query {
88
- println!(" {BOLD}{} matches for \"{q}\"{RESET}{DIM} (--sort name|price|cheap, --top N, --json){RESET}", rows.len());
89
- println!();
90
- }
91
- println!(
92
- " {:<44} {:>9} {:>9} {:>10} {:<8}",
93
- "model", "in $/1M", "out $/1M", "cache $/1M", "source"
94
- );
95
- println!(" {}", "-".repeat(44 + 3 + 9 + 3 + 9 + 3 + 10 + 2 + 8));
96
-
97
- for e in rows.iter().take(top) {
98
- let (i, o, c) = (e.price.input, e.price.output, e.price.cache_read);
99
- let fmt = |v: f64| {
100
- if v > 0.0 {
101
- format!("{v:>9.3}")
102
- } else {
103
- " ?".to_string()
104
- }
105
- };
106
- let src_col = match e.src.as_str() {
107
- "curated" => GREEN,
108
- "embedded" => CYAN,
109
- "cache" => YELLOW,
110
- _ => DIM,
111
- };
112
- println!(
113
- " {:<44} {} {} {:>10} ({src_col}{:<6}{DIM}){RESET}",
114
- truncate(&e.prefix, 44),
115
- fmt(i),
116
- fmt(o),
117
- fmt(c),
118
- e.src,
119
- );
120
- }
121
-
122
- if rows.len() > top {
123
- println!();
124
- println!(
125
- " {DIM}... {} more. narrower filter, or --top {}. e.g.:{RESET}",
126
- rows.len() - top,
127
- rows.len()
128
- );
129
- println!(
130
- " {DIM} costcrew prices {}{RESET}",
131
- query.unwrap_or("anthropic")
132
- );
133
- }
134
- if rows.iter().take(top).any(|e| e.price.input == 0.0) {
135
- println!();
136
- println!(
137
- " {DIM}? = free tier or unpriced. Unknown stays unknown — no invented prices.{RESET}"
138
- );
139
- }
140
- println!();
141
- }
142
-
143
- fn truncate(s: &str, n: usize) -> String {
144
- if s.chars().count() <= n {
145
- s.to_string()
146
- } else {
147
- s.chars().take(n - 1).collect::<String>() + "…"
148
- }
149
- }
150
-
151
- /// epoch ms → "Nov 10 2026" (civil-from-days, no chrono dependency)
152
- fn date_of(ms: u64) -> String {
153
- if ms == 0 {
154
- return "undated".into();
155
- }
156
- let days = ms / 86_400_000;
157
- // Howard Hinnant's civil_from_days
158
- let z = days as i64 + 719_468;
159
- let era = z.div_euclid(146_097);
160
- let doe = z.rem_euclid(146_097);
161
- let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
162
- let y = yoe + era * 400;
163
- let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
164
- let mp = (5 * doy + 2) / 153;
165
- let d = doy - (153 * mp + 2) / 5 + 1;
166
- let m = if mp < 10 { mp + 3 } else { mp - 9 };
167
- let y = if m <= 2 { y + 1 } else { y };
168
- const MON: [&str; 12] = [
169
- "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
170
- ];
171
- format!("{} {} {}", MON[(m - 1) as usize], d, y)
172
- }
173
-
174
- #[cfg(test)]
175
- mod tests {
176
- use super::date_of;
177
- #[test]
178
- fn epoch_to_civil() {
179
- assert_eq!(date_of(1_794_274_886_057), "Nov 10 2026");
180
- assert_eq!(date_of(0), "undated");
181
- }
182
- }
package/src/report.rs DELETED
@@ -1,194 +0,0 @@
1
- //! Terminal table + shareable markdown report.
2
-
3
- use crate::scan::Row;
4
-
5
- pub const RESET: &str = "\x1b[0m";
6
- pub const DIM: &str = "\x1b[2m";
7
- pub const BOLD: &str = "\x1b[1m";
8
- pub const CYAN: &str = "\x1b[36m";
9
- pub const YELLOW: &str = "\x1b[33m";
10
- pub const GREEN: &str = "\x1b[32m";
11
- pub const RED: &str = "\x1b[31m";
12
-
13
- pub struct Totals {
14
- pub total_cost: f64,
15
- pub total_requests: u64,
16
- pub unknown_models: usize,
17
- }
18
-
19
- pub fn print_table(rows: &[Row], since_days: u32, totals: &Totals) {
20
- let period = if since_days == 1 {
21
- "last 24h".to_string()
22
- } else {
23
- format!("last {since_days}d")
24
- };
25
-
26
- println!();
27
- println!(" {BOLD}costcrew{RESET} {DIM}· AI agent spend · {period}{RESET}");
28
- println!();
29
-
30
- let widths = col_widths(rows);
31
- let cost_w = 10;
32
-
33
- // header
34
- print!(" ");
35
- for (i, h) in [
36
- "provider",
37
- "model",
38
- "reqs",
39
- "tokens in",
40
- "tokens out",
41
- "cache",
42
- ]
43
- .iter()
44
- .enumerate()
45
- {
46
- print!("{DIM}{:>width$}{RESET} ", h, width = widths[i]);
47
- }
48
- println!("{DIM}{:>width$}{RESET}", "cost (usd)", width = cost_w);
49
-
50
- // separator
51
- print!(" ");
52
- for w in &widths {
53
- print!("{DIM}{}{RESET} ", "-".repeat(*w));
54
- }
55
- println!("{DIM}{}{RESET}", "-".repeat(cost_w));
56
-
57
- // rows
58
- for r in rows {
59
- print!(" ");
60
- print!("{:>w$} ", r.provider, w = widths[0]);
61
- print!("{:>w$} ", truncate(&r.model, widths[1]), w = widths[1]);
62
- print!("{:>w$} ", r.requests, w = widths[2]);
63
- print!("{:>w$} ", r.input_tokens, w = widths[3]);
64
- print!("{:>w$} ", r.output_tokens, w = widths[4]);
65
- print!("{:>w$} ", r.cache_tokens, w = widths[5]);
66
- if r.known_price {
67
- let color = cost_color(r.cost);
68
- print!("{}{:>10.4}{}", color, r.cost, RESET);
69
- } else {
70
- print!("{:>10}", "?");
71
- }
72
- println!();
73
- }
74
-
75
- // totals
76
- let t_in: u64 = rows.iter().map(|r| r.input_tokens).sum();
77
- let t_out: u64 = rows.iter().map(|r| r.output_tokens).sum();
78
- let t_cache: u64 = rows.iter().map(|r| r.cache_tokens).sum();
79
- print!(" ");
80
- for w in &widths[..2] {
81
- print!("{DIM}{}{RESET} ", "-".repeat(*w));
82
- }
83
- print!("{BOLD}total{RESET} ");
84
- print!("{:>w$} ", "", w = widths[2] + 1);
85
- print!("{:>w$} ", t_in, w = widths[3] + 1);
86
- print!("{:>w$} ", t_out, w = widths[4] + 1);
87
- print!("{:>w$} ", t_cache, w = widths[5] + 1);
88
- // -0.0 guard: an all-unpriced table shouldn't render as negative money
89
- let shown = if totals.total_cost.abs() < 0.000_1 {
90
- 0.0
91
- } else {
92
- totals.total_cost
93
- };
94
- println!("{BOLD}{GREEN}${:>8.2}{RESET}", shown);
95
- println!();
96
-
97
- // bar chart of spend share
98
- let max = rows.iter().map(|r| r.cost).fold(0.0_f64, f64::max);
99
- if max > 0.0 {
100
- for r in rows.iter().filter(|r| r.known_price) {
101
- let scale = ((r.cost / max) * 24.0).round() as usize;
102
- let units = if r.cost > 0.0 { scale.max(1) } else { 0 };
103
- let bar: String = std::iter::repeat_n('#', units).collect();
104
- let color = cost_color(r.cost);
105
- println!(
106
- " {:>26} {color}{bar}{RESET} ${:.2}",
107
- truncate(&r.model, 26),
108
- r.cost
109
- );
110
- }
111
- println!();
112
- }
113
-
114
- if totals.unknown_models > 0 {
115
- println!(
116
- " {DIM}{} model(s) not priced yet — saw them, didn't bill them.{RESET}",
117
- totals.unknown_models
118
- );
119
- }
120
- }
121
-
122
- fn cost_color(cost: f64) -> &'static str {
123
- if cost < 1.0 {
124
- GREEN
125
- } else if cost < 10.0 {
126
- YELLOW
127
- } else {
128
- RED
129
- }
130
- }
131
-
132
- fn truncate(s: &str, max: usize) -> String {
133
- if s.len() <= max {
134
- s.to_string()
135
- } else {
136
- format!("{}…", &s[..max.saturating_sub(1)])
137
- }
138
- }
139
-
140
- fn col_widths(rows: &[Row]) -> [usize; 6] {
141
- let mut w = [
142
- "provider".len(),
143
- "model".len(),
144
- "reqs".len(),
145
- "tokens in".len(),
146
- "tokens out".len(),
147
- "cache".len(),
148
- ];
149
- for r in rows {
150
- w[0] = w[0].max(r.provider.len());
151
- w[1] = w[1].max(r.model.len().min(40));
152
- w[2] = w[2].max(r.requests.to_string().len());
153
- w[3] = w[3].max(r.input_tokens.to_string().len());
154
- w[4] = w[4].max(r.output_tokens.to_string().len());
155
- w[5] = w[5].max(r.cache_tokens.to_string().len());
156
- }
157
- w
158
- }
159
-
160
- pub fn print_markdown(rows: &[Row], since_days: u32, totals: &Totals) {
161
- println!("# costcrew report");
162
- println!();
163
- println!(
164
- "AI agent spend, last {since_days} day(s). Generated by [costcrew](https://github.com/Arch087146/costcrew)."
165
- );
166
- println!();
167
- println!("| provider | model | reqs | tokens in | tokens out | cached | cost (USD) |");
168
- println!("|---|---|---:|---:|---:|---:|---:|");
169
- for r in rows {
170
- let cost = if r.known_price {
171
- format!("${:.4}", r.cost)
172
- } else {
173
- "?".to_string()
174
- };
175
- println!(
176
- "| {} | `{}` | {} | {} | {} | {} | {} |",
177
- r.provider, r.model, r.requests, r.input_tokens, r.output_tokens, r.cache_tokens, cost
178
- );
179
- }
180
- let t_in: u64 = rows.iter().map(|r| r.input_tokens).sum();
181
- let t_out: u64 = rows.iter().map(|r| r.output_tokens).sum();
182
- let t_cache: u64 = rows.iter().map(|r| r.cache_tokens).sum();
183
- println!(
184
- "| **total** | | **{}** | **{}** | **{}** | **{}** | **${:.2}** |",
185
- totals.total_requests, t_in, t_out, t_cache, totals.total_cost
186
- );
187
- if totals.unknown_models > 0 {
188
- println!();
189
- println!(
190
- "> {} model(s) had no price entry and were excluded from totals.",
191
- totals.unknown_models
192
- );
193
- }
194
- }