multicorn-shield 1.2.0 → 1.3.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 +15 -1
- package/dist/multicorn-proxy.js +263 -179
- package/dist/multicorn-shield.js +263 -179
- package/dist/openclaw-hook/handler.js +3 -3
- package/dist/openclaw-plugin/multicorn-shield.js +3 -3
- package/dist/shield-extension.js +14 -19
- package/package.json +2 -1
- package/plugins/multicorn-shield/.claude-plugin/plugin.json +8 -0
- package/plugins/multicorn-shield/hooks/hooks.json +26 -0
- package/plugins/multicorn-shield/hooks/scripts/claude-code-tool-map.cjs +138 -0
- package/plugins/multicorn-shield/hooks/scripts/post-tool-use.cjs +253 -0
- package/plugins/multicorn-shield/hooks/scripts/pre-tool-use.cjs +642 -0
- package/plugins/multicorn-shield/skills/shield-governance/SKILL.md +24 -0
package/dist/multicorn-shield.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { existsSync, statSync } from 'fs';
|
|
2
|
+
import { readFileSync, existsSync, statSync } from 'fs';
|
|
3
3
|
import { readFile, mkdir, writeFile, copyFile, chmod, unlink } from 'fs/promises';
|
|
4
4
|
import { join, dirname, resolve, basename, sep } from 'path';
|
|
5
5
|
import { homedir } from 'os';
|
|
6
6
|
import { fileURLToPath } from 'url';
|
|
7
|
+
import { createRequire } from 'module';
|
|
7
8
|
import { createInterface } from 'readline';
|
|
8
9
|
import { spawn } from 'child_process';
|
|
9
10
|
import { createHash } from 'crypto';
|
|
@@ -391,6 +392,32 @@ function isExistingDirectory(path) {
|
|
|
391
392
|
return false;
|
|
392
393
|
}
|
|
393
394
|
}
|
|
395
|
+
function jsonValueMentionsMulticornShield(value) {
|
|
396
|
+
if (typeof value === "string") {
|
|
397
|
+
return value.includes("multicorn-shield");
|
|
398
|
+
}
|
|
399
|
+
if (Array.isArray(value)) {
|
|
400
|
+
return value.some(jsonValueMentionsMulticornShield);
|
|
401
|
+
}
|
|
402
|
+
if (value !== null && typeof value === "object") {
|
|
403
|
+
for (const [k, v] of Object.entries(value)) {
|
|
404
|
+
if (k.includes("multicorn-shield")) return true;
|
|
405
|
+
if (jsonValueMentionsMulticornShield(v)) return true;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
return false;
|
|
409
|
+
}
|
|
410
|
+
function claudeInstalledPluginsListsMulticornShield() {
|
|
411
|
+
const path = join(homedir(), ".claude", "plugins", "installed_plugins.json");
|
|
412
|
+
if (!existsSync(path)) return false;
|
|
413
|
+
try {
|
|
414
|
+
const raw = readFileSync(path, "utf8");
|
|
415
|
+
const parsed = JSON.parse(raw);
|
|
416
|
+
return jsonValueMentionsMulticornShield(parsed);
|
|
417
|
+
} catch {
|
|
418
|
+
return false;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
394
421
|
function nativePluginSkippedSaveNote(wizardCommand, productName) {
|
|
395
422
|
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";
|
|
396
423
|
}
|
|
@@ -705,78 +732,48 @@ async function validateApiKey(apiKey, baseUrl) {
|
|
|
705
732
|
};
|
|
706
733
|
}
|
|
707
734
|
}
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
const raw = await readFile(OPENCLAW_CONFIG_PATH, "utf8");
|
|
711
|
-
const obj = JSON.parse(raw);
|
|
712
|
-
const hooks = obj["hooks"];
|
|
713
|
-
const internal = hooks?.["internal"];
|
|
714
|
-
const entries = internal?.["entries"];
|
|
715
|
-
const shield = entries?.["multicorn-shield"];
|
|
716
|
-
const env = shield?.["env"];
|
|
717
|
-
const key = env?.["MULTICORN_API_KEY"];
|
|
718
|
-
return typeof key === "string" && key.length > 0;
|
|
719
|
-
} catch {
|
|
720
|
-
return false;
|
|
721
|
-
}
|
|
735
|
+
function multicornShieldPackageRoot() {
|
|
736
|
+
return join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
722
737
|
}
|
|
723
|
-
function
|
|
738
|
+
function multicornShieldInstallRoot() {
|
|
724
739
|
try {
|
|
725
|
-
|
|
740
|
+
const req = createRequire(import.meta.url);
|
|
741
|
+
return dirname(req.resolve("multicorn-shield/package.json"));
|
|
726
742
|
} catch {
|
|
727
|
-
return
|
|
743
|
+
return multicornShieldPackageRoot();
|
|
728
744
|
}
|
|
729
745
|
}
|
|
730
|
-
function
|
|
731
|
-
return
|
|
746
|
+
function shieldInstalledVersionOlderThan(latest, installed) {
|
|
747
|
+
return latest.localeCompare(installed, void 0, { numeric: true }) > 0;
|
|
732
748
|
}
|
|
733
|
-
async function
|
|
749
|
+
async function warnIfInstalledShieldIsOutdated() {
|
|
734
750
|
try {
|
|
735
|
-
const
|
|
736
|
-
|
|
737
|
-
const
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
const
|
|
743
|
-
|
|
744
|
-
const
|
|
745
|
-
|
|
746
|
-
|
|
751
|
+
const res = await fetch("https://registry.npmjs.org/multicorn-shield/latest");
|
|
752
|
+
if (!res.ok) return;
|
|
753
|
+
const data = await res.json();
|
|
754
|
+
const latest = typeof data.version === "string" ? data.version : "";
|
|
755
|
+
if (latest.length === 0) return;
|
|
756
|
+
let installed = "";
|
|
757
|
+
try {
|
|
758
|
+
const req = createRequire(import.meta.url);
|
|
759
|
+
const pkgPath = req.resolve("multicorn-shield/package.json");
|
|
760
|
+
const raw = readFileSync(pkgPath, "utf8");
|
|
761
|
+
const pkg = JSON.parse(raw);
|
|
762
|
+
installed = typeof pkg.version === "string" ? pkg.version : "";
|
|
763
|
+
} catch {
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
if (installed.length === 0 || !shieldInstalledVersionOlderThan(latest, installed)) {
|
|
767
|
+
return;
|
|
747
768
|
}
|
|
748
|
-
return false;
|
|
749
|
-
} catch (err) {
|
|
750
769
|
process.stderr.write(
|
|
751
|
-
`
|
|
770
|
+
style.yellow("\u26A0") + ` multicorn-shield v${installed} is installed but v${latest} is available. Run npm update multicorn-shield to update.
|
|
771
|
+
|
|
752
772
|
`
|
|
753
773
|
);
|
|
754
|
-
return false;
|
|
755
|
-
}
|
|
756
|
-
}
|
|
757
|
-
function getWindsurfConfigPath() {
|
|
758
|
-
return join(homedir(), ".codeium", "windsurf", "mcp_config.json");
|
|
759
|
-
}
|
|
760
|
-
async function isWindsurfConnected() {
|
|
761
|
-
try {
|
|
762
|
-
const raw = await readFile(getWindsurfConfigPath(), "utf8");
|
|
763
|
-
const obj = JSON.parse(raw);
|
|
764
|
-
const mcpServers = obj["mcpServers"];
|
|
765
|
-
if (mcpServers === void 0 || typeof mcpServers !== "object") return false;
|
|
766
|
-
for (const entry of Object.values(mcpServers)) {
|
|
767
|
-
if (typeof entry !== "object" || entry === null) continue;
|
|
768
|
-
const rec = entry;
|
|
769
|
-
const url = rec["serverUrl"];
|
|
770
|
-
if (typeof url === "string" && url.includes("multicorn")) return true;
|
|
771
|
-
}
|
|
772
|
-
return false;
|
|
773
774
|
} catch {
|
|
774
|
-
return false;
|
|
775
775
|
}
|
|
776
776
|
}
|
|
777
|
-
function multicornShieldPackageRoot() {
|
|
778
|
-
return join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
779
|
-
}
|
|
780
777
|
function getWindsurfHooksInstallDir() {
|
|
781
778
|
return join(homedir(), ".multicorn", "windsurf-hooks");
|
|
782
779
|
}
|
|
@@ -1006,6 +1003,140 @@ function geminiStripMulticornHookEntries(hooks) {
|
|
|
1006
1003
|
out["AfterTool"] = geminiStripMatcherGroups(out["AfterTool"]);
|
|
1007
1004
|
return out;
|
|
1008
1005
|
}
|
|
1006
|
+
function getClaudeCodeUserSettingsPath() {
|
|
1007
|
+
return join(homedir(), ".claude", "settings.json");
|
|
1008
|
+
}
|
|
1009
|
+
function commandLooksLikeMulticornClaudePre(cmd) {
|
|
1010
|
+
return typeof cmd === "string" && cmd.includes("pre-tool-use.cjs") && cmd.includes("multicorn-shield");
|
|
1011
|
+
}
|
|
1012
|
+
function commandLooksLikeMulticornClaudePost(cmd) {
|
|
1013
|
+
return typeof cmd === "string" && cmd.includes("post-tool-use.cjs") && cmd.includes("multicorn-shield");
|
|
1014
|
+
}
|
|
1015
|
+
function claudeSettingsMatcherGroupReferencesShield(group, kind) {
|
|
1016
|
+
const inner = group["hooks"];
|
|
1017
|
+
if (!Array.isArray(inner)) return false;
|
|
1018
|
+
const pred = kind === "pre" ? commandLooksLikeMulticornClaudePre : commandLooksLikeMulticornClaudePost;
|
|
1019
|
+
for (const h of inner) {
|
|
1020
|
+
if (typeof h !== "object" || h === null) continue;
|
|
1021
|
+
const rec = h;
|
|
1022
|
+
if (pred(rec["command"])) return true;
|
|
1023
|
+
}
|
|
1024
|
+
return false;
|
|
1025
|
+
}
|
|
1026
|
+
function claudeHooksHaveShieldEntries(hooks) {
|
|
1027
|
+
for (const key of ["PreToolUse", "PostToolUse"]) {
|
|
1028
|
+
const arr = hooks[key];
|
|
1029
|
+
if (!Array.isArray(arr)) continue;
|
|
1030
|
+
const kind = key === "PreToolUse" ? "pre" : "post";
|
|
1031
|
+
for (const g of arr) {
|
|
1032
|
+
if (typeof g === "object" && g !== null) {
|
|
1033
|
+
if (claudeSettingsMatcherGroupReferencesShield(g, kind)) {
|
|
1034
|
+
return true;
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
return false;
|
|
1040
|
+
}
|
|
1041
|
+
function stripClaudeShieldHookGroups(arr, kind) {
|
|
1042
|
+
return arr.filter((g) => {
|
|
1043
|
+
if (typeof g !== "object" || g === null) return true;
|
|
1044
|
+
return !claudeSettingsMatcherGroupReferencesShield(g, kind);
|
|
1045
|
+
});
|
|
1046
|
+
}
|
|
1047
|
+
async function installClaudeCodeUserSettingsHooks(ask) {
|
|
1048
|
+
const root = multicornShieldInstallRoot();
|
|
1049
|
+
const prePath = join(root, "plugins", "multicorn-shield", "hooks", "scripts", "pre-tool-use.cjs");
|
|
1050
|
+
const postPath = join(
|
|
1051
|
+
root,
|
|
1052
|
+
"plugins",
|
|
1053
|
+
"multicorn-shield",
|
|
1054
|
+
"hooks",
|
|
1055
|
+
"scripts",
|
|
1056
|
+
"post-tool-use.cjs"
|
|
1057
|
+
);
|
|
1058
|
+
if (!existsSync(prePath) || !existsSync(postPath)) {
|
|
1059
|
+
process.stderr.write(
|
|
1060
|
+
style.red(
|
|
1061
|
+
"Could not find Shield Claude Code hook scripts next to the multicorn-shield package.\n"
|
|
1062
|
+
)
|
|
1063
|
+
);
|
|
1064
|
+
process.stderr.write(style.dim(` Expected: ${prePath}`) + "\n");
|
|
1065
|
+
return false;
|
|
1066
|
+
}
|
|
1067
|
+
const settingsPath = getClaudeCodeUserSettingsPath();
|
|
1068
|
+
await mkdir(dirname(settingsPath), { recursive: true });
|
|
1069
|
+
let existing = {};
|
|
1070
|
+
try {
|
|
1071
|
+
const rawText = await readFile(settingsPath, "utf8");
|
|
1072
|
+
const parsed = JSON.parse(rawText);
|
|
1073
|
+
if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
1074
|
+
existing = parsed;
|
|
1075
|
+
}
|
|
1076
|
+
} catch (err) {
|
|
1077
|
+
if (isErrnoException(err) && err.code === "ENOENT") {
|
|
1078
|
+
existing = {};
|
|
1079
|
+
} else {
|
|
1080
|
+
process.stderr.write(
|
|
1081
|
+
style.yellow("\u26A0") + ` Could not parse ${settingsPath}. Fix or remove the file, then run init again.
|
|
1082
|
+
`
|
|
1083
|
+
);
|
|
1084
|
+
return false;
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
const hooksRaw = existing["hooks"];
|
|
1088
|
+
const hooksObj = typeof hooksRaw === "object" && hooksRaw !== null && !Array.isArray(hooksRaw) ? { ...hooksRaw } : {};
|
|
1089
|
+
if (claudeHooksHaveShieldEntries(hooksObj)) {
|
|
1090
|
+
const answer = await ask(
|
|
1091
|
+
"Existing Multicorn Shield hooks were found in ~/.claude/settings.json. Overwrite? (Y/n) "
|
|
1092
|
+
);
|
|
1093
|
+
if (answer.trim().toLowerCase() === "n") {
|
|
1094
|
+
return false;
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
const preCmd = `node ${JSON.stringify(prePath)}`;
|
|
1098
|
+
const postCmd = `node ${JSON.stringify(postPath)}`;
|
|
1099
|
+
const preArr = stripClaudeShieldHookGroups(
|
|
1100
|
+
Array.isArray(hooksObj["PreToolUse"]) ? [...hooksObj["PreToolUse"]] : [],
|
|
1101
|
+
"pre"
|
|
1102
|
+
);
|
|
1103
|
+
const postArr = stripClaudeShieldHookGroups(
|
|
1104
|
+
Array.isArray(hooksObj["PostToolUse"]) ? [...hooksObj["PostToolUse"]] : [],
|
|
1105
|
+
"post"
|
|
1106
|
+
);
|
|
1107
|
+
preArr.push({
|
|
1108
|
+
matcher: "*",
|
|
1109
|
+
hooks: [
|
|
1110
|
+
{
|
|
1111
|
+
type: "command",
|
|
1112
|
+
name: "multicorn-shield-pre",
|
|
1113
|
+
command: preCmd,
|
|
1114
|
+
timeout: 600
|
|
1115
|
+
}
|
|
1116
|
+
]
|
|
1117
|
+
});
|
|
1118
|
+
postArr.push({
|
|
1119
|
+
matcher: "*",
|
|
1120
|
+
hooks: [
|
|
1121
|
+
{
|
|
1122
|
+
type: "command",
|
|
1123
|
+
name: "multicorn-shield-post",
|
|
1124
|
+
command: postCmd,
|
|
1125
|
+
timeout: 120
|
|
1126
|
+
}
|
|
1127
|
+
]
|
|
1128
|
+
});
|
|
1129
|
+
hooksObj["PreToolUse"] = preArr;
|
|
1130
|
+
hooksObj["PostToolUse"] = postArr;
|
|
1131
|
+
const out = { ...existing, hooks: hooksObj };
|
|
1132
|
+
const serialized = JSON.stringify(out, null, 2) + "\n";
|
|
1133
|
+
await writeFile(settingsPath, serialized, "utf8");
|
|
1134
|
+
process.stderr.write(
|
|
1135
|
+
"\n" + style.dim("Wrote ") + style.cyan(settingsPath) + style.dim(":") + "\n"
|
|
1136
|
+
);
|
|
1137
|
+
process.stderr.write(style.dim(JSON.stringify({ hooks: hooksObj }, null, 2)) + "\n");
|
|
1138
|
+
return true;
|
|
1139
|
+
}
|
|
1009
1140
|
async function installGeminiCliNativeHooks(ask) {
|
|
1010
1141
|
const root = multicornShieldPackageRoot();
|
|
1011
1142
|
const srcBefore = join(root, "plugins", "gemini-cli", "hooks", "scripts", "before-tool.cjs");
|
|
@@ -1141,54 +1272,48 @@ function getClaudeDesktopConfigPath() {
|
|
|
1141
1272
|
}
|
|
1142
1273
|
}
|
|
1143
1274
|
var INIT_WIZARD_PLATFORM_REGISTRY = [
|
|
1144
|
-
{ slug: "openclaw", displayName: "OpenClaw", section: "native"
|
|
1145
|
-
{ slug: "claude-code", displayName: "Claude Code", section: "native"
|
|
1146
|
-
{ slug: "windsurf", displayName: "Windsurf", section: "native"
|
|
1147
|
-
{ slug: "cline", displayName: "Cline", section: "native"
|
|
1148
|
-
{ slug: "gemini-cli", displayName: "Gemini CLI", section: "native"
|
|
1275
|
+
{ slug: "openclaw", displayName: "OpenClaw", section: "native" },
|
|
1276
|
+
{ slug: "claude-code", displayName: "Claude Code", section: "native" },
|
|
1277
|
+
{ slug: "windsurf", displayName: "Windsurf", section: "native" },
|
|
1278
|
+
{ slug: "cline", displayName: "Cline", section: "native" },
|
|
1279
|
+
{ slug: "gemini-cli", displayName: "Gemini CLI", section: "native" },
|
|
1149
1280
|
{
|
|
1150
1281
|
slug: "cursor",
|
|
1151
1282
|
displayName: "Cursor",
|
|
1152
1283
|
section: "hosted",
|
|
1153
|
-
prereqUrl: "https://www.cursor.com/downloads"
|
|
1154
|
-
detectable: true
|
|
1284
|
+
prereqUrl: "https://www.cursor.com/downloads"
|
|
1155
1285
|
},
|
|
1156
1286
|
{
|
|
1157
1287
|
slug: "claude-desktop",
|
|
1158
1288
|
displayName: "Claude Desktop",
|
|
1159
1289
|
section: "hosted",
|
|
1160
|
-
prereqUrl: "https://claude.ai/download"
|
|
1161
|
-
detectable: false
|
|
1290
|
+
prereqUrl: "https://claude.ai/download"
|
|
1162
1291
|
},
|
|
1163
1292
|
{
|
|
1164
1293
|
slug: "github-copilot",
|
|
1165
1294
|
displayName: "GitHub Copilot",
|
|
1166
1295
|
section: "hosted",
|
|
1167
|
-
prereqUrl: "https://docs.github.com/en/copilot/get-started"
|
|
1168
|
-
detectable: false
|
|
1296
|
+
prereqUrl: "https://docs.github.com/en/copilot/get-started"
|
|
1169
1297
|
},
|
|
1170
1298
|
{
|
|
1171
1299
|
slug: "kilo-code",
|
|
1172
1300
|
displayName: "Kilo Code",
|
|
1173
1301
|
section: "hosted",
|
|
1174
|
-
prereqUrl: "https://kilocode.ai/docs/getting-started"
|
|
1175
|
-
detectable: false
|
|
1302
|
+
prereqUrl: "https://kilocode.ai/docs/getting-started"
|
|
1176
1303
|
},
|
|
1177
1304
|
{
|
|
1178
1305
|
slug: "continue-dev",
|
|
1179
1306
|
displayName: "Continue",
|
|
1180
1307
|
section: "hosted",
|
|
1181
|
-
prereqUrl: "https://docs.continue.dev/ide-extensions/install"
|
|
1182
|
-
detectable: false
|
|
1308
|
+
prereqUrl: "https://docs.continue.dev/ide-extensions/install"
|
|
1183
1309
|
},
|
|
1184
1310
|
{
|
|
1185
1311
|
slug: "goose",
|
|
1186
1312
|
displayName: "Goose",
|
|
1187
1313
|
section: "hosted",
|
|
1188
|
-
prereqUrl: "https://goose-docs.ai/docs/quickstart/"
|
|
1189
|
-
detectable: false
|
|
1314
|
+
prereqUrl: "https://goose-docs.ai/docs/quickstart/"
|
|
1190
1315
|
},
|
|
1191
|
-
{ slug: "other-mcp", displayName: "Local MCP / Other", section: "hosted"
|
|
1316
|
+
{ slug: "other-mcp", displayName: "Local MCP / Other", section: "hosted" }
|
|
1192
1317
|
];
|
|
1193
1318
|
var INIT_WIZARD_MENU_SECTIONS = (() => {
|
|
1194
1319
|
const itemsFor = (section) => INIT_WIZARD_PLATFORM_REGISTRY.filter((e) => e.section === section).map((e) => ({
|
|
@@ -1219,45 +1344,17 @@ async function promptHostedProxyInstallPrereq(ask, platformLabel, prereqUrl) {
|
|
|
1219
1344
|
const answer = await ask("Ready to continue? (Y/n) ");
|
|
1220
1345
|
return answer.trim().toLowerCase() !== "n";
|
|
1221
1346
|
}
|
|
1222
|
-
function isPlatformDetectedForMenu(slug) {
|
|
1223
|
-
switch (slug) {
|
|
1224
|
-
case "openclaw":
|
|
1225
|
-
return isOpenClawConnected();
|
|
1226
|
-
case "claude-code":
|
|
1227
|
-
return Promise.resolve(isClaudeCodeConnected());
|
|
1228
|
-
case "cursor":
|
|
1229
|
-
return isCursorConnected();
|
|
1230
|
-
case "windsurf":
|
|
1231
|
-
return isWindsurfConnected();
|
|
1232
|
-
default:
|
|
1233
|
-
return Promise.resolve(false);
|
|
1234
|
-
}
|
|
1235
|
-
}
|
|
1236
1347
|
async function promptPlatformSelection(ask) {
|
|
1237
1348
|
process.stderr.write(
|
|
1238
1349
|
"\n" + style.bold(style.violet("Which platform are you connecting?")) + "\n\n"
|
|
1239
1350
|
);
|
|
1240
|
-
const detectionSlugs = INIT_WIZARD_PLATFORM_REGISTRY.filter((e) => e.detectable).map(
|
|
1241
|
-
(e) => e.slug
|
|
1242
|
-
);
|
|
1243
|
-
const connectedFlags = await Promise.all(
|
|
1244
|
-
detectionSlugs.map((slug) => isPlatformDetectedForMenu(slug))
|
|
1245
|
-
);
|
|
1246
|
-
function markerFor(platform) {
|
|
1247
|
-
const idx = detectionSlugs.indexOf(platform);
|
|
1248
|
-
if (idx === -1) return "";
|
|
1249
|
-
if (!connectedFlags[idx]) return "";
|
|
1250
|
-
return " " + style.dim("\u25CF detected locally");
|
|
1251
|
-
}
|
|
1252
1351
|
let optionNum = 1;
|
|
1253
1352
|
for (const section of INIT_WIZARD_MENU_SECTIONS) {
|
|
1254
1353
|
process.stderr.write(" " + style.dim(section.title) + "\n");
|
|
1255
1354
|
for (const item of section.items) {
|
|
1256
1355
|
const indent = optionNum >= 10 ? " " : " ";
|
|
1257
|
-
process.stderr.write(
|
|
1258
|
-
|
|
1259
|
-
`
|
|
1260
|
-
);
|
|
1356
|
+
process.stderr.write(`${indent}${style.violet(String(optionNum))}. ${item.label}
|
|
1357
|
+
`);
|
|
1261
1358
|
optionNum++;
|
|
1262
1359
|
}
|
|
1263
1360
|
}
|
|
@@ -1720,6 +1817,7 @@ async function runInit(explicitBaseUrl) {
|
|
|
1720
1817
|
spinner.stop(true, "Key validated");
|
|
1721
1818
|
apiKey = key;
|
|
1722
1819
|
}
|
|
1820
|
+
await warnIfInstalledShieldIsOutdated();
|
|
1723
1821
|
const configuredAgents = [];
|
|
1724
1822
|
let currentAgents = collectAgentsFromConfig(existing);
|
|
1725
1823
|
let lastConfig = {
|
|
@@ -1791,63 +1889,41 @@ async function runInit(explicitBaseUrl) {
|
|
|
1791
1889
|
) + "\n"
|
|
1792
1890
|
);
|
|
1793
1891
|
if (agentsForPlatform.length > 0) {
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
);
|
|
1797
|
-
if (exactForWorkspace !== void 0) {
|
|
1798
|
-
process.stderr.write(
|
|
1799
|
-
`
|
|
1800
|
-
This workspace already has a ${selectedLabel} agent registered (${style.cyan(
|
|
1801
|
-
exactForWorkspace.name
|
|
1802
|
-
)}).
|
|
1803
|
-
`
|
|
1804
|
-
);
|
|
1805
|
-
process.stderr.write(
|
|
1806
|
-
style.dim(
|
|
1807
|
-
"Replace updates this directory's saved agent. (n) returns to platform selection \u2014 the wizard keeps running."
|
|
1808
|
-
) + "\n"
|
|
1809
|
-
);
|
|
1810
|
-
const replace = await ask("Replace it? (Y/n) ");
|
|
1811
|
-
if (replace.trim().toLowerCase() === "n") {
|
|
1812
|
-
process.stderr.write(style.dim("Skipping. Returning to platform selection.") + "\n");
|
|
1813
|
-
continue;
|
|
1814
|
-
}
|
|
1815
|
-
removeAgentNameBeforeSave = exactForWorkspace.name;
|
|
1816
|
-
} else {
|
|
1817
|
-
process.stderr.write(
|
|
1818
|
-
`
|
|
1892
|
+
process.stderr.write(
|
|
1893
|
+
`
|
|
1819
1894
|
You have ${String(agentsForPlatform.length)} agent(s) connected for ${selectedLabel}:
|
|
1820
1895
|
`
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1896
|
+
);
|
|
1897
|
+
for (const a of agentsForPlatform) {
|
|
1898
|
+
const isThisWorkspace = typeof a.workspacePath === "string" && a.workspacePath.length > 0 && resolve(a.workspacePath) === initWorkspacePath;
|
|
1899
|
+
const wsHint = typeof a.workspacePath === "string" && a.workspacePath.length > 0 ? ` ${style.dim(a.workspacePath)}` : "";
|
|
1900
|
+
const marker = isThisWorkspace ? ` ${style.yellow("(this workspace)")}` : "";
|
|
1901
|
+
process.stderr.write(` ${style.dim("\u2022")} ${style.cyan(a.name)}${wsHint}${marker}
|
|
1825
1902
|
`);
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1903
|
+
}
|
|
1904
|
+
process.stderr.write("\n" + style.bold("What would you like to do?") + "\n");
|
|
1905
|
+
const actionIdx = await arrowSelect(
|
|
1906
|
+
[
|
|
1907
|
+
"Add a new agent alongside these",
|
|
1908
|
+
"Replace an existing agent",
|
|
1909
|
+
"Skip - choose a different platform"
|
|
1910
|
+
],
|
|
1911
|
+
ask,
|
|
1912
|
+
"Action"
|
|
1913
|
+
);
|
|
1914
|
+
if (actionIdx === 2) {
|
|
1915
|
+
continue;
|
|
1916
|
+
}
|
|
1917
|
+
if (actionIdx === 1) {
|
|
1918
|
+
process.stderr.write("\n" + style.bold("Which agent to replace?") + "\n");
|
|
1919
|
+
const replaceIdx = await arrowSelect(
|
|
1920
|
+
agentsForPlatform.map((a) => a.name),
|
|
1834
1921
|
ask,
|
|
1835
|
-
"
|
|
1922
|
+
"Agent"
|
|
1836
1923
|
);
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
if (actionIdx === 1) {
|
|
1841
|
-
process.stderr.write("\n" + style.bold("Which agent to replace?") + "\n");
|
|
1842
|
-
const replaceIdx = await arrowSelect(
|
|
1843
|
-
agentsForPlatform.map((a) => a.name),
|
|
1844
|
-
ask,
|
|
1845
|
-
"Agent"
|
|
1846
|
-
);
|
|
1847
|
-
const victim = agentsForPlatform[replaceIdx];
|
|
1848
|
-
if (victim !== void 0) {
|
|
1849
|
-
removeAgentNameBeforeSave = victim.name;
|
|
1850
|
-
}
|
|
1924
|
+
const victim = agentsForPlatform[replaceIdx];
|
|
1925
|
+
if (victim !== void 0) {
|
|
1926
|
+
removeAgentNameBeforeSave = victim.name;
|
|
1851
1927
|
}
|
|
1852
1928
|
}
|
|
1853
1929
|
}
|
|
@@ -1949,16 +2025,24 @@ You have ${String(agentsForPlatform.length)} agent(s) connected for ${selectedLa
|
|
|
1949
2025
|
});
|
|
1950
2026
|
setupSucceeded = true;
|
|
1951
2027
|
} else if (selectedPlatform === "claude-code") {
|
|
1952
|
-
process.stderr.write("\nTo connect Claude Code to Shield:\n\n");
|
|
1953
|
-
process.stderr.write(
|
|
1954
|
-
" " + style.bold("Step 1") + " - Add the Multicorn marketplace:\n " + style.cyan("claude plugin marketplace add Multicorn-AI/multicorn-shield") + "\n\n"
|
|
1955
|
-
);
|
|
1956
2028
|
process.stderr.write(
|
|
1957
|
-
"
|
|
2029
|
+
"\n" + style.dim("Configuring Shield hooks in Claude Code user settings...") + "\n"
|
|
1958
2030
|
);
|
|
2031
|
+
const hooksOk = await installClaudeCodeUserSettingsHooks(ask);
|
|
2032
|
+
if (!hooksOk) {
|
|
2033
|
+
process.stderr.write(style.dim("Skipped Claude Code hook installation.\n"));
|
|
2034
|
+
continue;
|
|
2035
|
+
}
|
|
1959
2036
|
process.stderr.write(
|
|
1960
|
-
style.
|
|
2037
|
+
style.green("\u2713") + " Shield hooks added to " + style.cyan("~/.claude/settings.json") + "\n"
|
|
1961
2038
|
);
|
|
2039
|
+
if (claudeInstalledPluginsListsMulticornShield()) {
|
|
2040
|
+
process.stderr.write(
|
|
2041
|
+
style.dim(
|
|
2042
|
+
"Note: You have the multicorn-shield Claude Code plugin installed. The plugin is no longer needed - hooks are now written directly to settings.json. You can uninstall it with: "
|
|
2043
|
+
) + style.cyan("claude plugin uninstall multicorn-shield@multicorn-shield") + "\n"
|
|
2044
|
+
);
|
|
2045
|
+
}
|
|
1962
2046
|
configuredAgents.push({
|
|
1963
2047
|
selection,
|
|
1964
2048
|
platform: selectedPlatform,
|
|
@@ -2313,42 +2397,42 @@ You have ${String(agentsForPlatform.length)} agent(s) connected for ${selectedLa
|
|
|
2313
2397
|
const blocks = [];
|
|
2314
2398
|
if (configuredPlatforms.has("openclaw")) {
|
|
2315
2399
|
blocks.push(
|
|
2316
|
-
"\n" + style.bold("
|
|
2400
|
+
"\n" + style.bold("OpenClaw") + "\n \u2192 Restart your gateway: " + style.cyan("openclaw gateway restart") + "\n \u2192 Start a session: " + style.cyan("openclaw tui") + "\n"
|
|
2317
2401
|
);
|
|
2318
2402
|
}
|
|
2319
2403
|
if (configuredPlatforms.has("claude-code")) {
|
|
2320
2404
|
blocks.push(
|
|
2321
|
-
"\n" + style.bold("
|
|
2405
|
+
"\n" + style.bold("Claude Code") + "\n \u2192 Start Claude Code: " + style.cyan("claude") + "\n \u2192 Shield will intercept tool calls automatically.\n"
|
|
2322
2406
|
);
|
|
2323
2407
|
}
|
|
2324
2408
|
if (configuredPlatforms.has("claude-desktop")) {
|
|
2325
2409
|
blocks.push(
|
|
2326
|
-
"\n" + style.bold("
|
|
2410
|
+
"\n" + style.bold("Claude Desktop") + "\n \u2192 Restart Claude Desktop to pick up config changes\n"
|
|
2327
2411
|
);
|
|
2328
2412
|
}
|
|
2329
2413
|
if (configuredPlatforms.has("cursor")) {
|
|
2330
2414
|
blocks.push(
|
|
2331
|
-
"\n" + style.bold("
|
|
2415
|
+
"\n" + style.bold("Cursor") + "\n \u2192 If needed, download Cursor from " + style.cyan("https://cursor.com/downloads") + "\n \u2192 Restart Cursor so it loads the MCP server\n \u2192 Ask the agent to use your Shield MCP tools by short name\n"
|
|
2332
2416
|
);
|
|
2333
2417
|
}
|
|
2334
2418
|
if (configuredPlatforms.has("kilo-code")) {
|
|
2335
2419
|
blocks.push(
|
|
2336
|
-
"\n" + style.bold("
|
|
2420
|
+
"\n" + style.bold("Kilo Code") + "\n \u2192 Restart the editor or reload the window if the MCP server does not appear\n \u2192 Run your next task in Kilo Code so it picks up Shield\n"
|
|
2337
2421
|
);
|
|
2338
2422
|
}
|
|
2339
2423
|
if (configuredPlatforms.has("github-copilot")) {
|
|
2340
2424
|
blocks.push(
|
|
2341
|
-
"\n" + style.bold("GitHub Copilot
|
|
2425
|
+
"\n" + style.bold("GitHub Copilot") + "\n \u2192 Reload the editor window if the MCP server does not appear\n \u2192 Use Copilot Agent mode and verify the MCP server connects\n"
|
|
2342
2426
|
);
|
|
2343
2427
|
}
|
|
2344
2428
|
if (configuredPlatforms.has("continue-dev")) {
|
|
2345
2429
|
blocks.push(
|
|
2346
|
-
"\n" + style.bold("Continue
|
|
2430
|
+
"\n" + style.bold("Continue") + "\n \u2192 If needed, install Continue from " + style.cyan("https://docs.continue.dev/ide-extensions/install") + "\n \u2192 Reload VS Code and open Continue agent mode\n"
|
|
2347
2431
|
);
|
|
2348
2432
|
}
|
|
2349
2433
|
if (configuredPlatforms.has("goose")) {
|
|
2350
2434
|
blocks.push(
|
|
2351
|
-
"\n" + style.bold("Goose
|
|
2435
|
+
"\n" + style.bold("Goose") + "\n \u2192 Start a new Goose session after updating config\n"
|
|
2352
2436
|
);
|
|
2353
2437
|
}
|
|
2354
2438
|
const windsurfNativeConfigured = configuredAgents.some(
|
|
@@ -2359,12 +2443,12 @@ You have ${String(agentsForPlatform.length)} agent(s) connected for ${selectedLa
|
|
|
2359
2443
|
);
|
|
2360
2444
|
if (windsurfNativeConfigured) {
|
|
2361
2445
|
blocks.push(
|
|
2362
|
-
"\n" + style.bold("
|
|
2446
|
+
"\n" + style.bold("Windsurf (native)") + "\n \u2192 Restart Windsurf (quit fully, then reopen)\n"
|
|
2363
2447
|
);
|
|
2364
2448
|
}
|
|
2365
2449
|
if (windsurfHostedConfigured) {
|
|
2366
2450
|
blocks.push(
|
|
2367
|
-
"\n" + style.bold("
|
|
2451
|
+
"\n" + style.bold("Windsurf (hosted)") + "\n \u2192 If needed, install from " + style.cyan("https://windsurf.com/download") + "\n \u2192 Restart Windsurf so it loads the MCP server\n"
|
|
2368
2452
|
);
|
|
2369
2453
|
}
|
|
2370
2454
|
const clineNativeConfigured = configuredAgents.some(
|
|
@@ -2375,12 +2459,12 @@ You have ${String(agentsForPlatform.length)} agent(s) connected for ${selectedLa
|
|
|
2375
2459
|
);
|
|
2376
2460
|
if (clineNativeConfigured) {
|
|
2377
2461
|
blocks.push(
|
|
2378
|
-
"\n" + style.bold("
|
|
2462
|
+
"\n" + style.bold("Cline (native)") + "\n \u2192 Enable Hooks in Cline settings (Advanced), then reload the VS Code window\n \u2192 Trigger a tool call to verify Shield is intercepting\n"
|
|
2379
2463
|
);
|
|
2380
2464
|
}
|
|
2381
2465
|
if (clineHostedConfigured) {
|
|
2382
2466
|
blocks.push(
|
|
2383
|
-
"\n" + style.bold("
|
|
2467
|
+
"\n" + style.bold("Cline (hosted)") + "\n \u2192 Restart Cline or reload the VS Code window\n"
|
|
2384
2468
|
);
|
|
2385
2469
|
}
|
|
2386
2470
|
const geminiCliNativeConfigured = configuredAgents.some(
|
|
@@ -2391,12 +2475,12 @@ You have ${String(agentsForPlatform.length)} agent(s) connected for ${selectedLa
|
|
|
2391
2475
|
);
|
|
2392
2476
|
if (geminiCliNativeConfigured) {
|
|
2393
2477
|
blocks.push(
|
|
2394
|
-
"\n" + style.bold("Gemini CLI native
|
|
2478
|
+
"\n" + style.bold("Gemini CLI (native)") + "\n \u2192 Restart Gemini CLI to activate Shield governance\n"
|
|
2395
2479
|
);
|
|
2396
2480
|
}
|
|
2397
2481
|
if (geminiCliHostedConfigured) {
|
|
2398
2482
|
blocks.push(
|
|
2399
|
-
"\n" + style.bold("
|
|
2483
|
+
"\n" + style.bold("Gemini CLI (hosted)") + "\n \u2192 Restart Gemini CLI, then run " + style.cyan("/mcp") + " to verify the server\n"
|
|
2400
2484
|
);
|
|
2401
2485
|
}
|
|
2402
2486
|
if (blocks.length > 0) {
|
|
@@ -45,9 +45,9 @@ var TOOL_MAP = {
|
|
|
45
45
|
slack_read: { service: "slack", permissionLevel: "read" },
|
|
46
46
|
slack_message: { service: "slack", permissionLevel: "write" },
|
|
47
47
|
// Payments
|
|
48
|
-
payments: { service: "payments", permissionLevel: "
|
|
49
|
-
payment: { service: "payments", permissionLevel: "
|
|
50
|
-
stripe: { service: "payments", permissionLevel: "
|
|
48
|
+
payments: { service: "payments", permissionLevel: "write" },
|
|
49
|
+
payment: { service: "payments", permissionLevel: "write" },
|
|
50
|
+
stripe: { service: "payments", permissionLevel: "write" }
|
|
51
51
|
};
|
|
52
52
|
function mapToolToScope(toolName, command) {
|
|
53
53
|
const normalized = toolName.trim().toLowerCase();
|