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
37 changes: 36 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,36 @@ git status # Automatically rewritten to rtk git status

Hook-based agents rewrite Bash commands (e.g., `git status` -> `rtk git status`) before execution. Plugin-based agents, including Hermes, use their plugin API to rewrite commands before execution. The agent receives compact output without needing to call `rtk` explicitly.

## Grep (agent-friendly)

```bash
rtk grep "Foo" . --files-only
rtk grep "Foo" . --count-by-file
rtk grep "Foo" . --top-files 10
rtk grep "Foo" . --agent-safe
rtk grep "Foo" . --agent-safe --max-per-file 30
rtk grep "Foo" . --json
rtk grep "Foo" . --agent-safe --json
rtk grep "Foo" . --all --full-lines

# Opt-in preset for agents (grep only in this slice):
RTK_AGENT_SAFE=1 rtk grep "Foo" .
```

Notes:
- `--files-only`: locator mode (paths only)
- `--count-by-file`: counts per file
- `--top-files N`: ranked file summary (top N files)
- `--agent-safe`: caps match spam + adds summary/hints (flags override env/config)
- `--json`: machine-readable JSON only (no human text)
- `--all`: disables match caps
- `--full-lines`: disables line clipping

PowerShell:
```powershell
$env:RTK_AGENT_SAFE="1"; rtk grep "Foo" src
```

**Important:** the hook only runs on Bash tool calls. Claude Code built-in tools like `Read`, `Grep`, and `Glob` do not pass through the Bash hook, so they are not auto-rewritten. To get RTK's compact output for those workflows, use shell commands (`cat`/`head`/`tail`, `rg`/`grep`, `find`) or call `rtk read`, `rtk grep`, or `rtk find` directly.

## How It Works
Expand Down Expand Up @@ -148,7 +178,12 @@ rtk read file.rs -l aggressive # Signatures only (strips bodies)
rtk read file.rs --lines 430:540 # Inclusive line range (1-based)
rtk smart file.rs # 2-line heuristic code summary
rtk find "*.rs" . # Compact find results
rtk grep "pattern" . # Grouped search results
rtk grep "pattern" . # Grouped search results (legacy defaults)
rtk grep "Foo" . --files-only # Unique matching file paths
rtk grep "Foo" . --count-by-file # Counts per file
rtk grep "Foo" . --agent-safe # Token-safe preset (caps + clipping + summary)
rtk grep "Foo" . --agent-safe --max-per-file 30
rtk grep "Foo" . --all --full-lines # Legacy full output (uncapped + unclipped)
rtk diff file1 file2 # Condensed diff
```

Expand Down
3 changes: 1 addition & 2 deletions src/cmds/cpp/msbuild_cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -450,10 +450,9 @@ fn dedup_diags(diags: &[MsbuildDiag]) -> Vec<MsbuildDiag> {
let mut seen: HashSet<String> = HashSet::new();
for d in diags {
let key = format!("{}|{}", d.code, d.raw);
if seen.contains(&key) {
if !seen.insert(key) {
continue;
}
seen.insert(key);
out.push(d.clone());
}
out
Expand Down
67 changes: 67 additions & 0 deletions src/cmds/git/git.rs
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,11 @@ where
if arg.contains('/') || arg.contains('\\') {
return path_exists(arg);
}
// Filename with extension (README.md) - treat as path if it exists.
// This is a safe middle-ground between "bare word" (main) and a ref.
if arg.contains('.') {
return path_exists(arg);
}
// Bare word (no separator, no special prefix) — never inject `--`
// This avoids misidentifying a ref/branch as a path even if a same-named
// file happens to exist on disk.
Expand Down Expand Up @@ -2022,6 +2027,17 @@ mod tests {
assert_eq!(normalize_diff_args_impl(&args, exists_mock(&[])), args);
}

/// Baseline: `--` already present with multiple pathspecs → no-op, args unchanged.
#[test]
fn test_normalize_diff_args_noop_when_separator_present_multiple_paths() {
let args = vec![
"--".to_string(),
"README.md".to_string(),
"src/cmds/system/README.md".to_string(),
];
assert_eq!(normalize_diff_args_impl(&args, exists_mock(&[])), args);
}

/// Core regression (issue #1215): clap ate `--` before a real file path.
/// When the path exists on disk, `--` must be re-inserted.
#[test]
Expand Down Expand Up @@ -2056,6 +2072,13 @@ mod tests {
);
}

/// Ref with explicit separator before a filename-with-extension → no-op, args unchanged.
#[test]
fn test_normalize_diff_args_noop_ref_then_separator_then_filename() {
let args = vec!["HEAD".to_string(), "--".to_string(), "README.md".to_string()];
assert_eq!(normalize_diff_args_impl(&args, exists_mock(&[])), args);
}

/// Flags before path: ["--cached", "src/foo.rs"] where src/foo.rs exists.
#[test]
fn test_normalize_diff_args_reinserts_separator_after_flag() {
Expand All @@ -2071,6 +2094,20 @@ mod tests {
);
}

/// Flag then filename-with-extension pathspec → inject separator after flag.
#[test]
fn test_normalize_diff_args_inject_after_flag_for_filename_with_extension() {
let args = vec!["--name-only".to_string(), "README.md".to_string()];
assert_eq!(
normalize_diff_args_impl(&args, exists_mock(&["README.md"])),
vec![
"--name-only".to_string(),
"--".to_string(),
"README.md".to_string()
]
);
}

/// Pure flags (no paths) → no injection.
#[test]
fn test_normalize_diff_args_no_injection_for_pure_flags() {
Expand Down Expand Up @@ -2130,6 +2167,36 @@ mod tests {
);
}

/// Filename with extension that exists on disk → inject `--`.
#[test]
fn test_normalize_diff_args_inject_for_filename_with_extension() {
let args = vec!["README.md".to_string()];
assert_eq!(
normalize_diff_args_impl(&args, exists_mock(&["README.md"])),
vec!["--".to_string(), "README.md".to_string()]
);
}

/// Multiple existing paths (including a filename-with-extension) → inject once before first path.
#[test]
fn test_normalize_diff_args_inject_for_multiple_existing_paths() {
let args = vec![
"README.md".to_string(),
"src/cmds/system/README.md".to_string(),
];
assert_eq!(
normalize_diff_args_impl(
&args,
exists_mock(&["README.md", "src/cmds/system/README.md"]),
),
vec![
"--".to_string(),
"README.md".to_string(),
"src/cmds/system/README.md".to_string()
]
);
}

#[test]
fn test_is_blob_show_arg() {
assert!(is_blob_show_arg("develop:modules/pairs_backtest.py"));
Expand Down
2 changes: 1 addition & 1 deletion src/cmds/system/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
## Specifics

- `read.rs` uses `core/filter` for language-aware code stripping (FilterLevel: none/minimal/aggressive)
- `grep_cmd.rs` reads `core/config` for `limits.grep_max_results` and `limits.grep_max_per_file`. Format-altering flags (`-c`, `-l`, `-L`, `-o`, `-Z`) bypass RTK filtering and run raw.
- `grep_cmd.rs` reads `core/config` for `limits.grep_max_results` and `limits.grep_max_per_file`. Flags: `--files-only`, `--count-by-file`, `--top-files <N>`, `--max-matches`, `--max-per-file`, `--max-line-chars`, `--full-lines`, `--all`, `--agent-safe`, `--json`. Env: `RTK_AGENT_SAFE=1` behaves like `--agent-safe` (grep only). Format-altering flags (`-c`, `-l`, `-L`, `-o`, `-Z`) bypass RTK filtering and run raw.
- `local_llm.rs` (`rtk smart`) uses `core/filter` for heuristic file summarization
- `format_cmd.rs` is a cross-ecosystem dispatcher: auto-detects and routes to `prettier_cmd` or `ruff_cmd` (black is handled inline, not as a separate module)

Expand Down
Loading