diff --git a/CHANGELOG.md b/CHANGELOG.md index 08cae60ae..dfea74def 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,31 @@ All notable changes to rtk (Rust Token Killer) will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.37.1](https://github.com/rtk-ai/rtk/compare/v0.37.0...v0.37.1) (2026-04-18) + + +### Bug Fixes + +* **docs:** user facing docs ([c8d6878](https://github.com/rtk-ai/rtk/commit/c8d68787fb8b31c52125e9fc7ea62e0aa590485f)) + +## [0.37.0](https://github.com/rtk-ai/rtk/compare/v0.36.0...v0.37.0) (2026-04-17) + + +### Features + +* **discover:** handle more npm/npx/pnpm/pnpx patterns ([9e96caa](https://github.com/rtk-ai/rtk/commit/9e96caa0a18a95c84da82ba57716a9d3ef86d0c8)) +* **refacto-core:** binary hook w/ native cmd exec + streaming ([e7b7f9a](https://github.com/rtk-ai/rtk/commit/e7b7f9ab665a0f7303d41d23ad156d24e5e8964e)) + + +### Bug Fixes + +* **docs:** use release please changelog no manual ([7591a14](https://github.com/rtk-ai/rtk/commit/7591a14e4ceb732ab7ca160ac01a852926abe77a)) +* isolate cursor hook tests from local settings (determinist) ([d8ddefe](https://github.com/rtk-ai/rtk/commit/d8ddefe78efe25c35bb2a2f9083f2eacb9dd7274)) +* P0+P1 fixes from pre-merge review of hook engine ([df8e035](https://github.com/rtk-ai/rtk/commit/df8e03558d4d6cc2f5cbac91c63ab1b3b51d3bcd)) +* P0+P1 fixes from pre-merge review of hook engine ([d34389c](https://github.com/rtk-ai/rtk/commit/d34389c3d0936c2b0790e14f450bb50a28a7edf7)) +* rename ship.md to ship/SKILL.md to match develop ([5916ecd](https://github.com/rtk-ai/rtk/commit/5916ecd86fb319c2519a0b4fb2891309833a3bb4)) +* **runner:** preserve fd separation on command failure ([e92d099](https://github.com/rtk-ai/rtk/commit/e92d0993c93f0b732316dfa932d265aeca7488d6)) +* **stream:** missing stderr fields ([a1d46f3](https://github.com/rtk-ai/rtk/commit/a1d46f39c291e3356b9c26a062bde05ba1de591a)) ## [0.36.0](https://github.com/rtk-ai/rtk/compare/v0.35.0...v0.36.0) (2026-04-13) diff --git a/src/analytics/gain.rs b/src/analytics/gain.rs index a43ebbeb0..c428cfe00 100644 --- a/src/analytics/gain.rs +++ b/src/analytics/gain.rs @@ -13,7 +13,8 @@ use std::path::PathBuf; #[allow(clippy::too_many_arguments)] pub fn run( - project: bool, // added: per-project scope flag + project: bool, + agent: Option, graph: bool, history: bool, quota: bool, @@ -27,7 +28,8 @@ pub fn run( _verbose: u8, ) -> Result<()> { let tracker = Tracker::new().context("Failed to initialize tracking database")?; - let project_scope = resolve_project_scope(project)?; // added: resolve project path + let project_scope = resolve_project_scope(project)?; + let agent_filter = agent.as_deref(); if failures { return show_failures(&tracker); @@ -42,7 +44,8 @@ pub fn run( weekly, monthly, all, - project_scope.as_deref(), // added: pass project scope + project_scope.as_deref(), + agent_filter, ); } "csv" => { @@ -52,14 +55,15 @@ pub fn run( weekly, monthly, all, - project_scope.as_deref(), // added: pass project scope + project_scope.as_deref(), + agent_filter, ); } _ => {} // Continue with text format } let summary = tracker - .get_summary_filtered(project_scope.as_deref()) // changed: use filtered variant + .get_summary_filtered(project_scope.as_deref(), agent_filter) .context("Failed to load token savings summary from database")?; if summary.total_commands == 0 { @@ -70,18 +74,20 @@ pub fn run( // Default view (summary) if !daily && !weekly && !monthly && !all { - // added: scope-aware styled header // changed: merged upstream styled + project scope - let title = if project_scope.is_some() { - "RTK Token Savings (Project Scope)" - } else { - "RTK Token Savings (Global Scope)" + let title = match (project_scope.is_some(), agent_filter) { + (true, Some(a)) => format!("RTK Token Savings (Project + Agent: {a})"), + (true, None) => "RTK Token Savings (Project Scope)".to_string(), + (false, Some(a)) => format!("RTK Token Savings (Agent: {a})"), + (false, None) => "RTK Token Savings (Global Scope)".to_string(), }; - println!("{}", styled(title, true)); + println!("{}", styled(&title, true)); println!("{}", "═".repeat(60)); - // added: show project path when scoped if let Some(ref scope) = project_scope { println!("Scope: {}", shorten_path(scope)); } + if let Some(a) = agent_filter { + println!("Agent: {a}"); + } println!(); // added: KPI-style aligned output @@ -226,7 +232,7 @@ pub fn run( } if history { - let recent = tracker.get_recent_filtered(10, project_scope.as_deref())?; // changed: filtered + let recent = tracker.get_recent_filtered(10, project_scope.as_deref(), agent_filter)?; if !recent.is_empty() { println!("{}", styled("Recent Commands", true)); // added: styled header println!("──────────────────────────────────────────────────────────"); @@ -289,15 +295,15 @@ pub fn run( // Time breakdown views if all || daily { - print_daily_full(&tracker, project_scope.as_deref())?; // changed: pass project scope + print_daily_full(&tracker, project_scope.as_deref(), agent_filter)?; } if all || weekly { - print_weekly(&tracker, project_scope.as_deref())?; // changed: pass project scope + print_weekly(&tracker, project_scope.as_deref(), agent_filter)?; } if all || monthly { - print_monthly(&tracker, project_scope.as_deref())?; // changed: pass project scope + print_monthly(&tracker, project_scope.as_deref(), agent_filter)?; } Ok(()) @@ -460,23 +466,32 @@ fn print_ascii_graph(data: &[(String, usize)]) { } } -fn print_daily_full(tracker: &Tracker, project_scope: Option<&str>) -> Result<()> { - // changed: add project scope - let days = tracker.get_all_days_filtered(project_scope)?; // changed: use filtered variant +fn print_daily_full( + tracker: &Tracker, + project_scope: Option<&str>, + agent_filter: Option<&str>, +) -> Result<()> { + let days = tracker.get_all_days_filtered(project_scope, agent_filter)?; print_period_table(&days); Ok(()) } -fn print_weekly(tracker: &Tracker, project_scope: Option<&str>) -> Result<()> { - // changed: add project scope - let weeks = tracker.get_by_week_filtered(project_scope)?; // changed: use filtered variant +fn print_weekly( + tracker: &Tracker, + project_scope: Option<&str>, + agent_filter: Option<&str>, +) -> Result<()> { + let weeks = tracker.get_by_week_filtered(project_scope, agent_filter)?; print_period_table(&weeks); Ok(()) } -fn print_monthly(tracker: &Tracker, project_scope: Option<&str>) -> Result<()> { - // changed: add project scope - let months = tracker.get_by_month_filtered(project_scope)?; // changed: use filtered variant +fn print_monthly( + tracker: &Tracker, + project_scope: Option<&str>, + agent_filter: Option<&str>, +) -> Result<()> { + let months = tracker.get_by_month_filtered(project_scope, agent_filter)?; print_period_table(&months); Ok(()) } @@ -509,10 +524,11 @@ fn export_json( weekly: bool, monthly: bool, all: bool, - project_scope: Option<&str>, // added: project scope + project_scope: Option<&str>, + agent_filter: Option<&str>, ) -> Result<()> { let summary = tracker - .get_summary_filtered(project_scope) // changed: use filtered variant + .get_summary_filtered(project_scope, agent_filter) .context("Failed to load token savings summary from database")?; let export = ExportData { @@ -526,17 +542,17 @@ fn export_json( avg_time_ms: summary.avg_time_ms, }, daily: if all || daily { - Some(tracker.get_all_days_filtered(project_scope)?) // changed: use filtered + Some(tracker.get_all_days_filtered(project_scope, agent_filter)?) } else { None }, weekly: if all || weekly { - Some(tracker.get_by_week_filtered(project_scope)?) // changed: use filtered + Some(tracker.get_by_week_filtered(project_scope, agent_filter)?) } else { None }, monthly: if all || monthly { - Some(tracker.get_by_month_filtered(project_scope)?) // changed: use filtered + Some(tracker.get_by_month_filtered(project_scope, agent_filter)?) } else { None }, @@ -554,10 +570,11 @@ fn export_csv( weekly: bool, monthly: bool, all: bool, - project_scope: Option<&str>, // added: project scope + project_scope: Option<&str>, + agent_filter: Option<&str>, ) -> Result<()> { if all || daily { - let days = tracker.get_all_days_filtered(project_scope)?; // changed: use filtered + let days = tracker.get_all_days_filtered(project_scope, agent_filter)?; println!("# Daily Data"); println!("date,commands,input_tokens,output_tokens,saved_tokens,savings_pct,total_time_ms,avg_time_ms"); for day in days { @@ -577,7 +594,7 @@ fn export_csv( } if all || weekly { - let weeks = tracker.get_by_week_filtered(project_scope)?; // changed: use filtered + let weeks = tracker.get_by_week_filtered(project_scope, agent_filter)?; println!("# Weekly Data"); println!( "week_start,week_end,commands,input_tokens,output_tokens,saved_tokens,savings_pct,total_time_ms,avg_time_ms" @@ -600,7 +617,7 @@ fn export_csv( } if all || monthly { - let months = tracker.get_by_month_filtered(project_scope)?; // changed: use filtered + let months = tracker.get_by_month_filtered(project_scope, agent_filter)?; println!("# Monthly Data"); println!("month,commands,input_tokens,output_tokens,saved_tokens,savings_pct,total_time_ms,avg_time_ms"); for month in months { diff --git a/src/core/tracking.rs b/src/core/tracking.rs index 982c937c4..f0519cfd8 100644 --- a/src/core/tracking.rs +++ b/src/core/tracking.rs @@ -307,6 +307,13 @@ impl Tracker { "CREATE INDEX IF NOT EXISTS idx_project_path_timestamp ON commands(project_path, timestamp)", [], ); + // Migration: add agent column for per-agent tracking + let _ = conn.execute("ALTER TABLE commands ADD COLUMN agent TEXT DEFAULT ''", []); + // Index for fast agent-scoped gain queries + let _ = conn.execute( + "CREATE INDEX IF NOT EXISTS idx_agent_timestamp ON commands(agent, timestamp)", + [], + ); conn.execute( "CREATE TABLE IF NOT EXISTS parse_failures ( @@ -364,15 +371,17 @@ impl Tracker { }; let project_path = current_project_path_string(); // added: record cwd + let agent = std::env::var("RTK_AGENT").unwrap_or_default(); self.conn.execute( - "INSERT INTO commands (timestamp, original_cmd, rtk_cmd, project_path, input_tokens, output_tokens, saved_tokens, savings_pct, exec_time_ms) - VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)", // added: project_path + "INSERT INTO commands (timestamp, original_cmd, rtk_cmd, project_path, agent, input_tokens, output_tokens, saved_tokens, savings_pct, exec_time_ms) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)", params![ Utc::now().to_rfc3339(), original_cmd, rtk_cmd, - project_path, // added + project_path, + agent, input_tokens as i64, output_tokens as i64, saved as i64, @@ -498,15 +507,20 @@ impl Tracker { /// ``` #[allow(dead_code)] pub fn get_summary(&self) -> Result { - self.get_summary_filtered(None) // delegate to filtered variant + self.get_summary_filtered(None, None) } - /// Get summary statistics filtered by project path. // added + /// Get summary statistics filtered by project path and/or agent. /// /// When `project_path` is `Some`, matches the exact working directory /// or any subdirectory (prefix match with path separator). - pub fn get_summary_filtered(&self, project_path: Option<&str>) -> Result { - let (project_exact, project_glob) = project_filter_params(project_path); // added + /// When `agent_filter` is `Some`, restricts to commands from that agent. + pub fn get_summary_filtered( + &self, + project_path: Option<&str>, + agent_filter: Option<&str>, + ) -> Result { + let (project_exact, project_glob) = project_filter_params(project_path); let mut total_commands = 0usize; let mut total_input = 0usize; let mut total_output = 0usize; @@ -516,11 +530,11 @@ impl Tracker { let mut stmt = self.conn.prepare( "SELECT input_tokens, output_tokens, saved_tokens, exec_time_ms FROM commands - WHERE (?1 IS NULL OR project_path = ?1 OR project_path GLOB ?2)", // added: project filter + WHERE (?1 IS NULL OR project_path = ?1 OR project_path GLOB ?2) + AND (?3 IS NULL OR agent = ?3)", )?; - let rows = stmt.query_map(params![project_exact, project_glob], |row| { - // added: params + let rows = stmt.query_map(params![project_exact, project_glob, agent_filter], |row| { Ok(( row.get::<_, i64>(0)? as usize, row.get::<_, i64>(1)? as usize, @@ -550,8 +564,8 @@ impl Tracker { 0 }; - let by_command = self.get_by_command(project_path)?; // added: pass project filter - let by_day = self.get_by_day(project_path)?; // added: pass project filter + let by_command = self.get_by_command(project_path, agent_filter)?; + let by_day = self.get_by_day(project_path, agent_filter)?; Ok(GainSummary { total_commands, @@ -568,20 +582,21 @@ impl Tracker { fn get_by_command( &self, - project_path: Option<&str>, // added + project_path: Option<&str>, + agent_filter: Option<&str>, ) -> Result> { - let (project_exact, project_glob) = project_filter_params(project_path); // added + let (project_exact, project_glob) = project_filter_params(project_path); let mut stmt = self.conn.prepare( "SELECT rtk_cmd, COUNT(*), SUM(saved_tokens), AVG(savings_pct), AVG(exec_time_ms) FROM commands WHERE (?1 IS NULL OR project_path = ?1 OR project_path GLOB ?2) + AND (?3 IS NULL OR agent = ?3) GROUP BY rtk_cmd ORDER BY SUM(saved_tokens) DESC - LIMIT 10", // added: project filter in WHERE + LIMIT 10", )?; - let rows = stmt.query_map(params![project_exact, project_glob], |row| { - // added: params + let rows = stmt.query_map(params![project_exact, project_glob, agent_filter], |row| { Ok(( row.get::<_, String>(0)?, row.get::<_, i64>(1)? as usize, @@ -596,20 +611,21 @@ impl Tracker { fn get_by_day( &self, - project_path: Option<&str>, // added + project_path: Option<&str>, + agent_filter: Option<&str>, ) -> Result> { - let (project_exact, project_glob) = project_filter_params(project_path); // added + let (project_exact, project_glob) = project_filter_params(project_path); let mut stmt = self.conn.prepare( "SELECT DATE(timestamp), SUM(saved_tokens) FROM commands WHERE (?1 IS NULL OR project_path = ?1 OR project_path GLOB ?2) + AND (?3 IS NULL OR agent = ?3) GROUP BY DATE(timestamp) ORDER BY DATE(timestamp) DESC - LIMIT 30", // added: project filter in WHERE + LIMIT 30", )?; - let rows = stmt.query_map(params![project_exact, project_glob], |row| { - // added: params + let rows = stmt.query_map(params![project_exact, project_glob, agent_filter], |row| { Ok((row.get::<_, String>(0)?, row.get::<_, i64>(1)? as usize)) })?; @@ -637,12 +653,16 @@ impl Tracker { /// # Ok::<(), anyhow::Error>(()) /// ``` pub fn get_all_days(&self) -> Result> { - self.get_all_days_filtered(None) // delegate to filtered variant + self.get_all_days_filtered(None, None) } - /// Get daily statistics filtered by project path. // added - pub fn get_all_days_filtered(&self, project_path: Option<&str>) -> Result> { - let (project_exact, project_glob) = project_filter_params(project_path); // added + /// Get daily statistics filtered by project path and/or agent. + pub fn get_all_days_filtered( + &self, + project_path: Option<&str>, + agent_filter: Option<&str>, + ) -> Result> { + let (project_exact, project_glob) = project_filter_params(project_path); let mut stmt = self.conn.prepare( "SELECT DATE(timestamp) as date, @@ -653,12 +673,12 @@ impl Tracker { SUM(exec_time_ms) as total_time FROM commands WHERE (?1 IS NULL OR project_path = ?1 OR project_path GLOB ?2) + AND (?3 IS NULL OR agent = ?3) GROUP BY DATE(timestamp) - ORDER BY DATE(timestamp) DESC", // added: project filter + ORDER BY DATE(timestamp) DESC", )?; - let rows = stmt.query_map(params![project_exact, project_glob], |row| { - // added: params + let rows = stmt.query_map(params![project_exact, project_glob, agent_filter], |row| { let input = row.get::<_, i64>(2)? as usize; let saved = row.get::<_, i64>(4)? as usize; let commands = row.get::<_, i64>(1)? as usize; @@ -710,12 +730,16 @@ impl Tracker { /// # Ok::<(), anyhow::Error>(()) /// ``` pub fn get_by_week(&self) -> Result> { - self.get_by_week_filtered(None) // delegate to filtered variant + self.get_by_week_filtered(None, None) } - /// Get weekly statistics filtered by project path. // added - pub fn get_by_week_filtered(&self, project_path: Option<&str>) -> Result> { - let (project_exact, project_glob) = project_filter_params(project_path); // added + /// Get weekly statistics filtered by project path and/or agent. + pub fn get_by_week_filtered( + &self, + project_path: Option<&str>, + agent_filter: Option<&str>, + ) -> Result> { + let (project_exact, project_glob) = project_filter_params(project_path); let mut stmt = self.conn.prepare( "SELECT DATE(timestamp, 'weekday 0', '-6 days') as week_start, @@ -727,12 +751,12 @@ impl Tracker { SUM(exec_time_ms) as total_time FROM commands WHERE (?1 IS NULL OR project_path = ?1 OR project_path GLOB ?2) + AND (?3 IS NULL OR agent = ?3) GROUP BY week_start - ORDER BY week_start DESC", // added: project filter + ORDER BY week_start DESC", )?; - let rows = stmt.query_map(params![project_exact, project_glob], |row| { - // added: params + let rows = stmt.query_map(params![project_exact, project_glob, agent_filter], |row| { let input = row.get::<_, i64>(3)? as usize; let saved = row.get::<_, i64>(5)? as usize; let commands = row.get::<_, i64>(2)? as usize; @@ -785,12 +809,16 @@ impl Tracker { /// # Ok::<(), anyhow::Error>(()) /// ``` pub fn get_by_month(&self) -> Result> { - self.get_by_month_filtered(None) // delegate to filtered variant + self.get_by_month_filtered(None, None) } - /// Get monthly statistics filtered by project path. // added - pub fn get_by_month_filtered(&self, project_path: Option<&str>) -> Result> { - let (project_exact, project_glob) = project_filter_params(project_path); // added + /// Get monthly statistics filtered by project path and/or agent. + pub fn get_by_month_filtered( + &self, + project_path: Option<&str>, + agent_filter: Option<&str>, + ) -> Result> { + let (project_exact, project_glob) = project_filter_params(project_path); let mut stmt = self.conn.prepare( "SELECT strftime('%Y-%m', timestamp) as month, @@ -801,12 +829,12 @@ impl Tracker { SUM(exec_time_ms) as total_time FROM commands WHERE (?1 IS NULL OR project_path = ?1 OR project_path GLOB ?2) + AND (?3 IS NULL OR agent = ?3) GROUP BY month - ORDER BY month DESC", // added: project filter + ORDER BY month DESC", )?; - let rows = stmt.query_map(params![project_exact, project_glob], |row| { - // added: params + let rows = stmt.query_map(params![project_exact, project_glob, agent_filter], |row| { let input = row.get::<_, i64>(2)? as usize; let saved = row.get::<_, i64>(4)? as usize; let commands = row.get::<_, i64>(1)? as usize; @@ -862,26 +890,28 @@ impl Tracker { /// ``` #[allow(dead_code)] pub fn get_recent(&self, limit: usize) -> Result> { - self.get_recent_filtered(limit, None) // delegate to filtered variant + self.get_recent_filtered(limit, None, None) } - /// Get recent command history filtered by project path. // added + /// Get recent command history filtered by project path and/or agent. pub fn get_recent_filtered( &self, limit: usize, project_path: Option<&str>, + agent_filter: Option<&str>, ) -> Result> { - let (project_exact, project_glob) = project_filter_params(project_path); // added + let (project_exact, project_glob) = project_filter_params(project_path); let mut stmt = self.conn.prepare( "SELECT timestamp, rtk_cmd, saved_tokens, savings_pct FROM commands WHERE (?1 IS NULL OR project_path = ?1 OR project_path GLOB ?2) + AND (?3 IS NULL OR agent = ?3) ORDER BY timestamp DESC - LIMIT ?3", // added: project filter + LIMIT ?4", )?; let rows = stmt.query_map( - params![project_exact, project_glob, limit as i64], // added: project params + params![project_exact, project_glob, agent_filter, limit as i64], |row| { Ok(CommandRecord { timestamp: DateTime::parse_from_rfc3339(&row.get::<_, String>(0)?) diff --git a/src/hooks/hook_cmd.rs b/src/hooks/hook_cmd.rs index cd3c82d1e..24b879fec 100644 --- a/src/hooks/hook_cmd.rs +++ b/src/hooks/hook_cmd.rs @@ -56,7 +56,7 @@ pub fn run_copilot() -> Result<()> { }; match detect_format(&v) { - HookFormat::VsCode { command } => handle_vscode(&command), + HookFormat::VsCode { command } => handle_vscode(&command, "copilot"), HookFormat::CopilotCli { command } => handle_copilot_cli(&command), HookFormat::PassThrough => Ok(()), } @@ -120,7 +120,17 @@ fn get_rewritten(cmd: &str) -> Option { Some(rewritten) } -fn handle_vscode(cmd: &str) -> Result<()> { +/// Prepend `RTK_AGENT=` to a rewritten command so the proxy binary +/// can record which AI agent triggered the execution. +/// Skips injection if `agent` is empty or the prefix is already present. +fn prepend_agent_env(agent: &str, cmd: &str) -> String { + if agent.is_empty() || cmd.starts_with("RTK_AGENT=") { + return cmd.to_string(); + } + format!("RTK_AGENT={agent} {cmd}") +} + +fn handle_vscode(cmd: &str, agent: &str) -> Result<()> { let verdict = permissions::check_command(cmd); if verdict == PermissionVerdict::Deny { audit_log("deny", cmd, ""); @@ -131,6 +141,7 @@ fn handle_vscode(cmd: &str) -> Result<()> { Some(r) => r, None => return Ok(()), }; + let rewritten = prepend_agent_env(agent, &rewritten); // Allow (explicit rule matched): auto-allow the rewritten command. // Ask/Default (no allow rule matched): rewrite but let the host tool prompt. @@ -217,8 +228,9 @@ pub fn run_gemini() -> Result<()> { match rewrite_command(cmd, &excluded) { Some(ref rewritten) => { - audit_log("rewrite", cmd, rewritten); - print_rewrite(rewritten); + let rewritten = prepend_agent_env("gemini", rewritten); + audit_log("rewrite", cmd, &rewritten); + print_rewrite(&rewritten); } None => print_allow(), } @@ -297,7 +309,7 @@ enum PayloadAction { Ignore, } -fn process_claude_payload(v: &Value) -> PayloadAction { +fn process_claude_payload(v: &Value, agent: &str) -> PayloadAction { let cmd = match v .pointer("/tool_input/command") .and_then(|c| c.as_str()) @@ -324,6 +336,7 @@ fn process_claude_payload(v: &Value) -> PayloadAction { } } }; + let rewritten = prepend_agent_env(agent, &rewritten); let updated_input = { let mut ti = v.get("tool_input").cloned().unwrap_or_else(|| json!({})); @@ -370,7 +383,7 @@ pub fn run_claude() -> Result<()> { } }; - match process_claude_payload(&v) { + match process_claude_payload(&v, "claude") { PayloadAction::Rewrite { cmd, rewritten, @@ -391,7 +404,7 @@ pub fn run_claude() -> Result<()> { #[cfg(test)] fn run_claude_inner(input: &str) -> Option { let v: Value = serde_json::from_str(input).ok()?; - match process_claude_payload(&v) { + match process_claude_payload(&v, "") { PayloadAction::Rewrite { output, .. } => Some(output.to_string()), _ => None, } @@ -443,6 +456,7 @@ pub fn run_cursor() -> Result<()> { return Ok(()); } }; + let rewritten = prepend_agent_env("cursor", &rewritten); let decision = match verdict { PermissionVerdict::Allow => "allow", diff --git a/src/main.rs b/src/main.rs index e8a19c2be..6c01a5c3c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -379,9 +379,12 @@ enum Commands { /// Show token savings summary and history Gain { - /// Filter statistics to current project (current working directory) // added + /// Filter statistics to current project (current working directory) #[arg(short, long)] project: bool, + /// Filter statistics by agent name (claude, cursor, gemini, copilot, windsurf, etc.) + #[arg(long)] + agent: Option, /// Show ASCII graph of daily savings #[arg(short, long)] graph: bool, @@ -1794,7 +1797,8 @@ fn run_cli() -> Result { Commands::Wc { args } => wc_cmd::run(&args, cli.verbose)?, Commands::Gain { - project, // added + project, + agent, graph, history, quota, @@ -1807,7 +1811,8 @@ fn run_cli() -> Result { failures, } => { analytics::gain::run( - project, // added: pass project flag + project, + agent, graph, history, quota,