compact-code 0.1.0__tar.gz → 0.2.1__tar.gz

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.
@@ -1,7 +1,10 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: compact-code
3
- Version: 0.1.0
3
+ Version: 0.2.1
4
4
  Summary: Dense Python for AI agents, readable view for humans. Same logic, ~30-55% fewer tokens.
5
+ Project-URL: Homepage, https://github.com/EmmanuelFreund/compact-code
6
+ Project-URL: Repository, https://github.com/EmmanuelFreund/compact-code
7
+ Project-URL: Issues, https://github.com/EmmanuelFreund/compact-code/issues
5
8
  Author-email: Emmanuel Freund <freund.emmanuel@gmail.com>
6
9
  License: MIT
7
10
  License-File: LICENSE
@@ -61,10 +64,22 @@ Codebase density gain: -19% to -41% tokens depending on the project (less if doc
61
64
  | `compact-code compact --check` | dry run: report savings without writing |
62
65
  | `compact-code compact --keep-docstrings` | keep docstrings (use when they are functional: CLI help text, doctests, flit) |
63
66
  | `compact-code expand <file>` | print a readable 4-space-indented view (`--write` to rewrite in place) |
64
- | `compact-code stats` | tokens and $ saved per full codebase read |
67
+ | `compact-code stats` | tokens and $ saved: per full read + cumulative real agent reads |
68
+ | `compact-code install-hook` | install a Claude Code hook that counts **real** savings on every agent read (`--project` for project-level) |
65
69
 
66
70
  Install `compact-code[exact]` for exact token counts (tiktoken).
67
71
 
72
+ ## The live savings counter
73
+
74
+ ```
75
+ compact-code install-hook # one-time, hooks into Claude Code
76
+ # ... let your agent work ...
77
+ compact-code stats
78
+ # Agent savings so far: 142 reads of compacted files, 87,310 tokens saved (~$0.26).
79
+ ```
80
+
81
+ The hook fires on every `Read` Claude Code performs; if the file is in this project's compaction manifest, the per-file saving (prorated for partial reads) is credited to a local ledger. No network, no telemetry — everything stays in `.compact-code.json`.
82
+
68
83
  ## The honest fine print
69
84
 
70
85
  - The gain is proportional to how much code your agent actually **reads**. Exploration-heavy and read-heavy sessions save the most; one-file quick fixes save nothing.
@@ -43,10 +43,22 @@ Codebase density gain: -19% to -41% tokens depending on the project (less if doc
43
43
  | `compact-code compact --check` | dry run: report savings without writing |
44
44
  | `compact-code compact --keep-docstrings` | keep docstrings (use when they are functional: CLI help text, doctests, flit) |
45
45
  | `compact-code expand <file>` | print a readable 4-space-indented view (`--write` to rewrite in place) |
46
- | `compact-code stats` | tokens and $ saved per full codebase read |
46
+ | `compact-code stats` | tokens and $ saved: per full read + cumulative real agent reads |
47
+ | `compact-code install-hook` | install a Claude Code hook that counts **real** savings on every agent read (`--project` for project-level) |
47
48
 
48
49
  Install `compact-code[exact]` for exact token counts (tiktoken).
49
50
 
51
+ ## The live savings counter
52
+
53
+ ```
54
+ compact-code install-hook # one-time, hooks into Claude Code
55
+ # ... let your agent work ...
56
+ compact-code stats
57
+ # Agent savings so far: 142 reads of compacted files, 87,310 tokens saved (~$0.26).
58
+ ```
59
+
60
+ The hook fires on every `Read` Claude Code performs; if the file is in this project's compaction manifest, the per-file saving (prorated for partial reads) is credited to a local ledger. No network, no telemetry — everything stays in `.compact-code.json`.
61
+
50
62
  ## The honest fine print
51
63
 
52
64
  - The gain is proportional to how much code your agent actually **reads**. Exploration-heavy and read-heavy sessions save the most; one-file quick fixes save nothing.
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "compact-code"
7
- version = "0.1.0"
7
+ version = "0.2.1"
8
8
  description = "Dense Python for AI agents, readable view for humans. Same logic, ~30-55% fewer tokens."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -19,6 +19,11 @@ classifiers = [
19
19
  "Topic :: Software Development :: Code Generators",
20
20
  ]
21
21
 
22
+ [project.urls]
23
+ Homepage = "https://github.com/EmmanuelFreund/compact-code"
24
+ Repository = "https://github.com/EmmanuelFreund/compact-code"
25
+ Issues = "https://github.com/EmmanuelFreund/compact-code/issues"
26
+
22
27
  [project.optional-dependencies]
23
28
  exact = ["tiktoken>=0.5"]
24
29
 
@@ -3,5 +3,5 @@
3
3
  from .compactor import compact_source, verify_equivalence
4
4
  from .expand import expand_source
5
5
 
6
- __version__ = "0.1.0"
6
+ __version__ = "0.2.1"
7
7
  __all__ = ["compact_source", "verify_equivalence", "expand_source", "__version__"]
@@ -6,7 +6,7 @@ import json
6
6
  import os
7
7
  import sys
8
8
 
9
- from . import compactor, expand, tokens
9
+ from . import compactor, expand, hooks, ledger, tokens
10
10
 
11
11
  MANIFEST = ".compact-code.json"
12
12
  EXCLUDED_DIRS = {
@@ -115,11 +115,43 @@ def cmd_stats(args):
115
115
  print(f"Codebase: {before:,} -> {after:,} tokens ({pct:.1f}% smaller, {len(files)} files)")
116
116
  print(f"Every full read of this codebase by your agent now saves "
117
117
  f"{saved:,} tokens (~${dollars:.2f} at ${args.price}/Mtok input).")
118
+ led = manifest.get("ledger")
119
+ if led and led.get("reads"):
120
+ led_saved = int(led["tokens_saved"])
121
+ led_dollars = led_saved / 1_000_000 * args.price
122
+ print(f"Agent savings so far: {led['reads']:,} reads of compacted files, "
123
+ f"{led_saved:,} tokens saved (~${led_dollars:.2f}).")
124
+ else:
125
+ print("No agent reads recorded yet. Run `compact-code install-hook` to start "
126
+ "counting real savings (Claude Code).")
118
127
  print("Sessions that read a lot of code (exploration, onboarding, refactors) "
119
128
  "benefit the most; surgical one-file fixes benefit little.")
120
129
  return 0
121
130
 
122
131
 
132
+ def cmd_hook(args):
133
+ try:
134
+ payload = json.load(sys.stdin)
135
+ tool_input = payload.get("tool_input", {})
136
+ path = tool_input.get("file_path")
137
+ if path and path.endswith(".py"):
138
+ ledger.record_read(path, limit=tool_input.get("limit"))
139
+ except Exception:
140
+ pass
141
+ return 0
142
+
143
+
144
+ def cmd_install_hook(args):
145
+ path, installed = hooks.install(project=args.project)
146
+ if installed:
147
+ print(f"Hook installed in {path}.")
148
+ print("Claude Code will now credit real savings on every read of a "
149
+ "compacted file. Restart Claude Code (or open /hooks) to activate.")
150
+ else:
151
+ print(f"Hook already installed in {path}.")
152
+ return 0
153
+
154
+
123
155
  def main(argv=None):
124
156
  parser = argparse.ArgumentParser(
125
157
  prog="compact-code",
@@ -145,6 +177,15 @@ def main(argv=None):
145
177
  help="USD per million input tokens (default: 3.0)")
146
178
  p.set_defaults(func=cmd_stats)
147
179
 
180
+ p = sub.add_parser("hook", help="(internal) Claude Code PostToolUse hook, reads JSON on stdin")
181
+ p.set_defaults(func=cmd_hook)
182
+
183
+ p = sub.add_parser("install-hook",
184
+ help="install the Claude Code hook that counts real savings")
185
+ p.add_argument("--project", action="store_true",
186
+ help="install in ./.claude/settings.json instead of ~/.claude/settings.json")
187
+ p.set_defaults(func=cmd_install_hook)
188
+
148
189
  args = parser.parse_args(argv)
149
190
  return args.func(args)
150
191
 
@@ -0,0 +1,49 @@
1
+ """Install the Claude Code hook that feeds the savings ledger."""
2
+
3
+ import json
4
+ import os
5
+ import shutil
6
+
7
+ HOOK_COMMAND = "compact-code hook"
8
+
9
+
10
+ def _hook_command():
11
+ """Absolute path to the executable: hooks run in shells whose PATH may differ."""
12
+ exe = shutil.which("compact-code")
13
+ if exe:
14
+ return f'"{exe.replace(os.sep, "/")}" hook'
15
+ return HOOK_COMMAND
16
+
17
+
18
+ def settings_path(project=False):
19
+ if project:
20
+ return os.path.join(".claude", "settings.json")
21
+ return os.path.join(os.path.expanduser("~"), ".claude", "settings.json")
22
+
23
+
24
+ def install(project=False):
25
+ """Merge the PostToolUse/Read hook into Claude Code settings.
26
+
27
+ Returns (path, installed) — installed is False if it was already there.
28
+ """
29
+ path = settings_path(project)
30
+ settings = {}
31
+ if os.path.exists(path):
32
+ with open(path, encoding="utf-8") as f:
33
+ content = f.read().strip()
34
+ if content:
35
+ settings = json.loads(content)
36
+ post = settings.setdefault("hooks", {}).setdefault("PostToolUse", [])
37
+ for matcher in post:
38
+ for hook in matcher.get("hooks", []):
39
+ cmd = hook.get("command", "")
40
+ if "compact-code" in cmd and cmd.endswith(" hook"):
41
+ return path, False
42
+ post.append({
43
+ "matcher": "Read",
44
+ "hooks": [{"type": "command", "command": _hook_command()}],
45
+ })
46
+ os.makedirs(os.path.dirname(path), exist_ok=True)
47
+ with open(path, "w", encoding="utf-8", newline="\n") as f:
48
+ json.dump(settings, f, indent=2)
49
+ return path, True
@@ -0,0 +1,52 @@
1
+ """Cumulative savings ledger: records real agent reads of compacted files."""
2
+
3
+ import json
4
+ import os
5
+
6
+ MANIFEST = ".compact-code.json"
7
+
8
+
9
+ def find_manifest(start_dir):
10
+ """Walk up from start_dir looking for a manifest. Returns its path or None."""
11
+ current = os.path.abspath(start_dir)
12
+ while True:
13
+ candidate = os.path.join(current, MANIFEST)
14
+ if os.path.isfile(candidate):
15
+ return candidate
16
+ parent = os.path.dirname(current)
17
+ if parent == current:
18
+ return None
19
+ current = parent
20
+
21
+
22
+ def record_read(file_path, limit=None):
23
+ """Credit the savings of one agent read of file_path, if it was compacted.
24
+
25
+ Returns the tokens credited (0 if the file is not in any manifest).
26
+ """
27
+ file_path = os.path.abspath(file_path)
28
+ manifest_path = find_manifest(os.path.dirname(file_path))
29
+ if manifest_path is None:
30
+ return 0
31
+ with open(manifest_path, encoding="utf-8") as f:
32
+ manifest = json.load(f)
33
+ root = os.path.dirname(manifest_path)
34
+ key = os.path.relpath(file_path, root).replace("\\", "/")
35
+ entry = manifest.get("files", {}).get(key)
36
+ if entry is None:
37
+ return 0
38
+ saved = entry["before"] - entry["after"]
39
+ if limit is not None:
40
+ try:
41
+ with open(file_path, encoding="utf-8", errors="replace") as f:
42
+ total_lines = sum(1 for _ in f)
43
+ if total_lines > 0:
44
+ saved = saved * min(1.0, limit / total_lines)
45
+ except OSError:
46
+ pass
47
+ ledger = manifest.setdefault("ledger", {"reads": 0, "tokens_saved": 0})
48
+ ledger["reads"] += 1
49
+ ledger["tokens_saved"] = round(ledger["tokens_saved"] + saved, 1)
50
+ with open(manifest_path, "w", encoding="utf-8", newline="\n") as f:
51
+ json.dump(manifest, f, indent=1)
52
+ return saved
@@ -0,0 +1,102 @@
1
+ import io
2
+ import json
3
+
4
+ import pytest
5
+
6
+ from compact_code import cli, hooks, ledger
7
+
8
+ SRC = "def f(x: int) -> int:\n \"\"\"Doubles.\"\"\"\n # comment\n return x * 2\n"
9
+
10
+
11
+ @pytest.fixture
12
+ def compacted(tmp_path, monkeypatch):
13
+ (tmp_path / "pkg").mkdir()
14
+ (tmp_path / "pkg" / "mod.py").write_text(SRC, encoding="utf-8")
15
+ monkeypatch.chdir(tmp_path)
16
+ cli.main(["compact"])
17
+ return tmp_path
18
+
19
+
20
+ def _ledger(root):
21
+ manifest = json.loads((root / cli.MANIFEST).read_text(encoding="utf-8"))
22
+ return manifest.get("ledger")
23
+
24
+
25
+ def test_record_read_accumulates(compacted):
26
+ target = compacted / "pkg" / "mod.py"
27
+ saved1 = ledger.record_read(str(target))
28
+ saved2 = ledger.record_read(str(target))
29
+ assert saved1 == saved2 > 0
30
+ led = _ledger(compacted)
31
+ assert led["reads"] == 2
32
+ assert led["tokens_saved"] == pytest.approx(saved1 * 2)
33
+
34
+
35
+ def test_record_read_unknown_file(compacted):
36
+ other = compacted / "pkg" / "other.py"
37
+ other.write_text("x = 1\n", encoding="utf-8")
38
+ assert ledger.record_read(str(other)) == 0
39
+ assert _ledger(compacted) is None
40
+
41
+
42
+ def test_record_read_no_manifest(tmp_path):
43
+ f = tmp_path / "a.py"
44
+ f.write_text("x = 1\n", encoding="utf-8")
45
+ assert ledger.record_read(str(f)) == 0
46
+
47
+
48
+ def test_record_read_partial_prorated(compacted):
49
+ target = compacted / "pkg" / "mod.py"
50
+ full = ledger.record_read(str(target))
51
+ total_lines = len(target.read_text(encoding="utf-8").splitlines())
52
+ partial = ledger.record_read(str(target), limit=1)
53
+ assert 0 < partial < full
54
+ assert partial == pytest.approx(full / total_lines)
55
+
56
+
57
+ def test_hook_command_credits_read(compacted, monkeypatch):
58
+ target = compacted / "pkg" / "mod.py"
59
+ payload = {"tool_name": "Read", "tool_input": {"file_path": str(target)}}
60
+ monkeypatch.setattr("sys.stdin", io.StringIO(json.dumps(payload)))
61
+ assert cli.main(["hook"]) == 0
62
+ assert _ledger(compacted)["reads"] == 1
63
+
64
+
65
+ def test_hook_command_survives_garbage(compacted, monkeypatch):
66
+ monkeypatch.setattr("sys.stdin", io.StringIO("not json"))
67
+ assert cli.main(["hook"]) == 0
68
+
69
+
70
+ def test_stats_shows_cumulative(compacted, capsys):
71
+ ledger.record_read(str(compacted / "pkg" / "mod.py"))
72
+ capsys.readouterr()
73
+ cli.main(["stats"])
74
+ out = capsys.readouterr().out
75
+ assert "Agent savings so far: 1" in out
76
+
77
+
78
+ def test_install_hook_project(compacted, capsys):
79
+ assert cli.main(["install-hook", "--project"]) == 0
80
+ settings = json.loads(
81
+ (compacted / ".claude" / "settings.json").read_text(encoding="utf-8"))
82
+ cmds = [h["command"]
83
+ for m in settings["hooks"]["PostToolUse"] for h in m["hooks"]]
84
+ assert any("compact-code" in c and c.endswith(" hook") for c in cmds)
85
+
86
+
87
+ def test_install_hook_idempotent_and_merging(compacted):
88
+ claude = compacted / ".claude"
89
+ claude.mkdir()
90
+ existing = {"permissions": {"allow": ["Bash(git *)"]},
91
+ "hooks": {"PostToolUse": [
92
+ {"matcher": "Write", "hooks": [{"type": "command", "command": "echo hi"}]}]}}
93
+ (claude / "settings.json").write_text(json.dumps(existing), encoding="utf-8")
94
+ cli.main(["install-hook", "--project"])
95
+ cli.main(["install-hook", "--project"])
96
+ settings = json.loads((claude / "settings.json").read_text(encoding="utf-8"))
97
+ assert settings["permissions"]["allow"] == ["Bash(git *)"]
98
+ post = settings["hooks"]["PostToolUse"]
99
+ assert len(post) == 2
100
+ cmds = [h["command"] for m in post for h in m["hooks"]]
101
+ ours = [c for c in cmds if "compact-code" in c and c.endswith(" hook")]
102
+ assert len(ours) == 1 and "echo hi" in cmds
@@ -138,7 +138,7 @@ wheels = [
138
138
 
139
139
  [[package]]
140
140
  name = "compact-code"
141
- version = "0.1.0"
141
+ version = "0.2.1"
142
142
  source = { editable = "." }
143
143
 
144
144
  [package.optional-dependencies]
File without changes
File without changes