kompactor 0.5.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.

Potentially problematic release.


This version of kompactor might be problematic. Click here for more details.

kompactor/samples.py ADDED
@@ -0,0 +1,217 @@
1
+ """Realistic sample outputs used by the dashboard's live preview, so users can
2
+ see what Claude would receive at each quality/savings setting."""
3
+
4
+
5
+ def _pytest():
6
+ passed = "\n".join(f"tests/test_api.py::test_endpoint_{i} PASSED{' ' * 30}[{i:3d}%]" for i in range(1, 61))
7
+ return f"""============================= test session starts ==============================
8
+ platform linux -- Python 3.12.4, pytest-8.3.2, pluggy-1.5.0
9
+ rootdir: /home/dev/shop-api
10
+ configfile: pyproject.toml
11
+ plugins: anyio-4.4.0, cov-5.0.0
12
+ collected 61 items
13
+
14
+ {passed}
15
+ tests/test_orders.py::test_create_order FAILED [100%]
16
+
17
+ =================================== FAILURES ===================================
18
+ ______________________________ test_create_order _______________________________
19
+
20
+ client = <FlaskClient <Flask 'shop'>>
21
+
22
+ def test_create_order(client):
23
+ > resp = client.post("/orders", json={{"sku": "A-1", "qty": 2}})
24
+
25
+ tests/test_orders.py:14:
26
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
27
+ .venv/lib/python3.12/site-packages/werkzeug/test.py:1167: in post
28
+ return self.open(*args, method="POST", **kw)
29
+ .venv/lib/python3.12/site-packages/flask/testing.py:235: in open
30
+ response = super().open(
31
+ .venv/lib/python3.12/site-packages/werkzeug/test.py:1116: in open
32
+ response_parts = self.run_wsgi_app(request.environ, buffered=buffered)
33
+ .venv/lib/python3.12/site-packages/werkzeug/test.py:988: in run_wsgi_app
34
+ rv = run_wsgi_app(self.application, environ, buffered=buffered)
35
+ .venv/lib/python3.12/site-packages/werkzeug/test.py:1264: in run_wsgi_app
36
+ app_rv = app(environ, start_response)
37
+ .venv/lib/python3.12/site-packages/flask/app.py:1498: in __call__
38
+ return self.wsgi_app(environ, start_response)
39
+ .venv/lib/python3.12/site-packages/flask/app.py:1476: in wsgi_app
40
+ response = self.handle_exception(e)
41
+ .venv/lib/python3.12/site-packages/flask/app.py:1473: in wsgi_app
42
+ response = self.full_dispatch_request()
43
+ .venv/lib/python3.12/site-packages/flask/app.py:882: in full_dispatch_request
44
+ rv = self.handle_user_exception(e)
45
+ .venv/lib/python3.12/site-packages/flask/app.py:880: in full_dispatch_request
46
+ rv = self.dispatch_request()
47
+ .venv/lib/python3.12/site-packages/flask/app.py:865: in dispatch_request
48
+ return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args)
49
+ shop/orders.py:41: in create_order
50
+ total = price_for(sku) * payload["qty"]
51
+ shop/pricing.py:18: in price_for
52
+ return CATALOG[sku].price
53
+ E KeyError: 'A-1'
54
+
55
+ shop/pricing.py:18: KeyError
56
+ =========================== short test summary info ============================
57
+ FAILED tests/test_orders.py::test_create_order - KeyError: 'A-1'
58
+ ========================= 1 failed, 60 passed in 2.31s ========================="""
59
+
60
+
61
+ def _gotest():
62
+ runs = "\n".join(f"=== RUN TestHandler/case_{i}\n--- PASS: TestHandler/case_{i} (0.00s)" for i in range(1, 41))
63
+ return f"""go: downloading github.com/stretchr/testify v1.9.0
64
+ go: downloading github.com/davecgh/go-spew v1.1.1
65
+ go: downloading gopkg.in/yaml.v3 v3.0.1
66
+ ? example.com/shop/cmd/server [no test files]
67
+ ? example.com/shop/internal/version [no test files]
68
+ === RUN TestHandler
69
+ {runs}
70
+ --- PASS: TestHandler (0.02s)
71
+ === RUN TestCheckout
72
+ --- FAIL: TestCheckout (0.00s)
73
+ panic: runtime error: invalid memory address or nil pointer dereference [recovered]
74
+ panic: runtime error: invalid memory address or nil pointer dereference
75
+ [signal SIGSEGV: segmentation violation code=0x1 addr=0x18 pc=0x6b2f1a]
76
+
77
+ goroutine 21 [running]:
78
+ testing.tRunner.func1.2({{0x6f1e40, 0x9a8b30}})
79
+ /usr/local/go/src/testing/testing.go:1631 +0x24a
80
+ testing.tRunner.func1()
81
+ /usr/local/go/src/testing/testing.go:1634 +0x377
82
+ panic({{0x6f1e40?, 0x9a8b30?}})
83
+ /usr/local/go/src/runtime/panic.go:770 +0x132
84
+ example.com/shop/internal/cart.(*Cart).Total(0x0)
85
+ /home/dev/shop/internal/cart/cart.go:58 +0x1a
86
+ example.com/shop/internal/checkout.Checkout(0xc0000a6000)
87
+ /home/dev/shop/internal/checkout/checkout.go:33 +0x45
88
+ example.com/shop/internal/checkout.TestCheckout(0xc0001044e0)
89
+ /home/dev/shop/internal/checkout/checkout_test.go:21 +0x5d
90
+ testing.tRunner(0xc0001044e0, 0x7486a8)
91
+ /usr/local/go/src/testing/testing.go:1689 +0xfb
92
+ created by testing.(*T).Run in goroutine 1
93
+ /usr/local/go/src/testing/testing.go:1742 +0x390
94
+ FAIL example.com/shop/internal/checkout 0.014s
95
+ ok example.com/shop/internal/cart 0.009s
96
+ ok example.com/shop/internal/http 0.031s
97
+ FAIL"""
98
+
99
+
100
+ def _vitest():
101
+ ok = "\n".join(f" ✓ src/components/ProductCard.test.tsx > ProductCard > renders variant {i} {3 + i % 7}ms"
102
+ for i in range(1, 46))
103
+ return f"""
104
+ RUN v2.0.5 /home/dev/storefront
105
+
106
+ {ok}
107
+ ❯ src/hooks/useCart.test.ts (3 tests | 1 failed) 18ms
108
+ × useCart > adds item and updates total 9ms
109
+ → expected 0 to be 2400 // Object.is equality
110
+
111
+ ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Failed Tests 1 ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
112
+
113
+ FAIL src/hooks/useCart.test.ts > useCart > adds item and updates total
114
+ AssertionError: expected 0 to be 2400 // Object.is equality
115
+
116
+ - Expected
117
+ + Received
118
+
119
+ - 2400
120
+ + 0
121
+
122
+ ❯ src/hooks/useCart.test.ts:27:32
123
+ 25| act(() => result.current.add({{ sku: 'A-1', price: 1200 }}, 2));
124
+ 26|
125
+ 27| expect(result.current.total).toBe(2400);
126
+ | ^
127
+ 28| }});
128
+ at Object.toBe (node_modules/@vitest/expect/dist/index.js:1235:20)
129
+ at Proxy.<anonymous> (node_modules/chai/chai.js:1661:16)
130
+ at Proxy.methodWrapper (node_modules/chai/chai.js:1584:25)
131
+ at node_modules/@vitest/runner/dist/index.js:146:14
132
+ at node_modules/@vitest/runner/dist/index.js:61:7
133
+ at runTest (node_modules/@vitest/runner/dist/index.js:960:17)
134
+ at processTicksAndRejections (node:internal/process/task_queues:95:5)
135
+
136
+ ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
137
+
138
+ Test Files 1 failed | 4 passed (5)
139
+ Tests 1 failed | 47 passed (48)
140
+ Start at 10:42:11
141
+ Duration 1.94s"""
142
+
143
+
144
+ def _gitlog():
145
+ authors = ["Priya Shah <priya@shop.dev>", "Marco Rossi <marco@shop.dev>", "Lee Kim <lee@shop.dev>"]
146
+ msgs = ["fix cart total rounding", "add checkout retry", "bump vite to 5.4", "refactor pricing service",
147
+ "handle empty SKU", "add order webhook", "tidy logging", "split ProductCard", "cache catalog lookups",
148
+ "go: upgrade to 1.23"]
149
+ out = []
150
+ for i in range(24):
151
+ out.append(f"commit {('%040x' % (0x9f3a1c * (i + 7)))[-40:]}\nAuthor: {authors[i % 3]}\n"
152
+ f"Date: Tue Oct {20 - i % 19} 1{i % 10}:0{i % 6}:12 2026 +0000\n\n {msgs[i % 10]}\n\n"
153
+ f" {'Longer explanation of why this change was needed and how it was tested.' if i % 2 else ''}\n")
154
+ return "\n".join(out)
155
+
156
+
157
+ def _npm_build():
158
+ chunks = "\n".join(f"dist/assets/{n}-{abs(hash(n)) % 10**8:08x}.js {12 + i * 3.7:6.2f} kB │ gzip: {4 + i:5.2f} kB"
159
+ for i, n in enumerate(["index", "vendor", "react-dom", "router", "cart", "checkout", "product",
160
+ "account", "search", "icons", "charts", "i18n", "auth", "admin"]))
161
+ deps = "\n".join(f"npm http fetch GET 200 https://registry.npmjs.org/{p} {20 + i}ms (cache revalidated)"
162
+ for i, p in enumerate(["react", "react-dom", "vite", "typescript", "zod", "@tanstack/react-query",
163
+ "clsx", "date-fns", "eslint", "vitest", "msw", "postcss"] * 6))
164
+ return f"""{deps}
165
+ npm WARN deprecated inflight@1.0.6: This module is not supported, and leaks memory.
166
+ npm WARN deprecated glob@7.2.3: Glob versions prior to v9 are no longer supported
167
+ npm WARN deprecated rimraf@3.0.2: Rimraf versions prior to v4 are no longer supported
168
+
169
+ added 412 packages, and audited 413 packages in 9s
170
+
171
+ > storefront@1.4.0 build
172
+ > tsc -b && vite build
173
+
174
+ vite v5.4.2 building for production...
175
+ transforming...
176
+ ✓ 1324 modules transformed.
177
+ rendering chunks...
178
+ computing gzip size...
179
+ dist/index.html 0.62 kB │ gzip: 0.38 kB
180
+ dist/assets/index-4c1e2b9a.css 18.40 kB │ gzip: 4.12 kB
181
+ {chunks}
182
+ (!) Some chunks are larger than 500 kB after minification.
183
+ ✓ built in 6.81s"""
184
+
185
+
186
+ def _gitdiff():
187
+ body = "\n".join(f" const line{i} = compute({i});" for i in range(1, 19))
188
+ lock = "\n".join(f'+ "node_modules/pkg-{i}": {{ "version": "1.{i}.0", "resolved": "https://registry.npmjs.org/pkg-{i}/-/pkg-{i}-1.{i}.0.tgz" }},'
189
+ for i in range(60))
190
+ return f"""diff --git a/src/hooks/useCart.ts b/src/hooks/useCart.ts
191
+ index 3f2a1c4..9b7e2d0 100644
192
+ --- a/src/hooks/useCart.ts
193
+ +++ b/src/hooks/useCart.ts
194
+ @@ -10,30 +10,31 @@ export function useCart() {{
195
+ {body}
196
+ - const total = items.reduce((sum, i) => sum + i.price, 0);
197
+ + const total = items.reduce((sum, i) => sum + i.price * i.qty, 0);
198
+ {body}
199
+ diff --git a/package-lock.json b/package-lock.json
200
+ index 11aa22b..33cc44d 100644
201
+ --- a/package-lock.json
202
+ +++ b/package-lock.json
203
+ @@ -1,6 +1,66 @@
204
+ {lock}"""
205
+
206
+
207
+ SAMPLES = [
208
+ {"id": "pytest", "short": "pytest", "lang": "python", "label": "Python · pytest -v (1 failure)", "cmd": "pytest -v", "text": _pytest()},
209
+ {"id": "gotest", "short": "go test", "lang": "go", "label": "Go · go test -v ./... (panic)", "cmd": "go test -v ./...", "text": _gotest()},
210
+ {"id": "vitest", "short": "vitest", "lang": "react", "label": "React · vitest run (1 failure)", "cmd": "npx vitest run",
211
+ "text": _vitest()},
212
+ {"id": "build", "short": "npm build", "lang": "typescript", "label": "TypeScript · npm install && npm run build",
213
+ "cmd": "npm install && npm run build", "text": _npm_build()},
214
+ {"id": "gitdiff", "short": "git diff", "lang": "typescript", "label": "git diff (with lock file)", "cmd": "git diff",
215
+ "text": _gitdiff()},
216
+ {"id": "gitlog", "short": "git log", "lang": "any", "label": "git log -n 24", "cmd": "git log -n 24", "text": _gitlog()},
217
+ ]
kompactor/server.py ADDED
@@ -0,0 +1,301 @@
1
+ """Local settings & savings dashboard: `kompactor ui`.
2
+
3
+ Stdlib only. Binds to 127.0.0.1, checks the Host header (DNS rebinding) and a
4
+ per-launch token on every write (CSRF), so other websites can't change settings.
5
+ """
6
+
7
+ import json
8
+ import os
9
+ import secrets
10
+ import socket
11
+ import threading
12
+ import time
13
+ import webbrowser
14
+ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
15
+ from pathlib import Path
16
+ from urllib.parse import parse_qs, urlparse
17
+
18
+ import difflib
19
+ import mimetypes
20
+
21
+ from . import __version__, config, estimate, filters, graph, installer, langs, samples, store, tokens
22
+
23
+ TOKEN = secrets.token_urlsafe(24)
24
+ UI_FILE = Path(__file__).with_name("ui.html")
25
+ STATIC = Path(__file__).with_name("static")
26
+
27
+
28
+ # ---------------------------------------------------------------------------
29
+ # API
30
+
31
+
32
+ def preview(knobs: dict, languages) -> list:
33
+ out = []
34
+ for s in samples.SAMPLES:
35
+ cfg = dict(knobs, languages=languages)
36
+ raw = tokens.count(s["text"])
37
+ if not cfg.get("bash", True) or raw < cfg.get("bash_min_tokens", 0):
38
+ text = s["text"]
39
+ else:
40
+ text = filters.compress(s["cmd"], s["text"], 0, stash_id=42, opts=cfg)
41
+ if tokens.count(text) > raw * 0.9:
42
+ text = s["text"]
43
+ out.append({"id": s["id"], "label": s["label"], "short": s.get("short", s["id"]), "lang": s["lang"],
44
+ "cmd": s["cmd"], "raw": raw,
45
+ "kept": tokens.count(text), "before": s["text"], "after": text,
46
+ "segments": segments(s["text"], text)})
47
+ return out
48
+
49
+
50
+ def segments(before: str, after: str) -> list:
51
+ """Line-level view of what was kept, dropped or added (notes) — for the preview."""
52
+ a, b = filters.clean(before), after.split("\n")
53
+ out = []
54
+ for op, i1, i2, j1, j2 in difflib.SequenceMatcher(None, a, b, autojunk=False).get_opcodes():
55
+ if op == "equal":
56
+ out += [["keep", ln] for ln in a[i1:i2]]
57
+ else:
58
+ out += [["drop", ln] for ln in a[i1:i2]]
59
+ out += [["note", ln] for ln in b[j1:j2]]
60
+ return out
61
+
62
+
63
+ def preset_savings() -> dict:
64
+ res = {}
65
+ for p, preset in config.PRESETS.items():
66
+ rows = preview({k: preset[k] for k in config.KNOBS}, [])
67
+ r, k = sum(x["raw"] for x in rows), sum(x["kept"] for x in rows)
68
+ res[p] = round(100 * (r - k) / r) if r else 0
69
+ return res
70
+
71
+
72
+ def state() -> dict:
73
+ data = config.read_file()
74
+ with store.connect() as db:
75
+ projects = [r[0] for r in db.execute(
76
+ "SELECT project FROM events WHERE project != '' GROUP BY project ORDER BY MAX(ts) DESC LIMIT 30")]
77
+ projects = list(dict.fromkeys(projects + list(data["projects"])))
78
+ cwd_project = config.project_root(os.getcwd())
79
+ if cwd_project and (Path(cwd_project) / ".git").exists() and cwd_project not in projects:
80
+ projects.append(cwd_project)
81
+ return {
82
+ "version": __version__,
83
+ "config": data,
84
+ "presets": {p: {"name": v["name"], "blurb": v["blurb"], **{k: v[k] for k in config.KNOBS}}
85
+ for p, v in config.PRESETS.items()},
86
+ "preset_savings": preset_savings(),
87
+ "knobs": {k: list(v) for k, v in config.KNOB_INFO.items()},
88
+ "knob_order": config.KNOBS,
89
+ "languages": {k: v["label"] for k, v in langs.PROFILES.items()},
90
+ "projects": [{"path": p, "detected": langs.detect(p) if os.path.isdir(p) else [],
91
+ "override": data["projects"].get(p),
92
+ "repo_file": config.repo_settings(p) if os.path.isdir(p) else {}} for p in projects],
93
+ "setup": installer.status(),
94
+ "home": str(store.home()),
95
+ }
96
+
97
+
98
+ def stats(params: dict) -> dict:
99
+ days = float(params.get("days", ["30"])[0])
100
+ project = params.get("project", [""])[0]
101
+ since = time.time() - days * 86400
102
+ where, args = "ts > ?", [since]
103
+ if project:
104
+ where += " AND project = ?"
105
+ args.append(project)
106
+ with store.connect() as db:
107
+ tot = db.execute(f"SELECT COUNT(*), COALESCE(SUM(raw_tokens),0), COALESCE(SUM(kept_tokens),0) FROM events "
108
+ f"WHERE {where}", args).fetchone()
109
+ feats = db.execute(f"SELECT feature, COUNT(*), SUM(raw_tokens), SUM(kept_tokens) FROM events WHERE {where} "
110
+ f"GROUP BY feature ORDER BY SUM(raw_tokens)-SUM(kept_tokens) DESC", args).fetchall()
111
+ turns = db.execute(f"SELECT session, idx, ts, project, raw_tokens, kept_tokens, context_tokens, features "
112
+ f"FROM turns WHERE {where} ORDER BY ts DESC LIMIT 60", args).fetchall()
113
+ top = db.execute(f"SELECT detail, COUNT(*), SUM(raw_tokens), SUM(kept_tokens) FROM events WHERE {where} "
114
+ f"AND feature='bash' GROUP BY detail ORDER BY SUM(raw_tokens)-SUM(kept_tokens) DESC LIMIT 10",
115
+ args).fetchall()
116
+ sessions = db.execute(f"SELECT COUNT(DISTINCT session) FROM events WHERE {where}", args).fetchone()[0]
117
+ return {
118
+ "events": tot[0], "raw": tot[1], "kept": tot[2], "saved": tot[1] - tot[2], "sessions": sessions,
119
+ "features": [{"feature": f[0], "count": f[1], "raw": f[2], "kept": f[3]} for f in feats],
120
+ "turns": [{"session": t[0], "idx": t[1], "ts": t[2], "project": t[3], "raw": t[4], "kept": t[5],
121
+ "context": t[6], "features": json.loads(t[7] or "{}")} for t in reversed(turns)],
122
+ "top": [{"cmd": t[0], "runs": t[1], "raw": t[2], "kept": t[3]} for t in top],
123
+ "live": store.read_live(),
124
+ }
125
+
126
+
127
+ def graph_state(project: str) -> dict:
128
+ if not project or not os.path.isdir(project):
129
+ return {"exists": False}
130
+ if not graph.exists(project):
131
+ return {"exists": False, "project": project}
132
+ v = graph.viz(project, 70)
133
+ g = graph.Graph(project)
134
+ dead, public = graph.unused(project)
135
+ v.update(exists=True, project=project, cycles=len(graph.import_cycles(g)),
136
+ unused={"dead": len(dead), "public": len(public)},
137
+ hubs=[{"name": n, "refs": c, "files": len(g.ref_files[n])} for n, c in g.ref_count.most_common(300)
138
+ if n in g.by_name and len(n) > 2][:10],
139
+ core=[{"unit": u, "importers": len(g.in_units.get(u, ())), "symbols": graph._key_symbols(g, u)[:6]}
140
+ for u in g.core_units()[:12]])
141
+ return v
142
+
143
+
144
+ def save_config(body: dict) -> dict:
145
+ data = config.read_file()
146
+ if "preset" in body:
147
+ p = int(body["preset"])
148
+ if p not in config.PRESETS:
149
+ raise ValueError("preset must be 1-5")
150
+ data["preset"] = p
151
+ if "overrides" in body:
152
+ data["overrides"] = {k: config.coerce(k, v) for k, v in body["overrides"].items() if k in config.KNOBS}
153
+ if "languages" in body:
154
+ mode = body["languages"].get("mode", "auto")
155
+ sel = [s for s in body["languages"].get("selected", []) if s in langs.PROFILES]
156
+ data["languages"] = {"mode": "manual" if mode == "manual" else "auto", "selected": sel}
157
+ if "general" in body:
158
+ data["general"].update({k: bool(v) for k, v in body["general"].items() if k in config.GENERAL})
159
+ if "project" in body:
160
+ path, ov = body["project"].get("path", ""), body["project"].get("override")
161
+ if path:
162
+ if ov:
163
+ data["projects"][path] = {"preset": int(ov.get("preset", data["preset"])),
164
+ "overrides": {k: config.coerce(k, v) for k, v in
165
+ ov.get("overrides", {}).items() if k in config.KNOBS}}
166
+ else:
167
+ data["projects"].pop(path, None)
168
+ config.write_file(data)
169
+ return {"ok": True, "config": data}
170
+
171
+
172
+ # ---------------------------------------------------------------------------
173
+ # HTTP
174
+
175
+
176
+ class Handler(BaseHTTPRequestHandler):
177
+ server_version = "kompactor"
178
+
179
+ def log_message(self, *a): # quiet
180
+ pass
181
+
182
+ def _host_ok(self) -> bool:
183
+ host = (self.headers.get("Host") or "").split(":")[0]
184
+ return host in ("127.0.0.1", "localhost")
185
+
186
+ def _send(self, code: int, body, ctype="application/json"):
187
+ data = body if isinstance(body, bytes) else json.dumps(body).encode()
188
+ self.send_response(code)
189
+ self.send_header("Content-Type", ctype)
190
+ self.send_header("Content-Length", str(len(data)))
191
+ self.send_header("Cache-Control", "no-store")
192
+ self.send_header("X-Content-Type-Options", "nosniff")
193
+ self.end_headers()
194
+ self.wfile.write(data)
195
+
196
+ def do_GET(self):
197
+ if not self._host_ok():
198
+ return self._send(403, {"error": "bad host"})
199
+ u = urlparse(self.path)
200
+ try:
201
+ if u.path in ("/", "/index.html"):
202
+ html = UI_FILE.read_text().replace("__KOMPACTOR_TOKEN__", TOKEN)
203
+ return self._send(200, html.encode(), "text/html; charset=utf-8")
204
+ if u.path == "/api/ping":
205
+ return self._send(200, {"kompactor": __version__})
206
+ if u.path == "/api/state":
207
+ return self._send(200, state())
208
+ if u.path == "/api/stats":
209
+ return self._send(200, stats(parse_qs(u.query)))
210
+ if u.path == "/api/graph":
211
+ return self._send(200, graph_state(parse_qs(u.query).get("project", [""])[0]))
212
+ if u.path in ("/usage", "/usage.html", "/guide"):
213
+ self.send_response(302)
214
+ self.send_header("Location", "/static/usage.html")
215
+ self.send_header("Content-Length", "0")
216
+ self.end_headers()
217
+ return None
218
+ if u.path.startswith("/static/"):
219
+ f = (STATIC / u.path[len("/static/"):]).resolve()
220
+ if STATIC.resolve() in f.parents and f.is_file():
221
+ ctype = mimetypes.guess_type(f.name)[0] or "application/octet-stream"
222
+ if f.suffix == ".woff2":
223
+ ctype = "font/woff2"
224
+ return self._send(200, f.read_bytes(), ctype)
225
+ return self._send(404, {"error": "not found"})
226
+ return self._send(404, {"error": "not found"})
227
+ except Exception as e: # pragma: no cover - surfaced in the UI
228
+ return self._send(500, {"error": str(e)})
229
+
230
+ def do_POST(self):
231
+ if not self._host_ok() or self.headers.get("X-Kompactor-Token") != TOKEN:
232
+ return self._send(403, {"error": "forbidden"})
233
+ try:
234
+ body = json.loads(self.rfile.read(int(self.headers.get("Content-Length") or 0)) or b"{}")
235
+ u = urlparse(self.path).path
236
+ if u == "/api/config":
237
+ return self._send(200, save_config(body))
238
+ if u == "/api/preview":
239
+ knobs = {k: config.coerce(k, v) for k, v in body.get("knobs", {}).items() if k in config.KNOBS}
240
+ return self._send(200, {"samples": preview(knobs, body.get("languages") or [])})
241
+ if u == "/api/setup":
242
+ action = body.get("action")
243
+ target = installer.install(statusline=bool(body.get("statusline", True)),
244
+ uninstall=action == "uninstall")
245
+ return self._send(200, {"ok": True, "file": str(target), "setup": installer.status()})
246
+ if u == "/api/selftest":
247
+ return self._send(200, {"results": installer.selftest()})
248
+ if u.startswith("/api/graph/"):
249
+ project = body.get("project", "")
250
+ if not project or not os.path.isdir(project):
251
+ raise ValueError("unknown project")
252
+ action = u.rsplit("/", 1)[1]
253
+ if action == "build":
254
+ graph.build(project)
255
+ return self._send(200, graph_state(project))
256
+ text = {"query": lambda: graph.query(project, body.get("q", ""), 1800),
257
+ "report": lambda: graph.report(project, 2500),
258
+ "unused": lambda: graph.unused_text(project, 2500),
259
+ "debt": lambda: graph.debt_text(project, 2500),
260
+ "impact": lambda: graph.impact_text(project, body.get("q", ""), 1800),
261
+ "explain": lambda: graph.explain_text(project, body.get("q", ""), 1800)}.get(action)
262
+ if not text:
263
+ return self._send(404, {"error": "not found"})
264
+ return self._send(200, {"text": text()})
265
+ if u == "/api/estimate":
266
+ s = estimate.run(limit=int(body.get("limit") or 0) or None, style_pct=0)
267
+ return self._send(200, {"sessions": s["sessions"], "calls": s["calls"],
268
+ "tool_tokens": s["tool_tokens"], "saved_tool": s["saved_tool"],
269
+ "billed": s["billed"], "saved_billed": s["saved_billed"],
270
+ "cost": s["cost"], "saved_cost": s["saved_cost"],
271
+ "by_feature": dict(s["by_feature"])})
272
+ return self._send(404, {"error": "not found"})
273
+ except (ValueError, KeyError, RuntimeError) as e:
274
+ return self._send(400, {"error": str(e)})
275
+
276
+
277
+ def _free_port(preferred: int) -> int:
278
+ for port in [preferred] + list(range(preferred + 1, preferred + 20)):
279
+ with socket.socket() as s:
280
+ try:
281
+ s.bind(("127.0.0.1", port))
282
+ return port
283
+ except OSError:
284
+ continue
285
+ return 0
286
+
287
+
288
+ def serve(port: int = 7420, open_browser: bool = True, print_url: bool = True) -> None:
289
+ port = _free_port(port)
290
+ httpd = ThreadingHTTPServer(("127.0.0.1", port), Handler)
291
+ url = f"http://127.0.0.1:{httpd.server_address[1]}/"
292
+ if print_url:
293
+ print(f"Kompactor dashboard: {url} (Ctrl+C to stop)", flush=True)
294
+ if open_browser:
295
+ threading.Timer(0.3, lambda: webbrowser.open(url)).start()
296
+ try:
297
+ httpd.serve_forever()
298
+ except KeyboardInterrupt:
299
+ pass
300
+ finally:
301
+ httpd.server_close()
kompactor/shellwrap.py ADDED
@@ -0,0 +1,149 @@
1
+ """Shell prefix: compact test, build and lint output even when the command fails.
2
+
3
+ Claude Code only lets hooks rewrite the output of commands that succeed. When a
4
+ command exits non-zero it skips PostToolUse, fires PostToolUseFailure (which can
5
+ add context but not replace anything) and cuts the output to its first and last
6
+ few thousand characters, which usually drops the failures at the end of a test
7
+ run. Claude Code's CLAUDE_CODE_SHELL_PREFIX setting runs every Bash command as
8
+ `<prefix> '<shell command>'`. Kompactor's prefix (~/.kompactor/bin/kompactor-shell)
9
+ sends finite, noisy commands here: they run to completion, the output is
10
+ compacted, and the exit code is passed through unchanged. Permission rules
11
+ still see Claude's original command, because the prefix applies only when the
12
+ command runs.
13
+
14
+ Everything else, and any problem inside Kompactor before the command starts,
15
+ runs the command untouched.
16
+ """
17
+
18
+ import os
19
+ import shlex
20
+ import signal
21
+ import subprocess
22
+ import sys
23
+ from collections import deque
24
+
25
+ from . import config, filters, installer, store, tokens
26
+
27
+ HEAD_BYTES = 16 << 20 # keep at most 16 MB from the start and 8 MB from the end of the output
28
+ TAIL_BYTES = 8 << 20
29
+ SHOWN_LIMIT = 30000 # Claude Code saves longer successful outputs to a file and shows a 2 KB preview
30
+ PREVIEW_CHARS = 2048
31
+ ERROR_LIMIT = 10000 # failed commands: Claude Code keeps about this many characters
32
+
33
+
34
+ def user_command(full: str) -> str:
35
+ """Claude's own command inside the string Claude Code hands the prefix:
36
+ `source <snapshot> ... && eval '<command>' < /dev/null && pwd -P >| <cwd file>`."""
37
+ i = full.find(" eval ")
38
+ if i < 0:
39
+ return ""
40
+ try:
41
+ lex = shlex.shlex(full[i + 6:], posix=True)
42
+ lex.whitespace_split = True
43
+ return lex.get_token() or ""
44
+ except ValueError:
45
+ return ""
46
+
47
+
48
+ def decide(full: str):
49
+ """(command, config) when the command should run through Kompactor, else (None, None)."""
50
+ if os.environ.get("KOMPACTOR_RAW"):
51
+ return None, None
52
+ cmd = user_command(full)
53
+ if not cmd or cmd.lstrip().startswith("KOMPACTOR_RAW=1") or not filters.wrappable(cmd):
54
+ return None, None
55
+ cfg = config.load(os.getcwd())
56
+ if not cfg.get("bash") or not cfg.get("bash_wrap"):
57
+ return None, None
58
+ words = " ".join(filters.argv0(cmd))
59
+ if any(words.startswith(c.strip()) for c in cfg.get("never_compact", []) if c.strip()):
60
+ return None, None
61
+ return cmd, cfg
62
+
63
+
64
+ def shown_tokens(text: str, code: int) -> int:
65
+ """Roughly what Claude would have read without Kompactor (the honest baseline for the ledger)."""
66
+ if code != 0:
67
+ if len(text) > ERROR_LIMIT:
68
+ return tokens.count(text[:ERROR_LIMIT // 2]) + tokens.count(text[-ERROR_LIMIT // 2:])
69
+ return tokens.count(text)
70
+ if len(text) > int(os.environ.get("BASH_MAX_OUTPUT_LENGTH") or SHOWN_LIMIT):
71
+ return tokens.count(text[:PREVIEW_CHARS]) + 60
72
+ return tokens.count(text)
73
+
74
+
75
+ def compact(cmd: str, raw: str, code: int, cfg: dict, stopped: bool = False) -> str:
76
+ raw_tokens = tokens.count(raw)
77
+ if raw_tokens < cfg["bash_min_tokens"]:
78
+ return raw
79
+ sid = store.stash(cmd, raw)
80
+ new = filters.compress(cmd, raw, code, stash_id=sid, opts=cfg)
81
+ kept = tokens.count(new)
82
+ if kept > raw_tokens * 0.9 and len(raw) <= (ERROR_LIMIT if code else SHOWN_LIMIT):
83
+ return raw # barely compressible, and Claude Code would show all of it anyway
84
+ note = "; the command was stopped before it finished" if stopped else ""
85
+ new = new.rstrip("\n") + f"\n[kompactor: {raw_tokens}›{kept} tokens{note}; full output: {installer.cli_name()} show {sid}]\n"
86
+ store.record("bash", shown_tokens(raw, code), kept, cmd, os.environ.get("CLAUDE_CODE_SESSION_ID", ""),
87
+ project=cfg["project"])
88
+ return new
89
+
90
+
91
+ def run(shell: str, full: str, cmd: str, cfg: dict) -> int:
92
+ proc = subprocess.Popen([shell, "-c", full], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
93
+ stopped = []
94
+
95
+ def forward(sig, _frame): # a timeout or Esc in Claude Code: stop the command, still report what it printed
96
+ stopped.append(sig)
97
+ try:
98
+ proc.send_signal(sig)
99
+ except OSError:
100
+ pass
101
+
102
+ for sig in (signal.SIGTERM, signal.SIGINT, signal.SIGHUP):
103
+ signal.signal(sig, forward)
104
+ head, tail, tail_size, dropped = bytearray(), deque(), 0, 0
105
+ try:
106
+ while True:
107
+ chunk = proc.stdout.read1(65536)
108
+ if not chunk:
109
+ break
110
+ room = HEAD_BYTES - len(head)
111
+ if room > 0:
112
+ head += chunk[:room]
113
+ chunk = chunk[room:]
114
+ if chunk:
115
+ tail.append(chunk)
116
+ tail_size += len(chunk)
117
+ while tail_size - len(tail[0]) >= TAIL_BYTES:
118
+ tail_size -= len(tail[0])
119
+ dropped += len(tail.popleft())
120
+ except OSError:
121
+ pass
122
+ code = proc.wait()
123
+ if code < 0:
124
+ code = 128 - code
125
+ raw = head.decode("utf-8", "replace")
126
+ if dropped:
127
+ raw += f"\n… [kompactor: {dropped} bytes from the middle were not kept] …\n"
128
+ raw += b"".join(tail).decode("utf-8", "replace")
129
+ try:
130
+ text = compact(cmd, raw, code, cfg, bool(stopped))
131
+ except Exception: # never lose the output to a Kompactor bug
132
+ text = raw
133
+ try:
134
+ sys.stdout.write(text)
135
+ sys.stdout.flush()
136
+ except OSError:
137
+ pass
138
+ return code
139
+
140
+
141
+ def main(argv) -> int:
142
+ shell, full = argv[0], argv[1]
143
+ try:
144
+ cmd, cfg = decide(full)
145
+ except Exception:
146
+ cmd = cfg = None
147
+ if not cmd:
148
+ os.execvp(shell, [shell, "-c", full])
149
+ return run(shell, full, cmd, cfg)