@cg3/equip 0.1.1 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -8
- package/bin/equip.js +51 -0
- package/index.js +8 -3
- package/lib/detect.js +39 -1
- package/lib/mcp.js +273 -6
- package/lib/platforms.js +33 -1
- package/package.json +8 -2
package/README.md
CHANGED
|
@@ -8,14 +8,24 @@ Equip handles the hard part of distributing your MCP tool: detecting which AI co
|
|
|
8
8
|
|
|
9
9
|
| Platform | MCP Config | Rules |
|
|
10
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) |
|
|
11
|
+
| Claude Code | `~/.claude.json` (JSON, `mcpServers`) | `~/.claude/CLAUDE.md` (append) |
|
|
12
|
+
| Cursor | `~/.cursor/mcp.json` (JSON, `mcpServers`) | Clipboard (no writable global path) |
|
|
13
|
+
| Windsurf | `~/.codeium/windsurf/mcp_config.json` (JSON, `mcpServers`) | `global_rules.md` (append) |
|
|
14
|
+
| VS Code | `Code/User/mcp.json` (JSON, `servers`, `type: "http"`) | Clipboard |
|
|
15
|
+
| Cline | `globalStorage/.../cline_mcp_settings.json` (JSON, `mcpServers`) | `~/Documents/Cline/Rules/` (standalone file) |
|
|
16
|
+
| Roo Code | `globalStorage/.../cline_mcp_settings.json` (JSON, `mcpServers`) | `~/.roo/rules/` (standalone file) |
|
|
17
|
+
| Codex | `~/.codex/config.toml` (TOML, `mcp_servers`) | `~/.codex/AGENTS.md` (append) |
|
|
18
|
+
| Gemini CLI | `~/.gemini/settings.json` (JSON, `mcpServers`, `httpUrl`) | `~/.gemini/GEMINI.md` (append) |
|
|
19
|
+
|
|
20
|
+
## Quick Start
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npx @cg3/equip prior
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
That's it. Detects your platforms, authenticates, installs MCP + rules, and verifies — all in one command. Pass `--dry-run` to preview without writing files.
|
|
17
27
|
|
|
18
|
-
## Usage
|
|
28
|
+
## Programmatic Usage
|
|
19
29
|
|
|
20
30
|
```js
|
|
21
31
|
const { Equip } = require("@cg3/equip");
|
|
@@ -84,7 +94,7 @@ const { detectPlatforms, installMcpJson, installRules, createManualPlatform, pla
|
|
|
84
94
|
## Key Features
|
|
85
95
|
|
|
86
96
|
- **Zero dependencies** — Pure Node.js, works with Node 18+
|
|
87
|
-
- **Platform-aware** — Handles each platform's config quirks (root keys, URL fields, type requirements)
|
|
97
|
+
- **Platform-aware** — Handles each platform's config quirks (JSON vs TOML, root keys, URL fields, type requirements)
|
|
88
98
|
- **Non-destructive** — Merges into existing configs, creates backups, preserves other servers
|
|
89
99
|
- **Versioned rules** — Marker-based blocks enable idempotent updates without clobbering user content
|
|
90
100
|
- **Dry-run support** — Preview changes without writing files
|
package/bin/equip.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// @cg3/equip CLI — universal entry point for AI tool setup.
|
|
3
|
+
// Usage: npx @cg3/equip <tool> [args...]
|
|
4
|
+
// e.g. npx @cg3/equip prior --dry-run
|
|
5
|
+
|
|
6
|
+
"use strict";
|
|
7
|
+
|
|
8
|
+
const { spawn } = require("child_process");
|
|
9
|
+
|
|
10
|
+
// ─── Tool Registry ──────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
const TOOLS = {
|
|
13
|
+
prior: { package: "@cg3/prior-node", command: "setup" },
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
// ─── CLI ─────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
const alias = process.argv[2];
|
|
19
|
+
const extraArgs = process.argv.slice(3);
|
|
20
|
+
|
|
21
|
+
if (!alias || alias === "--help" || alias === "-h") {
|
|
22
|
+
console.log("Usage: npx @cg3/equip <tool> [options]");
|
|
23
|
+
console.log("");
|
|
24
|
+
console.log("Available tools:");
|
|
25
|
+
for (const [name, info] of Object.entries(TOOLS)) {
|
|
26
|
+
console.log(` ${name} → ${info.package} ${info.command}`);
|
|
27
|
+
}
|
|
28
|
+
console.log("");
|
|
29
|
+
console.log("Options are forwarded to the tool (e.g. --dry-run, --platform codex)");
|
|
30
|
+
process.exit(0);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const entry = TOOLS[alias];
|
|
34
|
+
if (!entry) {
|
|
35
|
+
console.error(`Unknown tool: ${alias}`);
|
|
36
|
+
console.error(`Available: ${Object.keys(TOOLS).join(", ")}`);
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Spawn: npx -y <package> <command> [...extraArgs]
|
|
41
|
+
const npxCmd = process.platform === "win32" ? "npx.cmd" : "npx";
|
|
42
|
+
const child = spawn(npxCmd, ["-y", entry.package, entry.command, ...extraArgs], {
|
|
43
|
+
stdio: "inherit",
|
|
44
|
+
shell: process.platform === "win32",
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
child.on("close", (code) => process.exit(code || 0));
|
|
48
|
+
child.on("error", (err) => {
|
|
49
|
+
console.error(`Failed to run ${entry.package}: ${err.message}`);
|
|
50
|
+
process.exit(1);
|
|
51
|
+
});
|
package/index.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
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.
|
|
2
|
+
// Zero dependencies. Works with Claude Code, Cursor, Windsurf, VS Code, Cline, Roo Code, Codex, Gemini CLI.
|
|
3
3
|
|
|
4
4
|
"use strict";
|
|
5
5
|
|
|
6
6
|
const { detectPlatforms, whichSync, dirExists, fileExists } = require("./lib/detect");
|
|
7
|
-
const { readMcpEntry, buildHttpConfig, buildHttpConfigWithAuth, buildStdioConfig, installMcp, installMcpJson, uninstallMcp, updateMcpKey } = require("./lib/mcp");
|
|
7
|
+
const { readMcpEntry, buildHttpConfig, buildHttpConfigWithAuth, buildStdioConfig, installMcp, installMcpJson, installMcpToml, uninstallMcp, updateMcpKey, parseTomlServerEntry, parseTomlSubTables, buildTomlEntry, removeTomlEntry } = require("./lib/mcp");
|
|
8
8
|
const { parseRulesVersion, installRules, uninstallRules, markerPatterns } = require("./lib/rules");
|
|
9
9
|
const { createManualPlatform, platformName, KNOWN_PLATFORMS } = require("./lib/platforms");
|
|
10
10
|
const cli = require("./lib/cli");
|
|
@@ -145,7 +145,7 @@ class Equip {
|
|
|
145
145
|
* @returns {object|null} Existing MCP config or null
|
|
146
146
|
*/
|
|
147
147
|
readMcp(platform) {
|
|
148
|
-
return readMcpEntry(platform.configPath, platform.rootKey, this.name);
|
|
148
|
+
return readMcpEntry(platform.configPath, platform.rootKey, this.name, platform.configFormat || "json");
|
|
149
149
|
}
|
|
150
150
|
}
|
|
151
151
|
|
|
@@ -159,8 +159,13 @@ module.exports = {
|
|
|
159
159
|
buildStdioConfig,
|
|
160
160
|
installMcp,
|
|
161
161
|
installMcpJson,
|
|
162
|
+
installMcpToml,
|
|
162
163
|
uninstallMcp,
|
|
163
164
|
updateMcpKey,
|
|
165
|
+
parseTomlServerEntry,
|
|
166
|
+
parseTomlSubTables,
|
|
167
|
+
buildTomlEntry,
|
|
168
|
+
removeTomlEntry,
|
|
164
169
|
installRules,
|
|
165
170
|
uninstallRules,
|
|
166
171
|
parseRulesVersion,
|
package/lib/detect.js
CHANGED
|
@@ -7,7 +7,7 @@ const fs = require("fs");
|
|
|
7
7
|
const path = require("path");
|
|
8
8
|
const os = require("os");
|
|
9
9
|
const { execSync } = require("child_process");
|
|
10
|
-
const { getVsCodeMcpPath, getVsCodeUserDir, getClineConfigPath, getRooConfigPath } = require("./platforms");
|
|
10
|
+
const { getVsCodeMcpPath, getVsCodeUserDir, getClineConfigPath, getRooConfigPath, getCodexConfigPath, getGeminiSettingsPath } = require("./platforms");
|
|
11
11
|
const { readMcpEntry } = require("./mcp");
|
|
12
12
|
|
|
13
13
|
// ─── Helpers ─────────────────────────────────────────────────
|
|
@@ -67,6 +67,7 @@ function detectPlatforms(serverName) {
|
|
|
67
67
|
existingMcp: serverName ? readMcpEntry(configPath, "mcpServers", serverName) : null,
|
|
68
68
|
hasCli: !!whichSync("claude"),
|
|
69
69
|
rootKey: "mcpServers",
|
|
70
|
+
configFormat: "json",
|
|
70
71
|
});
|
|
71
72
|
}
|
|
72
73
|
|
|
@@ -82,6 +83,7 @@ function detectPlatforms(serverName) {
|
|
|
82
83
|
existingMcp: serverName ? readMcpEntry(configPath, "mcpServers", serverName) : null,
|
|
83
84
|
hasCli: !!whichSync("cursor"),
|
|
84
85
|
rootKey: "mcpServers",
|
|
86
|
+
configFormat: "json",
|
|
85
87
|
});
|
|
86
88
|
}
|
|
87
89
|
|
|
@@ -98,6 +100,7 @@ function detectPlatforms(serverName) {
|
|
|
98
100
|
existingMcp: serverName ? readMcpEntry(configPath, "mcpServers", serverName) : null,
|
|
99
101
|
hasCli: false,
|
|
100
102
|
rootKey: "mcpServers",
|
|
103
|
+
configFormat: "json",
|
|
101
104
|
});
|
|
102
105
|
}
|
|
103
106
|
|
|
@@ -112,6 +115,7 @@ function detectPlatforms(serverName) {
|
|
|
112
115
|
existingMcp: serverName ? readMcpEntry(vscodeMcpPath, "servers", serverName) : null,
|
|
113
116
|
hasCli: !!whichSync("code"),
|
|
114
117
|
rootKey: "servers",
|
|
118
|
+
configFormat: "json",
|
|
115
119
|
});
|
|
116
120
|
}
|
|
117
121
|
|
|
@@ -127,6 +131,7 @@ function detectPlatforms(serverName) {
|
|
|
127
131
|
existingMcp: serverName ? readMcpEntry(clineConfigPath, "mcpServers", serverName) : null,
|
|
128
132
|
hasCli: false,
|
|
129
133
|
rootKey: "mcpServers",
|
|
134
|
+
configFormat: "json",
|
|
130
135
|
});
|
|
131
136
|
}
|
|
132
137
|
|
|
@@ -141,6 +146,39 @@ function detectPlatforms(serverName) {
|
|
|
141
146
|
existingMcp: serverName ? readMcpEntry(rooConfigPath, "mcpServers", serverName) : null,
|
|
142
147
|
hasCli: false,
|
|
143
148
|
rootKey: "mcpServers",
|
|
149
|
+
configFormat: "json",
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Codex (OpenAI CLI)
|
|
154
|
+
const codexHome = process.env.CODEX_HOME || path.join(home, ".codex");
|
|
155
|
+
const codexConfigPath = getCodexConfigPath();
|
|
156
|
+
if (whichSync("codex") || dirExists(codexHome)) {
|
|
157
|
+
platforms.push({
|
|
158
|
+
platform: "codex",
|
|
159
|
+
version: cliVersion("codex") || "unknown",
|
|
160
|
+
configPath: codexConfigPath,
|
|
161
|
+
rulesPath: path.join(codexHome, "AGENTS.md"),
|
|
162
|
+
existingMcp: serverName ? readMcpEntry(codexConfigPath, "mcp_servers", serverName, "toml") : null,
|
|
163
|
+
hasCli: !!whichSync("codex"),
|
|
164
|
+
rootKey: "mcp_servers",
|
|
165
|
+
configFormat: "toml",
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Gemini CLI (Google)
|
|
170
|
+
const geminiDir = path.join(home, ".gemini");
|
|
171
|
+
const geminiSettingsPath = getGeminiSettingsPath();
|
|
172
|
+
if (whichSync("gemini") || dirExists(geminiDir)) {
|
|
173
|
+
platforms.push({
|
|
174
|
+
platform: "gemini-cli",
|
|
175
|
+
version: cliVersion("gemini") || "unknown",
|
|
176
|
+
configPath: geminiSettingsPath,
|
|
177
|
+
rulesPath: path.join(geminiDir, "GEMINI.md"),
|
|
178
|
+
existingMcp: serverName ? readMcpEntry(geminiSettingsPath, "mcpServers", serverName) : null,
|
|
179
|
+
hasCli: !!whichSync("gemini"),
|
|
180
|
+
rootKey: "mcpServers",
|
|
181
|
+
configFormat: "json",
|
|
144
182
|
});
|
|
145
183
|
}
|
|
146
184
|
|
package/lib/mcp.js
CHANGED
|
@@ -8,19 +8,170 @@ const fs = require("fs");
|
|
|
8
8
|
const path = require("path");
|
|
9
9
|
const { execSync } = require("child_process");
|
|
10
10
|
|
|
11
|
+
// ─── TOML Helpers (minimal, zero-dep) ───────────────────────
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Parse a TOML table entry for [mcp_servers.<name>].
|
|
15
|
+
* Returns key-value pairs as a plain object. Supports string, number, boolean, arrays.
|
|
16
|
+
* This is NOT a full TOML parser — only handles flat tables needed for MCP config.
|
|
17
|
+
*/
|
|
18
|
+
function parseTomlServerEntry(tomlContent, rootKey, serverName) {
|
|
19
|
+
const tableHeader = `[${rootKey}.${serverName}]`;
|
|
20
|
+
const idx = tomlContent.indexOf(tableHeader);
|
|
21
|
+
if (idx === -1) return null;
|
|
22
|
+
|
|
23
|
+
const afterHeader = tomlContent.slice(idx + tableHeader.length);
|
|
24
|
+
const nextTable = afterHeader.search(/\n\[(?!\[)/); // next top-level table
|
|
25
|
+
const block = nextTable === -1 ? afterHeader : afterHeader.slice(0, nextTable);
|
|
26
|
+
|
|
27
|
+
const result = {};
|
|
28
|
+
for (const line of block.split("\n")) {
|
|
29
|
+
const trimmed = line.trim();
|
|
30
|
+
if (!trimmed || trimmed.startsWith("#") || trimmed.startsWith("[")) continue;
|
|
31
|
+
const eq = trimmed.indexOf("=");
|
|
32
|
+
if (eq === -1) continue;
|
|
33
|
+
const key = trimmed.slice(0, eq).trim();
|
|
34
|
+
let val = trimmed.slice(eq + 1).trim();
|
|
35
|
+
// Parse value
|
|
36
|
+
if (val.startsWith('"') && val.endsWith('"')) {
|
|
37
|
+
result[key] = val.slice(1, -1);
|
|
38
|
+
} else if (val === "true") {
|
|
39
|
+
result[key] = true;
|
|
40
|
+
} else if (val === "false") {
|
|
41
|
+
result[key] = false;
|
|
42
|
+
} else if (!isNaN(Number(val)) && val !== "") {
|
|
43
|
+
result[key] = Number(val);
|
|
44
|
+
} else {
|
|
45
|
+
result[key] = val;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return Object.keys(result).length > 0 ? result : null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Parse a nested TOML sub-table (e.g., [mcp_servers.prior.env] or [mcp_servers.prior.http_headers]).
|
|
53
|
+
*/
|
|
54
|
+
function parseTomlSubTables(tomlContent, rootKey, serverName) {
|
|
55
|
+
const prefix = `[${rootKey}.${serverName}.`;
|
|
56
|
+
const result = {};
|
|
57
|
+
let idx = 0;
|
|
58
|
+
while ((idx = tomlContent.indexOf(prefix, idx)) !== -1) {
|
|
59
|
+
const lineStart = tomlContent.lastIndexOf("\n", idx) + 1;
|
|
60
|
+
const lineEnd = tomlContent.indexOf("\n", idx);
|
|
61
|
+
const header = tomlContent.slice(idx, lineEnd === -1 ? undefined : lineEnd).trim();
|
|
62
|
+
// Extract sub-table name from [mcp_servers.prior.env]
|
|
63
|
+
const subName = header.slice(prefix.length, -1); // remove trailing ]
|
|
64
|
+
if (!subName || subName.includes(".")) { idx++; continue; }
|
|
65
|
+
|
|
66
|
+
const afterHeader = tomlContent.slice(lineEnd === -1 ? tomlContent.length : lineEnd);
|
|
67
|
+
const nextTable = afterHeader.search(/\n\[(?!\[)/);
|
|
68
|
+
const block = nextTable === -1 ? afterHeader : afterHeader.slice(0, nextTable);
|
|
69
|
+
|
|
70
|
+
const sub = {};
|
|
71
|
+
for (const line of block.split("\n")) {
|
|
72
|
+
const t = line.trim();
|
|
73
|
+
if (!t || t.startsWith("#") || t.startsWith("[")) continue;
|
|
74
|
+
const eq = t.indexOf("=");
|
|
75
|
+
if (eq === -1) continue;
|
|
76
|
+
const k = t.slice(0, eq).trim();
|
|
77
|
+
let v = t.slice(eq + 1).trim();
|
|
78
|
+
if (v.startsWith('"') && v.endsWith('"')) sub[k] = v.slice(1, -1);
|
|
79
|
+
else sub[k] = v;
|
|
80
|
+
}
|
|
81
|
+
if (Object.keys(sub).length > 0) result[subName] = sub;
|
|
82
|
+
idx++;
|
|
83
|
+
}
|
|
84
|
+
return result;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Build TOML text for a server entry.
|
|
89
|
+
* @param {string} rootKey - e.g., "mcp_servers"
|
|
90
|
+
* @param {string} serverName - e.g., "prior"
|
|
91
|
+
* @param {object} config - { url, bearer_token_env_var, http_headers, ... }
|
|
92
|
+
* @returns {string} TOML text block
|
|
93
|
+
*/
|
|
94
|
+
function buildTomlEntry(rootKey, serverName, config) {
|
|
95
|
+
const lines = [`[${rootKey}.${serverName}]`];
|
|
96
|
+
const subTables = {};
|
|
97
|
+
|
|
98
|
+
for (const [k, v] of Object.entries(config)) {
|
|
99
|
+
if (typeof v === "object" && v !== null && !Array.isArray(v)) {
|
|
100
|
+
subTables[k] = v;
|
|
101
|
+
} else if (typeof v === "string") {
|
|
102
|
+
lines.push(`${k} = "${v}"`);
|
|
103
|
+
} else if (typeof v === "boolean" || typeof v === "number") {
|
|
104
|
+
lines.push(`${k} = ${v}`);
|
|
105
|
+
} else if (Array.isArray(v)) {
|
|
106
|
+
lines.push(`${k} = [${v.map(x => typeof x === "string" ? `"${x}"` : x).join(", ")}]`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
for (const [subName, subObj] of Object.entries(subTables)) {
|
|
111
|
+
lines.push("", `[${rootKey}.${serverName}.${subName}]`);
|
|
112
|
+
for (const [k, v] of Object.entries(subObj)) {
|
|
113
|
+
if (typeof v === "string") lines.push(`${k} = "${v}"`);
|
|
114
|
+
else lines.push(`${k} = ${v}`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return lines.join("\n");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Remove a TOML server entry block from content.
|
|
123
|
+
* Removes [rootKey.serverName] and any [rootKey.serverName.*] sub-tables.
|
|
124
|
+
*/
|
|
125
|
+
function removeTomlEntry(tomlContent, rootKey, serverName) {
|
|
126
|
+
const mainHeader = `[${rootKey}.${serverName}]`;
|
|
127
|
+
const subPrefix = `[${rootKey}.${serverName}.`;
|
|
128
|
+
|
|
129
|
+
// Find all lines belonging to this entry
|
|
130
|
+
const lines = tomlContent.split("\n");
|
|
131
|
+
const result = [];
|
|
132
|
+
let inEntry = false;
|
|
133
|
+
|
|
134
|
+
for (const line of lines) {
|
|
135
|
+
const trimmed = line.trim();
|
|
136
|
+
if (trimmed === mainHeader || trimmed.startsWith(subPrefix)) {
|
|
137
|
+
inEntry = true;
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
if (inEntry && trimmed.startsWith("[") && !trimmed.startsWith(subPrefix)) {
|
|
141
|
+
inEntry = false;
|
|
142
|
+
}
|
|
143
|
+
if (!inEntry) {
|
|
144
|
+
result.push(line);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Clean up extra blank lines
|
|
149
|
+
return result.join("\n").replace(/\n{3,}/g, "\n\n").trim() + "\n";
|
|
150
|
+
}
|
|
151
|
+
|
|
11
152
|
// ─── Read ────────────────────────────────────────────────────
|
|
12
153
|
|
|
13
154
|
/**
|
|
14
|
-
* Read an MCP server entry from a config file.
|
|
15
|
-
* @param {string} configPath - Path to config
|
|
16
|
-
* @param {string} rootKey - Root key ("mcpServers" or "
|
|
155
|
+
* Read an MCP server entry from a config file (JSON or TOML).
|
|
156
|
+
* @param {string} configPath - Path to config file
|
|
157
|
+
* @param {string} rootKey - Root key ("mcpServers", "servers", or "mcp_servers")
|
|
17
158
|
* @param {string} serverName - Server name to read
|
|
159
|
+
* @param {string} [configFormat="json"] - "json" or "toml"
|
|
18
160
|
* @returns {object|null} Server config or null
|
|
19
161
|
*/
|
|
20
|
-
function readMcpEntry(configPath, rootKey, serverName) {
|
|
162
|
+
function readMcpEntry(configPath, rootKey, serverName, configFormat = "json") {
|
|
21
163
|
try {
|
|
22
164
|
let raw = fs.readFileSync(configPath, "utf-8");
|
|
23
165
|
if (raw.charCodeAt(0) === 0xFEFF) raw = raw.slice(1); // Strip BOM
|
|
166
|
+
|
|
167
|
+
if (configFormat === "toml") {
|
|
168
|
+
const entry = parseTomlServerEntry(raw, rootKey, serverName);
|
|
169
|
+
if (!entry) return null;
|
|
170
|
+
// Merge sub-tables
|
|
171
|
+
const subs = parseTomlSubTables(raw, rootKey, serverName);
|
|
172
|
+
return { ...entry, ...subs };
|
|
173
|
+
}
|
|
174
|
+
|
|
24
175
|
const data = JSON.parse(raw);
|
|
25
176
|
return data?.[rootKey]?.[serverName] || null;
|
|
26
177
|
} catch { return null; }
|
|
@@ -38,6 +189,9 @@ function readMcpEntry(configPath, rootKey, serverName) {
|
|
|
38
189
|
function buildHttpConfig(serverUrl, platform) {
|
|
39
190
|
if (platform === "windsurf") return { serverUrl };
|
|
40
191
|
if (platform === "vscode") return { type: "http", url: serverUrl };
|
|
192
|
+
if (platform === "cursor") return { type: "streamable-http", url: serverUrl };
|
|
193
|
+
if (platform === "gemini-cli") return { httpUrl: serverUrl };
|
|
194
|
+
// codex, claude-code, cline, roo-code all use { url }
|
|
41
195
|
return { url: serverUrl };
|
|
42
196
|
}
|
|
43
197
|
|
|
@@ -50,8 +204,32 @@ function buildHttpConfig(serverUrl, platform) {
|
|
|
50
204
|
* @returns {object} MCP config with headers
|
|
51
205
|
*/
|
|
52
206
|
function buildHttpConfigWithAuth(serverUrl, apiKey, platform, extraHeaders) {
|
|
207
|
+
const base = buildHttpConfig(serverUrl, platform);
|
|
208
|
+
|
|
209
|
+
if (platform === "codex") {
|
|
210
|
+
// Codex TOML uses http_headers for static headers
|
|
211
|
+
return {
|
|
212
|
+
...base,
|
|
213
|
+
http_headers: {
|
|
214
|
+
Authorization: `Bearer ${apiKey}`,
|
|
215
|
+
...extraHeaders,
|
|
216
|
+
},
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (platform === "gemini-cli") {
|
|
221
|
+
// Gemini CLI uses headers object
|
|
222
|
+
return {
|
|
223
|
+
...base,
|
|
224
|
+
headers: {
|
|
225
|
+
Authorization: `Bearer ${apiKey}`,
|
|
226
|
+
...extraHeaders,
|
|
227
|
+
},
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
53
231
|
return {
|
|
54
|
-
...
|
|
232
|
+
...base,
|
|
55
233
|
headers: {
|
|
56
234
|
Authorization: `Bearer ${apiKey}`,
|
|
57
235
|
...extraHeaders,
|
|
@@ -141,7 +319,29 @@ function installMcp(platform, serverName, mcpEntry, options = {}) {
|
|
|
141
319
|
} catch { /* fall through */ }
|
|
142
320
|
}
|
|
143
321
|
|
|
144
|
-
//
|
|
322
|
+
// Codex: try CLI first (codex mcp add <name> <url>)
|
|
323
|
+
if (platform.platform === "codex" && platform.hasCli && mcpEntry.url) {
|
|
324
|
+
try {
|
|
325
|
+
if (!dryRun) {
|
|
326
|
+
execSync(`codex mcp add ${serverName} ${mcpEntry.url}`, {
|
|
327
|
+
encoding: "utf-8", timeout: 15000, stdio: "pipe",
|
|
328
|
+
});
|
|
329
|
+
const check = readMcpEntry(platform.configPath, platform.rootKey, serverName, "toml");
|
|
330
|
+
if (check) return { success: true, method: "cli" };
|
|
331
|
+
} else {
|
|
332
|
+
return { success: true, method: "cli" };
|
|
333
|
+
}
|
|
334
|
+
} catch { /* fall through to TOML write */ }
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Gemini CLI: try CLI first (gemini mcp add <name> -- or manual JSON)
|
|
338
|
+
// Gemini's `gemini mcp add` is for stdio primarily; HTTP goes through settings.json
|
|
339
|
+
// Fall through to JSON write for HTTP
|
|
340
|
+
|
|
341
|
+
// TOML write for Codex, JSON write for all others
|
|
342
|
+
if (platform.configFormat === "toml") {
|
|
343
|
+
return installMcpToml(platform, serverName, mcpEntry, dryRun);
|
|
344
|
+
}
|
|
145
345
|
return installMcpJson(platform, serverName, mcpEntry, dryRun);
|
|
146
346
|
}
|
|
147
347
|
|
|
@@ -182,6 +382,44 @@ function installMcpJson(platform, serverName, mcpEntry, dryRun) {
|
|
|
182
382
|
return { success: true, method: "json" };
|
|
183
383
|
}
|
|
184
384
|
|
|
385
|
+
/**
|
|
386
|
+
* Write MCP config to TOML file (Codex).
|
|
387
|
+
* Appends or replaces a [mcp_servers.<name>] table.
|
|
388
|
+
* @param {object} platform - Platform object
|
|
389
|
+
* @param {string} serverName - Server name
|
|
390
|
+
* @param {object} mcpEntry - MCP config
|
|
391
|
+
* @param {boolean} dryRun
|
|
392
|
+
* @returns {{ success: boolean, method: string }}
|
|
393
|
+
*/
|
|
394
|
+
function installMcpToml(platform, serverName, mcpEntry, dryRun) {
|
|
395
|
+
const { configPath, rootKey } = platform;
|
|
396
|
+
|
|
397
|
+
let existing = "";
|
|
398
|
+
try { existing = fs.readFileSync(configPath, "utf-8"); } catch { /* start fresh */ }
|
|
399
|
+
|
|
400
|
+
// Remove existing entry if present
|
|
401
|
+
const tableHeader = `[${rootKey}.${serverName}]`;
|
|
402
|
+
if (existing.includes(tableHeader)) {
|
|
403
|
+
existing = removeTomlEntry(existing, rootKey, serverName);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const newBlock = buildTomlEntry(rootKey, serverName, mcpEntry);
|
|
407
|
+
|
|
408
|
+
if (!dryRun) {
|
|
409
|
+
const dir = path.dirname(configPath);
|
|
410
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
411
|
+
|
|
412
|
+
if (fileExists(configPath)) {
|
|
413
|
+
try { fs.copyFileSync(configPath, configPath + ".bak"); } catch {}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const sep = existing && !existing.endsWith("\n\n") ? (existing.endsWith("\n") ? "\n" : "\n\n") : "";
|
|
417
|
+
fs.writeFileSync(configPath, existing + sep + newBlock + "\n");
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
return { success: true, method: "toml" };
|
|
421
|
+
}
|
|
422
|
+
|
|
185
423
|
/**
|
|
186
424
|
* Remove an MCP server entry from a platform config.
|
|
187
425
|
* @param {object} platform - Platform object
|
|
@@ -193,6 +431,26 @@ function uninstallMcp(platform, serverName, dryRun) {
|
|
|
193
431
|
const { configPath, rootKey } = platform;
|
|
194
432
|
if (!fileExists(configPath)) return false;
|
|
195
433
|
|
|
434
|
+
// TOML path (Codex)
|
|
435
|
+
if (platform.configFormat === "toml") {
|
|
436
|
+
try {
|
|
437
|
+
const content = fs.readFileSync(configPath, "utf-8");
|
|
438
|
+
const tableHeader = `[${rootKey}.${serverName}]`;
|
|
439
|
+
if (!content.includes(tableHeader)) return false;
|
|
440
|
+
if (!dryRun) {
|
|
441
|
+
fs.copyFileSync(configPath, configPath + ".bak");
|
|
442
|
+
const cleaned = removeTomlEntry(content, rootKey, serverName);
|
|
443
|
+
if (cleaned.trim()) {
|
|
444
|
+
fs.writeFileSync(configPath, cleaned);
|
|
445
|
+
} else {
|
|
446
|
+
fs.unlinkSync(configPath);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
return true;
|
|
450
|
+
} catch { return false; }
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// JSON path
|
|
196
454
|
try {
|
|
197
455
|
const data = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
198
456
|
if (!data?.[rootKey]?.[serverName]) return false;
|
|
@@ -218,6 +476,9 @@ function uninstallMcp(platform, serverName, dryRun) {
|
|
|
218
476
|
* @returns {{ success: boolean, method: string }}
|
|
219
477
|
*/
|
|
220
478
|
function updateMcpKey(platform, serverName, mcpEntry) {
|
|
479
|
+
if (platform.configFormat === "toml") {
|
|
480
|
+
return installMcpToml(platform, serverName, mcpEntry, false);
|
|
481
|
+
}
|
|
221
482
|
return installMcpJson(platform, serverName, mcpEntry, false);
|
|
222
483
|
}
|
|
223
484
|
|
|
@@ -228,6 +489,12 @@ module.exports = {
|
|
|
228
489
|
buildStdioConfig,
|
|
229
490
|
installMcp,
|
|
230
491
|
installMcpJson,
|
|
492
|
+
installMcpToml,
|
|
231
493
|
uninstallMcp,
|
|
232
494
|
updateMcpKey,
|
|
495
|
+
// TOML helpers (exported for testing)
|
|
496
|
+
parseTomlServerEntry,
|
|
497
|
+
parseTomlSubTables,
|
|
498
|
+
buildTomlEntry,
|
|
499
|
+
removeTomlEntry,
|
|
233
500
|
};
|
package/lib/platforms.js
CHANGED
|
@@ -29,6 +29,16 @@ function getRooConfigPath() {
|
|
|
29
29
|
return path.join(base, "globalStorage", "rooveterinaryinc.roo-cline", "settings", "cline_mcp_settings.json");
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
function getCodexConfigPath() {
|
|
33
|
+
const home = os.homedir();
|
|
34
|
+
return path.join(process.env.CODEX_HOME || path.join(home, ".codex"), "config.toml");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function getGeminiSettingsPath() {
|
|
38
|
+
const home = os.homedir();
|
|
39
|
+
return path.join(home, ".gemini", "settings.json");
|
|
40
|
+
}
|
|
41
|
+
|
|
32
42
|
// ─── Platform Registry ──────────────────────────────────────
|
|
33
43
|
|
|
34
44
|
/**
|
|
@@ -43,31 +53,49 @@ function createManualPlatform(platformId) {
|
|
|
43
53
|
configPath: path.join(home, ".claude.json"),
|
|
44
54
|
rulesPath: path.join(home, ".claude", "CLAUDE.md"),
|
|
45
55
|
rootKey: "mcpServers",
|
|
56
|
+
configFormat: "json",
|
|
46
57
|
},
|
|
47
58
|
cursor: {
|
|
48
59
|
configPath: path.join(home, ".cursor", "mcp.json"),
|
|
49
60
|
rulesPath: null,
|
|
50
61
|
rootKey: "mcpServers",
|
|
62
|
+
configFormat: "json",
|
|
51
63
|
},
|
|
52
64
|
windsurf: {
|
|
53
65
|
configPath: path.join(home, ".codeium", "windsurf", "mcp_config.json"),
|
|
54
66
|
rulesPath: path.join(home, ".codeium", "windsurf", "memories", "global_rules.md"),
|
|
55
67
|
rootKey: "mcpServers",
|
|
68
|
+
configFormat: "json",
|
|
56
69
|
},
|
|
57
70
|
vscode: {
|
|
58
71
|
configPath: getVsCodeMcpPath(),
|
|
59
72
|
rulesPath: null,
|
|
60
73
|
rootKey: "servers",
|
|
74
|
+
configFormat: "json",
|
|
61
75
|
},
|
|
62
76
|
cline: {
|
|
63
77
|
configPath: getClineConfigPath(),
|
|
64
78
|
rulesPath: path.join(home, "Documents", "Cline", "Rules"),
|
|
65
79
|
rootKey: "mcpServers",
|
|
80
|
+
configFormat: "json",
|
|
66
81
|
},
|
|
67
82
|
"roo-code": {
|
|
68
83
|
configPath: getRooConfigPath(),
|
|
69
84
|
rulesPath: path.join(home, ".roo", "rules"),
|
|
70
85
|
rootKey: "mcpServers",
|
|
86
|
+
configFormat: "json",
|
|
87
|
+
},
|
|
88
|
+
codex: {
|
|
89
|
+
configPath: getCodexConfigPath(),
|
|
90
|
+
rulesPath: path.join(process.env.CODEX_HOME || path.join(home, ".codex"), "AGENTS.md"),
|
|
91
|
+
rootKey: "mcp_servers",
|
|
92
|
+
configFormat: "toml",
|
|
93
|
+
},
|
|
94
|
+
"gemini-cli": {
|
|
95
|
+
configPath: getGeminiSettingsPath(),
|
|
96
|
+
rulesPath: path.join(home, ".gemini", "GEMINI.md"),
|
|
97
|
+
rootKey: "mcpServers",
|
|
98
|
+
configFormat: "json",
|
|
71
99
|
},
|
|
72
100
|
};
|
|
73
101
|
|
|
@@ -90,6 +118,8 @@ function platformName(id) {
|
|
|
90
118
|
vscode: "VS Code",
|
|
91
119
|
cline: "Cline",
|
|
92
120
|
"roo-code": "Roo Code",
|
|
121
|
+
codex: "Codex",
|
|
122
|
+
"gemini-cli": "Gemini CLI",
|
|
93
123
|
};
|
|
94
124
|
return names[id] || id;
|
|
95
125
|
}
|
|
@@ -97,13 +127,15 @@ function platformName(id) {
|
|
|
97
127
|
/**
|
|
98
128
|
* All known platform IDs.
|
|
99
129
|
*/
|
|
100
|
-
const KNOWN_PLATFORMS = ["claude-code", "cursor", "windsurf", "vscode", "cline", "roo-code"];
|
|
130
|
+
const KNOWN_PLATFORMS = ["claude-code", "cursor", "windsurf", "vscode", "cline", "roo-code", "codex", "gemini-cli"];
|
|
101
131
|
|
|
102
132
|
module.exports = {
|
|
103
133
|
getVsCodeUserDir,
|
|
104
134
|
getVsCodeMcpPath,
|
|
105
135
|
getClineConfigPath,
|
|
106
136
|
getRooConfigPath,
|
|
137
|
+
getCodexConfigPath,
|
|
138
|
+
getGeminiSettingsPath,
|
|
107
139
|
createManualPlatform,
|
|
108
140
|
platformName,
|
|
109
141
|
KNOWN_PLATFORMS,
|
package/package.json
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cg3/equip",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Universal MCP + behavioral rules installer for AI coding agents",
|
|
5
5
|
"main": "index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"equip": "bin/equip.js"
|
|
8
|
+
},
|
|
6
9
|
"exports": {
|
|
7
10
|
".": "./index.js",
|
|
8
11
|
"./detect": "./lib/detect.js",
|
|
@@ -13,6 +16,7 @@
|
|
|
13
16
|
},
|
|
14
17
|
"files": [
|
|
15
18
|
"index.js",
|
|
19
|
+
"bin/",
|
|
16
20
|
"lib/"
|
|
17
21
|
],
|
|
18
22
|
"keywords": [
|
|
@@ -25,7 +29,9 @@
|
|
|
25
29
|
"windsurf",
|
|
26
30
|
"vscode",
|
|
27
31
|
"cline",
|
|
28
|
-
"roo-code"
|
|
32
|
+
"roo-code",
|
|
33
|
+
"codex",
|
|
34
|
+
"gemini"
|
|
29
35
|
],
|
|
30
36
|
"author": "CG3 LLC",
|
|
31
37
|
"license": "MIT",
|