Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions docs/usage/AUDIT_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ rtk gain --all --format csv > savings.csv
# Combined flags
rtk gain --graph --history --quota # Classic view with extras
rtk gain --daily --weekly --monthly # Multiple breakdowns

# Reset all tracking data
rtk gain --reset # prompts [y/N] before deleting
rtk gain --reset --yes # skip prompt (CI/scripts)
```

## Command Options
Expand All @@ -51,6 +55,15 @@ rtk gain --daily --weekly --monthly # Multiple breakdowns
| `--quota` | Monthly quota analysis (Pro/5x/20x tiers) |
| `--tier <TIER>` | Quota tier: pro, 5x, 20x (default: 20x) |

### Reset Flag

| Flag | Description |
|------|-------------|
| `--reset` | Permanently delete all tracking data (commands + parse failures) |
| `--yes` | Skip the confirmation prompt (for CI/scripts) |

> **Warning**: `--reset` is irreversible. It clears both the `commands` and `parse_failures` tables atomically. A `[y/N]` confirmation prompt is shown by default. In non-interactive environments (piped stdin), it defaults to `N` unless `--yes` is passed.

### Export Formats

| Format | Flag | Use Case |
Expand Down
37 changes: 37 additions & 0 deletions src/analytics/gain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,25 @@ pub fn run(
all: bool,
format: &str,
failures: bool,
reset: bool,
yes: bool,
_verbose: u8,
) -> Result<()> {
let tracker = Tracker::new().context("Failed to initialize tracking database")?;
let project_scope = resolve_project_scope(project)?; // added: resolve project path

if reset {
if !yes && !confirm_reset()? {
println!("Aborted.");
return Ok(());
}
tracker
.reset_all()
.context("Failed to reset token savings")?;
println!("{}", styled("Token savings stats reset to zero.", true));
return Ok(());
}

if failures {
return show_failures(&tracker);
}
Expand Down Expand Up @@ -725,3 +739,26 @@ fn show_failures(tracker: &Tracker) -> Result<()> {

Ok(())
}

/// Prompt the user to confirm a destructive reset operation.
/// Defaults to No in non-interactive (piped) environments.
fn confirm_reset() -> Result<bool> {
use std::io::{self, BufRead, IsTerminal, Write};

eprint!("This will permanently delete all tracking data. Continue? [y/N] ");
io::stderr().flush().ok();

if !io::stdin().is_terminal() {
eprintln!("(non-interactive mode, defaulting to N)");
return Ok(false);
}

let stdin = io::stdin();
let mut line = String::new();
stdin
.lock()
.read_line(&mut line)
.context("Failed to read confirmation")?;

Ok(matches!(line.trim().to_lowercase().as_str(), "y" | "yes"))
}
105 changes: 104 additions & 1 deletion src/core/tracking.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
//!
//! See [docs/tracking.md](../docs/tracking.md) for full documentation.

use anyhow::Result;
use anyhow::{Context, Result};
use chrono::{DateTime, Utc};
use rusqlite::{params, Connection};
use serde::Serialize;
Expand Down Expand Up @@ -326,6 +326,56 @@ impl Tracker {
Ok(Self { conn })
}

/// Create an isolated in-memory tracker for tests.
#[cfg(test)]
pub fn new_in_memory() -> Result<Self> {
let conn = Connection::open_in_memory().context("Failed to open in-memory DB")?;
let tracker = Self { conn };
tracker.init_schema()?;
Ok(tracker)
}

fn init_schema(&self) -> Result<()> {
self.conn.execute(
"CREATE TABLE IF NOT EXISTS commands (
id INTEGER PRIMARY KEY,
timestamp TEXT NOT NULL,
original_cmd TEXT NOT NULL,
rtk_cmd TEXT NOT NULL,
input_tokens INTEGER NOT NULL,
output_tokens INTEGER NOT NULL,
saved_tokens INTEGER NOT NULL,
savings_pct REAL NOT NULL,
exec_time_ms INTEGER DEFAULT 0,
project_path TEXT DEFAULT ''
)",
[],
)?;
self.conn.execute(
"CREATE INDEX IF NOT EXISTS idx_timestamp ON commands(timestamp)",
[],
)?;
self.conn.execute(
"CREATE INDEX IF NOT EXISTS idx_project_path_timestamp ON commands(project_path, timestamp)",
[],
)?;
self.conn.execute(
"CREATE TABLE IF NOT EXISTS parse_failures (
id INTEGER PRIMARY KEY,
timestamp TEXT NOT NULL,
raw_command TEXT NOT NULL,
error_message TEXT NOT NULL,
fallback_succeeded INTEGER NOT NULL DEFAULT 0
)",
[],
)?;
self.conn.execute(
"CREATE INDEX IF NOT EXISTS idx_pf_timestamp ON parse_failures(timestamp)",
[],
)?;
Ok(())
}

/// Record a command execution with token counts and timing.
///
/// Calculates savings metrics and stores the record in the database.
Expand Down Expand Up @@ -398,6 +448,19 @@ impl Tracker {
Ok(())
}

/// Delete all tracked data (commands + parse_failures), resetting all stats to zero.
pub fn reset_all(&self) -> Result<()> {
self.conn
.execute_batch(
"BEGIN;
DELETE FROM commands;
DELETE FROM parse_failures;
COMMIT;",
)
.context("Failed to reset tracking database")?;
Ok(())
}

/// Record a parse failure for analytics.
pub fn record_parse_failure(
&self,
Expand Down Expand Up @@ -1389,4 +1452,44 @@ mod tests {
// but we can verify recovery_rate is between 0 and 100
assert!(summary.recovery_rate >= 0.0 && summary.recovery_rate <= 100.0);
}

#[test]
fn test_reset_all_clears_both_tables() {
let tracker = Tracker::new_in_memory().expect("Failed to create in-memory tracker");
let pid = std::process::id();

// Insert into commands
tracker
.record(
"git status",
&format!("rtk git status reset_test_{}", pid),
100,
20,
50,
)
.expect("Failed to record command");

// Insert into parse_failures
tracker
.record_parse_failure(&format!("bad_cmd_reset_test_{}", pid), "parse error", false)
.expect("Failed to record parse failure");

// Reset everything
tracker.reset_all().expect("Failed to reset");

// Both tables should be empty
let summary = tracker.get_summary().expect("Failed to get summary");
assert_eq!(
summary.total_commands, 0,
"commands table should be empty after reset"
);

let failures = tracker
.get_parse_failure_summary()
.expect("Failed to get failure summary");
assert_eq!(
failures.total, 0,
"parse_failures table should be empty after reset"
);
}
}
10 changes: 10 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,12 @@ enum Commands {
/// Show parse failure log (commands that fell back to raw execution)
#[arg(short = 'F', long)]
failures: bool,
/// Reset all token savings stats to zero
#[arg(long)]
reset: bool,
/// Skip confirmation prompt when resetting
#[arg(long, requires = "reset")]
yes: bool,
},

/// Claude Code economics: spending (ccusage) vs savings (rtk) analysis
Expand Down Expand Up @@ -1684,6 +1690,8 @@ fn run_cli() -> Result<i32> {
all,
format,
failures,
reset,
yes,
} => {
analytics::gain::run(
project, // added: pass project flag
Expand All @@ -1697,6 +1705,8 @@ fn run_cli() -> Result<i32> {
all,
&format,
failures,
reset,
yes,
cli.verbose,
)?;
0
Expand Down