tidyout-cli 0.1.0__py3-none-any.whl
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.
- tidyout/__init__.py +1 -0
- tidyout/cli.py +31 -0
- tidyout/init_agent.py +28 -0
- tidyout/rules/cargo_build.yaml +19 -0
- tidyout/rules/docker_logs.yaml +19 -0
- tidyout/rules/docker_ps.yaml +5 -0
- tidyout/rules/find.yaml +23 -0
- tidyout/rules/git_log.yaml +15 -0
- tidyout/rules/git_status.yaml +31 -0
- tidyout/rules/jest.yaml +33 -0
- tidyout/rules/ls.yaml +6 -0
- tidyout/rules/make.yaml +9 -0
- tidyout/rules/npm_install.yaml +23 -0
- tidyout/rules/pip_install.yaml +35 -0
- tidyout/rules/pytest.yaml +41 -0
- tidyout/rules/tree.yaml +11 -0
- tidyout/runner.py +15 -0
- tidyout/stripper.py +297 -0
- tidyout_cli-0.1.0.dist-info/METADATA +105 -0
- tidyout_cli-0.1.0.dist-info/RECORD +22 -0
- tidyout_cli-0.1.0.dist-info/WHEEL +4 -0
- tidyout_cli-0.1.0.dist-info/entry_points.txt +2 -0
tidyout/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
tidyout/cli.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
|
|
3
|
+
from tidyout.runner import run_command
|
|
4
|
+
from tidyout.stripper import strip_output
|
|
5
|
+
from tidyout.init_agent import run_init
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def main():
|
|
9
|
+
args = sys.argv[1:]
|
|
10
|
+
|
|
11
|
+
if not args:
|
|
12
|
+
print("Usage: tidy <command> [args...]")
|
|
13
|
+
print(" tidy init")
|
|
14
|
+
sys.exit(1)
|
|
15
|
+
|
|
16
|
+
if args[0] == "init":
|
|
17
|
+
run_init()
|
|
18
|
+
return
|
|
19
|
+
|
|
20
|
+
raw_output, return_code = run_command(args)
|
|
21
|
+
compressed, original_lines, compressed_lines, saved_tokens, percent = strip_output(args, raw_output)
|
|
22
|
+
|
|
23
|
+
if compressed:
|
|
24
|
+
print(compressed, end="" if compressed.endswith("\n") else "\n")
|
|
25
|
+
|
|
26
|
+
print(
|
|
27
|
+
f"\u2702\ufe0f tidyout: {original_lines} lines \u2192 {compressed_lines} lines"
|
|
28
|
+
f" | saved ~{saved_tokens} tokens ({percent}% reduction)"
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
sys.exit(return_code)
|
tidyout/init_agent.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
TIDYOUT_BLOCK = """
|
|
4
|
+
## Token Optimization with tidyout
|
|
5
|
+
Always prefix the following commands with `tidy` when running in terminal:
|
|
6
|
+
pytest, npm test, jest, rspec, git status, git log, ls, find, tree,
|
|
7
|
+
pip install, npm install, docker logs, docker ps, make, cargo build
|
|
8
|
+
This reduces token usage by 60-97% on command outputs.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def run_init():
|
|
13
|
+
cwd = Path.cwd()
|
|
14
|
+
claude_md = cwd / "CLAUDE.md"
|
|
15
|
+
cursorrules = cwd / ".cursorrules"
|
|
16
|
+
|
|
17
|
+
if claude_md.exists():
|
|
18
|
+
with open(claude_md, "a") as f:
|
|
19
|
+
f.write(TIDYOUT_BLOCK)
|
|
20
|
+
print(f"Appended tidyout rules to {claude_md}")
|
|
21
|
+
elif cursorrules.exists():
|
|
22
|
+
with open(cursorrules, "a") as f:
|
|
23
|
+
f.write(TIDYOUT_BLOCK)
|
|
24
|
+
print(f"Appended tidyout rules to {cursorrules}")
|
|
25
|
+
else:
|
|
26
|
+
with open(claude_md, "w") as f:
|
|
27
|
+
f.write(TIDYOUT_BLOCK.lstrip())
|
|
28
|
+
print(f"Created {claude_md} with tidyout rules")
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
strip:
|
|
2
|
+
- pattern: " Downloading "
|
|
3
|
+
type: prefix
|
|
4
|
+
- pattern: " Downloaded "
|
|
5
|
+
type: prefix
|
|
6
|
+
- pattern: " Updating "
|
|
7
|
+
type: prefix
|
|
8
|
+
- pattern: " Locking "
|
|
9
|
+
type: prefix
|
|
10
|
+
|
|
11
|
+
keep:
|
|
12
|
+
- pattern: "^warning"
|
|
13
|
+
type: regex
|
|
14
|
+
- pattern: "^error"
|
|
15
|
+
type: regex
|
|
16
|
+
- pattern: " Finished"
|
|
17
|
+
type: prefix
|
|
18
|
+
- pattern: " Compiling "
|
|
19
|
+
type: prefix
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
strip:
|
|
2
|
+
- pattern: "health_check\\|"
|
|
3
|
+
type: regex
|
|
4
|
+
- pattern: "healthcheck"
|
|
5
|
+
type: contains
|
|
6
|
+
|
|
7
|
+
keep:
|
|
8
|
+
- pattern: "ERROR"
|
|
9
|
+
type: contains
|
|
10
|
+
- pattern: "WARN"
|
|
11
|
+
type: contains
|
|
12
|
+
- pattern: "FATAL"
|
|
13
|
+
type: contains
|
|
14
|
+
- pattern: "Exception"
|
|
15
|
+
type: contains
|
|
16
|
+
- pattern: "Traceback"
|
|
17
|
+
type: contains
|
|
18
|
+
- pattern: "panic:"
|
|
19
|
+
type: contains
|
tidyout/rules/find.yaml
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
strip:
|
|
2
|
+
- pattern: "/.git/"
|
|
3
|
+
type: contains
|
|
4
|
+
- pattern: "^\\.git"
|
|
5
|
+
type: regex
|
|
6
|
+
- pattern: "/node_modules/"
|
|
7
|
+
type: contains
|
|
8
|
+
- pattern: "node_modules/"
|
|
9
|
+
type: prefix
|
|
10
|
+
- pattern: "/__pycache__/"
|
|
11
|
+
type: contains
|
|
12
|
+
- pattern: "__pycache__/"
|
|
13
|
+
type: prefix
|
|
14
|
+
- pattern: "/\\.git$"
|
|
15
|
+
type: regex
|
|
16
|
+
- pattern: "node_modules$"
|
|
17
|
+
type: regex
|
|
18
|
+
- pattern: "__pycache__$"
|
|
19
|
+
type: regex
|
|
20
|
+
- pattern: "\\.pyc$"
|
|
21
|
+
type: regex
|
|
22
|
+
|
|
23
|
+
keep: []
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
strip:
|
|
2
|
+
- pattern: "Your branch is"
|
|
3
|
+
type: prefix
|
|
4
|
+
- pattern: "^ \\(use \""
|
|
5
|
+
type: regex
|
|
6
|
+
- pattern: "nothing to commit"
|
|
7
|
+
type: prefix
|
|
8
|
+
- pattern: "no changes added"
|
|
9
|
+
type: prefix
|
|
10
|
+
- pattern: "Changes not staged"
|
|
11
|
+
type: prefix
|
|
12
|
+
- pattern: "Changes to be committed"
|
|
13
|
+
type: prefix
|
|
14
|
+
- pattern: "Untracked files:"
|
|
15
|
+
type: prefix
|
|
16
|
+
- pattern: "^$"
|
|
17
|
+
type: regex
|
|
18
|
+
|
|
19
|
+
keep:
|
|
20
|
+
- pattern: "On branch "
|
|
21
|
+
type: prefix
|
|
22
|
+
- pattern: "modified:"
|
|
23
|
+
type: contains
|
|
24
|
+
- pattern: "deleted:"
|
|
25
|
+
type: contains
|
|
26
|
+
- pattern: "new file:"
|
|
27
|
+
type: contains
|
|
28
|
+
- pattern: "renamed:"
|
|
29
|
+
type: contains
|
|
30
|
+
- pattern: "both modified:"
|
|
31
|
+
type: contains
|
tidyout/rules/jest.yaml
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
strip:
|
|
2
|
+
- pattern: " ✓ "
|
|
3
|
+
type: prefix
|
|
4
|
+
- pattern: " ✔ "
|
|
5
|
+
type: prefix
|
|
6
|
+
- pattern: " ✅ "
|
|
7
|
+
type: prefix
|
|
8
|
+
- pattern: " PASS "
|
|
9
|
+
type: contains
|
|
10
|
+
- pattern: "^\\s*$"
|
|
11
|
+
type: regex
|
|
12
|
+
|
|
13
|
+
keep:
|
|
14
|
+
- pattern: " FAIL "
|
|
15
|
+
type: contains
|
|
16
|
+
- pattern: " ✕ "
|
|
17
|
+
type: prefix
|
|
18
|
+
- pattern: " ✗ "
|
|
19
|
+
type: prefix
|
|
20
|
+
- pattern: " ● "
|
|
21
|
+
type: prefix
|
|
22
|
+
- pattern: "Tests:"
|
|
23
|
+
type: contains
|
|
24
|
+
- pattern: "Test Suites:"
|
|
25
|
+
type: contains
|
|
26
|
+
- pattern: "Snapshots:"
|
|
27
|
+
type: contains
|
|
28
|
+
- pattern: "Time:"
|
|
29
|
+
type: contains
|
|
30
|
+
- pattern: "FAIL "
|
|
31
|
+
type: contains
|
|
32
|
+
- pattern: "expect("
|
|
33
|
+
type: contains
|
tidyout/rules/ls.yaml
ADDED
tidyout/rules/make.yaml
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
strip:
|
|
2
|
+
- pattern: "npm warn"
|
|
3
|
+
type: prefix
|
|
4
|
+
- pattern: "npm notice"
|
|
5
|
+
type: prefix
|
|
6
|
+
- pattern: "^\\s*$"
|
|
7
|
+
type: regex
|
|
8
|
+
|
|
9
|
+
keep:
|
|
10
|
+
- pattern: "added "
|
|
11
|
+
type: contains
|
|
12
|
+
- pattern: "removed "
|
|
13
|
+
type: contains
|
|
14
|
+
- pattern: "changed "
|
|
15
|
+
type: contains
|
|
16
|
+
- pattern: "audited "
|
|
17
|
+
type: contains
|
|
18
|
+
- pattern: "vulnerabilit"
|
|
19
|
+
type: contains
|
|
20
|
+
- pattern: "found 0 vulnerabilities"
|
|
21
|
+
type: contains
|
|
22
|
+
- pattern: "npm error"
|
|
23
|
+
type: prefix
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
strip:
|
|
2
|
+
- pattern: "Collecting "
|
|
3
|
+
type: prefix
|
|
4
|
+
- pattern: " Downloading "
|
|
5
|
+
type: prefix
|
|
6
|
+
- pattern: "Requirement already satisfied:"
|
|
7
|
+
type: prefix
|
|
8
|
+
- pattern: " Already satisfied:"
|
|
9
|
+
type: prefix
|
|
10
|
+
- pattern: "Building wheels"
|
|
11
|
+
type: prefix
|
|
12
|
+
- pattern: " Building wheel"
|
|
13
|
+
type: prefix
|
|
14
|
+
- pattern: " Created wheel"
|
|
15
|
+
type: prefix
|
|
16
|
+
- pattern: "Stored in directory"
|
|
17
|
+
type: prefix
|
|
18
|
+
- pattern: "Installing collected packages:"
|
|
19
|
+
type: prefix
|
|
20
|
+
- pattern: "WARNING: You are using pip version"
|
|
21
|
+
type: contains
|
|
22
|
+
- pattern: "You should consider upgrading"
|
|
23
|
+
type: contains
|
|
24
|
+
- pattern: "━"
|
|
25
|
+
type: contains
|
|
26
|
+
- pattern: "^\\s*$"
|
|
27
|
+
type: regex
|
|
28
|
+
|
|
29
|
+
keep:
|
|
30
|
+
- pattern: "Successfully installed"
|
|
31
|
+
type: prefix
|
|
32
|
+
- pattern: "ERROR"
|
|
33
|
+
type: prefix
|
|
34
|
+
- pattern: "error:"
|
|
35
|
+
type: prefix
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
strip:
|
|
2
|
+
- pattern: "collecting "
|
|
3
|
+
type: prefix
|
|
4
|
+
- pattern: "platform "
|
|
5
|
+
type: prefix
|
|
6
|
+
- pattern: "rootdir:"
|
|
7
|
+
type: prefix
|
|
8
|
+
- pattern: "plugins:"
|
|
9
|
+
type: prefix
|
|
10
|
+
- pattern: "cacheprovider"
|
|
11
|
+
type: prefix
|
|
12
|
+
- pattern: " PASSED$"
|
|
13
|
+
type: regex
|
|
14
|
+
- pattern: " PASSED "
|
|
15
|
+
type: contains
|
|
16
|
+
- pattern: "^={10,}$"
|
|
17
|
+
type: regex
|
|
18
|
+
- pattern: "^-{10,}$"
|
|
19
|
+
type: regex
|
|
20
|
+
|
|
21
|
+
keep:
|
|
22
|
+
- pattern: "FAILED"
|
|
23
|
+
type: contains
|
|
24
|
+
- pattern: "ERROR"
|
|
25
|
+
type: contains
|
|
26
|
+
- pattern: " passed"
|
|
27
|
+
type: contains
|
|
28
|
+
- pattern: " failed"
|
|
29
|
+
type: contains
|
|
30
|
+
- pattern: " error"
|
|
31
|
+
type: contains
|
|
32
|
+
- pattern: "warning"
|
|
33
|
+
type: contains
|
|
34
|
+
- pattern: "short test summary"
|
|
35
|
+
type: contains
|
|
36
|
+
- pattern: "^FAILED "
|
|
37
|
+
type: regex
|
|
38
|
+
- pattern: "^E "
|
|
39
|
+
type: regex
|
|
40
|
+
- pattern: "^>"
|
|
41
|
+
type: regex
|
tidyout/rules/tree.yaml
ADDED
tidyout/runner.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def run_command(args):
|
|
5
|
+
"""Execute a command, merging stdout and stderr. Returns (output, return_code)."""
|
|
6
|
+
try:
|
|
7
|
+
result = subprocess.run(
|
|
8
|
+
args,
|
|
9
|
+
stdout=subprocess.PIPE,
|
|
10
|
+
stderr=subprocess.STDOUT,
|
|
11
|
+
text=True,
|
|
12
|
+
)
|
|
13
|
+
return result.stdout, result.returncode
|
|
14
|
+
except FileNotFoundError:
|
|
15
|
+
return f"tidy: command not found: {args[0]}\n", 127
|
tidyout/stripper.py
ADDED
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
import yaml
|
|
5
|
+
|
|
6
|
+
RULES_DIR = Path(__file__).parent / "rules"
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
# ---------------------------------------------------------------------------
|
|
10
|
+
# Rule loading & matching
|
|
11
|
+
# ---------------------------------------------------------------------------
|
|
12
|
+
|
|
13
|
+
def get_rule_key(args):
|
|
14
|
+
"""Return the rule key for a command. Tries two-word key first."""
|
|
15
|
+
if len(args) >= 2:
|
|
16
|
+
two_word = f"{args[0]}_{args[1]}"
|
|
17
|
+
if (RULES_DIR / f"{two_word}.yaml").exists():
|
|
18
|
+
return two_word
|
|
19
|
+
return args[0]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def load_rule(rule_key):
|
|
23
|
+
"""Load a YAML rule file. Returns None if no rule exists for this key."""
|
|
24
|
+
rule_path = RULES_DIR / f"{rule_key}.yaml"
|
|
25
|
+
if not rule_path.exists():
|
|
26
|
+
return None
|
|
27
|
+
with open(rule_path) as f:
|
|
28
|
+
return yaml.safe_load(f)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _matches(line, entry):
|
|
32
|
+
pattern = entry["pattern"]
|
|
33
|
+
ptype = entry.get("type", "contains")
|
|
34
|
+
if ptype == "regex":
|
|
35
|
+
return bool(re.search(pattern, line))
|
|
36
|
+
if ptype == "prefix":
|
|
37
|
+
return line.startswith(pattern)
|
|
38
|
+
if ptype == "exact":
|
|
39
|
+
return line == pattern
|
|
40
|
+
# default: contains
|
|
41
|
+
return pattern in line
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def apply_rule(lines, rule):
|
|
45
|
+
"""Filter lines using strip/keep rules. keep always wins over strip."""
|
|
46
|
+
strip_patterns = rule.get("strip") or []
|
|
47
|
+
keep_patterns = rule.get("keep") or []
|
|
48
|
+
|
|
49
|
+
result = []
|
|
50
|
+
for line in lines:
|
|
51
|
+
if any(_matches(line, p) for p in keep_patterns):
|
|
52
|
+
result.append(line)
|
|
53
|
+
continue
|
|
54
|
+
if any(_matches(line, p) for p in strip_patterns):
|
|
55
|
+
continue
|
|
56
|
+
result.append(line)
|
|
57
|
+
return result
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# ---------------------------------------------------------------------------
|
|
61
|
+
# Transform hooks (reformatting beyond simple line filtering)
|
|
62
|
+
# ---------------------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
def _month_num(month):
|
|
65
|
+
return {
|
|
66
|
+
"Jan": "01", "Feb": "02", "Mar": "03", "Apr": "04",
|
|
67
|
+
"May": "05", "Jun": "06", "Jul": "07", "Aug": "08",
|
|
68
|
+
"Sep": "09", "Oct": "10", "Nov": "11", "Dec": "12",
|
|
69
|
+
}.get(month, "??")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def transform_ls(lines):
|
|
73
|
+
dirs, files = [], []
|
|
74
|
+
for line in lines:
|
|
75
|
+
if not line.strip():
|
|
76
|
+
continue
|
|
77
|
+
parts = line.split()
|
|
78
|
+
# Long format: permissions links owner group size ... name
|
|
79
|
+
if len(parts) >= 9 and line[0] in ("-", "d", "l"):
|
|
80
|
+
permissions = parts[0]
|
|
81
|
+
size = parts[4]
|
|
82
|
+
name = " ".join(parts[8:])
|
|
83
|
+
if name in (".", ".."):
|
|
84
|
+
continue
|
|
85
|
+
if permissions.startswith("d"):
|
|
86
|
+
dirs.append(f"DIR:{name}")
|
|
87
|
+
elif permissions.startswith("l"):
|
|
88
|
+
link_name = name.split(" -> ")[0]
|
|
89
|
+
files.append(f"L:{link_name}")
|
|
90
|
+
else:
|
|
91
|
+
files.append(f"F:{name}:{size}")
|
|
92
|
+
else:
|
|
93
|
+
files.append(line)
|
|
94
|
+
return dirs + files
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def transform_git_status(lines):
|
|
98
|
+
result = []
|
|
99
|
+
for line in lines:
|
|
100
|
+
stripped = line.strip()
|
|
101
|
+
if line.startswith("On branch "):
|
|
102
|
+
result.append(f"branch: {line[len('On branch '):]}")
|
|
103
|
+
continue
|
|
104
|
+
for prefix in ("modified:", "deleted:", "new file:", "renamed:", "both modified:"):
|
|
105
|
+
if stripped.startswith(prefix):
|
|
106
|
+
result.append(f"{prefix} {stripped[len(prefix):].strip()}")
|
|
107
|
+
break
|
|
108
|
+
else:
|
|
109
|
+
result.append(line)
|
|
110
|
+
return result
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def transform_git_log(lines):
|
|
114
|
+
result = []
|
|
115
|
+
cur_hash = cur_date = cur_msg = None
|
|
116
|
+
|
|
117
|
+
for line in lines:
|
|
118
|
+
if line.startswith("commit "):
|
|
119
|
+
if cur_hash and cur_msg:
|
|
120
|
+
result.append(f"{cur_hash} {cur_date} {cur_msg}")
|
|
121
|
+
cur_hash = line.split()[1][:7]
|
|
122
|
+
cur_date = cur_msg = None
|
|
123
|
+
elif line.startswith("Date:"):
|
|
124
|
+
date_str = line[5:].strip()
|
|
125
|
+
parts = date_str.split()
|
|
126
|
+
if len(parts) >= 5:
|
|
127
|
+
cur_date = f"{parts[4]}-{_month_num(parts[1])}-{parts[2].zfill(2)}"
|
|
128
|
+
else:
|
|
129
|
+
cur_date = date_str
|
|
130
|
+
elif line.startswith(" ") and cur_hash and cur_msg is None:
|
|
131
|
+
cur_msg = line.strip()
|
|
132
|
+
|
|
133
|
+
if cur_hash and cur_msg:
|
|
134
|
+
result.append(f"{cur_hash} {cur_date} {cur_msg}")
|
|
135
|
+
return result
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def transform_docker_ps(lines):
|
|
139
|
+
result = []
|
|
140
|
+
col_starts = {}
|
|
141
|
+
col_order = ["CONTAINER ID", "IMAGE", "COMMAND", "CREATED", "STATUS", "PORTS", "NAMES"]
|
|
142
|
+
|
|
143
|
+
for line in lines:
|
|
144
|
+
if not line.strip():
|
|
145
|
+
continue
|
|
146
|
+
if "CONTAINER ID" in line:
|
|
147
|
+
for col in col_order:
|
|
148
|
+
idx = line.find(col)
|
|
149
|
+
if idx >= 0:
|
|
150
|
+
col_starts[col] = idx
|
|
151
|
+
continue
|
|
152
|
+
|
|
153
|
+
if not col_starts:
|
|
154
|
+
result.append(line)
|
|
155
|
+
continue
|
|
156
|
+
|
|
157
|
+
def get_field(col):
|
|
158
|
+
start = col_starts.get(col)
|
|
159
|
+
if start is None:
|
|
160
|
+
return ""
|
|
161
|
+
col_idx = col_order.index(col)
|
|
162
|
+
for next_col in col_order[col_idx + 1:]:
|
|
163
|
+
if next_col in col_starts:
|
|
164
|
+
return line[start:col_starts[next_col]].strip()
|
|
165
|
+
return line[start:].strip()
|
|
166
|
+
|
|
167
|
+
name = get_field("NAMES")
|
|
168
|
+
status = get_field("STATUS")
|
|
169
|
+
ports = get_field("PORTS")
|
|
170
|
+
if not name:
|
|
171
|
+
continue
|
|
172
|
+
entry = f"{name}:{status}:{ports}" if ports else f"{name}:{status}"
|
|
173
|
+
result.append(entry)
|
|
174
|
+
|
|
175
|
+
return result
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def transform_find(lines):
|
|
179
|
+
result = []
|
|
180
|
+
for line in lines:
|
|
181
|
+
line = line.strip()
|
|
182
|
+
if line.startswith("./"):
|
|
183
|
+
line = line[2:]
|
|
184
|
+
if line and line != ".":
|
|
185
|
+
result.append(line)
|
|
186
|
+
return result
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _tree_depth(line):
|
|
190
|
+
i = depth = 0
|
|
191
|
+
while i < len(line):
|
|
192
|
+
chunk = line[i:i+4]
|
|
193
|
+
if chunk in ("│ ", " "):
|
|
194
|
+
depth += 1
|
|
195
|
+
i += 4
|
|
196
|
+
elif chunk in ("├── ", "└── "):
|
|
197
|
+
depth += 1
|
|
198
|
+
break
|
|
199
|
+
else:
|
|
200
|
+
break
|
|
201
|
+
return depth
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def transform_tree(lines):
|
|
205
|
+
result = []
|
|
206
|
+
for line in lines:
|
|
207
|
+
if not line.rstrip():
|
|
208
|
+
result.append(line)
|
|
209
|
+
continue
|
|
210
|
+
if _tree_depth(line) <= 3:
|
|
211
|
+
result.append(line)
|
|
212
|
+
return result
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def transform_docker_logs(lines):
|
|
216
|
+
# Keep last 50 lines after filtering
|
|
217
|
+
if len(lines) > 50:
|
|
218
|
+
return lines[-50:]
|
|
219
|
+
return lines
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def transform_cargo_build(lines):
|
|
223
|
+
# Keep only the last "Compiling" line (the project crate itself)
|
|
224
|
+
compiling = [l for l in lines if l.startswith(" Compiling ")]
|
|
225
|
+
others = [l for l in lines if not l.startswith(" Compiling ")]
|
|
226
|
+
if compiling:
|
|
227
|
+
# Insert the last compiling line back in its original position
|
|
228
|
+
last = compiling[-1]
|
|
229
|
+
original_idx = lines.index(last)
|
|
230
|
+
result = []
|
|
231
|
+
inserted = False
|
|
232
|
+
pos = 0
|
|
233
|
+
for line in others:
|
|
234
|
+
orig_pos = lines.index(line) if line in lines else 9999
|
|
235
|
+
if not inserted and original_idx < orig_pos:
|
|
236
|
+
result.append(last)
|
|
237
|
+
inserted = True
|
|
238
|
+
result.append(line)
|
|
239
|
+
if not inserted:
|
|
240
|
+
result.append(last)
|
|
241
|
+
return result
|
|
242
|
+
return others
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
TRANSFORMS = {
|
|
246
|
+
"ls": transform_ls,
|
|
247
|
+
"git_status": transform_git_status,
|
|
248
|
+
"git_log": transform_git_log,
|
|
249
|
+
"docker_ps": transform_docker_ps,
|
|
250
|
+
"find": transform_find,
|
|
251
|
+
"tree": transform_tree,
|
|
252
|
+
"docker_logs": transform_docker_logs,
|
|
253
|
+
"cargo_build": transform_cargo_build,
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
# ---------------------------------------------------------------------------
|
|
258
|
+
# Savings calculation
|
|
259
|
+
# ---------------------------------------------------------------------------
|
|
260
|
+
|
|
261
|
+
def calculate_savings(original, compressed):
|
|
262
|
+
orig_chars = len(original)
|
|
263
|
+
comp_chars = len(compressed)
|
|
264
|
+
saved_tokens = max(0, (orig_chars - comp_chars) // 4)
|
|
265
|
+
percent = max(0, int((1 - comp_chars / orig_chars) * 100)) if orig_chars else 0
|
|
266
|
+
return saved_tokens, percent
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
# ---------------------------------------------------------------------------
|
|
270
|
+
# Public entry point
|
|
271
|
+
# ---------------------------------------------------------------------------
|
|
272
|
+
|
|
273
|
+
def strip_output(args, raw_output):
|
|
274
|
+
"""
|
|
275
|
+
Apply rules and transforms to raw command output.
|
|
276
|
+
|
|
277
|
+
Returns:
|
|
278
|
+
(compressed_text, original_line_count, compressed_line_count, saved_tokens, percent)
|
|
279
|
+
"""
|
|
280
|
+
rule_key = get_rule_key(args)
|
|
281
|
+
rule = load_rule(rule_key)
|
|
282
|
+
|
|
283
|
+
lines = raw_output.splitlines()
|
|
284
|
+
original_line_count = len(lines)
|
|
285
|
+
|
|
286
|
+
filtered = apply_rule(lines, rule) if rule is not None else list(lines)
|
|
287
|
+
|
|
288
|
+
if rule_key in TRANSFORMS:
|
|
289
|
+
filtered = TRANSFORMS[rule_key](filtered)
|
|
290
|
+
|
|
291
|
+
compressed = "\n".join(filtered)
|
|
292
|
+
if raw_output.endswith("\n"):
|
|
293
|
+
compressed += "\n"
|
|
294
|
+
|
|
295
|
+
saved_tokens, percent = calculate_savings(raw_output, compressed)
|
|
296
|
+
|
|
297
|
+
return compressed, original_line_count, len(filtered), saved_tokens, percent
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tidyout-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: CLI wrapper that compresses command output for AI agents, reducing token usage by 60-97%
|
|
5
|
+
License: MIT
|
|
6
|
+
Keywords: ai,cli,compression,developer-tools,tokens
|
|
7
|
+
Classifier: Development Status :: 3 - Alpha
|
|
8
|
+
Classifier: Environment :: Console
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
17
|
+
Classifier: Topic :: Utilities
|
|
18
|
+
Requires-Python: >=3.9
|
|
19
|
+
Requires-Dist: pyyaml>=6.0
|
|
20
|
+
Provides-Extra: dev
|
|
21
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# tidyout
|
|
25
|
+
|
|
26
|
+
A CLI wrapper that sits between you (or your AI agent) and the terminal. Prefix any command with `tidy` and tidyout executes it, strips the noise, and returns a compressed version with a token savings summary.
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
✂️ tidyout: 312 lines → 18 lines | saved ~2376 tokens (94% reduction)
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Built for AI coding sessions (Claude Code, Cursor, Copilot) where terminal output burns tokens fast.
|
|
33
|
+
|
|
34
|
+
## Install
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install tidyout-cli
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Usage
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
tidy pytest tests/
|
|
44
|
+
tidy git status
|
|
45
|
+
tidy git log
|
|
46
|
+
tidy ls -la
|
|
47
|
+
tidy find . -name "*.py"
|
|
48
|
+
tidy docker logs mycontainer
|
|
49
|
+
tidy npm install
|
|
50
|
+
tidy cargo build
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Every run ends with a summary line:
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
✂️ tidyout: {original} lines → {compressed} lines | saved ~{tokens} tokens ({percent}% reduction)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Commands without a matching rule pass through unchanged and still show the summary (0% reduction).
|
|
60
|
+
|
|
61
|
+
## Supported commands
|
|
62
|
+
|
|
63
|
+
| Command | What gets stripped | Typical savings |
|
|
64
|
+
|---|---|---|
|
|
65
|
+
| `pytest` | PASSED lines, platform info, separators | ~80% |
|
|
66
|
+
| `git status` | hints, up-to-date messages, blank lines | ~60% |
|
|
67
|
+
| `git log` | Author email, full hashes, blank lines | ~50% |
|
|
68
|
+
| `ls` | permissions, owner, group, timestamp — reformatted to `DIR:name` / `F:name:size` | ~70% |
|
|
69
|
+
| `find` | `.git/`, `node_modules/`, `__pycache__/`, `./` prefix | ~40% |
|
|
70
|
+
| `tree` | entries beyond depth 3, `node_modules`, `.git`, `__pycache__` | ~50% |
|
|
71
|
+
| `pip install` | progress bars, Collecting/Downloading, already-satisfied | ~90% |
|
|
72
|
+
| `npm install` | warn/notice lines, blank lines | ~85% |
|
|
73
|
+
| `docker logs` | health check noise, keeps last 50 lines | ~70% |
|
|
74
|
+
| `docker ps` | header, full IDs, padding — reformatted to `name:status:ports` | ~60% |
|
|
75
|
+
| `jest` | passing test names, keeps failures and summary | ~80% |
|
|
76
|
+
| `make` | Entering/Leaving directory noise | ~40% |
|
|
77
|
+
| `cargo build` | dependency Compiling lines, Downloading | ~75% |
|
|
78
|
+
|
|
79
|
+
## `tidy init` — inject rules into your AI agent config
|
|
80
|
+
|
|
81
|
+
Running `tidy init` in your project directory automatically tells your AI agent to always use `tidy`:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
tidy init
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
It detects which config file to update:
|
|
88
|
+
|
|
89
|
+
- `CLAUDE.md` exists → appends tidyout usage rules
|
|
90
|
+
- `.cursorrules` exists → appends tidyout usage rules
|
|
91
|
+
- Neither exists → creates `CLAUDE.md` with the rules
|
|
92
|
+
|
|
93
|
+
Injected block:
|
|
94
|
+
|
|
95
|
+
```markdown
|
|
96
|
+
## Token Optimization with tidyout
|
|
97
|
+
Always prefix the following commands with `tidy` when running in terminal:
|
|
98
|
+
pytest, npm test, jest, rspec, git status, git log, ls, find, tree,
|
|
99
|
+
pip install, npm install, docker logs, docker ps, make, cargo build
|
|
100
|
+
This reduces token usage by 60-97% on command outputs.
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Contributing a new rule
|
|
104
|
+
|
|
105
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) — it's just one YAML file.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
tidyout/__init__.py,sha256=kUR5RAFc7HCeiqdlX36dZOHkUI5wI6V_43RpEcD8b-0,22
|
|
2
|
+
tidyout/cli.py,sha256=k8v8PK4N5INOu1pdpCbAdSQnoyzMxqxf9Tc0weVNpzk,800
|
|
3
|
+
tidyout/init_agent.py,sha256=vE2nASWFFVcA0gFhVaW7NYkngn_hw5annsDX2aFdN5U,939
|
|
4
|
+
tidyout/runner.py,sha256=ttVTFFs-xb9o_yh-hBtgnTCRJO0osEXqkdmytlIg6c8,435
|
|
5
|
+
tidyout/stripper.py,sha256=mIhKLHCEV5kUVCqSrorb8ObgV8nN8F0JH7mIdWSANDE,8876
|
|
6
|
+
tidyout/rules/cargo_build.yaml,sha256=HeNIVxVDE-InR5B9lDwC6s8gac8WA7k581h638maohE,370
|
|
7
|
+
tidyout/rules/docker_logs.yaml,sha256=ZIO-RBNKO-NcBNsu1eng1UZk5rkexjCTKac1c5gASk0,355
|
|
8
|
+
tidyout/rules/docker_ps.yaml,sha256=LwR5gG_-MQhkQ5I7HrVJKdOAfpJesFra1rndeFwZnqM,51
|
|
9
|
+
tidyout/rules/find.yaml,sha256=szijnx8TeuZBAAS5WvRjahmfnOueRlmOtbm4XpQChrI,453
|
|
10
|
+
tidyout/rules/git_log.yaml,sha256=ZYkZgFqNOxXZxmhc53t1xioFykEtoxlkc16Ds5-Pb7g,244
|
|
11
|
+
tidyout/rules/git_status.yaml,sha256=KSdC1oL-JCpQ0vyt9VV2z0adW5R7QwWW7Up2IghV59w,660
|
|
12
|
+
tidyout/rules/jest.yaml,sha256=37gKOVJcA3-LYZtwQqCPcyVCIltT_-9GATbxnjdroMw,623
|
|
13
|
+
tidyout/rules/ls.yaml,sha256=gT6jJp5M-yEG_2sKeMqZyWCfdXXtmemsef83r2Ti9Po,89
|
|
14
|
+
tidyout/rules/make.yaml,sha256=08KUROTBUFf1Qrd_1eW-V5rsM4XAy-FlbaBIsSgDlbQ,169
|
|
15
|
+
tidyout/rules/npm_install.yaml,sha256=3WDDo23r3cMKTZpvv6SKy6NVG4pYHKDczxhfbGz7kyM,453
|
|
16
|
+
tidyout/rules/pip_install.yaml,sha256=SejE1eTCKUNllzKGdwwQk6uAdMsz-2zi3bon8fjuBNg,822
|
|
17
|
+
tidyout/rules/pytest.yaml,sha256=5tRwxJyKSYLuCrCtpqkisAq5ER-g1k8KPTDOfvRoWl0,801
|
|
18
|
+
tidyout/rules/tree.yaml,sha256=b-v6fD0k487Bx2aIWcDoSNYn7VzX4wXD57M_LOynA6Y,192
|
|
19
|
+
tidyout_cli-0.1.0.dist-info/METADATA,sha256=tI5DiMNwspq_67u5K3sbUTJIRK480Fnq2vnl6pcm_3w,3627
|
|
20
|
+
tidyout_cli-0.1.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
21
|
+
tidyout_cli-0.1.0.dist-info/entry_points.txt,sha256=7xhGowKdbIkQepakCivMjKIh_Tv1kqm_HRM81Hk-5Wo,42
|
|
22
|
+
tidyout_cli-0.1.0.dist-info/RECORD,,
|