Skip to content
Merged
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
66 changes: 61 additions & 5 deletions crates/flare-proxy/src/shortcircuit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,25 @@ fn extract_command_prefix(command: &str) -> String {
first_word.to_string()
}

/// True for shell redirection/control-operator tokens (`>`, `>>`, `<`,
/// `2>`, `2>&1`, `&>`, `|`, `||`, `&&`, `;`) that a naive whitespace split
/// leaves as their own token (e.g. `2>/dev/null`) or attached to a target
/// (e.g. `>out.txt`) -- neither is a filepath the command reads/writes by
/// name, so both must be excluded rather than mistaken for one.
fn is_shell_operator_token(part: &str) -> bool {
let trimmed = part.trim_start_matches(|c: char| c.is_ascii_digit());
matches!(trimmed.chars().next(), Some('>' | '<' | '&' | '|' | ';'))
}
Comment on lines +259 to +262

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- target implementation ---'
sed -n '230,330p' crates/flare-proxy/src/shortcircuit.rs

printf '%s\n' '--- filepath extraction and tests ---'
sed -n '330,370p' crates/flare-proxy/src/shortcircuit.rs
sed -n '700,750p' crates/flare-proxy/src/shortcircuit.rs

printf '%s\n' '--- related symbols and command parsing ---'
rg -n -C 3 'is_shell_operator_token|split_whitespace|filepath|filepaths|redirect|shortcircuit' crates/flare-proxy/src/shortcircuit.rs

Repository: getappz/agentflare

Length of output: 17627


🏁 Script executed:

#!/bin/bash
set -eu

python3 - <<'PY'
def is_shell_operator_token(part):
    trimmed = part.lstrip("0123456789")
    return bool(trimmed) and trimmed[0] in "><&|;"

def is_bare_redirect_operator(part):
    trimmed = part.lstrip("0123456789")
    return trimmed in (">", ">>", "<", "<<", "&>", "&>>")

def extract_filepaths(command):
    reading_commands = {"cat", "head", "tail", "less", "more", "bat", "type"}
    parts = command.split()
    if not parts:
        return []
    base_cmd = parts[0].replace("\\", "/").rsplit("/", 1)[-1].lower()

    if base_cmd in reading_commands:
        filepaths = []
        skip_next = False
        for part in parts[1:]:
            if skip_next:
                skip_next = False
                continue
            if part.startswith("-"):
                continue
            if is_shell_operator_token(part):
                if is_bare_redirect_operator(part):
                    skip_next = True
                continue
            filepaths.append(part)
        return filepaths

    if base_cmd == "grep":
        flags_with_args = {"-e", "-f", "-m", "-A", "-B", "-C"}
        pattern_via_flag = False
        positional = []
        skip_next = False
        for part in parts[1:]:
            if skip_next:
                skip_next = False
                continue
            if part.startswith("-"):
                if part in flags_with_args:
                    if part in {"-e", "-f"}:
                        pattern_via_flag = True
                    skip_next = True
                continue
            if is_shell_operator_token(part):
                if is_bare_redirect_operator(part):
                    skip_next = True
                continue
            positional.append(part)
        return positional if pattern_via_flag else (positional[1:] if len(positional) > 1 else [])

for command in (
    "cat src/main.rs>out.txt",
    "grep pattern src/main.rs>out.txt",
    "cat src/main.rs > out.txt",
    "grep pattern src/main.rs > out.txt",
):
    print(f"{command!r} -> {extract_filepaths(command)!r}")
PY

printf '%s\n' '--- operator/tokenizer implementations elsewhere ---'
rg -n -i -C 2 'shell.?token|tokeniz|split_whitespace|is_shell_operator|redirect' crates/flare-proxy

Repository: getappz/agentflare

Length of output: 5316


Handle redirects attached to preceding arguments. command.split_whitespace() keeps src/main.rs>out.txt as one token. Because is_shell_operator_token checks only the first non-digit character, the parser reports the combined token as a filepath. Tokenize shell operators before filepath classification, or use a shell-aware tokenizer. Add cases for cat src/main.rs>out.txt and grep pattern src/main.rs>out.txt.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/flare-proxy/src/shortcircuit.rs` around lines 259 - 262, Update
is_shell_operator_token and the surrounding command-token parsing to recognize
shell operators embedded within an argument, such as src/main.rs>out.txt, before
filepath classification. Ensure redirects attached to preceding arguments are
tokenized or otherwise handled as operators, and add coverage for both cat
src/main.rs>out.txt and grep pattern src/main.rs>out.txt.


/// True when `part` is a redirection operator with no target fused onto it
/// (e.g. `>`, `2>`, `>>`) -- the shell takes the *next* whitespace-separated
/// token as the redirect target, so that token must also be dropped rather
/// than kept as a filepath the command reads.
fn is_bare_redirect_operator(part: &str) -> bool {
let trimmed = part.trim_start_matches(|c: char| c.is_ascii_digit());
matches!(trimmed, ">" | ">>" | "<" | "<<" | "&>" | "&>>")
}

fn extract_filepaths(command: &str, _output: &str) -> String {
let listing_commands = [
"ls", "dir", "find", "tree", "pwd", "cd", "mkdir", "rmdir", "rm",
Expand All @@ -273,11 +292,24 @@ fn extract_filepaths(command: &str, _output: &str) -> String {
}

if reading_commands.contains(&base_cmd.as_str()) {
let filepaths: Vec<&str> = parts[1..]
.iter()
.filter(|p| !p.starts_with('-'))
.copied()
.collect();
let mut filepaths: Vec<&str> = Vec::new();
let mut skip_next = false;
for part in &parts[1..] {
if skip_next {
skip_next = false;
continue;
}
if part.starts_with('-') {
continue;
}
if is_shell_operator_token(part) {
if is_bare_redirect_operator(part) {
skip_next = true;
}
continue;
}
Comment on lines +305 to +310

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- available context tools ---'
command -v ctx_search || true
command -v ctx_callgraph || true
command -v ctx_compose || true
command -v ctx_read || true
command -v ctx_shell || true

printf '%s\n' '--- target file outline ---'
if command -v ast-grep >/dev/null 2>&1; then
  ast-grep outline crates/flare-proxy/src/shortcircuit.rs
else
  printf '%s\n' 'ast-grep unavailable'
fi

printf '%s\n' '--- relevant source ---'
sed -n '260,365p' crates/flare-proxy/src/shortcircuit.rs

printf '%s\n' '--- symbols and call sites ---'
rg -n -C 3 'is_shell_operator_token|is_bare_redirect_operator|positional|skip_next|shortcircuit' crates/flare-proxy/src/shortcircuit.rs crates/flare-proxy/src

Repository: getappz/agentflare

Length of output: 24533


🏁 Script executed:

#!/bin/bash
set -eu

python3 - <<'PY'
from pathlib import Path

p = Path("crates/flare-proxy/src/shortcircuit.rs")
lines = p.read_text().splitlines()

for start, end in [(1, 180), (260, 365)]:
    print(f"--- shortcircuit.rs:{start}-{end} ---")
    for i in range(start, min(end, len(lines)) + 1):
        print(f"{i:4}: {lines[i-1]}")
PY

printf '%s\n' '--- tests and fixtures related to path extraction ---'
rg -n -C 4 'filepath|file path|positional|redirect|pipeline|operator|&&|\|\||cat |grep ' crates/flare-proxy tests crates 2>/dev/null | head -n 500

Repository: getappz/agentflare

Length of output: 46911


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- available context tools ---'
command -v ctx_search || true
command -v ctx_callgraph || true
command -v ctx_compose || true
command -v ctx_read || true
command -v ctx_shell || true

printf '%s\n' '--- relevant source ---'
sed -n '260,365p' crates/flare-proxy/src/shortcircuit.rs

printf '%s\n' '--- related symbols and tests ---'
rg -n -C 4 'is_shell_operator_token|is_bare_redirect_operator|positional|skip_next|filepath|redirect|pipeline|operator|&&|\|\||cat |grep ' crates/flare-proxy/src crates/flare-proxy/tests tests 2>/dev/null | head -n 500

Repository: getappz/agentflare

Length of output: 39976


🏁 Script executed:

#!/bin/bash
set -eu

python3 - <<'PY'
def is_shell_operator_token(part):
    trimmed = part.lstrip("0123456789")
    return bool(trimmed) and trimmed[0] in "><&|;"

def is_bare_redirect_operator(part):
    trimmed = part.lstrip("0123456789")
    return trimmed in {">", ">>", "<", "<<", "&>", "&>>"}

def implemented_paths(command):
    parts = command.split()
    if not parts:
        return []

    base_cmd = parts[0].replace("\\", "/").split("/")[-1].lower()
    reading = {"cat", "head", "tail", "less", "more", "bat", "type"}

    if base_cmd in reading:
        paths = []
        skip_next = False
        for part in parts[1:]:
            if skip_next:
                skip_next = False
                continue
            if part.startswith("-"):
                continue
            if is_shell_operator_token(part):
                if is_bare_redirect_operator(part):
                    skip_next = True
                continue
            paths.append(part)
        return paths

    if base_cmd == "grep":
        flags_with_args = {"-e", "-f", "-m", "-A", "-B", "-C"}
        pattern_via_flag = False
        positional = []
        skip_next = False
        for part in parts[1:]:
            if skip_next:
                skip_next = False
                continue
            if part.startswith("-"):
                if part in flags_with_args:
                    if part in {"-e", "-f"}:
                        pattern_via_flag = True
                    skip_next = True
                continue
            if is_shell_operator_token(part):
                if is_bare_redirect_operator(part):
                    skip_next = True
                continue
            positional.append(part)
        return positional if pattern_via_flag else positional[1:] if len(positional) > 1 else []

cases = {
    "cat src/main.rs && echo done": ["src/main.rs", "echo", "done"],
    "grep pattern src/main.rs | sort": ["src/main.rs", "sort"],
    "cat src/main.rs > out.txt": ["src/main.rs"],
    "grep -e pattern src/main.rs | sort": ["src/main.rs", "sort"],
}
for command, expected in cases.items():
    actual = implemented_paths(command)
    print(f"{command!r} -> {actual!r}")
    assert actual == expected, (command, actual, expected)
PY

printf '%s\n' '--- command extraction implementation ---'
sed -n '175,215p' crates/flare-proxy/src/shortcircuit.rs

printf '%s\n' '--- existing filepath tests ---'
sed -n '690,755p' crates/flare-proxy/src/shortcircuit.rs

Repository: getappz/agentflare

Length of output: 4120


🏁 Script executed:

#!/bin/bash
set -eu

python3 - <<'PY'
def is_shell_operator_token(part):
    trimmed = part.lstrip("0123456789")
    return bool(trimmed) and trimmed[0] in "><&|;"

def is_bare_redirect_operator(part):
    trimmed = part.lstrip("0123456789")
    return trimmed in {">", ">>", "<", "<<", "&>", "&>>"}

def implemented_paths(command):
    parts = command.split()
    base_cmd = parts[0].replace("\\", "/").split("/")[-1].lower()
    if base_cmd in {"cat", "head", "tail", "less", "more", "bat", "type"}:
        paths, skip_next = [], False
        for part in parts[1:]:
            if skip_next:
                skip_next = False
                continue
            if part.startswith("-"):
                continue
            if is_shell_operator_token(part):
                if is_bare_redirect_operator(part):
                    skip_next = True
                continue
            paths.append(part)
        return paths
    if base_cmd == "grep":
        positional, skip_next, pattern_via_flag = [], False, False
        for part in parts[1:]:
            if skip_next:
                skip_next = False
                continue
            if part.startswith("-"):
                if part in {"-e", "-f", "-m", "-A", "-B", "-C"}:
                    pattern_via_flag |= part in {"-e", "-f"}
                    skip_next = True
                continue
            if is_shell_operator_token(part):
                if is_bare_redirect_operator(part):
                    skip_next = True
                continue
            positional.append(part)
        return positional if pattern_via_flag else positional[1:] if len(positional) > 1 else []

cases = {
    "cat src/main.rs && echo done": ["src/main.rs", "echo", "done"],
    "grep pattern src/main.rs | sort": ["src/main.rs", "sort"],
}
for command, expected in cases.items():
    actual = implemented_paths(command)
    print(f"{command!r} -> {actual!r}")
    assert actual == expected
PY

Repository: getappz/agentflare

Length of output: 283


Stop filepath extraction at command separators. In both loops, later command tokens become filepaths. For example, cat src/main.rs && echo done includes echo and done, and grep pattern src/main.rs | sort includes sort. Stop scanning at |, ||, &&, ;, and &, while preserving redirect handling. Add regression tests for both cases.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/flare-proxy/src/shortcircuit.rs` around lines 305 - 310, Update the
token-scanning loops in shortcircuit filepath extraction to terminate when
encountering command separators |, ||, &&, ;, or &, while retaining existing
bare-redirect handling before continuing. Add regression tests covering
separators followed by command tokens, including the cat/&& and grep/| examples,
and verify later command arguments are not extracted as filepaths.

filepaths.push(part);
}
if filepaths.is_empty() {
return "<filepaths>\n</filepaths>".into();
}
Expand All @@ -304,6 +336,12 @@ fn extract_filepaths(command: &str, _output: &str) -> String {
}
continue;
}
if is_shell_operator_token(part) {
if is_bare_redirect_operator(part) {
skip_next = true;
}
continue;
}
positional.push(part);
}

Expand Down Expand Up @@ -677,6 +715,24 @@ mod tests {
assert!(result.contains("tests/test.rs"));
}

#[test]
fn test_extract_filepaths_ignores_stderr_redirect() {
let result = extract_filepaths("tail -5 src/main.rs 2>/dev/null", "some content");
assert_eq!(result, "<filepaths>\nsrc/main.rs\n</filepaths>");
}

#[test]
fn test_extract_filepaths_ignores_stdout_redirect() {
let result = extract_filepaths("cat src/main.rs > out.txt", "some content");
assert_eq!(result, "<filepaths>\nsrc/main.rs\n</filepaths>");
}

#[test]
fn test_extract_filepaths_grep_ignores_redirect() {
let result = extract_filepaths("grep pattern src/main.rs 2>/dev/null", "matches");
assert_eq!(result, "<filepaths>\nsrc/main.rs\n</filepaths>");
}

#[test]
fn test_extract_filepaths_empty() {
let result = extract_filepaths("", "");
Expand Down
Loading