@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,6 @@
1
1
  {
2
2
  "name": "@complexthings/superpowers-agent",
3
- "version": "9.0.1",
3
+ "version": "9.0.2",
4
4
  "description": "Superpowers agent CLI — skills system for AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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
- ## Core workflows
43
+ ## Use them with discipline
41
44
 
42
- **Filter before reading** find the matches, then read only those.
43
- ```bash
44
- rg -l "password.*hash" src/auth/ --type ts | xargs rg "TODO"
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
- **Compose tools** — search, parse, dedup in one pass.
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
- In Claude Code the `Grep` and `Glob` tools are themselves built on ripgrepprefer them for in-context searches, and reach for the CLI tools when you need piping, transforms, or rewrites.
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())