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
34 changes: 34 additions & 0 deletions docs/processors/cargo.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Cargo Processor

**File:** `src/processors/cargo.py` | **Priority:** 22 | **Name:** `cargo`

Dedicated processor for Rust's cargo build system.

## Supported Commands

cargo build, cargo check, cargo doc, cargo update, cargo bench.

## Strategy

| Subcommand | Strategy |
|---|---|
| **build/check** | Collapse `Compiling X v1.0` lines into count. Group warnings by type (unused_variable, unused_import, dead_code, unused_mut, lifetime, borrow_checker). Show first N examples per type. Keep ALL errors with full span context (`-->`, `|`, `^^` markers). Keep `Finished` summary |
| **doc** | Collapse `Documenting X` and `Compiling X` lines. Keep doc warnings, errors, `Finished`, and `Generated` lines |
| **update** | Show all major version bumps explicitly (breaking changes). Collapse minor/patch bumps into count. Keep `Adding` and `Removing` lines |
| **bench** | Keep benchmark result lines (`bench: N ns/iter`). Strip `Compiling` and `Running` noise. Keep `test result:` summary |

## Exclusions

- `cargo test` is routed to `TestOutputProcessor`
- `cargo clippy` is routed to `LintOutputProcessor`

## Configuration

| Parameter | Default | Description |
|---|---|---|
| cargo_warning_example_count | 2 | Number of example warnings to show per category |
| cargo_warning_group_threshold | 3 | Minimum occurrences before warnings are grouped |

## Removed Noise

`Compiling X v1.0.0` lines, `Downloading X v1.0.0` lines, `Running` lines (bench), intermediate blank lines between warnings.
36 changes: 36 additions & 0 deletions docs/processors/go.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Go Processor

**File:** `src/processors/go.py` | **Priority:** 23 | **Name:** `go`

Dedicated processor for Go toolchain commands.

## Supported Commands

go build, go vet, go mod tidy, go mod download, go generate, go install.

## Strategy

| Subcommand | Strategy |
|---|---|
| **build/install** | Keep all `file.go:line:col: message` errors. For multi-package builds with many `# package` headers, truncate to first 3. Pass through unchanged if no errors (successful builds produce no output) |
| **vet** | Group warnings by type (printf, unreachable, shadow, unused, nil, loop). Show first N examples per type. Keep `# package` headers for context |
| **mod tidy/download** | Collapse `go: downloading X v1.0` lines into count. Keep `go: added/upgraded/downgraded/removed` lines (important dependency changes) |
| **generate** | Collapse `running` lines into count. Keep errors and generator output |

## Exclusions

- `go test` is routed to `TestOutputProcessor`
- `golangci-lint` is routed to `LintOutputProcessor`

## Configuration

Uses existing parameters:

| Parameter | Default | Description |
|---|---|---|
| lint_example_count | 2 | Examples per warning type (go vet) |
| lint_group_threshold | 3 | Minimum occurrences before grouping |

## Removed Noise

`go: downloading X` lines, `# package` headers (when redundant), `running` lines from go generate.
29 changes: 29 additions & 0 deletions docs/processors/jq_yq.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# JQ/YQ Processor

**File:** `src/processors/jq_yq.py` | **Priority:** 44 | **Name:** `jq_yq`

Compresses large JSON and YAML outputs from jq and yq.

## Supported Commands

jq, yq.

## Strategy

| Output Type | Strategy |
|---|---|
| **Small output** (< 50 lines) | Pass through unchanged |
| **jq JSON** | Parse as JSON, compress with `compress_json_value()` (truncate arrays > 5 items, summarize deeply nested objects). Re-serialize with indent |
| **jq streaming** (one JSON per line) | Detect repeated structure (same keys), show first 3 + count. Fallback to head/tail |
| **yq YAML** | Count top-level keys and list items. Collapse large arrays (> 3 items at same indent) to count. Add structure summary header |

## Configuration

| Parameter | Default | Description |
|---|---|---|
| jq_passthrough_threshold | 50 | Lines below which output passes through unchanged |

## Notes

- No runtime dependencies: JSON parsing uses stdlib `json` module, YAML uses heuristic analysis (no PyYAML dependency)
- Streaming jq output (one value per line) is detected and compressed separately from single-document output
29 changes: 29 additions & 0 deletions docs/processors/ssh.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# SSH Processor

**File:** `src/processors/ssh.py` | **Priority:** 43 | **Name:** `ssh`

Handles non-interactive SSH and SCP command output.

## Supported Commands

- `ssh host 'command'` or `ssh host "command"` (non-interactive SSH with quoted remote command)
- `ssh -o Option=value host 'command'` (with SSH options)
- `scp` (all forms — always non-interactive)

## Not Supported

- `ssh host` (interactive SSH — no remote command) remains excluded from compression

## Strategy

| Command | Strategy |
|---|---|
| **SSH remote** | Apply log-style compression: keep first 10 + last 20 lines, preserve error lines with context in the middle section |
| **SCP** | Collapse progress bar lines (containing `%` and transfer rates) to final status per file. Keep error lines (permission denied, connection refused, etc.) |

## How It Works

The SSH/SCP exclusion in `hook_pretool.py` was narrowed from a blanket `ssh|scp` exclusion to only exclude interactive SSH (no quoted command). This allows:
- `ssh host 'ls -la'` — compressed (non-interactive)
- `scp file host:/path` — compressed (always non-interactive)
- `ssh host` — still excluded (interactive)
4 changes: 2 additions & 2 deletions scripts/hook_pretool.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ def _load_compressible_patterns() -> list[str]:
EXCLUDED_PATTERNS = [
r"(?<!['\"])\|(?!['\"])", # unquoted pipe (complex pipelines)
r"^\s*(vi|vim|nano|emacs|code)\b",
r"^\s*(ssh|scp)\b",
r"^\s*ssh\s+(?:-\S+\s+)*\S+\s*$", # interactive ssh only (no remote command)
r"^\s*rsync\b.*\S+:\S+", # only exclude remote rsync (host:path)
r"(?:^|\s)token[-_]saver\s", # avoid wrapping token-saver CLI itself
r"wrap\.py",
Expand All @@ -106,7 +106,7 @@ def _load_compressible_patterns() -> list[str]:
r"<\(", # process substitution
r"^\s*sudo\b",
r"^\s*(vi|vim|nano|emacs|code)\b",
r"^\s*(ssh|scp)\b",
r"^\s*ssh\s+(?:-\S+\s+)*\S+\s*$", # interactive ssh only
r"^\s*rsync\b.*\S+:\S+", # only exclude remote rsync (host:path)
r"^\s*env\s+\S+=",
r"(?:^|\s)token[-_]saver\s",
Expand Down
3 changes: 3 additions & 0 deletions src/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@
"db_prune_days": 90,
"chars_per_token": 4,
"user_processors_dir": "",
"cargo_warning_example_count": 2,
"cargo_warning_group_threshold": 3,
"jq_passthrough_threshold": 50,
"debug": False,
}

Expand Down
13 changes: 13 additions & 0 deletions src/engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,12 @@ class CompressionEngine:

processors: list[Processor]
_generic: Processor
_by_name: dict[str, Processor]

def __init__(self) -> None:
self.processors = discover_processors()
self._generic = self.processors[-1] # Last = GenericProcessor (priority 999)
self._by_name = {p.name: p for p in self.processors}

def compress(self, command: str, output: str) -> tuple[str, str, bool]:
"""Compress output for a given command.
Expand All @@ -49,6 +51,17 @@ def compress(self, command: str, output: str) -> tuple[str, str, bool]:
if compressed is output or compressed == output:
return output, processor.name, False

# Chain to secondary processor if declared (max depth = 1)
if (
processor.chain_to
and processor.chain_to != processor.name
and processor.chain_to in self._by_name
):
secondary = self._by_name[processor.chain_to]
chained = secondary.process(command, compressed)
if chained is not compressed and chained != compressed:
compressed = chained

# If a specialized processor handled it, also run generic
# cleanup (ANSI strip, blank line collapse) but not truncation
if processor is not self._generic:
Expand Down
1 change: 1 addition & 0 deletions src/processors/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ class Processor(ABC):

priority: int = 50
hook_patterns: list[str] = []
chain_to: str | None = None

@abstractmethod
def can_handle(self, command: str) -> bool:
Expand Down
3 changes: 3 additions & 0 deletions src/processors/build_output.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ def can_handle(self, command: str) -> bool:
# Exclude cargo clippy (handled by LintOutputProcessor)
if re.search(r"\bcargo\s+clippy\b", command):
return False
# Exclude cargo build/check (handled by CargoProcessor)
if re.search(r"\bcargo\s+(build|check)\b", command):
return False
return bool(
re.search(
r"\b(npm\s+(run|install|ci|build|audit)|yarn\s+(run|install|build|add|audit)|pnpm\s+(run|install|build|add|audit)|"
Expand Down
Loading
Loading