chisle 3.6.1 → 3.8.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/.agents/rules/chisle.md +48 -0
- package/.claude-plugin/plugin.json +1 -1
- package/.clinerules/chisle.md +5 -6
- package/.cursor/rules/chisle.mdc +5 -6
- package/.github/copilot-instructions.md +5 -6
- package/.kiro/steering/chisle.md +5 -6
- package/.opencode/plugins/chisle.mjs +5 -6
- package/.windsurf/rules/chisle.md +5 -6
- package/AGENTS.md +5 -7
- package/NOTICE +12 -0
- package/README.md +74 -397
- package/bin/install.js +152 -26
- package/bin/lib/mcp.js +179 -0
- package/hooks/chisle-compress-output.js +96 -40
- package/hooks/chisle-config.js +13 -6
- package/hooks/chisle-mcp-proxy.js +195 -0
- package/package.json +5 -2
- package/skills/chisle/SKILL.md +1 -1
package/bin/install.js
CHANGED
|
@@ -7,9 +7,10 @@
|
|
|
7
7
|
// - Pi → Pi package (extension + skill)
|
|
8
8
|
// - Gemini CLI → gemini extensions install
|
|
9
9
|
// - Codex → fenced ruleset appended to ~/.codex/AGENTS.md
|
|
10
|
-
// -
|
|
10
|
+
// - Copilot CLI → fenced ruleset + postToolUse compression hook in ~/.copilot
|
|
11
|
+
// - OpenCode → fenced ruleset + skills + plugin in its global config root
|
|
11
12
|
// - Hermes → skills copy in ~/.hermes/skills (Agent Skills standard)
|
|
12
|
-
// - Cursor/Windsurf/Cline/Kiro/Copilot → project rule file dropped into CWD
|
|
13
|
+
// - Cursor/Windsurf/Cline/Kiro/Copilot/Antigravity → project rule file dropped into CWD
|
|
13
14
|
//
|
|
14
15
|
// Usage:
|
|
15
16
|
// npx chisle auto-detect + install
|
|
@@ -17,6 +18,7 @@
|
|
|
17
18
|
// npx chisle --only claude install for one agent
|
|
18
19
|
// npx chisle --dry-run print actions, change nothing
|
|
19
20
|
// npx chisle --uninstall remove what we installed
|
|
21
|
+
// npx chisle mcp wrap route stdio MCP servers through the compressor
|
|
20
22
|
// npx chisle --help
|
|
21
23
|
//
|
|
22
24
|
// Pure stdlib, zero runtime deps.
|
|
@@ -29,6 +31,8 @@ const path = require('path');
|
|
|
29
31
|
const cp = require('child_process');
|
|
30
32
|
|
|
31
33
|
const SETTINGS = require('./lib/settings');
|
|
34
|
+
const MCP = require('./lib/mcp');
|
|
35
|
+
const { getOpencodeDir, getOpencodeStateDir } = require('../hooks/chisle-config');
|
|
32
36
|
|
|
33
37
|
const REPO = 'JayPokale/Chisle';
|
|
34
38
|
const IS_WIN = process.platform === 'win32';
|
|
@@ -45,7 +49,8 @@ const PROVIDERS = [
|
|
|
45
49
|
{ id: 'pi', label: 'Pi', scope: 'global', detect: 'cmd:pi' },
|
|
46
50
|
{ id: 'gemini', label: 'Gemini CLI', scope: 'global', detect: 'cmd:gemini' },
|
|
47
51
|
{ id: 'codex', label: 'Codex CLI', scope: 'global', detect: 'cmd:codex||dir:~/.codex' },
|
|
48
|
-
{ id: '
|
|
52
|
+
{ id: 'copilot-cli', label: 'Copilot CLI', scope: 'global', detect: 'cmd:copilot' },
|
|
53
|
+
{ id: 'opencode', label: 'OpenCode', scope: 'global', detect: 'cmd:opencode' },
|
|
49
54
|
{ id: 'hermes', label: 'Hermes Agent', scope: 'global', detect: 'cmd:hermes||dir:~/.hermes' },
|
|
50
55
|
{ id: 'cursor', label: 'Cursor', scope: 'project', detect: 'cmd:cursor||dir:~/.cursor',
|
|
51
56
|
rule: '.cursor/rules/chisle.mdc' },
|
|
@@ -57,6 +62,10 @@ const PROVIDERS = [
|
|
|
57
62
|
rule: '.kiro/steering/chisle.md' },
|
|
58
63
|
{ id: 'copilot', label: 'GitHub Copilot',scope: 'project', detect: 'vscode-ext:github.copilot||vscode-ext:github.copilot-chat',
|
|
59
64
|
rule: '.github/copilot-instructions.md' },
|
|
65
|
+
// Antigravity IDE (`antigravity`) and its CLI (`agy`, antigravity-cli) both
|
|
66
|
+
// load workspace rules from .agents/rules/*.md.
|
|
67
|
+
{ id: 'antigravity', label: 'Antigravity', scope: 'project', detect: 'cmd:antigravity||cmd:agy||dir:~/.antigravity',
|
|
68
|
+
rule: '.agents/rules/chisle.md' },
|
|
60
69
|
];
|
|
61
70
|
|
|
62
71
|
// ── argv ────────────────────────────────────────────────────────────────────
|
|
@@ -146,6 +155,11 @@ function detectMatch(spec) {
|
|
|
146
155
|
return false;
|
|
147
156
|
}
|
|
148
157
|
|
|
158
|
+
function providerDetected(provider) {
|
|
159
|
+
return detectMatch(provider.detect) ||
|
|
160
|
+
(provider.id === 'opencode' && safeStat(opencodeDir(), 'isDirectory'));
|
|
161
|
+
}
|
|
162
|
+
|
|
149
163
|
// ── spawn helpers ───────────────────────────────────────────────────────────
|
|
150
164
|
function quoteWin(a) {
|
|
151
165
|
if (!IS_WIN) return a;
|
|
@@ -184,6 +198,11 @@ function claudeDir(opts) {
|
|
|
184
198
|
// (tests, containers); mirrors the CLAUDE_CONFIG_DIR precedent for Claude.
|
|
185
199
|
function homeDir() { return process.env.CHISLE_HOME || os.homedir(); }
|
|
186
200
|
|
|
201
|
+
function opencodeDir() { return getOpencodeDir({ home: homeDir() }); }
|
|
202
|
+
// Copilot CLI reads hooks, instructions and MCP config under COPILOT_HOME.
|
|
203
|
+
function copilotDir() { return process.env.COPILOT_HOME || path.join(homeDir(), '.copilot'); }
|
|
204
|
+
function opencodeStateDir() { return getOpencodeStateDir({ home: homeDir() }); }
|
|
205
|
+
|
|
187
206
|
// ── what is already installed ───────────────────────────────────────────────
|
|
188
207
|
// Every install path skips when Chisle is already present, which is right for
|
|
189
208
|
// `npx chisle` and useless for an upgrade: the run reports success and changes
|
|
@@ -221,13 +240,15 @@ function installedFor(id, opts) {
|
|
|
221
240
|
}
|
|
222
241
|
case 'opencode': {
|
|
223
242
|
try {
|
|
224
|
-
const md = path.join(
|
|
243
|
+
const md = path.join(opencodeDir(), 'AGENTS.md');
|
|
225
244
|
if (fs.existsSync(md) && fs.readFileSync(md, 'utf8').includes(FENCE_BEGIN)) return true;
|
|
226
245
|
} catch (_) {}
|
|
227
|
-
return fs.existsSync(path.join(
|
|
246
|
+
return fs.existsSync(path.join(opencodeDir(), 'skills', 'chisle', 'SKILL.md'));
|
|
228
247
|
}
|
|
229
248
|
case 'hermes':
|
|
230
249
|
return fs.existsSync(path.join(homeDir(), '.hermes', 'skills', 'chisle', 'SKILL.md'));
|
|
250
|
+
case 'copilot-cli':
|
|
251
|
+
return fs.existsSync(path.join(copilotDir(), 'hooks', 'chisle.json'));
|
|
231
252
|
default: {
|
|
232
253
|
const prov = PROVIDERS.find(x => x.id === id);
|
|
233
254
|
return !!(prov && prov.rule && fs.existsSync(path.join(process.cwd(), prov.rule)));
|
|
@@ -506,13 +527,13 @@ function installCodex(ctx) {
|
|
|
506
527
|
}
|
|
507
528
|
|
|
508
529
|
// ── OpenCode ─────────────────────────────────────────────────────────────
|
|
509
|
-
// Global
|
|
510
|
-
//
|
|
511
|
-
//
|
|
530
|
+
// Global AGENTS.md carries the always-on output axis next to existing user
|
|
531
|
+
// instructions; the bundled skills copy under the same OpenCode config root
|
|
532
|
+
// for on-demand loading via the native skill tool
|
|
512
533
|
// (the global skills dir is scanned by default, so skills.paths needs no
|
|
513
534
|
// edit and no second system message is added).
|
|
514
535
|
// Copies the OpenCode compression plugin and the shared compressor core it
|
|
515
|
-
// requires into
|
|
536
|
+
// requires into the configured OpenCode plugins directory. It auto-discovers *.js
|
|
516
537
|
// or *.ts file in that dir at startup (not *.mjs), and its config dir is
|
|
517
538
|
// type:module, so the ESM source ships as chisle.js. Returns the number of
|
|
518
539
|
// files written. Never called on a dry run (installOpencode returns early),
|
|
@@ -523,8 +544,8 @@ function installCodex(ctx) {
|
|
|
523
544
|
// dies on its first `require` with a ReferenceError that takes the whole
|
|
524
545
|
// plugin with it (loadFrom only swallows MODULE_NOT_FOUND). Bun is lenient
|
|
525
546
|
// enough not to care; Node is not, so pin it rather than rely on the runtime.
|
|
526
|
-
function copyOpencodePlugin() {
|
|
527
|
-
const pluginDir = path.join(
|
|
547
|
+
function copyOpencodePlugin(root) {
|
|
548
|
+
const pluginDir = path.join(root, 'plugins');
|
|
528
549
|
const hooksDir = path.join(pluginDir, 'chisle-hooks');
|
|
529
550
|
const files = [
|
|
530
551
|
['.opencode/plugins/chisle.mjs', path.join(pluginDir, 'chisle.js')],
|
|
@@ -546,9 +567,10 @@ function installOpencode(ctx) {
|
|
|
546
567
|
const { say, note, opts, results } = ctx;
|
|
547
568
|
results.detected++;
|
|
548
569
|
say('\u2192 OpenCode detected');
|
|
549
|
-
const
|
|
550
|
-
const
|
|
551
|
-
const
|
|
570
|
+
const root = opencodeDir();
|
|
571
|
+
const target = path.join(root, 'AGENTS.md');
|
|
572
|
+
const skillsDst = path.join(root, 'skills');
|
|
573
|
+
const pluginDst = path.join(root, 'plugins');
|
|
552
574
|
|
|
553
575
|
if (opts.dryRun) {
|
|
554
576
|
note(' would write chisle ruleset to ' + target);
|
|
@@ -563,7 +585,7 @@ function installOpencode(ctx) {
|
|
|
563
585
|
const st = writeFencedRuleset(target, opts, note);
|
|
564
586
|
const n = copySkills(skillsDst, opts);
|
|
565
587
|
process.stdout.write(' installed: ' + n + ' skill file(s) to ' + skillsDst + '\n');
|
|
566
|
-
const p = copyOpencodePlugin();
|
|
588
|
+
const p = copyOpencodePlugin(root);
|
|
567
589
|
process.stdout.write(' installed: ' + p + ' plugin file(s) to ' + pluginDst + '\n');
|
|
568
590
|
if (st === 'installed') results.installed.push('opencode');
|
|
569
591
|
else results.skipped.push(['opencode', 'ruleset already present; skills + plugin refreshed']);
|
|
@@ -571,6 +593,65 @@ function installOpencode(ctx) {
|
|
|
571
593
|
process.stdout.write('\n');
|
|
572
594
|
}
|
|
573
595
|
|
|
596
|
+
// ── Copilot CLI ─────────────────────────────────────────────────────────────
|
|
597
|
+
// Rules: fenced block in ~/.copilot/copilot-instructions.md, Copilot CLI's
|
|
598
|
+
// user-level instructions file. Input axis: a user-level postToolUse hook
|
|
599
|
+
// (~/.copilot/hooks/chisle.json, no workspace admission needed) running the
|
|
600
|
+
// shared compressor, which already speaks Copilot's camelCase payload and
|
|
601
|
+
// answers with modifiedResult. Verified live on Copilot CLI 1.0.95: a
|
|
602
|
+
// 13.9k-char shell result reached the model at ~100 lines with the mid-log
|
|
603
|
+
// error line and the `<shellId: 0 completed with exit code 0>` tail intact.
|
|
604
|
+
function copilotHookConfig(hookPath) {
|
|
605
|
+
return JSON.stringify({
|
|
606
|
+
version: 1,
|
|
607
|
+
hooks: {
|
|
608
|
+
postToolUse: [{
|
|
609
|
+
type: 'command',
|
|
610
|
+
bash: 'node "' + hookPath + '"',
|
|
611
|
+
powershell: 'node "' + hookPath + '"',
|
|
612
|
+
timeoutSec: 10,
|
|
613
|
+
}],
|
|
614
|
+
},
|
|
615
|
+
}, null, 2) + '\n';
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
function installCopilotCli(ctx) {
|
|
619
|
+
const { say, note, opts, results } = ctx;
|
|
620
|
+
results.detected++;
|
|
621
|
+
say('\u2192 Copilot CLI detected');
|
|
622
|
+
const root = copilotDir();
|
|
623
|
+
const rules = path.join(root, 'copilot-instructions.md');
|
|
624
|
+
const hooksDir = path.join(root, 'chisle-hooks');
|
|
625
|
+
const hookFile = path.join(root, 'hooks', 'chisle.json');
|
|
626
|
+
|
|
627
|
+
if (opts.dryRun) {
|
|
628
|
+
note(' would write chisle ruleset to ' + rules);
|
|
629
|
+
note(' would copy compressor to ' + hooksDir);
|
|
630
|
+
note(' would write postToolUse hook ' + hookFile);
|
|
631
|
+
results.installed.push('copilot-cli');
|
|
632
|
+
process.stdout.write('\n');
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
try {
|
|
637
|
+
const st = writeFencedRuleset(rules, opts, note);
|
|
638
|
+
fs.mkdirSync(hooksDir, { recursive: true });
|
|
639
|
+
for (const f of ['chisle-compress-output.js', 'chisle-config.js']) {
|
|
640
|
+
fs.copyFileSync(path.join(REPO_ROOT, 'hooks', f), path.join(hooksDir, f));
|
|
641
|
+
}
|
|
642
|
+
fs.writeFileSync(path.join(hooksDir, 'package.json'), '{ "type": "commonjs" }\n');
|
|
643
|
+
const hookExisted = fs.existsSync(hookFile);
|
|
644
|
+
if (!hookExisted || opts.force) {
|
|
645
|
+
fs.mkdirSync(path.dirname(hookFile), { recursive: true });
|
|
646
|
+
fs.writeFileSync(hookFile, copilotHookConfig(path.join(hooksDir, 'chisle-compress-output.js')), { mode: 0o644 });
|
|
647
|
+
process.stdout.write(' installed: ' + hookFile + '\n');
|
|
648
|
+
}
|
|
649
|
+
if (st === 'installed' || !hookExisted) results.installed.push('copilot-cli');
|
|
650
|
+
else results.skipped.push(['copilot-cli', 'already present; compressor refreshed']);
|
|
651
|
+
} catch (e) { results.failed.push(['copilot-cli', (e && e.message) || 'write failed']); }
|
|
652
|
+
process.stdout.write('\n');
|
|
653
|
+
}
|
|
654
|
+
|
|
574
655
|
// ── Hermes ──────────────────────────────────────────────────────────
|
|
575
656
|
// Agent-Skills host: bundled skills land verbatim in ~/.hermes/skills, where
|
|
576
657
|
// Hermes discovers them as /chisle slash commands. No ruleset is injected:
|
|
@@ -597,7 +678,7 @@ function installHermes(ctx) {
|
|
|
597
678
|
process.stdout.write('\n');
|
|
598
679
|
}
|
|
599
680
|
|
|
600
|
-
// ── Project-scoped rule agents (Cursor/Windsurf/Cline/Kiro/Copilot)
|
|
681
|
+
// ── Project-scoped rule agents (Cursor/Windsurf/Cline/Kiro/Copilot/Antigravity)
|
|
601
682
|
function installProjectRule(ctx, prov) {
|
|
602
683
|
const { say, note, warn, opts, results } = ctx;
|
|
603
684
|
results.detected++;
|
|
@@ -686,7 +767,9 @@ function uninstall(ctx) {
|
|
|
686
767
|
}
|
|
687
768
|
|
|
688
769
|
if (wants('opencode')) {
|
|
689
|
-
const
|
|
770
|
+
const ocRoot = opencodeDir();
|
|
771
|
+
const ocState = opencodeStateDir();
|
|
772
|
+
const ocMd = path.join(ocRoot, 'AGENTS.md');
|
|
690
773
|
if (fs.existsSync(ocMd)) {
|
|
691
774
|
const txt = fs.readFileSync(ocMd, 'utf8');
|
|
692
775
|
const b = txt.indexOf(FENCE_BEGIN);
|
|
@@ -700,17 +783,17 @@ function uninstall(ctx) {
|
|
|
700
783
|
note(' removed chisle block from ' + ocMd); touched++;
|
|
701
784
|
}
|
|
702
785
|
}
|
|
703
|
-
const ocSkills = path.join(
|
|
786
|
+
const ocSkills = path.join(ocRoot, 'skills');
|
|
704
787
|
const ocGone = opts.dryRun ? ownedSkillNames().filter(function (nm) { return fs.existsSync(path.join(ocSkills, nm)); }).length : pruneSkills(ocSkills);
|
|
705
788
|
if (ocGone > 0) { note(' removed ' + ocGone + ' chisle skill dir(s) from ' + ocSkills); touched++; }
|
|
706
789
|
|
|
707
|
-
const ocPlugins = path.join(
|
|
790
|
+
const ocPlugins = path.join(ocRoot, 'plugins');
|
|
708
791
|
for (const rel of ['chisle.js', 'chisle-hooks']) {
|
|
709
792
|
const dst = path.join(ocPlugins, rel);
|
|
710
793
|
if (fs.existsSync(dst)) { if (!opts.dryRun) fs.rmSync(dst, { recursive: true, force: true }); note(' removed ' + dst); touched++; }
|
|
711
794
|
}
|
|
712
795
|
for (const f of ['chisle-spill', 'chisle-dedup', '.chisle-compress-stats.json', '.chisle-compress-last.json']) {
|
|
713
|
-
const p = path.join(
|
|
796
|
+
const p = path.join(ocState, f);
|
|
714
797
|
if (fs.existsSync(p)) { if (!opts.dryRun) fs.rmSync(p, { recursive: true, force: true }); note(' removed ' + p); touched++; }
|
|
715
798
|
}
|
|
716
799
|
}
|
|
@@ -721,9 +804,39 @@ function uninstall(ctx) {
|
|
|
721
804
|
if (hGone > 0) { note(' removed ' + hGone + ' chisle skill dir(s) from ' + hSkills); touched++; }
|
|
722
805
|
}
|
|
723
806
|
|
|
807
|
+
if (wants('copilot-cli')) {
|
|
808
|
+
const cpRoot = copilotDir();
|
|
809
|
+
const cpMd = path.join(cpRoot, 'copilot-instructions.md');
|
|
810
|
+
if (fs.existsSync(cpMd)) {
|
|
811
|
+
const txt = fs.readFileSync(cpMd, 'utf8');
|
|
812
|
+
const b = txt.indexOf(FENCE_BEGIN);
|
|
813
|
+
const e = b === -1 ? -1 : txt.indexOf(FENCE_END, b);
|
|
814
|
+
if (b !== -1 && e !== -1) {
|
|
815
|
+
const tail = txt.slice(e + FENCE_END.length).replace(/^\n/, '');
|
|
816
|
+
const stripped = (txt.slice(0, b) + tail).replace(/\n{3,}/g, '\n\n').replace(/\n{2,}$/, '\n');
|
|
817
|
+
if (!opts.dryRun) fs.writeFileSync(cpMd, stripped, { mode: 0o644 });
|
|
818
|
+
note(' removed chisle block from ' + cpMd); touched++;
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
for (const rel of [path.join('hooks', 'chisle.json'), 'chisle-hooks', 'chisle-spill', 'chisle-dedup',
|
|
822
|
+
'.chisle-compress-stats.json']) {
|
|
823
|
+
const p = path.join(cpRoot, rel);
|
|
824
|
+
if (fs.existsSync(p)) { if (!opts.dryRun) fs.rmSync(p, { recursive: true, force: true }); note(' removed ' + p); touched++; }
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
// MCP servers routed through the proxy by `npx chisle mcp wrap`: restore
|
|
829
|
+
// their original commands before anything else could remove the proxy.
|
|
830
|
+
if (!opts.only.length) {
|
|
831
|
+
const r = MCP.run(['unwrap'].concat(opts.dryRun ? ['--dry-run'] : []), () => {});
|
|
832
|
+
if (r.changed) { note(' restored ' + r.changed + ' MCP server command(s) wrapped by `chisle mcp wrap`'); touched++; }
|
|
833
|
+
const pd = MCP.proxyDir();
|
|
834
|
+
if (fs.existsSync(pd)) { if (!opts.dryRun) fs.rmSync(pd, { recursive: true, force: true }); note(' removed ' + pd); touched++; }
|
|
835
|
+
}
|
|
836
|
+
|
|
724
837
|
if (!opts.only.length || opts.only.some(id => PROVIDERS.find(p => p.id === id).scope === 'project')) {
|
|
725
838
|
note('');
|
|
726
|
-
note('Project-scoped rule files (.cursor/, .windsurf/, .clinerules/, .kiro/, .github/copilot-instructions.md)');
|
|
839
|
+
note('Project-scoped rule files (.cursor/, .windsurf/, .clinerules/, .kiro/, .agents/rules/, .github/copilot-instructions.md)');
|
|
727
840
|
note('live in your project repos, so remove them per-project with git if you added them there.');
|
|
728
841
|
}
|
|
729
842
|
say(touched ? c.green(`\nUninstalled. ${touched} item(s) cleaned.`) : c.yellow('\nNothing to uninstall.'));
|
|
@@ -743,18 +856,26 @@ Flags:
|
|
|
743
856
|
--only <id> Limit install/uninstall to one agent (repeatable)
|
|
744
857
|
--dry-run Print actions, change nothing
|
|
745
858
|
--force Reinstall / overwrite even if already present
|
|
746
|
-
--uninstall, -u Remove what chisle installed
|
|
859
|
+
--uninstall, -u Remove what chisle installed (also restores wrapped MCP servers)
|
|
747
860
|
--config-dir <p> Override Claude config dir (default: $CLAUDE_CONFIG_DIR or ~/.claude)
|
|
748
861
|
--no-color Disable ANSI color
|
|
749
862
|
--help, -h This help
|
|
750
863
|
|
|
751
864
|
Agents: ${PROVIDERS.map(p => p.id).join(', ')}
|
|
752
865
|
|
|
866
|
+
MCP output compression (opt-in, any agent that launches stdio MCP servers):
|
|
867
|
+
npx chisle mcp wrap [--only <id>] [--file <mcp.json>] [--dry-run]
|
|
868
|
+
npx chisle mcp unwrap [--only <id>] [--file <mcp.json>] [--dry-run]
|
|
869
|
+
npx chisle mcp status
|
|
870
|
+
Targets: ${[...new Set(MCP.targets().map(t => t.id))].join(', ')}. Never Claude Code
|
|
871
|
+
(its own hook already compresses MCP output).
|
|
872
|
+
|
|
753
873
|
Examples:
|
|
754
874
|
npx chisle # auto-detect + install
|
|
755
875
|
npx chisle --only claude # just Claude Code
|
|
756
876
|
npx chisle --dry-run # preview
|
|
757
877
|
npx chisle --stats # what the compressor has saved
|
|
878
|
+
npx chisle mcp wrap # compress MCP tool output in Cursor, Gemini, ...
|
|
758
879
|
npx chisle@latest --update # upgrade what is already installed
|
|
759
880
|
`);
|
|
760
881
|
}
|
|
@@ -766,7 +887,10 @@ Examples:
|
|
|
766
887
|
// without the ruleset, which is what the benchmark arms are for. A counter that
|
|
767
888
|
// blended the two would be inventing the interesting half.
|
|
768
889
|
function printStats(c, opts) {
|
|
769
|
-
const
|
|
890
|
+
const only = opts.only.length === 1 ? opts.only[0] : null;
|
|
891
|
+
const dir = only === 'opencode' ? opencodeStateDir()
|
|
892
|
+
: only === 'copilot-cli' ? copilotDir()
|
|
893
|
+
: claudeDir(opts);
|
|
770
894
|
const p = path.join(dir, '.chisle-compress-stats.json');
|
|
771
895
|
process.stdout.write(c.orange('chisle') + ': tool-output savings\n\n');
|
|
772
896
|
|
|
@@ -801,7 +925,7 @@ function printStats(c, opts) {
|
|
|
801
925
|
function printList(c) {
|
|
802
926
|
process.stdout.write(c.orange('chisle') + ': detected agents:\n\n');
|
|
803
927
|
for (const p of PROVIDERS) {
|
|
804
|
-
const found =
|
|
928
|
+
const found = providerDetected(p);
|
|
805
929
|
const mark = found ? c.green('✓') : c.dim('·');
|
|
806
930
|
const scope = p.scope === 'project' ? c.dim(' (project-scoped)') : '';
|
|
807
931
|
process.stdout.write(` ${mark} ${p.label}${scope}\n`);
|
|
@@ -811,6 +935,7 @@ function printList(c) {
|
|
|
811
935
|
|
|
812
936
|
// ── main ────────────────────────────────────────────────────────────────────
|
|
813
937
|
function main() {
|
|
938
|
+
if (process.argv[2] === 'mcp') return MCP.main(process.argv.slice(3));
|
|
814
939
|
const opts = parseArgs(process.argv.slice(2));
|
|
815
940
|
const c = makeChalk(opts.noColor);
|
|
816
941
|
|
|
@@ -830,13 +955,13 @@ function main() {
|
|
|
830
955
|
say(c.dim(opts.dryRun ? ' (dry run, nothing will change)' : ' maximum signal, minimum noise'));
|
|
831
956
|
say('');
|
|
832
957
|
|
|
833
|
-
let targets = PROVIDERS.filter(p => opts.only.length ? opts.only.includes(p.id) :
|
|
958
|
+
let targets = PROVIDERS.filter(p => opts.only.length ? opts.only.includes(p.id) : providerDetected(p));
|
|
834
959
|
if (opts.update) targets = targets.filter(p => installedFor(p.id, opts));
|
|
835
960
|
if (targets.length === 0) {
|
|
836
961
|
if (opts.update) {
|
|
837
962
|
warn('Nothing to update: Chisle is not installed for any detected agent.');
|
|
838
963
|
note('Run `npx chisle` to install it, or `npx chisle --list` to see what we look for.');
|
|
839
|
-
note('Project-scoped agents (Cursor, Windsurf, Cline, Kiro, Copilot) are per-repo:');
|
|
964
|
+
note('Project-scoped agents (Cursor, Windsurf, Cline, Kiro, Copilot, Antigravity) are per-repo:');
|
|
840
965
|
note('run this from the project that has the rule file.');
|
|
841
966
|
return;
|
|
842
967
|
}
|
|
@@ -852,6 +977,7 @@ function main() {
|
|
|
852
977
|
else if (p.id === 'codex') installCodex(ctx);
|
|
853
978
|
else if (p.id === 'opencode') installOpencode(ctx);
|
|
854
979
|
else if (p.id === 'hermes') installHermes(ctx);
|
|
980
|
+
else if (p.id === 'copilot-cli') installCopilotCli(ctx);
|
|
855
981
|
else installProjectRule(ctx, p);
|
|
856
982
|
}
|
|
857
983
|
|
package/bin/lib/mcp.js
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
// chisle — `npx chisle mcp <wrap|unwrap|status>`: route stdio MCP servers
|
|
2
|
+
// through hooks/chisle-mcp-proxy.js so their oversized tool results get the
|
|
3
|
+
// same compression Claude Code and Pi get from PostToolUse.
|
|
4
|
+
//
|
|
5
|
+
// Opt-in, because it rewrites other tools' config files:
|
|
6
|
+
// - strict JSON only: a file with comments is reported and left alone,
|
|
7
|
+
// since rewriting it would drop them
|
|
8
|
+
// - first write of each file keeps a `<file>.chisle-bak` copy
|
|
9
|
+
// - only stdio entries (`command`) are wrapped; url/http servers untouched
|
|
10
|
+
// - wrapped form is reversible from the args alone, no extra keys a host
|
|
11
|
+
// schema might reject: { command: "node", args: [proxy, "--", cmd, ...args] }
|
|
12
|
+
//
|
|
13
|
+
// Claude Code is never a target: its PostToolUse hook already compresses
|
|
14
|
+
// mcp__* output, and wrapping would only add a process per server.
|
|
15
|
+
// Codex (TOML config) is not wrapped automatically.
|
|
16
|
+
|
|
17
|
+
'use strict';
|
|
18
|
+
|
|
19
|
+
const fs = require('fs');
|
|
20
|
+
const os = require('os');
|
|
21
|
+
const path = require('path');
|
|
22
|
+
const { getConfigDir } = require('../../hooks/chisle-config');
|
|
23
|
+
|
|
24
|
+
const REPO_ROOT = path.resolve(__dirname, '..', '..');
|
|
25
|
+
const PROXY_FILE = 'chisle-mcp-proxy.js';
|
|
26
|
+
const PROXY_SOURCES = ['chisle-mcp-proxy.js', 'chisle-compress-output.js', 'chisle-config.js'];
|
|
27
|
+
|
|
28
|
+
function home() { return process.env.CHISLE_HOME || os.homedir(); }
|
|
29
|
+
function proxyDir() { return path.join(getConfigDir(), 'mcp-proxy'); }
|
|
30
|
+
|
|
31
|
+
// VS Code-family user dirs, for Cline's global MCP settings.
|
|
32
|
+
function vscodeUserDirs() {
|
|
33
|
+
const base = process.platform === 'darwin' ? path.join(home(), 'Library', 'Application Support')
|
|
34
|
+
: process.platform === 'win32' ? (process.env.APPDATA || path.join(home(), 'AppData', 'Roaming'))
|
|
35
|
+
: (process.env.XDG_CONFIG_HOME || path.join(home(), '.config'));
|
|
36
|
+
return ['Code', 'Code - Insiders', 'VSCodium', 'Cursor', 'Windsurf'].map(n => path.join(base, n, 'User'));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Global MCP config files, all `{ "mcpServers": { name: { command, args } } }`.
|
|
40
|
+
function targets() {
|
|
41
|
+
const h = home();
|
|
42
|
+
const copilot = process.env.COPILOT_HOME || path.join(h, '.copilot');
|
|
43
|
+
const list = [
|
|
44
|
+
['cursor', 'Cursor', path.join(h, '.cursor', 'mcp.json')],
|
|
45
|
+
['windsurf', 'Windsurf', path.join(h, '.codeium', 'windsurf', 'mcp_config.json')],
|
|
46
|
+
['gemini', 'Gemini CLI', path.join(h, '.gemini', 'settings.json')],
|
|
47
|
+
['qwen', 'Qwen Code', path.join(h, '.qwen', 'settings.json')],
|
|
48
|
+
['kiro', 'Kiro', path.join(h, '.kiro', 'settings', 'mcp.json')],
|
|
49
|
+
['copilot-cli', 'Copilot CLI', path.join(copilot, 'mcp-config.json')],
|
|
50
|
+
];
|
|
51
|
+
for (const dir of vscodeUserDirs()) {
|
|
52
|
+
list.push(['cline', 'Cline', path.join(dir, 'globalStorage', 'saoudrizwan.claude-dev', 'settings', 'cline_mcp_settings.json')]);
|
|
53
|
+
}
|
|
54
|
+
return list.map(([id, label, file]) => ({ id, label, file }));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function isWrapped(entry) {
|
|
58
|
+
return !!(entry && Array.isArray(entry.args) && typeof entry.args[0] === 'string' &&
|
|
59
|
+
path.basename(entry.args[0]) === PROXY_FILE && entry.args[1] === '--');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function isStdio(entry) {
|
|
63
|
+
return !!(entry && typeof entry.command === 'string' && entry.command &&
|
|
64
|
+
!entry.url && !entry.httpUrl && !entry.serverUrl &&
|
|
65
|
+
(entry.type == null || entry.type === 'stdio' || entry.type === 'local'));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function wrapEntry(entry, proxy) {
|
|
69
|
+
const args = Array.isArray(entry.args) ? entry.args : [];
|
|
70
|
+
entry.args = [proxy, '--', entry.command, ...args];
|
|
71
|
+
entry.command = 'node';
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function unwrapEntry(entry) {
|
|
75
|
+
const rest = entry.args.slice(2);
|
|
76
|
+
entry.command = rest[0];
|
|
77
|
+
entry.args = rest.slice(1);
|
|
78
|
+
if (!entry.args.length) delete entry.args; // wrap added it; most configs omit an empty one
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function readConfig(file) {
|
|
82
|
+
let raw;
|
|
83
|
+
try { raw = fs.readFileSync(file, 'utf8'); } catch (e) { return { missing: true }; }
|
|
84
|
+
try {
|
|
85
|
+
const data = JSON.parse(raw.replace(/^/, ''));
|
|
86
|
+
if (!data || typeof data !== 'object' || Array.isArray(data)) return { error: 'not a JSON object' };
|
|
87
|
+
return { data, raw };
|
|
88
|
+
} catch (e) { return { error: 'not strict JSON (comments?); edit by hand' }; }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function writeConfig(file, raw, data) {
|
|
92
|
+
const bak = file + '.chisle-bak';
|
|
93
|
+
if (!fs.existsSync(bak)) fs.writeFileSync(bak, raw, { mode: 0o600 });
|
|
94
|
+
let mode = 0o600;
|
|
95
|
+
try { mode = fs.statSync(file).mode & 0o777; } catch (e) {}
|
|
96
|
+
const tmp = file + '.chisle-tmp-' + process.pid;
|
|
97
|
+
fs.writeFileSync(tmp, JSON.stringify(data, null, 2) + '\n', { mode });
|
|
98
|
+
fs.renameSync(tmp, file);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function installProxy() {
|
|
102
|
+
const dir = proxyDir();
|
|
103
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
104
|
+
for (const f of PROXY_SOURCES) fs.copyFileSync(path.join(REPO_ROOT, 'hooks', f), path.join(dir, f));
|
|
105
|
+
fs.writeFileSync(path.join(dir, 'package.json'), '{ "type": "commonjs" }\n');
|
|
106
|
+
return path.join(dir, PROXY_FILE);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function parse(argv) {
|
|
110
|
+
const o = { action: argv[0], dryRun: false, only: [], files: [] };
|
|
111
|
+
for (let i = 1; i < argv.length; i++) {
|
|
112
|
+
const a = argv[i];
|
|
113
|
+
if (a === '--dry-run') o.dryRun = true;
|
|
114
|
+
else if (a === '--only' && argv[i + 1]) o.only.push(argv[++i]);
|
|
115
|
+
else if (a === '--file' && argv[i + 1]) o.files.push(path.resolve(argv[++i]));
|
|
116
|
+
else if (a === '--no-color' || a === '--') continue;
|
|
117
|
+
else throw new Error('unknown option: ' + a);
|
|
118
|
+
}
|
|
119
|
+
if (!['wrap', 'unwrap', 'status'].includes(o.action)) throw new Error('usage: npx chisle mcp <wrap|unwrap|status> [--only <agent>] [--file <mcp.json>] [--dry-run]');
|
|
120
|
+
return o;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function selected(o) {
|
|
124
|
+
if (o.files.length) return o.files.map(file => ({ id: 'file', label: 'custom', file }));
|
|
125
|
+
return targets().filter(t => !o.only.length || o.only.includes(t.id));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Returns { changed, files } so `--uninstall` can reuse it quietly.
|
|
129
|
+
function run(argv, out) {
|
|
130
|
+
out = out || (s => process.stdout.write(s + '\n'));
|
|
131
|
+
const o = parse(argv);
|
|
132
|
+
const proxy = o.action === 'wrap' && !o.dryRun ? installProxy() : path.join(proxyDir(), PROXY_FILE);
|
|
133
|
+
let changed = 0, found = 0;
|
|
134
|
+
|
|
135
|
+
for (const t of selected(o)) {
|
|
136
|
+
const cfg = readConfig(t.file);
|
|
137
|
+
if (cfg.missing) { if (o.files.length) out(' ' + t.file + ': not found'); continue; }
|
|
138
|
+
found++;
|
|
139
|
+
if (cfg.error) { out(' ' + t.label + ': ' + t.file + ': skipped, ' + cfg.error); continue; }
|
|
140
|
+
const servers = cfg.data.mcpServers;
|
|
141
|
+
if (!servers || typeof servers !== 'object') { out(' ' + t.label + ': no mcpServers in ' + t.file); continue; }
|
|
142
|
+
|
|
143
|
+
const names = Object.keys(servers);
|
|
144
|
+
const wrapped = names.filter(n => isWrapped(servers[n]));
|
|
145
|
+
if (o.action === 'status') {
|
|
146
|
+
out(' ' + t.label + ': ' + wrapped.length + '/' + names.filter(n => isStdio(servers[n])).length +
|
|
147
|
+
' stdio server(s) wrapped ' + t.file);
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const todo = o.action === 'wrap'
|
|
152
|
+
? names.filter(n => isStdio(servers[n]) && !isWrapped(servers[n]))
|
|
153
|
+
: wrapped;
|
|
154
|
+
if (!todo.length) { out(' ' + t.label + ': nothing to ' + o.action + ' ' + t.file); continue; }
|
|
155
|
+
for (const n of todo) (o.action === 'wrap' ? wrapEntry(servers[n], proxy) : unwrapEntry(servers[n]));
|
|
156
|
+
if (!o.dryRun) writeConfig(t.file, cfg.raw, cfg.data);
|
|
157
|
+
changed += todo.length;
|
|
158
|
+
out(' ' + t.label + ': ' + (o.dryRun ? 'would ' + o.action : o.action + 'ped') + ' ' + todo.join(', ') + ' ' + t.file);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (!found) out(' no MCP config files found' + (o.only.length ? ' for ' + o.only.join(', ') : ''));
|
|
162
|
+
if (o.action === 'status') {
|
|
163
|
+
const dir = process.env.CHISLE_STATE_DIR || getConfigDir();
|
|
164
|
+
let stats = null;
|
|
165
|
+
try { stats = JSON.parse(fs.readFileSync(path.join(dir, '.chisle-compress-stats.json'), 'utf8')); } catch (e) {}
|
|
166
|
+
out('\n saved so far: ' + (stats && Number.isFinite(stats.savedChars)
|
|
167
|
+
? stats.savedChars.toLocaleString('en-US') + ' chars over ' + stats.events + ' tool result(s)'
|
|
168
|
+
: 'nothing yet') + ' (' + dir + ')');
|
|
169
|
+
}
|
|
170
|
+
if (o.action === 'wrap' && changed && !o.dryRun) out('\n Restart the agent so it relaunches its MCP servers.');
|
|
171
|
+
return { changed, found };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function main(argv) {
|
|
175
|
+
try { run(argv); }
|
|
176
|
+
catch (e) { process.stderr.write('error: ' + e.message + '\n'); process.exit(2); }
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
module.exports = { main, run, targets, isWrapped, isStdio, proxyDir };
|