@mergesafe-io/connect 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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,43 @@ 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 sse mergesafe https://mcp.mergesafe.io/sse --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
+ | `--with-rules` | Also write agent rules files (`.cursor/rules/mergesafe.mdc`, `AGENTS.md` block) so the AI runs MergeSafe after coding |
60
+ | `--dry-run` | Print the target paths and resulting files without writing |
61
+ | `--uninstall` | Remove the `mergesafe` entry (no client = every detected client) |
46
62
  | `--help` | Usage |
47
63
 
48
64
  ## Development
49
65
 
50
66
  ```bash
51
67
  npm test # node --test, zero deps
52
- node bin/mergesafe-connect.js cursor --key msk_x --dry-run
68
+ node bin/mergesafe-connect.js --key msk_x --dry-run # auto-detect
69
+ node bin/mergesafe-connect.js cursor --key msk_x --dry-run # single client
53
70
  ```
54
71
 
55
72
  ## Publishing
56
73
 
57
- The `@mergesafe` npm scope must exist (create the org on npmjs.com), then:
58
-
59
74
  ```bash
60
75
  npm publish --access public
61
76
  ```
package/lib/actions.js ADDED
@@ -0,0 +1,104 @@
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
+ import { writeRulesFiles } from "./rules.js";
8
+
9
+ export async function handleInstall(client, key, configPath, dryRun, withRules, cwd) {
10
+ const existingText = await readIfExists(configPath);
11
+
12
+ const newText = client.buildFileContent
13
+ ? // Dedicated-file clients (Continue): whole-file write
14
+ client.buildFileContent(key)
15
+ : // Merge via applyEntry — only the mergesafe entry is touched
16
+ applyEntry(existingText ?? "", client.rootKey, SERVER_NAME, client.buildEntry(key));
17
+
18
+ if (dryRun) {
19
+ console.log(`Would write ${configPath}:\n\n${newText}`);
20
+ if (withRules) {
21
+ console.log("Would also write agent rules files (.cursor/rules/mergesafe.mdc, AGENTS.md).");
22
+ }
23
+ return;
24
+ }
25
+
26
+ await backupIfPresent(existingText, configPath);
27
+ await fs.mkdir(path.dirname(configPath), { recursive: true });
28
+ await fs.writeFile(configPath, newText, "utf8");
29
+
30
+ console.log(`✔ ${client.label} is connected to MergeSafe (${configPath})`);
31
+ console.log(`→ ${client.restartHint}`);
32
+
33
+ if (withRules) {
34
+ await reportRules(cwd);
35
+ }
36
+ }
37
+
38
+ export async function handleUninstall(client, configPath, dryRun) {
39
+ const existingText = await readIfExists(configPath);
40
+
41
+ if (existingText === null) {
42
+ console.log(`⚠ No config found at ${configPath} — nothing to remove.`);
43
+ return;
44
+ }
45
+
46
+ if (client.deleteFile) {
47
+ // Continue: delete the dedicated YAML file entirely
48
+ if (!dryRun) {
49
+ await fs.unlink(configPath);
50
+ }
51
+ console.log(`🗑 ${dryRun ? "Would remove" : "Removed"} ${configPath}`);
52
+ console.log(`→ ${client.restartHint}`);
53
+ return;
54
+ }
55
+
56
+ if (!client.deleteKey) {
57
+ throw new Error(`Client "${client.label}" does not support --uninstall`);
58
+ }
59
+
60
+ const newText = removeEntry(existingText, client.rootKey, SERVER_NAME);
61
+
62
+ if (newText === null) {
63
+ console.log(`⚠ No mergesafe entry found in ${configPath} — nothing to remove.`);
64
+ return;
65
+ }
66
+
67
+ if (dryRun) {
68
+ console.log(`Would write ${configPath}:\n\n${newText}`);
69
+ return;
70
+ }
71
+
72
+ await backupIfPresent(existingText, configPath);
73
+ await fs.writeFile(configPath, newText, "utf8");
74
+ console.log(`🗑 Removed mergesafe entry from ${configPath}`);
75
+ console.log(`→ ${client.restartHint}`);
76
+ }
77
+
78
+ export async function reportRules(cwd) {
79
+ const written = await writeRulesFiles({ cwd });
80
+ for (const file of written) {
81
+ console.log(`✔ Rules written: ${file}`);
82
+ }
83
+ console.log("→ Your AI agent will now run MergeSafe after each coding session.");
84
+ }
85
+
86
+ async function backupIfPresent(existingText, configPath) {
87
+ if (existingText === null) return;
88
+ const backupPath = `${configPath}.bak-${timestamp()}`;
89
+ await fs.copyFile(configPath, backupPath);
90
+ console.log(`• Backed up existing config to ${backupPath}`);
91
+ }
92
+
93
+ async function readIfExists(filePath) {
94
+ try {
95
+ return await fs.readFile(filePath, "utf8");
96
+ } catch (err) {
97
+ if (err.code === "ENOENT") return null;
98
+ throw err;
99
+ }
100
+ }
101
+
102
+ function timestamp() {
103
+ return new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
104
+ }
package/lib/cli.js CHANGED
@@ -1,35 +1,40 @@
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, reportRules } 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)
33
+ --with-rules Also write agent rules files (.cursor/rules/mergesafe.mdc,
34
+ AGENTS.md block) so the AI runs MergeSafe after coding
31
35
  --dry-run Print the target path and resulting file without writing
32
- --help Show this help
36
+ --uninstall Remove the mergesafe entry (no client = every detected client)
37
+ --help Show usage
33
38
  `;
34
39
 
35
40
  export async function main(argv) {
@@ -39,21 +44,18 @@ export async function main(argv) {
39
44
  options: {
40
45
  key: { type: "string" },
41
46
  project: { type: "boolean", default: false },
47
+ "with-rules": { type: "boolean", default: false },
42
48
  "dry-run": { type: "boolean", default: false },
49
+ uninstall: { type: "boolean", default: false },
43
50
  help: { type: "boolean", default: false },
44
51
  },
45
52
  });
46
53
 
47
- if (values.help || positionals.length === 0) {
54
+ if (values.help) {
48
55
  console.log(USAGE);
49
- if (!values.help) {
50
- throw new Error(`Missing <client> argument. Supported: ${listClientIds().join(", ")}`);
51
- }
52
56
  return;
53
57
  }
54
58
 
55
- const client = getClient(positionals[0]);
56
- const key = await resolveKey(values.key);
57
59
  const ctx = {
58
60
  platform: process.platform,
59
61
  home: os.homedir(),
@@ -62,29 +64,74 @@ export async function main(argv) {
62
64
  project: values.project,
63
65
  };
64
66
 
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}`);
67
+ // --uninstall can come before the client name: "uninstall cursor" or "cursor --uninstall"
68
+ const filtered = positionals.filter((p) => p !== "--uninstall");
69
+ if (filtered.length === 0) {
70
+ await runAllDetected(values, ctx);
75
71
  return;
76
72
  }
77
73
 
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}`);
74
+ const client = getClient(filtered[0]);
75
+ const configPath = client.configPath(ctx);
76
+ if (values.uninstall) {
77
+ await handleUninstall(client, configPath, values["dry-run"]);
78
+ } else {
79
+ const key = await resolveKey(values.key);
80
+ await handleInstall(client, key, configPath, values["dry-run"], values["with-rules"], ctx.cwd);
81
+ }
82
+ }
83
+
84
+ /** No-client mode: detect every installed client and configure them globally. */
85
+ async function runAllDetected(values, ctx) {
86
+ if (values.project) {
87
+ throw new Error(
88
+ "--project needs an explicit client (e.g. `npx @mergesafe-io/connect cursor --project`).",
89
+ );
90
+ }
91
+
92
+ const detected = detectClients(ctx);
93
+ if (detected.length === 0) {
94
+ console.log(USAGE);
95
+ throw new Error(
96
+ `No supported AI client detected on this machine. Specify one explicitly: ${listClientIds().join(", ")}`,
97
+ );
98
+ }
99
+ console.log(`Detected clients: ${detected.map((id) => getClient(id).label).join(", ")}\n`);
100
+
101
+ const key = values.uninstall ? null : await resolveKey(values.key);
102
+ const failures = [];
103
+ for (const id of detected) {
104
+ const client = getClient(id);
105
+ try {
106
+ if (values.uninstall) {
107
+ await handleUninstall(client, client.configPath(ctx), values["dry-run"]);
108
+ } else {
109
+ await handleInstall(client, key, client.configPath(ctx), values["dry-run"], false, ctx.cwd);
110
+ }
111
+ } catch (err) {
112
+ failures.push(`${client.label}: ${err.message}`);
113
+ }
114
+ console.log("");
82
115
  }
83
- await fs.mkdir(path.dirname(configPath), { recursive: true });
84
- await fs.writeFile(configPath, newText, "utf8");
85
116
 
86
- console.log(`✔ ${client.label} is connected to MergeSafe (${configPath})`);
87
- console.log(`→ ${client.restartHint}`);
117
+ if (values["with-rules"] && !values.uninstall && !values["dry-run"]) {
118
+ await reportRules(ctx.cwd);
119
+ }
120
+ reportOutcome(detected.length, failures, values.uninstall);
121
+ }
122
+
123
+ function reportOutcome(total, failures, uninstall) {
124
+ for (const failure of failures) {
125
+ console.error(`⚠ ${failure}`);
126
+ }
127
+ if (failures.length === total) {
128
+ throw new Error("Nothing was configured — every detected client failed (see above).");
129
+ }
130
+ if (!uninstall) {
131
+ console.log(
132
+ "✔ MergeSafe is connected globally — restart your clients and the tools work in every project.",
133
+ );
134
+ }
88
135
  }
89
136
 
90
137
  async function resolveKey(flagValue) {
@@ -102,16 +149,3 @@ async function resolveKey(flagValue) {
102
149
  }
103
150
  return key;
104
151
  }
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,24 +1,48 @@
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
+
6
+ export const MCP_HTTP_URL = "https://mcp.mergesafe.io/mcp";
7
+ // Legacy endpoint, kept for clients without solid Streamable HTTP support
8
+ // (opencode: SSE-only "remote" type; cline: open transport bugs; continue:
9
+ // header auth unverified on streamable-http). Revisit as they catch up.
4
10
  export const MCP_SSE_URL = "https://mcp.mergesafe.io/sse";
5
11
  export const SERVER_NAME = "mergesafe";
6
12
 
7
13
  /**
8
14
  * ctx: { platform, home, cwd, env, project } — injected by the CLI so
9
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.
10
20
  */
11
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
+ },
12
33
  "claude-desktop": {
13
34
  label: "Claude Desktop",
14
35
  rootKey: "mcpServers",
15
36
  configPath: (ctx) => claudeDesktopConfigPath(ctx),
16
- // Claude Desktop has no native SSE + Bearer support; bridge via
17
- // mcp-remote. No space after "Authorization:" and env-expanded token —
18
- // both required by mcp-remote arg parsing.
37
+ detectPaths: (ctx) => [path.dirname(claudeDesktopConfigPath(ctx))],
38
+ deleteKey: true,
39
+ // Claude Desktop has no native remote + Bearer support; bridge via
40
+ // mcp-remote (http-first: tries Streamable HTTP, falls back to SSE).
41
+ // No space after "Authorization:" and env-expanded token — both
42
+ // required by mcp-remote arg parsing.
19
43
  buildEntry: (key) => ({
20
44
  command: "npx",
21
- args: ["mcp-remote", MCP_SSE_URL, "--header", "Authorization:Bearer ${AUTH_TOKEN}"],
45
+ args: ["mcp-remote", MCP_HTTP_URL, "--header", "Authorization:Bearer ${AUTH_TOKEN}"],
22
46
  env: { AUTH_TOKEN: key },
23
47
  }),
24
48
  restartHint: "Fully quit and reopen Claude Desktop (needs Node.js 18+ for npx).",
@@ -30,67 +54,73 @@ const CLIENTS = {
30
54
  ctx.project
31
55
  ? path.join(ctx.cwd, ".cursor", "mcp.json")
32
56
  : path.join(ctx.home, ".cursor", "mcp.json"),
33
- buildEntry: (key) => ({ url: MCP_SSE_URL, headers: bearer(key) }),
57
+ detectPaths: (ctx) => [path.join(ctx.home, ".cursor")],
58
+ deleteKey: true,
59
+ // Cursor auto-detects Streamable HTTP from the URL (SSE fallback built in).
60
+ buildEntry: (key) => ({ url: MCP_HTTP_URL, headers: bearer(key) }),
34
61
  restartHint: "Restart Cursor or reload the window.",
35
62
  },
36
63
  windsurf: {
37
64
  label: "Windsurf",
38
65
  rootKey: "mcpServers",
39
66
  configPath: (ctx) => path.join(ctx.home, ".codeium", "windsurf", "mcp_config.json"),
40
- buildEntry: (key) => ({ serverUrl: MCP_SSE_URL, headers: bearer(key) }),
67
+ detectPaths: (ctx) => [path.join(ctx.home, ".codeium", "windsurf")],
68
+ deleteKey: true,
69
+ buildEntry: (key) => ({ serverUrl: MCP_HTTP_URL, headers: bearer(key) }),
41
70
  restartHint: "Fully quit and reopen Windsurf.",
42
71
  },
43
72
  vscode: {
44
- label: "VS Code (project)",
73
+ label: "VS Code",
45
74
  rootKey: "servers",
46
- configPath: (ctx) => path.join(ctx.cwd, ".vscode", "mcp.json"),
47
- buildEntry: (key) => ({ type: "sse", url: MCP_SSE_URL, headers: bearer(key) }),
48
- restartHint: "Open mcp.json in VS Code and press the “Start” CodeLens.",
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,
81
+ buildEntry: (key) => ({ type: "http", url: MCP_HTTP_URL, headers: bearer(key) }),
82
+ restartHint: "Restart VS Code, then check MCP: List Servers in the command palette.",
49
83
  },
50
84
  opencode: {
51
- label: "opencode (project)",
85
+ label: "opencode",
52
86
  rootKey: "mcp",
53
- 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,
54
90
  buildEntry: (key) => ({ type: "remote", url: MCP_SSE_URL, enabled: true, headers: bearer(key) }),
55
91
  restartHint: "Restart opencode for changes to take effect.",
56
92
  },
57
- "cursor-local": {
58
- label: "Cursor (local server)",
59
- rootKey: "mcpServers",
60
- configPath: (ctx) => path.join(ctx.cwd, ".cursor", "mcp.json"),
61
- // Local stdio server so Cursor can scan the local project — requires the
62
- // mergesafe-mcp command (Python package) on PATH; Cursor starts it itself.
63
- buildEntry: (key) => ({
64
- command: "mergesafe-mcp",
65
- env: {
66
- MCP_TRANSPORT: "stdio",
67
- MERGESAFE_API_URL: "https://api.mergesafe.io",
68
- MERGESAFE_FRONTEND_URL: "https://app.mergesafe.io",
69
- MERGESAFE_API_KEY: key,
70
- },
71
- }),
72
- restartHint:
73
- "Restart Cursor. Requires the mergesafe-mcp command installed (Python 3.11+).",
74
- },
75
93
  cline: {
76
94
  label: "Cline",
77
95
  rootKey: "mcpServers",
78
96
  configPath: (ctx) =>
79
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,
80
100
  buildEntry: (key) => ({ url: MCP_SSE_URL, headers: bearer(key) }),
81
101
  restartHint: "Reload the VS Code window (stock VS Code path — not Insiders/VSCodium).",
82
102
  },
83
103
  "roo-code": {
84
- label: "Roo Code (project)",
104
+ label: "Roo Code",
85
105
  rootKey: "mcpServers",
86
- 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,
87
112
  buildEntry: (key) => ({ type: "sse", url: MCP_SSE_URL, headers: bearer(key), disabled: false }),
88
- restartHint: "Reload the VS Code window; project config overrides global settings.",
113
+ restartHint: "Reload the VS Code window.",
89
114
  },
90
115
  continue: {
91
- label: "Continue (project)",
92
- 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")],
93
122
  // Continue uses one YAML file per MCP server — whole-file write, no merge.
123
+ deleteFile: true,
94
124
  buildFileContent: (key) =>
95
125
  [
96
126
  "name: Mergesafe MCP",
@@ -113,32 +143,6 @@ function bearer(key) {
113
143
  return { Authorization: `Bearer ${key}` };
114
144
  }
115
145
 
116
- /** Stock VS Code user dir per OS (globalStorage lives under it). */
117
- function vscodeUserDir(ctx) {
118
- if (ctx.platform === "darwin") {
119
- return path.join(ctx.home, "Library", "Application Support", "Code", "User");
120
- }
121
- if (ctx.platform === "win32") {
122
- const appData = ctx.env.APPDATA || path.join(ctx.home, "AppData", "Roaming");
123
- return path.join(appData, "Code", "User");
124
- }
125
- const configHome = ctx.env.XDG_CONFIG_HOME || path.join(ctx.home, ".config");
126
- return path.join(configHome, "Code", "User");
127
- }
128
-
129
- function claudeDesktopConfigPath(ctx) {
130
- const file = "claude_desktop_config.json";
131
- if (ctx.platform === "darwin") {
132
- return path.join(ctx.home, "Library", "Application Support", "Claude", file);
133
- }
134
- if (ctx.platform === "win32") {
135
- const appData = ctx.env.APPDATA || path.join(ctx.home, "AppData", "Roaming");
136
- return path.join(appData, "Claude", file);
137
- }
138
- const configHome = ctx.env.XDG_CONFIG_HOME || path.join(ctx.home, ".config");
139
- return path.join(configHome, "Claude", file);
140
- }
141
-
142
146
  export function getClient(id) {
143
147
  const client = CLIENTS[id];
144
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/lib/rules.js ADDED
@@ -0,0 +1,80 @@
1
+ /** Agent rules files: make the AI call MergeSafe after every coding session.
2
+ *
3
+ * MCP server instructions are honored unevenly across clients; project rules
4
+ * files are the reliable trigger. This module owns:
5
+ * - .cursor/rules/mergesafe.mdc (whole file, alwaysApply)
6
+ * - AGENTS.md / CLAUDE.md (one marked block, upserted in place)
7
+ */
8
+ import fs from "node:fs/promises";
9
+ import path from "node:path";
10
+
11
+ export const RULES_START = "<!-- mergesafe:start -->";
12
+ export const RULES_END = "<!-- mergesafe:end -->";
13
+
14
+ const RULE_TEXT = `After generating or modifying code in this project, run the MergeSafe MCP tool \`analyze_directory\` on the project root before you conclude, and report its verdict. If \`release_status\` is "blocked", fix the listed blockers (each finding ships a ready-to-use fix prompt) and re-run the check before declaring the work done.`;
15
+
16
+ const CURSOR_RULE = `---
17
+ description: MergeSafe post-generation check
18
+ alwaysApply: true
19
+ ---
20
+
21
+ ${RULE_TEXT}
22
+ `;
23
+
24
+ const MARKDOWN_BLOCK = `${RULES_START}
25
+ ## MergeSafe check
26
+
27
+ ${RULE_TEXT}
28
+ ${RULES_END}`;
29
+
30
+ /** Upsert the marked MergeSafe block into a markdown document (idempotent). */
31
+ export function applyRulesBlock(existingText) {
32
+ const text = existingText ?? "";
33
+ const start = text.indexOf(RULES_START);
34
+ const end = text.indexOf(RULES_END);
35
+ if (start !== -1 && end !== -1 && end > start) {
36
+ return text.slice(0, start) + MARKDOWN_BLOCK + text.slice(end + RULES_END.length);
37
+ }
38
+ if (!text.trim()) return `${MARKDOWN_BLOCK}\n`;
39
+ return `${text.replace(/\n*$/, "")}\n\n${MARKDOWN_BLOCK}\n`;
40
+ }
41
+
42
+ async function readIfExists(filePath) {
43
+ try {
44
+ return await fs.readFile(filePath, "utf8");
45
+ } catch (err) {
46
+ if (err.code === "ENOENT") return null;
47
+ throw err;
48
+ }
49
+ }
50
+
51
+ /**
52
+ * Write the rules files for a project.
53
+ *
54
+ * AGENTS.md is always upserted (created if missing — it is the cross-agent
55
+ * standard); CLAUDE.md is only updated when it already exists, so we never
56
+ * plant a client-specific file uninvited.
57
+ *
58
+ * @returns {Promise<string[]>} the paths written.
59
+ */
60
+ export async function writeRulesFiles({ cwd }) {
61
+ const written = [];
62
+
63
+ const mdcPath = path.join(cwd, ".cursor", "rules", "mergesafe.mdc");
64
+ await fs.mkdir(path.dirname(mdcPath), { recursive: true });
65
+ await fs.writeFile(mdcPath, CURSOR_RULE, "utf8");
66
+ written.push(mdcPath);
67
+
68
+ const agentsPath = path.join(cwd, "AGENTS.md");
69
+ await fs.writeFile(agentsPath, applyRulesBlock(await readIfExists(agentsPath)), "utf8");
70
+ written.push(agentsPath);
71
+
72
+ const claudePath = path.join(cwd, "CLAUDE.md");
73
+ const claudeText = await readIfExists(claudePath);
74
+ if (claudeText !== null) {
75
+ await fs.writeFile(claudePath, applyRulesBlock(claudeText), "utf8");
76
+ written.push(claudePath);
77
+ }
78
+
79
+ return written;
80
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@mergesafe-io/connect",
3
- "version": "0.1.0",
4
- "description": "One-command MergeSafe MCP setup for Claude Desktop, Cursor, Windsurf, VS Code and opencode.",
3
+ "version": "0.3.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",