chisle 3.0.0 → 3.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/.claude-plugin/plugin.json +1 -1
- package/README.md +30 -18
- package/bin/install.js +92 -40
- package/hooks/chisle-compress-output.js +26 -12
- package/hooks/chisle-mode-tracker.js +6 -32
- package/hooks/chisle-mode.js +19 -0
- package/package.json +13 -6
- package/pi-extension/index.js +188 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "chisle",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.1.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
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
<p align="center">
|
|
16
16
|
<a href="https://www.npmjs.com/package/chisle"><img src="https://img.shields.io/npm/v/chisle?style=flat-square&color=d78a3c" alt="npm version"></a>
|
|
17
|
-
<img src="https://img.shields.io/badge/works%20with-
|
|
17
|
+
<img src="https://img.shields.io/badge/works%20with-9%20agents-d78a3c?style=flat-square" alt="Works with 9 agents">
|
|
18
18
|
<a href="https://github.com/JayPokale/Chisle/actions/workflows/test.yml"><img src="https://img.shields.io/github/actions/workflow/status/JayPokale/Chisle/test.yml?style=flat-square&label=CI" alt="CI"></a>
|
|
19
19
|
<img src="https://img.shields.io/badge/deps-0-2da44e?style=flat-square" alt="Zero deps">
|
|
20
20
|
<img src="https://img.shields.io/badge/license-MIT-d78a3c?style=flat-square" alt="MIT">
|
|
@@ -79,30 +79,30 @@ Most efficiency tools compress one thing. Chisle compresses **three**:
|
|
|
79
79
|
|---|---|---|
|
|
80
80
|
| **Output: prose** | filler, hedging, manufactured structure | zero-fluff ruleset, injected per session |
|
|
81
81
|
| **Output: code** | speculative abstractions, unrequested boilerplate | YAGNI efficiency ladder |
|
|
82
|
-
| **Input: context** | oversized tool output flooding the window | `PostToolUse`
|
|
82
|
+
| **Input: context** | oversized tool output flooding the window | Claude `PostToolUse` / Pi `tool_result` — scrub, elide, dedup — plus prevention rules |
|
|
83
83
|
|
|
84
84
|
Where each one attaches to a session:
|
|
85
85
|
|
|
86
86
|
```mermaid
|
|
87
87
|
flowchart LR
|
|
88
88
|
subgraph S["Session start"]
|
|
89
|
-
H1["
|
|
89
|
+
H1["Ruleset injection<br/>once per active session"]
|
|
90
90
|
end
|
|
91
91
|
subgraph T["Every turn"]
|
|
92
|
-
H2["
|
|
92
|
+
H2["Mode tracking<br/>Claude + Pi"]
|
|
93
93
|
end
|
|
94
94
|
subgraph L["Every tool call"]
|
|
95
|
-
H3["PostToolUse
|
|
95
|
+
H3["PostToolUse / tool_result<br/>scrub → elide → dedup"]
|
|
96
96
|
end
|
|
97
97
|
|
|
98
98
|
H1 --> M(["Model"])
|
|
99
99
|
H2 --> M
|
|
100
100
|
M -->|writes| O["Output:<br/>terser prose,<br/>YAGNI-first code"]
|
|
101
|
-
M -->|calls a tool| TOOL[["Bash /
|
|
101
|
+
M -->|calls a tool| TOOL[["Bash / grep / web / extension tools"]]
|
|
102
102
|
TOOL -->|raw output| H3
|
|
103
103
|
H3 -->|"compressed, rebuilt into<br/>the tool's own shape"| M
|
|
104
104
|
|
|
105
|
-
RE["Read / Edit"] -.->|"never touched —<br/>exact bytes feed later edits"| M
|
|
105
|
+
RE["Read / Edit / Write"] -.->|"never touched —<br/>exact bytes feed later edits"| M
|
|
106
106
|
|
|
107
107
|
style M fill:#1f2937,stroke:#d78a3c,color:#e6edf3
|
|
108
108
|
style O fill:#14532d,stroke:#2da44e,color:#e6edf3
|
|
@@ -110,7 +110,7 @@ flowchart LR
|
|
|
110
110
|
style RE fill:#3f1d1d,stroke:#cf3b3b,color:#e6edf3
|
|
111
111
|
```
|
|
112
112
|
|
|
113
|
-
The loop on the right is the input axis: tool output is billed again on *every* later request in the session, so shrinking it once pays repeatedly. `Read` and `
|
|
113
|
+
The loop on the right is the input axis: tool output is billed again on *every* later request in the session, so shrinking it once pays repeatedly. `Read`, `Edit`, and `Write` are deliberately outside it.
|
|
114
114
|
|
|
115
115
|
<p align="center">
|
|
116
116
|
<img src="assets/thumbnail.png" width="820" alt="Chisle — less tokens, same results. Four parts: output compressor (compress noisy tool outputs), context diet (read only what's actually relevant), terse persona (short, focused, YAGNI-first), YAGNI ladder (do less, reuse more, build only when needed).">
|
|
@@ -122,7 +122,7 @@ Every "be concise" tool has a worst day — the day it makes the model write *mo
|
|
|
122
122
|
|
|
123
123
|
## Install
|
|
124
124
|
|
|
125
|
-
One command. Auto-detects your agents (Claude Code, Cursor, Windsurf, Cline, Kiro, Codex, Gemini, Copilot) and wires each one. `--uninstall` puts everything back.
|
|
125
|
+
One command. Auto-detects your agents (Claude Code, Pi, Cursor, Windsurf, Cline, Kiro, Codex, Gemini, Copilot) and wires each one. `--uninstall` puts everything back.
|
|
126
126
|
|
|
127
127
|
```bash
|
|
128
128
|
npx chisle
|
|
@@ -138,9 +138,9 @@ curl -fsSL https://raw.githubusercontent.com/JayPokale/Chisle/main/install.sh |
|
|
|
138
138
|
irm https://raw.githubusercontent.com/JayPokale/Chisle/main/install.ps1 | iex
|
|
139
139
|
```
|
|
140
140
|
|
|
141
|
-
Preview first with `npx chisle --dry-run`, scope with `--only claude`, see everything with `npx chisle --help`. Remove with `npx chisle --uninstall`.
|
|
141
|
+
Preview first with `npx chisle --dry-run`, scope with `--only claude` or `--only pi`, see everything with `npx chisle --help`. Remove with `npx chisle --uninstall`.
|
|
142
142
|
|
|
143
|
-
**Requirements:** Node ≥18 (installer / `npx`) · Claude Code for `/chisle` toggling and input-side compression — the always-on ruleset still ships to every other agent.
|
|
143
|
+
**Requirements:** Node ≥18 (installer / `npx`) · Claude Code or Pi for `/chisle` toggling and input-side compression — the always-on ruleset still ships to every other agent.
|
|
144
144
|
|
|
145
145
|
### Upgrading
|
|
146
146
|
|
|
@@ -167,17 +167,26 @@ claude plugin marketplace add JayPokale/Chisle # register the marketplace
|
|
|
167
167
|
claude plugin install chisle@chisle # enable the plugin
|
|
168
168
|
```
|
|
169
169
|
|
|
170
|
+
### Pi package
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
pi install npm:chisle
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
The package loads the zero-dependency extension and `chisle` skill globally. Pi extensions run with your user permissions; review the source before installation. Project-local installs (`pi install -l npm:chisle`) load only after you trust that project.
|
|
177
|
+
|
|
170
178
|
### See what it would do, before it does it
|
|
171
179
|
|
|
172
180
|
```bash
|
|
173
181
|
npx chisle --dry-run # prints every file it would touch, changes nothing
|
|
174
182
|
```
|
|
175
183
|
|
|
176
|
-
Want the savings measured on your own work rather than ours? Clone the repo and replay the compressor over
|
|
184
|
+
Want the savings measured on your own work rather than ours? Clone the repo and replay the compressor over local transcripts — it reads them locally, writes nothing, and reports the input the hook would have stripped:
|
|
177
185
|
|
|
178
186
|
```bash
|
|
179
187
|
git clone https://github.com/JayPokale/Chisle && cd Chisle
|
|
180
|
-
node benchmarks/replay-compress.js
|
|
188
|
+
node benchmarks/replay-compress.js # Claude Code
|
|
189
|
+
node benchmarks/replay-compress.js pi # Pi; marginal over Pi's native truncation
|
|
181
190
|
```
|
|
182
191
|
|
|
183
192
|
---
|
|
@@ -283,7 +292,7 @@ SUITE=large bash benchmarks/run-live.sh <model> benchmarks/results/raw-large
|
|
|
283
292
|
|
|
284
293
|
The row that matters is the last one. Every tool here looks good on its best day; the numbers above are the only ones in this class published alongside the run that went wrong. [Full comparison →](docs/comparison.md)
|
|
285
294
|
|
|
286
|
-
### Input axis — tool-output compression (Claude Code)
|
|
295
|
+
### Input axis — tool-output compression (Claude Code + Pi)
|
|
287
296
|
|
|
288
297
|
Measured over 171 real sessions ([receipts](benchmarks/results/2026-07-07-input-axis.md)): tool output is **67.5%** of context content, and every byte of it is re-billed on *every subsequent request* in the session (median: 171 requests). A `PostToolUse` hook shrinks it before the model reads it — deterministic, zero LLM, zero network:
|
|
289
298
|
|
|
@@ -293,10 +302,13 @@ Measured over 171 real sessions ([receipts](benchmarks/results/2026-07-07-input-
|
|
|
293
302
|
| **elide** | oversized output → head + tail, error-like lines salvaged from the cut | bounded, guarded |
|
|
294
303
|
| **dedup** | byte-identical repeat of a tool's previous output (same session) → one-line marker | none — the copy is already in context |
|
|
295
304
|
|
|
296
|
-
Replayed over the same 171 sessions: **~61k tokens** saved one-shot, ~46% off every eligible output — floor, not estimate, since each saved byte also stops being re-sent on every later request. Correctness rules: allowlist only (`Bash`, `Agent`, `WebFetch`, `WebSearch`, `Grep`, `Glob`, `mcp__*`) — never `Read`/`Edit`, whose exact bytes feed later edits. Honest ledger: dedup scored **0 hits** on this corpus (rtk-filtered at source); it's kept for the test-rerun case, kill-switchable, and labeled speculative until it earns a number.
|
|
305
|
+
Replayed over the same 171 Claude Code sessions: **~61k tokens** saved one-shot, ~46% off every eligible output — floor, not estimate, since each saved byte also stops being re-sent on every later request. Correctness rules: allowlist only (`Bash`, `Agent`, `WebFetch`, `WebSearch`, `Grep`, `Glob`, `mcp__*`) — never `Read`/`Edit`, whose exact bytes feed later edits. Honest ledger: dedup scored **0 hits** on this corpus (rtk-filtered at source); it's kept for the test-rerun case, kill-switchable, and labeled speculative until it earns a number.
|
|
306
|
+
|
|
307
|
+
Pi already truncates built-in output at 50KB/2,000 lines. Replay over 8,044 persisted Pi results measured the compressor's **marginal** saving after that truncation: **27.4% of tool-output chars** ([receipt + raw live Pi arm](benchmarks/results/2026-09-11-pi.md)). Pi compresses `bash`, `powershell`, `grep`, `find`, `ls`, MCP, and explicitly allowlisted extension-tool results; `read`/`edit`/`write` remain untouched. `CHISLE_COMPRESS_TOOLS` supplies the same explicit override for runtime and replay. Runtime dedup only compares earlier turns, so concurrently completed sibling calls cannot dedup one another.
|
|
297
308
|
|
|
298
309
|
```bash
|
|
299
|
-
node benchmarks/replay-compress.js
|
|
310
|
+
node benchmarks/replay-compress.js # Claude Code
|
|
311
|
+
node benchmarks/replay-compress.js pi # Pi
|
|
300
312
|
```
|
|
301
313
|
|
|
302
314
|
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.
|
|
@@ -404,12 +416,12 @@ Chisle borrows the best published token-saving techniques and implements the one
|
|
|
404
416
|
|
|
405
417
|
## Multi-agent
|
|
406
418
|
|
|
407
|
-
|
|
419
|
+
Ships to nine agents: Claude Code and Pi get both axes, live `/chisle` toggling, and a status badge; Cursor, Windsurf, Cline, Kiro, Codex, Gemini, and Copilot get the always-on ruleset. Per-agent static copies come from `scripts/build-rules.js`; Pi uses its package extension plus the Agent Skills standard. See [`docs/agent-portability.md`](./docs/agent-portability.md).
|
|
408
420
|
|
|
409
421
|
## FAQ
|
|
410
422
|
|
|
411
423
|
**Doesn't injecting a persona every turn cost tokens?**
|
|
412
|
-
|
|
424
|
+
Claude Code uses a ruleset at session start (~1.6k tokens) plus a ~50-token reminder per turn. Pi injects one persistent rules message only when project `AGENTS.md` does not already provide it; resume/reload does not duplicate it, and compaction restores it only if removed.
|
|
413
425
|
|
|
414
426
|
Worth reading the dissent before you take that on faith: [@enc0ded](https://github.com/enc0ded) measured 173 of their own sessions ([#2](https://github.com/JayPokale/Chisle/issues/2)) and found the injection overhead roughly cancelling the compressor's savings — because the ruleset was being re-sent on every resume and clear, not just at startup. That re-injection is fixed, which removes most of the overhead they measured, but their wider point stands: prose is only ~25% of what the model emits, so the ceiling on the output axis is lower than the headline suggests, and on a one-line throwaway prompt the overhead still exceeds the saving.
|
|
415
427
|
|
package/bin/install.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// Detects the AI coding agents on this machine and installs Chisle for each:
|
|
5
5
|
// - Claude Code → plugin (marketplace add + install), fallback to standalone
|
|
6
6
|
// hooks + settings.json merge + statusline badge
|
|
7
|
+
// - Pi → Pi package (extension + skill)
|
|
7
8
|
// - Gemini CLI → gemini extensions install
|
|
8
9
|
// - Codex → fenced ruleset appended to ~/.codex/AGENTS.md
|
|
9
10
|
// - Cursor/Windsurf/Cline/Kiro/Copilot → project rule file dropped into CWD
|
|
@@ -39,6 +40,7 @@ const REPO_ROOT = path.resolve(__dirname, '..');
|
|
|
39
40
|
// (that's how Cursor/Cline/Copilot rules actually work).
|
|
40
41
|
const PROVIDERS = [
|
|
41
42
|
{ id: 'claude', label: 'Claude Code', scope: 'global', detect: 'cmd:claude' },
|
|
43
|
+
{ id: 'pi', label: 'Pi', scope: 'global', detect: 'cmd:pi' },
|
|
42
44
|
{ id: 'gemini', label: 'Gemini CLI', scope: 'global', detect: 'cmd:gemini' },
|
|
43
45
|
{ id: 'codex', label: 'Codex CLI', scope: 'global', detect: 'cmd:codex||dir:~/.codex' },
|
|
44
46
|
{ id: 'cursor', label: 'Cursor', scope: 'project', detect: 'cmd:cursor||dir:~/.cursor',
|
|
@@ -196,6 +198,18 @@ function installClaude(ctx) {
|
|
|
196
198
|
else results.failed.push(['claude-hooks', r]);
|
|
197
199
|
} else {
|
|
198
200
|
note(' hooks: plugin manifest handles SessionStart + UserPromptSubmit + PostToolUse');
|
|
201
|
+
// Plugin path won — drop any standalone wiring left by an earlier run, or
|
|
202
|
+
// both copies of every hook run on each event and the PostToolUse pair
|
|
203
|
+
// dedups first-seen output against itself.
|
|
204
|
+
const settingsPath = path.join(claudeDir(opts), 'settings.json');
|
|
205
|
+
const settings = SETTINGS.readSettings(settingsPath);
|
|
206
|
+
if (settings && SETTINGS.removeHooks(settings, 'chisle-') > 0) {
|
|
207
|
+
if (!opts.dryRun) {
|
|
208
|
+
SETTINGS.validateHookFields(settings);
|
|
209
|
+
SETTINGS.writeSettings(settingsPath, settings);
|
|
210
|
+
}
|
|
211
|
+
note(' removed superseded standalone hook entries from settings.json');
|
|
212
|
+
}
|
|
199
213
|
}
|
|
200
214
|
process.stdout.write('\n');
|
|
201
215
|
}
|
|
@@ -209,7 +223,7 @@ function installClaudeHooks(ctx) {
|
|
|
209
223
|
const hooksDst = path.join(cfg, 'chisle-hooks');
|
|
210
224
|
const settingsPath = path.join(cfg, 'settings.json');
|
|
211
225
|
const HOOK_FILES = ['package.json', 'chisle-config.js', 'chisle-activate.js',
|
|
212
|
-
'chisle-mode-tracker.js', 'chisle-compress-output.js',
|
|
226
|
+
'chisle-mode.js', 'chisle-mode-tracker.js', 'chisle-compress-output.js',
|
|
213
227
|
'chisle-statusline.sh', 'chisle-statusline.ps1'];
|
|
214
228
|
|
|
215
229
|
if (opts.dryRun) {
|
|
@@ -263,6 +277,30 @@ function installClaudeHooks(ctx) {
|
|
|
263
277
|
return 'ok';
|
|
264
278
|
}
|
|
265
279
|
|
|
280
|
+
// ── Pi ──────────────────────────────────────────────────────────────────────
|
|
281
|
+
function piSources(output) {
|
|
282
|
+
return String(output || '').split('\n').map(line => line.trim())
|
|
283
|
+
.filter(line => /^npm:chisle(?:@|$)/i.test(line) || /^git:.*github\.com\/JayPokale\/Chisle(?:@|$)/i.test(line))
|
|
284
|
+
.map(source => source.replace(/@[^/@]+$/, ''));
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function installPi(ctx) {
|
|
288
|
+
const { say, note, opts, results } = ctx;
|
|
289
|
+
results.detected++;
|
|
290
|
+
say('→ Pi detected');
|
|
291
|
+
if (!opts.force) {
|
|
292
|
+
const installed = piSources(capture('pi', ['list']).stdout);
|
|
293
|
+
if (installed.length) {
|
|
294
|
+
note(' chisle package already installed (use --force)');
|
|
295
|
+
results.skipped.push(['pi', 'already installed']); process.stdout.write('\n'); return;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
const r = run('pi', ['install', 'npm:chisle'], opts.dryRun);
|
|
299
|
+
if ((r.status || 0) === 0) results.installed.push('pi');
|
|
300
|
+
else results.failed.push(['pi', 'pi package install failed']);
|
|
301
|
+
process.stdout.write('\n');
|
|
302
|
+
}
|
|
303
|
+
|
|
266
304
|
// ── Gemini ──────────────────────────────────────────────────────────────────
|
|
267
305
|
function installGemini(ctx) {
|
|
268
306
|
const { say, note, opts, results } = ctx;
|
|
@@ -347,53 +385,66 @@ function uninstall(ctx) {
|
|
|
347
385
|
say(c.orange('Chisle uninstall'));
|
|
348
386
|
let touched = 0;
|
|
349
387
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
388
|
+
const wants = id => !opts.only.length || opts.only.includes(id);
|
|
389
|
+
|
|
390
|
+
if (wants('claude')) {
|
|
391
|
+
// Claude plugin
|
|
392
|
+
if (hasCmd('claude') && !opts.dryRun) {
|
|
393
|
+
const r = capture('claude', ['plugin', 'list']);
|
|
394
|
+
if (r.status === 0 && /chisle/i.test(r.stdout || '')) {
|
|
395
|
+
run('claude', ['plugin', 'uninstall', 'chisle@chisle'], opts.dryRun); touched++;
|
|
396
|
+
}
|
|
355
397
|
}
|
|
356
|
-
}
|
|
357
398
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
399
|
+
// Claude standalone hooks + statusline
|
|
400
|
+
const cfg = claudeDir(opts);
|
|
401
|
+
const settingsPath = path.join(cfg, 'settings.json');
|
|
402
|
+
const settings = SETTINGS.readSettings(settingsPath);
|
|
403
|
+
if (settings) {
|
|
404
|
+
const removed = SETTINGS.removeHooks(settings, 'chisle-');
|
|
405
|
+
let slRemoved = false;
|
|
406
|
+
if (settings.statusLine && typeof settings.statusLine.command === 'string'
|
|
407
|
+
&& settings.statusLine.command.includes('chisle-statusline')) { delete settings.statusLine; slRemoved = true; }
|
|
408
|
+
if (removed > 0 || slRemoved) {
|
|
409
|
+
if (!opts.dryRun) { SETTINGS.validateHookFields(settings); SETTINGS.writeSettings(settingsPath, settings); }
|
|
410
|
+
note(` removed ${removed} hook entr${removed === 1 ? 'y' : 'ies'}${slRemoved ? ' + statusline' : ''} from settings.json`);
|
|
411
|
+
touched++;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
const hooksDst = path.join(cfg, 'chisle-hooks');
|
|
415
|
+
if (fs.existsSync(hooksDst)) { if (!opts.dryRun) fs.rmSync(hooksDst, { recursive: true, force: true }); note(` removed ${hooksDst}`); touched++; }
|
|
416
|
+
|
|
417
|
+
for (const f of ['.chisle-active', '.chisle-session-turns', '.chisle-statusline-suffix',
|
|
418
|
+
'.chisle-compress-stats.json', '.chisle-compress-last.json', '.chisle-update-check.json']) {
|
|
419
|
+
const p = path.join(cfg, f);
|
|
420
|
+
if (fs.existsSync(p)) { if (!opts.dryRun) { try { fs.unlinkSync(p); } catch (_) {} } note(` removed ${p}`); touched++; }
|
|
371
421
|
}
|
|
372
422
|
}
|
|
373
|
-
const hooksDst = path.join(cfg, 'chisle-hooks');
|
|
374
|
-
if (fs.existsSync(hooksDst)) { if (!opts.dryRun) fs.rmSync(hooksDst, { recursive: true, force: true }); note(` removed ${hooksDst}`); touched++; }
|
|
375
423
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
424
|
+
if (wants('pi') && hasCmd('pi')) {
|
|
425
|
+
const sources = piSources(capture('pi', ['list']).stdout);
|
|
426
|
+
for (const source of sources) {
|
|
427
|
+
if ((run('pi', ['remove', source], opts.dryRun).status || 0) === 0) touched++;
|
|
428
|
+
}
|
|
381
429
|
}
|
|
382
430
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
431
|
+
if (wants('codex')) {
|
|
432
|
+
const codexMd = path.join(os.homedir(), '.codex', 'AGENTS.md');
|
|
433
|
+
if (fs.existsSync(codexMd)) {
|
|
434
|
+
const txt = fs.readFileSync(codexMd, 'utf8');
|
|
435
|
+
if (txt.includes(FENCE_BEGIN)) {
|
|
436
|
+
const stripped = txt.replace(new RegExp(`\\n?${FENCE_BEGIN}[\\s\\S]*?${FENCE_END}\\n?`), '\n').replace(/\n{3,}/g, '\n\n');
|
|
437
|
+
if (!opts.dryRun) fs.writeFileSync(codexMd, stripped, { mode: 0o644 });
|
|
438
|
+
note(` removed chisle block from ${codexMd}`); touched++;
|
|
439
|
+
}
|
|
391
440
|
}
|
|
392
441
|
}
|
|
393
442
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
443
|
+
if (!opts.only.length || opts.only.some(id => PROVIDERS.find(p => p.id === id).scope === 'project')) {
|
|
444
|
+
note('');
|
|
445
|
+
note('Project-scoped rule files (.cursor/, .windsurf/, .clinerules/, .kiro/, .github/copilot-instructions.md)');
|
|
446
|
+
note('live in your project repos — remove them per-project with git if you added them there.');
|
|
447
|
+
}
|
|
397
448
|
say(touched ? c.green(`\nUninstalled. ${touched} item(s) cleaned.`) : c.yellow('\nNothing to uninstall.'));
|
|
398
449
|
}
|
|
399
450
|
|
|
@@ -406,7 +457,7 @@ Usage:
|
|
|
406
457
|
|
|
407
458
|
Flags:
|
|
408
459
|
--list Detect agents and print them; install nothing
|
|
409
|
-
--only <id>
|
|
460
|
+
--only <id> Limit install/uninstall to one agent (repeatable)
|
|
410
461
|
--dry-run Print actions, change nothing
|
|
411
462
|
--force Reinstall / overwrite even if already present
|
|
412
463
|
--uninstall, -u Remove what chisle installed
|
|
@@ -463,6 +514,7 @@ function main() {
|
|
|
463
514
|
|
|
464
515
|
for (const p of targets) {
|
|
465
516
|
if (p.id === 'claude') installClaude(ctx);
|
|
517
|
+
else if (p.id === 'pi') installPi(ctx);
|
|
466
518
|
else if (p.id === 'gemini') installGemini(ctx);
|
|
467
519
|
else if (p.id === 'codex') installCodex(ctx);
|
|
468
520
|
else installProjectRule(ctx, p);
|
|
@@ -476,7 +528,7 @@ function main() {
|
|
|
476
528
|
if (results.failed.length) say(c.red(` failed: ${results.failed.map(s => s[0] + ' (' + s[1] + ')').join(', ')}`));
|
|
477
529
|
say('');
|
|
478
530
|
if (!opts.dryRun && results.installed.length) {
|
|
479
|
-
say(c.orange('Done.') + ' Restart your agent. Type ' + c.orange('/chisle') + '
|
|
531
|
+
say(c.orange('Done.') + ' Restart your agent. Type ' + c.orange('/chisle') + ' in Claude Code or Pi, or just start coding.');
|
|
480
532
|
note('If Chisle earns its keep, a star helps others find it: https://github.com/JayPokale/Chisle');
|
|
481
533
|
}
|
|
482
534
|
process.exit(results.failed.length ? 1 : 0);
|
|
@@ -130,7 +130,17 @@ function scrub(text) {
|
|
|
130
130
|
// skipped when the payload carries none.
|
|
131
131
|
const DEDUP_MIN = 2048;
|
|
132
132
|
|
|
133
|
-
function
|
|
133
|
+
function duplicateMarker(toolName, text, reference = 'previous') {
|
|
134
|
+
const lines = text.split('\n');
|
|
135
|
+
const prior = reference === 'previous' ? 'the previous' : 'an earlier';
|
|
136
|
+
const preview = lines.slice(0, 5)
|
|
137
|
+
.map(l => l.length > MAX_SALVAGE_LINE ? l.slice(0, MAX_SALVAGE_LINE) + '…' : l);
|
|
138
|
+
return '[chisle: output byte-identical to ' + prior + ' ' + toolName + ' result — ' +
|
|
139
|
+
text.length.toLocaleString('en-US') + ' chars / ' + lines.length +
|
|
140
|
+
' lines, unchanged. First lines:]\n' + preview.join('\n');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function dedupCheck(toolName, text, sessionId, toolUseId) {
|
|
134
144
|
if (process.env.CHISLE_COMPRESS_DEDUP === '0') return null;
|
|
135
145
|
if (!sessionId || typeof sessionId !== 'string') return null;
|
|
136
146
|
if (text.length < DEDUP_MIN) return null;
|
|
@@ -142,18 +152,22 @@ function dedupCheck(toolName, text, sessionId) {
|
|
|
142
152
|
if (state.session !== sessionId) state = { session: sessionId, tools: {} };
|
|
143
153
|
if (!state.tools || typeof state.tools !== 'object') state.tools = {};
|
|
144
154
|
const hash = crypto.createHash('sha256').update(text).digest('hex');
|
|
145
|
-
|
|
146
|
-
|
|
155
|
+
// A second hook invocation for the SAME tool call is not the model seeing
|
|
156
|
+
// the output twice. That happens whenever this hook is registered more than
|
|
157
|
+
// once (plugin manifest + a leftover settings.json entry), because both
|
|
158
|
+
// copies share this state file: copy A stores the hash, copy B matches it
|
|
159
|
+
// and reports first-seen output as a duplicate of itself. Old string-valued
|
|
160
|
+
// entries read through `rec`, so an existing state file needs no migration,
|
|
161
|
+
// and a payload with no tool_use_id keeps today's exact behaviour.
|
|
162
|
+
const prev = state.tools[toolName];
|
|
163
|
+
const rec = (prev && typeof prev === 'object') ? prev : { hash: prev, id: null };
|
|
164
|
+
const sameCall = !!(toolUseId && rec.id && rec.id === toolUseId);
|
|
165
|
+
const dup = rec.hash === hash && !sameCall;
|
|
166
|
+
state.tools[toolName] = { hash, id: toolUseId || rec.id || null };
|
|
147
167
|
const tmp = p + '.' + process.pid + '.tmp';
|
|
148
168
|
fs.writeFileSync(tmp, JSON.stringify(state), { mode: 0o600 });
|
|
149
169
|
fs.renameSync(tmp, p);
|
|
150
|
-
|
|
151
|
-
const lines = text.split('\n');
|
|
152
|
-
const preview = lines.slice(0, 5)
|
|
153
|
-
.map(l => l.length > MAX_SALVAGE_LINE ? l.slice(0, MAX_SALVAGE_LINE) + '…' : l);
|
|
154
|
-
return '[chisle: output byte-identical to the previous ' + toolName + ' result — ' +
|
|
155
|
-
text.length.toLocaleString('en-US') + ' chars / ' + lines.length +
|
|
156
|
-
' lines, unchanged. First lines:]\n' + preview.join('\n');
|
|
170
|
+
return dup ? duplicateMarker(toolName, text) : null;
|
|
157
171
|
} catch (e) { return null; }
|
|
158
172
|
}
|
|
159
173
|
|
|
@@ -259,7 +273,7 @@ function processPayload(payload, mode) {
|
|
|
259
273
|
|
|
260
274
|
const text = extractText(payload.tool_response != null ? payload.tool_response : payload.tool_output);
|
|
261
275
|
if (!text) return null;
|
|
262
|
-
const dup = dedupCheck(payload.tool_name, text, payload.session_id);
|
|
276
|
+
const dup = dedupCheck(payload.tool_name, text, payload.session_id, payload.tool_use_id);
|
|
263
277
|
if (dup != null && dup.length < text.length) return dup;
|
|
264
278
|
return transform(text, limitsFor());
|
|
265
279
|
}
|
|
@@ -293,4 +307,4 @@ function main() {
|
|
|
293
307
|
|
|
294
308
|
if (require.main === module) main();
|
|
295
309
|
|
|
296
|
-
module.exports = { extractText, rebuildResponse, scrub, compress, transform, limitsFor, toolAllowed, processPayload, THRESHOLDS, SAFE_TOOLS };
|
|
310
|
+
module.exports = { extractText, rebuildResponse, scrub, compress, transform, limitsFor, toolAllowed, processPayload, duplicateMarker, THRESHOLDS, SAFE_TOOLS };
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
|
|
6
6
|
const fs = require('fs');
|
|
7
7
|
const path = require('path');
|
|
8
|
-
const {
|
|
8
|
+
const { getClaudeDir, safeWriteFlag, readFlag } = require('./chisle-config');
|
|
9
|
+
const { requestedMode } = require('./chisle-mode');
|
|
9
10
|
|
|
10
11
|
const claudeDir = getClaudeDir();
|
|
11
12
|
const flagPath = path.join(claudeDir, '.chisle-active');
|
|
@@ -16,39 +17,12 @@ process.stdin.on('end', () => {
|
|
|
16
17
|
try {
|
|
17
18
|
const data = JSON.parse(input.replace(/^/, ''));
|
|
18
19
|
const prompt = (data.prompt || '').trim();
|
|
19
|
-
const
|
|
20
|
+
const request = requestedMode(prompt);
|
|
20
21
|
|
|
21
|
-
|
|
22
|
-
if (/\b(activate|enable|turn on|start|use)\b.*\bchisle\b/i.test(promptLower) ||
|
|
23
|
-
/\bchisle\b.*\b(mode|activate|enable|on)\b/i.test(promptLower) ||
|
|
24
|
-
/\bchislif(y|ier)\b/i.test(promptLower)) {
|
|
25
|
-
if (!/\b(stop|disable|turn off|deactivate|off)\b/i.test(promptLower)) {
|
|
26
|
-
const mode = getDefaultMode();
|
|
27
|
-
if (mode !== 'off') safeWriteFlag(flagPath, mode);
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
// /chisle slash commands. No level argument any more: /chisle on,
|
|
32
|
-
// /chisle off, nothing else.
|
|
33
|
-
if (/^\/chisle(\b|:chisle\b)/.test(promptLower)) {
|
|
34
|
-
const parts = promptLower.split(/\s+/);
|
|
35
|
-
const arg = parts[1] || '';
|
|
36
|
-
if (arg === 'off' || arg === 'stop' || arg === 'disable') {
|
|
37
|
-
try { fs.unlinkSync(flagPath); } catch (e) {}
|
|
38
|
-
} else {
|
|
39
|
-
const mode = getDefaultMode();
|
|
40
|
-
if (mode !== 'off') safeWriteFlag(flagPath, mode);
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// Natural language deactivation.
|
|
45
|
-
// Only fire when the off-verb actually targets chisle — NOT when chisle merely
|
|
46
|
-
// appears in a sentence that also mentions turning something else off.
|
|
47
|
-
// ("use chisle to turn off the logger" must NOT deactivate.)
|
|
48
|
-
if (/\b(turn off|disable|deactivate|stop|kill|exit)\s+chisle\b/i.test(promptLower) ||
|
|
49
|
-
/\bchisle\s+(mode\s+)?(off|stop|disable|deactivate)\b/i.test(promptLower) ||
|
|
50
|
-
/\bnormal mode\b/i.test(promptLower)) {
|
|
22
|
+
if (request === 'off') {
|
|
51
23
|
try { fs.unlinkSync(flagPath); } catch (e) {}
|
|
24
|
+
} else if (request === 'on') {
|
|
25
|
+
safeWriteFlag(flagPath, 'on');
|
|
52
26
|
}
|
|
53
27
|
|
|
54
28
|
// Per-turn reinforcement
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Shared mode-directive parser. Harness adapters decide how to persist mode.
|
|
4
|
+
function requestedMode(prompt) {
|
|
5
|
+
const text = String(prompt || '').trim().toLowerCase();
|
|
6
|
+
|
|
7
|
+
if (/\b(turn off|disable|deactivate|stop|kill|exit)\s+chisle\b/i.test(text) ||
|
|
8
|
+
/\bchisle\s+(mode\s+)?(off|stop|disable|deactivate)\b/i.test(text) ||
|
|
9
|
+
/\bnormal mode\b/i.test(text)) return 'off';
|
|
10
|
+
|
|
11
|
+
if (/^\/chisle(\b|:chisle\b)/.test(text) ||
|
|
12
|
+
/\b(activate|enable|turn on|start|use)\b.*\bchisle\b/i.test(text) ||
|
|
13
|
+
/\bchisle\b.*\b(mode|activate|enable|on)\b/i.test(text) ||
|
|
14
|
+
/\bchislif(y|ier)\b/i.test(text)) return 'on';
|
|
15
|
+
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
module.exports = { requestedMode };
|
package/package.json
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "chisle",
|
|
3
|
-
"version": "3.
|
|
4
|
-
"description": "Maximum-efficiency dev mode for
|
|
3
|
+
"version": "3.1.0",
|
|
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",
|
|
7
7
|
"claude-code-plugin",
|
|
8
|
+
"pi-package",
|
|
9
|
+
"pi-coding-agent",
|
|
8
10
|
"productivity",
|
|
9
11
|
"yagni",
|
|
10
12
|
"minimal"
|
|
@@ -19,10 +21,6 @@
|
|
|
19
21
|
"name": "Claude (Anthropic)",
|
|
20
22
|
"url": "https://claude.com/claude-code"
|
|
21
23
|
},
|
|
22
|
-
{
|
|
23
|
-
"name": "Antigravity (Google)",
|
|
24
|
-
"url": "https://antigravity.google"
|
|
25
|
-
},
|
|
26
24
|
{
|
|
27
25
|
"name": "Codex (OpenAI)",
|
|
28
26
|
"url": "https://openai.com/codex/"
|
|
@@ -42,6 +40,14 @@
|
|
|
42
40
|
"engines": {
|
|
43
41
|
"node": ">=18"
|
|
44
42
|
},
|
|
43
|
+
"pi": {
|
|
44
|
+
"extensions": [
|
|
45
|
+
"./pi-extension/index.js"
|
|
46
|
+
],
|
|
47
|
+
"skills": [
|
|
48
|
+
"./skills"
|
|
49
|
+
]
|
|
50
|
+
},
|
|
45
51
|
"scripts": {
|
|
46
52
|
"test": "node --test tests/*.js",
|
|
47
53
|
"build:rules": "node scripts/build-rules.js",
|
|
@@ -55,6 +61,7 @@
|
|
|
55
61
|
"files": [
|
|
56
62
|
"bin/",
|
|
57
63
|
"hooks/",
|
|
64
|
+
"pi-extension/",
|
|
58
65
|
"skills/",
|
|
59
66
|
"commands/",
|
|
60
67
|
"rules/",
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('node:crypto');
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
const { getDefaultMode } = require('../hooks/chisle-config');
|
|
7
|
+
const { duplicateMarker, limitsFor, transform } = require('../hooks/chisle-compress-output');
|
|
8
|
+
const { requestedMode } = require('../hooks/chisle-mode');
|
|
9
|
+
|
|
10
|
+
const RULES_TYPE = 'chisle-rules';
|
|
11
|
+
const MODE_TYPE = 'chisle-mode';
|
|
12
|
+
const SAFE_TOOLS = new Set(['bash', 'powershell', 'grep', 'find', 'ls']);
|
|
13
|
+
const NEVER_COMPRESS = new Set(['read', 'edit', 'write']);
|
|
14
|
+
const DEDUP_MIN = 2048;
|
|
15
|
+
const FALLBACK_RULES = `CHISLE ACTIVE
|
|
16
|
+
|
|
17
|
+
Chisle: maximum-efficiency dev mode. Zero-fluff prose. YAGNI-first code.
|
|
18
|
+
|
|
19
|
+
Drop articles, filler, pleasantries, and hedging. Fragments OK; technical terms exact.
|
|
20
|
+
Code ladder: YAGNI → reuse → stdlib → native → installed dependency → one line → minimum code.
|
|
21
|
+
Never simplify away input validation, data-loss prevention, security, accessibility, or explicit requirements.`;
|
|
22
|
+
|
|
23
|
+
function loadRules() {
|
|
24
|
+
try {
|
|
25
|
+
const skill = fs.readFileSync(path.join(__dirname, '..', 'skills', 'chisle', 'SKILL.md'), 'utf8');
|
|
26
|
+
return 'CHISLE ACTIVE\n\n' + skill.replace(/^---[\s\S]*?---\s*/, '');
|
|
27
|
+
} catch (_) {
|
|
28
|
+
return FALLBACK_RULES;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function hasRulesEntry(entries) {
|
|
33
|
+
return (entries || []).some(entry =>
|
|
34
|
+
(entry.type === 'custom_message' && entry.customType === RULES_TYPE) ||
|
|
35
|
+
(entry.type === 'message' && entry.message && entry.message.role === 'custom' &&
|
|
36
|
+
entry.message.customType === RULES_TYPE));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function promptHasRules(event) {
|
|
40
|
+
const containsRules = text => /^# Chisle\s*$/m.test(text || '') &&
|
|
41
|
+
(text || '').includes('## Prose: zero fluff') &&
|
|
42
|
+
(text || '').includes('## Code: the efficiency ladder');
|
|
43
|
+
if (containsRules(event.systemPrompt)) return true;
|
|
44
|
+
return (event.systemPromptOptions && event.systemPromptOptions.contextFiles || [])
|
|
45
|
+
.some(file => containsRules(file.content));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function piToolAllowed(name) {
|
|
49
|
+
if (!name || NEVER_COMPRESS.has(name.toLowerCase())) return false;
|
|
50
|
+
if (process.env.CHISLE_COMPRESS_TOOLS) {
|
|
51
|
+
return process.env.CHISLE_COMPRESS_TOOLS.split(',').map(s => s.trim()).filter(Boolean).includes(name);
|
|
52
|
+
}
|
|
53
|
+
return SAFE_TOOLS.has(name.toLowerCase()) || name.startsWith('mcp__');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function chisleExtension(pi) {
|
|
57
|
+
const rules = loadRules();
|
|
58
|
+
let active = getDefaultMode() !== 'off';
|
|
59
|
+
let rulesInContext = false;
|
|
60
|
+
let savedChars = 0;
|
|
61
|
+
let lastByToolBlock = new Map();
|
|
62
|
+
let dedupBaseline = new Map();
|
|
63
|
+
const stopRulesSignal = pi.events.on('chisle:rules-injected', () => { rulesInContext = true; });
|
|
64
|
+
const stopModeSignal = pi.events.on('chisle:mode', mode => { active = mode === 'on'; });
|
|
65
|
+
|
|
66
|
+
function updateStatus(ctx) {
|
|
67
|
+
if (!ctx.hasUI) return;
|
|
68
|
+
if (!active) return ctx.ui.setStatus('chisle', undefined);
|
|
69
|
+
const tokens = Math.floor(savedChars / 4);
|
|
70
|
+
const saved = tokens >= 1000 ? ` ⇣${Math.floor(tokens / 1000)}k tok` : tokens ? ` ⇣${tokens} tok` : '';
|
|
71
|
+
ctx.ui.setStatus('chisle', ctx.ui.theme.fg('accent', `[CHISLE]${saved}`));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function restoreBranch(ctx) {
|
|
75
|
+
active = getDefaultMode() !== 'off';
|
|
76
|
+
savedChars = 0;
|
|
77
|
+
lastByToolBlock = new Map();
|
|
78
|
+
dedupBaseline = new Map();
|
|
79
|
+
|
|
80
|
+
const modeEntry = ctx.sessionManager.getBranch()
|
|
81
|
+
.filter(entry => entry.type === 'custom' && entry.customType === MODE_TYPE).pop();
|
|
82
|
+
if (modeEntry && modeEntry.data && ['on', 'off'].includes(modeEntry.data.mode)) {
|
|
83
|
+
active = modeEntry.data.mode === 'on';
|
|
84
|
+
}
|
|
85
|
+
rulesInContext = hasRulesEntry(ctx.sessionManager.buildContextEntries());
|
|
86
|
+
updateStatus(ctx);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function setMode(request, ctx) {
|
|
90
|
+
active = request === 'on';
|
|
91
|
+
pi.events.emit('chisle:mode', active ? 'on' : 'off');
|
|
92
|
+
pi.appendEntry(MODE_TYPE, { mode: active ? 'on' : 'off' });
|
|
93
|
+
updateStatus(ctx);
|
|
94
|
+
if (ctx.hasUI) ctx.ui.notify(active ? 'Chisle enabled.' : 'Chisle disabled.', 'info');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
pi.registerCommand('chisle', {
|
|
98
|
+
description: 'Enable Chisle or set /chisle on|off',
|
|
99
|
+
handler: async (args, ctx) => {
|
|
100
|
+
const request = requestedMode(`/chisle ${args || ''}`) || 'on';
|
|
101
|
+
setMode(request, ctx);
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
pi.on('session_start', async (_event, ctx) => restoreBranch(ctx));
|
|
106
|
+
pi.on('session_tree', async (_event, ctx) => restoreBranch(ctx));
|
|
107
|
+
|
|
108
|
+
pi.on('session_shutdown', async (_event, ctx) => {
|
|
109
|
+
lastByToolBlock.clear();
|
|
110
|
+
dedupBaseline.clear();
|
|
111
|
+
stopRulesSignal();
|
|
112
|
+
stopModeSignal();
|
|
113
|
+
if (ctx.hasUI) ctx.ui.setStatus('chisle', undefined);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
pi.on('session_compact', async (_event, ctx) => {
|
|
117
|
+
lastByToolBlock.clear();
|
|
118
|
+
dedupBaseline.clear();
|
|
119
|
+
rulesInContext = hasRulesEntry(ctx.sessionManager.buildContextEntries());
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
pi.on('input', async (event, ctx) => {
|
|
123
|
+
if (event.source === 'extension') return { action: 'continue' };
|
|
124
|
+
const request = requestedMode(event.text);
|
|
125
|
+
if (!request) return { action: 'continue' };
|
|
126
|
+
setMode(request, ctx);
|
|
127
|
+
return request === 'off' ? { action: 'handled' } : { action: 'continue' };
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
pi.on('before_agent_start', async event => {
|
|
131
|
+
if (!active) {
|
|
132
|
+
if (event.systemPrompt.includes('CHISLE DISABLED.')) return;
|
|
133
|
+
return { systemPrompt: event.systemPrompt + '\n\nCHISLE DISABLED. Ignore only Chisle style and efficiency-ladder instructions.' };
|
|
134
|
+
}
|
|
135
|
+
if (rulesInContext || promptHasRules(event)) return;
|
|
136
|
+
rulesInContext = true;
|
|
137
|
+
pi.events.emit('chisle:rules-injected');
|
|
138
|
+
return { message: { customType: RULES_TYPE, content: rules, display: false } };
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
pi.on('context', async event => {
|
|
142
|
+
if (active) return;
|
|
143
|
+
return { messages: event.messages.filter(message =>
|
|
144
|
+
!(message.role === 'custom' && message.customType === RULES_TYPE)) };
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
pi.on('turn_start', async () => {
|
|
148
|
+
dedupBaseline = new Map(lastByToolBlock);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
pi.on('tool_result', async (event, ctx) => {
|
|
152
|
+
try {
|
|
153
|
+
if (!active || process.env.CHISLE_COMPRESS === '0' || !piToolAllowed(event.toolName) ||
|
|
154
|
+
!Array.isArray(event.content)) return;
|
|
155
|
+
|
|
156
|
+
let changed = false;
|
|
157
|
+
const content = event.content.map((block, index) => {
|
|
158
|
+
if (!block || block.type !== 'text' || typeof block.text !== 'string') return block;
|
|
159
|
+
const text = block.text;
|
|
160
|
+
const key = `${event.toolName}:${index}`;
|
|
161
|
+
let updated = null;
|
|
162
|
+
|
|
163
|
+
if (process.env.CHISLE_COMPRESS_DEDUP !== '0' && text.length >= DEDUP_MIN) {
|
|
164
|
+
const hash = crypto.createHash('sha256').update(text).digest('hex');
|
|
165
|
+
const previous = dedupBaseline.get(key);
|
|
166
|
+
lastByToolBlock.set(key, hash);
|
|
167
|
+
if (previous === hash) updated = duplicateMarker(event.toolName, text, 'earlier');
|
|
168
|
+
}
|
|
169
|
+
if (updated == null) updated = transform(text, limitsFor());
|
|
170
|
+
if (updated == null || updated.length >= text.length) return block;
|
|
171
|
+
changed = true;
|
|
172
|
+
savedChars += text.length - updated.length;
|
|
173
|
+
return { ...block, text: updated };
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
if (!changed) return;
|
|
177
|
+
updateStatus(ctx);
|
|
178
|
+
return { content };
|
|
179
|
+
} catch (_) {
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
module.exports = chisleExtension;
|
|
186
|
+
module.exports.loadRules = loadRules;
|
|
187
|
+
module.exports.hasRulesEntry = hasRulesEntry;
|
|
188
|
+
module.exports.piToolAllowed = piToolAllowed;
|