chisle 3.2.1 → 3.3.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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chisle",
3
- "version": "3.2.1",
3
+ "version": "3.3.0",
4
4
  "description": "Maximum-efficiency dev mode. Zero-fluff prose combined with YAGNI/ladder-first code decisions. One persona: the senior dev who deletes code for fun and bills by the syllable.",
5
5
  "hooks": {
6
6
  "SessionStart": [
package/README.md CHANGED
@@ -340,7 +340,7 @@ node benchmarks/replay-compress.js # Claude Code
340
340
  node benchmarks/replay-compress.js pi # Pi
341
341
  ```
342
342
 
343
- Outputs over 8k chars are elided. `stop chisle`, `CHISLE_COMPRESS=0`, `CHISLE_COMPRESS_SCRUB=0`, `CHISLE_COMPRESS_DEDUP=0`. Every tier has an off switch.
343
+ Outputs over 8k chars are elided, and the elided original spills to `<config>/chisle-spill/` so the dropped middle stays reachable. The marker carries the path, so recovering one line is a targeted grep rather than a re-run of the command, which matters most when the command is not idempotent: a test run, a build, a `git log` at a moment in time. The newest 40 spills are kept, owner-readable only. `stop chisle`, `CHISLE_COMPRESS=0`, `CHISLE_COMPRESS_SCRUB=0`, `CHISLE_COMPRESS_DEDUP=0`, `CHISLE_COMPRESS_SPILL=0`. Every tier has an off switch.
344
344
 
345
345
  ### Pi arm: where the output axis lost
346
346
 
package/bin/install.js CHANGED
@@ -458,8 +458,10 @@ function uninstall(ctx) {
458
458
  touched++;
459
459
  }
460
460
  }
461
- const hooksDst = path.join(cfg, 'chisle-hooks');
462
- if (fs.existsSync(hooksDst)) { if (!opts.dryRun) fs.rmSync(hooksDst, { recursive: true, force: true }); note(` removed ${hooksDst}`); touched++; }
461
+ for (const d of ['chisle-hooks', 'chisle-spill']) {
462
+ const dst = path.join(cfg, d);
463
+ if (fs.existsSync(dst)) { if (!opts.dryRun) fs.rmSync(dst, { recursive: true, force: true }); note(` removed ${dst}`); touched++; }
464
+ }
463
465
 
464
466
  for (const f of ['.chisle-active', '.chisle-session-turns', '.chisle-statusline-suffix',
465
467
  '.chisle-compress-stats.json', '.chisle-compress-last.json', '.chisle-update-check.json']) {
@@ -31,8 +31,11 @@
31
31
  // CHISLE_COMPRESS_HEAD_LINES — lines kept from the top
32
32
  // CHISLE_COMPRESS_TAIL_LINES — lines kept from the bottom
33
33
  // CHISLE_COMPRESS_TOOLS=Bash,Grep — override the tool allowlist
34
+ // CHISLE_COMPRESS_SPILL=0 — elide without writing the recovery copy
34
35
  //
35
36
  // Savings accrue in <claudeDir>/.chisle-compress-stats.json for the statusline.
37
+ // Elided originals spill to <claudeDir>/chisle-spill/ so the dropped middle can
38
+ // be grepped back instead of re-running the command; the newest 40 are kept.
36
39
 
37
40
  const fs = require('fs');
38
41
  const path = require('path');
@@ -174,17 +177,59 @@ function dedupCheck(toolName, text, sessionId, toolUseId) {
174
177
  // ── Tier 2: elision ──────────────────────────────────────────────────────────
175
178
  // Keep head + tail; salvage error-looking lines from the elided middle so the
176
179
  // one line that mattered in a 3000-line build log survives the cut.
177
- function compress(text, limits) {
180
+ // ── recoverable elision ─────────────────────────────────────────────────────
181
+ // Elision used to destroy the middle. If the agent then needed a line from it,
182
+ // its only recourse was re-running the command: more expensive than the elision
183
+ // saved, and wrong outright when the command is not idempotent (a test run, a
184
+ // build, `git log` at a moment in time). So the full text spills to disk first
185
+ // and the marker carries the path. Recovery becomes a targeted grep instead of
186
+ // a re-run. Best-effort throughout: if the spill fails, the elision still
187
+ // happens, it just loses the escape hatch.
188
+ const SPILL_DIR = 'chisle-spill';
189
+ const SPILL_KEEP = 40;
190
+
191
+ function spillDir() {
192
+ return path.join(getClaudeDir(), SPILL_DIR);
193
+ }
194
+
195
+ // Keep the directory bounded. Oldest-first by mtime; cheap enough at this size
196
+ // and it runs at most once per elided output.
197
+ function pruneSpill(dir) {
198
+ try {
199
+ const files = fs.readdirSync(dir)
200
+ .filter(f => f.endsWith('.txt'))
201
+ .map(f => { const p = path.join(dir, f); return { p, t: fs.statSync(p).mtimeMs }; })
202
+ .sort((a, b) => b.t - a.t);
203
+ for (const f of files.slice(SPILL_KEEP)) { try { fs.unlinkSync(f.p); } catch (e) {} }
204
+ } catch (e) {}
205
+ }
206
+
207
+ function spill(text, toolName) {
208
+ if (process.env.CHISLE_COMPRESS_SPILL === '0') return null;
209
+ try {
210
+ const dir = spillDir();
211
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
212
+ const hash = crypto.createHash('sha256').update(text).digest('hex').slice(0, 12);
213
+ const safeTool = String(toolName || 'tool').replace(/[^A-Za-z0-9_-]/g, '') || 'tool';
214
+ const file = path.join(dir, `${safeTool}-${hash}.txt`);
215
+ if (!fs.existsSync(file)) fs.writeFileSync(file, text, { mode: 0o600 });
216
+ pruneSpill(dir);
217
+ return file;
218
+ } catch (e) { return null; }
219
+ }
220
+
221
+ function compress(text, limits, spillPath) {
178
222
  const { maxChars, headLines, tailLines } = limits;
179
223
  const lines = text.split('\n');
224
+ const recover = spillPath ? ' Full output: ' + spillPath + ' (grep it, do not re-run)' : '';
180
225
 
181
226
  if (lines.length <= headLines + tailLines) {
182
- // Big in bytes, few lines (one giant line) — hard char cut.
227
+ // Big in bytes, few lines (one giant line): hard char cut.
183
228
  const keep = Math.floor(maxChars / 2);
184
229
  const elided = text.length - 2 * keep;
185
230
  if (elided <= 0) return null;
186
231
  return text.slice(0, keep) +
187
- '\n... [chisle: elided ' + elided.toLocaleString('en-US') + ' chars from the middle] ...\n' +
232
+ '\n... [chisle: elided ' + elided.toLocaleString('en-US') + ' chars from the middle.' + recover + '] ...\n' +
188
233
  text.slice(-keep);
189
234
  }
190
235
 
@@ -199,8 +244,9 @@ function compress(text, limits) {
199
244
  }
200
245
 
201
246
  const marker = '... [chisle: elided ' + middle.length.toLocaleString('en-US') +
202
- ' lines — kept first ' + headLines + ', last ' + tailLines +
203
- (salvaged.length ? ', and ' + salvaged.length + ' error-like line(s) below' : '') + '] ...';
247
+ ' lines, kept first ' + headLines + ', last ' + tailLines +
248
+ (salvaged.length ? ', and ' + salvaged.length + ' error-like line(s) below' : '') +
249
+ '.' + recover + '] ...';
204
250
 
205
251
  const out = head.concat([marker], salvaged, tail).join('\n');
206
252
  return out.length < text.length ? out : null;
@@ -227,11 +273,11 @@ function recordSavings(saved) {
227
273
 
228
274
  // Scrub + elide on plain text → transformed text, or null if no meaningful
229
275
  // win. Stateless — this is what the replay benchmark measures.
230
- function transform(text, limits) {
276
+ function transform(text, limits, toolName) {
231
277
  if (!text || text.length <= SCRUB_MIN) return null;
232
278
  let t = process.env.CHISLE_COMPRESS_SCRUB === '0' ? text : scrub(text);
233
279
  if (t.length > limits.maxChars) {
234
- const elided = compress(t, limits);
280
+ const elided = compress(t, limits, toolName ? spill(t, toolName) : null);
235
281
  if (elided != null) t = elided;
236
282
  }
237
283
  return text.length - t.length >= MIN_WIN ? t : null;
@@ -275,7 +321,7 @@ function processPayload(payload, mode) {
275
321
  if (!text) return null;
276
322
  const dup = dedupCheck(payload.tool_name, text, payload.session_id, payload.tool_use_id);
277
323
  if (dup != null && dup.length < text.length) return dup;
278
- return transform(text, limitsFor());
324
+ return transform(text, limitsFor(), payload.tool_name);
279
325
  }
280
326
 
281
327
  function main() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chisle",
3
- "version": "3.2.1",
3
+ "version": "3.3.0",
4
4
  "description": "Maximum-efficiency dev mode for AI coding agents. Zero-fluff prose, YAGNI-first code, and safe tool-output compression.",
5
5
  "keywords": [
6
6
  "claude-code",
@@ -166,7 +166,7 @@ function chisleExtension(pi) {
166
166
  lastByToolBlock.set(key, hash);
167
167
  if (previous === hash) updated = duplicateMarker(event.toolName, text, 'earlier');
168
168
  }
169
- if (updated == null) updated = transform(text, limitsFor());
169
+ if (updated == null) updated = transform(text, limitsFor(), event.toolName);
170
170
  if (updated == null || updated.length >= text.length) return block;
171
171
  changed = true;
172
172
  savedChars += text.length - updated.length;