@cg3/equip 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 CG3 LLC
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,95 @@
1
+ # @cg3/equip
2
+
3
+ Universal MCP server + behavioral rules installer for AI coding agents.
4
+
5
+ Equip handles the hard part of distributing your MCP tool: detecting which AI coding platforms are installed, writing the correct config format for each one, and managing versioned behavioral rules — all with zero dependencies.
6
+
7
+ ## Supported Platforms
8
+
9
+ | Platform | MCP Config | Rules |
10
+ |---|---|---|
11
+ | Claude Code | `~/.claude.json` (`mcpServers`) | `~/.claude/CLAUDE.md` (append) |
12
+ | Cursor | `~/.cursor/mcp.json` (`mcpServers`) | Clipboard (no writable global path) |
13
+ | Windsurf | `~/.codeium/windsurf/mcp_config.json` (`mcpServers`) | `global_rules.md` (append) |
14
+ | VS Code | `Code/User/mcp.json` (`servers`, `type: "http"`) | Clipboard |
15
+ | Cline | `globalStorage/.../cline_mcp_settings.json` (`mcpServers`) | `~/Documents/Cline/Rules/` (standalone file) |
16
+ | Roo Code | `globalStorage/.../cline_mcp_settings.json` (`mcpServers`) | `~/.roo/rules/` (standalone file) |
17
+
18
+ ## Usage
19
+
20
+ ```js
21
+ const { Equip } = require("@cg3/equip");
22
+
23
+ const equip = new Equip({
24
+ name: "my-tool",
25
+ serverUrl: "https://mcp.example.com",
26
+ rules: {
27
+ content: `<!-- my-tool:v1.0.0 -->\n## My Tool\nAlways check My Tool first.\n<!-- /my-tool -->`,
28
+ version: "1.0.0",
29
+ marker: "my-tool",
30
+ fileName: "my-tool.md", // For platforms with rules directories
31
+ },
32
+ });
33
+
34
+ // Detect installed platforms
35
+ const platforms = equip.detect();
36
+
37
+ // Install MCP + rules on all detected platforms
38
+ for (const p of platforms) {
39
+ equip.installMcp(p, "api_key_here");
40
+ equip.installRules(p);
41
+ }
42
+
43
+ // Uninstall
44
+ for (const p of platforms) {
45
+ equip.uninstallMcp(p);
46
+ equip.uninstallRules(p);
47
+ }
48
+ ```
49
+
50
+ ## API
51
+
52
+ ### `new Equip(config)`
53
+
54
+ - `config.name` — Server name in MCP configs (required)
55
+ - `config.serverUrl` — Remote MCP server URL (required unless `stdio` provided)
56
+ - `config.rules` — Behavioral rules config (optional)
57
+ - `content` — Markdown content with version markers
58
+ - `version` — Version string for idempotency tracking
59
+ - `marker` — Marker name used in `<!-- marker:vX.X -->` comments
60
+ - `fileName` — Standalone filename for directory-based platforms
61
+ - `clipboardPlatforms` — Platform IDs that use clipboard (default: `["cursor", "vscode"]`)
62
+ - `config.stdio` — Stdio transport config (optional, alternative to HTTP)
63
+ - `command`, `args`, `envKey`
64
+
65
+ ### Instance Methods
66
+
67
+ - `equip.detect()` — Returns array of detected platform objects
68
+ - `equip.installMcp(platform, apiKey, options?)` — Install MCP config
69
+ - `equip.uninstallMcp(platform, dryRun?)` — Remove MCP config
70
+ - `equip.updateMcpKey(platform, apiKey, transport?)` — Update API key
71
+ - `equip.installRules(platform, options?)` — Install behavioral rules
72
+ - `equip.uninstallRules(platform, dryRun?)` — Remove behavioral rules
73
+ - `equip.readMcp(platform)` — Check if MCP is configured
74
+ - `equip.buildConfig(platformId, apiKey, transport?)` — Build MCP config object
75
+
76
+ ### Primitives
77
+
78
+ All internal functions are also exported for advanced usage:
79
+
80
+ ```js
81
+ const { detectPlatforms, installMcpJson, installRules, createManualPlatform, platformName, cli } = require("@cg3/equip");
82
+ ```
83
+
84
+ ## Key Features
85
+
86
+ - **Zero dependencies** — Pure Node.js, works with Node 18+
87
+ - **Platform-aware** — Handles each platform's config quirks (root keys, URL fields, type requirements)
88
+ - **Non-destructive** — Merges into existing configs, creates backups, preserves other servers
89
+ - **Versioned rules** — Marker-based blocks enable idempotent updates without clobbering user content
90
+ - **Dry-run support** — Preview changes without writing files
91
+ - **CLI helpers** — Colored output, prompts, clipboard utilities included
92
+
93
+ ## License
94
+
95
+ MIT — CG3 LLC
package/index.js ADDED
@@ -0,0 +1,172 @@
1
+ // @cg3/equip — Universal MCP + behavioral rules installer for AI coding agents.
2
+ // Zero dependencies. Works with Claude Code, Cursor, Windsurf, VS Code, Cline, Roo Code.
3
+
4
+ "use strict";
5
+
6
+ const { detectPlatforms, whichSync, dirExists, fileExists } = require("./lib/detect");
7
+ const { readMcpEntry, buildHttpConfig, buildHttpConfigWithAuth, buildStdioConfig, installMcp, installMcpJson, uninstallMcp, updateMcpKey } = require("./lib/mcp");
8
+ const { parseRulesVersion, installRules, uninstallRules, markerPatterns } = require("./lib/rules");
9
+ const { createManualPlatform, platformName, KNOWN_PLATFORMS } = require("./lib/platforms");
10
+ const cli = require("./lib/cli");
11
+
12
+ /**
13
+ * Equip — configure AI coding tools with your MCP server and behavioral rules.
14
+ *
15
+ * @example
16
+ * const equip = new Equip({
17
+ * name: "prior",
18
+ * serverUrl: "https://api.cg3.io/mcp",
19
+ * rules: { content: "...", version: "0.5.3", marker: "prior" },
20
+ * });
21
+ * const platforms = equip.detect();
22
+ * await equip.install(platforms, { apiKey: "ask_xxx" });
23
+ */
24
+ class Equip {
25
+ /**
26
+ * @param {object} config
27
+ * @param {string} config.name - Server name in MCP configs (e.g., "prior")
28
+ * @param {string} config.serverUrl - Remote MCP server URL
29
+ * @param {object} [config.rules] - Behavioral rules config
30
+ * @param {string} config.rules.content - Rules markdown content (with markers)
31
+ * @param {string} config.rules.version - Version string for marker tracking
32
+ * @param {string} config.rules.marker - Marker name (e.g., "prior")
33
+ * @param {string} [config.rules.fileName] - Standalone file name for file-based platforms (e.g., "prior.md")
34
+ * @param {string[]} [config.rules.clipboardPlatforms] - Platforms using clipboard (default: ["cursor", "vscode"])
35
+ * @param {object} [config.stdio] - Stdio transport config (alternative to HTTP)
36
+ * @param {string} config.stdio.command - Command to run
37
+ * @param {string[]} config.stdio.args - Command arguments
38
+ * @param {string} config.stdio.envKey - Env var name for API key
39
+ */
40
+ constructor(config) {
41
+ if (!config.name) throw new Error("Equip: name is required");
42
+ if (!config.serverUrl && !config.stdio) throw new Error("Equip: serverUrl or stdio is required");
43
+
44
+ this.name = config.name;
45
+ this.serverUrl = config.serverUrl;
46
+ this.rules = config.rules || null;
47
+ this.stdio = config.stdio || null;
48
+ }
49
+
50
+ /**
51
+ * Detect installed AI coding platforms.
52
+ * @returns {Array<object>} Platform objects
53
+ */
54
+ detect() {
55
+ return detectPlatforms(this.name);
56
+ }
57
+
58
+ /**
59
+ * Build MCP config for a platform.
60
+ * @param {string} platformId - Platform id
61
+ * @param {string} apiKey - API key
62
+ * @param {string} [transport="http"] - "http" or "stdio"
63
+ * @returns {object} MCP config object
64
+ */
65
+ buildConfig(platformId, apiKey, transport = "http") {
66
+ if (transport === "stdio" && this.stdio) {
67
+ const env = { [this.stdio.envKey]: apiKey };
68
+ return buildStdioConfig(this.stdio.command, this.stdio.args, env);
69
+ }
70
+ return buildHttpConfigWithAuth(this.serverUrl, apiKey, platformId);
71
+ }
72
+
73
+ /**
74
+ * Install MCP config on a platform.
75
+ * @param {object} platform - Platform object from detect()
76
+ * @param {string} apiKey - API key
77
+ * @param {object} [options] - { transport, dryRun }
78
+ * @returns {{ success: boolean, method: string }}
79
+ */
80
+ installMcp(platform, apiKey, options = {}) {
81
+ const { transport = "http", dryRun = false } = options;
82
+ const config = this.buildConfig(platform.platform, apiKey, transport);
83
+ return installMcp(platform, this.name, config, { dryRun, serverUrl: this.serverUrl });
84
+ }
85
+
86
+ /**
87
+ * Uninstall MCP config from a platform.
88
+ * @param {object} platform - Platform object
89
+ * @param {boolean} [dryRun=false]
90
+ * @returns {boolean}
91
+ */
92
+ uninstallMcp(platform, dryRun = false) {
93
+ return uninstallMcp(platform, this.name, dryRun);
94
+ }
95
+
96
+ /**
97
+ * Update API key in MCP config.
98
+ * @param {object} platform - Platform object
99
+ * @param {string} apiKey - New API key
100
+ * @param {string} [transport="http"]
101
+ * @returns {{ success: boolean, method: string }}
102
+ */
103
+ updateMcpKey(platform, apiKey, transport = "http") {
104
+ const config = this.buildConfig(platform.platform, apiKey, transport);
105
+ return updateMcpKey(platform, this.name, config);
106
+ }
107
+
108
+ /**
109
+ * Install behavioral rules on a platform.
110
+ * @param {object} platform - Platform object
111
+ * @param {object} [options] - { dryRun }
112
+ * @returns {{ action: string }}
113
+ */
114
+ installRules(platform, options = {}) {
115
+ if (!this.rules) return { action: "skipped" };
116
+ return installRules(platform, {
117
+ content: this.rules.content,
118
+ version: this.rules.version,
119
+ marker: this.rules.marker,
120
+ fileName: this.rules.fileName,
121
+ clipboardPlatforms: this.rules.clipboardPlatforms,
122
+ dryRun: options.dryRun || false,
123
+ copyToClipboard: cli.copyToClipboard,
124
+ });
125
+ }
126
+
127
+ /**
128
+ * Uninstall behavioral rules from a platform.
129
+ * @param {object} platform - Platform object
130
+ * @param {boolean} [dryRun=false]
131
+ * @returns {boolean}
132
+ */
133
+ uninstallRules(platform, dryRun = false) {
134
+ if (!this.rules) return false;
135
+ return uninstallRules(platform, {
136
+ marker: this.rules.marker,
137
+ fileName: this.rules.fileName,
138
+ dryRun,
139
+ });
140
+ }
141
+
142
+ /**
143
+ * Check if MCP is configured on a platform.
144
+ * @param {object} platform - Platform object
145
+ * @returns {object|null} Existing MCP config or null
146
+ */
147
+ readMcp(platform) {
148
+ return readMcpEntry(platform.configPath, platform.rootKey, this.name);
149
+ }
150
+ }
151
+
152
+ module.exports = {
153
+ Equip,
154
+ // Re-export primitives for advanced usage
155
+ detectPlatforms,
156
+ readMcpEntry,
157
+ buildHttpConfig,
158
+ buildHttpConfigWithAuth,
159
+ buildStdioConfig,
160
+ installMcp,
161
+ installMcpJson,
162
+ uninstallMcp,
163
+ updateMcpKey,
164
+ installRules,
165
+ uninstallRules,
166
+ parseRulesVersion,
167
+ markerPatterns,
168
+ createManualPlatform,
169
+ platformName,
170
+ KNOWN_PLATFORMS,
171
+ cli,
172
+ };
package/lib/cli.js ADDED
@@ -0,0 +1,95 @@
1
+ // CLI output helpers, prompts, and clipboard.
2
+ // Zero dependencies.
3
+
4
+ "use strict";
5
+
6
+ const os = require("os");
7
+ const readline = require("readline");
8
+
9
+ // ─── Colors ──────────────────────────────────────────────────
10
+
11
+ const GREEN = "\x1b[32m";
12
+ const RED = "\x1b[31m";
13
+ const YELLOW = "\x1b[33m";
14
+ const CYAN = "\x1b[36m";
15
+ const BOLD = "\x1b[1m";
16
+ const DIM = "\x1b[2m";
17
+ const RESET = "\x1b[0m";
18
+
19
+ // ─── Output ──────────────────────────────────────────────────
20
+
21
+ function log(msg = "") { process.stderr.write(msg + "\n"); }
22
+ function ok(msg) { log(` ${GREEN}✓${RESET} ${msg}`); }
23
+ function fail(msg) { log(` ${RED}✗${RESET} ${msg}`); }
24
+ function warn(msg) { log(` ${YELLOW}⚠${RESET} ${msg}`); }
25
+ function info(msg) { log(` ${CYAN}ⓘ${RESET} ${msg}`); }
26
+ function step(n, total, title) { log(`\n${BOLD}[${n}/${total}] ${title}${RESET}`); }
27
+
28
+ // ─── Prompts ─────────────────────────────────────────────────
29
+
30
+ function prompt(question) {
31
+ return new Promise((resolve) => {
32
+ const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
33
+ rl.question(question, (answer) => { rl.close(); resolve(answer.trim()); });
34
+ });
35
+ }
36
+
37
+ /**
38
+ * Prompt that resolves on Enter (true) or Esc (false).
39
+ * Falls back to readline if stdin isn't a TTY.
40
+ */
41
+ function promptEnterOrEsc(question) {
42
+ return new Promise((resolve) => {
43
+ process.stderr.write(question);
44
+ if (!process.stdin.isTTY) {
45
+ const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
46
+ rl.question("", (answer) => { rl.close(); resolve(answer.toLowerCase() !== "n"); });
47
+ return;
48
+ }
49
+ process.stdin.setRawMode(true);
50
+ process.stdin.resume();
51
+ process.stdin.setEncoding("utf-8");
52
+ const onData = (key) => {
53
+ process.stdin.setRawMode(false);
54
+ process.stdin.pause();
55
+ process.stdin.removeListener("data", onData);
56
+ if (key === "\x1b") { process.stderr.write("\n"); resolve(false); }
57
+ else if (key === "\r" || key === "\n") { process.stderr.write("\n"); resolve(true); }
58
+ else if (key === "\x03") { process.stderr.write("\n"); process.exit(0); }
59
+ else { process.stderr.write("\n"); resolve(true); }
60
+ };
61
+ process.stdin.on("data", onData);
62
+ });
63
+ }
64
+
65
+ // ─── Clipboard ───────────────────────────────────────────────
66
+
67
+ function copyToClipboard(text) {
68
+ try {
69
+ const cp = require("child_process");
70
+ if (process.platform === "darwin") {
71
+ cp.execSync("pbcopy", { input: text, timeout: 3000 });
72
+ } else if (process.platform === "win32") {
73
+ cp.execSync("clip", { input: text, timeout: 3000 });
74
+ } else {
75
+ try { cp.execSync("xclip -selection clipboard", { input: text, timeout: 3000 }); }
76
+ catch { try { cp.execSync("xsel --clipboard --input", { input: text, timeout: 3000 }); }
77
+ catch { cp.execSync("wl-copy", { input: text, timeout: 3000 }); } }
78
+ }
79
+ return true;
80
+ } catch { return false; }
81
+ }
82
+
83
+ // ─── Utilities ───────────────────────────────────────────────
84
+
85
+ function sanitizeError(msg) {
86
+ return msg.replace(os.homedir(), "~");
87
+ }
88
+
89
+ module.exports = {
90
+ GREEN, RED, YELLOW, CYAN, BOLD, DIM, RESET,
91
+ log, ok, fail, warn, info, step,
92
+ prompt, promptEnterOrEsc,
93
+ copyToClipboard,
94
+ sanitizeError,
95
+ };
package/lib/detect.js ADDED
@@ -0,0 +1,156 @@
1
+ // Platform detection — discovers installed AI coding tools.
2
+ // Zero dependencies.
3
+
4
+ "use strict";
5
+
6
+ const fs = require("fs");
7
+ const path = require("path");
8
+ const os = require("os");
9
+ const { execSync } = require("child_process");
10
+ const { getVsCodeMcpPath, getVsCodeUserDir, getClineConfigPath, getRooConfigPath } = require("./platforms");
11
+ const { readMcpEntry } = require("./mcp");
12
+
13
+ // ─── Helpers ─────────────────────────────────────────────────
14
+
15
+ function whichSync(cmd) {
16
+ try {
17
+ const r = execSync(process.platform === "win32" ? `where ${cmd} 2>nul` : `which ${cmd} 2>/dev/null`, { encoding: "utf-8", timeout: 5000 });
18
+ return r.trim().split(/\r?\n/)[0] || null;
19
+ } catch { return null; }
20
+ }
21
+
22
+ function dirExists(p) {
23
+ try { return fs.statSync(p).isDirectory(); } catch { return false; }
24
+ }
25
+
26
+ function fileExists(p) {
27
+ try { return fs.statSync(p).isFile(); } catch { return false; }
28
+ }
29
+
30
+ function cliVersion(cmd, regex) {
31
+ try {
32
+ const out = execSync(`${cmd} --version 2>&1`, { encoding: "utf-8", timeout: 5000 });
33
+ const m = out.match(regex || /(\d+\.\d+[\.\d]*)/);
34
+ return m ? m[1] : "unknown";
35
+ } catch { return null; }
36
+ }
37
+
38
+ function getClaudeCodeVersion() {
39
+ try {
40
+ const out = execSync("claude --version 2>&1", { encoding: "utf-8", timeout: 5000 });
41
+ const m = out.match(/(\d+\.\d+[\.\d]*)/);
42
+ return m ? m[1] : "unknown";
43
+ } catch { return null; }
44
+ }
45
+
46
+ // ─── Detection ──────────────────────────────────────────────
47
+
48
+ /**
49
+ * Detect installed AI coding platforms.
50
+ * @param {string} [serverName] - MCP server name to check for existing config (default: null)
51
+ * @returns {Array<object>} Array of platform objects
52
+ */
53
+ function detectPlatforms(serverName) {
54
+ const home = os.homedir();
55
+ const platforms = [];
56
+
57
+ // Claude Code
58
+ const claudeVersion = whichSync("claude") ? getClaudeCodeVersion() : null;
59
+ if (claudeVersion || dirExists(path.join(home, ".claude"))) {
60
+ const configPath = path.join(home, ".claude.json");
61
+ const rulesPath = path.join(home, ".claude", "CLAUDE.md");
62
+ platforms.push({
63
+ platform: "claude-code",
64
+ version: claudeVersion || "unknown",
65
+ configPath,
66
+ rulesPath,
67
+ existingMcp: serverName ? readMcpEntry(configPath, "mcpServers", serverName) : null,
68
+ hasCli: !!whichSync("claude"),
69
+ rootKey: "mcpServers",
70
+ });
71
+ }
72
+
73
+ // Cursor
74
+ const cursorDir = path.join(home, ".cursor");
75
+ if (whichSync("cursor") || dirExists(cursorDir)) {
76
+ const configPath = path.join(cursorDir, "mcp.json");
77
+ platforms.push({
78
+ platform: "cursor",
79
+ version: cliVersion("cursor") || "unknown",
80
+ configPath,
81
+ rulesPath: null, // Cursor: clipboard only
82
+ existingMcp: serverName ? readMcpEntry(configPath, "mcpServers", serverName) : null,
83
+ hasCli: !!whichSync("cursor"),
84
+ rootKey: "mcpServers",
85
+ });
86
+ }
87
+
88
+ // Windsurf
89
+ const windsurfDir = path.join(home, ".codeium", "windsurf");
90
+ if (dirExists(windsurfDir)) {
91
+ const configPath = path.join(windsurfDir, "mcp_config.json");
92
+ const rulesPath = path.join(windsurfDir, "memories", "global_rules.md");
93
+ platforms.push({
94
+ platform: "windsurf",
95
+ version: "unknown",
96
+ configPath,
97
+ rulesPath,
98
+ existingMcp: serverName ? readMcpEntry(configPath, "mcpServers", serverName) : null,
99
+ hasCli: false,
100
+ rootKey: "mcpServers",
101
+ });
102
+ }
103
+
104
+ // VS Code (Copilot)
105
+ const vscodeMcpPath = getVsCodeMcpPath();
106
+ if (whichSync("code") || fileExists(vscodeMcpPath) || dirExists(getVsCodeUserDir())) {
107
+ platforms.push({
108
+ platform: "vscode",
109
+ version: cliVersion("code") || "unknown",
110
+ configPath: vscodeMcpPath,
111
+ rulesPath: null, // VS Code: clipboard only
112
+ existingMcp: serverName ? readMcpEntry(vscodeMcpPath, "servers", serverName) : null,
113
+ hasCli: !!whichSync("code"),
114
+ rootKey: "servers",
115
+ });
116
+ }
117
+
118
+ // Cline (VS Code extension)
119
+ const clineConfigPath = getClineConfigPath();
120
+ if (fileExists(clineConfigPath) || dirExists(path.dirname(clineConfigPath))) {
121
+ const home_ = os.homedir();
122
+ platforms.push({
123
+ platform: "cline",
124
+ version: "unknown",
125
+ configPath: clineConfigPath,
126
+ rulesPath: path.join(home_, "Documents", "Cline", "Rules"),
127
+ existingMcp: serverName ? readMcpEntry(clineConfigPath, "mcpServers", serverName) : null,
128
+ hasCli: false,
129
+ rootKey: "mcpServers",
130
+ });
131
+ }
132
+
133
+ // Roo Code (VS Code extension)
134
+ const rooConfigPath = getRooConfigPath();
135
+ if (fileExists(rooConfigPath) || dirExists(path.dirname(rooConfigPath))) {
136
+ platforms.push({
137
+ platform: "roo-code",
138
+ version: "unknown",
139
+ configPath: rooConfigPath,
140
+ rulesPath: path.join(os.homedir(), ".roo", "rules"),
141
+ existingMcp: serverName ? readMcpEntry(rooConfigPath, "mcpServers", serverName) : null,
142
+ hasCli: false,
143
+ rootKey: "mcpServers",
144
+ });
145
+ }
146
+
147
+ return platforms;
148
+ }
149
+
150
+ module.exports = {
151
+ detectPlatforms,
152
+ whichSync,
153
+ dirExists,
154
+ fileExists,
155
+ cliVersion,
156
+ };
package/lib/mcp.js ADDED
@@ -0,0 +1,233 @@
1
+ // MCP config read/write/merge/uninstall.
2
+ // Handles all platform-specific config format differences.
3
+ // Zero dependencies.
4
+
5
+ "use strict";
6
+
7
+ const fs = require("fs");
8
+ const path = require("path");
9
+ const { execSync } = require("child_process");
10
+
11
+ // ─── Read ────────────────────────────────────────────────────
12
+
13
+ /**
14
+ * Read an MCP server entry from a config file.
15
+ * @param {string} configPath - Path to config JSON file
16
+ * @param {string} rootKey - Root key ("mcpServers" or "servers")
17
+ * @param {string} serverName - Server name to read
18
+ * @returns {object|null} Server config or null
19
+ */
20
+ function readMcpEntry(configPath, rootKey, serverName) {
21
+ try {
22
+ let raw = fs.readFileSync(configPath, "utf-8");
23
+ if (raw.charCodeAt(0) === 0xFEFF) raw = raw.slice(1); // Strip BOM
24
+ const data = JSON.parse(raw);
25
+ return data?.[rootKey]?.[serverName] || null;
26
+ } catch { return null; }
27
+ }
28
+
29
+ // ─── Config Builders ─────────────────────────────────────────
30
+
31
+ /**
32
+ * Build HTTP MCP config for a platform.
33
+ * Handles platform-specific field names (url vs serverUrl, type field).
34
+ * @param {string} serverUrl - MCP server URL
35
+ * @param {string} platform - Platform id
36
+ * @returns {object} MCP config object
37
+ */
38
+ function buildHttpConfig(serverUrl, platform) {
39
+ if (platform === "windsurf") return { serverUrl };
40
+ if (platform === "vscode") return { type: "http", url: serverUrl };
41
+ return { url: serverUrl };
42
+ }
43
+
44
+ /**
45
+ * Build HTTP MCP config with auth headers.
46
+ * @param {string} serverUrl - MCP server URL
47
+ * @param {string} apiKey - API key for auth
48
+ * @param {string} platform - Platform id
49
+ * @param {object} [extraHeaders] - Additional headers
50
+ * @returns {object} MCP config with headers
51
+ */
52
+ function buildHttpConfigWithAuth(serverUrl, apiKey, platform, extraHeaders) {
53
+ return {
54
+ ...buildHttpConfig(serverUrl, platform),
55
+ headers: {
56
+ Authorization: `Bearer ${apiKey}`,
57
+ ...extraHeaders,
58
+ },
59
+ };
60
+ }
61
+
62
+ /**
63
+ * Build stdio MCP config.
64
+ * @param {string} command - Command to run
65
+ * @param {string[]} args - Command arguments
66
+ * @param {object} env - Environment variables
67
+ * @returns {object} MCP stdio config
68
+ */
69
+ function buildStdioConfig(command, args, env) {
70
+ if (process.platform === "win32") {
71
+ return { command: "cmd", args: ["/c", command, ...args], env };
72
+ }
73
+ return { command, args, env };
74
+ }
75
+
76
+ // ─── Install ─────────────────────────────────────────────────
77
+
78
+ function fileExists(p) {
79
+ try { return fs.statSync(p).isFile(); } catch { return false; }
80
+ }
81
+
82
+ /**
83
+ * Install MCP config for a platform.
84
+ * Tries platform CLI first (if available), falls back to JSON write.
85
+ * @param {object} platform - Platform object from detect
86
+ * @param {string} serverName - Server name (e.g., "prior")
87
+ * @param {object} mcpEntry - MCP config object
88
+ * @param {object} [options] - { dryRun, serverUrl }
89
+ * @returns {{ success: boolean, method: string }}
90
+ */
91
+ function installMcp(platform, serverName, mcpEntry, options = {}) {
92
+ const { dryRun = false, serverUrl } = options;
93
+
94
+ // Claude Code: try CLI first
95
+ if (platform.platform === "claude-code" && platform.hasCli && mcpEntry.url) {
96
+ try {
97
+ if (!dryRun) {
98
+ const headerArgs = mcpEntry.headers
99
+ ? Object.entries(mcpEntry.headers).map(([k, v]) => `--header "${k}: ${v}"`).join(" ")
100
+ : "";
101
+ execSync(`claude mcp add --transport http -s user ${headerArgs} ${serverName} ${mcpEntry.url}`, {
102
+ encoding: "utf-8", timeout: 15000, stdio: "pipe",
103
+ });
104
+ const check = readMcpEntry(platform.configPath, platform.rootKey, serverName);
105
+ if (check) return { success: true, method: "cli" };
106
+ } else {
107
+ return { success: true, method: "cli" };
108
+ }
109
+ } catch { /* fall through */ }
110
+ }
111
+
112
+ // Cursor: try CLI first
113
+ if (platform.platform === "cursor" && platform.hasCli) {
114
+ try {
115
+ const mcpJson = JSON.stringify({ name: serverName, ...mcpEntry });
116
+ if (!dryRun) {
117
+ execSync(`cursor --add-mcp '${mcpJson.replace(/'/g, "'\\''")}'`, {
118
+ encoding: "utf-8", timeout: 15000, stdio: "pipe",
119
+ });
120
+ const check = readMcpEntry(platform.configPath, platform.rootKey, serverName);
121
+ if (check) return { success: true, method: "cli" };
122
+ } else {
123
+ return { success: true, method: "cli" };
124
+ }
125
+ } catch { /* fall through */ }
126
+ }
127
+
128
+ // VS Code: try CLI first
129
+ if (platform.platform === "vscode" && platform.hasCli) {
130
+ try {
131
+ const mcpJson = JSON.stringify({ name: serverName, ...mcpEntry });
132
+ if (!dryRun) {
133
+ execSync(`code --add-mcp '${mcpJson.replace(/'/g, "'\\''")}'`, {
134
+ encoding: "utf-8", timeout: 15000, stdio: "pipe",
135
+ });
136
+ const check = readMcpEntry(platform.configPath, platform.rootKey, serverName);
137
+ if (check) return { success: true, method: "cli" };
138
+ } else {
139
+ return { success: true, method: "cli" };
140
+ }
141
+ } catch { /* fall through */ }
142
+ }
143
+
144
+ // JSON write (all platforms, fallback for CLI failures)
145
+ return installMcpJson(platform, serverName, mcpEntry, dryRun);
146
+ }
147
+
148
+ /**
149
+ * Write MCP config directly to JSON file.
150
+ * Merges with existing config, creates backup.
151
+ * @param {object} platform - Platform object
152
+ * @param {string} serverName - Server name
153
+ * @param {object} mcpEntry - MCP config
154
+ * @param {boolean} dryRun
155
+ * @returns {{ success: boolean, method: string }}
156
+ */
157
+ function installMcpJson(platform, serverName, mcpEntry, dryRun) {
158
+ const { configPath, rootKey } = platform;
159
+
160
+ let existing = {};
161
+ try {
162
+ let raw = fs.readFileSync(configPath, "utf-8");
163
+ if (raw.charCodeAt(0) === 0xFEFF) raw = raw.slice(1);
164
+ existing = JSON.parse(raw);
165
+ if (typeof existing !== "object" || existing === null) existing = {};
166
+ } catch { /* start fresh */ }
167
+
168
+ if (!existing[rootKey]) existing[rootKey] = {};
169
+ existing[rootKey][serverName] = mcpEntry;
170
+
171
+ if (!dryRun) {
172
+ const dir = path.dirname(configPath);
173
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
174
+
175
+ if (fileExists(configPath)) {
176
+ try { fs.copyFileSync(configPath, configPath + ".bak"); } catch {}
177
+ }
178
+
179
+ fs.writeFileSync(configPath, JSON.stringify(existing, null, 2) + "\n");
180
+ }
181
+
182
+ return { success: true, method: "json" };
183
+ }
184
+
185
+ /**
186
+ * Remove an MCP server entry from a platform config.
187
+ * @param {object} platform - Platform object
188
+ * @param {string} serverName - Server name to remove
189
+ * @param {boolean} dryRun
190
+ * @returns {boolean} Whether anything was removed
191
+ */
192
+ function uninstallMcp(platform, serverName, dryRun) {
193
+ const { configPath, rootKey } = platform;
194
+ if (!fileExists(configPath)) return false;
195
+
196
+ try {
197
+ const data = JSON.parse(fs.readFileSync(configPath, "utf-8"));
198
+ if (!data?.[rootKey]?.[serverName]) return false;
199
+ delete data[rootKey][serverName];
200
+ if (Object.keys(data[rootKey]).length === 0) delete data[rootKey];
201
+ if (!dryRun) {
202
+ fs.copyFileSync(configPath, configPath + ".bak");
203
+ if (Object.keys(data).length === 0) {
204
+ fs.unlinkSync(configPath);
205
+ } else {
206
+ fs.writeFileSync(configPath, JSON.stringify(data, null, 2) + "\n");
207
+ }
208
+ }
209
+ return true;
210
+ } catch { return false; }
211
+ }
212
+
213
+ /**
214
+ * Update API key in existing MCP config.
215
+ * @param {object} platform - Platform object
216
+ * @param {string} serverName - Server name
217
+ * @param {object} mcpEntry - New MCP config
218
+ * @returns {{ success: boolean, method: string }}
219
+ */
220
+ function updateMcpKey(platform, serverName, mcpEntry) {
221
+ return installMcpJson(platform, serverName, mcpEntry, false);
222
+ }
223
+
224
+ module.exports = {
225
+ readMcpEntry,
226
+ buildHttpConfig,
227
+ buildHttpConfigWithAuth,
228
+ buildStdioConfig,
229
+ installMcp,
230
+ installMcpJson,
231
+ uninstallMcp,
232
+ updateMcpKey,
233
+ };
@@ -0,0 +1,110 @@
1
+ // Platform path resolution and metadata.
2
+ // Zero dependencies.
3
+
4
+ "use strict";
5
+
6
+ const path = require("path");
7
+ const os = require("os");
8
+
9
+ // ─── Path Helpers ────────────────────────────────────────────
10
+
11
+ function getVsCodeUserDir() {
12
+ const home = os.homedir();
13
+ if (process.platform === "win32") return path.join(process.env.APPDATA || path.join(home, "AppData", "Roaming"), "Code", "User");
14
+ if (process.platform === "darwin") return path.join(home, "Library", "Application Support", "Code", "User");
15
+ return path.join(home, ".config", "Code", "User");
16
+ }
17
+
18
+ function getVsCodeMcpPath() {
19
+ return path.join(getVsCodeUserDir(), "mcp.json");
20
+ }
21
+
22
+ function getClineConfigPath() {
23
+ const base = getVsCodeUserDir();
24
+ return path.join(base, "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json");
25
+ }
26
+
27
+ function getRooConfigPath() {
28
+ const base = getVsCodeUserDir();
29
+ return path.join(base, "globalStorage", "rooveterinaryinc.roo-cline", "settings", "cline_mcp_settings.json");
30
+ }
31
+
32
+ // ─── Platform Registry ──────────────────────────────────────
33
+
34
+ /**
35
+ * Returns platform definition for manual override.
36
+ * @param {string} platformId
37
+ * @returns {object} Platform object with configPath, rulesPath, rootKey, etc.
38
+ */
39
+ function createManualPlatform(platformId) {
40
+ const home = os.homedir();
41
+ const configs = {
42
+ "claude-code": {
43
+ configPath: path.join(home, ".claude.json"),
44
+ rulesPath: path.join(home, ".claude", "CLAUDE.md"),
45
+ rootKey: "mcpServers",
46
+ },
47
+ cursor: {
48
+ configPath: path.join(home, ".cursor", "mcp.json"),
49
+ rulesPath: null,
50
+ rootKey: "mcpServers",
51
+ },
52
+ windsurf: {
53
+ configPath: path.join(home, ".codeium", "windsurf", "mcp_config.json"),
54
+ rulesPath: path.join(home, ".codeium", "windsurf", "memories", "global_rules.md"),
55
+ rootKey: "mcpServers",
56
+ },
57
+ vscode: {
58
+ configPath: getVsCodeMcpPath(),
59
+ rulesPath: null,
60
+ rootKey: "servers",
61
+ },
62
+ cline: {
63
+ configPath: getClineConfigPath(),
64
+ rulesPath: path.join(home, "Documents", "Cline", "Rules"),
65
+ rootKey: "mcpServers",
66
+ },
67
+ "roo-code": {
68
+ configPath: getRooConfigPath(),
69
+ rulesPath: path.join(home, ".roo", "rules"),
70
+ rootKey: "mcpServers",
71
+ },
72
+ };
73
+
74
+ const def = configs[platformId];
75
+ if (!def) {
76
+ throw new Error(`Unknown platform: ${platformId}. Supported: ${Object.keys(configs).join(", ")}`);
77
+ }
78
+
79
+ return { platform: platformId, version: "unknown", hasCli: false, existingMcp: null, ...def };
80
+ }
81
+
82
+ /**
83
+ * Display name for a platform id.
84
+ */
85
+ function platformName(id) {
86
+ const names = {
87
+ "claude-code": "Claude Code",
88
+ cursor: "Cursor",
89
+ windsurf: "Windsurf",
90
+ vscode: "VS Code",
91
+ cline: "Cline",
92
+ "roo-code": "Roo Code",
93
+ };
94
+ return names[id] || id;
95
+ }
96
+
97
+ /**
98
+ * All known platform IDs.
99
+ */
100
+ const KNOWN_PLATFORMS = ["claude-code", "cursor", "windsurf", "vscode", "cline", "roo-code"];
101
+
102
+ module.exports = {
103
+ getVsCodeUserDir,
104
+ getVsCodeMcpPath,
105
+ getClineConfigPath,
106
+ getRooConfigPath,
107
+ createManualPlatform,
108
+ platformName,
109
+ KNOWN_PLATFORMS,
110
+ };
package/lib/rules.js ADDED
@@ -0,0 +1,152 @@
1
+ // Behavioral rules installation — marker-based versioned blocks.
2
+ // Handles appending, updating, and removing rules from shared files.
3
+ // Zero dependencies.
4
+
5
+ "use strict";
6
+
7
+ const fs = require("fs");
8
+ const path = require("path");
9
+
10
+ // ─── Constants ──────────────────────────────────────────────
11
+
12
+ /**
13
+ * Create regex patterns for a given marker name.
14
+ * @param {string} marker - Marker name (e.g., "prior")
15
+ * @returns {{ MARKER_RE: RegExp, BLOCK_RE: RegExp }}
16
+ */
17
+ function markerPatterns(marker) {
18
+ return {
19
+ MARKER_RE: new RegExp(`<!-- ${marker}:v[\\d.]+ -->`),
20
+ BLOCK_RE: new RegExp(`<!-- ${marker}:v[\\d.]+ -->[\\s\\S]*?<!-- \\/${marker} -->\\n?`),
21
+ };
22
+ }
23
+
24
+ /**
25
+ * Parse version from marker in content.
26
+ * @param {string} content
27
+ * @param {string} marker
28
+ * @returns {string|null}
29
+ */
30
+ function parseRulesVersion(content, marker) {
31
+ const m = content.match(new RegExp(`<!-- ${marker}:v([\\d.]+) -->`));
32
+ return m ? m[1] : null;
33
+ }
34
+
35
+ // ─── Install ─────────────────────────────────────────────────
36
+
37
+ /**
38
+ * Install behavioral rules to a platform's rules file.
39
+ * Supports: file-based (append/update), standalone file, or clipboard.
40
+ *
41
+ * @param {object} platform - Platform object with rulesPath
42
+ * @param {object} options
43
+ * @param {string} options.content - Rules content (with markers)
44
+ * @param {string} options.version - Current version string
45
+ * @param {string} options.marker - Marker name for tracking
46
+ * @param {string} [options.fileName] - For standalone file platforms (e.g., "prior.md")
47
+ * @param {string[]} [options.clipboardPlatforms] - Platform ids that use clipboard
48
+ * @param {boolean} [options.dryRun]
49
+ * @param {Function} [options.copyToClipboard] - Clipboard function
50
+ * @returns {{ action: string }} "created" | "updated" | "skipped" | "clipboard"
51
+ */
52
+ function installRules(platform, options) {
53
+ const {
54
+ content,
55
+ version,
56
+ marker,
57
+ fileName,
58
+ clipboardPlatforms = ["cursor", "vscode"],
59
+ dryRun = false,
60
+ copyToClipboard,
61
+ } = options;
62
+
63
+ // Clipboard-only platforms
64
+ if (clipboardPlatforms.includes(platform.platform)) {
65
+ if (!dryRun && copyToClipboard) {
66
+ copyToClipboard(content);
67
+ }
68
+ return { action: "clipboard" };
69
+ }
70
+
71
+ if (!platform.rulesPath) return { action: "skipped" };
72
+
73
+ // Determine actual file path — standalone file vs append
74
+ const rulesPath = fileName
75
+ ? path.join(platform.rulesPath, fileName)
76
+ : platform.rulesPath;
77
+
78
+ const { MARKER_RE, BLOCK_RE } = markerPatterns(marker);
79
+
80
+ let existing = "";
81
+ try { existing = fs.readFileSync(rulesPath, "utf-8"); } catch {}
82
+
83
+ const existingVersion = parseRulesVersion(existing, marker);
84
+
85
+ if (existingVersion === version) {
86
+ return { action: "skipped" };
87
+ }
88
+
89
+ if (!dryRun) {
90
+ const dir = path.dirname(rulesPath);
91
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
92
+
93
+ if (existingVersion) {
94
+ // Replace existing block
95
+ const updated = existing.replace(BLOCK_RE, content + "\n");
96
+ fs.writeFileSync(rulesPath, updated);
97
+ return { action: "updated" };
98
+ }
99
+
100
+ // Append
101
+ const sep = existing && !existing.endsWith("\n\n") ? (existing.endsWith("\n") ? "\n" : "\n\n") : "";
102
+ fs.writeFileSync(rulesPath, existing + sep + content + "\n");
103
+ return { action: "created" };
104
+ }
105
+
106
+ return { action: existingVersion ? "updated" : "created" };
107
+ }
108
+
109
+ /**
110
+ * Remove rules from a platform's rules file.
111
+ * @param {object} platform - Platform object
112
+ * @param {object} options
113
+ * @param {string} options.marker - Marker name
114
+ * @param {string} [options.fileName] - For standalone file platforms
115
+ * @param {boolean} [options.dryRun]
116
+ * @returns {boolean} Whether anything was removed
117
+ */
118
+ function uninstallRules(platform, options) {
119
+ const { marker, fileName, dryRun = false } = options;
120
+
121
+ if (!platform.rulesPath) return false;
122
+
123
+ const rulesPath = fileName
124
+ ? path.join(platform.rulesPath, fileName)
125
+ : platform.rulesPath;
126
+
127
+ try {
128
+ if (!fs.statSync(rulesPath).isFile()) return false;
129
+ } catch { return false; }
130
+
131
+ try {
132
+ const content = fs.readFileSync(rulesPath, "utf-8");
133
+ const { MARKER_RE, BLOCK_RE } = markerPatterns(marker);
134
+ if (!MARKER_RE.test(content)) return false;
135
+ if (!dryRun) {
136
+ const cleaned = content.replace(BLOCK_RE, "").replace(/\n{3,}/g, "\n\n").trim();
137
+ if (cleaned) {
138
+ fs.writeFileSync(rulesPath, cleaned + "\n");
139
+ } else {
140
+ fs.unlinkSync(rulesPath);
141
+ }
142
+ }
143
+ return true;
144
+ } catch { return false; }
145
+ }
146
+
147
+ module.exports = {
148
+ markerPatterns,
149
+ parseRulesVersion,
150
+ installRules,
151
+ uninstallRules,
152
+ };
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@cg3/equip",
3
+ "version": "0.1.0",
4
+ "description": "Universal MCP + behavioral rules installer for AI coding agents",
5
+ "main": "index.js",
6
+ "exports": {
7
+ ".": "./index.js",
8
+ "./detect": "./lib/detect.js",
9
+ "./mcp": "./lib/mcp.js",
10
+ "./rules": "./lib/rules.js",
11
+ "./platforms": "./lib/platforms.js",
12
+ "./cli": "./lib/cli.js"
13
+ },
14
+ "files": [
15
+ "index.js",
16
+ "lib/"
17
+ ],
18
+ "keywords": [
19
+ "mcp",
20
+ "ai",
21
+ "agent",
22
+ "setup",
23
+ "claude",
24
+ "cursor",
25
+ "windsurf",
26
+ "vscode",
27
+ "cline",
28
+ "roo-code"
29
+ ],
30
+ "author": "CG3 LLC",
31
+ "license": "MIT",
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "https://github.com/cg3-llc/equip"
35
+ },
36
+ "engines": {
37
+ "node": ">=18"
38
+ }
39
+ }