multicorn-shield 0.12.0 → 1.0.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.
@@ -696,7 +696,7 @@ async function beforeToolCall(event, ctx) {
696
696
  );
697
697
  if (config.apiKey.length === 0) {
698
698
  pluginLogger?.warn(
699
- "Multicorn Shield: No API key found. Run 'npx multicorn-proxy init' or set MULTICORN_API_KEY."
699
+ "Multicorn Shield: No API key found. Run 'npx multicorn-shield init' or set MULTICORN_API_KEY."
700
700
  );
701
701
  console.error("[SHIELD] DECISION: allow (no API key)");
702
702
  return void 0;
@@ -886,7 +886,7 @@ var plugin = {
886
886
  api.logger.info("Multicorn Shield plugin registered.");
887
887
  if (config.apiKey.length === 0) {
888
888
  api.logger.error(
889
- "Multicorn Shield: No API key found. Run 'npx multicorn-proxy init' or set MULTICORN_API_KEY."
889
+ "Multicorn Shield: No API key found. Run 'npx multicorn-shield init' or set MULTICORN_API_KEY."
890
890
  );
891
891
  } else {
892
892
  api.logger.info(`Multicorn Shield connecting to ${config.baseUrl}`);
package/dist/proxy.cjs CHANGED
@@ -76,7 +76,7 @@ function buildServiceUnreachableResponse(id, dashboardUrl) {
76
76
  };
77
77
  }
78
78
  function buildAuthErrorResponse(id) {
79
- const message = "Action blocked: Shield API key is invalid or has been revoked. Run npx multicorn-proxy init to reconfigure.";
79
+ const message = "Action blocked: Shield API key is invalid or has been revoked. Run npx multicorn-shield init to reconfigure.";
80
80
  return {
81
81
  jsonrpc: "2.0",
82
82
  id,
package/dist/proxy.js CHANGED
@@ -74,7 +74,7 @@ function buildServiceUnreachableResponse(id, dashboardUrl) {
74
74
  };
75
75
  }
76
76
  function buildAuthErrorResponse(id) {
77
- const message = "Action blocked: Shield API key is invalid or has been revoked. Run npx multicorn-proxy init to reconfigure.";
77
+ const message = "Action blocked: Shield API key is invalid or has been revoked. Run npx multicorn-shield init to reconfigure.";
78
78
  return {
79
79
  jsonrpc: "2.0",
80
80
  id,
@@ -22359,7 +22359,7 @@ async function writeExtensionBackup(claudeDesktopConfigPath, mcpServers) {
22359
22359
 
22360
22360
  // package.json
22361
22361
  var package_default = {
22362
- version: "0.12.0"};
22362
+ version: "1.0.0"};
22363
22363
 
22364
22364
  // src/package-meta.ts
22365
22365
  var PACKAGE_VERSION = package_default.version;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "multicorn-shield",
3
- "version": "0.12.0",
3
+ "version": "1.0.0",
4
4
  "description": "The control layer for AI agents: permissions, consent, spending limits, and audit logging.",
5
5
  "license": "MIT",
6
6
  "author": "Multicorn AI Pty Ltd",
@@ -31,13 +31,14 @@
31
31
  }
32
32
  },
33
33
  "bin": {
34
- "multicorn-proxy": "./dist/multicorn-proxy.js",
35
- "multicorn-shield": "./dist/multicorn-shield.js"
34
+ "multicorn-shield": "./dist/multicorn-shield.js",
35
+ "multicorn-proxy": "./dist/multicorn-proxy.js"
36
36
  },
37
37
  "files": [
38
38
  "dist",
39
39
  "plugins/windsurf",
40
40
  "plugins/cline",
41
+ "plugins/gemini-cli",
41
42
  "LICENSE",
42
43
  "README.md",
43
44
  "CHANGELOG.md"
@@ -0,0 +1,110 @@
1
+ #!/usr/bin/env node
2
+ // Copyright (c) Multicorn AI Pty Ltd. MIT License. See LICENSE file.
3
+ /**
4
+ * Gemini CLI AfterTool hook: audit logging to Shield.
5
+ * Always returns { "decision": "allow" } on stdout.
6
+ */
7
+
8
+ "use strict";
9
+
10
+ const {
11
+ loadConfig,
12
+ logPrefix,
13
+ mapToolName,
14
+ postJson,
15
+ readStdin,
16
+ scrubParameters,
17
+ scrubResultForMetadata,
18
+ } = require("./shared.cjs");
19
+
20
+ const HOOK_PREFIX = logPrefix("after-tool");
21
+
22
+ function respond() {
23
+ process.stdout.write(JSON.stringify({ decision: "allow" }) + "\n");
24
+ process.exit(0);
25
+ }
26
+
27
+ async function main() {
28
+ let raw;
29
+ try {
30
+ raw = await readStdin();
31
+ } catch {
32
+ respond();
33
+ return;
34
+ }
35
+
36
+ /** @type {Record<string, unknown>} */
37
+ let hookPayload;
38
+ try {
39
+ hookPayload = JSON.parse(raw.length > 0 ? raw : "{}");
40
+ } catch {
41
+ respond();
42
+ return;
43
+ }
44
+
45
+ const toolName = typeof hookPayload.tool_name === "string" ? hookPayload.tool_name : "";
46
+ const mapped = mapToolName(toolName);
47
+
48
+ if (mapped === null) {
49
+ respond();
50
+ return;
51
+ }
52
+ const { service, actionType } = mapped;
53
+
54
+ const config = loadConfig();
55
+ if (config === null || config.apiKey.length === 0 || config.agentName.length === 0) {
56
+ respond();
57
+ return;
58
+ }
59
+
60
+ const toolInput =
61
+ typeof hookPayload.tool_input === "object" && hookPayload.tool_input !== null
62
+ ? /** @type {Record<string, unknown>} */ (hookPayload.tool_input)
63
+ : {};
64
+
65
+ const paramsSerialized = scrubParameters(toolInput);
66
+ const toolResponse = hookPayload.tool_response;
67
+
68
+ /** @type {Record<string, unknown>} */
69
+ const metadata = {
70
+ tool_name: toolName,
71
+ session_id: typeof hookPayload.session_id === "string" ? hookPayload.session_id : "",
72
+ cwd: typeof hookPayload.cwd === "string" ? hookPayload.cwd : "",
73
+ parameters: paramsSerialized,
74
+ result: scrubResultForMetadata(toolResponse),
75
+ source: "gemini-cli",
76
+ };
77
+
78
+ /** @type {Record<string, unknown>} */
79
+ const payload = {
80
+ agent: config.agentName,
81
+ service,
82
+ actionType,
83
+ status: "approved",
84
+ metadata,
85
+ platform: "gemini-cli",
86
+ };
87
+
88
+ try {
89
+ const res = await postJson(config.baseUrl, config.apiKey, payload);
90
+ const code = res.statusCode ?? 0;
91
+ if (code < 200 || code >= 300) {
92
+ throw new Error(`HTTP ${String(code)}`);
93
+ }
94
+ } catch (e) {
95
+ const msg = e instanceof Error ? e.message : String(e);
96
+ process.stderr.write(
97
+ `${HOOK_PREFIX} Warning: failed to log action to Shield audit trail. Detail: ${msg}\n`,
98
+ );
99
+ }
100
+
101
+ respond();
102
+ }
103
+
104
+ main().catch((e) => {
105
+ const msg = e instanceof Error ? e.message : String(e);
106
+ process.stderr.write(
107
+ `${HOOK_PREFIX} Warning: failed to log action to Shield audit trail. Detail: ${msg}\n`,
108
+ );
109
+ respond();
110
+ });
@@ -0,0 +1,197 @@
1
+ #!/usr/bin/env node
2
+ // Copyright (c) Multicorn AI Pty Ltd. MIT License. See LICENSE file.
3
+ /**
4
+ * Gemini CLI BeforeTool hook: asks Shield whether a tool call is allowed.
5
+ * Reads JSON from stdin. Writes JSON to stdout only (decision allow/deny). Logs to stderr.
6
+ * Fail-open on missing config or unreachable API.
7
+ */
8
+
9
+ "use strict";
10
+
11
+ const {
12
+ loadConfig,
13
+ logPrefix,
14
+ mapToolName,
15
+ postJson,
16
+ readStdin,
17
+ safeJsonParse,
18
+ scrubParameters,
19
+ unwrapData,
20
+ consentUrl,
21
+ openBrowser,
22
+ } = require("./shared.cjs");
23
+
24
+ const HOOK_PREFIX = logPrefix("before-tool");
25
+
26
+ /**
27
+ * @param {string} decision
28
+ * @param {string} [reason]
29
+ */
30
+ function respond(decision, reason) {
31
+ /** @type {Record<string, unknown>} */
32
+ const out = { decision };
33
+ if (reason !== undefined && reason.length > 0) {
34
+ out.reason = reason;
35
+ }
36
+ process.stdout.write(JSON.stringify(out) + "\n");
37
+ process.exit(0);
38
+ }
39
+
40
+ function dashboardHintUrl(apiBaseUrl) {
41
+ try {
42
+ const raw = String(apiBaseUrl).replace(/\/+$/, "");
43
+ const lower = raw.toLowerCase();
44
+ if (lower.includes("localhost:8080") || lower.includes("127.0.0.1:8080")) {
45
+ return "http://localhost:5173/approvals";
46
+ }
47
+ const u = new URL(raw);
48
+ if (u.hostname.startsWith("api.")) {
49
+ u.hostname = "app." + u.hostname.slice(4);
50
+ }
51
+ return `${u.origin}/approvals`;
52
+ } catch {
53
+ return "https://app.multicorn.ai/approvals";
54
+ }
55
+ }
56
+
57
+ /**
58
+ * @param {unknown} data
59
+ * @param {string} approvalsUrl
60
+ */
61
+ function blockedReason(data, approvalsUrl) {
62
+ if (data !== null && typeof data === "object") {
63
+ const d = /** @type {Record<string, unknown>} */ (data);
64
+ const meta = d.metadata;
65
+ if (typeof meta === "string" && meta.length > 0) {
66
+ try {
67
+ const parsed = JSON.parse(meta);
68
+ if (parsed !== null && typeof parsed === "object" && "block_reason" in parsed) {
69
+ const br = /** @type {Record<string, unknown>} */ (parsed).block_reason;
70
+ if (typeof br === "string" && br.length > 0) {
71
+ return `Blocked by Multicorn Shield: ${br}. Grant access at ${approvalsUrl}`;
72
+ }
73
+ }
74
+ } catch {
75
+ /* ignore */
76
+ }
77
+ }
78
+ }
79
+ return `Blocked by Multicorn Shield. Grant access at ${approvalsUrl}`;
80
+ }
81
+
82
+ async function main() {
83
+ let raw;
84
+ try {
85
+ raw = await readStdin();
86
+ } catch (e) {
87
+ const msg = e instanceof Error ? e.message : String(e);
88
+ process.stderr.write(`${HOOK_PREFIX} could not read stdin (${msg}). Allowing.\n`);
89
+ respond("allow");
90
+ return;
91
+ }
92
+
93
+ /** @type {Record<string, unknown>} */
94
+ let hookPayload;
95
+ try {
96
+ hookPayload = JSON.parse(raw.length > 0 ? raw : "{}");
97
+ } catch (e) {
98
+ const msg = e instanceof Error ? e.message : String(e);
99
+ process.stderr.write(`${HOOK_PREFIX} invalid JSON (${msg}). Allowing.\n`);
100
+ respond("allow");
101
+ return;
102
+ }
103
+
104
+ const toolName = typeof hookPayload.tool_name === "string" ? hookPayload.tool_name : "";
105
+
106
+ const mapped = mapToolName(toolName);
107
+ if (mapped === null) {
108
+ respond("allow");
109
+ return;
110
+ }
111
+ const { service, actionType } = mapped;
112
+
113
+ const toolInput =
114
+ typeof hookPayload.tool_input === "object" && hookPayload.tool_input !== null
115
+ ? /** @type {Record<string, unknown>} */ (hookPayload.tool_input)
116
+ : {};
117
+
118
+ const config = loadConfig();
119
+ if (config === null || config.apiKey.length === 0 || config.agentName.length === 0) {
120
+ respond("allow");
121
+ return;
122
+ }
123
+
124
+ const paramsSerialized = scrubParameters(toolInput);
125
+ const approvalsUrl = dashboardHintUrl(config.baseUrl);
126
+
127
+ /** @type {Record<string, unknown>} */
128
+ const metadata = {
129
+ tool_name: toolName,
130
+ session_id: typeof hookPayload.session_id === "string" ? hookPayload.session_id : "",
131
+ cwd: typeof hookPayload.cwd === "string" ? hookPayload.cwd : "",
132
+ parameters: paramsSerialized,
133
+ source: "gemini-cli",
134
+ };
135
+
136
+ /** @type {Record<string, unknown>} */
137
+ const payload = {
138
+ agent: config.agentName,
139
+ service,
140
+ actionType,
141
+ status: "pending",
142
+ metadata,
143
+ platform: "gemini-cli",
144
+ };
145
+
146
+ let statusCode;
147
+ let bodyText;
148
+ try {
149
+ const res = await postJson(config.baseUrl, config.apiKey, payload);
150
+ statusCode = res.statusCode;
151
+ bodyText = res.bodyText;
152
+ } catch (e) {
153
+ const msg = e instanceof Error ? e.message : String(e);
154
+ process.stderr.write(`${HOOK_PREFIX} Shield API unreachable (${msg}). Allowing.\n`);
155
+ respond("allow");
156
+ return;
157
+ }
158
+
159
+ const parsed = safeJsonParse(bodyText);
160
+ const data = unwrapData(parsed);
161
+
162
+ if (statusCode === 202) {
163
+ const url = consentUrl(config.baseUrl, config.agentName, service, actionType);
164
+ openBrowser(url);
165
+ respond("deny", `Action blocked by Multicorn Shield. Authorise at: ${url}`);
166
+ return;
167
+ }
168
+
169
+ if (statusCode === 201) {
170
+ if (data === null || typeof data !== "object") {
171
+ const url = consentUrl(config.baseUrl, config.agentName, service, actionType);
172
+ respond("deny", `Action blocked by Multicorn Shield. Authorise at: ${url}`);
173
+ return;
174
+ }
175
+ const st = String(/** @type {Record<string, unknown>} */ (data).status || "").toLowerCase();
176
+ if (st === "approved") {
177
+ respond("allow");
178
+ return;
179
+ }
180
+ if (st === "blocked") {
181
+ respond("deny", blockedReason(data, approvalsUrl));
182
+ return;
183
+ }
184
+ const url = consentUrl(config.baseUrl, config.agentName, service, actionType);
185
+ respond("deny", `Action blocked by Multicorn Shield. Authorise at: ${url}`);
186
+ return;
187
+ }
188
+
189
+ const url = consentUrl(config.baseUrl, config.agentName, service, actionType);
190
+ respond("deny", `Action blocked by Multicorn Shield. Authorise at: ${url}`);
191
+ }
192
+
193
+ main().catch((e) => {
194
+ const msg = e instanceof Error ? e.message : String(e);
195
+ process.stderr.write(`${HOOK_PREFIX} unexpected error (${msg}). Allowing.\n`);
196
+ respond("allow");
197
+ });
@@ -0,0 +1,319 @@
1
+ // Copyright (c) Multicorn AI Pty Ltd. MIT License. See LICENSE file.
2
+
3
+ /**
4
+ * @file Shared helpers for Gemini CLI BeforeTool / AfterTool Shield hooks.
5
+ */
6
+
7
+ "use strict";
8
+
9
+ const fs = require("node:fs");
10
+ const http = require("node:http");
11
+ const https = require("node:https");
12
+ const os = require("node:os");
13
+ const path = require("node:path");
14
+
15
+ const AUTH_HEADER = "X-Multicorn-Key";
16
+ const HTTP_REQUEST_TIMEOUT_MS = 10000;
17
+
18
+ /** Tools that should pass through without calling Shield (internal / UX-only). */
19
+ const SKIP_TOOLS = new Set([
20
+ "save_memory",
21
+ "activate_skill",
22
+ "get_internal_docs",
23
+ "ask_user",
24
+ "write_todos",
25
+ "enter_plan_mode",
26
+ "exit_plan_mode",
27
+ "update_topic",
28
+ "complete_task",
29
+ ]);
30
+
31
+ /** @type {Readonly<Record<string, { service: string; actionType: string }>>} */
32
+ const TOOL_MAP = {
33
+ read_file: { service: "filesystem", actionType: "read" },
34
+ read_many_files: { service: "filesystem", actionType: "read" },
35
+ list_directory: { service: "filesystem", actionType: "read" },
36
+ glob: { service: "filesystem", actionType: "read" },
37
+ grep_search: { service: "filesystem", actionType: "read" },
38
+ write_file: { service: "filesystem", actionType: "write" },
39
+ replace: { service: "filesystem", actionType: "write" },
40
+ run_shell_command: { service: "terminal", actionType: "execute" },
41
+ google_web_search: { service: "browser", actionType: "execute" },
42
+ web_fetch: { service: "browser", actionType: "execute" },
43
+ };
44
+
45
+ function logPrefix(label) {
46
+ return `[multicorn-shield] Gemini CLI ${label}:`;
47
+ }
48
+
49
+ function readStdin() {
50
+ return new Promise((resolve, reject) => {
51
+ const chunks = [];
52
+ process.stdin.setEncoding("utf8");
53
+ process.stdin.on("data", (c) => chunks.push(c));
54
+ process.stdin.on("end", () => resolve(chunks.join("")));
55
+ process.stdin.on("error", reject);
56
+ });
57
+ }
58
+
59
+ function resolveGeminiCliAgentName(obj) {
60
+ const agents = obj.agents;
61
+ if (Array.isArray(agents)) {
62
+ for (const entry of agents) {
63
+ if (
64
+ entry &&
65
+ typeof entry === "object" &&
66
+ /** @type {{ platform?: string; name?: string }} */ (entry).platform === "gemini-cli" &&
67
+ typeof (/** @type {{ platform?: string; name?: string }} */ (entry).name) === "string"
68
+ ) {
69
+ return /** @type {{ name: string }} */ (entry).name;
70
+ }
71
+ }
72
+ }
73
+ return typeof obj.agentName === "string" ? obj.agentName : "";
74
+ }
75
+
76
+ function loadConfig() {
77
+ try {
78
+ const configPath = path.join(os.homedir(), ".multicorn", "config.json");
79
+ const raw = fs.readFileSync(configPath, "utf8");
80
+ const obj = JSON.parse(raw);
81
+ const apiKey = typeof obj.apiKey === "string" ? obj.apiKey : "";
82
+ const baseUrl =
83
+ typeof obj.baseUrl === "string" && obj.baseUrl.length > 0
84
+ ? obj.baseUrl.replace(/\/+$/, "")
85
+ : "https://api.multicorn.ai";
86
+ const baseLower = baseUrl.toLowerCase();
87
+ const isHttps = baseLower.startsWith("https://");
88
+ const isLocal = baseLower.includes("localhost") || baseLower.includes("127.0.0.1");
89
+ if (!isHttps && !isLocal) {
90
+ process.stderr.write(
91
+ `${logPrefix("config")} baseUrl must use HTTPS for non-local servers. Fail-open: Shield disabled.\n`,
92
+ );
93
+ return null;
94
+ }
95
+ const agentName = resolveGeminiCliAgentName(obj);
96
+ return { apiKey, baseUrl, agentName };
97
+ } catch {
98
+ return null;
99
+ }
100
+ }
101
+
102
+ /**
103
+ * @param {string} toolName
104
+ * @returns {{ service: string; actionType: string } | null} null = skip hook API calls
105
+ */
106
+ function mapToolName(toolName) {
107
+ const name = String(toolName || "").trim();
108
+ if (name.length === 0) return null;
109
+ if (SKIP_TOOLS.has(name)) return null;
110
+
111
+ if (name.startsWith("mcp_")) {
112
+ const rest = name.slice(4);
113
+ const idx = rest.indexOf("_");
114
+ if (idx <= 0) {
115
+ const safe = rest.replace(/[^a-zA-Z0-9._-]+/g, "_");
116
+ return { service: `mcp:${safe}`, actionType: "execute" };
117
+ }
118
+ const server = rest.slice(0, idx).replace(/[^a-zA-Z0-9._-]+/g, "_");
119
+ const tool = rest.slice(idx + 1).replace(/[^a-zA-Z0-9._-]+/g, "_");
120
+ return { service: `mcp:${server}.${tool}`, actionType: "execute" };
121
+ }
122
+
123
+ const mapped = TOOL_MAP[name];
124
+ if (mapped !== undefined) {
125
+ return mapped;
126
+ }
127
+
128
+ return { service: "unknown", actionType: "execute" };
129
+ }
130
+
131
+ function postJson(baseUrl, apiKey, bodyObj) {
132
+ return new Promise((resolve, reject) => {
133
+ let u;
134
+ try {
135
+ const root = String(baseUrl).replace(/\/+$/, "");
136
+ u = new URL(`${root}/api/v1/actions`);
137
+ } catch (e) {
138
+ reject(e);
139
+ return;
140
+ }
141
+ const payload = JSON.stringify(bodyObj);
142
+ const isHttps = u.protocol === "https:";
143
+ const lib = isHttps ? https : http;
144
+ const port = u.port || (isHttps ? 443 : 80);
145
+ const hostname = u.hostname;
146
+ /** @type {string} */
147
+ const pathnamePlusSearch = u.pathname + u.search;
148
+ const options = {
149
+ hostname,
150
+ port,
151
+ path: pathnamePlusSearch,
152
+ method: "POST",
153
+ headers: {
154
+ Connection: "close",
155
+ "Content-Type": "application/json",
156
+ "Content-Length": Buffer.byteLength(payload, "utf8"),
157
+ [AUTH_HEADER]: apiKey,
158
+ },
159
+ };
160
+ const req = lib.request(options, (res) => {
161
+ const chunks = [];
162
+ res.on("data", (c) => chunks.push(c));
163
+ res.on("end", () => {
164
+ resolve({
165
+ statusCode: res.statusCode ?? 0,
166
+ bodyText: Buffer.concat(chunks).toString("utf8"),
167
+ });
168
+ });
169
+ });
170
+ req.setTimeout(HTTP_REQUEST_TIMEOUT_MS, () => {
171
+ req.destroy(new Error("request timeout"));
172
+ });
173
+ req.on("error", reject);
174
+ req.write(payload);
175
+ req.end();
176
+ });
177
+ }
178
+
179
+ function safeJsonParse(text) {
180
+ try {
181
+ return JSON.parse(text);
182
+ } catch {
183
+ return null;
184
+ }
185
+ }
186
+
187
+ function unwrapData(body) {
188
+ if (typeof body !== "object" || body === null) return null;
189
+ const o = /** @type {Record<string, unknown>} */ (body);
190
+ return o.success === true ? o.data : null;
191
+ }
192
+
193
+ function applyParameterScrub(parameters) {
194
+ const scrubbedParams = { ...parameters };
195
+ if (typeof scrubbedParams.content === "string") {
196
+ scrubbedParams.content = `[${scrubbedParams.content.length} chars redacted]`;
197
+ }
198
+ if (typeof scrubbedParams.command === "string" && scrubbedParams.command.length > 200) {
199
+ scrubbedParams.command = scrubbedParams.command.slice(0, 200) + "... [truncated]";
200
+ }
201
+ return scrubbedParams;
202
+ }
203
+
204
+ function scrubParameters(parameters, maxLen = 4096) {
205
+ /** @type {Record<string, unknown>} */
206
+ let base = {};
207
+ if (typeof parameters === "object" && parameters !== null) {
208
+ base = { .../** @type {Record<string, unknown>} */ (parameters) };
209
+ } else if (typeof parameters === "string") {
210
+ try {
211
+ const p = JSON.parse(parameters);
212
+ if (p !== null && typeof p === "object") {
213
+ base = { .../** @type {Record<string, unknown>} */ (p) };
214
+ } else {
215
+ base = { raw: parameters };
216
+ }
217
+ } catch {
218
+ base = { raw: parameters };
219
+ }
220
+ }
221
+
222
+ const scrubbed = applyParameterScrub(base);
223
+ let paramsSerialized;
224
+ try {
225
+ paramsSerialized = JSON.stringify(scrubbed);
226
+ } catch {
227
+ paramsSerialized = "{}";
228
+ }
229
+ if (paramsSerialized.length > maxLen) {
230
+ paramsSerialized = paramsSerialized.slice(0, maxLen);
231
+ }
232
+ return paramsSerialized;
233
+ }
234
+
235
+ function scrubResultForMetadata(result) {
236
+ if (result === null || result === undefined) return "";
237
+ let s;
238
+ try {
239
+ s = typeof result === "string" ? result : JSON.stringify(result);
240
+ } catch {
241
+ s = String(result);
242
+ }
243
+ s = s.replace(/\bsk-[A-Za-z0-9_-]{8,}\b/g, "[REDACTED]");
244
+ s = s.replace(/\bmcs_[A-Za-z0-9_-]+\b/g, "[REDACTED]");
245
+ s = s.replace(/\bghp_[A-Za-z0-9]{20,}\b/g, "[REDACTED]");
246
+ s = s.replace(/Bearer\s+[^\s]+/gi, "[REDACTED]");
247
+ s = s.replace(/\b(password|token)\s*[:=]\s*[^\s]+\b/gi, "[REDACTED]");
248
+ if (s.length > 500) {
249
+ s = s.slice(0, 500) + "[truncated]";
250
+ }
251
+ return s;
252
+ }
253
+
254
+ /**
255
+ * @param {string} apiBaseUrl
256
+ * @param {string} agentName
257
+ * @param {string} service
258
+ * @param {string} actionType
259
+ */
260
+ function consentUrl(apiBaseUrl, agentName, service, actionType) {
261
+ let origin = "https://app.multicorn.ai";
262
+ try {
263
+ const raw = String(apiBaseUrl).replace(/\/+$/, "");
264
+ const lower = raw.toLowerCase();
265
+ if (lower.includes("localhost:8080") || lower.includes("127.0.0.1:8080")) {
266
+ origin = "http://localhost:5173";
267
+ } else {
268
+ const u = new URL(raw);
269
+ if (u.hostname.startsWith("api.")) {
270
+ u.hostname = "app." + u.hostname.slice(4);
271
+ }
272
+ origin = u.origin;
273
+ }
274
+ } catch {
275
+ /* keep default */
276
+ }
277
+ const params = new URLSearchParams();
278
+ params.set("agent", agentName);
279
+ params.set("scopes", `${service}:${actionType}`);
280
+ params.set("platform", "gemini-cli");
281
+ return `${origin}/consent?${params.toString()}`;
282
+ }
283
+
284
+ /** @param {string} url */
285
+ function openBrowser(url) {
286
+ try {
287
+ const { execFileSync } = require("node:child_process");
288
+ if (process.platform === "darwin") {
289
+ execFileSync("open", [url], { stdio: "ignore" });
290
+ } else if (process.platform === "win32") {
291
+ execFileSync("cmd.exe", ["/c", "start", "", url], {
292
+ stdio: "ignore",
293
+ windowsHide: true,
294
+ });
295
+ } else {
296
+ execFileSync("xdg-open", [url], { stdio: "ignore" });
297
+ }
298
+ } catch {
299
+ /* ignore */
300
+ }
301
+ }
302
+
303
+ module.exports = {
304
+ AUTH_HEADER,
305
+ logPrefix,
306
+ HTTP_REQUEST_TIMEOUT_MS,
307
+ TOOL_MAP,
308
+ readStdin,
309
+ loadConfig,
310
+ resolveGeminiCliAgentName,
311
+ mapToolName,
312
+ postJson,
313
+ safeJsonParse,
314
+ unwrapData,
315
+ scrubParameters,
316
+ scrubResultForMetadata,
317
+ consentUrl,
318
+ openBrowser,
319
+ };