chisle 3.7.0 → 3.8.1
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 +5 -6
- package/.claude-plugin/plugin.json +8 -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 +4 -6
- package/NOTICE +12 -0
- package/README.md +61 -51
- package/bin/install.js +144 -22
- 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 +3 -2
- package/skills/chisle/SKILL.md +1 -1
package/bin/install.js
CHANGED
|
@@ -7,7 +7,8 @@
|
|
|
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
13
|
// - Cursor/Windsurf/Cline/Kiro/Copilot/Antigravity → project rule file dropped into CWD
|
|
13
14
|
//
|
|
@@ -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' },
|
|
@@ -150,6 +155,11 @@ function detectMatch(spec) {
|
|
|
150
155
|
return false;
|
|
151
156
|
}
|
|
152
157
|
|
|
158
|
+
function providerDetected(provider) {
|
|
159
|
+
return detectMatch(provider.detect) ||
|
|
160
|
+
(provider.id === 'opencode' && safeStat(opencodeDir(), 'isDirectory'));
|
|
161
|
+
}
|
|
162
|
+
|
|
153
163
|
// ── spawn helpers ───────────────────────────────────────────────────────────
|
|
154
164
|
function quoteWin(a) {
|
|
155
165
|
if (!IS_WIN) return a;
|
|
@@ -188,6 +198,11 @@ function claudeDir(opts) {
|
|
|
188
198
|
// (tests, containers); mirrors the CLAUDE_CONFIG_DIR precedent for Claude.
|
|
189
199
|
function homeDir() { return process.env.CHISLE_HOME || os.homedir(); }
|
|
190
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
|
+
|
|
191
206
|
// ── what is already installed ───────────────────────────────────────────────
|
|
192
207
|
// Every install path skips when Chisle is already present, which is right for
|
|
193
208
|
// `npx chisle` and useless for an upgrade: the run reports success and changes
|
|
@@ -225,13 +240,15 @@ function installedFor(id, opts) {
|
|
|
225
240
|
}
|
|
226
241
|
case 'opencode': {
|
|
227
242
|
try {
|
|
228
|
-
const md = path.join(
|
|
243
|
+
const md = path.join(opencodeDir(), 'AGENTS.md');
|
|
229
244
|
if (fs.existsSync(md) && fs.readFileSync(md, 'utf8').includes(FENCE_BEGIN)) return true;
|
|
230
245
|
} catch (_) {}
|
|
231
|
-
return fs.existsSync(path.join(
|
|
246
|
+
return fs.existsSync(path.join(opencodeDir(), 'skills', 'chisle', 'SKILL.md'));
|
|
232
247
|
}
|
|
233
248
|
case 'hermes':
|
|
234
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'));
|
|
235
252
|
default: {
|
|
236
253
|
const prov = PROVIDERS.find(x => x.id === id);
|
|
237
254
|
return !!(prov && prov.rule && fs.existsSync(path.join(process.cwd(), prov.rule)));
|
|
@@ -510,13 +527,13 @@ function installCodex(ctx) {
|
|
|
510
527
|
}
|
|
511
528
|
|
|
512
529
|
// ── OpenCode ─────────────────────────────────────────────────────────────
|
|
513
|
-
// Global
|
|
514
|
-
//
|
|
515
|
-
//
|
|
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
|
|
516
533
|
// (the global skills dir is scanned by default, so skills.paths needs no
|
|
517
534
|
// edit and no second system message is added).
|
|
518
535
|
// Copies the OpenCode compression plugin and the shared compressor core it
|
|
519
|
-
// requires into
|
|
536
|
+
// requires into the configured OpenCode plugins directory. It auto-discovers *.js
|
|
520
537
|
// or *.ts file in that dir at startup (not *.mjs), and its config dir is
|
|
521
538
|
// type:module, so the ESM source ships as chisle.js. Returns the number of
|
|
522
539
|
// files written. Never called on a dry run (installOpencode returns early),
|
|
@@ -527,8 +544,8 @@ function installCodex(ctx) {
|
|
|
527
544
|
// dies on its first `require` with a ReferenceError that takes the whole
|
|
528
545
|
// plugin with it (loadFrom only swallows MODULE_NOT_FOUND). Bun is lenient
|
|
529
546
|
// enough not to care; Node is not, so pin it rather than rely on the runtime.
|
|
530
|
-
function copyOpencodePlugin() {
|
|
531
|
-
const pluginDir = path.join(
|
|
547
|
+
function copyOpencodePlugin(root) {
|
|
548
|
+
const pluginDir = path.join(root, 'plugins');
|
|
532
549
|
const hooksDir = path.join(pluginDir, 'chisle-hooks');
|
|
533
550
|
const files = [
|
|
534
551
|
['.opencode/plugins/chisle.mjs', path.join(pluginDir, 'chisle.js')],
|
|
@@ -550,9 +567,10 @@ function installOpencode(ctx) {
|
|
|
550
567
|
const { say, note, opts, results } = ctx;
|
|
551
568
|
results.detected++;
|
|
552
569
|
say('\u2192 OpenCode detected');
|
|
553
|
-
const
|
|
554
|
-
const
|
|
555
|
-
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');
|
|
556
574
|
|
|
557
575
|
if (opts.dryRun) {
|
|
558
576
|
note(' would write chisle ruleset to ' + target);
|
|
@@ -567,7 +585,7 @@ function installOpencode(ctx) {
|
|
|
567
585
|
const st = writeFencedRuleset(target, opts, note);
|
|
568
586
|
const n = copySkills(skillsDst, opts);
|
|
569
587
|
process.stdout.write(' installed: ' + n + ' skill file(s) to ' + skillsDst + '\n');
|
|
570
|
-
const p = copyOpencodePlugin();
|
|
588
|
+
const p = copyOpencodePlugin(root);
|
|
571
589
|
process.stdout.write(' installed: ' + p + ' plugin file(s) to ' + pluginDst + '\n');
|
|
572
590
|
if (st === 'installed') results.installed.push('opencode');
|
|
573
591
|
else results.skipped.push(['opencode', 'ruleset already present; skills + plugin refreshed']);
|
|
@@ -575,6 +593,65 @@ function installOpencode(ctx) {
|
|
|
575
593
|
process.stdout.write('\n');
|
|
576
594
|
}
|
|
577
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
|
+
|
|
578
655
|
// ── Hermes ──────────────────────────────────────────────────────────
|
|
579
656
|
// Agent-Skills host: bundled skills land verbatim in ~/.hermes/skills, where
|
|
580
657
|
// Hermes discovers them as /chisle slash commands. No ruleset is injected:
|
|
@@ -690,7 +767,9 @@ function uninstall(ctx) {
|
|
|
690
767
|
}
|
|
691
768
|
|
|
692
769
|
if (wants('opencode')) {
|
|
693
|
-
const
|
|
770
|
+
const ocRoot = opencodeDir();
|
|
771
|
+
const ocState = opencodeStateDir();
|
|
772
|
+
const ocMd = path.join(ocRoot, 'AGENTS.md');
|
|
694
773
|
if (fs.existsSync(ocMd)) {
|
|
695
774
|
const txt = fs.readFileSync(ocMd, 'utf8');
|
|
696
775
|
const b = txt.indexOf(FENCE_BEGIN);
|
|
@@ -704,17 +783,17 @@ function uninstall(ctx) {
|
|
|
704
783
|
note(' removed chisle block from ' + ocMd); touched++;
|
|
705
784
|
}
|
|
706
785
|
}
|
|
707
|
-
const ocSkills = path.join(
|
|
786
|
+
const ocSkills = path.join(ocRoot, 'skills');
|
|
708
787
|
const ocGone = opts.dryRun ? ownedSkillNames().filter(function (nm) { return fs.existsSync(path.join(ocSkills, nm)); }).length : pruneSkills(ocSkills);
|
|
709
788
|
if (ocGone > 0) { note(' removed ' + ocGone + ' chisle skill dir(s) from ' + ocSkills); touched++; }
|
|
710
789
|
|
|
711
|
-
const ocPlugins = path.join(
|
|
790
|
+
const ocPlugins = path.join(ocRoot, 'plugins');
|
|
712
791
|
for (const rel of ['chisle.js', 'chisle-hooks']) {
|
|
713
792
|
const dst = path.join(ocPlugins, rel);
|
|
714
793
|
if (fs.existsSync(dst)) { if (!opts.dryRun) fs.rmSync(dst, { recursive: true, force: true }); note(' removed ' + dst); touched++; }
|
|
715
794
|
}
|
|
716
795
|
for (const f of ['chisle-spill', 'chisle-dedup', '.chisle-compress-stats.json', '.chisle-compress-last.json']) {
|
|
717
|
-
const p = path.join(
|
|
796
|
+
const p = path.join(ocState, f);
|
|
718
797
|
if (fs.existsSync(p)) { if (!opts.dryRun) fs.rmSync(p, { recursive: true, force: true }); note(' removed ' + p); touched++; }
|
|
719
798
|
}
|
|
720
799
|
}
|
|
@@ -725,6 +804,36 @@ function uninstall(ctx) {
|
|
|
725
804
|
if (hGone > 0) { note(' removed ' + hGone + ' chisle skill dir(s) from ' + hSkills); touched++; }
|
|
726
805
|
}
|
|
727
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
|
+
|
|
728
837
|
if (!opts.only.length || opts.only.some(id => PROVIDERS.find(p => p.id === id).scope === 'project')) {
|
|
729
838
|
note('');
|
|
730
839
|
note('Project-scoped rule files (.cursor/, .windsurf/, .clinerules/, .kiro/, .agents/rules/, .github/copilot-instructions.md)');
|
|
@@ -747,18 +856,26 @@ Flags:
|
|
|
747
856
|
--only <id> Limit install/uninstall to one agent (repeatable)
|
|
748
857
|
--dry-run Print actions, change nothing
|
|
749
858
|
--force Reinstall / overwrite even if already present
|
|
750
|
-
--uninstall, -u Remove what chisle installed
|
|
859
|
+
--uninstall, -u Remove what chisle installed (also restores wrapped MCP servers)
|
|
751
860
|
--config-dir <p> Override Claude config dir (default: $CLAUDE_CONFIG_DIR or ~/.claude)
|
|
752
861
|
--no-color Disable ANSI color
|
|
753
862
|
--help, -h This help
|
|
754
863
|
|
|
755
864
|
Agents: ${PROVIDERS.map(p => p.id).join(', ')}
|
|
756
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
|
+
|
|
757
873
|
Examples:
|
|
758
874
|
npx chisle # auto-detect + install
|
|
759
875
|
npx chisle --only claude # just Claude Code
|
|
760
876
|
npx chisle --dry-run # preview
|
|
761
877
|
npx chisle --stats # what the compressor has saved
|
|
878
|
+
npx chisle mcp wrap # compress MCP tool output in Cursor, Gemini, ...
|
|
762
879
|
npx chisle@latest --update # upgrade what is already installed
|
|
763
880
|
`);
|
|
764
881
|
}
|
|
@@ -770,7 +887,10 @@ Examples:
|
|
|
770
887
|
// without the ruleset, which is what the benchmark arms are for. A counter that
|
|
771
888
|
// blended the two would be inventing the interesting half.
|
|
772
889
|
function printStats(c, opts) {
|
|
773
|
-
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);
|
|
774
894
|
const p = path.join(dir, '.chisle-compress-stats.json');
|
|
775
895
|
process.stdout.write(c.orange('chisle') + ': tool-output savings\n\n');
|
|
776
896
|
|
|
@@ -805,7 +925,7 @@ function printStats(c, opts) {
|
|
|
805
925
|
function printList(c) {
|
|
806
926
|
process.stdout.write(c.orange('chisle') + ': detected agents:\n\n');
|
|
807
927
|
for (const p of PROVIDERS) {
|
|
808
|
-
const found =
|
|
928
|
+
const found = providerDetected(p);
|
|
809
929
|
const mark = found ? c.green('✓') : c.dim('·');
|
|
810
930
|
const scope = p.scope === 'project' ? c.dim(' (project-scoped)') : '';
|
|
811
931
|
process.stdout.write(` ${mark} ${p.label}${scope}\n`);
|
|
@@ -815,6 +935,7 @@ function printList(c) {
|
|
|
815
935
|
|
|
816
936
|
// ── main ────────────────────────────────────────────────────────────────────
|
|
817
937
|
function main() {
|
|
938
|
+
if (process.argv[2] === 'mcp') return MCP.main(process.argv.slice(3));
|
|
818
939
|
const opts = parseArgs(process.argv.slice(2));
|
|
819
940
|
const c = makeChalk(opts.noColor);
|
|
820
941
|
|
|
@@ -834,7 +955,7 @@ function main() {
|
|
|
834
955
|
say(c.dim(opts.dryRun ? ' (dry run, nothing will change)' : ' maximum signal, minimum noise'));
|
|
835
956
|
say('');
|
|
836
957
|
|
|
837
|
-
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));
|
|
838
959
|
if (opts.update) targets = targets.filter(p => installedFor(p.id, opts));
|
|
839
960
|
if (targets.length === 0) {
|
|
840
961
|
if (opts.update) {
|
|
@@ -856,6 +977,7 @@ function main() {
|
|
|
856
977
|
else if (p.id === 'codex') installCodex(ctx);
|
|
857
978
|
else if (p.id === 'opencode') installOpencode(ctx);
|
|
858
979
|
else if (p.id === 'hermes') installHermes(ctx);
|
|
980
|
+
else if (p.id === 'copilot-cli') installCopilotCli(ctx);
|
|
859
981
|
else installProjectRule(ctx, p);
|
|
860
982
|
}
|
|
861
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 };
|