multicorn-shield 1.10.0 → 1.11.1

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,15 +1,16 @@
1
1
  #!/usr/bin/env node
2
2
  import { createHash } from 'crypto';
3
- import { readFile, mkdir, writeFile, copyFile, chmod, unlink } from 'fs/promises';
4
- import { dirname, resolve, join, basename, sep } from 'path';
3
+ import { readFile, mkdir, writeFile, copyFile, chmod, rename, rm, unlink } from 'fs/promises';
4
+ import { dirname, resolve, basename, join, sep } from 'path';
5
5
  import { homedir } from 'os';
6
- import { spawn } from 'child_process';
7
- import { readFileSync, existsSync, statSync } from 'fs';
6
+ import { spawn, spawnSync } from 'child_process';
7
+ import { existsSync, readFileSync, mkdirSync, openSync, realpathSync, unlinkSync, writeFileSync, statSync, readdirSync } from 'fs';
8
8
  import { fileURLToPath } from 'url';
9
9
  import { createRequire } from 'module';
10
10
  import { createInterface } from 'readline';
11
11
  import { parse, stringify } from 'yaml';
12
12
  import 'stream';
13
+ import { createConnection } from 'net';
13
14
 
14
15
  var __defProp = Object.defineProperty;
15
16
  var __getOwnPropNames = Object.getOwnPropertyNames;
@@ -235,6 +236,7 @@ async function fetchGrantedScopes(agentId, apiKey, baseUrl) {
235
236
  if (perm.revoked_at !== null) continue;
236
237
  if (perm.read) scopes.push({ service: perm.service, permissionLevel: "read" });
237
238
  if (perm.write) scopes.push({ service: perm.service, permissionLevel: "write" });
239
+ if (perm.delete === true) scopes.push({ service: perm.service, permissionLevel: "delete" });
238
240
  if (perm.execute) scopes.push({ service: perm.service, permissionLevel: "execute" });
239
241
  }
240
242
  return scopes;
@@ -324,7 +326,7 @@ function detectScopeHints() {
324
326
  return [];
325
327
  }
326
328
  function sleep(ms) {
327
- return new Promise((resolve2) => setTimeout(resolve2, ms));
329
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
328
330
  }
329
331
  function isApiSuccessResponse(value) {
330
332
  if (typeof value !== "object" || value === null) return false;
@@ -1878,6 +1880,17 @@ async function warnIfApiKeyFileNotGitignored(workspaceRoot, relativePosixPath) {
1878
1880
  style.yellow("\u26A0") + " Config contains your API key. Add " + style.cyan(norm) + " to .gitignore to avoid committing credentials.\n"
1879
1881
  );
1880
1882
  }
1883
+ async function writeFileAtomic(filePath, body) {
1884
+ await mkdir(dirname(filePath), { recursive: true });
1885
+ const tmp = `${filePath}.${String(process.pid)}.${String(Date.now())}.tmp`;
1886
+ try {
1887
+ await writeFile(tmp, body, SECRET_JSON_FILE_OPTIONS);
1888
+ await rename(tmp, filePath);
1889
+ } catch (e) {
1890
+ await rm(tmp, { force: true }).catch(() => void 0);
1891
+ throw e;
1892
+ }
1893
+ }
1881
1894
  async function mergeTopLevelKeyedJsonFile(filePath, topLevelKey, shortName, entry, options) {
1882
1895
  let root = {};
1883
1896
  try {
@@ -1902,21 +1915,25 @@ async function mergeTopLevelKeyedJsonFile(filePath, topLevelKey, shortName, entr
1902
1915
  }
1903
1916
  }
1904
1917
  const bucketRaw = root[topLevelKey];
1905
- const bucket = typeof bucketRaw === "object" && bucketRaw !== null && !Array.isArray(bucketRaw) ? { ...bucketRaw } : {};
1906
- if (options.onExisting === "skip" && bucket[shortName] !== void 0) {
1918
+ const existingBucket = typeof bucketRaw === "object" && bucketRaw !== null && !Array.isArray(bucketRaw) ? bucketRaw : {};
1919
+ if (options.onExisting === "skip" && existingBucket[shortName] !== void 0) {
1907
1920
  return "unchanged";
1908
1921
  }
1922
+ const staleKeys = new Set((options.removeKeys ?? []).filter((k) => k !== shortName));
1923
+ const bucket = Object.fromEntries(
1924
+ Object.entries(existingBucket).filter(([k]) => !staleKeys.has(k))
1925
+ );
1909
1926
  bucket[shortName] = entry;
1910
1927
  root[topLevelKey] = bucket;
1911
- await mkdir(dirname(filePath), { recursive: true });
1912
- await writeFile(filePath, JSON.stringify(root, null, 2) + "\n", SECRET_JSON_FILE_OPTIONS);
1928
+ await writeFileAtomic(filePath, JSON.stringify(root, null, 2) + "\n");
1913
1929
  writeMcpAddedLine(shortName, filePath);
1914
1930
  return "ok";
1915
1931
  }
1916
- async function mergeMcpServersObjectStyle(filePath, shortName, entry) {
1932
+ async function mergeMcpServersObjectStyle(filePath, shortName, entry, removeKeys) {
1917
1933
  const result = await mergeTopLevelKeyedJsonFile(filePath, "mcpServers", shortName, entry, {
1918
1934
  stripJsonComments: false,
1919
- onExisting: "overwrite"
1935
+ onExisting: "overwrite",
1936
+ removeKeys
1920
1937
  });
1921
1938
  return result === "parse-error" ? "parse-error" : "ok";
1922
1939
  }
@@ -2651,8 +2668,8 @@ async function runInit(explicitBaseUrl, options) {
2651
2668
  process.exit(1);
2652
2669
  }
2653
2670
  const rl = createInterface({ input: process.stdin, output: process.stderr });
2654
- const ask = (question) => new Promise((resolve2) => {
2655
- rl.question(question, resolve2);
2671
+ const ask = (question) => new Promise((resolve3) => {
2672
+ rl.question(question, resolve3);
2656
2673
  });
2657
2674
  process.stderr.write("\n" + BANNER + "\n");
2658
2675
  process.stderr.write(style.dim("Agent governance for the AI era") + "\n\n");
@@ -3682,7 +3699,254 @@ You have ${String(agentsForPlatform.length)} agent(s) connected for ${selectedLa
3682
3699
  }
3683
3700
  return lastConfig;
3684
3701
  }
3685
- var SECRET_JSON_FILE_OPTIONS, style, BANNER, NativePluginPrerequisiteMissingError, CONFIG_DIR, CONFIG_PATH, OPENCLAW_CONFIG_PATH, ANSI_PATTERN, UPSTREAM_AUTH_KNOWN_SCHEME_WITH_PAYLOAD, OPENCLAW_MIN_VERSION, INIT_WIZARD_PLATFORM_REGISTRY, INIT_WIZARD_MENU_SECTIONS, INIT_WIZARD_SELECTION_MAX, INIT_EXISTING_AGENTS_PLATFORM_ACTIONS, PLATFORM_BY_SELECTION, HOSTED_PROXY_PLATFORMS_WITH_URL_KEY, OPENCODE_CONFIG_SCHEMA_URL, DEFAULT_SHIELD_API_BASE_URL;
3702
+ function clientDisplayName(client) {
3703
+ return CLIENT_DISPLAY_NAMES[client];
3704
+ }
3705
+ function clientReloadInstruction(client) {
3706
+ return CLIENT_RELOAD_INSTRUCTIONS[client];
3707
+ }
3708
+ function clientConfigPath(client, workspacePath) {
3709
+ switch (client) {
3710
+ case "cursor":
3711
+ return getCursorMcpJsonPath();
3712
+ case "cline":
3713
+ return getClineMcpSettingsPath();
3714
+ case "windsurf":
3715
+ return getWindsurfMcpConfigPath();
3716
+ case "claude":
3717
+ return getClaudeDesktopConfigPath();
3718
+ case "goose":
3719
+ return join(homedir(), ".config", "goose", "config.yaml");
3720
+ case "gemini":
3721
+ return join(homedir(), ".gemini", "settings.json");
3722
+ case "codex":
3723
+ return join(homedir(), ".codex", "config.toml");
3724
+ case "copilot":
3725
+ return join(workspacePath ?? ".", ".vscode", "mcp.json");
3726
+ case "continue":
3727
+ return join(workspacePath ?? ".", ".continue", "mcpServers");
3728
+ case "kilo":
3729
+ return join(workspacePath ?? ".", ".kilo", "kilo.jsonc");
3730
+ case "opencode":
3731
+ return join(workspacePath ?? ".", "opencode.json");
3732
+ }
3733
+ }
3734
+ function detectInstalledClients() {
3735
+ const found = [];
3736
+ const homeClients = [
3737
+ "cursor",
3738
+ "cline",
3739
+ "windsurf",
3740
+ "claude",
3741
+ "goose",
3742
+ "gemini",
3743
+ "codex"
3744
+ ];
3745
+ for (const client of homeClients) {
3746
+ const p = clientConfigPath(client);
3747
+ if (existsSync(p)) {
3748
+ found.push(client);
3749
+ } else {
3750
+ const dir = dirname(p);
3751
+ if (existsSync(dir)) {
3752
+ found.push(client);
3753
+ }
3754
+ }
3755
+ }
3756
+ return found;
3757
+ }
3758
+ async function writeLocalMcpEntry(client, agentName, localProxyUrl, apiKey, workspacePath) {
3759
+ const filePath = clientConfigPath(client, workspacePath);
3760
+ const entryKey = agentName;
3761
+ const legacyKeys = [`${agentName}-files`];
3762
+ if (apiKey.trim().length === 0) {
3763
+ process.stderr.write(
3764
+ style.yellow("\u26A0") + ` Refusing to write MCP config for "${agentName}": missing API key. Left the config file unchanged.
3765
+ `
3766
+ );
3767
+ return null;
3768
+ }
3769
+ const authHeader = `Bearer ${apiKey}`;
3770
+ const url = shouldEmbedKeyInHostedProxyUrl(CODING_CLIENT_TO_PLATFORM[client]) ? hostedProxyUrlWithKeyParam(localProxyUrl, apiKey) : localProxyUrl;
3771
+ switch (client) {
3772
+ case "cursor":
3773
+ return await mergeMcpServersObjectStyle(
3774
+ filePath,
3775
+ entryKey,
3776
+ {
3777
+ url,
3778
+ headers: { Authorization: authHeader }
3779
+ },
3780
+ legacyKeys
3781
+ ) === "ok" ? filePath : null;
3782
+ case "cline":
3783
+ return await mergeMcpServersObjectStyle(
3784
+ filePath,
3785
+ entryKey,
3786
+ {
3787
+ url
3788
+ },
3789
+ legacyKeys
3790
+ ) === "ok" ? filePath : null;
3791
+ case "windsurf":
3792
+ return await mergeMcpServersObjectStyle(
3793
+ filePath,
3794
+ entryKey,
3795
+ {
3796
+ serverUrl: url,
3797
+ headers: { Authorization: authHeader }
3798
+ },
3799
+ legacyKeys
3800
+ ) === "ok" ? filePath : null;
3801
+ case "claude":
3802
+ return await mergeMcpServersObjectStyle(
3803
+ filePath,
3804
+ entryKey,
3805
+ {
3806
+ url,
3807
+ headers: { Authorization: authHeader }
3808
+ },
3809
+ legacyKeys
3810
+ ) === "ok" ? filePath : null;
3811
+ case "gemini": {
3812
+ return await mergeMcpServersObjectStyle(
3813
+ filePath,
3814
+ entryKey,
3815
+ {
3816
+ httpUrl: url,
3817
+ headers: { Authorization: authHeader }
3818
+ },
3819
+ legacyKeys
3820
+ ) === "ok" ? filePath : null;
3821
+ }
3822
+ case "copilot": {
3823
+ const result = await mergeTopLevelKeyedJsonFile(
3824
+ filePath,
3825
+ "servers",
3826
+ entryKey,
3827
+ { type: "http", url, headers: { Authorization: authHeader } },
3828
+ { stripJsonComments: false, onExisting: "overwrite", removeKeys: legacyKeys }
3829
+ );
3830
+ return result === "ok" ? filePath : null;
3831
+ }
3832
+ case "goose": {
3833
+ const goosePath = filePath;
3834
+ let content = "";
3835
+ try {
3836
+ content = await readFile(goosePath, "utf8");
3837
+ } catch (e) {
3838
+ if (isErrnoException(e) && e.code === "ENOENT") {
3839
+ content = "";
3840
+ } else {
3841
+ return null;
3842
+ }
3843
+ }
3844
+ let root;
3845
+ try {
3846
+ const data = content.trim().length === 0 ? {} : parse(content);
3847
+ if (data === null || typeof data !== "object" || Array.isArray(data)) return null;
3848
+ root = data;
3849
+ } catch {
3850
+ return null;
3851
+ }
3852
+ const extensionsRaw = root["extensions"];
3853
+ let extensions;
3854
+ if (isYamlPlainObject(extensionsRaw)) {
3855
+ extensions = { ...extensionsRaw };
3856
+ } else if (extensionsRaw === void 0) {
3857
+ extensions = {};
3858
+ } else {
3859
+ return null;
3860
+ }
3861
+ extensions[entryKey] = {
3862
+ enabled: true,
3863
+ type: "streamable_http",
3864
+ name: entryKey,
3865
+ description: "",
3866
+ uri: url,
3867
+ envs: {},
3868
+ env_keys: [],
3869
+ headers: { Authorization: authHeader },
3870
+ timeout: 300,
3871
+ socket: null,
3872
+ bundled: null,
3873
+ available_tools: []
3874
+ };
3875
+ root["extensions"] = extensions;
3876
+ const out = stringify(root, { indent: 2, lineWidth: 0 });
3877
+ const body = out.endsWith("\n") ? out : `${out}
3878
+ `;
3879
+ await mkdir(dirname(goosePath), { recursive: true });
3880
+ await writeFile(goosePath, body, SECRET_JSON_FILE_OPTIONS);
3881
+ return goosePath;
3882
+ }
3883
+ case "codex": {
3884
+ const codexPath = filePath;
3885
+ let existing = "";
3886
+ try {
3887
+ existing = await readFile(codexPath, "utf8");
3888
+ } catch (e) {
3889
+ if (isErrnoException(e) && e.code === "ENOENT") {
3890
+ existing = "";
3891
+ } else {
3892
+ return null;
3893
+ }
3894
+ }
3895
+ const sectionHeader = `[mcp_servers.${entryKey}]`;
3896
+ const sectionBlock = `${sectionHeader}
3897
+ type = "http"
3898
+ url = "${url}"
3899
+
3900
+ [mcp_servers.${entryKey}.http_headers]
3901
+ Authorization = "${authHeader}"
3902
+ `;
3903
+ if (existing.includes(sectionHeader)) {
3904
+ const idx = existing.indexOf(sectionHeader);
3905
+ const nextSection = existing.indexOf("\n[", idx + sectionHeader.length);
3906
+ const before = existing.slice(0, idx);
3907
+ const after = nextSection >= 0 ? existing.slice(nextSection + 1) : "";
3908
+ existing = before + sectionBlock + (after.length > 0 ? "\n" + after : "");
3909
+ } else {
3910
+ existing = existing.trimEnd() + "\n\n" + sectionBlock;
3911
+ }
3912
+ await mkdir(dirname(codexPath), { recursive: true });
3913
+ await writeFile(codexPath, existing, SECRET_JSON_FILE_OPTIONS);
3914
+ return codexPath;
3915
+ }
3916
+ case "continue": {
3917
+ const dir = join(workspacePath ?? ".", ".continue", "mcpServers");
3918
+ const continuePath = join(dir, `${entryKey}.yaml`);
3919
+ const yamlContent = stringify(
3920
+ { name: entryKey, type: "streamableHttp", url },
3921
+ { indent: 2, lineWidth: 0 }
3922
+ );
3923
+ await mkdir(dir, { recursive: true });
3924
+ await writeFile(continuePath, yamlContent, SECRET_JSON_FILE_OPTIONS);
3925
+ return continuePath;
3926
+ }
3927
+ case "kilo": {
3928
+ const result = await mergeTopLevelKeyedJsonFile(
3929
+ filePath,
3930
+ "mcp",
3931
+ entryKey,
3932
+ { url, headers: { Authorization: authHeader } },
3933
+ { stripJsonComments: true, onExisting: "overwrite", removeKeys: legacyKeys }
3934
+ );
3935
+ return result === "ok" ? filePath : null;
3936
+ }
3937
+ case "opencode": {
3938
+ const result = await mergeTopLevelKeyedJsonFile(
3939
+ filePath,
3940
+ "mcp",
3941
+ entryKey,
3942
+ { type: "streamablehttp", url, headers: { Authorization: authHeader } },
3943
+ { stripJsonComments: false, onExisting: "overwrite", removeKeys: legacyKeys }
3944
+ );
3945
+ return result === "ok" ? filePath : null;
3946
+ }
3947
+ }
3948
+ }
3949
+ var SECRET_JSON_FILE_OPTIONS, style, BANNER, NativePluginPrerequisiteMissingError, CONFIG_DIR, CONFIG_PATH, OPENCLAW_CONFIG_PATH, ANSI_PATTERN, UPSTREAM_AUTH_KNOWN_SCHEME_WITH_PAYLOAD, OPENCLAW_MIN_VERSION, INIT_WIZARD_PLATFORM_REGISTRY, INIT_WIZARD_MENU_SECTIONS, INIT_WIZARD_SELECTION_MAX, INIT_EXISTING_AGENTS_PLATFORM_ACTIONS, PLATFORM_BY_SELECTION, HOSTED_PROXY_PLATFORMS_WITH_URL_KEY, OPENCODE_CONFIG_SCHEMA_URL, DEFAULT_SHIELD_API_BASE_URL, CODING_CLIENTS, CLIENT_DISPLAY_NAMES, CLIENT_RELOAD_INSTRUCTIONS, CODING_CLIENT_TO_PLATFORM;
3686
3950
  var init_config = __esm({
3687
3951
  "src/proxy/config.ts"() {
3688
3952
  init_consent();
@@ -3802,6 +4066,58 @@ var init_config = __esm({
3802
4066
  ]);
3803
4067
  OPENCODE_CONFIG_SCHEMA_URL = "https://opencode.ai/config.json";
3804
4068
  DEFAULT_SHIELD_API_BASE_URL = "https://api.multicorn.ai";
4069
+ CODING_CLIENTS = [
4070
+ "cursor",
4071
+ "cline",
4072
+ "windsurf",
4073
+ "claude",
4074
+ "copilot",
4075
+ "goose",
4076
+ "gemini",
4077
+ "codex",
4078
+ "continue",
4079
+ "kilo",
4080
+ "opencode"
4081
+ ];
4082
+ CLIENT_DISPLAY_NAMES = {
4083
+ cursor: "Cursor",
4084
+ cline: "Cline",
4085
+ windsurf: "Windsurf",
4086
+ claude: "Claude Desktop",
4087
+ copilot: "GitHub Copilot",
4088
+ goose: "Goose",
4089
+ gemini: "Gemini CLI",
4090
+ codex: "Codex CLI",
4091
+ continue: "Continue",
4092
+ kilo: "Kilo Code",
4093
+ opencode: "OpenCode"
4094
+ };
4095
+ CLIENT_RELOAD_INSTRUCTIONS = {
4096
+ cursor: "Restart Cursor or reload the window to pick up the new server.",
4097
+ cline: "Cline picks up config changes automatically.",
4098
+ windsurf: "Restart Windsurf to pick up the new server.",
4099
+ claude: "Restart Claude Desktop to pick up the new server.",
4100
+ copilot: "Reload the VS Code window (Cmd+Shift+P > Reload Window).",
4101
+ goose: "Restart Goose to pick up the new extension.",
4102
+ gemini: "Restart Gemini CLI to pick up the new server.",
4103
+ codex: "Restart Codex CLI to pick up the new server.",
4104
+ continue: "Continue picks up config changes automatically.",
4105
+ kilo: "Kilo Code picks up config changes automatically.",
4106
+ opencode: "Restart OpenCode to pick up the new server."
4107
+ };
4108
+ CODING_CLIENT_TO_PLATFORM = {
4109
+ cursor: "cursor",
4110
+ cline: "cline",
4111
+ windsurf: "windsurf",
4112
+ claude: "claude-desktop",
4113
+ copilot: "github-copilot",
4114
+ goose: "goose",
4115
+ gemini: "gemini-cli",
4116
+ codex: "codex-cli",
4117
+ continue: "continue-dev",
4118
+ kilo: "kilo-code",
4119
+ opencode: "opencode"
4120
+ };
3805
4121
  }
3806
4122
  });
3807
4123
 
@@ -3812,6 +4128,7 @@ var init_types = __esm({
3812
4128
  PERMISSION_LEVELS = {
3813
4129
  Read: "read",
3814
4130
  Write: "write",
4131
+ Delete: "delete",
3815
4132
  Execute: "execute",
3816
4133
  Publish: "publish",
3817
4134
  Create: "create"
@@ -4034,7 +4351,7 @@ function createActionLogger(config) {
4034
4351
  };
4035
4352
  }
4036
4353
  function sleep2(ms) {
4037
- return new Promise((resolve2) => setTimeout(resolve2, ms));
4354
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
4038
4355
  }
4039
4356
  var init_action_logger = __esm({
4040
4357
  "src/logger/action-logger.ts"() {
@@ -4602,9 +4919,9 @@ function createProxyServer(config) {
4602
4919
  timer.unref();
4603
4920
  }
4604
4921
  config.logger.info("Proxy ready.", { agent: config.agentName });
4605
- return new Promise((resolve2) => {
4922
+ return new Promise((resolve3) => {
4606
4923
  childProcess.on("exit", () => {
4607
- resolve2();
4924
+ resolve3();
4608
4925
  });
4609
4926
  });
4610
4927
  }
@@ -4745,7 +5062,7 @@ var init_package = __esm({
4745
5062
  "package.json"() {
4746
5063
  package_default = {
4747
5064
  name: "multicorn-shield",
4748
- version: "1.10.0",
5065
+ version: "1.11.1",
4749
5066
  description: "The control layer for AI agents: permissions, consent, spending limits, and audit logging.",
4750
5067
  license: "MIT",
4751
5068
  author: "Multicorn AI Pty Ltd",
@@ -4805,7 +5122,7 @@ var init_package = __esm({
4805
5122
  node: ">=20"
4806
5123
  },
4807
5124
  scripts: {
4808
- build: "tsup",
5125
+ build: "tsup && node scripts/stage-local-proxy-server.mjs",
4809
5126
  dev: "tsup --watch",
4810
5127
  lint: "eslint . --no-warn-ignored && prettier --check .",
4811
5128
  "lint:fix": "eslint --fix . --no-warn-ignored && prettier --write .",
@@ -4836,9 +5153,11 @@ var init_package = __esm({
4836
5153
  ]
4837
5154
  },
4838
5155
  dependencies: {
4839
- "@modelcontextprotocol/sdk": "^1.27.1",
4840
5156
  "@lit/reactive-element": "^2.0.0",
5157
+ "@modelcontextprotocol/sdk": "^1.27.1",
5158
+ "@modelcontextprotocol/server-filesystem": "^2026.1.14",
4841
5159
  lit: "^3.2.0",
5160
+ supergateway: "^3.4.3",
4842
5161
  yaml: "^2.8.2",
4843
5162
  zod: "^4.3.6"
4844
5163
  },
@@ -4913,8 +5232,11 @@ var init_package = __esm({
4913
5232
  rollup: ">=4.59.0",
4914
5233
  picomatch: ">=4.0.4",
4915
5234
  "path-to-regexp": ">=8.4.0",
5235
+ "express>path-to-regexp": ">=0.1.13",
4916
5236
  "node-forge": ">=1.4.0",
4917
- "fast-uri": ">=3.1.2"
5237
+ "fast-uri": ">=3.1.2",
5238
+ hono: ">=4.12.25",
5239
+ ws: ">=8.21.0"
4918
5240
  }
4919
5241
  }
4920
5242
  };
@@ -4929,180 +5251,1332 @@ var init_package_meta = __esm({
4929
5251
  PACKAGE_VERSION = package_default.version;
4930
5252
  }
4931
5253
  });
4932
-
4933
- // bin/multicorn-shield.ts
4934
- var multicorn_shield_exports = {};
4935
- __export(multicorn_shield_exports, {
4936
- parseArgs: () => parseArgs,
4937
- resolveWrapConfig: () => resolveWrapConfig,
4938
- runCli: () => runCli
4939
- });
4940
- function parseArgs(argv) {
4941
- const args = argv.slice(2);
4942
- let subcommand = "help";
4943
- let wrapCommand = "";
4944
- let wrapArgs = [];
4945
- let logLevel = "info";
4946
- let baseUrl = void 0;
4947
- let dashboardUrl = "";
4948
- let agentName = "";
4949
- let deleteAgentName = "";
4950
- let apiKey = void 0;
4951
- let verbose = false;
4952
- for (let i = 0; i < args.length; i++) {
4953
- const arg = args[i];
4954
- if (arg === "init") {
4955
- subcommand = "init";
4956
- } else if (arg === "agents") {
4957
- subcommand = "agents";
4958
- } else if (arg === "delete-agent") {
4959
- subcommand = "delete-agent";
4960
- const name = args[i + 1];
4961
- if (name === void 0 || name.startsWith("-")) {
4962
- process.stderr.write("Error: delete-agent requires an agent name.\n");
4963
- process.stderr.write("Example: npx multicorn-shield delete-agent my-agent\n");
4964
- process.exit(1);
4965
- }
4966
- deleteAgentName = name;
4967
- i++;
4968
- } else if (arg === "--wrap") {
4969
- subcommand = "wrap";
4970
- const tail = args.slice(i + 1);
4971
- const remaining = [];
4972
- for (let j = 0; j < tail.length; j++) {
4973
- const token = tail[j];
4974
- if (token === void 0) continue;
4975
- if (remaining.length > 0) {
4976
- remaining.push(token);
4977
- continue;
4978
- }
4979
- if (token === "--agent-name") {
4980
- const value = tail[j + 1];
4981
- if (value !== void 0) {
4982
- agentName = value;
4983
- j++;
4984
- }
4985
- } else if (token === "--log-level") {
4986
- const value = tail[j + 1];
4987
- if (value !== void 0 && isValidLogLevel(value)) {
4988
- logLevel = value;
4989
- j++;
4990
- }
4991
- } else if (token === "--base-url") {
4992
- const value = tail[j + 1];
4993
- if (value !== void 0) {
4994
- baseUrl = value;
4995
- j++;
4996
- }
4997
- } else if (token === "--dashboard-url") {
4998
- const value = tail[j + 1];
4999
- if (value !== void 0) {
5000
- dashboardUrl = value;
5001
- j++;
5002
- }
5003
- } else if (token === "--api-key") {
5004
- const value = tail[j + 1];
5005
- if (value !== void 0) {
5006
- apiKey = value;
5007
- j++;
5008
- }
5009
- } else {
5010
- remaining.push(token);
5254
+ function findMulticornShieldPackageRoot(moduleDir = dirname(fileURLToPath(import.meta.url))) {
5255
+ let current = moduleDir;
5256
+ for (; ; ) {
5257
+ const pkgPath = join(current, "package.json");
5258
+ if (existsSync(pkgPath)) {
5259
+ try {
5260
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
5261
+ if (pkg.name === "multicorn-shield") {
5262
+ return current;
5011
5263
  }
5264
+ } catch {
5012
5265
  }
5013
- if (remaining.length === 0) {
5014
- process.stderr.write("Error: --wrap requires a command to run.\n");
5015
- process.stderr.write("Example: npx multicorn-shield --wrap my-mcp-server\n");
5016
- process.exit(1);
5017
- }
5018
- wrapCommand = remaining[0] ?? "";
5019
- wrapArgs = remaining.slice(1);
5266
+ }
5267
+ const parent = dirname(current);
5268
+ if (parent === current) {
5020
5269
  break;
5021
- } else if (arg === "--log-level") {
5022
- const next = args[i + 1];
5023
- if (next !== void 0 && isValidLogLevel(next)) {
5024
- logLevel = next;
5025
- i++;
5026
- }
5027
- } else if (arg === "--base-url") {
5028
- const next = args[i + 1];
5029
- if (next !== void 0) {
5030
- baseUrl = next;
5031
- i++;
5032
- }
5033
- } else if (arg === "--dashboard-url") {
5034
- const next = args[i + 1];
5035
- if (next !== void 0) {
5036
- dashboardUrl = next;
5037
- i++;
5038
- }
5039
- } else if (arg === "--agent-name") {
5040
- const next = args[i + 1];
5041
- if (next !== void 0) {
5042
- agentName = next;
5043
- i++;
5044
- }
5045
- } else if (arg === "--api-key") {
5046
- const next = args[i + 1];
5047
- if (next !== void 0) {
5048
- apiKey = next;
5049
- i++;
5050
- }
5051
- } else if (arg === "--verbose" || arg === "--debug") {
5052
- verbose = true;
5053
5270
  }
5271
+ current = parent;
5272
+ }
5273
+ try {
5274
+ const req = createRequire(import.meta.url);
5275
+ return dirname(req.resolve("multicorn-shield/package.json"));
5276
+ } catch {
5277
+ throw new Error(NOT_FOUND_MESSAGE);
5278
+ }
5279
+ }
5280
+ function resolveLocalProxyServerEntry(options) {
5281
+ const moduleDir = dirname(fileURLToPath(import.meta.url));
5282
+ const candidates = [
5283
+ join(findMulticornShieldPackageRoot(moduleDir), "dist", "server.js")
5284
+ ];
5285
+ try {
5286
+ const req = createRequire(import.meta.url);
5287
+ const proxyRoot = dirname(req.resolve("multicorn-proxy/package.json"));
5288
+ candidates.unshift(join(proxyRoot, "dist", "server.js"));
5289
+ } catch {
5290
+ }
5291
+ for (const path of candidates) {
5292
+ if (existsSync(path)) return path;
5054
5293
  }
5294
+ throw new Error(NOT_FOUND_MESSAGE);
5295
+ }
5296
+ function buildLocalProxySpawnEnv(port, apiBaseUrl) {
5055
5297
  return {
5056
- subcommand,
5057
- wrapCommand,
5058
- wrapArgs,
5059
- logLevel,
5060
- baseUrl,
5061
- dashboardUrl,
5062
- agentName,
5063
- deleteAgentName,
5064
- apiKey,
5065
- verbose
5298
+ PORT: String(port),
5299
+ HOST: "127.0.0.1",
5300
+ SHIELD_API_BASE_URL: apiBaseUrl,
5301
+ ALLOW_PRIVATE_TARGETS: "true"
5066
5302
  };
5067
5303
  }
5068
- function printHelp() {
5069
- process.stderr.write(
5070
- [
5071
- "multicorn-shield: MCP permission proxy and Shield setup",
5072
- "",
5073
- "Usage:",
5074
- " npx multicorn-shield init",
5075
- " Interactive setup. Saves API key to ~/.multicorn/config.json.",
5076
- "",
5077
- " npx multicorn-shield restore",
5078
- " Restore MCP servers in claude_desktop_config.json from the Shield extension backup.",
5079
- "",
5080
- " npx multicorn-shield agents",
5081
- " List configured agents and show which is the default.",
5082
- "",
5083
- " npx multicorn-shield delete-agent <name>",
5084
- " Remove a saved agent.",
5085
- "",
5086
- " npx multicorn-shield --wrap <command> [args...]",
5087
- " Start <command> as an MCP server and proxy all tool calls through",
5088
- " Shield's permission layer.",
5089
- "",
5090
- "Options:",
5091
- " --version, -v Print version and exit",
5092
- " --verbose, --debug Print extra diagnostics during init (menu selection, agent counts)",
5093
- " --api-key <key> Multicorn API key (overrides MULTICORN_API_KEY env var and config file)",
5094
- " --log-level <level> Log level: debug | info | warn | error (default: info)",
5095
- " --base-url <url> Multicorn API base URL (default: https://api.multicorn.ai)",
5096
- " --dashboard-url <url> Dashboard URL for consent page (default: derived from --base-url)",
5097
- " --agent-name <name> Override agent name derived from the wrapped command",
5098
- "",
5099
- "Examples:",
5100
- " npx multicorn-shield init",
5101
- " npx multicorn-shield --wrap npx @modelcontextprotocol/server-filesystem /tmp",
5102
- " npx multicorn-shield --wrap my-mcp-server --log-level debug",
5103
- ""
5104
- ].join("\n")
5105
- );
5304
+ function buildLocalProxySpawnCommand(port, apiBaseUrl, execPath = process.execPath, serverEntryPath = resolveLocalProxyServerEntry()) {
5305
+ const env = buildLocalProxySpawnEnv(port, apiBaseUrl);
5306
+ return {
5307
+ executable: execPath,
5308
+ args: [serverEntryPath],
5309
+ env,
5310
+ serverEntryPath
5311
+ };
5312
+ }
5313
+ function readProxyLogTail(logPath, maxBytes = 8e3) {
5314
+ try {
5315
+ const content = readFileSync(logPath, "utf8");
5316
+ if (content.length <= maxBytes) return content;
5317
+ return content.slice(-maxBytes);
5318
+ } catch {
5319
+ return "";
5320
+ }
5321
+ }
5322
+ function formatLocalProxyStartError(port, logPath, childExited, exitCode) {
5323
+ const logTail = readProxyLogTail(logPath);
5324
+ const reason = childExited ? `Proxy process exited early${exitCode === null ? "" : ` (code ${String(exitCode)})`}.` : `Proxy did not become ready on port ${String(port)} within the timeout.`;
5325
+ let message = `Could not start the local proxy on port ${String(port)}. ${reason}`;
5326
+ message += logTail.length > 0 ? `
5327
+
5328
+ Output from ${logPath}:
5329
+ ${logTail.trimEnd()}` : `
5330
+
5331
+ See ${logPath} for details.`;
5332
+ return message;
5333
+ }
5334
+ var LOCAL_PROXY_READY_POLL_MS, LOCAL_PROXY_READY_MAX_POLLS, NOT_FOUND_MESSAGE;
5335
+ var init_local_proxy_start = __esm({
5336
+ "src/commands/local-proxy-start.ts"() {
5337
+ LOCAL_PROXY_READY_POLL_MS = 500;
5338
+ LOCAL_PROXY_READY_MAX_POLLS = 30;
5339
+ NOT_FOUND_MESSAGE = "Local proxy server entry (dist/server.js) not found. Reinstall multicorn-shield or run pnpm build.";
5340
+ }
5341
+ });
5342
+ function pidfilePath(agent) {
5343
+ return join(PIDFILE_DIR, `files-${agent}.pid`);
5344
+ }
5345
+ function writePidfile(data) {
5346
+ mkdirSync(PIDFILE_DIR, { recursive: true, mode: 448 });
5347
+ writeFileSync(pidfilePath(data.agent), JSON.stringify(data), {
5348
+ encoding: "utf8",
5349
+ mode: 384
5350
+ });
5351
+ }
5352
+ function readPidfile(agent) {
5353
+ const p = pidfilePath(agent);
5354
+ if (!existsSync(p)) return null;
5355
+ try {
5356
+ return JSON.parse(readFileSync(p, "utf8"));
5357
+ } catch {
5358
+ return null;
5359
+ }
5360
+ }
5361
+ function removePidfile(agent) {
5362
+ const p = pidfilePath(agent);
5363
+ try {
5364
+ unlinkSync(p);
5365
+ } catch {
5366
+ }
5367
+ }
5368
+ function isProcessAlive(pid) {
5369
+ try {
5370
+ process.kill(pid, 0);
5371
+ return true;
5372
+ } catch {
5373
+ return false;
5374
+ }
5375
+ }
5376
+ function listAllPidfiles() {
5377
+ if (!existsSync(PIDFILE_DIR)) return [];
5378
+ const files = readdirSync(PIDFILE_DIR).filter(
5379
+ (f) => f.startsWith("files-") && f.endsWith(".pid")
5380
+ );
5381
+ const results = [];
5382
+ for (const f of files) {
5383
+ try {
5384
+ const data = JSON.parse(readFileSync(join(PIDFILE_DIR, f), "utf8"));
5385
+ results.push(data);
5386
+ } catch {
5387
+ }
5388
+ }
5389
+ return results;
5390
+ }
5391
+ function runStatus() {
5392
+ const sessions = listAllPidfiles();
5393
+ if (sessions.length === 0) {
5394
+ process.stderr.write("No active file-sharing sessions.\n");
5395
+ return;
5396
+ }
5397
+ const proxyReg = readProxyRegistry();
5398
+ const fsReg = readFsRegistry();
5399
+ process.stderr.write("Active sessions:\n\n");
5400
+ for (const s of sessions) {
5401
+ const supervisorAlive = typeof s.supervisorPid === "number" && isProcessAlive(s.supervisorPid);
5402
+ const fsEntry = fsReg[s.dir];
5403
+ const fsAlive = fsEntry !== void 0 && isProcessAlive(fsEntry.pid);
5404
+ const proxyAlive = proxyReg !== null && proxyReg.port === s.proxyPort && isProcessAlive(proxyReg.pid);
5405
+ const agentStatus = supervisorAlive ? style2.green("running") : style2.dim("stopped");
5406
+ process.stderr.write(` ${style2.bold(s.agent)} ${agentStatus}
5407
+ `);
5408
+ process.stderr.write(` Folder: ${s.dir || "(unknown)"}
5409
+ `);
5410
+ process.stderr.write(
5411
+ ` FS server: :${String(s.fsPort)} ${fsAlive ? style2.green("running") : style2.dim("stopped")}
5412
+ `
5413
+ );
5414
+ process.stderr.write(
5415
+ ` Proxy: :${String(s.proxyPort)} ${proxyAlive ? style2.green("running") : style2.dim("stopped")}
5416
+ `
5417
+ );
5418
+ process.stderr.write("\n");
5419
+ if (!supervisorAlive) {
5420
+ removePidfile(s.agent);
5421
+ process.stderr.write(style2.dim(` (cleaned up stale pidfile)
5422
+
5423
+ `));
5424
+ }
5425
+ }
5426
+ }
5427
+ async function isPortListening(port) {
5428
+ return new Promise((resolve3) => {
5429
+ const sock = createConnection({ port, host: "127.0.0.1" });
5430
+ sock.once("connect", () => {
5431
+ sock.destroy();
5432
+ resolve3(true);
5433
+ });
5434
+ sock.once("error", () => {
5435
+ sock.destroy();
5436
+ resolve3(false);
5437
+ });
5438
+ });
5439
+ }
5440
+ async function probeProxyHealth(port) {
5441
+ try {
5442
+ const resp = await fetch(`http://127.0.0.1:${String(port)}/health`, {
5443
+ signal: AbortSignal.timeout(2e3)
5444
+ });
5445
+ return resp.ok;
5446
+ } catch {
5447
+ return false;
5448
+ }
5449
+ }
5450
+ async function readProxyVersion(port) {
5451
+ try {
5452
+ const resp = await fetch(`http://127.0.0.1:${String(port)}/health`, {
5453
+ signal: AbortSignal.timeout(2e3)
5454
+ });
5455
+ if (!resp.ok) return null;
5456
+ const body = await resp.json();
5457
+ return typeof body.version === "string" && body.version.length > 0 ? body.version : null;
5458
+ } catch {
5459
+ return null;
5460
+ }
5461
+ }
5462
+ function sleep3(ms) {
5463
+ return new Promise((r) => setTimeout(r, ms));
5464
+ }
5465
+ function readJsonFile(path) {
5466
+ if (!existsSync(path)) return null;
5467
+ try {
5468
+ return JSON.parse(readFileSync(path, "utf8"));
5469
+ } catch {
5470
+ return null;
5471
+ }
5472
+ }
5473
+ function writeJsonFile(path, data) {
5474
+ mkdirSync(PIDFILE_DIR, { recursive: true, mode: 448 });
5475
+ writeFileSync(path, JSON.stringify(data), { encoding: "utf8", mode: 384 });
5476
+ }
5477
+ function readProxyRegistry() {
5478
+ return readJsonFile(PROXY_REGISTRY);
5479
+ }
5480
+ function readFsRegistry() {
5481
+ return readJsonFile(FS_REGISTRY) ?? {};
5482
+ }
5483
+ function canonicalFolder(dir) {
5484
+ const abs = resolve(dir);
5485
+ try {
5486
+ return realpathSync(abs);
5487
+ } catch {
5488
+ return abs;
5489
+ }
5490
+ }
5491
+ function readLock() {
5492
+ return readJsonFile(LOCK_PATH);
5493
+ }
5494
+ function isLockStale(holder, now = Date.now()) {
5495
+ if (holder === null) return true;
5496
+ if (!isProcessAlive(holder.pid)) return true;
5497
+ return now - holder.ts > LOCK_STALE_MS;
5498
+ }
5499
+ async function acquireResourceLock() {
5500
+ mkdirSync(PIDFILE_DIR, { recursive: true, mode: 448 });
5501
+ const deadline = Date.now() + LOCK_WAIT_MS;
5502
+ for (; ; ) {
5503
+ try {
5504
+ writeFileSync(LOCK_PATH, JSON.stringify({ pid: process.pid, ts: Date.now() }), {
5505
+ encoding: "utf8",
5506
+ mode: 384,
5507
+ flag: "wx"
5508
+ });
5509
+ return;
5510
+ } catch {
5511
+ const holder = readLock();
5512
+ if (isLockStale(holder)) {
5513
+ try {
5514
+ unlinkSync(LOCK_PATH);
5515
+ } catch {
5516
+ }
5517
+ continue;
5518
+ }
5519
+ if (Date.now() > deadline) {
5520
+ try {
5521
+ unlinkSync(LOCK_PATH);
5522
+ } catch {
5523
+ }
5524
+ continue;
5525
+ }
5526
+ await sleep3(100);
5527
+ }
5528
+ }
5529
+ }
5530
+ function releaseResourceLock() {
5531
+ const holder = readLock();
5532
+ if (holder !== null && holder.pid === process.pid) {
5533
+ try {
5534
+ unlinkSync(LOCK_PATH);
5535
+ } catch {
5536
+ }
5537
+ }
5538
+ }
5539
+ async function withResourceLock(fn) {
5540
+ await acquireResourceLock();
5541
+ try {
5542
+ return await fn();
5543
+ } finally {
5544
+ releaseResourceLock();
5545
+ }
5546
+ }
5547
+ function liveAgents() {
5548
+ return listAllPidfiles().filter(
5549
+ (p) => typeof p.supervisorPid === "number" && isProcessAlive(p.supervisorPid)
5550
+ );
5551
+ }
5552
+ function agentsReferencingProxy(proxyPort, agents, excludeAgent) {
5553
+ return agents.filter((a) => a.agent !== excludeAgent && a.proxyPort === proxyPort);
5554
+ }
5555
+ function agentsReferencingFolder(folder, agents, excludeAgent) {
5556
+ return agents.filter((a) => a.agent !== excludeAgent && a.dir === folder);
5557
+ }
5558
+ async function nextFreePort(start, claimed, isBusy, range = FS_PORT_SCAN_RANGE) {
5559
+ for (let port = start; port < start + range; port++) {
5560
+ if (claimed.has(port)) continue;
5561
+ if (await isBusy(port)) continue;
5562
+ return port;
5563
+ }
5564
+ throw new Error(
5565
+ `No free port for the filesystem server in range ${String(start)}-${String(start + range)}.`
5566
+ );
5567
+ }
5568
+ async function resolveConfig(opts) {
5569
+ const fromFlag = opts.apiKey;
5570
+ if (fromFlag && fromFlag.length > 0) {
5571
+ return {
5572
+ apiKey: fromFlag,
5573
+ baseUrl: opts.baseUrl ?? DEFAULT_SHIELD_API_BASE_URL
5574
+ };
5575
+ }
5576
+ const envKey = process.env["MULTICORN_API_KEY"];
5577
+ if (typeof envKey === "string" && envKey.length > 0) {
5578
+ return {
5579
+ apiKey: envKey,
5580
+ baseUrl: opts.baseUrl ?? DEFAULT_SHIELD_API_BASE_URL
5581
+ };
5582
+ }
5583
+ const config = await loadConfig();
5584
+ if (config !== null) {
5585
+ return {
5586
+ apiKey: config.apiKey,
5587
+ baseUrl: opts.baseUrl ?? config.baseUrl
5588
+ };
5589
+ }
5590
+ process.stderr.write(
5591
+ "No API key found. Pass --api-key <key> or add it to ~/.multicorn/config.json.\n"
5592
+ );
5593
+ process.exit(1);
5594
+ }
5595
+ function startFsServerDetached(realDir, port) {
5596
+ mkdirSync(PIDFILE_DIR, { recursive: true, mode: 448 });
5597
+ const logFile = join(PIDFILE_DIR, `fs-${String(port)}.log`);
5598
+ const out = openSync(logFile, "a");
5599
+ const err = openSync(logFile, "a");
5600
+ const child = spawn(
5601
+ "npx",
5602
+ [
5603
+ "supergateway",
5604
+ // Serve the child over streamable HTTP at /mcp. Without this, supergateway
5605
+ // defaults to SSE (/sse + /message), but the proxy registers the target as
5606
+ // /mcp - the mismatch makes every request 404 with "Cannot POST /mcp".
5607
+ "--outputTransport",
5608
+ "streamableHttp",
5609
+ "--stdio",
5610
+ `npx @modelcontextprotocol/server-filesystem ${realDir}`,
5611
+ "--port",
5612
+ String(port)
5613
+ ],
5614
+ {
5615
+ stdio: ["ignore", out, err],
5616
+ detached: true,
5617
+ env: { ...process.env }
5618
+ }
5619
+ );
5620
+ child.unref();
5621
+ return child;
5622
+ }
5623
+ function startLocalProxyDetached(port, apiBaseUrl) {
5624
+ mkdirSync(PIDFILE_DIR, { recursive: true, mode: 448 });
5625
+ const logFile = join(PIDFILE_DIR, "proxy.log");
5626
+ const out = openSync(logFile, "a");
5627
+ const err = openSync(logFile, "a");
5628
+ const { executable, args, env: proxyEnv } = buildLocalProxySpawnCommand(port, apiBaseUrl);
5629
+ const child = spawn(executable, [...args], {
5630
+ stdio: ["ignore", out, err],
5631
+ detached: true,
5632
+ env: {
5633
+ ...process.env,
5634
+ ...proxyEnv
5635
+ // SAFETY: blanket-allow for private targets is acceptable ONLY because
5636
+ // this is a local single-user proxy. The only registered target is the
5637
+ // filesystem server on the same machine. Do NOT copy this pattern to a
5638
+ // multi-tenant or hosted proxy deployment.
5639
+ }
5640
+ });
5641
+ child.unref();
5642
+ return child;
5643
+ }
5644
+ async function ensureProxy(proxyPort, apiBaseUrl) {
5645
+ if (await probeProxyHealth(proxyPort)) {
5646
+ const reg2 = readProxyRegistry();
5647
+ const managed = reg2 !== null && reg2.port === proxyPort && isProcessAlive(reg2.pid);
5648
+ return { reused: true, managed };
5649
+ }
5650
+ const reg = readProxyRegistry();
5651
+ if (reg !== null && !isProcessAlive(reg.pid)) {
5652
+ try {
5653
+ unlinkSync(PROXY_REGISTRY);
5654
+ } catch {
5655
+ }
5656
+ }
5657
+ if (await isPortListening(proxyPort)) {
5658
+ throw new Error(
5659
+ `A server is already running on port ${String(proxyPort)} but it's not a healthy Shield proxy. Stop it or re-run with --proxy-port <n>.`
5660
+ );
5661
+ }
5662
+ const logFile = join(PIDFILE_DIR, "proxy.log");
5663
+ const child = startLocalProxyDetached(proxyPort, apiBaseUrl);
5664
+ for (let i = 0; i < LOCAL_PROXY_READY_MAX_POLLS; i++) {
5665
+ await sleep3(LOCAL_PROXY_READY_POLL_MS);
5666
+ if (child.exitCode !== null || child.signalCode !== null) break;
5667
+ if (await probeProxyHealth(proxyPort)) {
5668
+ if (child.pid !== void 0) {
5669
+ writeJsonFile(PROXY_REGISTRY, { pid: child.pid, port: proxyPort });
5670
+ }
5671
+ return { reused: false, managed: true };
5672
+ }
5673
+ }
5674
+ try {
5675
+ if (child.pid !== void 0) killWithEscalation(child.pid, true);
5676
+ } catch {
5677
+ }
5678
+ const childExited = child.exitCode !== null || child.signalCode !== null;
5679
+ throw new Error(formatLocalProxyStartError(proxyPort, logFile, childExited, child.exitCode));
5680
+ }
5681
+ async function ensureFsServer(realDir, requestedPort) {
5682
+ const reg = readFsRegistry();
5683
+ const existing = reg[realDir];
5684
+ if (existing !== void 0 && isProcessAlive(existing.pid) && await isPortListening(existing.port)) {
5685
+ return { port: existing.port, reused: true };
5686
+ }
5687
+ if (existing !== void 0) {
5688
+ const rest = Object.fromEntries(Object.entries(reg).filter(([k]) => k !== realDir));
5689
+ writeJsonFile(FS_REGISTRY, rest);
5690
+ }
5691
+ let port;
5692
+ if (requestedPort !== void 0) {
5693
+ if (await isPortListening(requestedPort)) {
5694
+ throw new Error(
5695
+ `A server is already running on port ${String(requestedPort)}. Re-run without --port to auto-pick a free port, or choose another.`
5696
+ );
5697
+ }
5698
+ port = requestedPort;
5699
+ } else {
5700
+ const claimed = new Set(Object.values(readFsRegistry()).map((e) => e.port));
5701
+ port = await nextFreePort(DEFAULT_FS_PORT, claimed, isPortListening);
5702
+ }
5703
+ const child = startFsServerDetached(realDir, port);
5704
+ let ready = false;
5705
+ for (let i = 0; i < 20; i++) {
5706
+ await sleep3(500);
5707
+ if (await isPortListening(port)) {
5708
+ ready = true;
5709
+ break;
5710
+ }
5711
+ }
5712
+ if (!ready) {
5713
+ try {
5714
+ if (child.pid !== void 0) killWithEscalation(child.pid, true);
5715
+ } catch {
5716
+ }
5717
+ throw new Error(
5718
+ `Filesystem server failed to start on port ${String(port)}. Check the directory path and the log at ${join(PIDFILE_DIR, `fs-${String(port)}.log`)}.`
5719
+ );
5720
+ }
5721
+ if (child.pid !== void 0) {
5722
+ const next = readFsRegistry();
5723
+ next[realDir] = { pid: child.pid, port };
5724
+ writeJsonFile(FS_REGISTRY, next);
5725
+ }
5726
+ return { port, reused: false };
5727
+ }
5728
+ function releaseProxyIfUnused(proxyPort, stoppingAgent) {
5729
+ const remaining = agentsReferencingProxy(proxyPort, liveAgents(), stoppingAgent);
5730
+ if (remaining.length > 0) return;
5731
+ const reg = readProxyRegistry();
5732
+ if (reg !== null && reg.port === proxyPort && isProcessAlive(reg.pid)) {
5733
+ killWithEscalation(reg.pid, true);
5734
+ }
5735
+ if (reg !== null && reg.port === proxyPort) {
5736
+ try {
5737
+ unlinkSync(PROXY_REGISTRY);
5738
+ } catch {
5739
+ }
5740
+ }
5741
+ }
5742
+ function releaseFsServerIfUnused(folder, stoppingAgent) {
5743
+ const remaining = agentsReferencingFolder(folder, liveAgents(), stoppingAgent);
5744
+ if (remaining.length > 0) return;
5745
+ const reg = readFsRegistry();
5746
+ const entry = reg[folder];
5747
+ if (entry === void 0) return;
5748
+ if (isProcessAlive(entry.pid)) {
5749
+ killWithEscalation(entry.pid, true);
5750
+ }
5751
+ const rest = Object.fromEntries(Object.entries(reg).filter(([k]) => k !== folder));
5752
+ writeJsonFile(FS_REGISTRY, rest);
5753
+ }
5754
+ function proxyServerSlug(agentLabel) {
5755
+ const t = agentLabel.trim().toLowerCase();
5756
+ const slug = t.replace(/[^a-z0-9._-]/g, "-").replace(/^-+|-+$/g, "");
5757
+ if (slug === "") return "shield-handoff-agent";
5758
+ if (!/^[a-z0-9]/i.test(slug)) return `a-${slug}`;
5759
+ return slug.slice(0, 180);
5760
+ }
5761
+ async function registerAgentAndConfig(apiKey, baseUrl, agentName, fsPort, localDir) {
5762
+ const targetUrl = `http://127.0.0.1:${String(fsPort)}/mcp`;
5763
+ const serverName = proxyServerSlug(agentName);
5764
+ const response = await fetch(`${baseUrl}/api/v1/proxy/config`, {
5765
+ method: "POST",
5766
+ headers: {
5767
+ "Content-Type": "application/json",
5768
+ "X-Multicorn-Key": apiKey
5769
+ },
5770
+ body: JSON.stringify({
5771
+ server_name: serverName,
5772
+ target_url: targetUrl,
5773
+ platform: "other-mcp",
5774
+ agent_name: agentName,
5775
+ local_dir: localDir
5776
+ }),
5777
+ signal: AbortSignal.timeout(1e4)
5778
+ });
5779
+ if (!response.ok) {
5780
+ let detail = `HTTP ${String(response.status)}`;
5781
+ try {
5782
+ const body = await response.json();
5783
+ const errObj = body["error"];
5784
+ if (typeof errObj?.["message"] === "string") detail = errObj["message"];
5785
+ else if (typeof body["message"] === "string") detail = body["message"];
5786
+ } catch {
5787
+ }
5788
+ throw new Error(`Failed to register agent: ${detail}`);
5789
+ }
5790
+ const envelope = await response.json();
5791
+ const data = envelope["data"];
5792
+ const proxyUrl = typeof data?.["proxy_url"] === "string" ? data["proxy_url"] : "";
5793
+ if (proxyUrl.length === 0) {
5794
+ throw new Error("Registration succeeded but no proxy URL was returned.");
5795
+ }
5796
+ let pathSegment = "";
5797
+ try {
5798
+ const parsed = new URL(proxyUrl);
5799
+ pathSegment = parsed.pathname + parsed.search;
5800
+ } catch {
5801
+ pathSegment = proxyUrl;
5802
+ }
5803
+ await grantScope(apiKey, baseUrl, agentName, "filesystem", "read");
5804
+ return { proxyUrl, pathSegment };
5805
+ }
5806
+ async function sendHeartbeat(apiKey, baseUrl, serverName, proxyVersion) {
5807
+ try {
5808
+ await fetch(`${baseUrl}/api/v1/proxy/heartbeat`, {
5809
+ method: "POST",
5810
+ headers: {
5811
+ "Content-Type": "application/json",
5812
+ "X-Multicorn-Key": apiKey
5813
+ },
5814
+ // proxy_version lets the backend stamp the agent's last-seen version + timestamp
5815
+ // from the heartbeat, so the dashboard can flag an out-of-date proxy that needs a
5816
+ // restart even if the editor never connects through it.
5817
+ body: JSON.stringify(
5818
+ proxyVersion !== null ? { server_name: serverName, proxy_version: proxyVersion } : { server_name: serverName }
5819
+ ),
5820
+ signal: AbortSignal.timeout(8e3)
5821
+ });
5822
+ } catch {
5823
+ }
5824
+ }
5825
+ async function grantScope(apiKey, baseUrl, agentName, service, level) {
5826
+ const agentsResp = await fetch(`${baseUrl}/api/v1/agents`, {
5827
+ headers: { "X-Multicorn-Key": apiKey },
5828
+ signal: AbortSignal.timeout(8e3)
5829
+ });
5830
+ if (!agentsResp.ok) return;
5831
+ const agentsBody = await agentsResp.json();
5832
+ const agentsList = agentsBody["data"];
5833
+ if (!Array.isArray(agentsList)) return;
5834
+ const agent = agentsList.find((a) => a["name"] === agentName);
5835
+ if (!agent || typeof agent["id"] !== "string") return;
5836
+ const agentId = agent["id"];
5837
+ await fetch(`${baseUrl}/api/v1/agents/${agentId}/scopes`, {
5838
+ method: "POST",
5839
+ headers: {
5840
+ "Content-Type": "application/json",
5841
+ "X-Multicorn-Key": apiKey
5842
+ },
5843
+ body: JSON.stringify({ service, permission_level: level }),
5844
+ signal: AbortSignal.timeout(8e3)
5845
+ });
5846
+ }
5847
+ function killWithEscalation(pid, group = false) {
5848
+ const signal = (sig) => {
5849
+ if (group) {
5850
+ try {
5851
+ process.kill(-pid, sig);
5852
+ return;
5853
+ } catch {
5854
+ }
5855
+ }
5856
+ process.kill(pid, sig);
5857
+ };
5858
+ try {
5859
+ signal("SIGTERM");
5860
+ } catch {
5861
+ return;
5862
+ }
5863
+ const deadline = Date.now() + 3e3;
5864
+ while (Date.now() < deadline) {
5865
+ if (!isProcessAlive(pid)) return;
5866
+ spawnSync("sleep", ["0.1"], { stdio: "ignore" });
5867
+ }
5868
+ try {
5869
+ signal("SIGKILL");
5870
+ } catch {
5871
+ }
5872
+ }
5873
+ async function runStop(agent) {
5874
+ const data = readPidfile(agent);
5875
+ if (data === null) {
5876
+ process.stderr.write(`No running session found for agent "${agent}".
5877
+ `);
5878
+ process.exit(1);
5879
+ }
5880
+ await withResourceLock(() => {
5881
+ if (typeof data.supervisorPid === "number" && isProcessAlive(data.supervisorPid)) {
5882
+ killWithEscalation(data.supervisorPid);
5883
+ }
5884
+ removePidfile(agent);
5885
+ releaseFsServerIfUnused(data.dir, agent);
5886
+ releaseProxyIfUnused(data.proxyPort, agent);
5887
+ });
5888
+ process.stderr.write(`Stopped agent "${agent}".
5889
+ `);
5890
+ }
5891
+ async function runRestart(opts) {
5892
+ if (opts.agent.length === 0) {
5893
+ process.stderr.write("Error: --agent <name> is required for restart.\n");
5894
+ process.exit(1);
5895
+ }
5896
+ const existing = readPidfile(opts.agent);
5897
+ const dir = opts.dir.length > 0 ? opts.dir : existing?.dir ?? "";
5898
+ if (dir.length === 0) {
5899
+ process.stderr.write(
5900
+ `Don't know which folder to restart agent "${opts.agent}" with.
5901
+ Run it once with the folder: npx multicorn-shield files restart <dir> --agent ${opts.agent}
5902
+ `
5903
+ );
5904
+ process.exit(1);
5905
+ }
5906
+ if (existing !== null) {
5907
+ await runStop(opts.agent);
5908
+ }
5909
+ await runDetached({
5910
+ ...opts,
5911
+ dir,
5912
+ proxyPort: opts.proxyPort ?? existing?.proxyPort});
5913
+ }
5914
+ async function runDetached(opts) {
5915
+ const absDir = resolve(opts.dir);
5916
+ if (!existsSync(absDir)) {
5917
+ process.stderr.write(`Directory not found: ${opts.dir}. Check the path and try again.
5918
+ `);
5919
+ process.exit(1);
5920
+ }
5921
+ const existing = readPidfile(opts.agent);
5922
+ if (existing !== null) {
5923
+ const alive = typeof existing.supervisorPid === "number" && isProcessAlive(existing.supervisorPid);
5924
+ if (alive) {
5925
+ process.stderr.write(
5926
+ `Already running for agent "${opts.agent}" (fs :${String(existing.fsPort)}, proxy :${String(existing.proxyPort)}).
5927
+ Stop with: npx multicorn-shield files stop --agent ${opts.agent}
5928
+ `
5929
+ );
5930
+ return;
5931
+ }
5932
+ removePidfile(opts.agent);
5933
+ }
5934
+ const args = ["files", absDir, "--agent", opts.agent, "--foreground"];
5935
+ if (opts.port !== void 0) args.push("--port", String(opts.port));
5936
+ if (opts.proxyPort !== void 0) args.push("--proxy-port", String(opts.proxyPort));
5937
+ if (opts.apiKey !== void 0) args.push("--api-key", opts.apiKey);
5938
+ if (opts.baseUrl !== void 0) args.push("--base-url", opts.baseUrl);
5939
+ if (opts.client !== void 0) args.push("--client", opts.client);
5940
+ const scriptPath = process.argv[1] ?? resolve("dist/multicorn-shield.js");
5941
+ const logFile = join(PIDFILE_DIR, `files-${opts.agent}.log`);
5942
+ mkdirSync(PIDFILE_DIR, { recursive: true, mode: 448 });
5943
+ const out = openSync(logFile, "a");
5944
+ const err = openSync(logFile, "a");
5945
+ const child = spawn(process.execPath, [scriptPath, ...args], {
5946
+ detached: true,
5947
+ stdio: ["ignore", out, err],
5948
+ env: { ...process.env }
5949
+ });
5950
+ child.unref();
5951
+ let pidData = null;
5952
+ for (let i = 0; i < 60; i++) {
5953
+ await sleep3(500);
5954
+ pidData = readPidfile(opts.agent);
5955
+ if (pidData !== null) break;
5956
+ if (child.pid !== void 0 && !isProcessAlive(child.pid)) break;
5957
+ }
5958
+ if (pidData === null) {
5959
+ process.stderr.write(`Failed to start in background. Check logs: ${logFile}
5960
+ `);
5961
+ process.exit(1);
5962
+ }
5963
+ process.stderr.write("\n");
5964
+ process.stderr.write(
5965
+ ` ${style2.green("\u2713")} Setup complete. Shield is running in the background.
5966
+ `
5967
+ );
5968
+ process.stderr.write(` ${style2.green("\u2713")} Folder: ${absDir}
5969
+ `);
5970
+ process.stderr.write(
5971
+ ` ${style2.green("\u2713")} FS server :${String(pidData.fsPort)}, Proxy :${String(pidData.proxyPort)}
5972
+ `
5973
+ );
5974
+ process.stderr.write("\n");
5975
+ process.stderr.write(` Stop it any time with:
5976
+ `);
5977
+ process.stderr.write(
5978
+ ` ${style2.cyan(`npx multicorn-shield files stop --agent ${opts.agent}`)}
5979
+ `
5980
+ );
5981
+ process.stderr.write("\n");
5982
+ process.stderr.write(style2.dim(` Status: npx multicorn-shield files status
5983
+ `));
5984
+ process.stderr.write(style2.dim(` Logs: ${logFile}
5985
+ `));
5986
+ process.stderr.write("\n");
5987
+ }
5988
+ async function runFilesCommand(opts) {
5989
+ if (opts.status) {
5990
+ runStatus();
5991
+ return;
5992
+ }
5993
+ if (opts.restart) {
5994
+ await runRestart(opts);
5995
+ return;
5996
+ }
5997
+ if (opts.stop) {
5998
+ await runStop(opts.agent);
5999
+ return;
6000
+ }
6001
+ if (!opts.foreground) {
6002
+ await runDetached(opts);
6003
+ return;
6004
+ }
6005
+ const absDir = resolve(opts.dir);
6006
+ if (!existsSync(absDir)) {
6007
+ process.stderr.write(`Directory not found: ${opts.dir}. Check the path and try again.
6008
+ `);
6009
+ process.exit(1);
6010
+ }
6011
+ if (opts.baseUrl && !isAllowedShieldApiBaseUrl(opts.baseUrl)) {
6012
+ process.stderr.write(
6013
+ "Error: --base-url must use HTTPS or http://localhost for local development.\n"
6014
+ );
6015
+ process.exit(1);
6016
+ }
6017
+ const config = await resolveConfig(opts);
6018
+ const realDir = canonicalFolder(absDir);
6019
+ const proxyPort = opts.proxyPort ?? DEFAULT_PROXY_PORT;
6020
+ const existingPidfile = readPidfile(opts.agent);
6021
+ if (existingPidfile !== null) {
6022
+ const supervisorAlive = typeof existingPidfile.supervisorPid === "number" && isProcessAlive(existingPidfile.supervisorPid);
6023
+ if (supervisorAlive) {
6024
+ process.stderr.write(
6025
+ `A session for agent "${opts.agent}" is already running. Run 'files stop --agent ${opts.agent}' first, or use a different --agent name.
6026
+ `
6027
+ );
6028
+ process.exit(1);
6029
+ }
6030
+ removePidfile(opts.agent);
6031
+ }
6032
+ let fsPort;
6033
+ let proxyReused;
6034
+ let fsReused;
6035
+ try {
6036
+ const ensured = await withResourceLock(async () => {
6037
+ const proxyRes = await ensureProxy(proxyPort, config.baseUrl);
6038
+ const fsRes = await ensureFsServer(realDir, opts.port);
6039
+ writePidfile({
6040
+ agent: opts.agent,
6041
+ dir: realDir,
6042
+ supervisorPid: process.pid,
6043
+ fsPort: fsRes.port,
6044
+ proxyPort
6045
+ });
6046
+ return { proxyRes, fsRes };
6047
+ });
6048
+ fsPort = ensured.fsRes.port;
6049
+ proxyReused = ensured.proxyRes.reused;
6050
+ fsReused = ensured.fsRes.reused;
6051
+ } catch (error) {
6052
+ const msg = error instanceof Error ? error.message : String(error);
6053
+ process.stderr.write(`${msg}
6054
+ `);
6055
+ process.exit(1);
6056
+ }
6057
+ let registration;
6058
+ try {
6059
+ registration = await registerAgentAndConfig(
6060
+ config.apiKey,
6061
+ config.baseUrl,
6062
+ opts.agent,
6063
+ fsPort,
6064
+ realDir
6065
+ );
6066
+ } catch (error) {
6067
+ await withResourceLock(() => {
6068
+ removePidfile(opts.agent);
6069
+ releaseFsServerIfUnused(realDir, opts.agent);
6070
+ releaseProxyIfUnused(proxyPort, opts.agent);
6071
+ });
6072
+ const msg = error instanceof Error ? error.message : String(error);
6073
+ process.stderr.write(`Registration failed: ${msg}
6074
+ `);
6075
+ process.exit(1);
6076
+ }
6077
+ const heartbeatServerName = proxyServerSlug(opts.agent);
6078
+ const heartbeatTick = async () => {
6079
+ const proxyVersion = await readProxyVersion(proxyPort);
6080
+ const proxyOk = proxyVersion !== null || await probeProxyHealth(proxyPort);
6081
+ const fsOk = await isPortListening(fsPort);
6082
+ if (proxyOk && fsOk) {
6083
+ await sendHeartbeat(config.apiKey, config.baseUrl, heartbeatServerName, proxyVersion);
6084
+ }
6085
+ };
6086
+ void heartbeatTick();
6087
+ const heartbeatTimer = setInterval(() => {
6088
+ void heartbeatTick();
6089
+ }, HEARTBEAT_INTERVAL_MS);
6090
+ let cleaningUp = false;
6091
+ process.on("SIGINT", () => {
6092
+ if (cleaningUp) return;
6093
+ cleaningUp = true;
6094
+ void (async () => {
6095
+ clearInterval(heartbeatTimer);
6096
+ await withResourceLock(() => {
6097
+ removePidfile(opts.agent);
6098
+ releaseFsServerIfUnused(realDir, opts.agent);
6099
+ releaseProxyIfUnused(proxyPort, opts.agent);
6100
+ });
6101
+ process.exit(0);
6102
+ })();
6103
+ });
6104
+ process.on("SIGTERM", () => {
6105
+ clearInterval(heartbeatTimer);
6106
+ removePidfile(opts.agent);
6107
+ process.exit(0);
6108
+ });
6109
+ const dirLabel = `./${basename(absDir)}`;
6110
+ const localProxyUrl = `http://127.0.0.1:${String(proxyPort)}${registration.pathSegment}`;
6111
+ process.stderr.write("\n");
6112
+ process.stderr.write(
6113
+ ` ${style2.green("\u2713")} ${proxyReused ? "Reusing shared proxy" : "Local proxy running"} (:${String(proxyPort)})
6114
+ `
6115
+ );
6116
+ process.stderr.write(
6117
+ ` ${style2.green("\u2713")} ${fsReused ? "Reusing filesystem server for" : "Filesystem server on"} ${dirLabel} (:${String(fsPort)})
6118
+ `
6119
+ );
6120
+ process.stderr.write(` ${style2.green("\u2713")} Agent '${opts.agent}' registered with Shield
6121
+ `);
6122
+ const writeResult = await autoWriteClientConfig(
6123
+ opts.client,
6124
+ opts.agent,
6125
+ localProxyUrl,
6126
+ config.apiKey,
6127
+ absDir
6128
+ );
6129
+ const firstWritten = writeResult.written[0];
6130
+ if (firstWritten !== void 0) {
6131
+ for (const w of writeResult.written) {
6132
+ process.stderr.write(` ${style2.green("\u2713")} Config written to ${style2.cyan(w.path)}
6133
+ `);
6134
+ }
6135
+ process.stderr.write("\n");
6136
+ process.stderr.write(style2.dim(` ${firstWritten.reload}
6137
+ `));
6138
+ } else if (!writeResult.prompted) {
6139
+ process.stderr.write("\n");
6140
+ process.stderr.write(
6141
+ " Add this to your coding agent's MCP config so it routes through Shield:\n\n"
6142
+ );
6143
+ const configBlock = JSON.stringify(
6144
+ {
6145
+ mcpServers: {
6146
+ [opts.agent]: {
6147
+ url: hostedProxyUrlWithKeyParam(localProxyUrl, config.apiKey),
6148
+ headers: { Authorization: `Bearer ${config.apiKey}` }
6149
+ }
6150
+ }
6151
+ },
6152
+ null,
6153
+ 2
6154
+ );
6155
+ process.stderr.write(style2.cyan(configBlock) + "\n\n");
6156
+ }
6157
+ process.stderr.write(style2.dim(" Write and delete will ask for approval the first time.\n"));
6158
+ process.stderr.write("\n");
6159
+ process.stderr.write(
6160
+ ` ${style2.green("\u2713")} Shield is running (foreground mode). Press Ctrl-C to stop.
6161
+ `
6162
+ );
6163
+ process.stderr.write("\n");
6164
+ }
6165
+ async function autoWriteClientConfig(clientFlag, agentName, localProxyUrl, apiKey, workspacePath) {
6166
+ if (clientFlag !== void 0 && clientFlag.length > 0) {
6167
+ if (clientFlag === "all") {
6168
+ const detected2 = detectInstalledClients();
6169
+ if (detected2.length === 0) return { written: [], prompted: false };
6170
+ const results = [];
6171
+ for (const c of detected2) {
6172
+ const path2 = await writeLocalMcpEntry(c, agentName, localProxyUrl, apiKey, workspacePath);
6173
+ if (path2 !== null) {
6174
+ results.push({ client: c, path: path2, reload: clientReloadInstruction(c) });
6175
+ }
6176
+ }
6177
+ return { written: results, prompted: false };
6178
+ }
6179
+ const client2 = clientFlag;
6180
+ if (!CODING_CLIENTS.includes(client2)) {
6181
+ process.stderr.write(
6182
+ `
6183
+ Unknown --client "${clientFlag}". Valid options: ${CODING_CLIENTS.join(", ")}, all
6184
+ `
6185
+ );
6186
+ return { written: [], prompted: false };
6187
+ }
6188
+ const path = await writeLocalMcpEntry(client2, agentName, localProxyUrl, apiKey, workspacePath);
6189
+ if (path !== null) {
6190
+ return {
6191
+ written: [{ client: client2, path, reload: clientReloadInstruction(client2) }],
6192
+ prompted: false
6193
+ };
6194
+ }
6195
+ process.stderr.write(
6196
+ `
6197
+ Could not write to ${clientDisplayName(client2)} config (parse error or permissions).
6198
+ `
6199
+ );
6200
+ return { written: [], prompted: false };
6201
+ }
6202
+ const detected = detectInstalledClients();
6203
+ if (detected.length === 0) {
6204
+ return { written: [], prompted: false };
6205
+ }
6206
+ if (detected.length === 1) {
6207
+ const client2 = detected[0];
6208
+ if (client2 === void 0) {
6209
+ return { written: [], prompted: false };
6210
+ }
6211
+ const path = await writeLocalMcpEntry(client2, agentName, localProxyUrl, apiKey, workspacePath);
6212
+ if (path !== null) {
6213
+ return {
6214
+ written: [{ client: client2, path, reload: clientReloadInstruction(client2) }],
6215
+ prompted: false
6216
+ };
6217
+ }
6218
+ process.stderr.write(
6219
+ `
6220
+ Could not safely update ${clientDisplayName(client2)} config (parse error or permissions). Left it unchanged.
6221
+ `
6222
+ );
6223
+ return { written: [], prompted: false };
6224
+ }
6225
+ if (!process.stdin.isTTY) {
6226
+ process.stderr.write("\n");
6227
+ process.stderr.write(" Multiple coding agents detected:\n");
6228
+ for (const c of detected) {
6229
+ process.stderr.write(` - ${clientDisplayName(c)} (--client ${c})
6230
+ `);
6231
+ }
6232
+ process.stderr.write(
6233
+ "\n Re-run with --client <name> to write config, or --client all for all.\n"
6234
+ );
6235
+ return { written: [], prompted: true };
6236
+ }
6237
+ process.stderr.write("\n");
6238
+ process.stderr.write(" Multiple coding agents detected. Which should route through Shield?\n\n");
6239
+ for (const [i, c] of detected.entries()) {
6240
+ process.stderr.write(` ${String(i + 1)}) ${clientDisplayName(c)}
6241
+ `);
6242
+ }
6243
+ process.stderr.write(` a) All of them
6244
+ `);
6245
+ process.stderr.write("\n");
6246
+ const choice = await promptLine(" Enter number (or 'a' for all): ");
6247
+ const trimmed = choice.trim().toLowerCase();
6248
+ if (trimmed === "a" || trimmed === "all") {
6249
+ const results = [];
6250
+ for (const c of detected) {
6251
+ const path = await writeLocalMcpEntry(c, agentName, localProxyUrl, apiKey, workspacePath);
6252
+ if (path !== null) {
6253
+ results.push({ client: c, path, reload: clientReloadInstruction(c) });
6254
+ }
6255
+ }
6256
+ return { written: results, prompted: false };
6257
+ }
6258
+ const num = Number.parseInt(trimmed, 10);
6259
+ const client = num >= 1 && num <= detected.length ? detected[num - 1] : void 0;
6260
+ if (client !== void 0) {
6261
+ const path = await writeLocalMcpEntry(client, agentName, localProxyUrl, apiKey, workspacePath);
6262
+ if (path !== null) {
6263
+ return {
6264
+ written: [{ client, path, reload: clientReloadInstruction(client) }],
6265
+ prompted: false
6266
+ };
6267
+ }
6268
+ }
6269
+ process.stderr.write(` Invalid choice. Use --client <name> next time.
6270
+ `);
6271
+ return { written: [], prompted: true };
6272
+ }
6273
+ function promptLine(question) {
6274
+ return new Promise((resolve3) => {
6275
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
6276
+ rl.question(question, (answer) => {
6277
+ rl.close();
6278
+ resolve3(answer);
6279
+ });
6280
+ });
6281
+ }
6282
+ var DEFAULT_FS_PORT, DEFAULT_PROXY_PORT, PIDFILE_DIR, PROXY_REGISTRY, FS_REGISTRY, LOCK_PATH, LOCK_WAIT_MS, LOCK_STALE_MS, FS_PORT_SCAN_RANGE, style2, HEARTBEAT_INTERVAL_MS;
6283
+ var init_files = __esm({
6284
+ "src/commands/files.ts"() {
6285
+ init_config();
6286
+ init_local_proxy_start();
6287
+ DEFAULT_FS_PORT = 3005;
6288
+ DEFAULT_PROXY_PORT = 3001;
6289
+ PIDFILE_DIR = process.env["MULTICORN_HOME"] ?? join(homedir(), ".multicorn");
6290
+ PROXY_REGISTRY = join(PIDFILE_DIR, "proxy.json");
6291
+ FS_REGISTRY = join(PIDFILE_DIR, "fs-servers.json");
6292
+ LOCK_PATH = join(PIDFILE_DIR, ".resources.lock");
6293
+ LOCK_WAIT_MS = 6e4;
6294
+ LOCK_STALE_MS = 12e4;
6295
+ FS_PORT_SCAN_RANGE = 200;
6296
+ style2 = {
6297
+ green: (s) => `\x1B[38;2;34;197;94m${s}\x1B[0m`,
6298
+ cyan: (s) => `\x1B[38;2;6;182;212m${s}\x1B[0m`,
6299
+ dim: (s) => `\x1B[2m${s}\x1B[0m`,
6300
+ bold: (s) => `\x1B[1m${s}\x1B[0m`
6301
+ };
6302
+ HEARTBEAT_INTERVAL_MS = 3e4;
6303
+ }
6304
+ });
6305
+
6306
+ // bin/multicorn-shield.ts
6307
+ var multicorn_shield_exports = {};
6308
+ __export(multicorn_shield_exports, {
6309
+ parseArgs: () => parseArgs,
6310
+ resolveWrapConfig: () => resolveWrapConfig,
6311
+ runCli: () => runCli
6312
+ });
6313
+ function parseArgs(argv) {
6314
+ const args = argv.slice(2);
6315
+ let subcommand = "help";
6316
+ let wrapCommand = "";
6317
+ let wrapArgs = [];
6318
+ let logLevel = "info";
6319
+ let baseUrl = void 0;
6320
+ let dashboardUrl = "";
6321
+ let agentName = "";
6322
+ let deleteAgentName = "";
6323
+ let apiKey = void 0;
6324
+ let verbose = false;
6325
+ let filesDir = "";
6326
+ let filesPort = void 0;
6327
+ let filesProxyPort = void 0;
6328
+ let filesStop = false;
6329
+ let filesClient = void 0;
6330
+ let filesForeground = false;
6331
+ let filesStatus = false;
6332
+ let filesRestart = false;
6333
+ for (let i = 0; i < args.length; i++) {
6334
+ const arg = args[i];
6335
+ if (arg === "init") {
6336
+ subcommand = "init";
6337
+ } else if (arg === "files") {
6338
+ subcommand = "files";
6339
+ const tail = args.slice(i + 1);
6340
+ for (let j = 0; j < tail.length; j++) {
6341
+ const token = tail[j];
6342
+ if (token === void 0) continue;
6343
+ if (token === "--agent") {
6344
+ const value = tail[j + 1];
6345
+ if (value !== void 0) {
6346
+ agentName = value;
6347
+ j++;
6348
+ }
6349
+ } else if (token === "--port") {
6350
+ const value = tail[j + 1];
6351
+ if (value !== void 0) {
6352
+ filesPort = Number.parseInt(value, 10);
6353
+ j++;
6354
+ }
6355
+ } else if (token === "--proxy-port") {
6356
+ const value = tail[j + 1];
6357
+ if (value !== void 0) {
6358
+ filesProxyPort = Number.parseInt(value, 10);
6359
+ j++;
6360
+ }
6361
+ } else if (token === "--api-key") {
6362
+ const value = tail[j + 1];
6363
+ if (value !== void 0) {
6364
+ apiKey = value;
6365
+ j++;
6366
+ }
6367
+ } else if (token === "--base-url") {
6368
+ const value = tail[j + 1];
6369
+ if (value !== void 0) {
6370
+ baseUrl = value;
6371
+ j++;
6372
+ }
6373
+ } else if (token === "--client") {
6374
+ const value = tail[j + 1];
6375
+ if (value !== void 0) {
6376
+ filesClient = value;
6377
+ j++;
6378
+ }
6379
+ } else if (token === "--stop") {
6380
+ filesStop = true;
6381
+ } else if (token === "--foreground") {
6382
+ filesForeground = true;
6383
+ } else if (token === "--detach") ; else if (token === "stop") {
6384
+ filesStop = true;
6385
+ } else if (token === "status") {
6386
+ filesStatus = true;
6387
+ } else if (token === "restart") {
6388
+ filesRestart = true;
6389
+ } else if (!token.startsWith("-") && filesDir === "") {
6390
+ filesDir = token;
6391
+ }
6392
+ }
6393
+ break;
6394
+ } else if (arg === "agents") {
6395
+ subcommand = "agents";
6396
+ } else if (arg === "delete-agent") {
6397
+ subcommand = "delete-agent";
6398
+ const name = args[i + 1];
6399
+ if (name === void 0 || name.startsWith("-")) {
6400
+ process.stderr.write("Error: delete-agent requires an agent name.\n");
6401
+ process.stderr.write("Example: npx multicorn-shield delete-agent my-agent\n");
6402
+ process.exit(1);
6403
+ }
6404
+ deleteAgentName = name;
6405
+ i++;
6406
+ } else if (arg === "--wrap") {
6407
+ subcommand = "wrap";
6408
+ const tail = args.slice(i + 1);
6409
+ const remaining = [];
6410
+ for (let j = 0; j < tail.length; j++) {
6411
+ const token = tail[j];
6412
+ if (token === void 0) continue;
6413
+ if (remaining.length > 0) {
6414
+ remaining.push(token);
6415
+ continue;
6416
+ }
6417
+ if (token === "--agent-name") {
6418
+ const value = tail[j + 1];
6419
+ if (value !== void 0) {
6420
+ agentName = value;
6421
+ j++;
6422
+ }
6423
+ } else if (token === "--log-level") {
6424
+ const value = tail[j + 1];
6425
+ if (value !== void 0 && isValidLogLevel(value)) {
6426
+ logLevel = value;
6427
+ j++;
6428
+ }
6429
+ } else if (token === "--base-url") {
6430
+ const value = tail[j + 1];
6431
+ if (value !== void 0) {
6432
+ baseUrl = value;
6433
+ j++;
6434
+ }
6435
+ } else if (token === "--dashboard-url") {
6436
+ const value = tail[j + 1];
6437
+ if (value !== void 0) {
6438
+ dashboardUrl = value;
6439
+ j++;
6440
+ }
6441
+ } else if (token === "--api-key") {
6442
+ const value = tail[j + 1];
6443
+ if (value !== void 0) {
6444
+ apiKey = value;
6445
+ j++;
6446
+ }
6447
+ } else {
6448
+ remaining.push(token);
6449
+ }
6450
+ }
6451
+ if (remaining.length === 0) {
6452
+ process.stderr.write("Error: --wrap requires a command to run.\n");
6453
+ process.stderr.write("Example: npx multicorn-shield --wrap my-mcp-server\n");
6454
+ process.exit(1);
6455
+ }
6456
+ wrapCommand = remaining[0] ?? "";
6457
+ wrapArgs = remaining.slice(1);
6458
+ break;
6459
+ } else if (arg === "--log-level") {
6460
+ const next = args[i + 1];
6461
+ if (next !== void 0 && isValidLogLevel(next)) {
6462
+ logLevel = next;
6463
+ i++;
6464
+ }
6465
+ } else if (arg === "--base-url") {
6466
+ const next = args[i + 1];
6467
+ if (next !== void 0) {
6468
+ baseUrl = next;
6469
+ i++;
6470
+ }
6471
+ } else if (arg === "--dashboard-url") {
6472
+ const next = args[i + 1];
6473
+ if (next !== void 0) {
6474
+ dashboardUrl = next;
6475
+ i++;
6476
+ }
6477
+ } else if (arg === "--agent-name") {
6478
+ const next = args[i + 1];
6479
+ if (next !== void 0) {
6480
+ agentName = next;
6481
+ i++;
6482
+ }
6483
+ } else if (arg === "--api-key") {
6484
+ const next = args[i + 1];
6485
+ if (next !== void 0) {
6486
+ apiKey = next;
6487
+ i++;
6488
+ }
6489
+ } else if (arg === "--verbose" || arg === "--debug") {
6490
+ verbose = true;
6491
+ }
6492
+ }
6493
+ return {
6494
+ subcommand,
6495
+ wrapCommand,
6496
+ wrapArgs,
6497
+ logLevel,
6498
+ baseUrl,
6499
+ dashboardUrl,
6500
+ agentName,
6501
+ deleteAgentName,
6502
+ apiKey,
6503
+ verbose,
6504
+ filesDir,
6505
+ filesPort,
6506
+ filesProxyPort,
6507
+ filesStop,
6508
+ filesClient,
6509
+ filesForeground,
6510
+ filesStatus,
6511
+ filesRestart
6512
+ };
6513
+ }
6514
+ function printHelp() {
6515
+ process.stderr.write(
6516
+ [
6517
+ "multicorn-shield: MCP permission proxy and Shield setup",
6518
+ "",
6519
+ "Usage:",
6520
+ " npx multicorn-shield init",
6521
+ " Interactive setup. Saves API key to ~/.multicorn/config.json.",
6522
+ "",
6523
+ " npx multicorn-shield files <dir> --agent <name> [--client <client>]",
6524
+ " Share a local folder with a coding agent. Starts a filesystem MCP server",
6525
+ " scoped to <dir>, registers the agent, writes your coding agent's MCP config,",
6526
+ " then exits. The service runs in the background until stopped.",
6527
+ "",
6528
+ " --client <name> Target client: cursor, cline, windsurf, claude, copilot,",
6529
+ " goose, gemini, codex, continue, kilo, opencode",
6530
+ " Auto-detected if omitted. Use --client all to write to every",
6531
+ " detected client.",
6532
+ " --foreground Keep the terminal open (for debugging). Default is background.",
6533
+ " --stop Tear down the servers started by a previous `files` invocation.",
6534
+ "",
6535
+ " npx multicorn-shield files stop --agent <name>",
6536
+ " Stop background processes for the named agent.",
6537
+ "",
6538
+ " npx multicorn-shield files restart --agent <name>",
6539
+ " Stop then start the named agent, reusing the folder from its last run.",
6540
+ " Rewrites the coding agent's MCP config entry, so this repairs a stale",
6541
+ " entry that a plain stop/start would leave in place.",
6542
+ "",
6543
+ " npx multicorn-shield files status",
6544
+ " Show all running file-sharing sessions.",
6545
+ "",
6546
+ " npx multicorn-shield restore",
6547
+ " Restore MCP servers in claude_desktop_config.json from the Shield extension backup.",
6548
+ "",
6549
+ " npx multicorn-shield agents",
6550
+ " List configured agents and show which is the default.",
6551
+ "",
6552
+ " npx multicorn-shield delete-agent <name>",
6553
+ " Remove a saved agent.",
6554
+ "",
6555
+ " npx multicorn-shield --wrap <command> [args...]",
6556
+ " Start <command> as an MCP server and proxy all tool calls through",
6557
+ " Shield's permission layer.",
6558
+ "",
6559
+ "Options:",
6560
+ " --version, -v Print version and exit",
6561
+ " --verbose, --debug Print extra diagnostics during init (menu selection, agent counts)",
6562
+ " --api-key <key> Multicorn API key (overrides MULTICORN_API_KEY env var and config file)",
6563
+ " --log-level <level> Log level: debug | info | warn | error (default: info)",
6564
+ " --base-url <url> Multicorn API base URL (default: https://api.multicorn.ai)",
6565
+ " --dashboard-url <url> Dashboard URL for consent page (default: derived from --base-url)",
6566
+ " --agent-name <name> Override agent name derived from the wrapped command",
6567
+ "",
6568
+ "Examples:",
6569
+ " npx multicorn-shield init",
6570
+ " npx multicorn-shield files ./my-repo --agent my-agent",
6571
+ " npx multicorn-shield files ./my-repo --agent my-agent --foreground",
6572
+ " npx multicorn-shield files status",
6573
+ " npx multicorn-shield files stop --agent my-agent",
6574
+ " npx multicorn-shield files restart --agent my-agent",
6575
+ " npx multicorn-shield --wrap npx @modelcontextprotocol/server-filesystem /tmp",
6576
+ " npx multicorn-shield --wrap my-mcp-server --log-level debug",
6577
+ ""
6578
+ ].join("\n")
6579
+ );
5106
6580
  }
5107
6581
  async function runCli() {
5108
6582
  const first = process.argv[2];
@@ -5128,6 +6602,48 @@ async function runCli() {
5128
6602
  await runInit(cli.baseUrl, { verbose: cli.verbose });
5129
6603
  return;
5130
6604
  }
6605
+ if (cli.subcommand === "files") {
6606
+ if (cli.filesStatus) {
6607
+ await runFilesCommand({
6608
+ dir: "",
6609
+ agent: "",
6610
+ port: void 0,
6611
+ proxyPort: void 0,
6612
+ apiKey: void 0,
6613
+ baseUrl: void 0,
6614
+ stop: false,
6615
+ client: void 0,
6616
+ foreground: true,
6617
+ status: true,
6618
+ restart: false
6619
+ });
6620
+ return;
6621
+ }
6622
+ if (!cli.filesStop && cli.agentName.length === 0) {
6623
+ process.stderr.write("Error: --agent <name> is required for the files command.\n");
6624
+ process.stderr.write("Example: npx multicorn-shield files ./my-repo --agent my-agent\n");
6625
+ process.exit(1);
6626
+ }
6627
+ if (!cli.filesStop && !cli.filesRestart && cli.filesDir.length === 0) {
6628
+ process.stderr.write("Error: a directory path is required.\n");
6629
+ process.stderr.write("Example: npx multicorn-shield files ./my-repo --agent my-agent\n");
6630
+ process.exit(1);
6631
+ }
6632
+ await runFilesCommand({
6633
+ dir: cli.filesDir,
6634
+ agent: cli.agentName,
6635
+ port: cli.filesPort,
6636
+ proxyPort: cli.filesProxyPort,
6637
+ apiKey: cli.apiKey,
6638
+ baseUrl: cli.baseUrl,
6639
+ stop: cli.filesStop,
6640
+ client: cli.filesClient,
6641
+ foreground: cli.filesForeground,
6642
+ status: false,
6643
+ restart: cli.filesRestart
6644
+ });
6645
+ return;
6646
+ }
5131
6647
  if (cli.subcommand === "agents") {
5132
6648
  const config2 = await loadConfig();
5133
6649
  if (config2 === null) {
@@ -5265,6 +6781,7 @@ var init_multicorn_shield = __esm({
5265
6781
  init_consent();
5266
6782
  init_restore();
5267
6783
  init_package_meta();
6784
+ init_files();
5268
6785
  isDirectRun = process.argv[1] !== void 0 && (import.meta.url.endsWith(process.argv[1]) || import.meta.url === `file://${process.argv[1]}` || import.meta.url.endsWith("/multicorn-shield.js") || import.meta.url.endsWith("/multicorn-shield.ts"));
5269
6786
  if (isDirectRun && process.env["VITEST"] === void 0) {
5270
6787
  runCli().catch((error) => {