@complexthings/superpowers-agent 9.0.1 → 9.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: leveraging-cli-tools
|
|
3
|
-
description: Use when performing code searches, JSON/YAML parsing, file finding, structural refactors, or data wrangling - ensures agents reach for high-performance CLI tools (rg, jq, fd, yq, ast-grep, gh, sd) over slower standard tools like grep/find/sed, cutting token cost and latency 5-50x. Check availability and offer to install a tool when a relevant task arises.
|
|
3
|
+
description: Use when performing code searches, JSON/YAML parsing, file finding, structural refactors, or data wrangling, or when a command floods context with verbose output like logs, CI, or test runs - ensures agents reach for high-performance CLI tools (rg, jq, fd, yq, ast-grep, gh, sd) over slower standard tools like grep/find/sed and use them with discipline, choosing the right output-reducing flags, composing pipelines so raw output never enters context, and using a bundled reducer for output that flags cannot shape, cutting token cost and latency 5-50x. Check availability and offer to install a tool when a relevant task arises.
|
|
4
|
+
compatibility: The bundled scripts/slim.py needs python3 (standard on macOS/Linux). The CLI tools install via the system package manager.
|
|
4
5
|
---
|
|
5
6
|
|
|
6
7
|
# Leveraging CLI Tools
|
|
@@ -9,6 +10,8 @@ description: Use when performing code searches, JSON/YAML parsing, file finding,
|
|
|
9
10
|
|
|
10
11
|
Reach for high-performance CLI tools over slower standard tools. The leverage is filtering and transforming with the right tool **before reading**, so tokens and time go to the answer, not the search. On a large tree `rg` is 10-50x faster than `grep` and returns far less noise; across a session that compounds into hours and tens of thousands of tokens saved.
|
|
11
12
|
|
|
13
|
+
Picking the faster tool is only half of it. The other half is using it with discipline — two levers that keep raw output out of your context: **ask for less** (the right flags and selectors), and **shrink what you can't shape** (a reducer for inherently verbose output). A fast tool fed a lazy command still floods your context.
|
|
14
|
+
|
|
12
15
|
## When a relevant task arises
|
|
13
16
|
|
|
14
17
|
1. Pick the right tool from the table below.
|
|
@@ -37,29 +40,56 @@ Check only the tools the current task needs — no upfront session-wide scan.
|
|
|
37
40
|
| 5 | `fzf` (`-f`) | manual fuzzy filtering | Non-interactive `-f`/`--filter` mode: fuzzy-rank a candidate list piped from `fd`/`rg`. | `brew install fzf` |
|
|
38
41
|
| 4 | `watchexec` | `while`+`sleep`, `entr` | Run a command on file change. Useful in build/test loops; non-interactive unlike most watchers. | `brew install watchexec` |
|
|
39
42
|
|
|
40
|
-
##
|
|
43
|
+
## Use them with discipline
|
|
41
44
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
45
|
+
Five habits separate a fast, low-noise result from a slow, context-flooding one. The token savings below are measured, not hypothetical.
|
|
46
|
+
|
|
47
|
+
**1 — Pick the tool that fits the data, not the one in muscle memory.** JSON → `jq`, YAML/TOML → `yq`, structural refactor → `ast-grep`, GitHub → `gh`. The reflex to resist is `grep`/`sed`/`awk` on structured data — they treat structure as flat text, so you pay in escaping bugs and noise the format-aware tool never produces.
|
|
48
|
+
|
|
49
|
+
**2 — Ask for less: use the flag or selector that returns the answer's _shape_.** The biggest single win is never pulling raw output into context. Match the request to the question:
|
|
50
|
+
|
|
51
|
+
| Question | Flag / selector | Returns |
|
|
52
|
+
|----------|-----------------|---------|
|
|
53
|
+
| Which files match? | `rg -l` | paths only, no lines |
|
|
54
|
+
| How many files? | `rg -l \| wc -l` | a file count, not a line count |
|
|
55
|
+
| How many hits? | `rg -c` (lines/file) / `rg --count-matches` (matches/file) | counts only |
|
|
56
|
+
| Just the matched bit? | `rg -o` | the substring, not the whole line |
|
|
57
|
+
| Enough to judge a hit? | `rg -A/-B/-C N`, `rg -m N` (cap per file) | bounded context |
|
|
58
|
+
| Only certain fields? | `jq -r '.a, .b'`, `yq`, `mlr --opprint cut -f` | projected values, not the whole doc |
|
|
59
|
+
| First N of a list? | pipe to `head`, or `fd --max-results N` | the slice you need |
|
|
46
60
|
|
|
47
|
-
**
|
|
61
|
+
For the **projection tools** (`jq`, `yq`, `dasel`, `miller`, `htmlq`) the selector *is* the filter — `jq` turning a 2,000-record array into one summed number is a ~100% reduction on its own. Shape their output at the selector; never post-filter it (that's what habit 4 is *not* for).
|
|
62
|
+
|
|
63
|
+
**3 — Compose so only the answer comes back.** Do the counting, dedup, and projection *in the pipeline*, not by reading raw output and reasoning over it. One pass keeps every intermediate result out of context:
|
|
48
64
|
```bash
|
|
49
65
|
rg -l '"error"' logs/ --type json | xargs jq -r 'select(.level=="error") | .code' | sort -u
|
|
50
66
|
```
|
|
51
|
-
|
|
52
|
-
**Structural rewrite, not regex** — refactor by AST so syntax can't trip you.
|
|
67
|
+
This is the lesson of programmatic tool calling (Amazon's PTC benchmarks: ~87-92% fewer tokens): when a task needs several tool calls plus processing, write **one** pipeline or script that does it all and returns only the result — the intermediate data never touches your context. So: filter before reading, and refactor by AST, not regex:
|
|
53
68
|
```bash
|
|
69
|
+
rg -l "password.*hash" src/auth/ --type ts | xargs rg "TODO"
|
|
54
70
|
sg --pattern 'console.log($$$A)' --rewrite 'logger.debug($$$A)' --lang ts
|
|
55
71
|
```
|
|
56
72
|
|
|
57
|
-
|
|
73
|
+
**4 — Shrink what you can't shape: the bundled reducer for verbose emitters.** Some output has no projection flag — logs, CI output (`gh run view --log`), test runs, stack traces, status dumps. Flags get you part way, but the residual stays repetitive, wide, and noisy. Pipe it through `scripts/slim.py` (relative to this skill; use its absolute path if you're working elsewhere) and ask only for the cuts the task needs:
|
|
74
|
+
```bash
|
|
75
|
+
gh run view --log | python3 scripts/slim.py --errors --uniq --max-line 200
|
|
76
|
+
cargo test 2>&1 | python3 scripts/slim.py --errors --dedup
|
|
77
|
+
rg -n TODO --no-ignore | python3 scripts/slim.py --group-dir
|
|
78
|
+
```
|
|
79
|
+
It does dedup (`--dedup`/`--uniq`, repeats collapse to `(xN)`), grouping (`--group-dir` → per-directory counts), truncation (`--head/--tail/--middle/--max-line`), and noise-stripping (`--comments/--errors/--grep`). On a real 1,236-line CI log, `rg` error-filtering alone cut 91% but left ~6,700 tokens; adding `slim` reached 97% — a third the residual. Run `python3 scripts/slim.py -h` for all modes. The cuts are **lossy and opt-in**: ask for what you need, know what you're discarding, and don't reach for it when a projection tool's own selector would do the job.
|
|
80
|
+
|
|
81
|
+
**5 — Read the output critically — it's a claim, not a fact.** A zero or suspiciously low result is the one to distrust: `rg` skips `.gitignore`d and hidden files by default, so a real match in `node_modules/`, `dist/`, or a dotfile is silently absent. Before concluding "none," re-run with `--no-ignore`, `-uu`, or `--hidden` — then decide whether those ignored hits (vendored deps, build output, generated mirrors) actually belong in the answer; surfacing them is the check, keeping them is a judgment call. Mind case (`-i`) and word boundaries (`-w`) so you neither miss `Subagent` nor over-match `tasks` — and when an identifier doubles as a common word (a `Task` tool vs. the word "task"), even `-w` isn't enough: glance at the context to confirm the hit is the symbol, not prose. When a count or list drives a decision, confirm a match means what you think before acting on it.
|
|
82
|
+
|
|
83
|
+
In Claude Code the `Grep` and `Glob` tools are themselves built on ripgrep — prefer them for in-context searches, and reach for the CLI tools when you need piping, transforms, rewrites, or output reduction.
|
|
58
84
|
|
|
59
85
|
## Red flags
|
|
60
86
|
|
|
61
87
|
- Parsing JSON/YAML with `awk`/`sed`/`grep` instead of `jq`/`yq`.
|
|
62
88
|
- Reading files before filtering them with `rg`.
|
|
89
|
+
- Piping a tool's full output into context to eyeball it, when `-l`/`-c`/`-o` or a `jq` projection would return just the answer.
|
|
90
|
+
- Letting a verbose command (test run, CI log, `git`/`docker` status dump) land raw in context when `slim` would cut it 90%+.
|
|
91
|
+
- Reaching for `slim` on a projection tool's output — shape it at the `jq`/`yq`/`mlr` selector instead.
|
|
92
|
+
- Concluding "no matches" from a default `rg` run without re-checking `--no-ignore`/`--hidden` — the hit may be sitting in an ignored directory.
|
|
63
93
|
- Hand-rolling `curl` against the GitHub API instead of `gh`.
|
|
64
94
|
- Regex codemods with `sed` where `ast-grep` is structurally safe.
|
|
65
95
|
|
|
@@ -70,6 +100,7 @@ In Claude Code the `Grep` and `Glob` tools are themselves built on ripgrep — p
|
|
|
70
100
|
| "grep works fine" | On a big tree it's 10-50x slower and floods context with noise `rg` would have filtered out. |
|
|
71
101
|
| "I don't know if they have jq" | One `command -v jq` answers it; install is seconds and pays back across the whole session. |
|
|
72
102
|
| "Not worth the setup" | One install = a speedup on every future task, not just this one. |
|
|
103
|
+
| "The test/CI output is just long, I'll scroll it" | A 1,200-line log is ~75k tokens of mostly noise; `slim --errors --uniq` makes it ~2k without losing the failures. |
|
|
73
104
|
| "User didn't ask for optimization" | Faster, lower-noise completion *is* better completion. |
|
|
74
105
|
|
|
75
106
|
## When NOT to use
|
|
@@ -77,3 +108,4 @@ In Claude Code the `Grep` and `Glob` tools are themselves built on ripgrep — p
|
|
|
77
108
|
- A tiny one-off (a handful of files, well under a megabyte) where the standard tool is already at hand.
|
|
78
109
|
- The user declined the install — note the cost once, then use the fallback.
|
|
79
110
|
- A teaching context where the standard tool is the point.
|
|
111
|
+
- Output you already shaped with a selector — don't add a `slim` pass for its own sake.
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""slim - reduce noisy command output to signal before it reaches your context.
|
|
3
|
+
|
|
4
|
+
A stdin->stdout filter for output you CAN'T shape with a tool's own flags:
|
|
5
|
+
logs, scan results, status dumps, stack traces, repeated lines. For tools that
|
|
6
|
+
already project output (jq/yq/dasel/miller/htmlq), use their selectors instead;
|
|
7
|
+
piping them through slim is redundant.
|
|
8
|
+
|
|
9
|
+
Pipe a verbose command through it and ask only for the reductions you need:
|
|
10
|
+
|
|
11
|
+
cargo test 2>&1 | slim --errors --dedup
|
|
12
|
+
rg -n TODO --no-ignore | slim --group-dir
|
|
13
|
+
gh run view --log | slim --uniq --max-line 200 --middle 40
|
|
14
|
+
kubectl get events | slim --dedup --stats
|
|
15
|
+
|
|
16
|
+
Reductions (compose freely):
|
|
17
|
+
--comments drop comment-only lines (# // ; -- *) and blank lines
|
|
18
|
+
--errors keep only lines matching error|warn|fail|fatal|panic|exception (-i)
|
|
19
|
+
--grep RE keep only lines matching regex RE (use --invert to drop them)
|
|
20
|
+
--invert invert --grep / --errors (drop matches instead of keeping)
|
|
21
|
+
--dedup collapse ALL repeated lines, first-seen order, annotate " (xN)"
|
|
22
|
+
--uniq collapse only CONSECUTIVE repeats, annotate " (xN)" (log-friendly)
|
|
23
|
+
--group-dir turn a list of file paths into per-directory counts
|
|
24
|
+
--max-line N truncate each line to N chars, append a horizontal ellipsis
|
|
25
|
+
--head N keep first N lines
|
|
26
|
+
--tail N keep last N lines
|
|
27
|
+
--middle N keep first N and last N lines, replace the gap with a marker
|
|
28
|
+
--stats print "lines A->B, bytes A->B (-P%)" to stderr (output unaffected)
|
|
29
|
+
|
|
30
|
+
Always-on, lossless: trailing whitespace is stripped and runs of blank lines
|
|
31
|
+
collapse to one. Everything lossy is opt-in. Order: filter -> transform ->
|
|
32
|
+
dedup -> group -> blank-collapse -> truncate. Reductions are LOSSY by design;
|
|
33
|
+
ask for what the task needs and know what you're discarding.
|
|
34
|
+
"""
|
|
35
|
+
import argparse
|
|
36
|
+
import re
|
|
37
|
+
import sys
|
|
38
|
+
|
|
39
|
+
COMMENT_RE = re.compile(r"^\s*(#|//|;|--|\*|/\*|\*/)")
|
|
40
|
+
ERROR_RE = re.compile(r"error|warn|fail|fatal|panic|exception|traceback", re.IGNORECASE)
|
|
41
|
+
ELLIPSIS = "…"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def annotate(line: str, n: int) -> str:
|
|
45
|
+
return line if n == 1 else f"{line} (x{n})"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def dedup_all(lines):
|
|
49
|
+
counts, order = {}, []
|
|
50
|
+
for ln in lines:
|
|
51
|
+
if ln not in counts:
|
|
52
|
+
counts[ln] = 0
|
|
53
|
+
order.append(ln)
|
|
54
|
+
counts[ln] += 1
|
|
55
|
+
return [annotate(ln, counts[ln]) for ln in order]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def dedup_adjacent(lines):
|
|
59
|
+
out, prev, n = [], None, 0
|
|
60
|
+
for ln in lines:
|
|
61
|
+
if ln == prev:
|
|
62
|
+
n += 1
|
|
63
|
+
else:
|
|
64
|
+
if prev is not None:
|
|
65
|
+
out.append(annotate(prev, n))
|
|
66
|
+
prev, n = ln, 1
|
|
67
|
+
if prev is not None:
|
|
68
|
+
out.append(annotate(prev, n))
|
|
69
|
+
return out
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def group_dir(lines):
|
|
73
|
+
counts, order = {}, []
|
|
74
|
+
for ln in lines:
|
|
75
|
+
token = ln.split(":", 1)[0].strip() # tolerate rg's path:line: prefix
|
|
76
|
+
d = token.rsplit("/", 1)[0] + "/" if "/" in token else "./"
|
|
77
|
+
if d not in counts:
|
|
78
|
+
counts[d] = 0
|
|
79
|
+
order.append(d)
|
|
80
|
+
counts[d] += 1
|
|
81
|
+
width = max((len(d) for d in order), default=0)
|
|
82
|
+
return [f"{d.ljust(width)} ({counts[d]} file{'s' if counts[d] != 1 else ''})" for d in order]
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def collapse_blanks(lines):
|
|
86
|
+
out, blank = [], False
|
|
87
|
+
for ln in lines:
|
|
88
|
+
if ln.strip() == "":
|
|
89
|
+
if not blank:
|
|
90
|
+
out.append("")
|
|
91
|
+
blank = True
|
|
92
|
+
else:
|
|
93
|
+
out.append(ln)
|
|
94
|
+
blank = False
|
|
95
|
+
while out and out[-1] == "":
|
|
96
|
+
out.pop()
|
|
97
|
+
return out
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def main() -> int:
|
|
101
|
+
p = argparse.ArgumentParser(add_help=True, description="Reduce noisy stdout to signal.")
|
|
102
|
+
p.add_argument("--comments", action="store_true")
|
|
103
|
+
p.add_argument("--errors", action="store_true")
|
|
104
|
+
p.add_argument("--grep", metavar="RE")
|
|
105
|
+
p.add_argument("--invert", action="store_true")
|
|
106
|
+
p.add_argument("--dedup", action="store_true")
|
|
107
|
+
p.add_argument("--uniq", action="store_true")
|
|
108
|
+
p.add_argument("--group-dir", action="store_true")
|
|
109
|
+
p.add_argument("--max-line", type=int, metavar="N")
|
|
110
|
+
p.add_argument("--head", type=int, metavar="N")
|
|
111
|
+
p.add_argument("--tail", type=int, metavar="N")
|
|
112
|
+
p.add_argument("--middle", type=int, metavar="N")
|
|
113
|
+
p.add_argument("--stats", action="store_true")
|
|
114
|
+
args = p.parse_args()
|
|
115
|
+
|
|
116
|
+
raw = sys.stdin.read()
|
|
117
|
+
in_lines = raw.splitlines()
|
|
118
|
+
in_bytes = len(raw.encode("utf-8", "replace"))
|
|
119
|
+
|
|
120
|
+
lines = [ln.rstrip() for ln in in_lines]
|
|
121
|
+
|
|
122
|
+
if args.comments:
|
|
123
|
+
lines = [ln for ln in lines if ln.strip() and not COMMENT_RE.match(ln)]
|
|
124
|
+
|
|
125
|
+
if args.errors or args.grep:
|
|
126
|
+
pat = re.compile(args.grep) if args.grep else ERROR_RE
|
|
127
|
+
keep = (lambda ln: not pat.search(ln)) if args.invert else (lambda ln: bool(pat.search(ln)))
|
|
128
|
+
lines = [ln for ln in lines if keep(ln)]
|
|
129
|
+
|
|
130
|
+
if args.max_line and args.max_line > 0:
|
|
131
|
+
lines = [ln if len(ln) <= args.max_line else ln[: args.max_line] + ELLIPSIS for ln in lines]
|
|
132
|
+
|
|
133
|
+
if args.dedup:
|
|
134
|
+
lines = dedup_all(lines)
|
|
135
|
+
elif args.uniq:
|
|
136
|
+
lines = dedup_adjacent(lines)
|
|
137
|
+
|
|
138
|
+
if args.group_dir:
|
|
139
|
+
lines = group_dir(lines)
|
|
140
|
+
|
|
141
|
+
lines = collapse_blanks(lines)
|
|
142
|
+
|
|
143
|
+
if args.middle and args.middle > 0 and len(lines) > 2 * args.middle:
|
|
144
|
+
omitted = len(lines) - 2 * args.middle
|
|
145
|
+
lines = lines[: args.middle] + [f"{ELLIPSIS} {omitted} lines omitted {ELLIPSIS}"] + lines[-args.middle :]
|
|
146
|
+
if args.head and args.head > 0:
|
|
147
|
+
lines = lines[: args.head]
|
|
148
|
+
if args.tail and args.tail > 0:
|
|
149
|
+
lines = lines[-args.tail :]
|
|
150
|
+
|
|
151
|
+
out = "\n".join(lines)
|
|
152
|
+
if out:
|
|
153
|
+
out += "\n"
|
|
154
|
+
sys.stdout.write(out)
|
|
155
|
+
|
|
156
|
+
if args.stats:
|
|
157
|
+
out_bytes = len(out.encode("utf-8", "replace"))
|
|
158
|
+
pct = 0 if in_bytes == 0 else round(100 * (1 - out_bytes / in_bytes))
|
|
159
|
+
change = f"-{pct}%" if pct >= 0 else f"+{-pct}% larger"
|
|
160
|
+
sys.stderr.write(
|
|
161
|
+
f"slim: lines {len(in_lines)}->{len(lines)}, bytes {in_bytes}->{out_bytes} ({change})\n"
|
|
162
|
+
)
|
|
163
|
+
return 0
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
if __name__ == "__main__":
|
|
167
|
+
sys.exit(main())
|