@mergesafe-io/connect 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,16 +1,31 @@
1
1
  # @mergesafe-io/connect
2
2
 
3
- One command to connect your AI client to the [MergeSafe](https://app.mergesafe.io) MCP server — no JSON editing.
3
+ One command connects every AI client on your machine to the
4
+ [MergeSafe](https://app.mergesafe.io) MCP server — globally, so it works in
5
+ every project. No JSON editing.
4
6
 
5
7
  ```bash
6
- npx @mergesafe-io/connect claude-desktop --key msk_your_api_key
8
+ npx @mergesafe-io/connect --key msk_your_api_key
7
9
  ```
8
10
 
9
- That's it. Restart the client and the `mergesafe` tools are available.
11
+ That's it. The CLI detects which clients are installed (Claude Code, Claude
12
+ Desktop, Cursor, Windsurf, VS Code, …), writes each one's **global** config,
13
+ and backs up every file it touches. Restart your clients and the `mergesafe`
14
+ tools are available everywhere.
15
+
16
+ To uninstall from every detected client:
17
+
18
+ ```bash
19
+ npx @mergesafe-io/connect --uninstall
20
+ ```
21
+
22
+ That removes only the mergesafe entry — all other configs are preserved.
10
23
 
11
24
  ## What it does
12
25
 
13
- - Finds the right MCP config file for your client and OS
26
+ - Detects the AI clients installed on your machine (or targets just one if
27
+ you name it: `npx @mergesafe-io/connect cursor --key msk_…`)
28
+ - Finds the right **global** MCP config file for each client and OS
14
29
  - Backs up the existing file next to it (`*.bak-<timestamp>`)
15
30
  - Merges in the `mergesafe` server entry — every other setting and server is preserved
16
31
  - Refuses to touch a file it can't parse (broken JSON aborts with a clear message)
@@ -19,43 +34,42 @@ No dependencies, no telemetry, Node 18+.
19
34
 
20
35
  ## Supported clients
21
36
 
22
- | Client | Config written |
37
+ | Client | Global config written |
23
38
  |---|---|
24
- | `claude-desktop` | `claude_desktop_config.json` (global, per-OS path) — bridges through `mcp-remote` |
25
- | `cursor` | `~/.cursor/mcp.json` (or `./.cursor/mcp.json` with `--project`) |
26
- | `cursor-local` | `./.cursor/mcp.json` — local stdio server (needs `mergesafe-mcp` on PATH) |
39
+ | `claude-code` | `~/.claude.json` (user scope — every project and session) |
40
+ | `claude-desktop` | `claude_desktop_config.json` (per-OS path) — bridges through `mcp-remote` |
41
+ | `cursor` | `~/.cursor/mcp.json` (`--project` for `./.cursor/mcp.json`) |
27
42
  | `windsurf` | `~/.codeium/windsurf/mcp_config.json` |
28
- | `vscode` | `./.vscode/mcp.json` (current project) |
29
- | `opencode` | `./opencode.json` (current project) |
43
+ | `vscode` | user-profile `mcp.json` (`--project` for `./.vscode/mcp.json`) |
44
+ | `opencode` | `~/.config/opencode/config.json` |
30
45
  | `cline` | `cline_mcp_settings.json` (stock VS Code global storage, per-OS path) |
31
- | `roo-code` | `./.roo/mcp.json` (current project) |
32
- | `continue` | `./.continue/mcpServers/mergesafe.yaml` (dedicated file, whole-file write) |
46
+ | `roo-code` | `mcp_settings.json` in VS Code global storage (`--project` for `./.roo/mcp.json`) |
47
+ | `continue` | `~/.continue/mcpServers/mergesafe.yaml` (`--project` for the project copy) |
33
48
 
34
49
  Not covered: Zed (its `settings.json` is JSONC with comments — a rewrite would
35
- destroy them; configure manually). For Claude Code CLI and Gemini CLI, use their
36
- built-in commands instead:
37
- `claude mcp add --transport http mergesafe https://mcp.mergesafe.io/mcp --header "Authorization: Bearer msk_…"`
50
+ destroy them; configure manually). Gemini CLI: use its built-in command:
51
+ `gemini mcp add --transport http mergesafe https://mcp.mergesafe.io/mcp -H "Authorization: Bearer msk_…"`
38
52
 
39
53
  ## Options
40
54
 
41
55
  | Flag | Effect |
42
56
  |---|---|
43
57
  | `--key <msk_…>` | Your MergeSafe API key (from app.mergesafe.io → Settings). Prompted if omitted. |
44
- | `--project` | Write the project-local config instead of the global one (cursor) |
45
- | `--dry-run` | Print the target path and resulting file without writing |
58
+ | `--project` | Write the project-local config instead of the global one (cursor, vscode, roo-code, continue — needs an explicit client) |
59
+ | `--dry-run` | Print the target paths and resulting files without writing |
60
+ | `--uninstall` | Remove the `mergesafe` entry (no client = every detected client) |
46
61
  | `--help` | Usage |
47
62
 
48
63
  ## Development
49
64
 
50
65
  ```bash
51
66
  npm test # node --test, zero deps
52
- node bin/mergesafe-connect.js cursor --key msk_x --dry-run
67
+ node bin/mergesafe-connect.js --key msk_x --dry-run # auto-detect
68
+ node bin/mergesafe-connect.js cursor --key msk_x --dry-run # single client
53
69
  ```
54
70
 
55
71
  ## Publishing
56
72
 
57
- The `@mergesafe` npm scope must exist (create the org on npmjs.com), then:
58
-
59
73
  ```bash
60
74
  npm publish --access public
61
75
  ```
package/lib/actions.js ADDED
@@ -0,0 +1,88 @@
1
+ /** Install/uninstall actions for one client: back up, merge, write. */
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+
5
+ import { SERVER_NAME } from "./clients.js";
6
+ import { applyEntry, removeEntry } from "./config-file.js";
7
+
8
+ export async function handleInstall(client, key, configPath, dryRun) {
9
+ const existingText = await readIfExists(configPath);
10
+
11
+ const newText = client.buildFileContent
12
+ ? // Dedicated-file clients (Continue): whole-file write
13
+ client.buildFileContent(key)
14
+ : // Merge via applyEntry — only the mergesafe entry is touched
15
+ applyEntry(existingText ?? "", client.rootKey, SERVER_NAME, client.buildEntry(key));
16
+
17
+ if (dryRun) {
18
+ console.log(`Would write ${configPath}:\n\n${newText}`);
19
+ return;
20
+ }
21
+
22
+ await backupIfPresent(existingText, configPath);
23
+ await fs.mkdir(path.dirname(configPath), { recursive: true });
24
+ await fs.writeFile(configPath, newText, "utf8");
25
+
26
+ console.log(`✔ ${client.label} is connected to MergeSafe (${configPath})`);
27
+ console.log(`→ ${client.restartHint}`);
28
+ }
29
+
30
+ export async function handleUninstall(client, configPath, dryRun) {
31
+ const existingText = await readIfExists(configPath);
32
+
33
+ if (existingText === null) {
34
+ console.log(`⚠ No config found at ${configPath} — nothing to remove.`);
35
+ return;
36
+ }
37
+
38
+ if (client.deleteFile) {
39
+ // Continue: delete the dedicated YAML file entirely
40
+ if (!dryRun) {
41
+ await fs.unlink(configPath);
42
+ }
43
+ console.log(`🗑 ${dryRun ? "Would remove" : "Removed"} ${configPath}`);
44
+ console.log(`→ ${client.restartHint}`);
45
+ return;
46
+ }
47
+
48
+ if (!client.deleteKey) {
49
+ throw new Error(`Client "${client.label}" does not support --uninstall`);
50
+ }
51
+
52
+ const newText = removeEntry(existingText, client.rootKey, SERVER_NAME);
53
+
54
+ if (newText === null) {
55
+ console.log(`⚠ No mergesafe entry found in ${configPath} — nothing to remove.`);
56
+ return;
57
+ }
58
+
59
+ if (dryRun) {
60
+ console.log(`Would write ${configPath}:\n\n${newText}`);
61
+ return;
62
+ }
63
+
64
+ await backupIfPresent(existingText, configPath);
65
+ await fs.writeFile(configPath, newText, "utf8");
66
+ console.log(`🗑 Removed mergesafe entry from ${configPath}`);
67
+ console.log(`→ ${client.restartHint}`);
68
+ }
69
+
70
+ async function backupIfPresent(existingText, configPath) {
71
+ if (existingText === null) return;
72
+ const backupPath = `${configPath}.bak-${timestamp()}`;
73
+ await fs.copyFile(configPath, backupPath);
74
+ console.log(`• Backed up existing config to ${backupPath}`);
75
+ }
76
+
77
+ async function readIfExists(filePath) {
78
+ try {
79
+ return await fs.readFile(filePath, "utf8");
80
+ } catch (err) {
81
+ if (err.code === "ENOENT") return null;
82
+ throw err;
83
+ }
84
+ }
85
+
86
+ function timestamp() {
87
+ return new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
88
+ }
package/lib/cli.js CHANGED
@@ -1,35 +1,38 @@
1
- /** CLI flow: parse args, resolve client + key, back up, merge, write. */
2
- import fs from "node:fs/promises";
1
+ /** CLI flow: parse args, detect or resolve clients, delegate to actions. */
3
2
  import os from "node:os";
4
- import path from "node:path";
5
3
  import readline from "node:readline/promises";
6
4
  import { parseArgs } from "node:util";
7
5
 
6
+ import { handleInstall, handleUninstall } from "./actions.js";
8
7
  import { SERVER_NAME, getClient, listClientIds } from "./clients.js";
9
- import { applyEntry } from "./config-file.js";
8
+ import { detectClients } from "./detect.js";
10
9
 
11
- const USAGE = `Usage: npx @mergesafe-io/connect <client> [options]
10
+ const USAGE = `Usage: npx @mergesafe-io/connect [client] [options]
12
11
 
13
- Writes the MergeSafe MCP server entry into the client's config file.
14
- The existing file is backed up first; only the "${SERVER_NAME}" entry is touched.
12
+ Connects your AI clients to the MergeSafe MCP server — globally, so it works
13
+ in every project. With no <client> argument, every installed client is
14
+ detected and configured in one go. Existing files are backed up first; only
15
+ the "${SERVER_NAME}" entry is touched.
15
16
 
16
- Clients:
17
- claude-desktop Claude Desktop (global config, via mcp-remote bridge)
18
- cursor Cursor — global ~/.cursor/mcp.json (--project for ./.cursor/mcp.json)
19
- cursor-local Cursor local stdio server — ./.cursor/mcp.json (needs mergesafe-mcp)
17
+ Clients (all global unless noted):
18
+ claude-code Claude Code — ~/.claude.json (user scope, every project)
19
+ claude-desktop Claude Desktop — claude_desktop_config.json (via mcp-remote bridge)
20
+ cursor Cursor — ~/.cursor/mcp.json (--project for ./.cursor/mcp.json)
20
21
  windsurf Windsurf — ~/.codeium/windsurf/mcp_config.json
21
- vscode VS Code — .vscode/mcp.json in the current project
22
- opencode opencode — opencode.json in the current project
22
+ vscode VS Code — user-profile mcp.json (--project for ./.vscode/mcp.json)
23
+ opencode opencode — ~/.config/opencode/config.json
23
24
  cline Cline — cline_mcp_settings.json (stock VS Code global storage)
24
- roo-code Roo Code — .roo/mcp.json in the current project
25
- continue Continue — .continue/mcpServers/mergesafe.yaml in the current project
25
+ roo-code Roo Code — VS Code global storage (--project for ./.roo/mcp.json)
26
+ continue Continue — ~/.continue/mcpServers/mergesafe.yaml (--project for ./.continue/…)
26
27
 
27
28
  Options:
28
29
  --key <msk_...> MergeSafe API key (create one on app.mergesafe.io/dashboard/settings;
29
30
  prompted interactively if omitted)
30
- --project Write the project-local config instead of the global one (cursor)
31
+ --project Write the project-local config instead of the global one
32
+ (cursor, vscode, roo-code, continue — needs an explicit client)
31
33
  --dry-run Print the target path and resulting file without writing
32
- --help Show this help
34
+ --uninstall Remove the mergesafe entry (no client = every detected client)
35
+ --help Show usage
33
36
  `;
34
37
 
35
38
  export async function main(argv) {
@@ -40,20 +43,16 @@ export async function main(argv) {
40
43
  key: { type: "string" },
41
44
  project: { type: "boolean", default: false },
42
45
  "dry-run": { type: "boolean", default: false },
46
+ uninstall: { type: "boolean", default: false },
43
47
  help: { type: "boolean", default: false },
44
48
  },
45
49
  });
46
50
 
47
- if (values.help || positionals.length === 0) {
51
+ if (values.help) {
48
52
  console.log(USAGE);
49
- if (!values.help) {
50
- throw new Error(`Missing <client> argument. Supported: ${listClientIds().join(", ")}`);
51
- }
52
53
  return;
53
54
  }
54
55
 
55
- const client = getClient(positionals[0]);
56
- const key = await resolveKey(values.key);
57
56
  const ctx = {
58
57
  platform: process.platform,
59
58
  home: os.homedir(),
@@ -62,29 +61,71 @@ export async function main(argv) {
62
61
  project: values.project,
63
62
  };
64
63
 
65
- const configPath = client.configPath(ctx);
66
- const existingText = await readIfExists(configPath);
67
- // Dedicated-file clients (Continue) get a whole-file write; the rest are
68
- // merged into the shared JSON config without touching other entries.
69
- const newText = client.buildFileContent
70
- ? client.buildFileContent(key)
71
- : applyEntry(existingText ?? "", client.rootKey, SERVER_NAME, client.buildEntry(key));
72
-
73
- if (values["dry-run"]) {
74
- console.log(`Would write ${configPath}:\n\n${newText}`);
64
+ // --uninstall can come before the client name: "uninstall cursor" or "cursor --uninstall"
65
+ const filtered = positionals.filter((p) => p !== "--uninstall");
66
+ if (filtered.length === 0) {
67
+ await runAllDetected(values, ctx);
75
68
  return;
76
69
  }
77
70
 
78
- if (existingText !== null) {
79
- const backupPath = `${configPath}.bak-${timestamp()}`;
80
- await fs.copyFile(configPath, backupPath);
81
- console.log(`• Backed up existing config to ${backupPath}`);
71
+ const client = getClient(filtered[0]);
72
+ const configPath = client.configPath(ctx);
73
+ if (values.uninstall) {
74
+ await handleUninstall(client, configPath, values["dry-run"]);
75
+ } else {
76
+ const key = await resolveKey(values.key);
77
+ await handleInstall(client, key, configPath, values["dry-run"]);
78
+ }
79
+ }
80
+
81
+ /** No-client mode: detect every installed client and configure them globally. */
82
+ async function runAllDetected(values, ctx) {
83
+ if (values.project) {
84
+ throw new Error(
85
+ "--project needs an explicit client (e.g. `npx @mergesafe-io/connect cursor --project`).",
86
+ );
87
+ }
88
+
89
+ const detected = detectClients(ctx);
90
+ if (detected.length === 0) {
91
+ console.log(USAGE);
92
+ throw new Error(
93
+ `No supported AI client detected on this machine. Specify one explicitly: ${listClientIds().join(", ")}`,
94
+ );
95
+ }
96
+ console.log(`Detected clients: ${detected.map((id) => getClient(id).label).join(", ")}\n`);
97
+
98
+ const key = values.uninstall ? null : await resolveKey(values.key);
99
+ const failures = [];
100
+ for (const id of detected) {
101
+ const client = getClient(id);
102
+ try {
103
+ if (values.uninstall) {
104
+ await handleUninstall(client, client.configPath(ctx), values["dry-run"]);
105
+ } else {
106
+ await handleInstall(client, key, client.configPath(ctx), values["dry-run"]);
107
+ }
108
+ } catch (err) {
109
+ failures.push(`${client.label}: ${err.message}`);
110
+ }
111
+ console.log("");
82
112
  }
83
- await fs.mkdir(path.dirname(configPath), { recursive: true });
84
- await fs.writeFile(configPath, newText, "utf8");
85
113
 
86
- console.log(`✔ ${client.label} is connected to MergeSafe (${configPath})`);
87
- console.log(`→ ${client.restartHint}`);
114
+ reportOutcome(detected.length, failures, values.uninstall);
115
+ }
116
+
117
+ function reportOutcome(total, failures, uninstall) {
118
+ for (const failure of failures) {
119
+ console.error(`⚠ ${failure}`);
120
+ }
121
+ if (failures.length === total) {
122
+ throw new Error("Nothing was configured — every detected client failed (see above).");
123
+ }
124
+ if (!uninstall) {
125
+ console.log(
126
+ "✔ MergeSafe is connected globally — restart your clients and the tools work in every project.",
127
+ );
128
+ }
88
129
  }
89
130
 
90
131
  async function resolveKey(flagValue) {
@@ -102,16 +143,3 @@ async function resolveKey(flagValue) {
102
143
  }
103
144
  return key;
104
145
  }
105
-
106
- async function readIfExists(filePath) {
107
- try {
108
- return await fs.readFile(filePath, "utf8");
109
- } catch (err) {
110
- if (err.code === "ENOENT") return null;
111
- throw err;
112
- }
113
- }
114
-
115
- function timestamp() {
116
- return new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
117
- }
package/lib/clients.js CHANGED
@@ -1,6 +1,8 @@
1
1
  /** Per-client knowledge: where the MCP config lives and what to write in it. */
2
2
  import path from "node:path";
3
3
 
4
+ import { claudeDesktopConfigPath, vscodeUserDir } from "./paths.js";
5
+
4
6
  export const MCP_HTTP_URL = "https://mcp.mergesafe.io/mcp";
5
7
  // Legacy endpoint, kept for clients without solid Streamable HTTP support
6
8
  // (opencode: SSE-only "remote" type; cline: open transport bugs; continue:
@@ -11,12 +13,29 @@ export const SERVER_NAME = "mergesafe";
11
13
  /**
12
14
  * ctx: { platform, home, cwd, env, project } — injected by the CLI so
13
15
  * every path decision stays testable without touching the real machine.
16
+ *
17
+ * Every client is global (user-wide) by default; `--project` opts the few
18
+ * clients that support it into a project-local config instead.
19
+ * `detectPaths` lists markers whose existence means the client is installed.
14
20
  */
15
21
  const CLIENTS = {
22
+ "claude-code": {
23
+ label: "Claude Code",
24
+ rootKey: "mcpServers",
25
+ configPath: (ctx) => path.join(ctx.home, ".claude.json"),
26
+ detectPaths: (ctx) => [path.join(ctx.home, ".claude.json"), path.join(ctx.home, ".claude")],
27
+ deleteKey: true,
28
+ // Same entry `claude mcp add --scope user --transport http` writes:
29
+ // user scope, so every project and session sees the server.
30
+ buildEntry: (key) => ({ type: "http", url: MCP_HTTP_URL, headers: bearer(key) }),
31
+ restartHint: "Restart any open Claude Code sessions — the server is user-wide, every project gets it.",
32
+ },
16
33
  "claude-desktop": {
17
34
  label: "Claude Desktop",
18
35
  rootKey: "mcpServers",
19
36
  configPath: (ctx) => claudeDesktopConfigPath(ctx),
37
+ detectPaths: (ctx) => [path.dirname(claudeDesktopConfigPath(ctx))],
38
+ deleteKey: true,
20
39
  // Claude Desktop has no native remote + Bearer support; bridge via
21
40
  // mcp-remote (http-first: tries Streamable HTTP, falls back to SSE).
22
41
  // No space after "Authorization:" and env-expanded token — both
@@ -35,6 +54,8 @@ const CLIENTS = {
35
54
  ctx.project
36
55
  ? path.join(ctx.cwd, ".cursor", "mcp.json")
37
56
  : path.join(ctx.home, ".cursor", "mcp.json"),
57
+ detectPaths: (ctx) => [path.join(ctx.home, ".cursor")],
58
+ deleteKey: true,
38
59
  // Cursor auto-detects Streamable HTTP from the URL (SSE fallback built in).
39
60
  buildEntry: (key) => ({ url: MCP_HTTP_URL, headers: bearer(key) }),
40
61
  restartHint: "Restart Cursor or reload the window.",
@@ -43,60 +64,63 @@ const CLIENTS = {
43
64
  label: "Windsurf",
44
65
  rootKey: "mcpServers",
45
66
  configPath: (ctx) => path.join(ctx.home, ".codeium", "windsurf", "mcp_config.json"),
67
+ detectPaths: (ctx) => [path.join(ctx.home, ".codeium", "windsurf")],
68
+ deleteKey: true,
46
69
  buildEntry: (key) => ({ serverUrl: MCP_HTTP_URL, headers: bearer(key) }),
47
70
  restartHint: "Fully quit and reopen Windsurf.",
48
71
  },
49
72
  vscode: {
50
- label: "VS Code (project)",
73
+ label: "VS Code",
51
74
  rootKey: "servers",
52
- configPath: (ctx) => path.join(ctx.cwd, ".vscode", "mcp.json"),
75
+ configPath: (ctx) =>
76
+ ctx.project
77
+ ? path.join(ctx.cwd, ".vscode", "mcp.json")
78
+ : path.join(vscodeUserDir(ctx), "mcp.json"),
79
+ detectPaths: (ctx) => [vscodeUserDir(ctx)],
80
+ deleteKey: true,
53
81
  buildEntry: (key) => ({ type: "http", url: MCP_HTTP_URL, headers: bearer(key) }),
54
- restartHint: "Open mcp.json in VS Code and press the “Start” CodeLens.",
82
+ restartHint: "Restart VS Code, then check MCP: List Servers in the command palette.",
55
83
  },
56
84
  opencode: {
57
- label: "opencode (project)",
85
+ label: "opencode",
58
86
  rootKey: "mcp",
59
- configPath: (ctx) => path.join(ctx.cwd, "opencode.json"),
87
+ configPath: (ctx) => path.join(ctx.home, ".config", "opencode", "config.json"),
88
+ detectPaths: (ctx) => [path.join(ctx.home, ".config", "opencode")],
89
+ deleteKey: true,
60
90
  buildEntry: (key) => ({ type: "remote", url: MCP_SSE_URL, enabled: true, headers: bearer(key) }),
61
91
  restartHint: "Restart opencode for changes to take effect.",
62
92
  },
63
- "cursor-local": {
64
- label: "Cursor (local server)",
65
- rootKey: "mcpServers",
66
- configPath: (ctx) => path.join(ctx.cwd, ".cursor", "mcp.json"),
67
- // Local stdio server so Cursor can scan the local project — requires the
68
- // mergesafe-mcp command (Python package) on PATH; Cursor starts it itself.
69
- buildEntry: (key) => ({
70
- command: "mergesafe-mcp",
71
- env: {
72
- MCP_TRANSPORT: "stdio",
73
- MERGESAFE_API_URL: "https://api.mergesafe.io",
74
- MERGESAFE_FRONTEND_URL: "https://app.mergesafe.io",
75
- MERGESAFE_API_KEY: key,
76
- },
77
- }),
78
- restartHint:
79
- "Restart Cursor. Requires the mergesafe-mcp command installed (Python 3.11+).",
80
- },
81
93
  cline: {
82
94
  label: "Cline",
83
95
  rootKey: "mcpServers",
84
96
  configPath: (ctx) =>
85
97
  path.join(vscodeUserDir(ctx), "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json"),
98
+ detectPaths: (ctx) => [path.join(vscodeUserDir(ctx), "globalStorage", "saoudrizwan.claude-dev")],
99
+ deleteKey: true,
86
100
  buildEntry: (key) => ({ url: MCP_SSE_URL, headers: bearer(key) }),
87
101
  restartHint: "Reload the VS Code window (stock VS Code path — not Insiders/VSCodium).",
88
102
  },
89
103
  "roo-code": {
90
- label: "Roo Code (project)",
104
+ label: "Roo Code",
91
105
  rootKey: "mcpServers",
92
- configPath: (ctx) => path.join(ctx.cwd, ".roo", "mcp.json"),
106
+ configPath: (ctx) =>
107
+ ctx.project
108
+ ? path.join(ctx.cwd, ".roo", "mcp.json")
109
+ : path.join(vscodeUserDir(ctx), "globalStorage", "rooveterinaryinc.roo-cline", "settings", "mcp_settings.json"),
110
+ detectPaths: (ctx) => [path.join(vscodeUserDir(ctx), "globalStorage", "rooveterinaryinc.roo-cline")],
111
+ deleteKey: true,
93
112
  buildEntry: (key) => ({ type: "sse", url: MCP_SSE_URL, headers: bearer(key), disabled: false }),
94
- restartHint: "Reload the VS Code window; project config overrides global settings.",
113
+ restartHint: "Reload the VS Code window.",
95
114
  },
96
115
  continue: {
97
- label: "Continue (project)",
98
- configPath: (ctx) => path.join(ctx.cwd, ".continue", "mcpServers", "mergesafe.yaml"),
116
+ label: "Continue",
117
+ configPath: (ctx) =>
118
+ ctx.project
119
+ ? path.join(ctx.cwd, ".continue", "mcpServers", "mergesafe.yaml")
120
+ : path.join(ctx.home, ".continue", "mcpServers", "mergesafe.yaml"),
121
+ detectPaths: (ctx) => [path.join(ctx.home, ".continue")],
99
122
  // Continue uses one YAML file per MCP server — whole-file write, no merge.
123
+ deleteFile: true,
100
124
  buildFileContent: (key) =>
101
125
  [
102
126
  "name: Mergesafe MCP",
@@ -119,32 +143,6 @@ function bearer(key) {
119
143
  return { Authorization: `Bearer ${key}` };
120
144
  }
121
145
 
122
- /** Stock VS Code user dir per OS (globalStorage lives under it). */
123
- function vscodeUserDir(ctx) {
124
- if (ctx.platform === "darwin") {
125
- return path.join(ctx.home, "Library", "Application Support", "Code", "User");
126
- }
127
- if (ctx.platform === "win32") {
128
- const appData = ctx.env.APPDATA || path.join(ctx.home, "AppData", "Roaming");
129
- return path.join(appData, "Code", "User");
130
- }
131
- const configHome = ctx.env.XDG_CONFIG_HOME || path.join(ctx.home, ".config");
132
- return path.join(configHome, "Code", "User");
133
- }
134
-
135
- function claudeDesktopConfigPath(ctx) {
136
- const file = "claude_desktop_config.json";
137
- if (ctx.platform === "darwin") {
138
- return path.join(ctx.home, "Library", "Application Support", "Claude", file);
139
- }
140
- if (ctx.platform === "win32") {
141
- const appData = ctx.env.APPDATA || path.join(ctx.home, "AppData", "Roaming");
142
- return path.join(appData, "Claude", file);
143
- }
144
- const configHome = ctx.env.XDG_CONFIG_HOME || path.join(ctx.home, ".config");
145
- return path.join(configHome, "Claude", file);
146
- }
147
-
148
146
  export function getClient(id) {
149
147
  const client = CLIENTS[id];
150
148
  if (!client) {
@@ -14,6 +14,40 @@ export function applyEntry(text, rootKey, serverName, entry) {
14
14
  return `${JSON.stringify(config, null, 2)}\n`;
15
15
  }
16
16
 
17
+ /**
18
+ * Remove the `serverName` entry under `rootKey` from the JSON text.
19
+ * Preserves all other entries. If the section becomes empty, removes it too.
20
+ * Returns the new file text (2-space indent, trailing newline).
21
+ */
22
+ export function removeEntry(text, rootKey, serverName) {
23
+ const config = parseConfig(text);
24
+ const section = config[rootKey];
25
+
26
+ if (!section || typeof section !== "object" || Array.isArray(section)) {
27
+ // Nothing to remove — the config has no such section
28
+ return null;
29
+ }
30
+
31
+ const copy = { ...section };
32
+ delete copy[serverName];
33
+
34
+ if (Object.keys(copy).length === 0) {
35
+ delete config[rootKey];
36
+ } else {
37
+ config[rootKey] = copy;
38
+ }
39
+
40
+ return `${JSON.stringify(config, null, 2)}\n`;
41
+ }
42
+
43
+ /**
44
+ * Remove the mergesafe server entry from a dedicated YAML file (Continue).
45
+ * Returns the new file text (overwrite), or null if the file doesn't exist.
46
+ */
47
+ export function removeFileContent() {
48
+ return "";
49
+ }
50
+
17
51
  function parseConfig(text) {
18
52
  if (!text || !text.trim()) {
19
53
  return {};
package/lib/detect.js ADDED
@@ -0,0 +1,14 @@
1
+ /** Detect which supported AI clients are installed on this machine. */
2
+ import fs from "node:fs";
3
+
4
+ import { getClient, listClientIds } from "./clients.js";
5
+
6
+ /**
7
+ * Return the ids of every client whose marker path exists.
8
+ * `exists` is injectable so detection stays testable without a real machine.
9
+ */
10
+ export function detectClients(ctx, exists = fs.existsSync) {
11
+ return listClientIds().filter((id) =>
12
+ getClient(id).detectPaths(ctx).some((markerPath) => exists(markerPath)),
13
+ );
14
+ }
package/lib/paths.js ADDED
@@ -0,0 +1,29 @@
1
+ /** Per-OS filesystem locations shared by several clients. */
2
+ import path from "node:path";
3
+
4
+ /** Stock VS Code user dir per OS (globalStorage and user mcp.json live under it). */
5
+ export function vscodeUserDir(ctx) {
6
+ if (ctx.platform === "darwin") {
7
+ return path.join(ctx.home, "Library", "Application Support", "Code", "User");
8
+ }
9
+ if (ctx.platform === "win32") {
10
+ const appData = ctx.env.APPDATA || path.join(ctx.home, "AppData", "Roaming");
11
+ return path.join(appData, "Code", "User");
12
+ }
13
+ const configHome = ctx.env.XDG_CONFIG_HOME || path.join(ctx.home, ".config");
14
+ return path.join(configHome, "Code", "User");
15
+ }
16
+
17
+ /** Claude Desktop's claude_desktop_config.json per OS. */
18
+ export function claudeDesktopConfigPath(ctx) {
19
+ const file = "claude_desktop_config.json";
20
+ if (ctx.platform === "darwin") {
21
+ return path.join(ctx.home, "Library", "Application Support", "Claude", file);
22
+ }
23
+ if (ctx.platform === "win32") {
24
+ const appData = ctx.env.APPDATA || path.join(ctx.home, "AppData", "Roaming");
25
+ return path.join(appData, "Claude", file);
26
+ }
27
+ const configHome = ctx.env.XDG_CONFIG_HOME || path.join(ctx.home, ".config");
28
+ return path.join(configHome, "Claude", file);
29
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@mergesafe-io/connect",
3
- "version": "0.2.0",
4
- "description": "One-command MergeSafe MCP setup for Claude Desktop, Cursor, Windsurf, VS Code and opencode.",
3
+ "version": "0.4.0",
4
+ "description": "One command connects every AI client on your machine to MergeSafe MCP — Claude Code, Claude Desktop, Cursor, Windsurf, VS Code and more, globally.",
5
5
  "keywords": ["mergesafe", "mcp", "model-context-protocol", "installer"],
6
6
  "homepage": "https://app.mergesafe.io",
7
7
  "license": "MIT",