1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
//! Scans AI coding sessions to find commands that could benefit from RTK filtering.
pub mod lexer;
pub mod provider;
pub mod registry;
mod report;
pub mod rules;
use anyhow::Result;
use std::collections::HashMap;
use provider::{ClaudeProvider, SessionProvider};
use registry::{
category_avg_tokens, classify_command, split_command_chain, strip_disabled_prefix,
Classification,
};
use report::{DiscoverReport, SupportedEntry, UnsupportedEntry};
use crate::discover::registry::prefix_contains_rtk_disabled;
/// Aggregation bucket for supported commands.
struct SupportedBucket {
rtk_equivalent: &'static str,
category: &'static str,
count: usize,
/// Total estimated tokens *saved* (post-filter). Used for the "Est. Savings" column.
total_output_tokens: usize,
/// Total estimated tokens *before* filtering (raw output). Accumulated alongside
/// `total_output_tokens` so the bucket's effective savings rate can be derived as
/// `total_output_tokens / total_raw_output_tokens` — a weighted average across
/// all sub-commands, regardless of which sub-command was seen first.
total_raw_output_tokens: usize,
// For display: the most common raw command
command_counts: HashMap<String, usize>,
}
/// Aggregation bucket for unsupported commands.
struct UnsupportedBucket {
count: usize,
example: String,
}
pub fn run(
project: Option<&str>,
all: bool,
since_days: u64,
limit: usize,
format: &str,
verbose: u8,
) -> Result<()> {
let provider = ClaudeProvider;
// Determine project filter
let project_filter = if all {
None
} else if let Some(p) = project {
Some(p.to_string())
} else {
// Default: current working directory
let cwd = std::env::current_dir()?;
let cwd_str = cwd.to_string_lossy().to_string();
let encoded = ClaudeProvider::encode_project_path(&cwd_str);
Some(encoded)
};
let sessions = provider.discover_sessions(project_filter.as_deref(), Some(since_days))?;
if verbose > 0 {
eprintln!("Scanning {} session files...", sessions.len());
for s in &sessions {
eprintln!(" {}", s.display());
}
}
let mut total_commands: usize = 0;
let mut already_rtk: usize = 0;
let mut parse_errors: usize = 0;
let mut rtk_disabled_count: usize = 0;
let mut rtk_disabled_cmds: HashMap<String, usize> = HashMap::new();
let mut supported_map: HashMap<&'static str, SupportedBucket> = HashMap::new();
let mut unsupported_map: HashMap<String, UnsupportedBucket> = HashMap::new();
for session_path in &sessions {
let extracted = match provider.extract_commands(session_path) {
Ok(cmds) => cmds,
Err(e) => {
if verbose > 0 {
eprintln!("Warning: skipping {}: {}", session_path.display(), e);
}
parse_errors += 1;
continue;
}
};
for ext_cmd in &extracted {
let parts = split_command_chain(&ext_cmd.command);
for part in parts {
total_commands += 1;
// Detect RTK_DISABLED= bypass before classification
let (env_prefix, actual_cmd) = strip_disabled_prefix(part);
if prefix_contains_rtk_disabled(env_prefix) {
// Only count if the underlying command is one RTK supports
match classify_command(actual_cmd) {
Classification::Supported { .. } => {
rtk_disabled_count += 1;
let display = truncate_command(actual_cmd);
*rtk_disabled_cmds.entry(display).or_insert(0) += 1;
}
_ => {
// RTK_DISABLED on unsupported/ignored command — not interesting
}
}
continue;
}
match classify_command(part) {
Classification::Supported {
rtk_equivalent,
category,
estimated_savings_pct,
status,
} => {
let bucket = supported_map.entry(rtk_equivalent).or_insert_with(|| {
SupportedBucket {
rtk_equivalent,
category,
count: 0,
total_output_tokens: 0,
total_raw_output_tokens: 0,
command_counts: HashMap::new(),
}
});
bucket.count += 1;
// Estimate tokens for this command
let output_tokens = if let Some(len) = ext_cmd.output_len {
// Real: from tool_result content length
len / 4
} else {
// Fallback: category average
let subcmd = extract_subcmd(part);
category_avg_tokens(category, subcmd)
};
let savings =
(output_tokens as f64 * estimated_savings_pct / 100.0) as usize;
bucket.total_output_tokens += savings;
// Accumulate pre-savings tokens so we can compute a weighted effective
// savings rate across all sub-commands in this bucket later.
bucket.total_raw_output_tokens += output_tokens;
// Track the display name with status
let display_name = truncate_command(part);
let entry = bucket
.command_counts
.entry(format!("{}:{:?}", display_name, status))
.or_insert(0);
*entry += 1;
}
Classification::Unsupported { base_command } => {
let bucket = unsupported_map.entry(base_command).or_insert_with(|| {
UnsupportedBucket {
count: 0,
example: part.to_string(),
}
});
bucket.count += 1;
}
Classification::Ignored => {
// Check if it starts with "rtk "
if part.trim().starts_with("rtk ") {
already_rtk += 1;
}
// Otherwise just skip
}
}
}
}
}
// Build report
let mut supported: Vec<SupportedEntry> = supported_map
.into_values()
.map(|bucket| {
// Pick the most common command as the display name
let (command_with_status, status) = bucket
.command_counts
.into_iter()
.max_by_key(|(_, c)| *c)
.map(|(name, _)| {
// Extract status from "command:Status" format
if let Some(colon_pos) = name.rfind(':') {
let cmd = name[..colon_pos].to_string();
let status_str = &name[colon_pos + 1..];
let status = match status_str {
"Passthrough" => report::RtkStatus::Passthrough,
"NotSupported" => report::RtkStatus::NotSupported,
_ => report::RtkStatus::Existing,
};
(cmd, status)
} else {
(name, report::RtkStatus::Existing)
}
})
.unwrap_or_else(|| (String::new(), report::RtkStatus::Existing));
// Derive the effective savings rate from accumulated totals rather than
// using the first-seen sub-command's rate. This gives a weighted average
// across all sub-commands that fell in this bucket.
let effective_savings_pct = if bucket.total_raw_output_tokens > 0 {
bucket.total_output_tokens as f64 * 100.0 / bucket.total_raw_output_tokens as f64
} else {
0.0
};
SupportedEntry {
command: command_with_status,
count: bucket.count,
rtk_equivalent: bucket.rtk_equivalent,
category: bucket.category,
estimated_savings_tokens: bucket.total_output_tokens,
estimated_savings_pct: effective_savings_pct,
rtk_status: status,
}
})
.collect();
// Sort by estimated savings descending
supported.sort_by_key(|b| std::cmp::Reverse(b.estimated_savings_tokens));
let mut unsupported: Vec<UnsupportedEntry> = unsupported_map
.into_iter()
.map(|(base, bucket)| UnsupportedEntry {
base_command: base,
count: bucket.count,
example: bucket.example,
})
.collect();
// Sort by count descending
unsupported.sort_by_key(|b| std::cmp::Reverse(b.count));
// Build RTK_DISABLED examples sorted by frequency (top 5)
let rtk_disabled_examples: Vec<String> = {
let mut sorted: Vec<_> = rtk_disabled_cmds.into_iter().collect();
sorted.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(&b.0)));
sorted
.into_iter()
.take(5)
.map(|(cmd, count)| format!("{} ({}x)", cmd, count))
.collect()
};
let report = DiscoverReport {
sessions_scanned: sessions.len(),
total_commands,
already_rtk,
since_days,
supported,
unsupported,
parse_errors,
rtk_disabled_count,
rtk_disabled_examples,
agent_status: report::AgentIntegrationStatus::detect(),
};
match format {
"json" => println!("{}", report::format_json(&report)),
_ => print!("{}", report::format_text(&report, limit, verbose > 0)),
}
Ok(())
}
/// Extract the subcommand from a command string (second word).
fn extract_subcmd(cmd: &str) -> &str {
let parts: Vec<&str> = cmd.trim().splitn(3, char::is_whitespace).collect();
if parts.len() >= 2 {
parts[1]
} else {
""
}
}
/// Truncate a command for display (keep first meaningful portion).
fn truncate_command(cmd: &str) -> String {
let trimmed = cmd.trim();
// Keep first two words for display
let parts: Vec<&str> = trimmed.splitn(3, char::is_whitespace).collect();
match parts.len() {
0 => String::new(),
1 => parts[0].to_string(),
_ => format!("{} {}", parts[0], parts[1]),
}
}