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/.claude-plugin/plugin.json +1 -1
- package/.clinerules/chisle.md +15 -8
- package/.cursor/rules/chisle.mdc +16 -9
- package/.github/copilot-instructions.md +15 -8
- package/.kiro/steering/chisle.md +15 -8
- package/.windsurf/rules/chisle.md +15 -8
- package/AGENTS.md +12 -3
- package/README.md +111 -60
- package/bin/install.js +144 -49
- package/commands/chisle-audit.toml +1 -1
- package/commands/chisle-help.toml +1 -1
- package/commands/chisle-review.toml +1 -1
- 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
- package/rules/chisle-activate.md +1 -1
- package/skills/chisle/SKILL.md +19 -11
- package/skills/chisle-audit/SKILL.md +4 -4
- package/skills/chisle-help/SKILL.md +2 -2
package/bin/install.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// chisle
|
|
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
|
|
189
|
+
else warn(' claude plugin install failed, falling back to standalone hooks');
|
|
187
190
|
}
|
|
188
191
|
} else {
|
|
189
|
-
note(' claude CLI not on PATH
|
|
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
|
|
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
|
|
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
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
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
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
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
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
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
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
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
|
-
|
|
395
|
-
|
|
396
|
-
|
|
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')}
|
|
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
|
-
--
|
|
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') + '
|
|
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
|
|
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') + '
|
|
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
|
|
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."
|
|
@@ -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.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/",
|