jevprune 0.1.0

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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ibrahemid
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,257 @@
1
+ # jevprune
2
+
3
+ Keeps the lines of a command's output that matter for the task you are working on. Every kept line is printed exactly as the command wrote it. The full output is saved on disk and any dropped range can be printed back.
4
+
5
+ Built for coding agents that run `npm test`, `cargo build`, `pytest` or `docker compose up` and push thousands of lines into their context. Line relevance is decided per line by [Jev](https://typesafe.ai), TypeSafe's model, with a fixed set of lines kept before Jev sees anything: the last 40 lines, every error signature, and the lines around it.
6
+
7
+ ## Install
8
+
9
+ ```sh
10
+ npm install -g jevprune
11
+ export TYPESAFE_API_KEY=... # early-access key from typesafe.ai
12
+ ```
13
+
14
+ Claude Code plugin (wraps every Bash call automatically):
15
+
16
+ ```sh
17
+ claude plugin marketplace add ibrahemid/jevprune
18
+ claude plugin install jevprune@jevprune
19
+ ```
20
+
21
+ The plugin calls the globally installed `jevprune` binary. Without the binary the hook does nothing and commands run as before. Without a key, commands are still wrapped and pruning falls back to head and tail.
22
+
23
+ ## Use
24
+
25
+ ```sh
26
+ jevprune run --task "fix the failing auth test" -- npm test
27
+ npm test 2>&1 | jevprune select --task "fix the failing auth test"
28
+ jevprune show <run-id> --lines 120-531
29
+ jevprune gain
30
+ ```
31
+
32
+ `run` executes the command with the given arguments (no shell), captures stdout and stderr in arrival order, prints the kept lines and exits with the command's exit code. `select` prunes a file or stdin. `show` prints an exact range of a saved run. `gain` sums what pruning saved so far on this machine.
33
+
34
+ The task comes from `--task`, else `JEVPRUNE_TASK`, else (inside the Claude Code plugin) the first prompt of the session, else the command itself.
35
+
36
+ ## Demo
37
+
38
+ ![jevprune demo](docs/demo.gif)
39
+
40
+ The fixture `test/fixtures/npm-test.log` is a 2,979-line vitest run with one failing test in `src/auth/login.test.ts` and a handful of slow tests. Same log, two tasks.
41
+
42
+ ```sh
43
+ jevprune select --task "fix the failing auth test" --command "npm test" --file test/fixtures/npm-test.log
44
+ ```
45
+
46
+ ```
47
+ [jevprune: 53 lines dropped, run mu6ln7nv-3cb9, lines 1-53]
48
+ stderr | src/auth/oauth.test.ts > oauth > paginates the sort order
49
+ [jevprune: 456 lines dropped, run mu6ln7nv-3cb9, lines 55-510]
50
+ ✓ src/auth/login.test.ts > login > merges a stale session 3ms
51
+ ✓ src/auth/login.test.ts > login > clears unicode input 3ms
52
+ ✓ src/auth/login.test.ts > login > ignores the previous state 2ms
53
+ × src/auth/login.test.ts > login > rejects an expired session token 14ms
54
+ ✓ src/auth/login.test.ts > login > updates the locale 3ms
55
+ ✓ src/auth/login.test.ts > login > keeps the default value 3ms
56
+ ✓ src/auth/login.test.ts > login > computes a network failure 18ms
57
+ [jevprune: 724 lines dropped, run mu6ln7nv-3cb9, lines 518-1241]
58
+ stderr | src/notifications/push.test.ts > push > paginates duplicate entries
59
+ Warning: An update to Form inside a test was not wrapped in act(...).
60
+ [jevprune: 1696 lines dropped, run mu6ln7nv-3cb9, lines 1244-2939]
61
+ ✓ src/components/toast.test.ts > toast > restores a stale session 5ms
62
+ ✓ src/components/toast.test.ts > toast > ignores a negative quantity 9ms
63
+ ✓ src/components/toast.test.ts > toast > emits an empty input 9ms
64
+ ✓ src/components/toast.test.ts > toast > emits an unknown id 12ms
65
+ ✓ src/components/toast.test.ts > toast > formats the previous state 1ms
66
+ ✓ src/components/toast.test.ts > toast > filters the cached result 4ms
67
+ ✓ src/components/toast.test.ts > toast > ignores a network failure 18ms
68
+ ✓ src/components/toast.test.ts > toast > clears a large payload 18ms
69
+ ✓ src/components/toast.test.ts > toast > merges a partial update 0ms
70
+ ✓ src/components/toast.test.ts > toast > clears leading whitespace 7ms
71
+ ✓ src/components/toast.test.ts > toast > filters a stale session 25ms
72
+ ✓ src/components/toast.test.ts > toast > returns a negative quantity 12ms
73
+
74
+ ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Failed Tests 1 ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
75
+
76
+ FAIL src/auth/login.test.ts > login > rejects an expired session token
77
+ AssertionError: expected 200 to be 401 // Object.is equality
78
+
79
+ - Expected
80
+ + Received
81
+
82
+ - 401
83
+ + 200
84
+
85
+ ❯ src/auth/login.test.ts:88:29
86
+ 86| const response = await login({ token: expiredToken });
87
+ 87|
88
+ 88| expect(response.status).toBe(401);
89
+ | ^
90
+ 89| expect(response.body.error).toBe("session expired");
91
+ 90| });
92
+
93
+ ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯
94
+
95
+
96
+ Test Files 1 failed | 94 passed (95)
97
+ Tests 1 failed | 2914 passed (2915)
98
+ Start at 09:12:04
99
+ Duration 41.20s (transform 4.11s, setup 1.62s, collect 12.30s, tests 33.94s, environment 9.81s)
100
+
101
+ jevprune: 2,979 → 54 lines, full output ~/.jevprune/runs/mu6ln7nv-3cb9.log
102
+ ```
103
+
104
+ ```sh
105
+ jevprune select --task "why is the build slow" --command "npm test" --file test/fixtures/npm-test.log
106
+ ```
107
+
108
+ Lines 53 to 78 of the 347 it printed, and the footer:
109
+
110
+ ```
111
+ ✓ src/billing/refunds.test.ts > refunds > rejects a negative quantity 12ms
112
+ ✓ src/billing/refunds.test.ts > refunds > clears a stale session 1170ms
113
+ [jevprune: 4 lines dropped, run mu6ln5j9-4968, lines 844-847]
114
+ ✓ src/billing/refunds.test.ts > refunds > keeps an empty input 1282ms
115
+ ✓ src/billing/refunds.test.ts > refunds > updates a trailing slash 1083ms
116
+ ✓ src/billing/refunds.test.ts > refunds > ignores an empty input 2ms
117
+ ✓ src/billing/refunds.test.ts > refunds > sorts an expired token 7ms
118
+ ✓ src/billing/refunds.test.ts > refunds > sorts a missing field 2216ms
119
+ ✓ src/billing/refunds.test.ts > refunds > renders a network failure 25ms
120
+ ✓ src/billing/refunds.test.ts > refunds > parses an expired token 1904ms
121
+ ✓ src/billing/refunds.test.ts > refunds > merges an empty input 7ms
122
+ ✓ src/billing/refunds.test.ts > refunds > parses the sort order 1013ms
123
+ [jevprune: 9 lines dropped, run mu6ln5j9-4968, lines 857-865]
124
+ ✓ src/hooks/use-form.test.ts > use-form > keeps a network failure 1610ms
125
+ ✓ src/hooks/use-form.test.ts > use-form > paginates a partial update 2095ms
126
+ ✓ src/hooks/use-form.test.ts > use-form > keeps the cached result 2512ms
127
+ ✓ src/hooks/use-form.test.ts > use-form > parses the locale 466ms
128
+ ✓ src/hooks/use-form.test.ts > use-form > updates a stale session 2ms
129
+ ✓ src/hooks/use-form.test.ts > use-form > merges a negative quantity 1ms
130
+ ✓ src/hooks/use-form.test.ts > use-form > restores an empty input 2073ms
131
+ ✓ src/hooks/use-form.test.ts > use-form > filters a missing field 2203ms
132
+ [jevprune: 4 lines dropped, run mu6ln5j9-4968, lines 874-877]
133
+ ✓ src/hooks/use-form.test.ts > use-form > paginates the previous state 483ms
134
+ [jevprune: 8 lines dropped, run mu6ln5j9-4968, lines 879-886]
135
+ ✓ src/hooks/use-form.test.ts > use-form > validates a large payload 1372ms
136
+ [jevprune: 6 lines dropped, run mu6ln5j9-4968, lines 888-893]
137
+ ```
138
+
139
+ ```
140
+ jevprune: 2,979 → 347 lines, full output ~/.jevprune/runs/mu6ln5j9-4968.log
141
+ ```
142
+
143
+ The first task keeps the failure block and the auth lines around it. The second keeps the timing lines instead. Neither view rewrote a line. A dropped section comes back exactly:
144
+
145
+ ```sh
146
+ jevprune show mu6ln7nv-3cb9 --lines 55-57
147
+ ```
148
+
149
+ ```
150
+ [deprecation] `fetchJson` is deprecated, use `http.get` instead
151
+
152
+ ✓ src/api/errors.test.ts > errors > merges the sort order 3ms
153
+ ```
154
+
155
+ ## What is kept
156
+
157
+ 1. 60 lines or fewer: everything, untouched, no request made.
158
+ 2. The command failed (non-zero exit or signal): everything, untouched. Failure output is evidence and is never pruned.
159
+ 3. Otherwise, before Jev: the last 40 lines, every line matching an error signature (`FAIL`, `error:`, `AssertionError`, `Traceback`, stack frames, `exited with code`, and similar), and the 3 lines on each side of it. Repeated identical signature lines count once.
160
+ 4. The remaining lines go to Jev in windows, one yes/no question per line: is this line needed for the task? Lines scoring at or above the threshold (default 0.3) are kept.
161
+ 5. Dropped runs shorter than 3 lines are kept. Longer ones collapse into one marker:
162
+
163
+ ```
164
+ [jevprune: 412 lines dropped, run m1xk2p7a-3f9c, lines 120-531]
165
+ ```
166
+
167
+ The last line of the output is the footer:
168
+
169
+ ```
170
+ jevprune: 3,104 → 88 lines, exit 0, full output ~/.jevprune/runs/m1xk2p7a-3f9c.log
171
+ ```
172
+
173
+ ## The exact-text guarantee
174
+
175
+ jevprune never rewrites, summarizes or reorders a line. Every line it prints is byte-identical to a line the command wrote, in the original order. The only text it adds is the collapse marker and the footer. The full output is on disk before any decision is made, and `jevprune show <id> --lines A-B` prints any range back exactly.
176
+
177
+ This is not lossless. A dropped line may have mattered. Retained text is exact; dropped text is recoverable; a wrong drop is possible. When a line you expect is missing, run `show` on the marker's range before concluding it is absent.
178
+
179
+ ## Fallback
180
+
181
+ No key, a rejected key, a rate limit, an outage, a timeout or a malformed answer never fails the command. jevprune keeps the deterministic set plus the first 40 and last 40 lines and says so in the footer:
182
+
183
+ ```
184
+ jevprune: fallback (no Jev: timeout), 3,104 → 83 lines, exit 0, full output ~/.jevprune/runs/<id>.log
185
+ ```
186
+
187
+ ## Claude Code plugin
188
+
189
+ A `PreToolUse` hook on `Bash` rewrites the command to `jevprune run --hook --transcript <path> -- bash -c '<command>'`. `bash -c` keeps pipelines, redirects and quoting as the agent wrote them; jevprune only owns capture and selection.
190
+
191
+ The hook leaves a command alone when it is already wrapped, runs in the background, ends with `&`, changes shell state (`cd`, `export`, `source`, `.`, `unset`, `alias`, `set`, `eval`, `exec`, `pushd`, `popd`), starts an interactive program (`vim`, `less`, `ssh`, `sudo`, `top`, `tmux`, `claude`, and similar), starts a REPL or shell with no arguments (`python`, `node`, `bash`, `psql`, `gh`), runs `docker exec` or `docker run` with a terminal flag, follows a file with `tail -f`, or starts with an allowlisted prefix (`cd`, `ls`, `pwd`, `echo`, `git status`, `git add`, `git commit`, `git log`, `git diff --stat`, `which`, `mkdir`, `touch`, `true`, `test`, `[`).
192
+
193
+ Set `"autoWrap": false` in `~/.jevprune/config.json` to turn the rewrite off. The bundled skill then tells the agent to call `jevprune run` itself for long commands.
194
+
195
+ ## Configuration
196
+
197
+ `~/.jevprune/config.json`, all keys optional:
198
+
199
+ | key | default | meaning |
200
+ |---|---|---|
201
+ | `threshold` | `0.3` | minimum Jev score to keep a line |
202
+ | `fastPathLines` | `60` | outputs up to this many lines pass through untouched |
203
+ | `tailLines` | `40` | lines always kept at the end |
204
+ | `headLines` | `40` | lines kept at the start in fallback |
205
+ | `contextLines` | `3` | lines kept around each error signature |
206
+ | `minCollapseLines` | `3` | shorter dropped runs are kept instead of collapsed |
207
+ | `windowTokens` | `25000` | estimated token budget per Jev request |
208
+ | `windowTimeoutMs` | `10000` | per-window timeout before fallback |
209
+ | `concurrency` | `4` | Jev requests in flight |
210
+ | `maxPruneBytes` | `16777216` | larger outputs use fallback on the head and tail |
211
+ | `retention.maxRuns` | `200` | saved runs kept |
212
+ | `retention.maxBytes` | `268435456` | total bytes of saved runs kept |
213
+ | `autoWrap` | `true` | plugin rewrites Bash commands |
214
+ | `allowlist` | see above | command prefixes the plugin never wraps |
215
+
216
+ Environment: `TYPESAFE_API_KEY`, `TYPESAFE_BASE_URL`, `JEVPRUNE_TASK`, `JEVPRUNE_HOME` (default `~/.jevprune`).
217
+
218
+ The defaults were set on real outputs from one machine (a vitest run, a Next.js build, a docker compose startup, a pytest run, a cargo build). They are a starting point.
219
+
220
+ ## Library
221
+
222
+ ```ts
223
+ import { pruneOutput } from "jevprune";
224
+
225
+ const result = await pruneOutput({
226
+ text: output,
227
+ task: "fix the failing auth test",
228
+ command: "npm test",
229
+ exitCode: 0,
230
+ });
231
+ result.kept; // exact retained text with collapse markers
232
+ result.dropped; // [{ from, to, count }]
233
+ result.runId; // for `jevprune show`
234
+ result.mode; // "fast-path" | "passthrough" | "jev" | "fallback"
235
+ ```
236
+
237
+ `pruneStream` takes a readable stream instead of `text`. Pass `client` to supply your own Jev client (a `FakeJevClient` is exported for tests) and `save: false` to skip the run store.
238
+
239
+ ## Limits
240
+
241
+ - Not lossless. See the guarantee above.
242
+ - Questions cost tokens too. A 3,000-line output is roughly 5 to 10 Jev requests depending on line length. Output tokens are free; input is billed by TypeSafe.
243
+ - Needs an early-access TypeSafe key. Without one, every pruned run is the head-and-tail fallback.
244
+ - `run` takes an executable and arguments, no shell string. Use `bash -c '...'` for pipelines, which is what the plugin does.
245
+ - Successful outputs over 16 MiB are pruned by head and tail only. Failed ones pass through in full.
246
+ - Output that is not valid UTF-8 passes through untouched.
247
+ - Everything jevprune saves stays under `~/.jevprune`. The task text is stored there with each run and nowhere else.
248
+
249
+ ## Neighbors
250
+
251
+ - [fast-jev-compaction](https://github.com/tamaratran/fast-jev-compaction) replaces Claude Code's compaction summary with Jev decisions over the whole conversation. jevprune prunes one command's output at the moment it is produced, before it enters context. They compose. An open pull request there trims Bash output in 20-line chunks behind an early-access function-hook flag; jevprune scores single lines against the task, keeps error signatures with context before Jev is asked, runs on the documented `PreToolUse` hook, and works as a CLI and library outside Claude Code.
252
+ - [RTK](https://github.com/rtk-ai/rtk) filters output with rules written per tool. jevprune has no per-tool rules; the task decides.
253
+ - [Squeez](https://github.com/KRLabsOrg/squeez) selects task-relevant lines with a local 2B model, in Python. jevprune does the same job through the TypeSafe API, with exact recall of dropped ranges and a run store.
254
+
255
+ ## License
256
+
257
+ MIT