@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 +21 -0
- package/README.md +95 -0
- package/index.js +172 -0
- package/lib/cli.js +95 -0
- package/lib/detect.js +156 -0
- package/lib/mcp.js +233 -0
- package/lib/platforms.js +110 -0
- package/lib/rules.js +152 -0
- package/package.json +39 -0
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
|
+
};
|
package/lib/platforms.js
ADDED
|
@@ -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
|
+
}
|