"""grunts — mine your Claude Code prompt history for your most-retyped grunts. Your typed prompts are the cheapest tokens in the context, so shorthand the MODEL decodes can never pay for its codebook (see README). But shorthand your KEYBOARD expands is free: this finds what you actually retype, so the long ones can become slash commands (~/.claude/commands/.md). python3 grunts.py # report python3 grunts.py --emit # also write ./grunts/.md command stubs """ import json import re import sys from collections import Counter from pathlib import Path _SKIP = ("", "", "") def user_messages(root=Path.home() / ".claude" / "projects"): for f in root.glob("*/*.jsonl"): for line in open(f, encoding="utf-8", errors="replace"): # ponytail: substring prefilter before json.loads keeps 1GB scans fast if '"type":"user"' not in line or '"isMeta":true' in line: continue if len(line) > 20000: # giant rows are pastes/images, never typed grunts continue try: c = json.loads(line)["message"]["content"] except (KeyError, TypeError, json.JSONDecodeError): continue if isinstance(c, list): if any(p.get("type") == "tool_result" for p in c if isinstance(p, dict)): continue c = " ".join(p.get("text", "") for p in c if isinstance(p, dict)) if not isinstance(c, str): continue t = c.strip() if t and len(t) <= 400 and not any(s in t for s in _SKIP): yield t def main(): msgs = list(user_messages()) norm = Counter(re.sub(r"\s+", " ", m.lower()) for m in msgs) grams = Counter() for m, n in norm.items(): words = m.split() for k in (4, 5, 6): for i in range(len(words) - k + 1): grams[" ".join(words[i:i + k])] += n print(f"{len(msgs):,} typed prompts scanned across {len(norm):,} distinct\n") top = [(m, c) for m, c in norm.most_common(30) if c >= 3] print("== most retyped whole prompts ==") for m, c in top[:20]: print(f"{c:5d}x {m[:90]}") print("\n== most retyped phrases (4-6 words, not already above) ==") shown = 0 for g, c in grams.most_common(300): if c < 5 or any(g in m for m, _ in top[:20]): continue print(f"{c:5d}x {g}") shown += 1 if shown == 15: break if "--emit" in sys.argv: out = Path("grunts") out.mkdir(exist_ok=True) for m, c in top: if len(m) < 25: # a short grunt is already faster to type than /slash continue slug = "-".join(re.findall(r"[a-z0-9]+", m)[:4]) (out / f"{slug}.md").write_text(m + "\n") print("\nwrote stubs to ./grunts/ — copy keepers to ~/.claude/commands/") if __name__ == "__main__": main()