multicorn-shield 0.11.0 → 0.13.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/CHANGELOG.md +39 -0
- package/dist/badge.js +4 -4
- package/dist/index.cjs +38 -19
- package/dist/index.d.cts +11 -2
- package/dist/index.d.ts +11 -2
- package/dist/index.js +37 -20
- package/dist/multicorn-proxy.js +578 -15
- package/dist/openclaw-hook/handler.js +0 -1
- package/dist/openclaw-plugin/multicorn-shield.js +11 -17
- package/dist/openclaw-plugin/openclaw.plugin.json +3 -1
- package/dist/proxy.cjs +174 -0
- package/dist/proxy.d.cts +228 -1
- package/dist/proxy.d.ts +228 -1
- package/dist/proxy.js +174 -1
- package/dist/shield-extension.js +1 -4
- package/package.json +4 -2
- package/plugins/cline/README.md +61 -0
- package/plugins/cline/hooks/scripts/post-tool-use.cjs +116 -0
- package/plugins/cline/hooks/scripts/pre-tool-use.cjs +271 -0
- package/plugins/cline/hooks/scripts/shared.cjs +303 -0
- package/plugins/gemini-cli/hooks/scripts/after-tool.cjs +110 -0
- package/plugins/gemini-cli/hooks/scripts/before-tool.cjs +197 -0
- package/plugins/gemini-cli/hooks/scripts/shared.cjs +319 -0
package/dist/multicorn-proxy.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { existsSync } from 'fs';
|
|
3
|
-
import { mkdir, writeFile, readFile, copyFile, unlink } from 'fs/promises';
|
|
2
|
+
import { existsSync, statSync } from 'fs';
|
|
3
|
+
import { mkdir, writeFile, readFile, copyFile, chmod, unlink } from 'fs/promises';
|
|
4
4
|
import { join, dirname } from 'path';
|
|
5
5
|
import { homedir } from 'os';
|
|
6
6
|
import { fileURLToPath } from 'url';
|
|
@@ -43,6 +43,23 @@ function withSpinner(message) {
|
|
|
43
43
|
}
|
|
44
44
|
};
|
|
45
45
|
}
|
|
46
|
+
var NativePluginPrerequisiteMissingError = class extends Error {
|
|
47
|
+
constructor() {
|
|
48
|
+
super("Native plugin prerequisites not met");
|
|
49
|
+
this.name = "NativePluginPrerequisiteMissingError";
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
function isExistingDirectory(path) {
|
|
53
|
+
try {
|
|
54
|
+
if (!existsSync(path)) return false;
|
|
55
|
+
return statSync(path).isDirectory();
|
|
56
|
+
} catch {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function nativePluginSkippedSaveNote(wizardCommand, productName) {
|
|
61
|
+
return "\n" + style.dim("Your agent config has been saved. Run ") + style.cyan(wizardCommand) + style.dim(` again after installing ${productName} to complete hook setup.`) + "\n";
|
|
62
|
+
}
|
|
46
63
|
var CONFIG_DIR = join(homedir(), ".multicorn");
|
|
47
64
|
var CONFIG_PATH = join(CONFIG_DIR, "config.json");
|
|
48
65
|
var OPENCLAW_CONFIG_PATH = join(homedir(), ".openclaw", "openclaw.json");
|
|
@@ -426,6 +443,18 @@ async function installWindsurfNativeHooks() {
|
|
|
426
443
|
`Could not find Shield Windsurf hook scripts at ${srcPre}. If you use npm, install the latest multicorn-shield package.`
|
|
427
444
|
);
|
|
428
445
|
}
|
|
446
|
+
const windsurfConfigDir = join(homedir(), ".codeium", "windsurf");
|
|
447
|
+
if (!isExistingDirectory(windsurfConfigDir)) {
|
|
448
|
+
process.stderr.write(
|
|
449
|
+
style.yellow("\u26A0") + " Windsurf does not appear to be installed (~/.codeium/windsurf/ not found).\n\n"
|
|
450
|
+
);
|
|
451
|
+
process.stderr.write(
|
|
452
|
+
"Open Windsurf at least once so this folder exists, or install from:\n " + style.cyan("https://windsurf.com/download") + "\n\n"
|
|
453
|
+
);
|
|
454
|
+
process.stderr.write("Then run this wizard again:\n");
|
|
455
|
+
process.stderr.write(" " + style.cyan("npx multicorn-proxy init") + "\n");
|
|
456
|
+
throw new NativePluginPrerequisiteMissingError();
|
|
457
|
+
}
|
|
429
458
|
const installDir = getWindsurfHooksInstallDir();
|
|
430
459
|
await mkdir(installDir, { recursive: true });
|
|
431
460
|
const destPre = join(installDir, "pre-action.cjs");
|
|
@@ -473,19 +502,302 @@ async function installWindsurfNativeHooks() {
|
|
|
473
502
|
await mkdir(hooksDir, { recursive: true });
|
|
474
503
|
await writeFile(hooksPath, JSON.stringify(base, null, 2) + "\n", { encoding: "utf8" });
|
|
475
504
|
}
|
|
476
|
-
|
|
505
|
+
function getClineHooksInstallDir() {
|
|
506
|
+
return join(homedir(), ".multicorn", "cline-hooks");
|
|
507
|
+
}
|
|
508
|
+
function getClineGlobalHooksDir() {
|
|
509
|
+
return join(homedir(), "Documents", "Cline", "Hooks");
|
|
510
|
+
}
|
|
511
|
+
async function installClineNativeHooks() {
|
|
512
|
+
const root = multicornShieldPackageRoot();
|
|
513
|
+
const srcPre = join(root, "plugins", "cline", "hooks", "scripts", "pre-tool-use.cjs");
|
|
514
|
+
const srcPost = join(root, "plugins", "cline", "hooks", "scripts", "post-tool-use.cjs");
|
|
515
|
+
const srcShared = join(root, "plugins", "cline", "hooks", "scripts", "shared.cjs");
|
|
516
|
+
if (!existsSync(srcPre) || !existsSync(srcPost) || !existsSync(srcShared)) {
|
|
517
|
+
throw new Error(
|
|
518
|
+
`Could not find Shield Cline hook scripts at ${srcPre}. If you use npm, install the latest multicorn-shield package.`
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
const clineDocsDir = join(homedir(), "Documents", "Cline");
|
|
522
|
+
if (!isExistingDirectory(clineDocsDir)) {
|
|
523
|
+
process.stderr.write(
|
|
524
|
+
style.yellow("\u26A0") + " Cline does not appear to be installed (~/Documents/Cline/ not found).\n\n"
|
|
525
|
+
);
|
|
526
|
+
process.stderr.write("Install the Cline VS Code extension first. See:\n");
|
|
527
|
+
process.stderr.write(
|
|
528
|
+
" " + style.cyan("https://docs.cline.bot/getting-started/installing-cline") + "\n\n"
|
|
529
|
+
);
|
|
530
|
+
process.stderr.write("Then run this wizard again:\n");
|
|
531
|
+
process.stderr.write(" " + style.cyan("npx multicorn-shield init") + "\n");
|
|
532
|
+
throw new NativePluginPrerequisiteMissingError();
|
|
533
|
+
}
|
|
534
|
+
const installDir = getClineHooksInstallDir();
|
|
535
|
+
await mkdir(installDir, { recursive: true });
|
|
536
|
+
const destPre = join(installDir, "pre-tool-use.cjs");
|
|
537
|
+
const destPost = join(installDir, "post-tool-use.cjs");
|
|
538
|
+
const destShared = join(installDir, "shared.cjs");
|
|
539
|
+
await copyFile(srcPre, destPre);
|
|
540
|
+
await copyFile(srcPost, destPost);
|
|
541
|
+
await copyFile(srcShared, destShared);
|
|
542
|
+
const hookScriptMode = 493;
|
|
543
|
+
await chmod(destPre, hookScriptMode);
|
|
544
|
+
await chmod(destPost, hookScriptMode);
|
|
545
|
+
await chmod(destShared, hookScriptMode);
|
|
546
|
+
const hooksDir = getClineGlobalHooksDir();
|
|
547
|
+
await mkdir(hooksDir, { recursive: true });
|
|
548
|
+
const preWrapper = join(hooksDir, "PreToolUse");
|
|
549
|
+
const postWrapper = join(hooksDir, "PostToolUse");
|
|
550
|
+
const preContent = `#!/usr/bin/env node
|
|
551
|
+
require(${JSON.stringify(destPre)});
|
|
552
|
+
`;
|
|
553
|
+
const postContent = `#!/usr/bin/env node
|
|
554
|
+
require(${JSON.stringify(destPost)});
|
|
555
|
+
`;
|
|
556
|
+
await writeFile(preWrapper, preContent, { encoding: "utf8", mode: 493 });
|
|
557
|
+
await writeFile(postWrapper, postContent, { encoding: "utf8", mode: 493 });
|
|
558
|
+
}
|
|
559
|
+
async function promptClineIntegrationMode(ask) {
|
|
560
|
+
process.stderr.write("\n" + style.bold("Cline integration") + "\n");
|
|
561
|
+
process.stderr.write(
|
|
562
|
+
" " + style.violet("1") + ". Native plugin (recommended) - Cline Hooks see every file, terminal, browser, and MCP action\n"
|
|
563
|
+
);
|
|
564
|
+
process.stderr.write(
|
|
565
|
+
" " + style.violet("2") + ". Hosted proxy - govern MCP traffic only (paste proxy URL into Cline MCP settings)\n"
|
|
566
|
+
);
|
|
567
|
+
let choice = 0;
|
|
568
|
+
while (choice === 0) {
|
|
569
|
+
const input = await ask("Choose integration (1-2): ");
|
|
570
|
+
const num = parseInt(input.trim(), 10);
|
|
571
|
+
if (num === 1) choice = 1;
|
|
572
|
+
if (num === 2) choice = 2;
|
|
573
|
+
}
|
|
574
|
+
return choice === 1 ? "native" : "hosted";
|
|
575
|
+
}
|
|
576
|
+
function getGeminiCliHooksInstallDir() {
|
|
577
|
+
return join(homedir(), ".multicorn", "gemini-cli-hooks");
|
|
578
|
+
}
|
|
579
|
+
function getGeminiCliSettingsPath() {
|
|
580
|
+
return join(homedir(), ".gemini", "settings.json");
|
|
581
|
+
}
|
|
582
|
+
function geminiInnerHooksReferenceShield(inner, multicornName) {
|
|
583
|
+
if (!Array.isArray(inner)) return false;
|
|
584
|
+
for (const h of inner) {
|
|
585
|
+
if (typeof h !== "object" || h === null) continue;
|
|
586
|
+
const rec = h;
|
|
587
|
+
if (rec["name"] === multicornName) return true;
|
|
588
|
+
const cmd = rec["command"];
|
|
589
|
+
if (typeof cmd === "string" && cmd.includes("gemini-cli-hooks")) return true;
|
|
590
|
+
}
|
|
591
|
+
return false;
|
|
592
|
+
}
|
|
593
|
+
function geminiHookEventsReferenceShield(arr) {
|
|
594
|
+
if (!Array.isArray(arr)) return false;
|
|
595
|
+
for (const entry of arr) {
|
|
596
|
+
if (typeof entry !== "object" || entry === null) continue;
|
|
597
|
+
const hooks = entry["hooks"];
|
|
598
|
+
if (geminiInnerHooksReferenceShield(hooks, "multicorn-shield") || geminiInnerHooksReferenceShield(hooks, "multicorn-shield-log")) {
|
|
599
|
+
return true;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
return false;
|
|
603
|
+
}
|
|
604
|
+
function geminiSettingsHasMulticornHooks(hooks) {
|
|
605
|
+
if (hooks === null || typeof hooks !== "object" || Array.isArray(hooks)) return false;
|
|
606
|
+
const h = hooks;
|
|
607
|
+
return geminiHookEventsReferenceShield(h["BeforeTool"]) || geminiHookEventsReferenceShield(h["AfterTool"]);
|
|
608
|
+
}
|
|
609
|
+
function geminiFilterInnerHooks(inner) {
|
|
610
|
+
if (!Array.isArray(inner)) return [];
|
|
611
|
+
return inner.filter((h) => {
|
|
612
|
+
if (typeof h !== "object" || h === null) return true;
|
|
613
|
+
const rec = h;
|
|
614
|
+
if (rec["name"] === "multicorn-shield" || rec["name"] === "multicorn-shield-log") return false;
|
|
615
|
+
const cmd = rec["command"];
|
|
616
|
+
if (typeof cmd === "string" && cmd.includes("gemini-cli-hooks")) return false;
|
|
617
|
+
return true;
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
function geminiStripMatcherGroups(arr) {
|
|
621
|
+
if (!Array.isArray(arr)) return [];
|
|
622
|
+
const out = [];
|
|
623
|
+
for (const entry of arr) {
|
|
624
|
+
if (typeof entry !== "object" || entry === null) continue;
|
|
625
|
+
const e = entry;
|
|
626
|
+
const filtered = geminiFilterInnerHooks(e["hooks"]);
|
|
627
|
+
if (filtered.length > 0) {
|
|
628
|
+
out.push({ ...e, hooks: filtered });
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
return out;
|
|
632
|
+
}
|
|
633
|
+
function geminiStripMulticornHookEntries(hooks) {
|
|
634
|
+
const out = { ...hooks };
|
|
635
|
+
out["BeforeTool"] = geminiStripMatcherGroups(out["BeforeTool"]);
|
|
636
|
+
out["AfterTool"] = geminiStripMatcherGroups(out["AfterTool"]);
|
|
637
|
+
return out;
|
|
638
|
+
}
|
|
639
|
+
async function installGeminiCliNativeHooks(ask) {
|
|
640
|
+
const root = multicornShieldPackageRoot();
|
|
641
|
+
const srcBefore = join(root, "plugins", "gemini-cli", "hooks", "scripts", "before-tool.cjs");
|
|
642
|
+
const srcAfter = join(root, "plugins", "gemini-cli", "hooks", "scripts", "after-tool.cjs");
|
|
643
|
+
const srcShared = join(root, "plugins", "gemini-cli", "hooks", "scripts", "shared.cjs");
|
|
644
|
+
if (!existsSync(srcBefore) || !existsSync(srcAfter) || !existsSync(srcShared)) {
|
|
645
|
+
throw new Error(
|
|
646
|
+
`Could not find Shield Gemini CLI hook scripts at ${srcBefore}. If you use npm, install the latest multicorn-shield package.`
|
|
647
|
+
);
|
|
648
|
+
}
|
|
649
|
+
const geminiConfigDir = join(homedir(), ".gemini");
|
|
650
|
+
if (!isExistingDirectory(geminiConfigDir)) {
|
|
651
|
+
process.stderr.write(
|
|
652
|
+
style.yellow("\u26A0") + " Gemini CLI does not appear to be installed (~/.gemini/ not found).\n\n"
|
|
653
|
+
);
|
|
654
|
+
process.stderr.write("Install Gemini CLI first:\n");
|
|
655
|
+
process.stderr.write(" " + style.cyan("npm install -g @google/gemini-cli") + "\n\n");
|
|
656
|
+
process.stderr.write("Then run this wizard again:\n");
|
|
657
|
+
process.stderr.write(" " + style.cyan("npx multicorn-shield init") + "\n");
|
|
658
|
+
throw new NativePluginPrerequisiteMissingError();
|
|
659
|
+
}
|
|
660
|
+
const installDir = getGeminiCliHooksInstallDir();
|
|
661
|
+
await mkdir(installDir, { recursive: true });
|
|
662
|
+
const destBefore = join(installDir, "before-tool.cjs");
|
|
663
|
+
const destAfter = join(installDir, "after-tool.cjs");
|
|
664
|
+
const destShared = join(installDir, "shared.cjs");
|
|
665
|
+
await copyFile(srcBefore, destBefore);
|
|
666
|
+
await copyFile(srcAfter, destAfter);
|
|
667
|
+
await copyFile(srcShared, destShared);
|
|
668
|
+
const mode = 493;
|
|
669
|
+
await chmod(destBefore, mode);
|
|
670
|
+
await chmod(destAfter, mode);
|
|
671
|
+
await chmod(destShared, mode);
|
|
672
|
+
const settingsPath = getGeminiCliSettingsPath();
|
|
673
|
+
let existing = {};
|
|
674
|
+
try {
|
|
675
|
+
const rawText = await readFile(settingsPath, "utf8");
|
|
676
|
+
const parsed = JSON.parse(rawText);
|
|
677
|
+
if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
678
|
+
existing = parsed;
|
|
679
|
+
}
|
|
680
|
+
} catch (err) {
|
|
681
|
+
if (isErrnoException(err) && err.code === "ENOENT") {
|
|
682
|
+
existing = {};
|
|
683
|
+
} else {
|
|
684
|
+
process.stderr.write(
|
|
685
|
+
style.yellow("\u26A0") + ` Could not parse ${settingsPath}. Create valid JSON or remove the file, then run init again.
|
|
686
|
+
`
|
|
687
|
+
);
|
|
688
|
+
throw new Error(`Invalid Gemini CLI settings at ${settingsPath}`);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
const hooksRaw = existing["hooks"];
|
|
692
|
+
const hooksObj = typeof hooksRaw === "object" && hooksRaw !== null && !Array.isArray(hooksRaw) ? hooksRaw : {};
|
|
693
|
+
if (geminiSettingsHasMulticornHooks(hooksObj)) {
|
|
694
|
+
const answer = await ask(
|
|
695
|
+
"Existing Multicorn Shield hooks were found in ~/.gemini/settings.json. Overwrite? (Y/n) "
|
|
696
|
+
);
|
|
697
|
+
if (answer.trim().toLowerCase() === "n") {
|
|
698
|
+
throw new Error("Installation cancelled: existing Shield hooks left unchanged.");
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
const cleaned = geminiStripMulticornHookEntries({ ...hooksObj });
|
|
702
|
+
const beforeArr = Array.isArray(cleaned["BeforeTool"]) ? [...cleaned["BeforeTool"]] : [];
|
|
703
|
+
const afterArr = Array.isArray(cleaned["AfterTool"]) ? [...cleaned["AfterTool"]] : [];
|
|
704
|
+
const beforeCmd = `node ${destBefore}`;
|
|
705
|
+
const afterCmd = `node ${destAfter}`;
|
|
706
|
+
beforeArr.push({
|
|
707
|
+
matcher: ".*",
|
|
708
|
+
hooks: [
|
|
709
|
+
{
|
|
710
|
+
type: "command",
|
|
711
|
+
name: "multicorn-shield",
|
|
712
|
+
command: beforeCmd,
|
|
713
|
+
timeout: 6e4
|
|
714
|
+
}
|
|
715
|
+
]
|
|
716
|
+
});
|
|
717
|
+
afterArr.push({
|
|
718
|
+
matcher: ".*",
|
|
719
|
+
hooks: [
|
|
720
|
+
{
|
|
721
|
+
type: "command",
|
|
722
|
+
name: "multicorn-shield-log",
|
|
723
|
+
command: afterCmd,
|
|
724
|
+
timeout: 1e4
|
|
725
|
+
}
|
|
726
|
+
]
|
|
727
|
+
});
|
|
728
|
+
existing["hooks"] = {
|
|
729
|
+
...cleaned,
|
|
730
|
+
BeforeTool: beforeArr,
|
|
731
|
+
AfterTool: afterArr
|
|
732
|
+
};
|
|
733
|
+
await mkdir(dirname(settingsPath), { recursive: true });
|
|
734
|
+
await writeFile(settingsPath, JSON.stringify(existing, null, 2) + "\n", "utf8");
|
|
735
|
+
}
|
|
736
|
+
async function promptGeminiCliIntegrationMode(ask) {
|
|
737
|
+
process.stderr.write("\n" + style.bold("Gemini CLI integration") + "\n");
|
|
738
|
+
process.stderr.write(
|
|
739
|
+
" " + style.violet("1") + ". Native plugin (recommended) - Gemini CLI Hooks see every file, terminal, web, and MCP action\n"
|
|
740
|
+
);
|
|
741
|
+
process.stderr.write(
|
|
742
|
+
" " + style.violet("2") + ". Hosted proxy - govern MCP traffic only (paste proxy URL into Gemini CLI settings)\n"
|
|
743
|
+
);
|
|
744
|
+
let choice = 0;
|
|
745
|
+
while (choice === 0) {
|
|
746
|
+
const input = await ask("Choose integration (1-2): ");
|
|
747
|
+
const num = parseInt(input.trim(), 10);
|
|
748
|
+
if (num === 1) choice = 1;
|
|
749
|
+
if (num === 2) choice = 2;
|
|
750
|
+
}
|
|
751
|
+
return choice === 1 ? "native" : "hosted";
|
|
752
|
+
}
|
|
753
|
+
function getClaudeDesktopConfigPath() {
|
|
754
|
+
switch (process.platform) {
|
|
755
|
+
case "win32":
|
|
756
|
+
return join(
|
|
757
|
+
process.env["APPDATA"] ?? join(homedir(), "AppData", "Roaming"),
|
|
758
|
+
"Claude",
|
|
759
|
+
"claude_desktop_config.json"
|
|
760
|
+
);
|
|
761
|
+
case "linux":
|
|
762
|
+
return join(homedir(), ".config", "Claude", "claude_desktop_config.json");
|
|
763
|
+
default:
|
|
764
|
+
return join(
|
|
765
|
+
homedir(),
|
|
766
|
+
"Library",
|
|
767
|
+
"Application Support",
|
|
768
|
+
"Claude",
|
|
769
|
+
"claude_desktop_config.json"
|
|
770
|
+
);
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
var PLATFORM_LABELS = [
|
|
774
|
+
"OpenClaw",
|
|
775
|
+
"Claude Code",
|
|
776
|
+
"Cursor",
|
|
777
|
+
"Windsurf",
|
|
778
|
+
"Cline",
|
|
779
|
+
"Claude Desktop",
|
|
780
|
+
"Gemini CLI",
|
|
781
|
+
"Local MCP / Other"
|
|
782
|
+
];
|
|
477
783
|
var PLATFORM_BY_SELECTION = {
|
|
478
784
|
1: "openclaw",
|
|
479
785
|
2: "claude-code",
|
|
480
786
|
3: "cursor",
|
|
481
787
|
4: "windsurf",
|
|
482
|
-
5: "
|
|
788
|
+
5: "cline",
|
|
789
|
+
6: "claude-desktop",
|
|
790
|
+
7: "gemini-cli",
|
|
791
|
+
8: "other-mcp"
|
|
483
792
|
};
|
|
484
793
|
var DEFAULT_AGENT_NAMES = {
|
|
485
794
|
openclaw: "my-openclaw-agent",
|
|
486
795
|
"claude-code": "my-claude-code-agent",
|
|
487
796
|
cursor: "my-cursor-agent",
|
|
488
|
-
windsurf: "my-windsurf-agent"
|
|
797
|
+
windsurf: "my-windsurf-agent",
|
|
798
|
+
cline: "my-cline-agent",
|
|
799
|
+
"claude-desktop": "my-claude-desktop-agent",
|
|
800
|
+
"gemini-cli": "my-gemini-cli-agent"
|
|
489
801
|
};
|
|
490
802
|
async function promptPlatformSelection(ask) {
|
|
491
803
|
process.stderr.write(
|
|
@@ -505,13 +817,13 @@ async function promptPlatformSelection(ask) {
|
|
|
505
817
|
);
|
|
506
818
|
}
|
|
507
819
|
process.stderr.write(
|
|
508
|
-
style.dim(" Pick
|
|
820
|
+
style.dim(" Pick 8 if you want to wrap a local MCP server with multicorn-proxy --wrap.") + "\n"
|
|
509
821
|
);
|
|
510
822
|
let selection = 0;
|
|
511
823
|
while (selection === 0) {
|
|
512
|
-
const input = await ask("Select (1-
|
|
824
|
+
const input = await ask("Select (1-8): ");
|
|
513
825
|
const num = parseInt(input.trim(), 10);
|
|
514
|
-
if (num >= 1 && num <=
|
|
826
|
+
if (num >= 1 && num <= 8) {
|
|
515
827
|
selection = num;
|
|
516
828
|
}
|
|
517
829
|
}
|
|
@@ -520,10 +832,10 @@ async function promptPlatformSelection(ask) {
|
|
|
520
832
|
async function promptWindsurfIntegrationMode(ask) {
|
|
521
833
|
process.stderr.write("\n" + style.bold("Windsurf integration") + "\n");
|
|
522
834
|
process.stderr.write(
|
|
523
|
-
" " + style.violet("1") + ". Native plugin (recommended)
|
|
835
|
+
" " + style.violet("1") + ". Native plugin (recommended) - Cascade Hooks see every file, terminal, and MCP action\n"
|
|
524
836
|
);
|
|
525
837
|
process.stderr.write(
|
|
526
|
-
" " + style.violet("2") + ". Hosted proxy
|
|
838
|
+
" " + style.violet("2") + ". Hosted proxy - govern MCP traffic only (paste proxy URL into mcp_config)\n"
|
|
527
839
|
);
|
|
528
840
|
let choice = 0;
|
|
529
841
|
while (choice === 0) {
|
|
@@ -632,9 +944,9 @@ async function createProxyConfig(baseUrl, apiKey, agentName, targetUrl, serverNa
|
|
|
632
944
|
return typeof data?.["proxy_url"] === "string" ? data["proxy_url"] : "";
|
|
633
945
|
}
|
|
634
946
|
function printPlatformSnippet(platform, routingToken, shortName, apiKey) {
|
|
635
|
-
const usesInlineKey = platform === "cursor" || platform === "windsurf";
|
|
947
|
+
const usesInlineKey = platform === "cursor" || platform === "claude-desktop" || platform === "windsurf" || platform === "cline" || platform === "gemini-cli";
|
|
636
948
|
const authHeader = usesInlineKey ? `Bearer ${apiKey}` : "Bearer YOUR_SHIELD_API_KEY";
|
|
637
|
-
const urlKey = platform === "windsurf" ? "serverUrl" : "url";
|
|
949
|
+
const urlKey = platform === "windsurf" ? "serverUrl" : platform === "gemini-cli" ? "httpUrl" : "url";
|
|
638
950
|
const mcpSnippet = JSON.stringify(
|
|
639
951
|
{
|
|
640
952
|
mcpServers: {
|
|
@@ -653,10 +965,35 @@ function printPlatformSnippet(platform, routingToken, shortName, apiKey) {
|
|
|
653
965
|
process.stderr.write("\n" + style.dim("Add this to your OpenClaw agent config:") + "\n\n");
|
|
654
966
|
} else if (platform === "claude-code") {
|
|
655
967
|
process.stderr.write("\n" + style.dim("Add this to your Claude Code MCP config:") + "\n\n");
|
|
968
|
+
} else if (platform === "claude-desktop") {
|
|
969
|
+
process.stderr.write("\n" + style.dim(`Add this to ${getClaudeDesktopConfigPath()}:`) + "\n\n");
|
|
656
970
|
} else if (platform === "windsurf") {
|
|
657
971
|
process.stderr.write(
|
|
658
972
|
"\n" + style.dim("Add this to ~/.codeium/windsurf/mcp_config.json:") + "\n\n"
|
|
659
973
|
);
|
|
974
|
+
} else if (platform === "cline") {
|
|
975
|
+
process.stderr.write("\n" + style.dim("Add this to your Cline MCP settings file:") + "\n");
|
|
976
|
+
process.stderr.write(
|
|
977
|
+
style.dim(
|
|
978
|
+
" macOS: ~/Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json"
|
|
979
|
+
) + "\n"
|
|
980
|
+
);
|
|
981
|
+
process.stderr.write(
|
|
982
|
+
style.dim(
|
|
983
|
+
" Windows: %APPDATA%\\Code\\User\\globalStorage\\saoudrizwan.claude-dev\\settings\\cline_mcp_settings.json"
|
|
984
|
+
) + "\n"
|
|
985
|
+
);
|
|
986
|
+
process.stderr.write(
|
|
987
|
+
style.dim(
|
|
988
|
+
" Linux: ~/.config/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json"
|
|
989
|
+
) + "\n\n"
|
|
990
|
+
);
|
|
991
|
+
} else if (platform === "gemini-cli") {
|
|
992
|
+
process.stderr.write(
|
|
993
|
+
"\n" + style.dim(
|
|
994
|
+
"Add this to ~/.gemini/settings.json (create the file if it does not exist). For project-specific config, use .gemini/settings.json in your project root. Restart Gemini CLI after saving. Run /mcp to verify the server is connected."
|
|
995
|
+
) + "\n\n"
|
|
996
|
+
);
|
|
660
997
|
} else {
|
|
661
998
|
process.stderr.write("\n" + style.dim("Add this to ~/.cursor/mcp.json:") + "\n\n");
|
|
662
999
|
}
|
|
@@ -680,6 +1017,16 @@ function printPlatformSnippet(platform, routingToken, shortName, apiKey) {
|
|
|
680
1017
|
) + "\n"
|
|
681
1018
|
);
|
|
682
1019
|
}
|
|
1020
|
+
if (platform === "claude-desktop") {
|
|
1021
|
+
process.stderr.write(style.dim("Then restart Claude Desktop to load the MCP server.") + "\n");
|
|
1022
|
+
}
|
|
1023
|
+
if (platform === "cline") {
|
|
1024
|
+
process.stderr.write(
|
|
1025
|
+
style.dim(
|
|
1026
|
+
"After pasting, restart Cline or reload the VS Code window. Cline will discover the Shield tools automatically."
|
|
1027
|
+
) + "\n"
|
|
1028
|
+
);
|
|
1029
|
+
}
|
|
683
1030
|
if (platform === "windsurf") {
|
|
684
1031
|
process.stderr.write(style.dim("Then restart Windsurf (Cmd/Ctrl+Q, then reopen).") + "\n");
|
|
685
1032
|
process.stderr.write(
|
|
@@ -774,10 +1121,11 @@ async function runInit(explicitBaseUrl) {
|
|
|
774
1121
|
};
|
|
775
1122
|
let configuring = true;
|
|
776
1123
|
while (configuring) {
|
|
1124
|
+
let postSaveNativeSkipNote = null;
|
|
777
1125
|
const selection = await promptPlatformSelection(ask);
|
|
778
1126
|
const selectedPlatform = PLATFORM_BY_SELECTION[selection] ?? "cursor";
|
|
779
1127
|
const selectedLabel = PLATFORM_LABELS[selection - 1] ?? "Cursor";
|
|
780
|
-
if (selection ===
|
|
1128
|
+
if (selection === 8) {
|
|
781
1129
|
const raw = existing !== null ? { ...existing } : {};
|
|
782
1130
|
raw["apiKey"] = apiKey;
|
|
783
1131
|
raw["baseUrl"] = resolvedBaseUrl;
|
|
@@ -956,8 +1304,22 @@ An agent for ${selectedLabel} already exists: ${style.cyan(existingForPlatform.n
|
|
|
956
1304
|
});
|
|
957
1305
|
setupSucceeded = true;
|
|
958
1306
|
} catch (error) {
|
|
959
|
-
|
|
960
|
-
|
|
1307
|
+
if (error instanceof NativePluginPrerequisiteMissingError) {
|
|
1308
|
+
postSaveNativeSkipNote = nativePluginSkippedSaveNote(
|
|
1309
|
+
"npx multicorn-proxy init",
|
|
1310
|
+
"Windsurf"
|
|
1311
|
+
);
|
|
1312
|
+
configuredAgents.push({
|
|
1313
|
+
selection,
|
|
1314
|
+
platform: selectedPlatform,
|
|
1315
|
+
platformLabel: selectedLabel,
|
|
1316
|
+
agentName
|
|
1317
|
+
});
|
|
1318
|
+
setupSucceeded = true;
|
|
1319
|
+
} else {
|
|
1320
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1321
|
+
process.stderr.write(style.red("\u2717 ") + detail + "\n");
|
|
1322
|
+
}
|
|
961
1323
|
}
|
|
962
1324
|
} else {
|
|
963
1325
|
const { targetUrl, shortName } = await promptProxyConfig(ask, agentName);
|
|
@@ -1001,6 +1363,170 @@ An agent for ${selectedLabel} already exists: ${style.cyan(existingForPlatform.n
|
|
|
1001
1363
|
setupSucceeded = true;
|
|
1002
1364
|
}
|
|
1003
1365
|
}
|
|
1366
|
+
} else if (selection === 7) {
|
|
1367
|
+
const geminiMode = await promptGeminiCliIntegrationMode(ask);
|
|
1368
|
+
if (geminiMode === "native") {
|
|
1369
|
+
try {
|
|
1370
|
+
await installGeminiCliNativeHooks(ask);
|
|
1371
|
+
process.stderr.write("\n" + style.bold("Shield Gemini CLI hooks installed") + "\n\n");
|
|
1372
|
+
process.stderr.write(
|
|
1373
|
+
style.dim("Hook scripts: ") + style.cyan(getGeminiCliHooksInstallDir()) + "\n"
|
|
1374
|
+
);
|
|
1375
|
+
process.stderr.write(
|
|
1376
|
+
style.dim("Settings updated at ") + style.cyan("~/.gemini/settings.json") + "\n"
|
|
1377
|
+
);
|
|
1378
|
+
process.stderr.write(
|
|
1379
|
+
style.dim(
|
|
1380
|
+
"The Shield hook runs with your user permissions. It intercepts Gemini CLI tool calls to check permissions and log activity. Review the scripts if that is a concern."
|
|
1381
|
+
) + "\n"
|
|
1382
|
+
);
|
|
1383
|
+
configuredAgents.push({
|
|
1384
|
+
selection,
|
|
1385
|
+
platform: selectedPlatform,
|
|
1386
|
+
platformLabel: selectedLabel,
|
|
1387
|
+
agentName,
|
|
1388
|
+
geminiCliIntegration: "native"
|
|
1389
|
+
});
|
|
1390
|
+
setupSucceeded = true;
|
|
1391
|
+
} catch (error) {
|
|
1392
|
+
if (error instanceof NativePluginPrerequisiteMissingError) {
|
|
1393
|
+
postSaveNativeSkipNote = nativePluginSkippedSaveNote(
|
|
1394
|
+
"npx multicorn-shield init",
|
|
1395
|
+
"Gemini CLI"
|
|
1396
|
+
);
|
|
1397
|
+
configuredAgents.push({
|
|
1398
|
+
selection,
|
|
1399
|
+
platform: selectedPlatform,
|
|
1400
|
+
platformLabel: selectedLabel,
|
|
1401
|
+
agentName
|
|
1402
|
+
});
|
|
1403
|
+
setupSucceeded = true;
|
|
1404
|
+
} else {
|
|
1405
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1406
|
+
process.stderr.write(style.red("\u2717 ") + detail + "\n");
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
} else {
|
|
1410
|
+
const { targetUrl, shortName } = await promptProxyConfig(ask, agentName);
|
|
1411
|
+
let proxyUrl = "";
|
|
1412
|
+
let created = false;
|
|
1413
|
+
while (!created) {
|
|
1414
|
+
const spinner = withSpinner("Creating proxy config...");
|
|
1415
|
+
try {
|
|
1416
|
+
proxyUrl = await createProxyConfig(
|
|
1417
|
+
resolvedBaseUrl,
|
|
1418
|
+
apiKey,
|
|
1419
|
+
agentName,
|
|
1420
|
+
targetUrl,
|
|
1421
|
+
shortName,
|
|
1422
|
+
selectedPlatform
|
|
1423
|
+
);
|
|
1424
|
+
spinner.stop(true, "Proxy config created!");
|
|
1425
|
+
created = true;
|
|
1426
|
+
} catch (error) {
|
|
1427
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1428
|
+
spinner.stop(false, detail);
|
|
1429
|
+
const retry = await ask("Try again? (Y/n) ");
|
|
1430
|
+
if (retry.trim().toLowerCase() === "n") {
|
|
1431
|
+
break;
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
if (created && proxyUrl.length > 0) {
|
|
1436
|
+
process.stderr.write("\n" + style.bold("Your Shield proxy URL:") + "\n");
|
|
1437
|
+
process.stderr.write(" " + style.cyan(proxyUrl) + "\n");
|
|
1438
|
+
printPlatformSnippet(selectedPlatform, proxyUrl, shortName, apiKey);
|
|
1439
|
+
configuredAgents.push({
|
|
1440
|
+
selection,
|
|
1441
|
+
platform: selectedPlatform,
|
|
1442
|
+
platformLabel: selectedLabel,
|
|
1443
|
+
agentName,
|
|
1444
|
+
shortName,
|
|
1445
|
+
proxyUrl,
|
|
1446
|
+
geminiCliIntegration: "hosted"
|
|
1447
|
+
});
|
|
1448
|
+
setupSucceeded = true;
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
} else if (selection === 5) {
|
|
1452
|
+
const clineMode = await promptClineIntegrationMode(ask);
|
|
1453
|
+
if (clineMode === "native") {
|
|
1454
|
+
try {
|
|
1455
|
+
await installClineNativeHooks();
|
|
1456
|
+
process.stderr.write("\n" + style.bold("Shield Cline hooks installed") + "\n\n");
|
|
1457
|
+
process.stderr.write(
|
|
1458
|
+
style.dim(
|
|
1459
|
+
"The Shield hook runs with your user permissions. It intercepts Cline tool calls to check permissions and log activity. Review the scripts under "
|
|
1460
|
+
) + style.cyan("~/.multicorn/cline-hooks") + style.dim(" if that is a concern.") + "\n"
|
|
1461
|
+
);
|
|
1462
|
+
configuredAgents.push({
|
|
1463
|
+
selection,
|
|
1464
|
+
platform: selectedPlatform,
|
|
1465
|
+
platformLabel: selectedLabel,
|
|
1466
|
+
agentName,
|
|
1467
|
+
clineIntegration: "native"
|
|
1468
|
+
});
|
|
1469
|
+
setupSucceeded = true;
|
|
1470
|
+
} catch (error) {
|
|
1471
|
+
if (error instanceof NativePluginPrerequisiteMissingError) {
|
|
1472
|
+
postSaveNativeSkipNote = nativePluginSkippedSaveNote(
|
|
1473
|
+
"npx multicorn-shield init",
|
|
1474
|
+
"Cline"
|
|
1475
|
+
);
|
|
1476
|
+
configuredAgents.push({
|
|
1477
|
+
selection,
|
|
1478
|
+
platform: selectedPlatform,
|
|
1479
|
+
platformLabel: selectedLabel,
|
|
1480
|
+
agentName
|
|
1481
|
+
});
|
|
1482
|
+
setupSucceeded = true;
|
|
1483
|
+
} else {
|
|
1484
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1485
|
+
process.stderr.write(style.red("\u2717 ") + detail + "\n");
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
} else {
|
|
1489
|
+
const { targetUrl, shortName } = await promptProxyConfig(ask, agentName);
|
|
1490
|
+
let proxyUrl = "";
|
|
1491
|
+
let created = false;
|
|
1492
|
+
while (!created) {
|
|
1493
|
+
const spinner = withSpinner("Creating proxy config...");
|
|
1494
|
+
try {
|
|
1495
|
+
proxyUrl = await createProxyConfig(
|
|
1496
|
+
resolvedBaseUrl,
|
|
1497
|
+
apiKey,
|
|
1498
|
+
agentName,
|
|
1499
|
+
targetUrl,
|
|
1500
|
+
shortName,
|
|
1501
|
+
selectedPlatform
|
|
1502
|
+
);
|
|
1503
|
+
spinner.stop(true, "Proxy config created!");
|
|
1504
|
+
created = true;
|
|
1505
|
+
} catch (error) {
|
|
1506
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1507
|
+
spinner.stop(false, detail);
|
|
1508
|
+
const retry = await ask("Try again? (Y/n) ");
|
|
1509
|
+
if (retry.trim().toLowerCase() === "n") {
|
|
1510
|
+
break;
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
if (created && proxyUrl.length > 0) {
|
|
1515
|
+
process.stderr.write("\n" + style.bold("Your Shield proxy URL:") + "\n");
|
|
1516
|
+
process.stderr.write(" " + style.cyan(proxyUrl) + "\n");
|
|
1517
|
+
printPlatformSnippet(selectedPlatform, proxyUrl, shortName, apiKey);
|
|
1518
|
+
configuredAgents.push({
|
|
1519
|
+
selection,
|
|
1520
|
+
platform: selectedPlatform,
|
|
1521
|
+
platformLabel: selectedLabel,
|
|
1522
|
+
agentName,
|
|
1523
|
+
shortName,
|
|
1524
|
+
proxyUrl,
|
|
1525
|
+
clineIntegration: "hosted"
|
|
1526
|
+
});
|
|
1527
|
+
setupSucceeded = true;
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1004
1530
|
} else {
|
|
1005
1531
|
const { targetUrl, shortName } = await promptProxyConfig(ask, agentName);
|
|
1006
1532
|
let proxyUrl = "";
|
|
@@ -1059,9 +1585,14 @@ An agent for ${selectedLabel} already exists: ${style.cyan(existingForPlatform.n
|
|
|
1059
1585
|
style.green("\u2713") + ` Config saved to ${style.cyan(CONFIG_PATH)}
|
|
1060
1586
|
`
|
|
1061
1587
|
);
|
|
1588
|
+
if (postSaveNativeSkipNote != null) {
|
|
1589
|
+
process.stderr.write(postSaveNativeSkipNote);
|
|
1590
|
+
postSaveNativeSkipNote = null;
|
|
1591
|
+
}
|
|
1062
1592
|
} catch (error) {
|
|
1063
1593
|
const detail = error instanceof Error ? error.message : String(error);
|
|
1064
1594
|
process.stderr.write(style.red(`Failed to save config: ${detail}`) + "\n");
|
|
1595
|
+
postSaveNativeSkipNote = null;
|
|
1065
1596
|
}
|
|
1066
1597
|
}
|
|
1067
1598
|
const another = await ask("\nConnect another agent? (Y/n) ");
|
|
@@ -1119,6 +1650,38 @@ An agent for ${selectedLabel} already exists: ${style.cyan(existingForPlatform.n
|
|
|
1119
1650
|
"\n" + style.bold("To complete your Windsurf hosted-proxy setup:") + "\n 1. If you don't have Windsurf yet, download it from " + style.cyan("https://windsurf.com/download") + "\n 2. Open " + style.cyan("~/.codeium/windsurf/mcp_config.json") + " and paste the config snippet shown above\n 3. Restart Windsurf (or launch it for the first time) to load the new MCP server\n"
|
|
1120
1651
|
);
|
|
1121
1652
|
}
|
|
1653
|
+
const clineNativeConfigured = configuredAgents.some(
|
|
1654
|
+
(a) => a.platform === "cline" && a.clineIntegration === "native"
|
|
1655
|
+
);
|
|
1656
|
+
const clineHostedConfigured = configuredAgents.some(
|
|
1657
|
+
(a) => a.platform === "cline" && a.clineIntegration === "hosted"
|
|
1658
|
+
);
|
|
1659
|
+
if (clineNativeConfigured) {
|
|
1660
|
+
blocks.push(
|
|
1661
|
+
"\n" + style.bold("To complete native Cline (Shield) setup:") + "\n 1. Enable Hooks in Cline: open VS Code, click the Cline sidebar icon, click the gear icon,\n scroll down to the Advanced section, and toggle Hooks on.\n 2. Reload the VS Code window (Cmd+Shift+P > Reload Window)\n 3. Trigger any tool call to verify Shield is intercepting\n"
|
|
1662
|
+
);
|
|
1663
|
+
}
|
|
1664
|
+
if (clineHostedConfigured) {
|
|
1665
|
+
blocks.push(
|
|
1666
|
+
"\n" + style.bold("To complete your Cline hosted-proxy setup:") + "\n 1. If you don't have Cline yet, install it from the VS Code marketplace\n 2. Open your Cline MCP settings file and paste the config snippet shown above\n 3. Restart Cline or reload the VS Code window\n"
|
|
1667
|
+
);
|
|
1668
|
+
}
|
|
1669
|
+
const geminiCliNativeConfigured = configuredAgents.some(
|
|
1670
|
+
(a) => a.platform === "gemini-cli" && a.geminiCliIntegration === "native"
|
|
1671
|
+
);
|
|
1672
|
+
const geminiCliHostedConfigured = configuredAgents.some(
|
|
1673
|
+
(a) => a.platform === "gemini-cli" && a.geminiCliIntegration === "hosted"
|
|
1674
|
+
);
|
|
1675
|
+
if (geminiCliNativeConfigured) {
|
|
1676
|
+
blocks.push(
|
|
1677
|
+
"\n" + style.bold("Gemini CLI native hooks:") + "\n Your Gemini CLI hooks are installed. Restart Gemini CLI to activate Shield governance.\n"
|
|
1678
|
+
);
|
|
1679
|
+
}
|
|
1680
|
+
if (geminiCliHostedConfigured) {
|
|
1681
|
+
blocks.push(
|
|
1682
|
+
"\n" + style.bold("To complete your Gemini CLI setup:") + "\n 1. Open " + style.cyan("~/.gemini/settings.json") + "\n 2. Paste the config snippet shown above\n 3. Restart Gemini CLI, then run /mcp to verify\n"
|
|
1683
|
+
);
|
|
1684
|
+
}
|
|
1122
1685
|
if (blocks.length > 0) {
|
|
1123
1686
|
process.stderr.write("\n" + style.bold(style.violet("Next steps")) + "\n");
|
|
1124
1687
|
process.stderr.write(blocks.join("") + "\n");
|