multicorn-shield 1.1.0 → 1.2.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,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { existsSync, statSync } from 'fs';
3
3
  import { readFile, mkdir, writeFile, copyFile, chmod, unlink } from 'fs/promises';
4
- import { join, dirname } from 'path';
4
+ import { join, dirname, resolve, basename, sep } from 'path';
5
5
  import { homedir } from 'os';
6
6
  import { fileURLToPath } from 'url';
7
7
  import { createInterface } from 'readline';
@@ -9,714 +9,1083 @@ import { spawn } from 'child_process';
9
9
  import { createHash } from 'crypto';
10
10
  import 'stream';
11
11
 
12
- var style = {
13
- violet: (s) => `\x1B[38;2;124;58;237m${s}\x1B[0m`,
14
- violetLight: (s) => `\x1B[38;2;167;139;250m${s}\x1B[0m`,
15
- green: (s) => `\x1B[38;2;34;197;94m${s}\x1B[0m`,
16
- yellow: (s) => `\x1B[38;2;245;158;11m${s}\x1B[0m`,
17
- red: (s) => `\x1B[38;2;239;68;68m${s}\x1B[0m`,
18
- cyan: (s) => `\x1B[38;2;6;182;212m${s}\x1B[0m`,
19
- bold: (s) => `\x1B[1m${s}\x1B[0m`,
20
- dim: (s) => `\x1B[2m${s}\x1B[0m`
21
- };
22
- var BANNER = [
23
- " \u2588\u2588\u2588 \u2588 \u2588 \u2588 \u2588\u2588\u2588 \u2588 \u2588\u2588\u2584 ",
24
- " \u2588 \u2588 \u2588 \u2588 \u2588 \u2588 \u2588 \u2588",
25
- " \u2588\u2588\u2588 \u2588\u2588\u2588\u2588 \u2588 \u2588\u2588 \u2588 \u2588 \u2588",
26
- " \u2588 \u2588 \u2588 \u2588 \u2588 \u2588 \u2588 \u2588",
27
- " \u2588\u2588\u2588 \u2588 \u2588 \u2588 \u2588\u2588\u2588 \u2588\u2588\u2588 \u2588\u2588\u2580 "
28
- ].map((line) => style.violet(line)).join("\n");
29
- function withSpinner(message) {
30
- const frames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
31
- let i = 0;
32
- const interval = setInterval(() => {
33
- const frame = frames[i % frames.length];
34
- process.stderr.write(`\r${style.violet(frame ?? "\u280B")} ${message}`);
35
- i++;
36
- }, 80);
37
- return {
38
- stop(success, result) {
39
- clearInterval(interval);
40
- const icon = success ? style.green("\u2713") : style.red("\u2717");
41
- process.stderr.write(`\r\x1B[2K${icon} ${result}
42
- `);
43
- }
44
- };
12
+ var MULTICORN_DIR = join(homedir(), ".multicorn");
13
+ var SCOPES_PATH = join(MULTICORN_DIR, "scopes.json");
14
+ var CACHE_META_PATH = join(MULTICORN_DIR, "cache-meta.json");
15
+ function cacheKey(agentName, apiKey) {
16
+ return createHash("sha256").update(`${agentName}:${apiKey}`).digest("hex").slice(0, 16);
45
17
  }
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) {
18
+ async function ensureCacheIdentity(apiKey) {
19
+ const currentHash = createHash("sha256").update(apiKey).digest("hex");
20
+ let storedHash = null;
53
21
  try {
54
- if (!existsSync(path)) return false;
55
- return statSync(path).isDirectory();
22
+ const raw = await readFile(CACHE_META_PATH, "utf8");
23
+ const meta = JSON.parse(raw);
24
+ if (typeof meta === "object" && meta !== null && "apiKeyHash" in meta) {
25
+ storedHash = meta.apiKeyHash;
26
+ }
56
27
  } 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
- }
63
- var CONFIG_DIR = join(homedir(), ".multicorn");
64
- var CONFIG_PATH = join(CONFIG_DIR, "config.json");
65
- var OPENCLAW_CONFIG_PATH = join(homedir(), ".openclaw", "openclaw.json");
66
- var ANSI_PATTERN = new RegExp(String.fromCharCode(27) + "\\[[0-9;]*[a-zA-Z]", "g");
67
- function stripAnsi(str) {
68
- return str.replace(ANSI_PATTERN, "");
69
- }
70
- function normalizeAgentName(raw) {
71
- return raw.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "").replace(/-{2,}/g, "-").replace(/^-+|-+$/g, "").slice(0, 50);
72
- }
73
- function isErrnoException(e) {
74
- return typeof e === "object" && e !== null && "code" in e;
75
- }
76
- function isProxyConfig(value) {
77
- if (typeof value !== "object" || value === null) return false;
78
- const obj = value;
79
- return typeof obj["apiKey"] === "string" && typeof obj["baseUrl"] === "string";
80
- }
81
- function isAgentEntry(value) {
82
- if (typeof value !== "object" || value === null) return false;
83
- const o = value;
84
- return typeof o["name"] === "string" && typeof o["platform"] === "string";
85
- }
86
- function getAgentByPlatform(config, platform) {
87
- const list = config.agents;
88
- if (list === void 0 || list.length === 0) return void 0;
89
- return list.find((a) => a.platform === platform);
90
- }
91
- function getDefaultAgent(config) {
92
- const list = config.agents;
93
- if (list === void 0 || list.length === 0) return void 0;
94
- const defName = config.defaultAgent;
95
- if (typeof defName === "string" && defName.length > 0) {
96
- const match = list.find((a) => a.name === defName);
97
- if (match !== void 0) return match;
98
- }
99
- return list[0];
100
- }
101
- function collectAgentsFromConfig(cfg) {
102
- if (cfg === null) return [];
103
- if (cfg.agents !== void 0 && cfg.agents.length > 0) {
104
- return cfg.agents.map((a) => ({ name: a.name, platform: a.platform }));
105
- }
106
- const raw = cfg;
107
- const legacyName = raw["agentName"];
108
- const legacyPlatform = raw["platform"];
109
- if (typeof legacyName === "string" && legacyName.length > 0) {
110
- const plat = typeof legacyPlatform === "string" && legacyPlatform.length > 0 ? legacyPlatform : "unknown";
111
- return [{ name: legacyName, platform: plat }];
112
28
  }
113
- return [];
114
- }
115
- async function parseConfigFile() {
116
- try {
117
- const raw = await readFile(CONFIG_PATH, "utf8");
29
+ if (storedHash === null || storedHash !== currentHash) {
118
30
  try {
119
- return { kind: "ok", value: JSON.parse(raw) };
31
+ await unlink(SCOPES_PATH);
120
32
  } catch {
121
- return { kind: "parseError" };
122
- }
123
- } catch (error) {
124
- if (isErrnoException(error) && error.code === "ENOENT") {
125
- return { kind: "missing" };
126
33
  }
127
- const message = error instanceof Error ? error.message : String(error);
128
- return { kind: "readError", message };
129
34
  }
130
- }
131
- function isAllowedShieldApiBaseUrl(url) {
132
- return url.startsWith("https://") || url.startsWith("http://localhost") || url.startsWith("http://127.0.0.1");
133
- }
134
- async function loadConfig() {
135
- const result = await parseConfigFile();
136
- if (result.kind !== "ok") return null;
137
- const parsed = result.value;
138
- if (!isProxyConfig(parsed)) return null;
139
- const obj = parsed;
140
- const agentNameRaw = obj["agentName"];
141
- const agentsRaw = obj["agents"];
142
- const hasNonEmptyAgents = Array.isArray(agentsRaw) && agentsRaw.length > 0 && agentsRaw.every((e) => isAgentEntry(e));
143
- const needsMigrate = typeof agentNameRaw === "string" && agentNameRaw.length > 0 && !hasNonEmptyAgents;
144
- if (!needsMigrate) {
145
- return parsed;
35
+ if (storedHash !== currentHash) {
36
+ await mkdir(MULTICORN_DIR, { recursive: true, mode: 448 });
37
+ await writeFile(CACHE_META_PATH, JSON.stringify({ apiKeyHash: currentHash }, null, 2) + "\n", {
38
+ encoding: "utf8",
39
+ mode: 384
40
+ });
146
41
  }
147
- const platform = typeof obj["platform"] === "string" && obj["platform"].length > 0 ? obj["platform"] : "unknown";
148
- const next = { ...obj };
149
- delete next["agentName"];
150
- delete next["platform"];
151
- next["agents"] = [{ name: agentNameRaw, platform }];
152
- next["defaultAgent"] = agentNameRaw;
153
- const migrated = next;
154
- await saveConfig(migrated);
155
- return migrated;
156
42
  }
157
- async function readBaseUrlFromConfig() {
158
- const result = await parseConfigFile();
159
- if (result.kind === "missing") return void 0;
160
- if (result.kind === "readError") {
161
- process.stderr.write(
162
- style.yellow(`Warning: could not read base URL from config file: ${result.message}`) + "\n"
163
- );
164
- return void 0;
165
- }
166
- if (result.kind === "parseError") {
167
- process.stderr.write(
168
- style.yellow("Warning: could not parse ~/.multicorn/config.json as JSON.") + "\n"
169
- );
170
- return void 0;
43
+ async function loadCachedScopes(agentName, apiKey) {
44
+ if (apiKey.length === 0) return null;
45
+ await ensureCacheIdentity(apiKey);
46
+ const key = cacheKey(agentName, apiKey);
47
+ try {
48
+ const raw = await readFile(SCOPES_PATH, "utf8");
49
+ const parsed = JSON.parse(raw);
50
+ if (!isScopesCacheFile(parsed)) return null;
51
+ const entry = parsed[key];
52
+ return entry?.scopes ?? null;
53
+ } catch {
54
+ return null;
171
55
  }
172
- const parsed = result.value;
173
- if (typeof parsed !== "object" || parsed === null) return void 0;
174
- const u = parsed["baseUrl"];
175
- if (typeof u !== "string" || u.length === 0) return void 0;
176
- return u;
177
56
  }
178
- async function deleteAgentByName(name) {
179
- const config = await loadConfig();
180
- if (config === null) return false;
181
- const agents = collectAgentsFromConfig(config);
182
- const idx = agents.findIndex((a) => a.name === name);
183
- if (idx === -1) return false;
184
- const nextAgents = agents.filter((_, i) => i !== idx);
185
- let defaultAgent = config.defaultAgent;
186
- if (defaultAgent === name) {
187
- defaultAgent = void 0;
188
- }
189
- const raw = { ...config };
190
- if (nextAgents.length > 0) {
191
- raw["agents"] = nextAgents;
192
- } else {
193
- delete raw["agents"];
194
- }
195
- if (defaultAgent !== void 0 && defaultAgent.length > 0) {
196
- raw["defaultAgent"] = defaultAgent;
197
- } else {
198
- delete raw["defaultAgent"];
57
+ async function saveCachedScopes(agentName, agentId, scopes, apiKey) {
58
+ if (apiKey.length === 0) return;
59
+ await ensureCacheIdentity(apiKey);
60
+ const key = cacheKey(agentName, apiKey);
61
+ await mkdir(MULTICORN_DIR, { recursive: true, mode: 448 });
62
+ let existing = {};
63
+ try {
64
+ const raw = await readFile(SCOPES_PATH, "utf8");
65
+ const parsed = JSON.parse(raw);
66
+ if (isScopesCacheFile(parsed)) existing = parsed;
67
+ } catch {
199
68
  }
200
- await saveConfig(raw);
201
- return true;
202
- }
203
- async function saveConfig(config) {
204
- await mkdir(CONFIG_DIR, { recursive: true, mode: 448 });
205
- await writeFile(CONFIG_PATH, JSON.stringify(config, null, 2) + "\n", {
69
+ const updated = {
70
+ ...existing,
71
+ [key]: {
72
+ agentId,
73
+ scopes,
74
+ fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
75
+ }
76
+ };
77
+ await writeFile(SCOPES_PATH, JSON.stringify(updated, null, 2) + "\n", {
206
78
  encoding: "utf8",
207
79
  mode: 384
208
80
  });
209
81
  }
210
- var OPENCLAW_MIN_VERSION = "2026.2.26";
211
- async function detectOpenClaw() {
212
- let raw;
82
+ function isScopesCacheFile(value) {
83
+ return typeof value === "object" && value !== null;
84
+ }
85
+
86
+ // src/proxy/consent.ts
87
+ var CONSENT_POLL_INTERVAL_MS = 3e3;
88
+ var CONSENT_POLL_TIMEOUT_MS = 5 * 60 * 1e3;
89
+ function deriveDashboardUrl(baseUrl) {
213
90
  try {
214
- raw = await readFile(OPENCLAW_CONFIG_PATH, "utf8");
215
- } catch (e) {
216
- if (isErrnoException(e) && e.code === "ENOENT") {
217
- return { status: "not-found", version: null };
91
+ const url = new URL(baseUrl);
92
+ if (url.hostname === "localhost" || url.hostname === "127.0.0.1") {
93
+ url.port = "5173";
94
+ url.protocol = "http:";
95
+ return url.toString();
218
96
  }
219
- throw e;
220
- }
221
- let obj;
222
- try {
223
- obj = JSON.parse(raw);
224
- } catch {
225
- return { status: "parse-error", version: null };
226
- }
227
- const meta = obj["meta"];
228
- if (typeof meta === "object" && meta !== null) {
229
- const v = meta["lastTouchedVersion"];
230
- if (typeof v === "string" && v.length > 0) {
231
- return { status: "detected", version: v };
97
+ if (url.hostname === "api.multicorn.ai") {
98
+ url.hostname = "app.multicorn.ai";
99
+ return url.toString();
100
+ }
101
+ if (url.hostname.includes("api")) {
102
+ url.hostname = url.hostname.replace("api", "app");
103
+ return url.toString();
104
+ }
105
+ if (url.protocol === "https:" && url.hostname !== "localhost" && url.hostname !== "127.0.0.1") {
106
+ return "https://app.multicorn.ai";
232
107
  }
108
+ return "https://app.multicorn.ai";
109
+ } catch {
110
+ return "https://app.multicorn.ai";
233
111
  }
234
- return { status: "detected", version: null };
235
112
  }
236
- function isVersionAtLeast(version, minimum) {
237
- const vParts = version.split(".").map(Number);
238
- const mParts = minimum.split(".").map(Number);
239
- const len = Math.max(vParts.length, mParts.length);
240
- for (let i = 0; i < len; i++) {
241
- const v = vParts[i] ?? 0;
242
- const m = mParts[i] ?? 0;
243
- if (Number.isNaN(v) || Number.isNaN(m)) return false;
244
- if (v > m) return true;
245
- if (v < m) return false;
113
+ var ShieldAuthError = class _ShieldAuthError extends Error {
114
+ constructor(message) {
115
+ super(message);
116
+ this.name = "ShieldAuthError";
117
+ Object.setPrototypeOf(this, _ShieldAuthError.prototype);
246
118
  }
247
- return true;
248
- }
249
- async function updateOpenClawConfigIfPresent(apiKey, baseUrl, agentName) {
250
- let raw;
119
+ };
120
+ async function findAgentByName(agentName, apiKey, baseUrl) {
121
+ let response;
251
122
  try {
252
- raw = await readFile(OPENCLAW_CONFIG_PATH, "utf8");
253
- } catch (e) {
254
- if (isErrnoException(e) && e.code === "ENOENT") {
255
- return "not-found";
123
+ response = await fetch(`${baseUrl}/api/v1/agents`, {
124
+ headers: { "X-Multicorn-Key": apiKey },
125
+ signal: AbortSignal.timeout(8e3)
126
+ });
127
+ } catch {
128
+ return null;
129
+ }
130
+ if (!response.ok) {
131
+ if (response.status === 401 || response.status === 403) {
132
+ return { id: "", name: agentName, scopes: [], authInvalid: true };
256
133
  }
257
- throw e;
134
+ return null;
258
135
  }
259
- let obj;
136
+ let body;
260
137
  try {
261
- obj = JSON.parse(raw);
138
+ body = await response.json();
262
139
  } catch {
263
- return "parse-error";
264
- }
265
- let hooks = obj["hooks"];
266
- if (hooks === void 0 || typeof hooks !== "object") {
267
- hooks = {};
268
- obj["hooks"] = hooks;
269
- }
270
- let internal = hooks["internal"];
271
- if (internal === void 0 || typeof internal !== "object") {
272
- internal = { enabled: true, entries: {} };
273
- hooks["internal"] = internal;
274
- }
275
- let entries = internal["entries"];
276
- if (entries === void 0 || typeof entries !== "object") {
277
- entries = {};
278
- internal["entries"] = entries;
279
- }
280
- let shield = entries["multicorn-shield"];
281
- if (shield === void 0 || typeof shield !== "object") {
282
- shield = { enabled: true, env: {} };
283
- entries["multicorn-shield"] = shield;
284
- }
285
- let env = shield["env"];
286
- if (env === void 0 || typeof env !== "object") {
287
- env = {};
288
- shield["env"] = env;
289
- }
290
- env["MULTICORN_API_KEY"] = apiKey;
291
- env["MULTICORN_BASE_URL"] = baseUrl;
292
- if (agentName !== void 0) {
293
- env["MULTICORN_AGENT_NAME"] = agentName;
294
- const agentsList = obj["agents"];
295
- const list = agentsList?.["list"];
296
- if (Array.isArray(list) && list.length > 0) {
297
- const first = list[0];
298
- if (first["id"] !== agentName) {
299
- first["id"] = agentName;
300
- first["name"] = agentName;
301
- }
302
- } else {
303
- if (agentsList !== void 0 && typeof agentsList === "object") {
304
- agentsList["list"] = [{ id: agentName, name: agentName }];
305
- } else {
306
- obj["agents"] = { list: [{ id: agentName, name: agentName }] };
307
- }
308
- }
140
+ return null;
309
141
  }
310
- await writeFile(OPENCLAW_CONFIG_PATH, JSON.stringify(obj, null, 2) + "\n", {
311
- encoding: "utf8"
312
- });
313
- return "updated";
142
+ if (!isApiSuccessResponse(body)) return null;
143
+ const agents = body.data;
144
+ if (!Array.isArray(agents)) return null;
145
+ const match = agents.find(
146
+ (a) => isAgentSummaryShape(a) && a.name === agentName
147
+ );
148
+ if (match === void 0) return null;
149
+ return { id: match.id, name: match.name, scopes: [] };
314
150
  }
315
- async function validateApiKey(apiKey, baseUrl) {
151
+ async function fetchRemoteAgentsSummaries(apiKey, baseUrl) {
152
+ let response;
316
153
  try {
317
- const response = await fetch(`${baseUrl}/api/v1/agents`, {
154
+ response = await fetch(`${baseUrl}/api/v1/agents`, {
318
155
  headers: { "X-Multicorn-Key": apiKey },
319
156
  signal: AbortSignal.timeout(8e3)
320
157
  });
321
- if (response.status === 401) {
322
- return { valid: false, error: "API key not recognised. Check the key and try again." };
323
- }
324
- if (!response.ok) {
325
- return {
326
- valid: false,
327
- error: `Service returned ${String(response.status)}. Check your base URL and try again.`
328
- };
329
- }
330
- return { valid: true };
331
- } catch (error) {
332
- const detail = error instanceof Error ? error.message : String(error);
333
- return {
334
- valid: false,
335
- error: `Could not reach ${baseUrl}. Check your network connection. (${detail})`
336
- };
337
- }
338
- }
339
- async function isOpenClawConnected() {
340
- try {
341
- const raw = await readFile(OPENCLAW_CONFIG_PATH, "utf8");
342
- const obj = JSON.parse(raw);
343
- const hooks = obj["hooks"];
344
- const internal = hooks?.["internal"];
345
- const entries = internal?.["entries"];
346
- const shield = entries?.["multicorn-shield"];
347
- const env = shield?.["env"];
348
- const key = env?.["MULTICORN_API_KEY"];
349
- return typeof key === "string" && key.length > 0;
350
158
  } catch {
351
- return false;
159
+ return [];
352
160
  }
353
- }
354
- function isClaudeCodeConnected() {
161
+ if (!response.ok) return [];
162
+ let body;
355
163
  try {
356
- return existsSync(join(homedir(), ".claude", "plugins", "cache", "multicorn-shield"));
164
+ body = await response.json();
357
165
  } catch {
358
- return false;
166
+ return [];
359
167
  }
168
+ if (!isApiSuccessResponse(body)) return [];
169
+ const agents = body.data;
170
+ if (!Array.isArray(agents)) return [];
171
+ const out = [];
172
+ for (const a of agents) {
173
+ if (typeof a !== "object" || a === null) continue;
174
+ const o = a;
175
+ const rawName = o["name"];
176
+ if (typeof rawName !== "string" || rawName.length === 0) continue;
177
+ const plat = o["platform"];
178
+ const platform = plat === null || plat === void 0 ? null : typeof plat === "string" ? plat : null;
179
+ out.push({ name: rawName, platform });
180
+ }
181
+ return out;
360
182
  }
361
- function getCursorConfigPath() {
362
- return join(homedir(), ".cursor", "mcp.json");
363
- }
364
- async function isCursorConnected() {
365
- try {
366
- const raw = await readFile(getCursorConfigPath(), "utf8");
367
- const obj = JSON.parse(raw);
368
- const mcpServers = obj["mcpServers"];
369
- if (mcpServers === void 0 || typeof mcpServers !== "object") return false;
370
- for (const entry of Object.values(mcpServers)) {
371
- if (typeof entry !== "object" || entry === null) continue;
372
- const rec = entry;
373
- const url = rec["url"];
374
- if (typeof url === "string" && url.includes("multicorn")) return true;
375
- const args = rec["args"];
376
- if (Array.isArray(args) && (args.includes("multicorn-shield") || args.includes("multicorn-proxy")))
377
- return true;
183
+ async function registerAgent(agentName, apiKey, baseUrl, platform) {
184
+ const response = await fetch(`${baseUrl}/api/v1/agents`, {
185
+ method: "POST",
186
+ headers: {
187
+ "Content-Type": "application/json",
188
+ "X-Multicorn-Key": apiKey
189
+ },
190
+ body: JSON.stringify({ name: agentName, ...platform ? { platform } : {} }),
191
+ signal: AbortSignal.timeout(8e3)
192
+ });
193
+ if (!response.ok) {
194
+ if (response.status === 401 || response.status === 403) {
195
+ throw new ShieldAuthError(
196
+ `Failed to register agent "${agentName}": service returned ${String(response.status)}.`
197
+ );
378
198
  }
379
- return false;
380
- } catch (err) {
381
- process.stderr.write(
382
- `Warning: could not check Cursor connection status: ${err instanceof Error ? err.message : String(err)}
383
- `
199
+ throw new Error(
200
+ `Failed to register agent "${agentName}": service returned ${String(response.status)}.`
384
201
  );
385
- return false;
386
202
  }
203
+ const body = await response.json();
204
+ if (!isApiSuccessResponse(body)) {
205
+ throw new Error(`Failed to register agent "${agentName}": unexpected response format.`);
206
+ }
207
+ if (!isAgentSummaryShape(body.data)) {
208
+ throw new Error(`Failed to register agent "${agentName}": response missing agent ID.`);
209
+ }
210
+ return body.data.id;
387
211
  }
388
- function getWindsurfConfigPath() {
389
- return join(homedir(), ".codeium", "windsurf", "mcp_config.json");
390
- }
391
- async function isWindsurfConnected() {
212
+ async function fetchGrantedScopes(agentId, apiKey, baseUrl) {
213
+ let response;
392
214
  try {
393
- const raw = await readFile(getWindsurfConfigPath(), "utf8");
394
- const obj = JSON.parse(raw);
395
- const mcpServers = obj["mcpServers"];
396
- if (mcpServers === void 0 || typeof mcpServers !== "object") return false;
397
- for (const entry of Object.values(mcpServers)) {
398
- if (typeof entry !== "object" || entry === null) continue;
399
- const rec = entry;
400
- const url = rec["serverUrl"];
401
- if (typeof url === "string" && url.includes("multicorn")) return true;
402
- }
403
- return false;
215
+ response = await fetch(`${baseUrl}/api/v1/agents/${agentId}`, {
216
+ headers: { "X-Multicorn-Key": apiKey },
217
+ signal: AbortSignal.timeout(8e3)
218
+ });
404
219
  } catch {
405
- return false;
220
+ return [];
221
+ }
222
+ if (!response.ok) return [];
223
+ const body = await response.json();
224
+ if (!isApiSuccessResponse(body)) return [];
225
+ const agentDetail = body.data;
226
+ if (!isAgentDetailShape(agentDetail)) return [];
227
+ const scopes = [];
228
+ for (const perm of agentDetail.permissions) {
229
+ if (!isPermissionShape(perm)) continue;
230
+ if (perm.revoked_at !== null) continue;
231
+ if (perm.read) scopes.push({ service: perm.service, permissionLevel: "read" });
232
+ if (perm.write) scopes.push({ service: perm.service, permissionLevel: "write" });
233
+ if (perm.execute) scopes.push({ service: perm.service, permissionLevel: "execute" });
406
234
  }
235
+ return scopes;
407
236
  }
408
- function multicornShieldPackageRoot() {
409
- return join(dirname(fileURLToPath(import.meta.url)), "..");
410
- }
411
- function getWindsurfHooksInstallDir() {
412
- return join(homedir(), ".multicorn", "windsurf-hooks");
413
- }
414
- function getWindsurfCascadeHooksJsonPath() {
415
- return join(homedir(), ".codeium", "windsurf", "hooks.json");
416
- }
417
- function isShieldWindsurfHookCommand(cmd) {
418
- return cmd.includes("windsurf-hooks/pre-action.cjs") || cmd.includes("windsurf-hooks\\pre-action.cjs") || cmd.includes("windsurf-hooks/post-action.cjs") || cmd.includes("windsurf-hooks\\post-action.cjs");
237
+ function openBrowser(url) {
238
+ if (process.env["NODE_ENV"] === "test" || process.env["VITEST"] === "true") {
239
+ return;
240
+ }
241
+ const platform = process.platform;
242
+ const cmd = platform === "darwin" ? "open" : platform === "win32" ? "start" : "xdg-open";
243
+ spawn(cmd, [url], { detached: true, stdio: "ignore" }).unref();
419
244
  }
420
- function filterOutShieldWindsurfHooks(entries) {
421
- if (!Array.isArray(entries)) return [];
422
- const out = [];
423
- for (const e of entries) {
424
- if (typeof e !== "object" || e === null) continue;
425
- const rec = e;
426
- const cmd = rec["command"];
427
- if (typeof cmd !== "string" || isShieldWindsurfHookCommand(cmd)) continue;
428
- const powershell = rec["powershell"];
429
- const show_output = rec["show_output"];
430
- out.push({
431
- command: cmd,
432
- ...typeof powershell === "string" ? { powershell } : {},
433
- ...show_output === true ? { show_output: true } : {}
434
- });
245
+ async function waitForConsent(agentId, agentName, apiKey, baseUrl, dashboardUrl, logger, scope, platform) {
246
+ const scopeStrings = scope ? [`${scope.service}:${scope.permissionLevel}`] : detectScopeHints();
247
+ const consentUrl = buildConsentUrl(agentName, scopeStrings, dashboardUrl, platform);
248
+ logger.info("Opening consent page in your browser.", { url: consentUrl });
249
+ process.stderr.write(
250
+ `
251
+ Action requires permission. Opening consent page...
252
+ ${consentUrl}
253
+
254
+ Waiting for you to grant access in the Multicorn dashboard...
255
+ `
256
+ );
257
+ openBrowser(consentUrl);
258
+ const deadline = Date.now() + CONSENT_POLL_TIMEOUT_MS;
259
+ while (Date.now() < deadline) {
260
+ await sleep(CONSENT_POLL_INTERVAL_MS);
261
+ const scopes = await fetchGrantedScopes(agentId, apiKey, baseUrl);
262
+ if (scopes.length > 0) {
263
+ logger.info("Permissions granted.", { agent: agentName, scopeCount: scopes.length });
264
+ return scopes;
265
+ }
435
266
  }
436
- return out;
267
+ throw new Error(
268
+ `Consent not granted within ${String(CONSENT_POLL_TIMEOUT_MS / 6e4)} minutes. Grant access at ${dashboardUrl} and restart the proxy.`
269
+ );
437
270
  }
438
- async function installWindsurfNativeHooks() {
439
- const root = multicornShieldPackageRoot();
440
- const srcPre = join(root, "plugins", "windsurf", "hooks", "scripts", "pre-action.cjs");
441
- const srcPost = join(root, "plugins", "windsurf", "hooks", "scripts", "post-action.cjs");
442
- if (!existsSync(srcPre) || !existsSync(srcPost)) {
443
- throw new Error(
444
- `Could not find Shield Windsurf hook scripts at ${srcPre}. If you use npm, install the latest multicorn-shield package.`
445
- );
271
+ async function resolveAgentRecord(agentName, apiKey, baseUrl, logger, platform) {
272
+ const cachedScopes = await loadCachedScopes(agentName, apiKey);
273
+ if (cachedScopes !== null && cachedScopes.length > 0) {
274
+ logger.debug("Loaded scopes from cache.", { agent: agentName, count: cachedScopes.length });
446
275
  }
447
- const windsurfConfigDir = join(homedir(), ".codeium", "windsurf");
448
- if (!isExistingDirectory(windsurfConfigDir)) {
449
- process.stderr.write(
450
- style.yellow("\u26A0") + " Windsurf does not appear to be installed (~/.codeium/windsurf/ not found).\n\n"
451
- );
452
- process.stderr.write(
453
- "Open Windsurf at least once so this folder exists, or install from:\n " + style.cyan("https://windsurf.com/download") + "\n\n"
454
- );
455
- process.stderr.write("Then run this wizard again:\n");
456
- process.stderr.write(" " + style.cyan("npx multicorn-shield init") + "\n");
457
- throw new NativePluginPrerequisiteMissingError();
276
+ let agent = await findAgentByName(agentName, apiKey, baseUrl);
277
+ if (agent?.authInvalid) {
278
+ return agent;
458
279
  }
459
- const installDir = getWindsurfHooksInstallDir();
460
- await mkdir(installDir, { recursive: true });
461
- const destPre = join(installDir, "pre-action.cjs");
462
- const destPost = join(installDir, "post-action.cjs");
463
- await copyFile(srcPre, destPre);
464
- await copyFile(srcPost, destPost);
465
- const preCmd = `node ${JSON.stringify(destPre)}`;
466
- const postCmd = `node ${JSON.stringify(destPost)}`;
467
- const preEntry = { command: preCmd, powershell: preCmd, show_output: true };
468
- const postEntry = { command: postCmd, powershell: postCmd };
469
- const hooksPath = getWindsurfCascadeHooksJsonPath();
470
- let base = { hooks: {} };
471
- try {
472
- const raw = await readFile(hooksPath, "utf8");
473
- base = JSON.parse(raw);
474
- } catch (err) {
475
- if (!isErrnoException(err) || err.code !== "ENOENT") {
476
- throw err;
280
+ if (agent === null) {
281
+ try {
282
+ logger.info("Agent not found. Registering.", { agent: agentName });
283
+ const id = await registerAgent(agentName, apiKey, baseUrl, platform);
284
+ agent = { id, name: agentName, scopes: [] };
285
+ logger.info("Agent registered.", { agent: agentName, id });
286
+ } catch (error) {
287
+ if (error instanceof ShieldAuthError) {
288
+ return { id: "", name: agentName, scopes: [], authInvalid: true };
289
+ }
290
+ const detail = error instanceof Error ? error.message : String(error);
291
+ if (cachedScopes !== null && cachedScopes.length > 0) {
292
+ logger.warn("Service unreachable. Using cached scopes.", { error: detail });
293
+ return { id: "", name: agentName, scopes: cachedScopes };
294
+ }
295
+ logger.warn("Could not reach Multicorn service. Running with empty permissions.", {
296
+ error: detail
297
+ });
298
+ return { id: "", name: agentName, scopes: [] };
477
299
  }
478
300
  }
479
- const hooks = base["hooks"] ?? {};
480
- const preKeys = [
481
- "pre_read_code",
482
- "pre_write_code",
483
- "pre_run_command",
484
- "pre_mcp_tool_use"
485
- ];
486
- const postKeys = [
487
- "post_read_code",
488
- "post_write_code",
489
- "post_run_command",
490
- "post_mcp_tool_use"
491
- ];
492
- const nextHooks = { ...hooks };
493
- for (const k of preKeys) {
494
- const merged = filterOutShieldWindsurfHooks(nextHooks[k]);
495
- nextHooks[k] = [...merged, preEntry];
496
- }
497
- for (const k of postKeys) {
498
- const merged = filterOutShieldWindsurfHooks(nextHooks[k]);
499
- nextHooks[k] = [...merged, postEntry];
301
+ const scopes = await fetchGrantedScopes(agent.id, apiKey, baseUrl);
302
+ if (scopes.length > 0) {
303
+ await saveCachedScopes(agentName, agent.id, scopes, apiKey);
500
304
  }
501
- base["hooks"] = nextHooks;
502
- const hooksDir = dirname(hooksPath);
503
- await mkdir(hooksDir, { recursive: true });
504
- await writeFile(hooksPath, JSON.stringify(base, null, 2) + "\n", { encoding: "utf8" });
505
- }
506
- function getClineHooksInstallDir() {
507
- return join(homedir(), ".multicorn", "cline-hooks");
508
- }
509
- function getClineGlobalHooksDir() {
510
- return join(homedir(), "Documents", "Cline", "Hooks");
305
+ return { ...agent, scopes };
511
306
  }
512
- async function installClineNativeHooks() {
513
- const root = multicornShieldPackageRoot();
514
- const srcPre = join(root, "plugins", "cline", "hooks", "scripts", "pre-tool-use.cjs");
515
- const srcPost = join(root, "plugins", "cline", "hooks", "scripts", "post-tool-use.cjs");
516
- const srcShared = join(root, "plugins", "cline", "hooks", "scripts", "shared.cjs");
517
- if (!existsSync(srcPre) || !existsSync(srcPost) || !existsSync(srcShared)) {
518
- throw new Error(
519
- `Could not find Shield Cline hook scripts at ${srcPre}. If you use npm, install the latest multicorn-shield package.`
520
- );
307
+ function buildConsentUrl(agentName, scopes, dashboardUrl, platform) {
308
+ const base = dashboardUrl.replace(/\/+$/, "");
309
+ const params = new URLSearchParams({ agent: agentName });
310
+ if (scopes.length > 0) {
311
+ params.set("scopes", scopes.join(","));
521
312
  }
522
- const clineDocsDir = join(homedir(), "Documents", "Cline");
523
- if (!isExistingDirectory(clineDocsDir)) {
524
- process.stderr.write(
525
- style.yellow("\u26A0") + " Cline does not appear to be installed (~/Documents/Cline/ not found).\n\n"
526
- );
527
- process.stderr.write("Install the Cline VS Code extension first. See:\n");
528
- process.stderr.write(
529
- " " + style.cyan("https://docs.cline.bot/getting-started/installing-cline") + "\n\n"
530
- );
531
- process.stderr.write("Then run this wizard again:\n");
532
- process.stderr.write(" " + style.cyan("npx multicorn-shield init") + "\n");
533
- throw new NativePluginPrerequisiteMissingError();
313
+ if (platform) {
314
+ params.set("platform", platform);
534
315
  }
535
- const installDir = getClineHooksInstallDir();
536
- await mkdir(installDir, { recursive: true });
537
- const destPre = join(installDir, "pre-tool-use.cjs");
538
- const destPost = join(installDir, "post-tool-use.cjs");
539
- const destShared = join(installDir, "shared.cjs");
540
- await copyFile(srcPre, destPre);
541
- await copyFile(srcPost, destPost);
542
- await copyFile(srcShared, destShared);
543
- const hookScriptMode = 493;
544
- await chmod(destPre, hookScriptMode);
545
- await chmod(destPost, hookScriptMode);
546
- await chmod(destShared, hookScriptMode);
547
- const hooksDir = getClineGlobalHooksDir();
548
- await mkdir(hooksDir, { recursive: true });
549
- const preWrapper = join(hooksDir, "PreToolUse");
550
- const postWrapper = join(hooksDir, "PostToolUse");
551
- const preContent = `#!/usr/bin/env node
552
- require(${JSON.stringify(destPre)});
553
- `;
554
- const postContent = `#!/usr/bin/env node
555
- require(${JSON.stringify(destPost)});
556
- `;
557
- await writeFile(preWrapper, preContent, { encoding: "utf8", mode: 493 });
558
- await writeFile(postWrapper, postContent, { encoding: "utf8", mode: 493 });
316
+ return `${base}/consent?${params.toString()}`;
559
317
  }
560
- async function promptClineIntegrationMode(ask) {
561
- process.stderr.write("\n" + style.bold("Cline integration") + "\n");
562
- process.stderr.write(
563
- " " + style.violet("1") + ". Native plugin (recommended) - Cline Hooks see every file, terminal, browser, and MCP action\n"
564
- );
565
- process.stderr.write(
566
- " " + style.violet("2") + ". Hosted proxy - govern MCP traffic only (paste proxy URL into Cline MCP settings)\n"
567
- );
568
- let choice = 0;
569
- while (choice === 0) {
570
- const input = await ask("Choose integration (1-2): ");
571
- const num = parseInt(input.trim(), 10);
572
- if (num === 1) choice = 1;
573
- if (num === 2) choice = 2;
574
- }
575
- return choice === 1 ? "native" : "hosted";
318
+ function detectScopeHints() {
319
+ return [];
576
320
  }
577
- function getGeminiCliHooksInstallDir() {
578
- return join(homedir(), ".multicorn", "gemini-cli-hooks");
321
+ function sleep(ms) {
322
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
579
323
  }
580
- function getGeminiCliSettingsPath() {
581
- return join(homedir(), ".gemini", "settings.json");
324
+ function isApiSuccessResponse(value) {
325
+ if (typeof value !== "object" || value === null) return false;
326
+ const obj = value;
327
+ return obj["success"] === true;
582
328
  }
583
- function geminiInnerHooksReferenceShield(inner, multicornName) {
584
- if (!Array.isArray(inner)) return false;
585
- for (const h of inner) {
586
- if (typeof h !== "object" || h === null) continue;
587
- const rec = h;
588
- if (rec["name"] === multicornName) return true;
589
- const cmd = rec["command"];
590
- if (typeof cmd === "string" && cmd.includes("gemini-cli-hooks")) return true;
591
- }
592
- return false;
329
+ function isAgentSummaryShape(value) {
330
+ if (typeof value !== "object" || value === null) return false;
331
+ const obj = value;
332
+ return typeof obj["id"] === "string" && typeof obj["name"] === "string";
593
333
  }
594
- function geminiHookEventsReferenceShield(arr) {
595
- if (!Array.isArray(arr)) return false;
596
- for (const entry of arr) {
597
- if (typeof entry !== "object" || entry === null) continue;
598
- const hooks = entry["hooks"];
599
- if (geminiInnerHooksReferenceShield(hooks, "multicorn-shield") || geminiInnerHooksReferenceShield(hooks, "multicorn-shield-log")) {
600
- return true;
601
- }
602
- }
603
- return false;
604
- }
605
- function geminiSettingsHasMulticornHooks(hooks) {
606
- if (hooks === null || typeof hooks !== "object" || Array.isArray(hooks)) return false;
607
- const h = hooks;
608
- return geminiHookEventsReferenceShield(h["BeforeTool"]) || geminiHookEventsReferenceShield(h["AfterTool"]);
334
+ function isAgentDetailShape(value) {
335
+ if (typeof value !== "object" || value === null) return false;
336
+ const obj = value;
337
+ return Array.isArray(obj["permissions"]);
609
338
  }
610
- function geminiFilterInnerHooks(inner) {
611
- if (!Array.isArray(inner)) return [];
612
- return inner.filter((h) => {
613
- if (typeof h !== "object" || h === null) return true;
614
- const rec = h;
615
- if (rec["name"] === "multicorn-shield" || rec["name"] === "multicorn-shield-log") return false;
616
- const cmd = rec["command"];
617
- if (typeof cmd === "string" && cmd.includes("gemini-cli-hooks")) return false;
618
- return true;
619
- });
339
+ function isPermissionShape(value) {
340
+ if (typeof value !== "object" || value === null) return false;
341
+ const obj = value;
342
+ return typeof obj["service"] === "string" && typeof obj["read"] === "boolean" && typeof obj["write"] === "boolean" && typeof obj["execute"] === "boolean" && (obj["revoked_at"] === null || obj["revoked_at"] === void 0 || typeof obj["revoked_at"] === "string");
620
343
  }
621
- function geminiStripMatcherGroups(arr) {
622
- if (!Array.isArray(arr)) return [];
623
- const out = [];
624
- for (const entry of arr) {
625
- if (typeof entry !== "object" || entry === null) continue;
626
- const e = entry;
627
- const filtered = geminiFilterInnerHooks(e["hooks"]);
628
- if (filtered.length > 0) {
629
- out.push({ ...e, hooks: filtered });
344
+
345
+ // src/proxy/config.ts
346
+ var style = {
347
+ violet: (s) => `\x1B[38;2;124;58;237m${s}\x1B[0m`,
348
+ violetLight: (s) => `\x1B[38;2;167;139;250m${s}\x1B[0m`,
349
+ green: (s) => `\x1B[38;2;34;197;94m${s}\x1B[0m`,
350
+ yellow: (s) => `\x1B[38;2;245;158;11m${s}\x1B[0m`,
351
+ red: (s) => `\x1B[38;2;239;68;68m${s}\x1B[0m`,
352
+ cyan: (s) => `\x1B[38;2;6;182;212m${s}\x1B[0m`,
353
+ bold: (s) => `\x1B[1m${s}\x1B[0m`,
354
+ dim: (s) => `\x1B[2m${s}\x1B[0m`
355
+ };
356
+ var BANNER = [
357
+ " \u2588\u2588\u2588 \u2588 \u2588 \u2588 \u2588\u2588\u2588 \u2588 \u2588\u2588\u2584 ",
358
+ " \u2588 \u2588 \u2588 \u2588 \u2588 \u2588 \u2588 \u2588",
359
+ " \u2588\u2588\u2588 \u2588\u2588\u2588\u2588 \u2588 \u2588\u2588 \u2588 \u2588 \u2588",
360
+ " \u2588 \u2588 \u2588 \u2588 \u2588 \u2588 \u2588 \u2588",
361
+ " \u2588\u2588\u2588 \u2588 \u2588 \u2588 \u2588\u2588\u2588 \u2588\u2588\u2588 \u2588\u2588\u2580 "
362
+ ].map((line) => style.violet(line)).join("\n");
363
+ function withSpinner(message) {
364
+ const frames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
365
+ let i = 0;
366
+ const interval = setInterval(() => {
367
+ const frame = frames[i % frames.length];
368
+ process.stderr.write(`\r${style.violet(frame ?? "\u280B")} ${message}`);
369
+ i++;
370
+ }, 80);
371
+ return {
372
+ stop(success, result) {
373
+ clearInterval(interval);
374
+ const icon = success ? style.green("\u2713") : style.red("\u2717");
375
+ process.stderr.write(`\r\x1B[2K${icon} ${result}
376
+ `);
630
377
  }
378
+ };
379
+ }
380
+ var NativePluginPrerequisiteMissingError = class extends Error {
381
+ constructor() {
382
+ super("Native plugin prerequisites not met");
383
+ this.name = "NativePluginPrerequisiteMissingError";
384
+ }
385
+ };
386
+ function isExistingDirectory(path) {
387
+ try {
388
+ if (!existsSync(path)) return false;
389
+ return statSync(path).isDirectory();
390
+ } catch {
391
+ return false;
631
392
  }
632
- return out;
633
393
  }
634
- function geminiStripMulticornHookEntries(hooks) {
635
- const out = { ...hooks };
636
- out["BeforeTool"] = geminiStripMatcherGroups(out["BeforeTool"]);
637
- out["AfterTool"] = geminiStripMatcherGroups(out["AfterTool"]);
638
- return out;
394
+ function nativePluginSkippedSaveNote(wizardCommand, productName) {
395
+ 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";
639
396
  }
640
- async function installGeminiCliNativeHooks(ask) {
641
- const root = multicornShieldPackageRoot();
642
- const srcBefore = join(root, "plugins", "gemini-cli", "hooks", "scripts", "before-tool.cjs");
643
- const srcAfter = join(root, "plugins", "gemini-cli", "hooks", "scripts", "after-tool.cjs");
644
- const srcShared = join(root, "plugins", "gemini-cli", "hooks", "scripts", "shared.cjs");
645
- if (!existsSync(srcBefore) || !existsSync(srcAfter) || !existsSync(srcShared)) {
646
- throw new Error(
647
- `Could not find Shield Gemini CLI hook scripts at ${srcBefore}. If you use npm, install the latest multicorn-shield package.`
648
- );
397
+ var CONFIG_DIR = join(homedir(), ".multicorn");
398
+ var CONFIG_PATH = join(CONFIG_DIR, "config.json");
399
+ var OPENCLAW_CONFIG_PATH = join(homedir(), ".openclaw", "openclaw.json");
400
+ var ANSI_PATTERN = new RegExp(String.fromCharCode(27) + "\\[[0-9;]*[a-zA-Z]", "g");
401
+ function stripAnsi(str) {
402
+ return str.replace(ANSI_PATTERN, "");
403
+ }
404
+ function normalizeAgentName(raw) {
405
+ return raw.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "").replace(/-{2,}/g, "-").replace(/^-+|-+$/g, "").slice(0, 50);
406
+ }
407
+ function isErrnoException(e) {
408
+ return typeof e === "object" && e !== null && "code" in e;
409
+ }
410
+ function isProxyConfig(value) {
411
+ if (typeof value !== "object" || value === null) return false;
412
+ const obj = value;
413
+ return typeof obj["apiKey"] === "string" && typeof obj["baseUrl"] === "string";
414
+ }
415
+ function isAgentEntry(value) {
416
+ if (typeof value !== "object" || value === null) return false;
417
+ const o = value;
418
+ if (typeof o["name"] !== "string" || typeof o["platform"] !== "string") return false;
419
+ if (o["workspacePath"] !== void 0 && typeof o["workspacePath"] !== "string") return false;
420
+ return true;
421
+ }
422
+ function cwdUnderWorkspacePath(cwdResolved, workspacePath) {
423
+ const w = resolve(workspacePath);
424
+ if (cwdResolved === w) return true;
425
+ const prefix = w.endsWith(sep) ? w : w + sep;
426
+ return cwdResolved.startsWith(prefix);
427
+ }
428
+ function getAgentByPlatform(config, platform, cwd) {
429
+ const list = config.agents;
430
+ if (list === void 0 || list.length === 0) return void 0;
431
+ const matches = list.filter((a) => a.platform === platform);
432
+ if (matches.length === 0) return void 0;
433
+ if (cwd === void 0 || cwd.length === 0) return matches[0];
434
+ const resolvedCwd = resolve(cwd);
435
+ const withPath = matches.filter(
436
+ (a) => typeof a.workspacePath === "string" && a.workspacePath.length > 0
437
+ );
438
+ if (withPath.length === 0) return matches[0];
439
+ let best;
440
+ let bestLen = -1;
441
+ for (const a of withPath) {
442
+ const ws = a.workspacePath;
443
+ if (typeof ws !== "string" || ws.length === 0) continue;
444
+ if (!cwdUnderWorkspacePath(resolvedCwd, ws)) continue;
445
+ const len = resolve(ws).length;
446
+ if (len > bestLen) {
447
+ bestLen = len;
448
+ best = a;
449
+ }
450
+ }
451
+ if (best !== void 0) return best;
452
+ return matches[0];
453
+ }
454
+ function getDefaultAgent(config) {
455
+ const list = config.agents;
456
+ if (list === void 0 || list.length === 0) return void 0;
457
+ const defName = config.defaultAgent;
458
+ if (typeof defName === "string" && defName.length > 0) {
459
+ const match = list.find((a) => a.name === defName);
460
+ if (match !== void 0) return match;
649
461
  }
650
- const geminiConfigDir = join(homedir(), ".gemini");
651
- if (!isExistingDirectory(geminiConfigDir)) {
652
- process.stderr.write(
653
- style.yellow("\u26A0") + " Gemini CLI does not appear to be installed (~/.gemini/ not found).\n\n"
654
- );
655
- process.stderr.write("Install Gemini CLI first:\n");
656
- process.stderr.write(" " + style.cyan("npm install -g @google/gemini-cli") + "\n\n");
657
- process.stderr.write("Then run this wizard again:\n");
658
- process.stderr.write(" " + style.cyan("npx multicorn-shield init") + "\n");
659
- throw new NativePluginPrerequisiteMissingError();
462
+ return list[0];
463
+ }
464
+ function collectAgentsFromConfig(cfg) {
465
+ if (cfg === null) return [];
466
+ if (cfg.agents !== void 0 && cfg.agents.length > 0) {
467
+ return cfg.agents.map((a) => {
468
+ const e = { name: a.name, platform: a.platform };
469
+ if (typeof a.workspacePath === "string" && a.workspacePath.length > 0) {
470
+ return { ...e, workspacePath: a.workspacePath };
471
+ }
472
+ return e;
473
+ });
660
474
  }
661
- const installDir = getGeminiCliHooksInstallDir();
662
- await mkdir(installDir, { recursive: true });
663
- const destBefore = join(installDir, "before-tool.cjs");
664
- const destAfter = join(installDir, "after-tool.cjs");
665
- const destShared = join(installDir, "shared.cjs");
666
- await copyFile(srcBefore, destBefore);
667
- await copyFile(srcAfter, destAfter);
668
- await copyFile(srcShared, destShared);
669
- const mode = 493;
670
- await chmod(destBefore, mode);
671
- await chmod(destAfter, mode);
672
- await chmod(destShared, mode);
673
- const settingsPath = getGeminiCliSettingsPath();
674
- let existing = {};
475
+ const raw = cfg;
476
+ const legacyName = raw["agentName"];
477
+ const legacyPlatform = raw["platform"];
478
+ if (typeof legacyName === "string" && legacyName.length > 0) {
479
+ const plat = typeof legacyPlatform === "string" && legacyPlatform.length > 0 ? legacyPlatform : "unknown";
480
+ return [{ name: legacyName, platform: plat }];
481
+ }
482
+ return [];
483
+ }
484
+ async function parseConfigFile() {
675
485
  try {
676
- const rawText = await readFile(settingsPath, "utf8");
677
- const parsed = JSON.parse(rawText);
678
- if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) {
679
- existing = parsed;
486
+ const raw = await readFile(CONFIG_PATH, "utf8");
487
+ try {
488
+ return { kind: "ok", value: JSON.parse(raw) };
489
+ } catch {
490
+ return { kind: "parseError" };
680
491
  }
681
- } catch (err) {
682
- if (isErrnoException(err) && err.code === "ENOENT") {
683
- existing = {};
684
- } else {
685
- process.stderr.write(
686
- style.yellow("\u26A0") + ` Could not parse ${settingsPath}. Create valid JSON or remove the file, then run init again.
687
- `
688
- );
689
- throw new Error(`Invalid Gemini CLI settings at ${settingsPath}`);
492
+ } catch (error) {
493
+ if (isErrnoException(error) && error.code === "ENOENT") {
494
+ return { kind: "missing" };
690
495
  }
496
+ const message = error instanceof Error ? error.message : String(error);
497
+ return { kind: "readError", message };
691
498
  }
692
- const hooksRaw = existing["hooks"];
693
- const hooksObj = typeof hooksRaw === "object" && hooksRaw !== null && !Array.isArray(hooksRaw) ? hooksRaw : {};
694
- if (geminiSettingsHasMulticornHooks(hooksObj)) {
695
- const answer = await ask(
696
- "Existing Multicorn Shield hooks were found in ~/.gemini/settings.json. Overwrite? (Y/n) "
499
+ }
500
+ function isAllowedShieldApiBaseUrl(url) {
501
+ return url.startsWith("https://") || url.startsWith("http://localhost") || url.startsWith("http://127.0.0.1");
502
+ }
503
+ async function loadConfig() {
504
+ const result = await parseConfigFile();
505
+ if (result.kind !== "ok") return null;
506
+ const parsed = result.value;
507
+ if (!isProxyConfig(parsed)) return null;
508
+ const obj = parsed;
509
+ const agentNameRaw = obj["agentName"];
510
+ const agentsRaw = obj["agents"];
511
+ const hasNonEmptyAgents = Array.isArray(agentsRaw) && agentsRaw.length > 0 && agentsRaw.every((e) => isAgentEntry(e));
512
+ const needsMigrate = typeof agentNameRaw === "string" && agentNameRaw.length > 0 && !hasNonEmptyAgents;
513
+ if (!needsMigrate) {
514
+ return parsed;
515
+ }
516
+ const platform = typeof obj["platform"] === "string" && obj["platform"].length > 0 ? obj["platform"] : "unknown";
517
+ const next = { ...obj };
518
+ delete next["agentName"];
519
+ delete next["platform"];
520
+ next["agents"] = [{ name: agentNameRaw, platform }];
521
+ next["defaultAgent"] = agentNameRaw;
522
+ const migrated = next;
523
+ await saveConfig(migrated);
524
+ return migrated;
525
+ }
526
+ async function readBaseUrlFromConfig() {
527
+ const result = await parseConfigFile();
528
+ if (result.kind === "missing") return void 0;
529
+ if (result.kind === "readError") {
530
+ process.stderr.write(
531
+ style.yellow(`Warning: could not read base URL from config file: ${result.message}`) + "\n"
697
532
  );
698
- if (answer.trim().toLowerCase() === "n") {
699
- throw new Error("Installation cancelled: existing Shield hooks left unchanged.");
700
- }
533
+ return void 0;
701
534
  }
702
- const cleaned = geminiStripMulticornHookEntries({ ...hooksObj });
703
- const beforeArr = Array.isArray(cleaned["BeforeTool"]) ? [...cleaned["BeforeTool"]] : [];
704
- const afterArr = Array.isArray(cleaned["AfterTool"]) ? [...cleaned["AfterTool"]] : [];
705
- const beforeCmd = `node ${destBefore}`;
706
- const afterCmd = `node ${destAfter}`;
707
- beforeArr.push({
708
- matcher: ".*",
709
- hooks: [
710
- {
711
- type: "command",
712
- name: "multicorn-shield",
713
- command: beforeCmd,
714
- timeout: 6e4
715
- }
716
- ]
717
- });
718
- afterArr.push({
719
- matcher: ".*",
535
+ if (result.kind === "parseError") {
536
+ process.stderr.write(
537
+ style.yellow("Warning: could not parse ~/.multicorn/config.json as JSON.") + "\n"
538
+ );
539
+ return void 0;
540
+ }
541
+ const parsed = result.value;
542
+ if (typeof parsed !== "object" || parsed === null) return void 0;
543
+ const u = parsed["baseUrl"];
544
+ if (typeof u !== "string" || u.length === 0) return void 0;
545
+ return u;
546
+ }
547
+ async function deleteAgentByName(name) {
548
+ const config = await loadConfig();
549
+ if (config === null) return false;
550
+ const agents = collectAgentsFromConfig(config);
551
+ const idx = agents.findIndex((a) => a.name === name);
552
+ if (idx === -1) return false;
553
+ const nextAgents = agents.filter((_, i) => i !== idx);
554
+ let defaultAgent = config.defaultAgent;
555
+ if (defaultAgent === name) {
556
+ defaultAgent = void 0;
557
+ }
558
+ const raw = { ...config };
559
+ if (nextAgents.length > 0) {
560
+ raw["agents"] = nextAgents;
561
+ } else {
562
+ delete raw["agents"];
563
+ }
564
+ if (defaultAgent !== void 0 && defaultAgent.length > 0) {
565
+ raw["defaultAgent"] = defaultAgent;
566
+ } else {
567
+ delete raw["defaultAgent"];
568
+ }
569
+ await saveConfig(raw);
570
+ return true;
571
+ }
572
+ async function saveConfig(config) {
573
+ await mkdir(CONFIG_DIR, { recursive: true, mode: 448 });
574
+ await writeFile(CONFIG_PATH, JSON.stringify(config, null, 2) + "\n", {
575
+ encoding: "utf8",
576
+ mode: 384
577
+ });
578
+ }
579
+ var OPENCLAW_MIN_VERSION = "2026.2.26";
580
+ async function detectOpenClaw() {
581
+ let raw;
582
+ try {
583
+ raw = await readFile(OPENCLAW_CONFIG_PATH, "utf8");
584
+ } catch (e) {
585
+ if (isErrnoException(e) && e.code === "ENOENT") {
586
+ return { status: "not-found", version: null };
587
+ }
588
+ throw e;
589
+ }
590
+ let obj;
591
+ try {
592
+ obj = JSON.parse(raw);
593
+ } catch {
594
+ return { status: "parse-error", version: null };
595
+ }
596
+ const meta = obj["meta"];
597
+ if (typeof meta === "object" && meta !== null) {
598
+ const v = meta["lastTouchedVersion"];
599
+ if (typeof v === "string" && v.length > 0) {
600
+ return { status: "detected", version: v };
601
+ }
602
+ }
603
+ return { status: "detected", version: null };
604
+ }
605
+ function isVersionAtLeast(version, minimum) {
606
+ const vParts = version.split(".").map(Number);
607
+ const mParts = minimum.split(".").map(Number);
608
+ const len = Math.max(vParts.length, mParts.length);
609
+ for (let i = 0; i < len; i++) {
610
+ const v = vParts[i] ?? 0;
611
+ const m = mParts[i] ?? 0;
612
+ if (Number.isNaN(v) || Number.isNaN(m)) return false;
613
+ if (v > m) return true;
614
+ if (v < m) return false;
615
+ }
616
+ return true;
617
+ }
618
+ async function updateOpenClawConfigIfPresent(apiKey, baseUrl, agentName) {
619
+ let raw;
620
+ try {
621
+ raw = await readFile(OPENCLAW_CONFIG_PATH, "utf8");
622
+ } catch (e) {
623
+ if (isErrnoException(e) && e.code === "ENOENT") {
624
+ return "not-found";
625
+ }
626
+ throw e;
627
+ }
628
+ let obj;
629
+ try {
630
+ obj = JSON.parse(raw);
631
+ } catch {
632
+ return "parse-error";
633
+ }
634
+ let hooks = obj["hooks"];
635
+ if (hooks === void 0 || typeof hooks !== "object") {
636
+ hooks = {};
637
+ obj["hooks"] = hooks;
638
+ }
639
+ let internal = hooks["internal"];
640
+ if (internal === void 0 || typeof internal !== "object") {
641
+ internal = { enabled: true, entries: {} };
642
+ hooks["internal"] = internal;
643
+ }
644
+ let entries = internal["entries"];
645
+ if (entries === void 0 || typeof entries !== "object") {
646
+ entries = {};
647
+ internal["entries"] = entries;
648
+ }
649
+ let shield = entries["multicorn-shield"];
650
+ if (shield === void 0 || typeof shield !== "object") {
651
+ shield = { enabled: true, env: {} };
652
+ entries["multicorn-shield"] = shield;
653
+ }
654
+ let env = shield["env"];
655
+ if (env === void 0 || typeof env !== "object") {
656
+ env = {};
657
+ shield["env"] = env;
658
+ }
659
+ env["MULTICORN_API_KEY"] = apiKey;
660
+ env["MULTICORN_BASE_URL"] = baseUrl;
661
+ if (agentName !== void 0) {
662
+ env["MULTICORN_AGENT_NAME"] = agentName;
663
+ const agentsList = obj["agents"];
664
+ const list = agentsList?.["list"];
665
+ if (Array.isArray(list) && list.length > 0) {
666
+ const first = list[0];
667
+ if (first["id"] !== agentName) {
668
+ first["id"] = agentName;
669
+ first["name"] = agentName;
670
+ }
671
+ } else {
672
+ if (agentsList !== void 0 && typeof agentsList === "object") {
673
+ agentsList["list"] = [{ id: agentName, name: agentName }];
674
+ } else {
675
+ obj["agents"] = { list: [{ id: agentName, name: agentName }] };
676
+ }
677
+ }
678
+ }
679
+ await writeFile(OPENCLAW_CONFIG_PATH, JSON.stringify(obj, null, 2) + "\n", {
680
+ encoding: "utf8"
681
+ });
682
+ return "updated";
683
+ }
684
+ async function validateApiKey(apiKey, baseUrl) {
685
+ try {
686
+ const response = await fetch(`${baseUrl}/api/v1/agents`, {
687
+ headers: { "X-Multicorn-Key": apiKey },
688
+ signal: AbortSignal.timeout(8e3)
689
+ });
690
+ if (response.status === 401) {
691
+ return { valid: false, error: "API key not recognised. Check the key and try again." };
692
+ }
693
+ if (!response.ok) {
694
+ return {
695
+ valid: false,
696
+ error: `Service returned ${String(response.status)}. Check your base URL and try again.`
697
+ };
698
+ }
699
+ return { valid: true };
700
+ } catch (error) {
701
+ const detail = error instanceof Error ? error.message : String(error);
702
+ return {
703
+ valid: false,
704
+ error: `Could not reach ${baseUrl}. Check your network connection. (${detail})`
705
+ };
706
+ }
707
+ }
708
+ async function isOpenClawConnected() {
709
+ try {
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
+ }
722
+ }
723
+ function isClaudeCodeConnected() {
724
+ try {
725
+ return existsSync(join(homedir(), ".claude", "plugins", "cache", "multicorn-shield"));
726
+ } catch {
727
+ return false;
728
+ }
729
+ }
730
+ function getCursorConfigPath() {
731
+ return join(homedir(), ".cursor", "mcp.json");
732
+ }
733
+ async function isCursorConnected() {
734
+ try {
735
+ const raw = await readFile(getCursorConfigPath(), "utf8");
736
+ const obj = JSON.parse(raw);
737
+ const mcpServers = obj["mcpServers"];
738
+ if (mcpServers === void 0 || typeof mcpServers !== "object") return false;
739
+ for (const entry of Object.values(mcpServers)) {
740
+ if (typeof entry !== "object" || entry === null) continue;
741
+ const rec = entry;
742
+ const url = rec["url"];
743
+ if (typeof url === "string" && url.includes("multicorn")) return true;
744
+ const args = rec["args"];
745
+ if (Array.isArray(args) && (args.includes("multicorn-shield") || args.includes("multicorn-proxy")))
746
+ return true;
747
+ }
748
+ return false;
749
+ } catch (err) {
750
+ process.stderr.write(
751
+ `Warning: could not check Cursor connection status: ${err instanceof Error ? err.message : String(err)}
752
+ `
753
+ );
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
+ } catch {
774
+ return false;
775
+ }
776
+ }
777
+ function multicornShieldPackageRoot() {
778
+ return join(dirname(fileURLToPath(import.meta.url)), "..");
779
+ }
780
+ function getWindsurfHooksInstallDir() {
781
+ return join(homedir(), ".multicorn", "windsurf-hooks");
782
+ }
783
+ function getWindsurfCascadeHooksJsonPath() {
784
+ return join(homedir(), ".codeium", "windsurf", "hooks.json");
785
+ }
786
+ function isShieldWindsurfHookCommand(cmd) {
787
+ return cmd.includes("windsurf-hooks/pre-action.cjs") || cmd.includes("windsurf-hooks\\pre-action.cjs") || cmd.includes("windsurf-hooks/post-action.cjs") || cmd.includes("windsurf-hooks\\post-action.cjs");
788
+ }
789
+ function filterOutShieldWindsurfHooks(entries) {
790
+ if (!Array.isArray(entries)) return [];
791
+ const out = [];
792
+ for (const e of entries) {
793
+ if (typeof e !== "object" || e === null) continue;
794
+ const rec = e;
795
+ const cmd = rec["command"];
796
+ if (typeof cmd !== "string" || isShieldWindsurfHookCommand(cmd)) continue;
797
+ const powershell = rec["powershell"];
798
+ const show_output = rec["show_output"];
799
+ out.push({
800
+ command: cmd,
801
+ ...typeof powershell === "string" ? { powershell } : {},
802
+ ...show_output === true ? { show_output: true } : {}
803
+ });
804
+ }
805
+ return out;
806
+ }
807
+ async function installWindsurfNativeHooks() {
808
+ const root = multicornShieldPackageRoot();
809
+ const srcPre = join(root, "plugins", "windsurf", "hooks", "scripts", "pre-action.cjs");
810
+ const srcPost = join(root, "plugins", "windsurf", "hooks", "scripts", "post-action.cjs");
811
+ if (!existsSync(srcPre) || !existsSync(srcPost)) {
812
+ throw new Error(
813
+ `Could not find Shield Windsurf hook scripts at ${srcPre}. If you use npm, install the latest multicorn-shield package.`
814
+ );
815
+ }
816
+ const windsurfConfigDir = join(homedir(), ".codeium", "windsurf");
817
+ if (!isExistingDirectory(windsurfConfigDir)) {
818
+ process.stderr.write(
819
+ style.yellow("\u26A0") + " Windsurf does not appear to be installed (~/.codeium/windsurf/ not found).\n\n"
820
+ );
821
+ process.stderr.write(
822
+ "Open Windsurf at least once so this folder exists, or install from:\n " + style.cyan("https://windsurf.com/download") + "\n\n"
823
+ );
824
+ process.stderr.write("Then run this wizard again:\n");
825
+ process.stderr.write(" " + style.cyan("npx multicorn-shield init") + "\n");
826
+ throw new NativePluginPrerequisiteMissingError();
827
+ }
828
+ const installDir = getWindsurfHooksInstallDir();
829
+ await mkdir(installDir, { recursive: true });
830
+ const destPre = join(installDir, "pre-action.cjs");
831
+ const destPost = join(installDir, "post-action.cjs");
832
+ await copyFile(srcPre, destPre);
833
+ await copyFile(srcPost, destPost);
834
+ const preCmd = `node ${JSON.stringify(destPre)}`;
835
+ const postCmd = `node ${JSON.stringify(destPost)}`;
836
+ const preEntry = { command: preCmd, powershell: preCmd, show_output: true };
837
+ const postEntry = { command: postCmd, powershell: postCmd };
838
+ const hooksPath = getWindsurfCascadeHooksJsonPath();
839
+ let base = { hooks: {} };
840
+ try {
841
+ const raw = await readFile(hooksPath, "utf8");
842
+ base = JSON.parse(raw);
843
+ } catch (err) {
844
+ if (!isErrnoException(err) || err.code !== "ENOENT") {
845
+ throw err;
846
+ }
847
+ }
848
+ const hooks = base["hooks"] ?? {};
849
+ const preKeys = [
850
+ "pre_read_code",
851
+ "pre_write_code",
852
+ "pre_run_command",
853
+ "pre_mcp_tool_use"
854
+ ];
855
+ const postKeys = [
856
+ "post_read_code",
857
+ "post_write_code",
858
+ "post_run_command",
859
+ "post_mcp_tool_use"
860
+ ];
861
+ const nextHooks = { ...hooks };
862
+ for (const k of preKeys) {
863
+ const merged = filterOutShieldWindsurfHooks(nextHooks[k]);
864
+ nextHooks[k] = [...merged, preEntry];
865
+ }
866
+ for (const k of postKeys) {
867
+ const merged = filterOutShieldWindsurfHooks(nextHooks[k]);
868
+ nextHooks[k] = [...merged, postEntry];
869
+ }
870
+ base["hooks"] = nextHooks;
871
+ const hooksDir = dirname(hooksPath);
872
+ await mkdir(hooksDir, { recursive: true });
873
+ await writeFile(hooksPath, JSON.stringify(base, null, 2) + "\n", { encoding: "utf8" });
874
+ }
875
+ function getClineHooksInstallDir() {
876
+ return join(homedir(), ".multicorn", "cline-hooks");
877
+ }
878
+ function getClineGlobalHooksDir() {
879
+ return join(homedir(), "Documents", "Cline", "Hooks");
880
+ }
881
+ async function installClineNativeHooks() {
882
+ const root = multicornShieldPackageRoot();
883
+ const srcPre = join(root, "plugins", "cline", "hooks", "scripts", "pre-tool-use.cjs");
884
+ const srcPost = join(root, "plugins", "cline", "hooks", "scripts", "post-tool-use.cjs");
885
+ const srcShared = join(root, "plugins", "cline", "hooks", "scripts", "shared.cjs");
886
+ if (!existsSync(srcPre) || !existsSync(srcPost) || !existsSync(srcShared)) {
887
+ throw new Error(
888
+ `Could not find Shield Cline hook scripts at ${srcPre}. If you use npm, install the latest multicorn-shield package.`
889
+ );
890
+ }
891
+ const clineDocsDir = join(homedir(), "Documents", "Cline");
892
+ if (!isExistingDirectory(clineDocsDir)) {
893
+ process.stderr.write(
894
+ style.yellow("\u26A0") + " Cline does not appear to be installed (~/Documents/Cline/ not found).\n\n"
895
+ );
896
+ process.stderr.write("Install the Cline VS Code extension first. See:\n");
897
+ process.stderr.write(
898
+ " " + style.cyan("https://docs.cline.bot/getting-started/installing-cline") + "\n\n"
899
+ );
900
+ process.stderr.write("Then run this wizard again:\n");
901
+ process.stderr.write(" " + style.cyan("npx multicorn-shield init") + "\n");
902
+ throw new NativePluginPrerequisiteMissingError();
903
+ }
904
+ const installDir = getClineHooksInstallDir();
905
+ await mkdir(installDir, { recursive: true });
906
+ const destPre = join(installDir, "pre-tool-use.cjs");
907
+ const destPost = join(installDir, "post-tool-use.cjs");
908
+ const destShared = join(installDir, "shared.cjs");
909
+ await copyFile(srcPre, destPre);
910
+ await copyFile(srcPost, destPost);
911
+ await copyFile(srcShared, destShared);
912
+ const hookScriptMode = 493;
913
+ await chmod(destPre, hookScriptMode);
914
+ await chmod(destPost, hookScriptMode);
915
+ await chmod(destShared, hookScriptMode);
916
+ const hooksDir = getClineGlobalHooksDir();
917
+ await mkdir(hooksDir, { recursive: true });
918
+ const preWrapper = join(hooksDir, "PreToolUse");
919
+ const postWrapper = join(hooksDir, "PostToolUse");
920
+ const preContent = `#!/usr/bin/env node
921
+ require(${JSON.stringify(destPre)});
922
+ `;
923
+ const postContent = `#!/usr/bin/env node
924
+ require(${JSON.stringify(destPost)});
925
+ `;
926
+ await writeFile(preWrapper, preContent, { encoding: "utf8", mode: 493 });
927
+ await writeFile(postWrapper, postContent, { encoding: "utf8", mode: 493 });
928
+ }
929
+ async function promptClineIntegrationMode(ask) {
930
+ process.stderr.write("\n" + style.bold("Cline integration") + "\n");
931
+ process.stderr.write(
932
+ " " + style.violet("1") + ". Native plugin (recommended) - Cline Hooks see every file, terminal, browser, and MCP action\n"
933
+ );
934
+ process.stderr.write(
935
+ " " + style.violet("2") + ". Hosted proxy - govern MCP traffic only (paste proxy URL into Cline MCP settings)\n"
936
+ );
937
+ let choice = 0;
938
+ while (choice === 0) {
939
+ const input = await ask("Choose integration (1-2): ");
940
+ const num = parseInt(input.trim(), 10);
941
+ if (num === 1) choice = 1;
942
+ if (num === 2) choice = 2;
943
+ }
944
+ return choice === 1 ? "native" : "hosted";
945
+ }
946
+ function getGeminiCliHooksInstallDir() {
947
+ return join(homedir(), ".multicorn", "gemini-cli-hooks");
948
+ }
949
+ function getGeminiCliSettingsPath() {
950
+ return join(homedir(), ".gemini", "settings.json");
951
+ }
952
+ function geminiInnerHooksReferenceShield(inner, multicornName) {
953
+ if (!Array.isArray(inner)) return false;
954
+ for (const h of inner) {
955
+ if (typeof h !== "object" || h === null) continue;
956
+ const rec = h;
957
+ if (rec["name"] === multicornName) return true;
958
+ const cmd = rec["command"];
959
+ if (typeof cmd === "string" && cmd.includes("gemini-cli-hooks")) return true;
960
+ }
961
+ return false;
962
+ }
963
+ function geminiHookEventsReferenceShield(arr) {
964
+ if (!Array.isArray(arr)) return false;
965
+ for (const entry of arr) {
966
+ if (typeof entry !== "object" || entry === null) continue;
967
+ const hooks = entry["hooks"];
968
+ if (geminiInnerHooksReferenceShield(hooks, "multicorn-shield") || geminiInnerHooksReferenceShield(hooks, "multicorn-shield-log")) {
969
+ return true;
970
+ }
971
+ }
972
+ return false;
973
+ }
974
+ function geminiSettingsHasMulticornHooks(hooks) {
975
+ if (hooks === null || typeof hooks !== "object" || Array.isArray(hooks)) return false;
976
+ const h = hooks;
977
+ return geminiHookEventsReferenceShield(h["BeforeTool"]) || geminiHookEventsReferenceShield(h["AfterTool"]);
978
+ }
979
+ function geminiFilterInnerHooks(inner) {
980
+ if (!Array.isArray(inner)) return [];
981
+ return inner.filter((h) => {
982
+ if (typeof h !== "object" || h === null) return true;
983
+ const rec = h;
984
+ if (rec["name"] === "multicorn-shield" || rec["name"] === "multicorn-shield-log") return false;
985
+ const cmd = rec["command"];
986
+ if (typeof cmd === "string" && cmd.includes("gemini-cli-hooks")) return false;
987
+ return true;
988
+ });
989
+ }
990
+ function geminiStripMatcherGroups(arr) {
991
+ if (!Array.isArray(arr)) return [];
992
+ const out = [];
993
+ for (const entry of arr) {
994
+ if (typeof entry !== "object" || entry === null) continue;
995
+ const e = entry;
996
+ const filtered = geminiFilterInnerHooks(e["hooks"]);
997
+ if (filtered.length > 0) {
998
+ out.push({ ...e, hooks: filtered });
999
+ }
1000
+ }
1001
+ return out;
1002
+ }
1003
+ function geminiStripMulticornHookEntries(hooks) {
1004
+ const out = { ...hooks };
1005
+ out["BeforeTool"] = geminiStripMatcherGroups(out["BeforeTool"]);
1006
+ out["AfterTool"] = geminiStripMatcherGroups(out["AfterTool"]);
1007
+ return out;
1008
+ }
1009
+ async function installGeminiCliNativeHooks(ask) {
1010
+ const root = multicornShieldPackageRoot();
1011
+ const srcBefore = join(root, "plugins", "gemini-cli", "hooks", "scripts", "before-tool.cjs");
1012
+ const srcAfter = join(root, "plugins", "gemini-cli", "hooks", "scripts", "after-tool.cjs");
1013
+ const srcShared = join(root, "plugins", "gemini-cli", "hooks", "scripts", "shared.cjs");
1014
+ if (!existsSync(srcBefore) || !existsSync(srcAfter) || !existsSync(srcShared)) {
1015
+ throw new Error(
1016
+ `Could not find Shield Gemini CLI hook scripts at ${srcBefore}. If you use npm, install the latest multicorn-shield package.`
1017
+ );
1018
+ }
1019
+ const geminiConfigDir = join(homedir(), ".gemini");
1020
+ if (!isExistingDirectory(geminiConfigDir)) {
1021
+ process.stderr.write(
1022
+ style.yellow("\u26A0") + " Gemini CLI does not appear to be installed (~/.gemini/ not found).\n\n"
1023
+ );
1024
+ process.stderr.write("Install Gemini CLI first:\n");
1025
+ process.stderr.write(" " + style.cyan("npm install -g @google/gemini-cli") + "\n\n");
1026
+ process.stderr.write("Then run this wizard again:\n");
1027
+ process.stderr.write(" " + style.cyan("npx multicorn-shield init") + "\n");
1028
+ throw new NativePluginPrerequisiteMissingError();
1029
+ }
1030
+ const installDir = getGeminiCliHooksInstallDir();
1031
+ await mkdir(installDir, { recursive: true });
1032
+ const destBefore = join(installDir, "before-tool.cjs");
1033
+ const destAfter = join(installDir, "after-tool.cjs");
1034
+ const destShared = join(installDir, "shared.cjs");
1035
+ await copyFile(srcBefore, destBefore);
1036
+ await copyFile(srcAfter, destAfter);
1037
+ await copyFile(srcShared, destShared);
1038
+ const mode = 493;
1039
+ await chmod(destBefore, mode);
1040
+ await chmod(destAfter, mode);
1041
+ await chmod(destShared, mode);
1042
+ const settingsPath = getGeminiCliSettingsPath();
1043
+ let existing = {};
1044
+ try {
1045
+ const rawText = await readFile(settingsPath, "utf8");
1046
+ const parsed = JSON.parse(rawText);
1047
+ if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) {
1048
+ existing = parsed;
1049
+ }
1050
+ } catch (err) {
1051
+ if (isErrnoException(err) && err.code === "ENOENT") {
1052
+ existing = {};
1053
+ } else {
1054
+ process.stderr.write(
1055
+ style.yellow("\u26A0") + ` Could not parse ${settingsPath}. Create valid JSON or remove the file, then run init again.
1056
+ `
1057
+ );
1058
+ throw new Error(`Invalid Gemini CLI settings at ${settingsPath}`);
1059
+ }
1060
+ }
1061
+ const hooksRaw = existing["hooks"];
1062
+ const hooksObj = typeof hooksRaw === "object" && hooksRaw !== null && !Array.isArray(hooksRaw) ? hooksRaw : {};
1063
+ if (geminiSettingsHasMulticornHooks(hooksObj)) {
1064
+ const answer = await ask(
1065
+ "Existing Multicorn Shield hooks were found in ~/.gemini/settings.json. Overwrite? (Y/n) "
1066
+ );
1067
+ if (answer.trim().toLowerCase() === "n") {
1068
+ throw new Error("Installation cancelled: existing Shield hooks left unchanged.");
1069
+ }
1070
+ }
1071
+ const cleaned = geminiStripMulticornHookEntries({ ...hooksObj });
1072
+ const beforeArr = Array.isArray(cleaned["BeforeTool"]) ? [...cleaned["BeforeTool"]] : [];
1073
+ const afterArr = Array.isArray(cleaned["AfterTool"]) ? [...cleaned["AfterTool"]] : [];
1074
+ const beforeCmd = `node ${destBefore}`;
1075
+ const afterCmd = `node ${destAfter}`;
1076
+ beforeArr.push({
1077
+ matcher: ".*",
1078
+ hooks: [
1079
+ {
1080
+ type: "command",
1081
+ name: "multicorn-shield",
1082
+ command: beforeCmd,
1083
+ timeout: 6e4
1084
+ }
1085
+ ]
1086
+ });
1087
+ afterArr.push({
1088
+ matcher: ".*",
720
1089
  hooks: [
721
1090
  {
722
1091
  type: "command",
@@ -864,19 +1233,6 @@ function isPlatformDetectedForMenu(slug) {
864
1233
  return Promise.resolve(false);
865
1234
  }
866
1235
  }
867
- var DEFAULT_AGENT_NAMES = {
868
- openclaw: "my-openclaw-agent",
869
- "claude-code": "my-claude-code-agent",
870
- cursor: "my-cursor-agent",
871
- windsurf: "my-windsurf-agent",
872
- cline: "my-cline-agent",
873
- "claude-desktop": "my-claude-desktop-agent",
874
- "gemini-cli": "my-gemini-cli-agent",
875
- "kilo-code": "my-kilo-code-agent",
876
- "github-copilot": "my-github-copilot-agent",
877
- "continue-dev": "my-continue-agent",
878
- goose: "my-goose-agent"
879
- };
880
1236
  async function promptPlatformSelection(ask) {
881
1237
  process.stderr.write(
882
1238
  "\n" + style.bold(style.violet("Which platform are you connecting?")) + "\n\n"
@@ -937,8 +1293,79 @@ async function promptWindsurfIntegrationMode(ask) {
937
1293
  }
938
1294
  return choice === 1 ? "native" : "hosted";
939
1295
  }
1296
+ async function arrowSelect(options, ask, fallbackLabel) {
1297
+ const canRaw = process.stdin.isTTY && typeof process.stdin.setRawMode === "function";
1298
+ if (!canRaw) {
1299
+ for (let i = 0; i < options.length; i++) {
1300
+ const optLine = options.at(i) ?? "";
1301
+ process.stderr.write(` ${style.violet(String(i + 1))}. ${optLine}
1302
+ `);
1303
+ }
1304
+ const label = fallbackLabel ?? "Choose";
1305
+ let sel = -1;
1306
+ while (sel < 0) {
1307
+ const input = await ask(`${label} (1-${String(options.length)}): `);
1308
+ const n = parseInt(input.trim(), 10);
1309
+ if (n >= 1 && n <= options.length) sel = n - 1;
1310
+ }
1311
+ return sel;
1312
+ }
1313
+ let idx = 0;
1314
+ function render() {
1315
+ for (let i = 0; i < options.length; i++) {
1316
+ const opt = options.at(i);
1317
+ if (opt === void 0) continue;
1318
+ const prefix = i === idx ? style.violet("\u276F") : " ";
1319
+ const label = i === idx ? style.cyan(opt) : opt;
1320
+ process.stderr.write(`${prefix} ${label}
1321
+ `);
1322
+ }
1323
+ }
1324
+ function clearLines() {
1325
+ for (let n = options.length; n > 0; n -= 1) {
1326
+ process.stderr.write("\x1B[1A\x1B[2K");
1327
+ }
1328
+ }
1329
+ process.stderr.write("\n");
1330
+ render();
1331
+ return new Promise((resolvePromise) => {
1332
+ const wasRaw = process.stdin.isRaw;
1333
+ process.stdin.setRawMode(true);
1334
+ process.stdin.resume();
1335
+ function onData(buf) {
1336
+ const s = buf.toString("utf8");
1337
+ if (s === "\x1B[A" || s === "k") {
1338
+ idx = (idx - 1 + options.length) % options.length;
1339
+ clearLines();
1340
+ render();
1341
+ } else if (s === "\x1B[B" || s === "j") {
1342
+ idx = (idx + 1) % options.length;
1343
+ clearLines();
1344
+ render();
1345
+ } else if (s === "\r" || s === "\n") {
1346
+ cleanup();
1347
+ clearLines();
1348
+ const chosen = options.at(idx);
1349
+ if (chosen !== void 0) {
1350
+ process.stderr.write(`${style.violet("\u276F")} ${style.cyan(chosen)}
1351
+ `);
1352
+ }
1353
+ resolvePromise(idx);
1354
+ } else if (s === "") {
1355
+ cleanup();
1356
+ process.exit(130);
1357
+ }
1358
+ }
1359
+ function cleanup() {
1360
+ process.stdin.removeListener("data", onData);
1361
+ process.stdin.setRawMode(wasRaw);
1362
+ }
1363
+ process.stdin.on("data", onData);
1364
+ });
1365
+ }
940
1366
  async function promptAgentName(ask, platform) {
941
- const defaultAgentName = DEFAULT_AGENT_NAMES[platform] ?? "my-agent";
1367
+ const dirPart = normalizeAgentName(basename(process.cwd()));
1368
+ const defaultAgentName = dirPart.length > 0 ? normalizeAgentName(`${dirPart}-${platform}`) || platform : normalizeAgentName(platform) || platform;
942
1369
  let agentName = "";
943
1370
  while (agentName.length === 0) {
944
1371
  const input = await ask(
@@ -1208,6 +1635,18 @@ function printPlatformSnippet(platform, routingToken, shortName, apiKey) {
1208
1635
  process.stderr.write(style.dim("Start a new Goose session after updating config.") + "\n");
1209
1636
  }
1210
1637
  }
1638
+ function mergeAgentsForPlatform(localAgents, remoteAgents, selectedPlatform) {
1639
+ const localMatches = localAgents.filter((a) => a.platform === selectedPlatform);
1640
+ const seen = new Set(localMatches.map((a) => a.name));
1641
+ const out = localMatches.map((a) => ({ ...a }));
1642
+ for (const r of remoteAgents) {
1643
+ if (r.platform !== selectedPlatform) continue;
1644
+ if (seen.has(r.name)) continue;
1645
+ seen.add(r.name);
1646
+ out.push({ name: r.name, platform: selectedPlatform });
1647
+ }
1648
+ return out;
1649
+ }
1211
1650
  var DEFAULT_SHIELD_API_BASE_URL = "https://api.multicorn.ai";
1212
1651
  async function runInit(explicitBaseUrl) {
1213
1652
  if (!process.stdin.isTTY) {
@@ -1217,8 +1656,8 @@ async function runInit(explicitBaseUrl) {
1217
1656
  process.exit(1);
1218
1657
  }
1219
1658
  const rl = createInterface({ input: process.stdin, output: process.stderr });
1220
- const ask = (question) => new Promise((resolve) => {
1221
- rl.question(question, resolve);
1659
+ const ask = (question) => new Promise((resolve2) => {
1660
+ rl.question(question, resolve2);
1222
1661
  });
1223
1662
  process.stderr.write("\n" + BANNER + "\n");
1224
1663
  process.stderr.write(style.dim("Agent governance for the AI era") + "\n\n");
@@ -1294,6 +1733,8 @@ async function runInit(explicitBaseUrl) {
1294
1733
  let configuring = true;
1295
1734
  while (configuring) {
1296
1735
  let postSaveNativeSkipNote = null;
1736
+ let removeAgentNameBeforeSave = void 0;
1737
+ const initWorkspacePath = resolve(process.cwd());
1297
1738
  const selection = await promptPlatformSelection(ask);
1298
1739
  const selectedPlatform = PLATFORM_BY_SELECTION[selection] ?? "cursor";
1299
1740
  const selectedLabel = platformMenuLabelForSelection(selection);
@@ -1331,20 +1772,83 @@ async function runInit(explicitBaseUrl) {
1331
1772
  }
1332
1773
  continue;
1333
1774
  }
1334
- const existingForPlatform = currentAgents.find((a) => a.platform === selectedPlatform);
1335
- if (existingForPlatform !== void 0) {
1336
- process.stderr.write(
1337
- `
1338
- An agent for ${selectedLabel} already exists: ${style.cyan(existingForPlatform.name)}
1339
- `
1775
+ const remoteAccountAgents = await fetchRemoteAgentsSummaries(apiKey, resolvedBaseUrl);
1776
+ const agentsForPlatform = mergeAgentsForPlatform(
1777
+ currentAgents,
1778
+ remoteAccountAgents,
1779
+ selectedPlatform
1780
+ );
1781
+ const localForPlatformCount = currentAgents.filter(
1782
+ (a) => a.platform === selectedPlatform
1783
+ ).length;
1784
+ const accountForPlatformCount = remoteAccountAgents.filter(
1785
+ (r) => r.platform === selectedPlatform
1786
+ ).length;
1787
+ const savedSummary = currentAgents.length === 0 ? "none on disk" : currentAgents.map((a) => `${a.name} (${a.platform})`).join(", ");
1788
+ process.stderr.write(
1789
+ style.dim(
1790
+ `[shield init] Menu option ${String(selection)} -> platform slug "${selectedPlatform}". ${String(agentsForPlatform.length)} agent(s) for this platform (local file: ${String(localForPlatformCount)}, account API: ${String(accountForPlatformCount)}). On-disk entries: ${savedSummary}.`
1791
+ ) + "\n"
1792
+ );
1793
+ if (agentsForPlatform.length > 0) {
1794
+ const exactForWorkspace = agentsForPlatform.find(
1795
+ (a) => typeof a.workspacePath === "string" && a.workspacePath.length > 0 && resolve(a.workspacePath) === initWorkspacePath
1340
1796
  );
1341
- const replace = await ask("Replace it? (Y/n) ");
1342
- if (replace.trim().toLowerCase() === "n") {
1343
- const another2 = await ask("\nConnect another agent? (Y/n) ");
1344
- if (another2.trim().toLowerCase() === "n") {
1345
- configuring = false;
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
+ `
1819
+ You have ${String(agentsForPlatform.length)} agent(s) connected for ${selectedLabel}:
1820
+ `
1821
+ );
1822
+ for (const a of agentsForPlatform) {
1823
+ const wsHint = typeof a.workspacePath === "string" && a.workspacePath.length > 0 ? ` ${style.dim(a.workspacePath)}` : "";
1824
+ process.stderr.write(` ${style.dim("\u2022")} ${style.cyan(a.name)}${wsHint}
1825
+ `);
1826
+ }
1827
+ process.stderr.write("\n" + style.bold("What would you like to do?") + "\n");
1828
+ const actionIdx = await arrowSelect(
1829
+ [
1830
+ "Add a new agent alongside these",
1831
+ "Replace an existing agent",
1832
+ "Skip \u2014 choose a different platform"
1833
+ ],
1834
+ ask,
1835
+ "Action"
1836
+ );
1837
+ if (actionIdx === 2) {
1838
+ continue;
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
+ }
1346
1851
  }
1347
- continue;
1348
1852
  }
1349
1853
  }
1350
1854
  const prereqEntry = INIT_WIZARD_PLATFORM_REGISTRY.find((e) => e.slug === selectedPlatform);
@@ -1756,8 +2260,14 @@ An agent for ${selectedLabel} already exists: ${style.cyan(existingForPlatform.n
1756
2260
  }
1757
2261
  }
1758
2262
  if (setupSucceeded) {
1759
- currentAgents = currentAgents.filter((a) => a.platform !== selectedPlatform);
1760
- currentAgents.push({ name: agentName, platform: selectedPlatform });
2263
+ if (removeAgentNameBeforeSave !== void 0) {
2264
+ currentAgents = currentAgents.filter((a) => a.name !== removeAgentNameBeforeSave);
2265
+ }
2266
+ currentAgents.push({
2267
+ name: agentName,
2268
+ platform: selectedPlatform,
2269
+ workspacePath: initWorkspacePath
2270
+ });
1761
2271
  const raw = existing !== null ? { ...existing } : {};
1762
2272
  raw["apiKey"] = apiKey;
1763
2273
  raw["baseUrl"] = resolvedBaseUrl;
@@ -1807,848 +2317,548 @@ An agent for ${selectedLabel} already exists: ${style.cyan(existingForPlatform.n
1807
2317
  );
1808
2318
  }
1809
2319
  if (configuredPlatforms.has("claude-code")) {
1810
- blocks.push(
1811
- "\n" + style.bold("To complete your Claude Code setup:") + "\n \u2192 Add marketplace: " + style.cyan("claude plugin marketplace add Multicorn-AI/multicorn-shield") + "\n \u2192 Install plugin: " + style.cyan("claude plugin install multicorn-shield@multicorn-shield") + "\n"
1812
- );
1813
- }
1814
- if (configuredPlatforms.has("claude-desktop")) {
1815
- blocks.push(
1816
- "\n" + style.bold("To complete your Claude Desktop setup:") + "\n \u2192 Restart Claude Desktop to pick up config changes\n"
1817
- );
1818
- }
1819
- if (configuredPlatforms.has("cursor")) {
1820
- blocks.push(
1821
- "\n" + style.bold("To complete your Cursor setup:") + "\n 1. If you don't have Cursor yet, download it from " + style.cyan("https://cursor.com/downloads") + "\n 2. Open " + style.cyan("~/.cursor/mcp.json") + " and paste the config snippet shown above\n 3. Restart Cursor (or launch it for the first time) to load the new MCP server\n"
1822
- );
1823
- }
1824
- if (configuredPlatforms.has("kilo-code")) {
1825
- blocks.push(
1826
- "\n" + style.bold("To complete your Kilo Code setup:") + "\n 1. Save the snippet to " + style.cyan(".kilocode/mcp.json") + " in your project root, or under the mcp key in " + style.cyan("kilo.jsonc") + "\n 2. Run your next task in Kilo Code so it picks up the MCP server\n"
1827
- );
1828
- }
1829
- if (configuredPlatforms.has("github-copilot")) {
1830
- blocks.push(
1831
- "\n" + style.bold("GitHub Copilot MCP:") + "\n 1. Open VS Code Command Palette: Preferences: Open User Settings (JSON)\n 2. Merge the snippet under the " + style.cyan("mcp") + " key and save\n 3. Use Copilot Agent mode and verify the MCP server connects\n"
1832
- );
1833
- }
1834
- if (configuredPlatforms.has("continue-dev")) {
1835
- blocks.push(
1836
- "\n" + style.bold("Continue MCP:") + "\n 1. If you don't have Continue yet, install from " + style.cyan("https://docs.continue.dev/ide-extensions/install") + "\n 2. Save JSON as " + style.cyan(".continue/mcpServers/shield.json") + " in your workspace, or add to " + style.cyan("~/.continue/config.yaml") + "\n 3. Reload VS Code and open Continue agent mode\n"
1837
- );
1838
- }
1839
- if (configuredPlatforms.has("goose")) {
1840
- blocks.push(
1841
- "\n" + style.bold("Goose MCP extension:") + "\n 1. Edit " + style.cyan("~/.config/goose/config.yaml") + " (or use goose configure)\n 2. Restart Goose CLI or Desktop\n"
1842
- );
1843
- }
1844
- const windsurfNativeConfigured = configuredAgents.some(
1845
- (a) => a.platform === "windsurf" && a.windsurfIntegration === "native"
1846
- );
1847
- const windsurfHostedConfigured = configuredAgents.some(
1848
- (a) => a.platform === "windsurf" && a.windsurfIntegration === "hosted"
1849
- );
1850
- if (windsurfNativeConfigured) {
1851
- blocks.push(
1852
- "\n" + style.bold("To complete native Windsurf (Shield) setup:") + "\n 1. Hook scripts: " + style.cyan(getWindsurfHooksInstallDir()) + "\n 2. Hooks config: " + style.cyan(getWindsurfCascadeHooksJsonPath()) + "\n 3. Restart Windsurf (quit fully, then reopen)\n"
1853
- );
1854
- }
1855
- if (windsurfHostedConfigured) {
1856
- blocks.push(
1857
- "\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"
1858
- );
1859
- }
1860
- const clineNativeConfigured = configuredAgents.some(
1861
- (a) => a.platform === "cline" && a.clineIntegration === "native"
1862
- );
1863
- const clineHostedConfigured = configuredAgents.some(
1864
- (a) => a.platform === "cline" && a.clineIntegration === "hosted"
1865
- );
1866
- if (clineNativeConfigured) {
1867
- blocks.push(
1868
- "\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"
1869
- );
1870
- }
1871
- if (clineHostedConfigured) {
1872
- blocks.push(
1873
- "\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"
1874
- );
1875
- }
1876
- const geminiCliNativeConfigured = configuredAgents.some(
1877
- (a) => a.platform === "gemini-cli" && a.geminiCliIntegration === "native"
1878
- );
1879
- const geminiCliHostedConfigured = configuredAgents.some(
1880
- (a) => a.platform === "gemini-cli" && a.geminiCliIntegration === "hosted"
1881
- );
1882
- if (geminiCliNativeConfigured) {
1883
- blocks.push(
1884
- "\n" + style.bold("Gemini CLI native hooks:") + "\n Your Gemini CLI hooks are installed. Restart Gemini CLI to activate Shield governance.\n"
1885
- );
1886
- }
1887
- if (geminiCliHostedConfigured) {
1888
- blocks.push(
1889
- "\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"
1890
- );
1891
- }
1892
- if (blocks.length > 0) {
1893
- process.stderr.write("\n" + style.bold(style.violet("Next steps")) + "\n");
1894
- process.stderr.write(blocks.join("") + "\n");
1895
- }
1896
- }
1897
- return lastConfig;
1898
- }
1899
-
1900
- // src/types/index.ts
1901
- var PERMISSION_LEVELS = {
1902
- Read: "read",
1903
- Write: "write",
1904
- Execute: "execute",
1905
- Publish: "publish",
1906
- Create: "create"
1907
- };
1908
-
1909
- // src/scopes/scope-parser.ts
1910
- var VALID_PERMISSION_LEVELS = new Set(Object.values(PERMISSION_LEVELS));
1911
- [...VALID_PERMISSION_LEVELS].join(", ");
1912
- function formatScope(scope) {
1913
- return `${scope.permissionLevel}:${scope.service}`;
1914
- }
1915
-
1916
- // src/scopes/scope-validator.ts
1917
- function validateScopeAccess(grantedScopes, requested) {
1918
- const isGranted = grantedScopes.some(
1919
- (granted) => granted.service === requested.service && granted.permissionLevel === requested.permissionLevel
1920
- );
1921
- if (isGranted) {
1922
- return { allowed: true };
1923
- }
1924
- const serviceScopes = grantedScopes.filter((g) => g.service === requested.service);
1925
- if (serviceScopes.length > 0) {
1926
- const grantedLevels = serviceScopes.map((g) => `"${g.permissionLevel}"`).join(", ");
1927
- return {
1928
- allowed: false,
1929
- reason: `Permission "${requested.permissionLevel}" is not granted for service "${requested.service}". Currently granted permission level(s): ${grantedLevels}. Requested scope "${formatScope(requested)}" requires explicit consent.`
1930
- };
1931
- }
1932
- return {
1933
- allowed: false,
1934
- reason: `No permissions granted for service "${requested.service}". The agent has not been authorised to access this service. Request scope "${formatScope(requested)}" via the consent screen.`
1935
- };
1936
- }
1937
- function hasScope(grantedScopes, requested) {
1938
- return grantedScopes.some(
1939
- (granted) => granted.service === requested.service && granted.permissionLevel === requested.permissionLevel
1940
- );
1941
- }
1942
-
1943
- // src/logger/action-logger.ts
1944
- function createActionLogger(config) {
1945
- if (!config.apiKey || config.apiKey.trim().length === 0) {
1946
- throw new Error(
1947
- "[ActionLogger] API key is required. Provide it via the 'apiKey' config option."
1948
- );
1949
- }
1950
- const baseUrl = config.baseUrl ?? "https://api.multicorn.ai";
1951
- const timeout = config.timeout ?? 5e3;
1952
- if (!baseUrl.startsWith("https://") && !baseUrl.startsWith("http://localhost")) {
1953
- throw new Error(
1954
- `[ActionLogger] Base URL must use HTTPS for security. Received: "${baseUrl}". Use https:// or http://localhost for local development.`
1955
- );
1956
- }
1957
- const endpoint = `${baseUrl}/api/v1/actions`;
1958
- const batchEnabled = config.batchMode?.enabled ?? false;
1959
- const maxBatchSize = config.batchMode?.maxSize ?? 10;
1960
- const flushInterval = config.batchMode?.flushIntervalMs ?? 5e3;
1961
- const queue = [];
1962
- let flushTimer;
1963
- let isShutdown = false;
1964
- async function sendActions(actions) {
1965
- if (actions.length === 0) return;
1966
- const convertAction = (action) => ({
1967
- agent: action.agent,
1968
- service: action.service,
1969
- actionType: action.actionType,
1970
- status: action.status,
1971
- ...action.cost !== void 0 ? { cost: action.cost } : {},
1972
- ...action.metadata !== void 0 ? { metadata: action.metadata } : {}
1973
- });
1974
- const convertedActions = actions.map(convertAction);
1975
- const payload = batchEnabled ? { actions: convertedActions } : convertedActions[0];
1976
- let lastError;
1977
- for (let attempt = 0; attempt < 2; attempt++) {
1978
- try {
1979
- const controller = new AbortController();
1980
- const timeoutId = setTimeout(() => {
1981
- controller.abort();
1982
- }, timeout);
1983
- const response = await fetch(endpoint, {
1984
- method: "POST",
1985
- headers: {
1986
- "Content-Type": "application/json",
1987
- "X-Multicorn-Key": config.apiKey
1988
- },
1989
- body: JSON.stringify(payload),
1990
- signal: controller.signal
1991
- });
1992
- clearTimeout(timeoutId);
1993
- if (response.ok) {
1994
- return;
1995
- }
1996
- if (response.status >= 400 && response.status < 500) {
1997
- const body = await response.text().catch(() => "");
1998
- throw new Error(
1999
- `[ActionLogger] Client error (${String(response.status)}): ${response.statusText}. Response: ${body}. Check your API key and payload format.`
2000
- );
2001
- }
2002
- if (response.status >= 500 && attempt === 0) {
2003
- lastError = new Error(
2004
- `[ActionLogger] Server error (${String(response.status)}): ${response.statusText}. Retrying once...`
2005
- );
2006
- await sleep(100 * Math.pow(2, attempt));
2007
- continue;
2008
- }
2009
- throw new Error(
2010
- `[ActionLogger] Server error (${String(response.status)}) after retry: ${response.statusText}. Multicorn API may be experiencing issues.`
2011
- );
2012
- } catch (error) {
2013
- if (error instanceof Error) {
2014
- if (error.name === "AbortError") {
2015
- lastError = new Error(
2016
- `[ActionLogger] Request timeout after ${String(timeout)}ms. Increase the 'timeout' config option or check your network connection.`
2017
- );
2018
- } else if (error.message.includes("Client error") || error.message.includes("Server error")) {
2019
- lastError = error;
2020
- } else {
2021
- lastError = new Error(
2022
- `[ActionLogger] Network error: ${error.message}. Check your network connection and API endpoint.`
2023
- );
2024
- }
2025
- } else {
2026
- lastError = new Error(`[ActionLogger] Unknown error: ${String(error)}`);
2027
- }
2028
- if (attempt === 0 && !lastError.message.includes("Client error")) {
2029
- await sleep(100 * Math.pow(2, attempt));
2030
- continue;
2031
- }
2032
- break;
2033
- }
2320
+ blocks.push(
2321
+ "\n" + style.bold("To complete your Claude Code setup:") + "\n \u2192 Add marketplace: " + style.cyan("claude plugin marketplace add Multicorn-AI/multicorn-shield") + "\n \u2192 Install plugin: " + style.cyan("claude plugin install multicorn-shield@multicorn-shield") + "\n"
2322
+ );
2034
2323
  }
2035
- if (lastError) {
2036
- if (config.onError) {
2037
- config.onError(lastError);
2038
- }
2324
+ if (configuredPlatforms.has("claude-desktop")) {
2325
+ blocks.push(
2326
+ "\n" + style.bold("To complete your Claude Desktop setup:") + "\n \u2192 Restart Claude Desktop to pick up config changes\n"
2327
+ );
2039
2328
  }
2040
- }
2041
- async function flushQueue() {
2042
- if (queue.length === 0) return;
2043
- const actions = queue.map((item) => item.payload);
2044
- queue.length = 0;
2045
- await sendActions(actions);
2046
- }
2047
- function startFlushTimer() {
2048
- if (flushTimer !== void 0) return;
2049
- flushTimer = setInterval(() => {
2050
- flushQueue().catch(() => {
2051
- });
2052
- }, flushInterval);
2053
- const timer = flushTimer;
2054
- if (typeof timer.unref === "function") {
2055
- timer.unref();
2329
+ if (configuredPlatforms.has("cursor")) {
2330
+ blocks.push(
2331
+ "\n" + style.bold("To complete your Cursor setup:") + "\n 1. If you don't have Cursor yet, download it from " + style.cyan("https://cursor.com/downloads") + "\n 2. Open " + style.cyan("~/.cursor/mcp.json") + " and paste the config snippet shown above\n 3. Restart Cursor (or launch it for the first time) to load the new MCP server\n"
2332
+ );
2056
2333
  }
2057
- }
2058
- function stopFlushTimer() {
2059
- if (flushTimer) {
2060
- clearInterval(flushTimer);
2061
- flushTimer = void 0;
2334
+ if (configuredPlatforms.has("kilo-code")) {
2335
+ blocks.push(
2336
+ "\n" + style.bold("To complete your Kilo Code setup:") + "\n 1. Save the snippet to " + style.cyan(".kilocode/mcp.json") + " in your project root, or under the mcp key in " + style.cyan("kilo.jsonc") + "\n 2. Run your next task in Kilo Code so it picks up the MCP server\n"
2337
+ );
2062
2338
  }
2063
- }
2064
- if (batchEnabled) {
2065
- startFlushTimer();
2066
- }
2067
- return {
2068
- logAction(action) {
2069
- if (isShutdown) {
2070
- throw new Error(
2071
- "[ActionLogger] Cannot log action after shutdown. Create a new logger instance."
2072
- );
2073
- }
2074
- if (action.agent.trim().length === 0) {
2075
- throw new Error("[ActionLogger] Action must have a non-empty 'agent' field.");
2076
- }
2077
- if (action.service.trim().length === 0) {
2078
- throw new Error("[ActionLogger] Action must have a non-empty 'service' field.");
2079
- }
2080
- if (action.actionType.trim().length === 0) {
2081
- throw new Error("[ActionLogger] Action must have a non-empty 'actionType' field.");
2082
- }
2083
- if (action.status.trim().length === 0) {
2084
- throw new Error("[ActionLogger] Action must have a non-empty 'status' field.");
2085
- }
2086
- if (batchEnabled) {
2087
- queue.push({ payload: action, timestamp: Date.now() });
2088
- if (queue.length >= maxBatchSize) {
2089
- flushQueue().catch(() => {
2090
- });
2091
- }
2092
- } else {
2093
- sendActions([action]).catch(() => {
2094
- });
2095
- }
2096
- return Promise.resolve();
2097
- },
2098
- async flush() {
2099
- if (!batchEnabled) return;
2100
- await flushQueue();
2101
- },
2102
- async shutdown() {
2103
- if (isShutdown) return;
2104
- isShutdown = true;
2105
- stopFlushTimer();
2106
- if (batchEnabled) {
2107
- await flushQueue();
2108
- }
2339
+ if (configuredPlatforms.has("github-copilot")) {
2340
+ blocks.push(
2341
+ "\n" + style.bold("GitHub Copilot MCP:") + "\n 1. Open VS Code Command Palette: Preferences: Open User Settings (JSON)\n 2. Merge the snippet under the " + style.cyan("mcp") + " key and save\n 3. Use Copilot Agent mode and verify the MCP server connects\n"
2342
+ );
2109
2343
  }
2110
- };
2111
- }
2112
- function sleep(ms) {
2113
- return new Promise((resolve) => setTimeout(resolve, ms));
2114
- }
2115
-
2116
- // src/spending/spending-checker.ts
2117
- function createSpendingChecker(config) {
2118
- validateLimits(config.limits);
2119
- let dailySpendCents = 0;
2120
- let monthlySpendCents = 0;
2121
- let lastDailyReset = /* @__PURE__ */ new Date();
2122
- let lastMonthlyReset = /* @__PURE__ */ new Date();
2123
- function validateAmount(amountCents, context) {
2124
- if (!Number.isInteger(amountCents)) {
2125
- throw new Error(
2126
- `[SpendingChecker] ${context} must be an integer (cents). Received: ${String(amountCents)}. Convert dollars to cents by multiplying by 100.`
2344
+ if (configuredPlatforms.has("continue-dev")) {
2345
+ blocks.push(
2346
+ "\n" + style.bold("Continue MCP:") + "\n 1. If you don't have Continue yet, install from " + style.cyan("https://docs.continue.dev/ide-extensions/install") + "\n 2. Save JSON as " + style.cyan(".continue/mcpServers/shield.json") + " in your workspace, or add to " + style.cyan("~/.continue/config.yaml") + "\n 3. Reload VS Code and open Continue agent mode\n"
2127
2347
  );
2128
2348
  }
2129
- if (amountCents < 0) {
2130
- throw new Error(
2131
- `[SpendingChecker] ${context} must be non-negative. Received: ${String(amountCents)} cents.`
2349
+ if (configuredPlatforms.has("goose")) {
2350
+ blocks.push(
2351
+ "\n" + style.bold("Goose MCP extension:") + "\n 1. Edit " + style.cyan("~/.config/goose/config.yaml") + " (or use goose configure)\n 2. Restart Goose CLI or Desktop\n"
2132
2352
  );
2133
2353
  }
2134
- }
2135
- function formatCents(cents) {
2136
- const dollars = cents / 100;
2137
- return `$${dollars.toLocaleString("en-US", {
2138
- minimumFractionDigits: 2,
2139
- maximumFractionDigits: 2
2140
- })}`;
2141
- }
2142
- function checkAndResetDaily() {
2143
- const now = /* @__PURE__ */ new Date();
2144
- if (shouldResetDaily(lastDailyReset, now)) {
2145
- dailySpendCents = 0;
2146
- lastDailyReset = now;
2354
+ const windsurfNativeConfigured = configuredAgents.some(
2355
+ (a) => a.platform === "windsurf" && a.windsurfIntegration === "native"
2356
+ );
2357
+ const windsurfHostedConfigured = configuredAgents.some(
2358
+ (a) => a.platform === "windsurf" && a.windsurfIntegration === "hosted"
2359
+ );
2360
+ if (windsurfNativeConfigured) {
2361
+ blocks.push(
2362
+ "\n" + style.bold("To complete native Windsurf (Shield) setup:") + "\n 1. Hook scripts: " + style.cyan(getWindsurfHooksInstallDir()) + "\n 2. Hooks config: " + style.cyan(getWindsurfCascadeHooksJsonPath()) + "\n 3. Restart Windsurf (quit fully, then reopen)\n"
2363
+ );
2147
2364
  }
2148
- }
2149
- function checkAndResetMonthly() {
2150
- const now = /* @__PURE__ */ new Date();
2151
- if (shouldResetMonthly(lastMonthlyReset, now)) {
2152
- monthlySpendCents = 0;
2153
- lastMonthlyReset = now;
2365
+ if (windsurfHostedConfigured) {
2366
+ blocks.push(
2367
+ "\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"
2368
+ );
2154
2369
  }
2155
- }
2156
- function shouldResetDaily(lastReset, now) {
2157
- return lastReset.getDate() !== now.getDate() || lastReset.getMonth() !== now.getMonth() || lastReset.getFullYear() !== now.getFullYear();
2158
- }
2159
- function shouldResetMonthly(lastReset, now) {
2160
- return lastReset.getMonth() !== now.getMonth() || lastReset.getFullYear() !== now.getFullYear();
2161
- }
2162
- function calculateRemainingBudget() {
2163
- return {
2164
- transaction: config.limits.perTransaction,
2165
- daily: Math.max(0, config.limits.perDay - dailySpendCents),
2166
- monthly: Math.max(0, config.limits.perMonth - monthlySpendCents)
2167
- };
2168
- }
2169
- return {
2170
- checkSpend(amountCents) {
2171
- validateAmount(amountCents, "Spend amount");
2172
- checkAndResetDaily();
2173
- checkAndResetMonthly();
2174
- if (amountCents > config.limits.perTransaction) {
2175
- return {
2176
- allowed: false,
2177
- reason: `Action blocked: ${formatCents(amountCents)} exceeds per-transaction limit of ${formatCents(config.limits.perTransaction)}`,
2178
- remainingBudget: calculateRemainingBudget()
2179
- };
2180
- }
2181
- const projectedDaily = dailySpendCents + amountCents;
2182
- if (projectedDaily > config.limits.perDay) {
2183
- return {
2184
- allowed: false,
2185
- reason: `Action blocked: ${formatCents(amountCents)} would exceed per-day limit. Current spend today: ${formatCents(dailySpendCents)}, limit: ${formatCents(config.limits.perDay)}`,
2186
- remainingBudget: calculateRemainingBudget()
2187
- };
2188
- }
2189
- const projectedMonthly = monthlySpendCents + amountCents;
2190
- if (projectedMonthly > config.limits.perMonth) {
2191
- return {
2192
- allowed: false,
2193
- reason: `Action blocked: ${formatCents(amountCents)} would exceed per-month limit. Current spend this month: ${formatCents(monthlySpendCents)}, limit: ${formatCents(config.limits.perMonth)}`,
2194
- remainingBudget: calculateRemainingBudget()
2195
- };
2196
- }
2197
- return {
2198
- allowed: true,
2199
- remainingBudget: calculateRemainingBudget()
2200
- };
2201
- },
2202
- recordSpend(amountCents) {
2203
- validateAmount(amountCents, "Spend amount");
2204
- checkAndResetDaily();
2205
- checkAndResetMonthly();
2206
- dailySpendCents += amountCents;
2207
- monthlySpendCents += amountCents;
2208
- },
2209
- getCurrentSpend() {
2210
- checkAndResetDaily();
2211
- checkAndResetMonthly();
2212
- return {
2213
- daily: dailySpendCents,
2214
- monthly: monthlySpendCents
2215
- };
2216
- },
2217
- reset() {
2218
- dailySpendCents = 0;
2219
- monthlySpendCents = 0;
2220
- lastDailyReset = /* @__PURE__ */ new Date();
2221
- lastMonthlyReset = /* @__PURE__ */ new Date();
2370
+ const clineNativeConfigured = configuredAgents.some(
2371
+ (a) => a.platform === "cline" && a.clineIntegration === "native"
2372
+ );
2373
+ const clineHostedConfigured = configuredAgents.some(
2374
+ (a) => a.platform === "cline" && a.clineIntegration === "hosted"
2375
+ );
2376
+ if (clineNativeConfigured) {
2377
+ blocks.push(
2378
+ "\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"
2379
+ );
2222
2380
  }
2223
- };
2224
- }
2225
- function validateLimits(limits) {
2226
- const checks = [
2227
- { value: limits.perTransaction, name: "perTransaction" },
2228
- { value: limits.perDay, name: "perDay" },
2229
- { value: limits.perMonth, name: "perMonth" }
2230
- ];
2231
- for (const check of checks) {
2232
- if (!Number.isInteger(check.value)) {
2233
- throw new Error(
2234
- `[SpendingChecker] Limit "${check.name}" must be an integer (cents). Received: ${String(check.value)}. All limits must be specified in integer cents.`
2381
+ if (clineHostedConfigured) {
2382
+ blocks.push(
2383
+ "\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"
2384
+ );
2385
+ }
2386
+ const geminiCliNativeConfigured = configuredAgents.some(
2387
+ (a) => a.platform === "gemini-cli" && a.geminiCliIntegration === "native"
2388
+ );
2389
+ const geminiCliHostedConfigured = configuredAgents.some(
2390
+ (a) => a.platform === "gemini-cli" && a.geminiCliIntegration === "hosted"
2391
+ );
2392
+ if (geminiCliNativeConfigured) {
2393
+ blocks.push(
2394
+ "\n" + style.bold("Gemini CLI native hooks:") + "\n Your Gemini CLI hooks are installed. Restart Gemini CLI to activate Shield governance.\n"
2395
+ );
2396
+ }
2397
+ if (geminiCliHostedConfigured) {
2398
+ blocks.push(
2399
+ "\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"
2235
2400
  );
2236
2401
  }
2237
- if (check.value < 0) {
2238
- throw new Error(
2239
- `[SpendingChecker] Limit "${check.name}" must be non-negative. Received: ${String(check.value)} cents.`
2240
- );
2402
+ if (blocks.length > 0) {
2403
+ process.stderr.write("\n" + style.bold(style.violet("Next steps")) + "\n");
2404
+ process.stderr.write(blocks.join("") + "\n");
2241
2405
  }
2242
2406
  }
2407
+ return lastConfig;
2243
2408
  }
2244
- function dollarsToCents(dollars) {
2245
- return Math.round(dollars * 100);
2409
+
2410
+ // src/types/index.ts
2411
+ var PERMISSION_LEVELS = {
2412
+ Read: "read",
2413
+ Write: "write",
2414
+ Execute: "execute",
2415
+ Publish: "publish",
2416
+ Create: "create"
2417
+ };
2418
+
2419
+ // src/scopes/scope-parser.ts
2420
+ var VALID_PERMISSION_LEVELS = new Set(Object.values(PERMISSION_LEVELS));
2421
+ [...VALID_PERMISSION_LEVELS].join(", ");
2422
+ function formatScope(scope) {
2423
+ return `${scope.permissionLevel}:${scope.service}`;
2246
2424
  }
2247
2425
 
2248
- // src/proxy/interceptor.ts
2249
- var BLOCKED_ERROR_CODE = -32e3;
2250
- var SPENDING_BLOCKED_ERROR_CODE = -32001;
2251
- var INTERNAL_ERROR_CODE = -32002;
2252
- var SERVICE_UNREACHABLE_ERROR_CODE = -32003;
2253
- var AUTH_ERROR_CODE = -32004;
2254
- function parseJsonRpcLine(line) {
2255
- const trimmed = line.trim();
2256
- if (trimmed.length === 0) return null;
2257
- let parsed;
2258
- try {
2259
- parsed = JSON.parse(trimmed);
2260
- } catch {
2261
- return null;
2426
+ // src/scopes/scope-validator.ts
2427
+ function validateScopeAccess(grantedScopes, requested) {
2428
+ const isGranted = grantedScopes.some(
2429
+ (granted) => granted.service === requested.service && granted.permissionLevel === requested.permissionLevel
2430
+ );
2431
+ if (isGranted) {
2432
+ return { allowed: true };
2433
+ }
2434
+ const serviceScopes = grantedScopes.filter((g) => g.service === requested.service);
2435
+ if (serviceScopes.length > 0) {
2436
+ const grantedLevels = serviceScopes.map((g) => `"${g.permissionLevel}"`).join(", ");
2437
+ return {
2438
+ allowed: false,
2439
+ reason: `Permission "${requested.permissionLevel}" is not granted for service "${requested.service}". Currently granted permission level(s): ${grantedLevels}. Requested scope "${formatScope(requested)}" requires explicit consent.`
2440
+ };
2262
2441
  }
2263
- return isJsonRpcRequest(parsed) ? parsed : null;
2264
- }
2265
- function extractToolCallParams(request) {
2266
- if (request.method !== "tools/call") return null;
2267
- if (typeof request.params !== "object" || request.params === null) return null;
2268
- const params = request.params;
2269
- const name = params["name"];
2270
- const args = params["arguments"];
2271
- if (typeof name !== "string") return null;
2272
- if (typeof args !== "object" || args === null) return null;
2273
- return { name, arguments: args };
2274
- }
2275
- function buildBlockedResponse(id, service, permissionLevel, dashboardUrl) {
2276
- const displayService = capitalize(service);
2277
- const message = `Action blocked by Multicorn Shield: agent does not have ${permissionLevel} access to ${displayService}. Configure permissions at ${dashboardUrl}`;
2278
- return {
2279
- jsonrpc: "2.0",
2280
- id,
2281
- error: {
2282
- code: BLOCKED_ERROR_CODE,
2283
- message
2284
- }
2285
- };
2286
- }
2287
- function buildSpendingBlockedResponse(id, reason, dashboardUrl) {
2288
- const message = `Action blocked by Multicorn Shield: ${reason}. Review spending limits at ${dashboardUrl}`;
2289
- return {
2290
- jsonrpc: "2.0",
2291
- id,
2292
- error: {
2293
- code: SPENDING_BLOCKED_ERROR_CODE,
2294
- message
2295
- }
2296
- };
2297
- }
2298
- function buildInternalErrorResponse(id) {
2299
- const message = "Action blocked: Shield encountered an internal error and cannot verify permissions. Check proxy logs for details.";
2300
- return {
2301
- jsonrpc: "2.0",
2302
- id,
2303
- error: {
2304
- code: INTERNAL_ERROR_CODE,
2305
- message
2306
- }
2307
- };
2308
- }
2309
- function buildServiceUnreachableResponse(id, dashboardUrl) {
2310
- const message = `Action blocked: Shield cannot verify permissions (service unreachable). Configure offline behaviour at ${dashboardUrl}`;
2311
- return {
2312
- jsonrpc: "2.0",
2313
- id,
2314
- error: {
2315
- code: SERVICE_UNREACHABLE_ERROR_CODE,
2316
- message
2317
- }
2318
- };
2319
- }
2320
- function buildAuthErrorResponse(id) {
2321
- const message = "Action blocked: Shield API key is invalid or has been revoked. Run npx multicorn-shield init to reconfigure.";
2322
2442
  return {
2323
- jsonrpc: "2.0",
2324
- id,
2325
- error: {
2326
- code: AUTH_ERROR_CODE,
2327
- message
2328
- }
2443
+ allowed: false,
2444
+ reason: `No permissions granted for service "${requested.service}". The agent has not been authorised to access this service. Request scope "${formatScope(requested)}" via the consent screen.`
2329
2445
  };
2330
2446
  }
2331
- function extractServiceFromToolName(toolName) {
2332
- const idx = toolName.indexOf("_");
2333
- return idx === -1 ? toolName : toolName.slice(0, idx);
2334
- }
2335
- function extractActionFromToolName(toolName) {
2336
- const idx = toolName.indexOf("_");
2337
- return idx === -1 ? "call" : toolName.slice(idx + 1);
2338
- }
2339
- function isJsonRpcRequest(value) {
2340
- if (typeof value !== "object" || value === null) return false;
2341
- const obj = value;
2342
- if (obj["jsonrpc"] !== "2.0") return false;
2343
- if (typeof obj["method"] !== "string") return false;
2344
- const id = obj["id"];
2345
- const validId = id === null || id === void 0 || typeof id === "string" || typeof id === "number";
2346
- return validId;
2347
- }
2348
- function capitalize(str) {
2349
- if (str.length === 0) return str;
2350
- const first = str[0];
2351
- return first !== void 0 ? first.toUpperCase() + str.slice(1) : str;
2352
- }
2353
- var MULTICORN_DIR = join(homedir(), ".multicorn");
2354
- var SCOPES_PATH = join(MULTICORN_DIR, "scopes.json");
2355
- var CACHE_META_PATH = join(MULTICORN_DIR, "cache-meta.json");
2356
- function cacheKey(agentName, apiKey) {
2357
- return createHash("sha256").update(`${agentName}:${apiKey}`).digest("hex").slice(0, 16);
2447
+ function hasScope(grantedScopes, requested) {
2448
+ return grantedScopes.some(
2449
+ (granted) => granted.service === requested.service && granted.permissionLevel === requested.permissionLevel
2450
+ );
2358
2451
  }
2359
- async function ensureCacheIdentity(apiKey) {
2360
- const currentHash = createHash("sha256").update(apiKey).digest("hex");
2361
- let storedHash = null;
2362
- try {
2363
- const raw = await readFile(CACHE_META_PATH, "utf8");
2364
- const meta = JSON.parse(raw);
2365
- if (typeof meta === "object" && meta !== null && "apiKeyHash" in meta) {
2366
- storedHash = meta.apiKeyHash;
2367
- }
2368
- } catch {
2452
+
2453
+ // src/logger/action-logger.ts
2454
+ function createActionLogger(config) {
2455
+ if (!config.apiKey || config.apiKey.trim().length === 0) {
2456
+ throw new Error(
2457
+ "[ActionLogger] API key is required. Provide it via the 'apiKey' config option."
2458
+ );
2369
2459
  }
2370
- if (storedHash === null || storedHash !== currentHash) {
2371
- try {
2372
- await unlink(SCOPES_PATH);
2373
- } catch {
2460
+ const baseUrl = config.baseUrl ?? "https://api.multicorn.ai";
2461
+ const timeout = config.timeout ?? 5e3;
2462
+ if (!baseUrl.startsWith("https://") && !baseUrl.startsWith("http://localhost")) {
2463
+ throw new Error(
2464
+ `[ActionLogger] Base URL must use HTTPS for security. Received: "${baseUrl}". Use https:// or http://localhost for local development.`
2465
+ );
2466
+ }
2467
+ const endpoint = `${baseUrl}/api/v1/actions`;
2468
+ const batchEnabled = config.batchMode?.enabled ?? false;
2469
+ const maxBatchSize = config.batchMode?.maxSize ?? 10;
2470
+ const flushInterval = config.batchMode?.flushIntervalMs ?? 5e3;
2471
+ const queue = [];
2472
+ let flushTimer;
2473
+ let isShutdown = false;
2474
+ async function sendActions(actions) {
2475
+ if (actions.length === 0) return;
2476
+ const convertAction = (action) => ({
2477
+ agent: action.agent,
2478
+ service: action.service,
2479
+ actionType: action.actionType,
2480
+ status: action.status,
2481
+ ...action.cost !== void 0 ? { cost: action.cost } : {},
2482
+ ...action.metadata !== void 0 ? { metadata: action.metadata } : {}
2483
+ });
2484
+ const convertedActions = actions.map(convertAction);
2485
+ const payload = batchEnabled ? { actions: convertedActions } : convertedActions[0];
2486
+ let lastError;
2487
+ for (let attempt = 0; attempt < 2; attempt++) {
2488
+ try {
2489
+ const controller = new AbortController();
2490
+ const timeoutId = setTimeout(() => {
2491
+ controller.abort();
2492
+ }, timeout);
2493
+ const response = await fetch(endpoint, {
2494
+ method: "POST",
2495
+ headers: {
2496
+ "Content-Type": "application/json",
2497
+ "X-Multicorn-Key": config.apiKey
2498
+ },
2499
+ body: JSON.stringify(payload),
2500
+ signal: controller.signal
2501
+ });
2502
+ clearTimeout(timeoutId);
2503
+ if (response.ok) {
2504
+ return;
2505
+ }
2506
+ if (response.status >= 400 && response.status < 500) {
2507
+ const body = await response.text().catch(() => "");
2508
+ throw new Error(
2509
+ `[ActionLogger] Client error (${String(response.status)}): ${response.statusText}. Response: ${body}. Check your API key and payload format.`
2510
+ );
2511
+ }
2512
+ if (response.status >= 500 && attempt === 0) {
2513
+ lastError = new Error(
2514
+ `[ActionLogger] Server error (${String(response.status)}): ${response.statusText}. Retrying once...`
2515
+ );
2516
+ await sleep2(100 * Math.pow(2, attempt));
2517
+ continue;
2518
+ }
2519
+ throw new Error(
2520
+ `[ActionLogger] Server error (${String(response.status)}) after retry: ${response.statusText}. Multicorn API may be experiencing issues.`
2521
+ );
2522
+ } catch (error) {
2523
+ if (error instanceof Error) {
2524
+ if (error.name === "AbortError") {
2525
+ lastError = new Error(
2526
+ `[ActionLogger] Request timeout after ${String(timeout)}ms. Increase the 'timeout' config option or check your network connection.`
2527
+ );
2528
+ } else if (error.message.includes("Client error") || error.message.includes("Server error")) {
2529
+ lastError = error;
2530
+ } else {
2531
+ lastError = new Error(
2532
+ `[ActionLogger] Network error: ${error.message}. Check your network connection and API endpoint.`
2533
+ );
2534
+ }
2535
+ } else {
2536
+ lastError = new Error(`[ActionLogger] Unknown error: ${String(error)}`);
2537
+ }
2538
+ if (attempt === 0 && !lastError.message.includes("Client error")) {
2539
+ await sleep2(100 * Math.pow(2, attempt));
2540
+ continue;
2541
+ }
2542
+ break;
2543
+ }
2544
+ }
2545
+ if (lastError) {
2546
+ if (config.onError) {
2547
+ config.onError(lastError);
2548
+ }
2374
2549
  }
2375
2550
  }
2376
- if (storedHash !== currentHash) {
2377
- await mkdir(MULTICORN_DIR, { recursive: true, mode: 448 });
2378
- await writeFile(CACHE_META_PATH, JSON.stringify({ apiKeyHash: currentHash }, null, 2) + "\n", {
2379
- encoding: "utf8",
2380
- mode: 384
2381
- });
2551
+ async function flushQueue() {
2552
+ if (queue.length === 0) return;
2553
+ const actions = queue.map((item) => item.payload);
2554
+ queue.length = 0;
2555
+ await sendActions(actions);
2382
2556
  }
2383
- }
2384
- async function loadCachedScopes(agentName, apiKey) {
2385
- if (apiKey.length === 0) return null;
2386
- await ensureCacheIdentity(apiKey);
2387
- const key = cacheKey(agentName, apiKey);
2388
- try {
2389
- const raw = await readFile(SCOPES_PATH, "utf8");
2390
- const parsed = JSON.parse(raw);
2391
- if (!isScopesCacheFile(parsed)) return null;
2392
- const entry = parsed[key];
2393
- return entry?.scopes ?? null;
2394
- } catch {
2395
- return null;
2557
+ function startFlushTimer() {
2558
+ if (flushTimer !== void 0) return;
2559
+ flushTimer = setInterval(() => {
2560
+ flushQueue().catch(() => {
2561
+ });
2562
+ }, flushInterval);
2563
+ const timer = flushTimer;
2564
+ if (typeof timer.unref === "function") {
2565
+ timer.unref();
2566
+ }
2396
2567
  }
2397
- }
2398
- async function saveCachedScopes(agentName, agentId, scopes, apiKey) {
2399
- if (apiKey.length === 0) return;
2400
- await ensureCacheIdentity(apiKey);
2401
- const key = cacheKey(agentName, apiKey);
2402
- await mkdir(MULTICORN_DIR, { recursive: true, mode: 448 });
2403
- let existing = {};
2404
- try {
2405
- const raw = await readFile(SCOPES_PATH, "utf8");
2406
- const parsed = JSON.parse(raw);
2407
- if (isScopesCacheFile(parsed)) existing = parsed;
2408
- } catch {
2568
+ function stopFlushTimer() {
2569
+ if (flushTimer) {
2570
+ clearInterval(flushTimer);
2571
+ flushTimer = void 0;
2572
+ }
2409
2573
  }
2410
- const updated = {
2411
- ...existing,
2412
- [key]: {
2413
- agentId,
2414
- scopes,
2415
- fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
2574
+ if (batchEnabled) {
2575
+ startFlushTimer();
2576
+ }
2577
+ return {
2578
+ logAction(action) {
2579
+ if (isShutdown) {
2580
+ throw new Error(
2581
+ "[ActionLogger] Cannot log action after shutdown. Create a new logger instance."
2582
+ );
2583
+ }
2584
+ if (action.agent.trim().length === 0) {
2585
+ throw new Error("[ActionLogger] Action must have a non-empty 'agent' field.");
2586
+ }
2587
+ if (action.service.trim().length === 0) {
2588
+ throw new Error("[ActionLogger] Action must have a non-empty 'service' field.");
2589
+ }
2590
+ if (action.actionType.trim().length === 0) {
2591
+ throw new Error("[ActionLogger] Action must have a non-empty 'actionType' field.");
2592
+ }
2593
+ if (action.status.trim().length === 0) {
2594
+ throw new Error("[ActionLogger] Action must have a non-empty 'status' field.");
2595
+ }
2596
+ if (batchEnabled) {
2597
+ queue.push({ payload: action, timestamp: Date.now() });
2598
+ if (queue.length >= maxBatchSize) {
2599
+ flushQueue().catch(() => {
2600
+ });
2601
+ }
2602
+ } else {
2603
+ sendActions([action]).catch(() => {
2604
+ });
2605
+ }
2606
+ return Promise.resolve();
2607
+ },
2608
+ async flush() {
2609
+ if (!batchEnabled) return;
2610
+ await flushQueue();
2611
+ },
2612
+ async shutdown() {
2613
+ if (isShutdown) return;
2614
+ isShutdown = true;
2615
+ stopFlushTimer();
2616
+ if (batchEnabled) {
2617
+ await flushQueue();
2618
+ }
2416
2619
  }
2417
2620
  };
2418
- await writeFile(SCOPES_PATH, JSON.stringify(updated, null, 2) + "\n", {
2419
- encoding: "utf8",
2420
- mode: 384
2421
- });
2422
2621
  }
2423
- function isScopesCacheFile(value) {
2424
- return typeof value === "object" && value !== null;
2622
+ function sleep2(ms) {
2623
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
2425
2624
  }
2426
2625
 
2427
- // src/proxy/consent.ts
2428
- var CONSENT_POLL_INTERVAL_MS = 3e3;
2429
- var CONSENT_POLL_TIMEOUT_MS = 5 * 60 * 1e3;
2430
- function deriveDashboardUrl(baseUrl) {
2431
- try {
2432
- const url = new URL(baseUrl);
2433
- if (url.hostname === "localhost" || url.hostname === "127.0.0.1") {
2434
- url.port = "5173";
2435
- url.protocol = "http:";
2436
- return url.toString();
2437
- }
2438
- if (url.hostname === "api.multicorn.ai") {
2439
- url.hostname = "app.multicorn.ai";
2440
- return url.toString();
2441
- }
2442
- if (url.hostname.includes("api")) {
2443
- url.hostname = url.hostname.replace("api", "app");
2444
- return url.toString();
2626
+ // src/spending/spending-checker.ts
2627
+ function createSpendingChecker(config) {
2628
+ validateLimits(config.limits);
2629
+ let dailySpendCents = 0;
2630
+ let monthlySpendCents = 0;
2631
+ let lastDailyReset = /* @__PURE__ */ new Date();
2632
+ let lastMonthlyReset = /* @__PURE__ */ new Date();
2633
+ function validateAmount(amountCents, context) {
2634
+ if (!Number.isInteger(amountCents)) {
2635
+ throw new Error(
2636
+ `[SpendingChecker] ${context} must be an integer (cents). Received: ${String(amountCents)}. Convert dollars to cents by multiplying by 100.`
2637
+ );
2445
2638
  }
2446
- if (url.protocol === "https:" && url.hostname !== "localhost" && url.hostname !== "127.0.0.1") {
2447
- return "https://app.multicorn.ai";
2639
+ if (amountCents < 0) {
2640
+ throw new Error(
2641
+ `[SpendingChecker] ${context} must be non-negative. Received: ${String(amountCents)} cents.`
2642
+ );
2448
2643
  }
2449
- return "https://app.multicorn.ai";
2450
- } catch {
2451
- return "https://app.multicorn.ai";
2452
2644
  }
2453
- }
2454
- var ShieldAuthError = class _ShieldAuthError extends Error {
2455
- constructor(message) {
2456
- super(message);
2457
- this.name = "ShieldAuthError";
2458
- Object.setPrototypeOf(this, _ShieldAuthError.prototype);
2645
+ function formatCents(cents) {
2646
+ const dollars = cents / 100;
2647
+ return `$${dollars.toLocaleString("en-US", {
2648
+ minimumFractionDigits: 2,
2649
+ maximumFractionDigits: 2
2650
+ })}`;
2459
2651
  }
2460
- };
2461
- async function findAgentByName(agentName, apiKey, baseUrl) {
2462
- let response;
2463
- try {
2464
- response = await fetch(`${baseUrl}/api/v1/agents`, {
2465
- headers: { "X-Multicorn-Key": apiKey },
2466
- signal: AbortSignal.timeout(8e3)
2467
- });
2468
- } catch {
2469
- return null;
2652
+ function checkAndResetDaily() {
2653
+ const now = /* @__PURE__ */ new Date();
2654
+ if (shouldResetDaily(lastDailyReset, now)) {
2655
+ dailySpendCents = 0;
2656
+ lastDailyReset = now;
2657
+ }
2470
2658
  }
2471
- if (!response.ok) {
2472
- if (response.status === 401 || response.status === 403) {
2473
- return { id: "", name: agentName, scopes: [], authInvalid: true };
2659
+ function checkAndResetMonthly() {
2660
+ const now = /* @__PURE__ */ new Date();
2661
+ if (shouldResetMonthly(lastMonthlyReset, now)) {
2662
+ monthlySpendCents = 0;
2663
+ lastMonthlyReset = now;
2474
2664
  }
2475
- return null;
2476
2665
  }
2477
- let body;
2478
- try {
2479
- body = await response.json();
2480
- } catch {
2481
- return null;
2666
+ function shouldResetDaily(lastReset, now) {
2667
+ return lastReset.getDate() !== now.getDate() || lastReset.getMonth() !== now.getMonth() || lastReset.getFullYear() !== now.getFullYear();
2482
2668
  }
2483
- if (!isApiSuccessResponse(body)) return null;
2484
- const agents = body.data;
2485
- if (!Array.isArray(agents)) return null;
2486
- const match = agents.find(
2487
- (a) => isAgentSummaryShape(a) && a.name === agentName
2488
- );
2489
- if (match === void 0) return null;
2490
- return { id: match.id, name: match.name, scopes: [] };
2491
- }
2492
- async function registerAgent(agentName, apiKey, baseUrl, platform) {
2493
- const response = await fetch(`${baseUrl}/api/v1/agents`, {
2494
- method: "POST",
2495
- headers: {
2496
- "Content-Type": "application/json",
2497
- "X-Multicorn-Key": apiKey
2669
+ function shouldResetMonthly(lastReset, now) {
2670
+ return lastReset.getMonth() !== now.getMonth() || lastReset.getFullYear() !== now.getFullYear();
2671
+ }
2672
+ function calculateRemainingBudget() {
2673
+ return {
2674
+ transaction: config.limits.perTransaction,
2675
+ daily: Math.max(0, config.limits.perDay - dailySpendCents),
2676
+ monthly: Math.max(0, config.limits.perMonth - monthlySpendCents)
2677
+ };
2678
+ }
2679
+ return {
2680
+ checkSpend(amountCents) {
2681
+ validateAmount(amountCents, "Spend amount");
2682
+ checkAndResetDaily();
2683
+ checkAndResetMonthly();
2684
+ if (amountCents > config.limits.perTransaction) {
2685
+ return {
2686
+ allowed: false,
2687
+ reason: `Action blocked: ${formatCents(amountCents)} exceeds per-transaction limit of ${formatCents(config.limits.perTransaction)}`,
2688
+ remainingBudget: calculateRemainingBudget()
2689
+ };
2690
+ }
2691
+ const projectedDaily = dailySpendCents + amountCents;
2692
+ if (projectedDaily > config.limits.perDay) {
2693
+ return {
2694
+ allowed: false,
2695
+ reason: `Action blocked: ${formatCents(amountCents)} would exceed per-day limit. Current spend today: ${formatCents(dailySpendCents)}, limit: ${formatCents(config.limits.perDay)}`,
2696
+ remainingBudget: calculateRemainingBudget()
2697
+ };
2698
+ }
2699
+ const projectedMonthly = monthlySpendCents + amountCents;
2700
+ if (projectedMonthly > config.limits.perMonth) {
2701
+ return {
2702
+ allowed: false,
2703
+ reason: `Action blocked: ${formatCents(amountCents)} would exceed per-month limit. Current spend this month: ${formatCents(monthlySpendCents)}, limit: ${formatCents(config.limits.perMonth)}`,
2704
+ remainingBudget: calculateRemainingBudget()
2705
+ };
2706
+ }
2707
+ return {
2708
+ allowed: true,
2709
+ remainingBudget: calculateRemainingBudget()
2710
+ };
2711
+ },
2712
+ recordSpend(amountCents) {
2713
+ validateAmount(amountCents, "Spend amount");
2714
+ checkAndResetDaily();
2715
+ checkAndResetMonthly();
2716
+ dailySpendCents += amountCents;
2717
+ monthlySpendCents += amountCents;
2498
2718
  },
2499
- body: JSON.stringify({ name: agentName, ...platform ? { platform } : {} }),
2500
- signal: AbortSignal.timeout(8e3)
2501
- });
2502
- if (!response.ok) {
2503
- if (response.status === 401 || response.status === 403) {
2504
- throw new ShieldAuthError(
2505
- `Failed to register agent "${agentName}": service returned ${String(response.status)}.`
2719
+ getCurrentSpend() {
2720
+ checkAndResetDaily();
2721
+ checkAndResetMonthly();
2722
+ return {
2723
+ daily: dailySpendCents,
2724
+ monthly: monthlySpendCents
2725
+ };
2726
+ },
2727
+ reset() {
2728
+ dailySpendCents = 0;
2729
+ monthlySpendCents = 0;
2730
+ lastDailyReset = /* @__PURE__ */ new Date();
2731
+ lastMonthlyReset = /* @__PURE__ */ new Date();
2732
+ }
2733
+ };
2734
+ }
2735
+ function validateLimits(limits) {
2736
+ const checks = [
2737
+ { value: limits.perTransaction, name: "perTransaction" },
2738
+ { value: limits.perDay, name: "perDay" },
2739
+ { value: limits.perMonth, name: "perMonth" }
2740
+ ];
2741
+ for (const check of checks) {
2742
+ if (!Number.isInteger(check.value)) {
2743
+ throw new Error(
2744
+ `[SpendingChecker] Limit "${check.name}" must be an integer (cents). Received: ${String(check.value)}. All limits must be specified in integer cents.`
2745
+ );
2746
+ }
2747
+ if (check.value < 0) {
2748
+ throw new Error(
2749
+ `[SpendingChecker] Limit "${check.name}" must be non-negative. Received: ${String(check.value)} cents.`
2506
2750
  );
2507
2751
  }
2508
- throw new Error(
2509
- `Failed to register agent "${agentName}": service returned ${String(response.status)}.`
2510
- );
2511
- }
2512
- const body = await response.json();
2513
- if (!isApiSuccessResponse(body)) {
2514
- throw new Error(`Failed to register agent "${agentName}": unexpected response format.`);
2515
- }
2516
- if (!isAgentSummaryShape(body.data)) {
2517
- throw new Error(`Failed to register agent "${agentName}": response missing agent ID.`);
2518
2752
  }
2519
- return body.data.id;
2520
2753
  }
2521
- async function fetchGrantedScopes(agentId, apiKey, baseUrl) {
2522
- let response;
2754
+ function dollarsToCents(dollars) {
2755
+ return Math.round(dollars * 100);
2756
+ }
2757
+
2758
+ // src/proxy/interceptor.ts
2759
+ var BLOCKED_ERROR_CODE = -32e3;
2760
+ var SPENDING_BLOCKED_ERROR_CODE = -32001;
2761
+ var INTERNAL_ERROR_CODE = -32002;
2762
+ var SERVICE_UNREACHABLE_ERROR_CODE = -32003;
2763
+ var AUTH_ERROR_CODE = -32004;
2764
+ function parseJsonRpcLine(line) {
2765
+ const trimmed = line.trim();
2766
+ if (trimmed.length === 0) return null;
2767
+ let parsed;
2523
2768
  try {
2524
- response = await fetch(`${baseUrl}/api/v1/agents/${agentId}`, {
2525
- headers: { "X-Multicorn-Key": apiKey },
2526
- signal: AbortSignal.timeout(8e3)
2527
- });
2769
+ parsed = JSON.parse(trimmed);
2528
2770
  } catch {
2529
- return [];
2530
- }
2531
- if (!response.ok) return [];
2532
- const body = await response.json();
2533
- if (!isApiSuccessResponse(body)) return [];
2534
- const agentDetail = body.data;
2535
- if (!isAgentDetailShape(agentDetail)) return [];
2536
- const scopes = [];
2537
- for (const perm of agentDetail.permissions) {
2538
- if (!isPermissionShape(perm)) continue;
2539
- if (perm.revoked_at !== null) continue;
2540
- if (perm.read) scopes.push({ service: perm.service, permissionLevel: "read" });
2541
- if (perm.write) scopes.push({ service: perm.service, permissionLevel: "write" });
2542
- if (perm.execute) scopes.push({ service: perm.service, permissionLevel: "execute" });
2771
+ return null;
2543
2772
  }
2544
- return scopes;
2773
+ return isJsonRpcRequest(parsed) ? parsed : null;
2545
2774
  }
2546
- function openBrowser(url) {
2547
- if (process.env["NODE_ENV"] === "test" || process.env["VITEST"] === "true") {
2548
- return;
2549
- }
2550
- const platform = process.platform;
2551
- const cmd = platform === "darwin" ? "open" : platform === "win32" ? "start" : "xdg-open";
2552
- spawn(cmd, [url], { detached: true, stdio: "ignore" }).unref();
2775
+ function extractToolCallParams(request) {
2776
+ if (request.method !== "tools/call") return null;
2777
+ if (typeof request.params !== "object" || request.params === null) return null;
2778
+ const params = request.params;
2779
+ const name = params["name"];
2780
+ const args = params["arguments"];
2781
+ if (typeof name !== "string") return null;
2782
+ if (typeof args !== "object" || args === null) return null;
2783
+ return { name, arguments: args };
2553
2784
  }
2554
- async function waitForConsent(agentId, agentName, apiKey, baseUrl, dashboardUrl, logger, scope, platform) {
2555
- const scopeStrings = scope ? [`${scope.service}:${scope.permissionLevel}`] : detectScopeHints();
2556
- const consentUrl = buildConsentUrl(agentName, scopeStrings, dashboardUrl, platform);
2557
- logger.info("Opening consent page in your browser.", { url: consentUrl });
2558
- process.stderr.write(
2559
- `
2560
- Action requires permission. Opening consent page...
2561
- ${consentUrl}
2562
-
2563
- Waiting for you to grant access in the Multicorn dashboard...
2564
- `
2565
- );
2566
- openBrowser(consentUrl);
2567
- const deadline = Date.now() + CONSENT_POLL_TIMEOUT_MS;
2568
- while (Date.now() < deadline) {
2569
- await sleep2(CONSENT_POLL_INTERVAL_MS);
2570
- const scopes = await fetchGrantedScopes(agentId, apiKey, baseUrl);
2571
- if (scopes.length > 0) {
2572
- logger.info("Permissions granted.", { agent: agentName, scopeCount: scopes.length });
2573
- return scopes;
2785
+ function buildBlockedResponse(id, service, permissionLevel, dashboardUrl) {
2786
+ const displayService = capitalize(service);
2787
+ const message = `Action blocked by Multicorn Shield: agent does not have ${permissionLevel} access to ${displayService}. Configure permissions at ${dashboardUrl}`;
2788
+ return {
2789
+ jsonrpc: "2.0",
2790
+ id,
2791
+ error: {
2792
+ code: BLOCKED_ERROR_CODE,
2793
+ message
2574
2794
  }
2575
- }
2576
- throw new Error(
2577
- `Consent not granted within ${String(CONSENT_POLL_TIMEOUT_MS / 6e4)} minutes. Grant access at ${dashboardUrl} and restart the proxy.`
2578
- );
2795
+ };
2579
2796
  }
2580
- async function resolveAgentRecord(agentName, apiKey, baseUrl, logger, platform) {
2581
- const cachedScopes = await loadCachedScopes(agentName, apiKey);
2582
- if (cachedScopes !== null && cachedScopes.length > 0) {
2583
- logger.debug("Loaded scopes from cache.", { agent: agentName, count: cachedScopes.length });
2584
- }
2585
- let agent = await findAgentByName(agentName, apiKey, baseUrl);
2586
- if (agent?.authInvalid) {
2587
- return agent;
2588
- }
2589
- if (agent === null) {
2590
- try {
2591
- logger.info("Agent not found. Registering.", { agent: agentName });
2592
- const id = await registerAgent(agentName, apiKey, baseUrl, platform);
2593
- agent = { id, name: agentName, scopes: [] };
2594
- logger.info("Agent registered.", { agent: agentName, id });
2595
- } catch (error) {
2596
- if (error instanceof ShieldAuthError) {
2597
- return { id: "", name: agentName, scopes: [], authInvalid: true };
2598
- }
2599
- const detail = error instanceof Error ? error.message : String(error);
2600
- if (cachedScopes !== null && cachedScopes.length > 0) {
2601
- logger.warn("Service unreachable. Using cached scopes.", { error: detail });
2602
- return { id: "", name: agentName, scopes: cachedScopes };
2603
- }
2604
- logger.warn("Could not reach Multicorn service. Running with empty permissions.", {
2605
- error: detail
2606
- });
2607
- return { id: "", name: agentName, scopes: [] };
2797
+ function buildSpendingBlockedResponse(id, reason, dashboardUrl) {
2798
+ const message = `Action blocked by Multicorn Shield: ${reason}. Review spending limits at ${dashboardUrl}`;
2799
+ return {
2800
+ jsonrpc: "2.0",
2801
+ id,
2802
+ error: {
2803
+ code: SPENDING_BLOCKED_ERROR_CODE,
2804
+ message
2608
2805
  }
2609
- }
2610
- const scopes = await fetchGrantedScopes(agent.id, apiKey, baseUrl);
2611
- if (scopes.length > 0) {
2612
- await saveCachedScopes(agentName, agent.id, scopes, apiKey);
2613
- }
2614
- return { ...agent, scopes };
2806
+ };
2615
2807
  }
2616
- function buildConsentUrl(agentName, scopes, dashboardUrl, platform) {
2617
- const base = dashboardUrl.replace(/\/+$/, "");
2618
- const params = new URLSearchParams({ agent: agentName });
2619
- if (scopes.length > 0) {
2620
- params.set("scopes", scopes.join(","));
2621
- }
2622
- if (platform) {
2623
- params.set("platform", platform);
2624
- }
2625
- return `${base}/consent?${params.toString()}`;
2808
+ function buildInternalErrorResponse(id) {
2809
+ const message = "Action blocked: Shield encountered an internal error and cannot verify permissions. Check proxy logs for details.";
2810
+ return {
2811
+ jsonrpc: "2.0",
2812
+ id,
2813
+ error: {
2814
+ code: INTERNAL_ERROR_CODE,
2815
+ message
2816
+ }
2817
+ };
2626
2818
  }
2627
- function detectScopeHints() {
2628
- return [];
2819
+ function buildServiceUnreachableResponse(id, dashboardUrl) {
2820
+ const message = `Action blocked: Shield cannot verify permissions (service unreachable). Configure offline behaviour at ${dashboardUrl}`;
2821
+ return {
2822
+ jsonrpc: "2.0",
2823
+ id,
2824
+ error: {
2825
+ code: SERVICE_UNREACHABLE_ERROR_CODE,
2826
+ message
2827
+ }
2828
+ };
2629
2829
  }
2630
- function sleep2(ms) {
2631
- return new Promise((resolve) => setTimeout(resolve, ms));
2830
+ function buildAuthErrorResponse(id) {
2831
+ const message = "Action blocked: Shield API key is invalid or has been revoked. Run npx multicorn-shield init to reconfigure.";
2832
+ return {
2833
+ jsonrpc: "2.0",
2834
+ id,
2835
+ error: {
2836
+ code: AUTH_ERROR_CODE,
2837
+ message
2838
+ }
2839
+ };
2632
2840
  }
2633
- function isApiSuccessResponse(value) {
2634
- if (typeof value !== "object" || value === null) return false;
2635
- const obj = value;
2636
- return obj["success"] === true;
2841
+ function extractServiceFromToolName(toolName) {
2842
+ const idx = toolName.indexOf("_");
2843
+ return idx === -1 ? toolName : toolName.slice(0, idx);
2637
2844
  }
2638
- function isAgentSummaryShape(value) {
2639
- if (typeof value !== "object" || value === null) return false;
2640
- const obj = value;
2641
- return typeof obj["id"] === "string" && typeof obj["name"] === "string";
2845
+ function extractActionFromToolName(toolName) {
2846
+ const idx = toolName.indexOf("_");
2847
+ return idx === -1 ? "call" : toolName.slice(idx + 1);
2642
2848
  }
2643
- function isAgentDetailShape(value) {
2849
+ function isJsonRpcRequest(value) {
2644
2850
  if (typeof value !== "object" || value === null) return false;
2645
2851
  const obj = value;
2646
- return Array.isArray(obj["permissions"]);
2852
+ if (obj["jsonrpc"] !== "2.0") return false;
2853
+ if (typeof obj["method"] !== "string") return false;
2854
+ const id = obj["id"];
2855
+ const validId = id === null || id === void 0 || typeof id === "string" || typeof id === "number";
2856
+ return validId;
2647
2857
  }
2648
- function isPermissionShape(value) {
2649
- if (typeof value !== "object" || value === null) return false;
2650
- const obj = value;
2651
- return typeof obj["service"] === "string" && typeof obj["read"] === "boolean" && typeof obj["write"] === "boolean" && typeof obj["execute"] === "boolean" && (obj["revoked_at"] === null || obj["revoked_at"] === void 0 || typeof obj["revoked_at"] === "string");
2858
+ function capitalize(str) {
2859
+ if (str.length === 0) return str;
2860
+ const first = str[0];
2861
+ return first !== void 0 ? first.toUpperCase() + str.slice(1) : str;
2652
2862
  }
2653
2863
 
2654
2864
  // src/proxy/index.ts
@@ -2954,9 +3164,9 @@ function createProxyServer(config) {
2954
3164
  timer.unref();
2955
3165
  }
2956
3166
  config.logger.info("Proxy ready.", { agent: config.agentName });
2957
- return new Promise((resolve) => {
3167
+ return new Promise((resolve2) => {
2958
3168
  childProcess.on("exit", () => {
2959
- resolve();
3169
+ resolve2();
2960
3170
  });
2961
3171
  });
2962
3172
  }
@@ -3355,7 +3565,7 @@ function resolveWrapAgentName(cli, config) {
3355
3565
  const legacyPlatform = config.platform;
3356
3566
  const legacyAgentName = config.agentName;
3357
3567
  const platformKey = typeof legacyPlatform === "string" && legacyPlatform.length > 0 ? legacyPlatform : "other-mcp";
3358
- const fromPlatform = getAgentByPlatform(config, platformKey);
3568
+ const fromPlatform = getAgentByPlatform(config, platformKey, process.cwd());
3359
3569
  if (fromPlatform !== void 0) {
3360
3570
  return fromPlatform.name;
3361
3571
  }