chisle 3.0.0 → 3.2.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/bin/install.js CHANGED
@@ -1,9 +1,10 @@
1
1
  #!/usr/bin/env node
2
- // chisle — cross-platform installer.
2
+ // chisle: cross-platform installer.
3
3
  //
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',
@@ -56,13 +58,14 @@ const PROVIDERS = [
56
58
  // ── argv ────────────────────────────────────────────────────────────────────
57
59
  function parseArgs(argv) {
58
60
  const opts = { dryRun: false, force: false, list: false, uninstall: false,
59
- noColor: false, help: false, only: [], configDir: null };
61
+ noColor: false, help: false, stats: false, only: [], configDir: null };
60
62
  for (let i = 0; i < argv.length; i++) {
61
63
  const a = argv[i];
62
64
  switch (a) {
63
65
  case '--dry-run': opts.dryRun = true; break;
64
66
  case '--force': opts.force = true; break;
65
67
  case '--list': opts.list = true; break;
68
+ case '--stats': opts.stats = true; break;
66
69
  case '--uninstall': case '-u': opts.uninstall = true; break;
67
70
  case '--no-color': opts.noColor = true; break;
68
71
  case '-h': case '--help': opts.help = true; break;
@@ -183,10 +186,10 @@ function installClaude(ctx) {
183
186
  const r1 = run('claude', ['plugin', 'marketplace', 'add', REPO], opts.dryRun);
184
187
  const r2 = run('claude', ['plugin', 'install', 'chisle@chisle'], opts.dryRun);
185
188
  if ((r1.status || 0) === 0 && (r2.status || 0) === 0) { results.installed.push('claude'); pluginOK = true; }
186
- else warn(' claude plugin install failed — falling back to standalone hooks');
189
+ else warn(' claude plugin install failed, falling back to standalone hooks');
187
190
  }
188
191
  } else {
189
- note(' claude CLI not on PATH — wiring standalone hooks in settings.json');
192
+ note(' claude CLI not on PATH, wiring standalone hooks in settings.json');
190
193
  }
191
194
 
192
195
  if (!pluginOK) {
@@ -196,11 +199,23 @@ function installClaude(ctx) {
196
199
  else results.failed.push(['claude-hooks', r]);
197
200
  } else {
198
201
  note(' hooks: plugin manifest handles SessionStart + UserPromptSubmit + PostToolUse');
202
+ // Plugin path won, so drop any standalone wiring left by an earlier run, or
203
+ // both copies of every hook run on each event and the PostToolUse pair
204
+ // dedups first-seen output against itself.
205
+ const settingsPath = path.join(claudeDir(opts), 'settings.json');
206
+ const settings = SETTINGS.readSettings(settingsPath);
207
+ if (settings && SETTINGS.removeHooks(settings, 'chisle-') > 0) {
208
+ if (!opts.dryRun) {
209
+ SETTINGS.validateHookFields(settings);
210
+ SETTINGS.writeSettings(settingsPath, settings);
211
+ }
212
+ note(' removed superseded standalone hook entries from settings.json');
213
+ }
199
214
  }
200
215
  process.stdout.write('\n');
201
216
  }
202
217
 
203
- // Standalone wiring — copy hooks into <configDir>/chisle-hooks/ and merge
218
+ // Standalone wiring: copy hooks into <configDir>/chisle-hooks/ and merge
204
219
  // settings.json. Used when the plugin path is unavailable.
205
220
  function installClaudeHooks(ctx) {
206
221
  const { opts, warn, note } = ctx;
@@ -209,7 +224,7 @@ function installClaudeHooks(ctx) {
209
224
  const hooksDst = path.join(cfg, 'chisle-hooks');
210
225
  const settingsPath = path.join(cfg, 'settings.json');
211
226
  const HOOK_FILES = ['package.json', 'chisle-config.js', 'chisle-activate.js',
212
- 'chisle-mode-tracker.js', 'chisle-compress-output.js',
227
+ 'chisle-mode.js', 'chisle-mode-tracker.js', 'chisle-compress-output.js',
213
228
  'chisle-statusline.sh', 'chisle-statusline.ps1'];
214
229
 
215
230
  if (opts.dryRun) {
@@ -254,7 +269,7 @@ function installClaudeHooks(ctx) {
254
269
  } else {
255
270
  const existing = typeof settings.statusLine === 'string' ? settings.statusLine : (settings.statusLine.command || '');
256
271
  if (existing.includes('chisle-statusline')) process.stdout.write(' statusline badge already configured.\n');
257
- else process.stdout.write(' NOTE: existing statusline detected — CHISLE badge NOT added (see docs/install-windows.md).\n');
272
+ else process.stdout.write(' NOTE: existing statusline detected, CHISLE badge NOT added (see docs/install-windows.md).\n');
258
273
  }
259
274
 
260
275
  SETTINGS.validateHookFields(settings);
@@ -263,6 +278,30 @@ function installClaudeHooks(ctx) {
263
278
  return 'ok';
264
279
  }
265
280
 
281
+ // ── Pi ──────────────────────────────────────────────────────────────────────
282
+ function piSources(output) {
283
+ return String(output || '').split('\n').map(line => line.trim())
284
+ .filter(line => /^npm:chisle(?:@|$)/i.test(line) || /^git:.*github\.com\/JayPokale\/Chisle(?:@|$)/i.test(line))
285
+ .map(source => source.replace(/@[^/@]+$/, ''));
286
+ }
287
+
288
+ function installPi(ctx) {
289
+ const { say, note, opts, results } = ctx;
290
+ results.detected++;
291
+ say('→ Pi detected');
292
+ if (!opts.force) {
293
+ const installed = piSources(capture('pi', ['list']).stdout);
294
+ if (installed.length) {
295
+ note(' chisle package already installed (use --force)');
296
+ results.skipped.push(['pi', 'already installed']); process.stdout.write('\n'); return;
297
+ }
298
+ }
299
+ const r = run('pi', ['install', 'npm:chisle'], opts.dryRun);
300
+ if ((r.status || 0) === 0) results.installed.push('pi');
301
+ else results.failed.push(['pi', 'pi package install failed']);
302
+ process.stdout.write('\n');
303
+ }
304
+
266
305
  // ── Gemini ──────────────────────────────────────────────────────────────────
267
306
  function installGemini(ctx) {
268
307
  const { say, note, opts, results } = ctx;
@@ -347,66 +386,80 @@ function uninstall(ctx) {
347
386
  say(c.orange('Chisle uninstall'));
348
387
  let touched = 0;
349
388
 
350
- // Claude plugin
351
- if (hasCmd('claude') && !opts.dryRun) {
352
- const r = capture('claude', ['plugin', 'list']);
353
- if (r.status === 0 && /chisle/i.test(r.stdout || '')) {
354
- run('claude', ['plugin', 'uninstall', 'chisle@chisle'], opts.dryRun); touched++;
389
+ const wants = id => !opts.only.length || opts.only.includes(id);
390
+
391
+ if (wants('claude')) {
392
+ // Claude plugin
393
+ if (hasCmd('claude') && !opts.dryRun) {
394
+ const r = capture('claude', ['plugin', 'list']);
395
+ if (r.status === 0 && /chisle/i.test(r.stdout || '')) {
396
+ run('claude', ['plugin', 'uninstall', 'chisle@chisle'], opts.dryRun); touched++;
397
+ }
355
398
  }
356
- }
357
399
 
358
- // Claude standalone hooks + statusline
359
- const cfg = claudeDir(opts);
360
- const settingsPath = path.join(cfg, 'settings.json');
361
- const settings = SETTINGS.readSettings(settingsPath);
362
- if (settings) {
363
- const removed = SETTINGS.removeHooks(settings, 'chisle-');
364
- let slRemoved = false;
365
- if (settings.statusLine && typeof settings.statusLine.command === 'string'
366
- && settings.statusLine.command.includes('chisle-statusline')) { delete settings.statusLine; slRemoved = true; }
367
- if (removed > 0 || slRemoved) {
368
- if (!opts.dryRun) { SETTINGS.validateHookFields(settings); SETTINGS.writeSettings(settingsPath, settings); }
369
- note(` removed ${removed} hook entr${removed === 1 ? 'y' : 'ies'}${slRemoved ? ' + statusline' : ''} from settings.json`);
370
- touched++;
400
+ // Claude standalone hooks + statusline
401
+ const cfg = claudeDir(opts);
402
+ const settingsPath = path.join(cfg, 'settings.json');
403
+ const settings = SETTINGS.readSettings(settingsPath);
404
+ if (settings) {
405
+ const removed = SETTINGS.removeHooks(settings, 'chisle-');
406
+ let slRemoved = false;
407
+ if (settings.statusLine && typeof settings.statusLine.command === 'string'
408
+ && settings.statusLine.command.includes('chisle-statusline')) { delete settings.statusLine; slRemoved = true; }
409
+ if (removed > 0 || slRemoved) {
410
+ if (!opts.dryRun) { SETTINGS.validateHookFields(settings); SETTINGS.writeSettings(settingsPath, settings); }
411
+ note(` removed ${removed} hook entr${removed === 1 ? 'y' : 'ies'}${slRemoved ? ' + statusline' : ''} from settings.json`);
412
+ touched++;
413
+ }
414
+ }
415
+ const hooksDst = path.join(cfg, 'chisle-hooks');
416
+ if (fs.existsSync(hooksDst)) { if (!opts.dryRun) fs.rmSync(hooksDst, { recursive: true, force: true }); note(` removed ${hooksDst}`); touched++; }
417
+
418
+ for (const f of ['.chisle-active', '.chisle-session-turns', '.chisle-statusline-suffix',
419
+ '.chisle-compress-stats.json', '.chisle-compress-last.json', '.chisle-update-check.json']) {
420
+ const p = path.join(cfg, f);
421
+ if (fs.existsSync(p)) { if (!opts.dryRun) { try { fs.unlinkSync(p); } catch (_) {} } note(` removed ${p}`); touched++; }
371
422
  }
372
423
  }
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
424
 
376
- // Flag files
377
- for (const f of ['.chisle-active', '.chisle-session-turns', '.chisle-statusline-suffix',
378
- '.chisle-compress-stats.json', '.chisle-compress-last.json', '.chisle-update-check.json']) {
379
- const p = path.join(cfg, f);
380
- if (fs.existsSync(p)) { if (!opts.dryRun) { try { fs.unlinkSync(p); } catch (_) {} } note(` removed ${p}`); touched++; }
425
+ if (wants('pi') && hasCmd('pi')) {
426
+ const sources = piSources(capture('pi', ['list']).stdout);
427
+ for (const source of sources) {
428
+ if ((run('pi', ['remove', source], opts.dryRun).status || 0) === 0) touched++;
429
+ }
381
430
  }
382
431
 
383
- // Codex fenced block
384
- const codexMd = path.join(os.homedir(), '.codex', 'AGENTS.md');
385
- if (fs.existsSync(codexMd)) {
386
- const txt = fs.readFileSync(codexMd, 'utf8');
387
- if (txt.includes(FENCE_BEGIN)) {
388
- const stripped = txt.replace(new RegExp(`\\n?${FENCE_BEGIN}[\\s\\S]*?${FENCE_END}\\n?`), '\n').replace(/\n{3,}/g, '\n\n');
389
- if (!opts.dryRun) fs.writeFileSync(codexMd, stripped, { mode: 0o644 });
390
- note(` removed chisle block from ${codexMd}`); touched++;
432
+ if (wants('codex')) {
433
+ const codexMd = path.join(os.homedir(), '.codex', 'AGENTS.md');
434
+ if (fs.existsSync(codexMd)) {
435
+ const txt = fs.readFileSync(codexMd, 'utf8');
436
+ if (txt.includes(FENCE_BEGIN)) {
437
+ const stripped = txt.replace(new RegExp(`\\n?${FENCE_BEGIN}[\\s\\S]*?${FENCE_END}\\n?`), '\n').replace(/\n{3,}/g, '\n\n');
438
+ if (!opts.dryRun) fs.writeFileSync(codexMd, stripped, { mode: 0o644 });
439
+ note(` removed chisle block from ${codexMd}`); touched++;
440
+ }
391
441
  }
392
442
  }
393
443
 
394
- note('');
395
- note('Project-scoped rule files (.cursor/, .windsurf/, .clinerules/, .kiro/, .github/copilot-instructions.md)');
396
- note('live in your project repos — remove them per-project with git if you added them there.');
444
+ if (!opts.only.length || opts.only.some(id => PROVIDERS.find(p => p.id === id).scope === 'project')) {
445
+ note('');
446
+ note('Project-scoped rule files (.cursor/, .windsurf/, .clinerules/, .kiro/, .github/copilot-instructions.md)');
447
+ note('live in your project repos, so remove them per-project with git if you added them there.');
448
+ }
397
449
  say(touched ? c.green(`\nUninstalled. ${touched} item(s) cleaned.`) : c.yellow('\nNothing to uninstall.'));
398
450
  }
399
451
 
400
452
  // ── help / banner ───────────────────────────────────────────────────────────
401
453
  function printHelp(c) {
402
- process.stdout.write(`${c.orange('chisle')} — maximum-efficiency dev mode installer
454
+ process.stdout.write(`${c.orange('chisle')}: maximum-efficiency dev mode installer
403
455
 
404
456
  Usage:
405
457
  npx chisle [flags]
406
458
 
407
459
  Flags:
408
460
  --list Detect agents and print them; install nothing
409
- --only <id> Install only for one agent (repeatable)
461
+ --stats Print tool-output savings recorded so far; change nothing
462
+ --only <id> Limit install/uninstall to one agent (repeatable)
410
463
  --dry-run Print actions, change nothing
411
464
  --force Reinstall / overwrite even if already present
412
465
  --uninstall, -u Remove what chisle installed
@@ -420,11 +473,51 @@ Examples:
420
473
  npx chisle # auto-detect + install
421
474
  npx chisle --only claude # just Claude Code
422
475
  npx chisle --dry-run # preview
476
+ npx chisle --stats # what the compressor has saved
423
477
  `);
424
478
  }
425
479
 
480
+ // ── stats ───────────────────────────────────────────────────────────────────
481
+ // Reads the compressor's ledger. Input axis only, and deliberately so: chars
482
+ // elided have a real baseline (we know exactly what was cut), while the output
483
+ // axis has none, since there is no way to know what the model would have written
484
+ // without the ruleset, which is what the benchmark arms are for. A counter that
485
+ // blended the two would be inventing the interesting half.
486
+ function printStats(c, opts) {
487
+ const dir = claudeDir(opts);
488
+ const p = path.join(dir, '.chisle-compress-stats.json');
489
+ process.stdout.write(c.orange('chisle') + ': tool-output savings\n\n');
490
+
491
+ let stats = null;
492
+ try {
493
+ if (!fs.lstatSync(p).isSymbolicLink()) {
494
+ const parsed = JSON.parse(fs.readFileSync(p, 'utf8'));
495
+ if (parsed && Number.isFinite(parsed.savedChars) && Number.isFinite(parsed.events)) stats = parsed;
496
+ }
497
+ } catch (e) {}
498
+
499
+ if (!stats || stats.events === 0) {
500
+ process.stdout.write(' no compression recorded yet\n\n');
501
+ process.stdout.write(c.dim(` ledger: ${p}\n`));
502
+ process.stdout.write(c.dim(' it fills as the compressor elides oversized tool output.\n'));
503
+ process.stdout.write(c.dim(" nothing there after real use? check `npx chisle --list` and that Chisle is active.\n"));
504
+ return;
505
+ }
506
+
507
+ const chars = stats.savedChars;
508
+ const tokens = Math.floor(chars / 4);
509
+ const per = Math.round(chars / stats.events);
510
+ const fmt = (n) => n.toLocaleString('en-US');
511
+ process.stdout.write(` saved: ${c.green(fmt(chars) + ' chars')} (~${fmt(tokens)} tokens)\n`);
512
+ process.stdout.write(` outputs: ${fmt(stats.events)} compressed, ${fmt(per)} chars each on average\n\n`);
513
+ process.stdout.write(c.dim(' A floor, not an estimate: every saved byte also stops being re-sent\n'));
514
+ process.stdout.write(c.dim(' on every later request in that session.\n\n'));
515
+ process.stdout.write(c.dim(` ledger: ${p}\n`));
516
+ process.stdout.write(c.dim(' per-transcript detail: node benchmarks/replay-compress.js (in a repo clone)\n'));
517
+ }
518
+
426
519
  function printList(c) {
427
- process.stdout.write(c.orange('chisle') + ' — detected agents:\n\n');
520
+ process.stdout.write(c.orange('chisle') + ': detected agents:\n\n');
428
521
  for (const p of PROVIDERS) {
429
522
  const found = detectMatch(p.detect);
430
523
  const mark = found ? c.green('✓') : c.dim('·');
@@ -441,6 +534,7 @@ function main() {
441
534
 
442
535
  if (opts.help) return printHelp(c);
443
536
  if (opts.list) return printList(c);
537
+ if (opts.stats) return printStats(c, opts);
444
538
 
445
539
  const results = { detected: 0, installed: [], skipped: [], failed: [] };
446
540
  const say = (s) => process.stdout.write(s + '\n');
@@ -451,7 +545,7 @@ function main() {
451
545
  if (opts.uninstall) return uninstall(ctx);
452
546
 
453
547
  say(c.orange('╭─ Chisle installer ─╮'));
454
- say(c.dim(opts.dryRun ? ' (dry run — nothing will change)' : ' maximum signal, minimum noise'));
548
+ say(c.dim(opts.dryRun ? ' (dry run, nothing will change)' : ' maximum signal, minimum noise'));
455
549
  say('');
456
550
 
457
551
  const targets = PROVIDERS.filter(p => opts.only.length ? opts.only.includes(p.id) : detectMatch(p.detect));
@@ -463,6 +557,7 @@ function main() {
463
557
 
464
558
  for (const p of targets) {
465
559
  if (p.id === 'claude') installClaude(ctx);
560
+ else if (p.id === 'pi') installPi(ctx);
466
561
  else if (p.id === 'gemini') installGemini(ctx);
467
562
  else if (p.id === 'codex') installCodex(ctx);
468
563
  else installProjectRule(ctx, p);
@@ -476,7 +571,7 @@ function main() {
476
571
  if (results.failed.length) say(c.red(` failed: ${results.failed.map(s => s[0] + ' (' + s[1] + ')').join(', ')}`));
477
572
  say('');
478
573
  if (!opts.dryRun && results.installed.length) {
479
- say(c.orange('Done.') + ' Restart your agent. Type ' + c.orange('/chisle') + ' (Claude Code) or just start coding.');
574
+ say(c.orange('Done.') + ' Restart your agent. Type ' + c.orange('/chisle') + ' in Claude Code or Pi, or just start coding.');
480
575
  note('If Chisle earns its keep, a star helps others find it: https://github.com/JayPokale/Chisle');
481
576
  }
482
577
  process.exit(results.failed.length ? 1 : 0);
@@ -1,4 +1,4 @@
1
- # chisle-audit — one-shot efficiency audit across both axes
1
+ # chisle-audit: one-shot efficiency audit across both axes
2
2
  [command]
3
3
  name = "chisle-audit"
4
4
  description = "Audit a diff, file, or repo for BOTH over-engineered code and bloated prose/docs. Ranked report, biggest cut first. Changes nothing."
@@ -1,4 +1,4 @@
1
- # chisle-help — quick reference card
1
+ # chisle-help: quick reference card
2
2
  [command]
3
3
  name = "chisle-help"
4
4
  description = "Show Chisle quick reference: modes, levels, commands."
@@ -1,4 +1,4 @@
1
- # chisle-review — review a diff/file for over-engineering
1
+ # chisle-review: review a diff/file for over-engineering
2
2
  [command]
3
3
  name = "chisle-review"
4
4
  description = "Review the current diff or a file for bloat, speculative abstractions, and reinvented stdlib."
@@ -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 dedupCheck(toolName, text, sessionId) {
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
- const dup = state.tools[toolName] === hash;
146
- state.tools[toolName] = hash;
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
- if (!dup) return null;
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 { getDefaultMode, getClaudeDir, safeWriteFlag, readFlag } = require('./chisle-config');
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 promptLower = prompt.toLowerCase();
20
+ const request = requestedMode(prompt);
20
21
 
21
- // Natural language activation
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.0.0",
4
- "description": "Maximum-efficiency dev mode for Claude Code. Zero-fluff prose + YAGNI-first code decisions. One plugin that does both.",
3
+ "version": "3.2.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/",