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.
@@ -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
- var PLATFORM_LABELS = ["OpenClaw", "Claude Code", "Cursor", "Windsurf", "Local MCP / Other"];
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: "other-mcp"
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 5 if you want to wrap a local MCP server with multicorn-proxy --wrap.") + "\n"
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-5): ");
824
+ const input = await ask("Select (1-8): ");
513
825
  const num = parseInt(input.trim(), 10);
514
- if (num >= 1 && num <= 5) {
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) \u2014 Cascade Hooks see every file, terminal, and MCP action\n"
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 \u2014 govern MCP traffic only (paste proxy URL into mcp_config)\n"
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 === 5) {
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
- const detail = error instanceof Error ? error.message : String(error);
960
- process.stderr.write(style.red("\u2717 ") + detail + "\n");
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");