Skip to content
Merged
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
4 changes: 2 additions & 2 deletions src/analytics/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

## Scope

**Read-only dashboards** over the tracking database. Analytics presents the value that `cmds/` creates — it queries token savings, correlates with external spending data, and surfaces adoption opportunities. It never modifies the tracking DB.
**Read-only dashboards** over the tracking database. Queries token savings, correlates with external spending data, and surfaces adoption metrics. Never modifies the tracking DB.

Owns: `rtk gain` (savings dashboard), `rtk cc-economics` (cost reduction), `rtk session` (adoption analysis), and Claude Code usage data parsing.

Expand All @@ -15,7 +15,7 @@ Boundary rule: if a new module writes to the DB, it belongs in `core/` or `cmds/
## Purpose
Token savings analytics, economic modeling, and adoption metrics.

These modules read from the SQLite tracking database to produce dashboards, spending estimates, and session-level adoption reports that help users understand the value RTK provides.
These modules read from the SQLite tracking database to produce dashboards, spending estimates, and session-level adoption reports.

## Adding New Functionality
To add a new analytics view: (1) create a new `*_cmd.rs` file in this directory, (2) query `core/tracking` for the metrics you need using the existing `TrackingDb` API, (3) register the command in `main.rs` under the `Commands` enum, and (4) add `#[cfg(test)]` unit tests with sample tracking data. Analytics modules should be read-only against the tracking database and never modify it.
3 changes: 0 additions & 3 deletions src/analytics/cc_economics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,6 @@ use crate::core::utils::{format_cpt, format_tokens, format_usd};

// ── Constants ──

#[allow(dead_code)]
const BILLION: f64 = 1e9;

// API pricing ratios (verified Feb 2026, consistent across Claude models <=200K context)
// Source: https://docs.anthropic.com/en/docs/about-claude/models
const WEIGHT_OUTPUT: f64 = 5.0; // Output = 5x input
Expand Down
13 changes: 0 additions & 13 deletions src/analytics/ccusage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,12 +111,6 @@ fn build_command() -> Option<Command> {
None
}

/// Check if ccusage CLI is available (binary or via npx)
#[allow(dead_code)]
pub fn is_available() -> bool {
build_command().is_some()
}

/// Fetch usage data from ccusage for the last 90 days
///
/// Returns `Ok(None)` if ccusage is unavailable (graceful degradation)
Expand Down Expand Up @@ -328,11 +322,4 @@ mod tests {
assert_eq!(periods[0].metrics.cache_creation_tokens, 0); // default
assert_eq!(periods[0].metrics.cache_read_tokens, 0);
}

#[test]
fn test_is_available() {
// Just smoke test - actual availability depends on system
let _available = is_available();
// No assertion - just ensure it doesn't panic
}
}
189 changes: 119 additions & 70 deletions src/cmds/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## Scope

**Command execution and output filtering** — this is the core value RTK delivers. Every module here calls an external CLI tool (`Command::new("some_tool")`), transforms its stdout/stderr to reduce token consumption, and records savings via `core/tracking`.
**Command execution and output filtering.** Every module here calls an external CLI tool (`Command::new("some_tool")`), transforms its stdout/stderr to reduce token consumption, and records savings via `core/tracking`.

Owns: all command-specific filter logic, organized by ecosystem (git, rust, js, python, go, dotnet, cloud, system). Cross-ecosystem routing (e.g., `lint_cmd` detecting Python and delegating to `ruff_cmd`) is an intra-component concern.

Expand Down Expand Up @@ -35,47 +35,92 @@ Each subdirectory has its own README with file descriptions, parsing strategies,
- **[`system/`](system/README.md)** — ls, tree, read, grep, find, wc, env, json, log, deps, summary, format, smart — format_cmd routing, filter levels, language detection
- **[`ruby/`](ruby/README.md)** — rake/rails test, rspec, rubocop — JSON injection pattern, `ruby_exec()` bundle exec auto-detection

## Common Pattern
## Execution Flow: `runner::run_filtered()`

Every command module follows this structure:
The shared wrapper in [`core/runner.rs`](../core/runner.rs) encapsulates the six-phase execution skeleton. Modules build the `Command` (custom arg logic), then delegate to `run_filtered()` for everything else.

```
cmd.output() Filter applied to tee_and_hint()
| stdout or combined |
v | v
+---------+ stdout +-------+-------+ filtered +-------+
| Execute |--------->| filter_fn() |----------->| Print |
+---------+ stderr +---------------+ +-------+
| |
v v
+----------+ +---------+
| raw = | | Track |
| stdout + | | savings |
| stderr | +---------+
+----------+ |
v
+-----------+
| Ok(code) |
| returned |
+-----------+
```

**Six phases in order:**

1. **Execute** — `cmd.output()` captures stdout + stderr
2. **Filter** — `filter_fn` receives stdout-only or combined, returns compressed string
3. **Print** — filtered output printed; if tee enabled, appends recovery hint on failure
4. **Stderr passthrough** — when `filter_stdout_only`: stderr printed via `eprintln!()` unconditionally
5. **Track** — `timer.track()` records raw vs filtered for token savings
6. **Exit code** — returns `Ok(exit_code)` to caller; `main.rs` calls `process::exit(code)` once

**`RunOptions` builder:**

| Constructor | Behavior |
|-------------|----------|
| `RunOptions::default()` | Combined stdout+stderr to filter, no tee |
| `RunOptions::with_tee("label")` | Combined filtering + tee recovery |
| `RunOptions::stdout_only()` | Stdout-only to filter, stderr passthrough, no tee |
| `RunOptions::stdout_only().tee("label")` | Stdout-only + tee recovery |

**Example — filtered command (recommended):**

```rust
pub fn run(args: MyArgs, verbose: u8) -> Result<()> {
let timer = tracking::TimedExecution::start();
let output = resolved_command("mycmd").args(&args).output().context("Failed to execute mycmd")?;
let raw = format!("{}\n{}", String::from_utf8_lossy(&output.stdout), String::from_utf8_lossy(&output.stderr));

let filtered = filter_output(&raw).unwrap_or_else(|e| {
eprintln!("rtk: filter warning: {}", e);
raw.clone() // Fallback to raw on filter failure
});

let exit_code = output.status.code().unwrap_or(1);
if let Some(hint) = tee::tee_and_hint(&raw, "mycmd", exit_code) {
println!("{}\n{}", filtered, hint);
} else {
println!("{}", filtered);
}

timer.track("mycmd args", "rtk mycmd args", &raw, &filtered);
if !output.status.success() { std::process::exit(exit_code); }
Ok(())
pub fn run(args: &[String], verbose: u8) -> Result<i32> {
let mut cmd = resolved_command("mycmd");
for arg in args { cmd.arg(arg); }
if verbose > 0 { eprintln!("Running: mycmd {}", args.join(" ")); }

runner::run_filtered(
cmd, "mycmd", &args.join(" "),
filter_mycmd_output,
runner::RunOptions::stdout_only().tee("mycmd"),
)
}
```

Six phases: **timer** → **execute** → **filter (with fallback)** → **tee on failure** → **track** → **exit code**. See [core/README.md](../core/README.md#consumer-contracts) for the contracts each phase must honor.
Exit code handling is **fully automatic** when using `run_filtered()` — the wrapper extracts the exit code (including Unix signal handling via 128+signal), tracks savings, and returns `Ok(exit_code)`. Module authors just return the result.

## Token Savings by Category
**Example — passthrough command (no filtering):**

```rust
pub fn run_passthrough(args: &[OsString], verbose: u8) -> Result<i32> {
let status = resolved_command("mycmd").args(args)
.stdin(Stdio::inherit()).stdout(Stdio::inherit()).stderr(Stdio::inherit())
.status().context("Failed to run mycmd")?;
Ok(exit_code_from_status(&status, "mycmd"))
}
```

**Example — manual execution (custom logic):**

```rust
pub fn run(args: &[String], verbose: u8) -> Result<i32> {
let output = resolved_command("mycmd").args(args)
.output().context("Failed to run mycmd")?;
let exit_code = exit_code_from_output(&output, "mycmd");
// ... custom filtering, tracking ...
Ok(exit_code)
}
```

Modules with deviations (subcommand dispatch, parser trait systems, two-command fallback, synthetic output).

| Category | Commands | Typical Savings | Strategy |
|----------|----------|----------------|----------|
| Test Runners | vitest, pytest, cargo test, go test, playwright | 90-99% | Show failures only, aggregate passes |
| Build Tools | cargo build, npm, pnpm, dotnet | 70-90% | Strip progress bars, summarize errors |
| VCS | git status/log/diff/show | 70-80% | Compact commit hashes, stat summaries |
| Linters | eslint/biome, ruff, tsc, mypy, golangci-lint | 80-85% | Group by file/rule, strip context |
| Package Managers | pip, cargo install, pnpm list | 75-80% | Remove decorative output, compact trees |
| File Operations | ls, find, grep, cat/head/tail | 60-75% | Tree format, grouped results, truncation |
| Infrastructure | docker, kubectl, aws, terraform | 75-85% | Essential info only |

## Cross-Command Dependencies

Expand All @@ -89,7 +134,25 @@ These behaviors must be uniform across all command modules. Full audit details i

### Exit Code Propagation

Modules must capture the underlying command's exit code, propagate it via `std::process::exit()` only on failure, and return `Ok(())` on success. When the process is killed by signal (`.code()` returns `None`), default to exit code 1.
All module `run()` functions return `Result<i32>` where the `i32` is the underlying command's exit code. `main.rs` calls `std::process::exit(code)` once at the single exit point — **modules never call `process::exit()` directly**.

| Return value | Meaning | Who exits |
|--------------|---------|-----------|
| `Ok(0)` | Command succeeded | `main.rs` exits 0 |
| `Ok(N)` | Command failed with code N | `main.rs` exits N |
| `Err(e)` | RTK itself failed (not the command) | `main.rs` prints error, exits 1 |

**How exit codes are extracted:**

| Execution style | Helper | Signal handling |
|----------------|--------|-----------------|
| `cmd.output()` (filtered) | `exit_code_from_output(&output, "tool")` | 128+signal on Unix |
| `cmd.status()` (passthrough) | `exit_code_from_status(&status, "tool")` | 128+signal on Unix |
| `run_filtered()` (wrapper) | Automatic — no manual code needed | Built-in |

**When using `run_filtered()`**: exit code handling is fully automatic. The wrapper extracts the exit code, handles signals, and returns `Ok(exit_code)`. Module authors just return the wrapper's result — no exit code logic needed.

**When doing manual execution**: use `exit_code_from_output()` or `exit_code_from_status()` and return `Ok(exit_code)`. Never call `process::exit()`, never use `.code().unwrap_or(1)` (loses signal info).

### Filter Failure Passthrough

Expand All @@ -105,50 +168,36 @@ Modules must capture stderr and include it in the raw string passed to `timer.tr

### Tracking Completeness

All modules must call `timer.track()` on every path — success, failure, and fallback. Never exit before tracking.
All modules must call `timer.track()` on every path — success, failure, and fallback. Since modules return `Ok(exit_code)` instead of calling `process::exit()`, tracking always runs before the program exits.

### Verbose Flag

All modules accept `verbose: u8`. Use it to print debug info (command being run, savings %, filter tier). Do not accept and ignore it.

### Gaps (to be fixed)

**Exit code** — 5 different patterns coexist, should be reviewed for uniform behavior:
- `vitest_cmd.rs`, `tsc_cmd.rs`, `psql_cmd.rs` — exit unconditionally, even on success
- `lint_cmd.rs` — swallows signal kills silently
- `golangci_cmd.rs` — maps signal kill to exit 130 (correct but unique)

**Filter passthrough** — silent passthrough, no warning:
- `gh_cmd.rs`, `pip_cmd.rs`, `container.rs`, `dotnet_cmd.rs` — `run_passthrough()` skips filtering without warning
- `pnpm_cmd.rs` — 3-tier degradation but no tee recovery on final tier

**Tee recovery** — missing from some high-risk modules:
- `pnpm_cmd.rs` — 3-tier parser, no tee
- `gh_cmd.rs` — aggressive markdown filtering, no tee
- `ruff_cmd.rs`, `golangci_cmd.rs` — JSON parsers, no tee
- `psql_cmd.rs` — has tee but exits before calling it on error path

**Stderr handling** — 3 patterns coexist. Some modules combine stderr into raw (correct), others print via `eprintln!()` and exclude from tracking (inflates savings %). See `docs/ISO_ANALYZE.md` section 4.

**Tracking** — exit before track on error path:
- `ls.rs`, `tree.rs` — lost metrics on failure
- `container.rs` — inconsistent across subcommands

**Verbose** — accept parameter but ignore it:
- `container.rs` — all internal functions prefix `_verbose`
- `diff_cmd.rs` — `_verbose` unused

## Adding a New Command Filter

Adding a new filter or command requires changes in multiple places:
Adding a new filter or command requires changes in multiple places. For TOML-vs-Rust decision criteria, see [CONTRIBUTING.md](../../CONTRIBUTING.md#toml-vs-rust-which-one).

### Rust module (structured output, flag injection, state machines)

1. **Create the filter** — TOML file in [`src/filters/`](../filters/README.md) or Rust module in `src/cmds/<ecosystem>/`
2. **Add rewrite pattern** — Entry in `src/discover/rules.rs` (PATTERNS + RULES arrays at matching index) so hooks auto-rewrite the command
3. **Register in main.rs** — (Rust modules only) Three changes:
- Add `pub mod mymod;` to the ecosystem's `mod.rs` (e.g., `src/cmds/system/mod.rs`)
1. **Create module** in `src/cmds/<ecosystem>/mycmd_cmd.rs`:
- Write the `filter_mycmd()` function (pure: `&str -> String`, no side effects)
- Write `pub fn run(...) -> Result<i32>` using `runner::run_filtered()` — build the `Command`, choose `RunOptions`, delegate
- Use `RunOptions::stdout_only()` when the filter parses structured stdout (JSON, NDJSON) — stderr would corrupt parsing
- Use `RunOptions::default()` when filtering combined text output
- Add `.tee("label")` when the filter parses structured output (enables raw output recovery on failure)
- **Exit codes**: handled automatically by `run_filtered()` — just return its result
2. **Register module**:
- Add `pub mod mycmd_cmd;` to the ecosystem's `mod.rs`
- Add variant to `Commands` enum in `main.rs` with `#[arg(trailing_var_arg = true, allow_hyphen_values = true)]`
- Add routing match arm in `main.rs` to call `mymod::run()`
- Add routing match arm in `main.rs`: `Commands::Mycmd { args } => mycmd_cmd::run(&args, cli.verbose)?,`
3. **Add rewrite pattern** — Entry in `src/discover/rules.rs` (PATTERNS + RULES arrays at matching index) so hooks auto-rewrite the command
4. **Write tests** — Real fixture, snapshot test, token savings >= 60% (see [testing rules](../../.claude/rules/cli-testing.md))
5. **Update docs** — README.md command list, CHANGELOG.md
5. **Update docs** — Ecosystem README, CHANGELOG.md

### TOML filter (simple line-based filtering)

Follow the [Common Pattern](#common-pattern) above for the module template (timer, fallback, tee, tracking, exit code). For TOML-vs-Rust decision criteria, see [CONTRIBUTING.md](../../CONTRIBUTING.md#toml-vs-rust-which-one).
1. **Create filter** in [`src/filters/`](../filters/README.md)
2. **Add rewrite pattern** in `src/discover/rules.rs`
3. **Write tests** and **update docs**
Loading
Loading