Regex/stdlib passes (dedupe, alias, ANSI/timestamp/blob strip, JSON minify, caveman word-drop) measured at 8.9-12.2x on real logs with o200k counts. Optional LLMLingua-2 wrap. grunts.py mines Claude Code transcripts for retyped prompts -> slash-command stubs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
82 lines
3.0 KiB
Python
82 lines
3.0 KiB
Python
"""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/<name>.md).
|
|
|
|
python3 grunts.py # report
|
|
python3 grunts.py --emit # also write ./grunts/<slug>.md command stubs
|
|
"""
|
|
import json
|
|
import re
|
|
import sys
|
|
from collections import Counter
|
|
from pathlib import Path
|
|
|
|
_SKIP = ("<system-reminder>", "<command-name>", "<local-command",
|
|
"[Request interrupted", "Caveat: The messages below", "<task-notification>")
|
|
|
|
|
|
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()
|