feat: massive overhaul to manage costs (#103)
Security Scan and Upload / Security & DefectDojo Upload (push) Successful in 1m1s
CI / Lint & Test (push) Has been cancelled
CI / Build Linux (push) Has been cancelled
CI / Build Windows (cross-compile) (push) Has been cancelled

### Explanation

_No response_

### Issue

Closes #102

### Attestations

- [ ] I have read and agree to the [Code of Conduct](https://docs.nhcarrigan.com/community/coc/)
- [ ] I have read and agree to the [Community Guidelines](https://docs.nhcarrigan.com/community/guide/).
- [ ] My contribution complies with the [Contributor Covenant](https://docs.nhcarrigan.com/dev/covenant/).

### Dependencies

- [ ] I have pinned the dependencies to a specific patch version.

### Style

- [ ] I have run the linter and resolved any errors.
- [ ] My pull request uses an appropriate title, matching the conventional commit standards.
- [ ] My scope of feat/fix/chore/etc. correctly matches the nature of changes in my pull request.

### Tests

- [ ] My contribution adds new code, and I have added tests to cover it.
- [ ] My contribution modifies existing code, and I have updated the tests to reflect these changes.
- [ ] All new and existing tests pass locally with my changes.
- [ ] Code coverage remains at or above the configured threshold.

### Documentation

_No response_

### Versioning

_No response_

Reviewed-on: #103
Co-authored-by: Naomi Carrigan <commits@nhcarrigan.com>
Co-committed-by: Naomi Carrigan <commits@nhcarrigan.com>
This commit was merged in pull request #103.
This commit is contained in:
2026-02-04 19:58:43 -08:00
committed by Naomi Carrigan
parent daedbfd865
commit 1c45507cdf
30 changed files with 4024 additions and 103 deletions
+56 -41
View File
@@ -1935,6 +1935,7 @@ pub fn check_achievements(
let search_count: u64 = search_tools
.iter()
.filter_map(|tool| stats.tools_usage.get(*tool))
.map(|t| t.call_count)
.sum();
if search_count >= 50 && progress.unlock(AchievementId::Explorer) {
newly_unlocked.push(AchievementId::Explorer);
@@ -1988,25 +1989,25 @@ pub fn check_achievements(
// TODO: Track different Claude models used
// Tool mastery achievements
if let Some(bash_count) = stats.tools_usage.get("Bash") {
if *bash_count >= 50 && progress.unlock(AchievementId::BashMaster) {
if let Some(bash_stats) = stats.tools_usage.get("Bash") {
if bash_stats.call_count >= 50 && progress.unlock(AchievementId::BashMaster) {
newly_unlocked.push(AchievementId::BashMaster);
}
}
if let Some(read_count) = stats.tools_usage.get("Read") {
if *read_count >= 100 && progress.unlock(AchievementId::FileExplorer) {
if let Some(read_stats) = stats.tools_usage.get("Read") {
if read_stats.call_count >= 100 && progress.unlock(AchievementId::FileExplorer) {
newly_unlocked.push(AchievementId::FileExplorer);
}
}
if let Some(grep_count) = stats.tools_usage.get("Grep") {
if *grep_count >= 50 && progress.unlock(AchievementId::SearchExpert) {
if let Some(grep_stats) = stats.tools_usage.get("Grep") {
if grep_stats.call_count >= 50 && progress.unlock(AchievementId::SearchExpert) {
newly_unlocked.push(AchievementId::SearchExpert);
}
}
// Git Guru - check git command usage in Bash
if let Some(bash_count) = stats.tools_usage.get("Bash") {
if *bash_count >= 10 && progress.unlock(AchievementId::GitGuru) {
if let Some(bash_stats) = stats.tools_usage.get("Bash") {
if bash_stats.call_count >= 10 && progress.unlock(AchievementId::GitGuru) {
// TODO: More specific git command tracking
newly_unlocked.push(AchievementId::GitGuru);
}
@@ -2055,28 +2056,28 @@ pub fn check_achievements(
}
// More tool mastery achievements
if let Some(edit_count) = stats.tools_usage.get("Edit") {
if *edit_count >= 100 && progress.unlock(AchievementId::EditMaster) {
if let Some(edit_stats) = stats.tools_usage.get("Edit") {
if edit_stats.call_count >= 100 && progress.unlock(AchievementId::EditMaster) {
newly_unlocked.push(AchievementId::EditMaster);
}
}
if let Some(write_count) = stats.tools_usage.get("Write") {
if *write_count >= 50 && progress.unlock(AchievementId::WriteMaster) {
if let Some(write_stats) = stats.tools_usage.get("Write") {
if write_stats.call_count >= 50 && progress.unlock(AchievementId::WriteMaster) {
newly_unlocked.push(AchievementId::WriteMaster);
}
}
if let Some(glob_count) = stats.tools_usage.get("Glob") {
if *glob_count >= 100 && progress.unlock(AchievementId::GlobMaster) {
if let Some(glob_stats) = stats.tools_usage.get("Glob") {
if glob_stats.call_count >= 100 && progress.unlock(AchievementId::GlobMaster) {
newly_unlocked.push(AchievementId::GlobMaster);
}
}
if let Some(task_count) = stats.tools_usage.get("Task") {
if *task_count >= 50 && progress.unlock(AchievementId::TaskMaster) {
if let Some(task_stats) = stats.tools_usage.get("Task") {
if task_stats.call_count >= 50 && progress.unlock(AchievementId::TaskMaster) {
newly_unlocked.push(AchievementId::TaskMaster);
}
}
if let Some(web_count) = stats.tools_usage.get("WebFetch") {
if *web_count >= 20 && progress.unlock(AchievementId::WebFetcher) {
if let Some(web_stats) = stats.tools_usage.get("WebFetch") {
if web_stats.call_count >= 20 && progress.unlock(AchievementId::WebFetcher) {
newly_unlocked.push(AchievementId::WebFetcher);
}
}
@@ -2085,7 +2086,7 @@ pub fn check_achievements(
.tools_usage
.iter()
.filter(|(name, _)| name.starts_with("mcp__"))
.map(|(_, count)| count)
.map(|(_, tool_stats)| tool_stats.call_count)
.sum();
if mcp_count >= 50 && progress.unlock(AchievementId::McpExplorer) {
newly_unlocked.push(AchievementId::McpExplorer);
@@ -2323,6 +2324,11 @@ mod tests {
morning_sessions: 0,
night_sessions: 0,
last_session_date: None,
context_tokens_used: 0,
context_window_limit: 200_000,
context_utilisation_percent: 0.0,
potential_cache_hits: 0,
potential_cache_savings_tokens: 0,
achievements: AchievementProgress::new(),
}
}
@@ -2733,12 +2739,21 @@ mod tests {
// check_achievements tests - Tool Usage
// =====================
// Helper function to create a ToolTokenStats with just call count for tests
fn tool_stats(call_count: u64) -> crate::stats::ToolTokenStats {
crate::stats::ToolTokenStats {
call_count,
estimated_input_tokens: 0,
estimated_output_tokens: 0,
}
}
#[test]
fn test_check_achievements_first_tool() {
let mut stats = create_test_stats();
let mut progress = AchievementProgress::new();
stats.tools_usage.insert("Read".to_string(), 1);
stats.tools_usage.insert("Read".to_string(), tool_stats(1));
let newly = check_achievements(&stats, &mut progress);
assert!(newly.contains(&AchievementId::FirstTool));
@@ -2749,11 +2764,11 @@ mod tests {
let mut stats = create_test_stats();
let mut progress = AchievementProgress::new();
stats.tools_usage.insert("Read".to_string(), 1);
stats.tools_usage.insert("Write".to_string(), 1);
stats.tools_usage.insert("Edit".to_string(), 1);
stats.tools_usage.insert("Bash".to_string(), 1);
stats.tools_usage.insert("Grep".to_string(), 1);
stats.tools_usage.insert("Read".to_string(), tool_stats(1));
stats.tools_usage.insert("Write".to_string(), tool_stats(1));
stats.tools_usage.insert("Edit".to_string(), tool_stats(1));
stats.tools_usage.insert("Bash".to_string(), tool_stats(1));
stats.tools_usage.insert("Grep".to_string(), tool_stats(1));
let newly = check_achievements(&stats, &mut progress);
assert!(newly.contains(&AchievementId::Toolsmith));
@@ -2765,7 +2780,7 @@ mod tests {
let mut progress = AchievementProgress::new();
for i in 0..10 {
stats.tools_usage.insert(format!("Tool{}", i), 1);
stats.tools_usage.insert(format!("Tool{}", i), tool_stats(1));
}
let newly = check_achievements(&stats, &mut progress);
@@ -2777,7 +2792,7 @@ mod tests {
let mut stats = create_test_stats();
let mut progress = AchievementProgress::new();
stats.tools_usage.insert("Bash".to_string(), 50);
stats.tools_usage.insert("Bash".to_string(), tool_stats(50));
let newly = check_achievements(&stats, &mut progress);
assert!(newly.contains(&AchievementId::BashMaster));
@@ -2788,7 +2803,7 @@ mod tests {
let mut stats = create_test_stats();
let mut progress = AchievementProgress::new();
stats.tools_usage.insert("Read".to_string(), 100);
stats.tools_usage.insert("Read".to_string(), tool_stats(100));
let newly = check_achievements(&stats, &mut progress);
assert!(newly.contains(&AchievementId::FileExplorer));
@@ -2799,7 +2814,7 @@ mod tests {
let mut stats = create_test_stats();
let mut progress = AchievementProgress::new();
stats.tools_usage.insert("Grep".to_string(), 50);
stats.tools_usage.insert("Grep".to_string(), tool_stats(50));
let newly = check_achievements(&stats, &mut progress);
assert!(newly.contains(&AchievementId::SearchExpert));
@@ -2810,7 +2825,7 @@ mod tests {
let mut stats = create_test_stats();
let mut progress = AchievementProgress::new();
stats.tools_usage.insert("Edit".to_string(), 100);
stats.tools_usage.insert("Edit".to_string(), tool_stats(100));
let newly = check_achievements(&stats, &mut progress);
assert!(newly.contains(&AchievementId::EditMaster));
@@ -2821,7 +2836,7 @@ mod tests {
let mut stats = create_test_stats();
let mut progress = AchievementProgress::new();
stats.tools_usage.insert("Write".to_string(), 50);
stats.tools_usage.insert("Write".to_string(), tool_stats(50));
let newly = check_achievements(&stats, &mut progress);
assert!(newly.contains(&AchievementId::WriteMaster));
@@ -2832,7 +2847,7 @@ mod tests {
let mut stats = create_test_stats();
let mut progress = AchievementProgress::new();
stats.tools_usage.insert("Glob".to_string(), 100);
stats.tools_usage.insert("Glob".to_string(), tool_stats(100));
let newly = check_achievements(&stats, &mut progress);
assert!(newly.contains(&AchievementId::GlobMaster));
@@ -2843,7 +2858,7 @@ mod tests {
let mut stats = create_test_stats();
let mut progress = AchievementProgress::new();
stats.tools_usage.insert("Task".to_string(), 50);
stats.tools_usage.insert("Task".to_string(), tool_stats(50));
let newly = check_achievements(&stats, &mut progress);
assert!(newly.contains(&AchievementId::TaskMaster));
@@ -2854,7 +2869,7 @@ mod tests {
let mut stats = create_test_stats();
let mut progress = AchievementProgress::new();
stats.tools_usage.insert("WebFetch".to_string(), 20);
stats.tools_usage.insert("WebFetch".to_string(), tool_stats(20));
let newly = check_achievements(&stats, &mut progress);
assert!(newly.contains(&AchievementId::WebFetcher));
@@ -2865,8 +2880,8 @@ mod tests {
let mut stats = create_test_stats();
let mut progress = AchievementProgress::new();
stats.tools_usage.insert("mcp__github__create_issue".to_string(), 25);
stats.tools_usage.insert("mcp__notion__search".to_string(), 25);
stats.tools_usage.insert("mcp__github__create_issue".to_string(), tool_stats(25));
stats.tools_usage.insert("mcp__notion__search".to_string(), tool_stats(25));
let newly = check_achievements(&stats, &mut progress);
assert!(newly.contains(&AchievementId::McpExplorer));
@@ -2881,8 +2896,8 @@ mod tests {
let mut stats = create_test_stats();
let mut progress = AchievementProgress::new();
stats.tools_usage.insert("Grep".to_string(), 30);
stats.tools_usage.insert("Glob".to_string(), 20);
stats.tools_usage.insert("Grep".to_string(), tool_stats(30));
stats.tools_usage.insert("Glob".to_string(), tool_stats(20));
let newly = check_achievements(&stats, &mut progress);
assert!(newly.contains(&AchievementId::Explorer));
@@ -2893,9 +2908,9 @@ mod tests {
let mut stats = create_test_stats();
let mut progress = AchievementProgress::new();
stats.tools_usage.insert("Grep".to_string(), 200);
stats.tools_usage.insert("Glob".to_string(), 200);
stats.tools_usage.insert("Task".to_string(), 100);
stats.tools_usage.insert("Grep".to_string(), tool_stats(200));
stats.tools_usage.insert("Glob".to_string(), tool_stats(200));
stats.tools_usage.insert("Task".to_string(), tool_stats(100));
let newly = check_achievements(&stats, &mut progress);
assert!(newly.contains(&AchievementId::MasterSearcher));