@cg3/equip 0.2.22 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +26 -10
- package/bin/equip.js +159 -68
- package/demo/README.md +1 -1
- package/dist/index.d.ts +74 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +175 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/cli.d.ts +22 -0
- package/dist/lib/cli.d.ts.map +1 -0
- package/dist/lib/cli.js +148 -0
- package/dist/lib/cli.js.map +1 -0
- package/dist/lib/commands/doctor.d.ts +2 -0
- package/dist/lib/commands/doctor.d.ts.map +1 -0
- package/dist/lib/commands/doctor.js +162 -0
- package/dist/lib/commands/doctor.js.map +1 -0
- package/dist/lib/commands/status.d.ts +2 -0
- package/dist/lib/commands/status.d.ts.map +1 -0
- package/dist/lib/commands/status.js +134 -0
- package/dist/lib/commands/status.js.map +1 -0
- package/dist/lib/commands/update.d.ts +2 -0
- package/dist/lib/commands/update.d.ts.map +1 -0
- package/dist/lib/commands/update.js +93 -0
- package/dist/lib/commands/update.js.map +1 -0
- package/dist/lib/detect.d.ts +12 -0
- package/dist/lib/detect.d.ts.map +1 -0
- package/dist/lib/detect.js +109 -0
- package/dist/lib/detect.js.map +1 -0
- package/dist/lib/hooks.d.ts +40 -0
- package/dist/lib/hooks.d.ts.map +1 -0
- package/dist/lib/hooks.js +226 -0
- package/dist/lib/hooks.js.map +1 -0
- package/dist/lib/mcp.d.ts +73 -0
- package/dist/lib/mcp.d.ts.map +1 -0
- package/dist/lib/mcp.js +418 -0
- package/dist/lib/mcp.js.map +1 -0
- package/dist/lib/platforms.d.ts +67 -0
- package/dist/lib/platforms.d.ts.map +1 -0
- package/dist/lib/platforms.js +353 -0
- package/dist/lib/platforms.js.map +1 -0
- package/dist/lib/rules.d.ts +35 -0
- package/dist/lib/rules.d.ts.map +1 -0
- package/dist/lib/rules.js +161 -0
- package/dist/lib/rules.js.map +1 -0
- package/dist/lib/state.d.ts +33 -0
- package/dist/lib/state.d.ts.map +1 -0
- package/dist/lib/state.js +130 -0
- package/dist/lib/state.js.map +1 -0
- package/package.json +19 -13
- package/registry.json +9 -0
- package/index.js +0 -244
- package/lib/cli.js +0 -99
- package/lib/detect.js +0 -242
- package/lib/hooks.js +0 -238
- package/lib/mcp.js +0 -503
- package/lib/platforms.js +0 -184
- package/lib/rules.js +0 -170
package/lib/mcp.js
DELETED
|
@@ -1,503 +0,0 @@
|
|
|
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
|
-
// ─── 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
|
-
|
|
152
|
-
// ─── Read ────────────────────────────────────────────────────
|
|
153
|
-
|
|
154
|
-
/**
|
|
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")
|
|
158
|
-
* @param {string} serverName - Server name to read
|
|
159
|
-
* @param {string} [configFormat="json"] - "json" or "toml"
|
|
160
|
-
* @returns {object|null} Server config or null
|
|
161
|
-
*/
|
|
162
|
-
function readMcpEntry(configPath, rootKey, serverName, configFormat = "json") {
|
|
163
|
-
try {
|
|
164
|
-
let raw = fs.readFileSync(configPath, "utf-8");
|
|
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
|
-
|
|
175
|
-
const data = JSON.parse(raw);
|
|
176
|
-
return data?.[rootKey]?.[serverName] || null;
|
|
177
|
-
} catch { return null; }
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// ─── Config Builders ─────────────────────────────────────────
|
|
181
|
-
|
|
182
|
-
/**
|
|
183
|
-
* Build HTTP MCP config for a platform.
|
|
184
|
-
* Handles platform-specific field names (url vs serverUrl, type field).
|
|
185
|
-
* @param {string} serverUrl - MCP server URL
|
|
186
|
-
* @param {string} platform - Platform id
|
|
187
|
-
* @returns {object} MCP config object
|
|
188
|
-
*/
|
|
189
|
-
function buildHttpConfig(serverUrl, platform) {
|
|
190
|
-
if (platform === "windsurf") return { serverUrl };
|
|
191
|
-
if (platform === "claude-code") return { type: "http", url: serverUrl };
|
|
192
|
-
if (platform === "vscode") return { type: "http", url: serverUrl };
|
|
193
|
-
if (platform === "cursor") return { type: "streamable-http", url: serverUrl };
|
|
194
|
-
if (platform === "gemini-cli") return { httpUrl: serverUrl };
|
|
195
|
-
// codex, cline, roo-code all use { url }
|
|
196
|
-
return { url: serverUrl };
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
/**
|
|
200
|
-
* Build HTTP MCP config with auth headers.
|
|
201
|
-
* @param {string} serverUrl - MCP server URL
|
|
202
|
-
* @param {string} apiKey - API key for auth
|
|
203
|
-
* @param {string} platform - Platform id
|
|
204
|
-
* @param {object} [extraHeaders] - Additional headers
|
|
205
|
-
* @returns {object} MCP config with headers
|
|
206
|
-
*/
|
|
207
|
-
function buildHttpConfigWithAuth(serverUrl, apiKey, platform, extraHeaders) {
|
|
208
|
-
const base = buildHttpConfig(serverUrl, platform);
|
|
209
|
-
|
|
210
|
-
if (platform === "codex") {
|
|
211
|
-
// Codex TOML uses http_headers for static headers
|
|
212
|
-
return {
|
|
213
|
-
...base,
|
|
214
|
-
http_headers: {
|
|
215
|
-
Authorization: `Bearer ${apiKey}`,
|
|
216
|
-
...extraHeaders,
|
|
217
|
-
},
|
|
218
|
-
};
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
if (platform === "gemini-cli") {
|
|
222
|
-
// Gemini CLI uses headers object
|
|
223
|
-
return {
|
|
224
|
-
...base,
|
|
225
|
-
headers: {
|
|
226
|
-
Authorization: `Bearer ${apiKey}`,
|
|
227
|
-
...extraHeaders,
|
|
228
|
-
},
|
|
229
|
-
};
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
return {
|
|
233
|
-
...base,
|
|
234
|
-
headers: {
|
|
235
|
-
Authorization: `Bearer ${apiKey}`,
|
|
236
|
-
...extraHeaders,
|
|
237
|
-
},
|
|
238
|
-
};
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
/**
|
|
242
|
-
* Build stdio MCP config.
|
|
243
|
-
* @param {string} command - Command to run
|
|
244
|
-
* @param {string[]} args - Command arguments
|
|
245
|
-
* @param {object} env - Environment variables
|
|
246
|
-
* @returns {object} MCP stdio config
|
|
247
|
-
*/
|
|
248
|
-
function buildStdioConfig(command, args, env) {
|
|
249
|
-
if (process.platform === "win32") {
|
|
250
|
-
return { command: "cmd", args: ["/c", command, ...args], env };
|
|
251
|
-
}
|
|
252
|
-
return { command, args, env };
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
// ─── Install ─────────────────────────────────────────────────
|
|
256
|
-
|
|
257
|
-
function fileExists(p) {
|
|
258
|
-
try { return fs.statSync(p).isFile(); } catch { return false; }
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
/**
|
|
262
|
-
* Install MCP config for a platform.
|
|
263
|
-
* Tries platform CLI first (if available), falls back to JSON write.
|
|
264
|
-
* @param {object} platform - Platform object from detect
|
|
265
|
-
* @param {string} serverName - Server name (e.g., "prior")
|
|
266
|
-
* @param {object} mcpEntry - MCP config object
|
|
267
|
-
* @param {object} [options] - { dryRun, serverUrl }
|
|
268
|
-
* @returns {{ success: boolean, method: string }}
|
|
269
|
-
*/
|
|
270
|
-
function installMcp(platform, serverName, mcpEntry, options = {}) {
|
|
271
|
-
const { dryRun = false, serverUrl } = options;
|
|
272
|
-
|
|
273
|
-
// Claude Code: try CLI first
|
|
274
|
-
if (platform.platform === "claude-code" && platform.hasCli && mcpEntry.url) {
|
|
275
|
-
try {
|
|
276
|
-
if (!dryRun) {
|
|
277
|
-
const headerArgs = mcpEntry.headers
|
|
278
|
-
? Object.entries(mcpEntry.headers).map(([k, v]) => `--header "${k}: ${v}"`).join(" ")
|
|
279
|
-
: "";
|
|
280
|
-
execSync(`claude mcp add --transport http -s user ${headerArgs} ${serverName} ${mcpEntry.url}`, {
|
|
281
|
-
encoding: "utf-8", timeout: 15000, stdio: "pipe",
|
|
282
|
-
});
|
|
283
|
-
const check = readMcpEntry(platform.configPath, platform.rootKey, serverName);
|
|
284
|
-
if (check) return { success: true, method: "cli" };
|
|
285
|
-
} else {
|
|
286
|
-
return { success: true, method: "cli" };
|
|
287
|
-
}
|
|
288
|
-
} catch { /* fall through */ }
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
// Cursor: try CLI first
|
|
292
|
-
if (platform.platform === "cursor" && platform.hasCli) {
|
|
293
|
-
try {
|
|
294
|
-
const mcpJson = JSON.stringify({ name: serverName, ...mcpEntry });
|
|
295
|
-
if (!dryRun) {
|
|
296
|
-
execSync(`cursor --add-mcp '${mcpJson.replace(/'/g, "'\\''")}'`, {
|
|
297
|
-
encoding: "utf-8", timeout: 15000, stdio: "pipe",
|
|
298
|
-
});
|
|
299
|
-
const check = readMcpEntry(platform.configPath, platform.rootKey, serverName);
|
|
300
|
-
if (check) return { success: true, method: "cli" };
|
|
301
|
-
} else {
|
|
302
|
-
return { success: true, method: "cli" };
|
|
303
|
-
}
|
|
304
|
-
} catch { /* fall through */ }
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
// VS Code: try CLI first
|
|
308
|
-
if (platform.platform === "vscode" && platform.hasCli) {
|
|
309
|
-
try {
|
|
310
|
-
const mcpJson = JSON.stringify({ name: serverName, ...mcpEntry });
|
|
311
|
-
if (!dryRun) {
|
|
312
|
-
execSync(`code --add-mcp '${mcpJson.replace(/'/g, "'\\''")}'`, {
|
|
313
|
-
encoding: "utf-8", timeout: 15000, stdio: "pipe",
|
|
314
|
-
});
|
|
315
|
-
const check = readMcpEntry(platform.configPath, platform.rootKey, serverName);
|
|
316
|
-
if (check) return { success: true, method: "cli" };
|
|
317
|
-
} else {
|
|
318
|
-
return { success: true, method: "cli" };
|
|
319
|
-
}
|
|
320
|
-
} catch { /* fall through */ }
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
// Codex: skip CLI for HTTP (codex mcp add treats URLs as stdio commands).
|
|
324
|
-
// Use CLI only for stdio transport where the command syntax is unambiguous.
|
|
325
|
-
if (platform.platform === "codex" && platform.hasCli && mcpEntry.command) {
|
|
326
|
-
try {
|
|
327
|
-
const cliArgs = [serverName, "--", mcpEntry.command, ...(mcpEntry.args || [])].join(" ");
|
|
328
|
-
if (!dryRun) {
|
|
329
|
-
execSync(`codex mcp add ${cliArgs}`, {
|
|
330
|
-
encoding: "utf-8", timeout: 15000, stdio: "pipe",
|
|
331
|
-
});
|
|
332
|
-
const check = readMcpEntry(platform.configPath, platform.rootKey, serverName, "toml");
|
|
333
|
-
if (check) return { success: true, method: "cli" };
|
|
334
|
-
} else {
|
|
335
|
-
return { success: true, method: "cli" };
|
|
336
|
-
}
|
|
337
|
-
} catch { /* fall through to TOML write */ }
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
// Gemini CLI: try CLI first (gemini mcp add <name> -- or manual JSON)
|
|
341
|
-
// Gemini's `gemini mcp add` is for stdio primarily; HTTP goes through settings.json
|
|
342
|
-
// Fall through to JSON write for HTTP
|
|
343
|
-
|
|
344
|
-
// TOML write for Codex, JSON write for all others
|
|
345
|
-
if (platform.configFormat === "toml") {
|
|
346
|
-
return installMcpToml(platform, serverName, mcpEntry, dryRun);
|
|
347
|
-
}
|
|
348
|
-
return installMcpJson(platform, serverName, mcpEntry, dryRun);
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
/**
|
|
352
|
-
* Write MCP config directly to JSON file.
|
|
353
|
-
* Merges with existing config, creates backup.
|
|
354
|
-
* @param {object} platform - Platform object
|
|
355
|
-
* @param {string} serverName - Server name
|
|
356
|
-
* @param {object} mcpEntry - MCP config
|
|
357
|
-
* @param {boolean} dryRun
|
|
358
|
-
* @returns {{ success: boolean, method: string }}
|
|
359
|
-
*/
|
|
360
|
-
function installMcpJson(platform, serverName, mcpEntry, dryRun) {
|
|
361
|
-
const { configPath, rootKey } = platform;
|
|
362
|
-
|
|
363
|
-
let existing = {};
|
|
364
|
-
try {
|
|
365
|
-
let raw = fs.readFileSync(configPath, "utf-8");
|
|
366
|
-
if (raw.charCodeAt(0) === 0xFEFF) raw = raw.slice(1);
|
|
367
|
-
existing = JSON.parse(raw);
|
|
368
|
-
if (typeof existing !== "object" || existing === null) existing = {};
|
|
369
|
-
} catch { /* start fresh */ }
|
|
370
|
-
|
|
371
|
-
if (!existing[rootKey]) existing[rootKey] = {};
|
|
372
|
-
existing[rootKey][serverName] = mcpEntry;
|
|
373
|
-
|
|
374
|
-
if (!dryRun) {
|
|
375
|
-
const dir = path.dirname(configPath);
|
|
376
|
-
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
377
|
-
|
|
378
|
-
if (fileExists(configPath)) {
|
|
379
|
-
try { fs.copyFileSync(configPath, configPath + ".bak"); } catch {}
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
fs.writeFileSync(configPath, JSON.stringify(existing, null, 2) + "\n");
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
return { success: true, method: "json" };
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
/**
|
|
389
|
-
* Write MCP config to TOML file (Codex).
|
|
390
|
-
* Appends or replaces a [mcp_servers.<name>] table.
|
|
391
|
-
* @param {object} platform - Platform object
|
|
392
|
-
* @param {string} serverName - Server name
|
|
393
|
-
* @param {object} mcpEntry - MCP config
|
|
394
|
-
* @param {boolean} dryRun
|
|
395
|
-
* @returns {{ success: boolean, method: string }}
|
|
396
|
-
*/
|
|
397
|
-
function installMcpToml(platform, serverName, mcpEntry, dryRun) {
|
|
398
|
-
const { configPath, rootKey } = platform;
|
|
399
|
-
|
|
400
|
-
let existing = "";
|
|
401
|
-
try { existing = fs.readFileSync(configPath, "utf-8"); } catch { /* start fresh */ }
|
|
402
|
-
|
|
403
|
-
// Remove existing entry if present
|
|
404
|
-
const tableHeader = `[${rootKey}.${serverName}]`;
|
|
405
|
-
if (existing.includes(tableHeader)) {
|
|
406
|
-
existing = removeTomlEntry(existing, rootKey, serverName);
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
const newBlock = buildTomlEntry(rootKey, serverName, mcpEntry);
|
|
410
|
-
|
|
411
|
-
if (!dryRun) {
|
|
412
|
-
const dir = path.dirname(configPath);
|
|
413
|
-
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
414
|
-
|
|
415
|
-
if (fileExists(configPath)) {
|
|
416
|
-
try { fs.copyFileSync(configPath, configPath + ".bak"); } catch {}
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
const sep = existing && !existing.endsWith("\n\n") ? (existing.endsWith("\n") ? "\n" : "\n\n") : "";
|
|
420
|
-
fs.writeFileSync(configPath, existing + sep + newBlock + "\n");
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
return { success: true, method: "toml" };
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
/**
|
|
427
|
-
* Remove an MCP server entry from a platform config.
|
|
428
|
-
* @param {object} platform - Platform object
|
|
429
|
-
* @param {string} serverName - Server name to remove
|
|
430
|
-
* @param {boolean} dryRun
|
|
431
|
-
* @returns {boolean} Whether anything was removed
|
|
432
|
-
*/
|
|
433
|
-
function uninstallMcp(platform, serverName, dryRun) {
|
|
434
|
-
const { configPath, rootKey } = platform;
|
|
435
|
-
if (!fileExists(configPath)) return false;
|
|
436
|
-
|
|
437
|
-
// TOML path (Codex)
|
|
438
|
-
if (platform.configFormat === "toml") {
|
|
439
|
-
try {
|
|
440
|
-
const content = fs.readFileSync(configPath, "utf-8");
|
|
441
|
-
const tableHeader = `[${rootKey}.${serverName}]`;
|
|
442
|
-
if (!content.includes(tableHeader)) return false;
|
|
443
|
-
if (!dryRun) {
|
|
444
|
-
fs.copyFileSync(configPath, configPath + ".bak");
|
|
445
|
-
const cleaned = removeTomlEntry(content, rootKey, serverName);
|
|
446
|
-
if (cleaned.trim()) {
|
|
447
|
-
fs.writeFileSync(configPath, cleaned);
|
|
448
|
-
} else {
|
|
449
|
-
fs.unlinkSync(configPath);
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
return true;
|
|
453
|
-
} catch { return false; }
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
// JSON path
|
|
457
|
-
try {
|
|
458
|
-
const data = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
459
|
-
if (!data?.[rootKey]?.[serverName]) return false;
|
|
460
|
-
delete data[rootKey][serverName];
|
|
461
|
-
if (Object.keys(data[rootKey]).length === 0) delete data[rootKey];
|
|
462
|
-
if (!dryRun) {
|
|
463
|
-
fs.copyFileSync(configPath, configPath + ".bak");
|
|
464
|
-
if (Object.keys(data).length === 0) {
|
|
465
|
-
fs.unlinkSync(configPath);
|
|
466
|
-
} else {
|
|
467
|
-
fs.writeFileSync(configPath, JSON.stringify(data, null, 2) + "\n");
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
return true;
|
|
471
|
-
} catch { return false; }
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
/**
|
|
475
|
-
* Update API key in existing MCP config.
|
|
476
|
-
* @param {object} platform - Platform object
|
|
477
|
-
* @param {string} serverName - Server name
|
|
478
|
-
* @param {object} mcpEntry - New MCP config
|
|
479
|
-
* @returns {{ success: boolean, method: string }}
|
|
480
|
-
*/
|
|
481
|
-
function updateMcpKey(platform, serverName, mcpEntry) {
|
|
482
|
-
if (platform.configFormat === "toml") {
|
|
483
|
-
return installMcpToml(platform, serverName, mcpEntry, false);
|
|
484
|
-
}
|
|
485
|
-
return installMcpJson(platform, serverName, mcpEntry, false);
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
module.exports = {
|
|
489
|
-
readMcpEntry,
|
|
490
|
-
buildHttpConfig,
|
|
491
|
-
buildHttpConfigWithAuth,
|
|
492
|
-
buildStdioConfig,
|
|
493
|
-
installMcp,
|
|
494
|
-
installMcpJson,
|
|
495
|
-
installMcpToml,
|
|
496
|
-
uninstallMcp,
|
|
497
|
-
updateMcpKey,
|
|
498
|
-
// TOML helpers (exported for testing)
|
|
499
|
-
parseTomlServerEntry,
|
|
500
|
-
parseTomlSubTables,
|
|
501
|
-
buildTomlEntry,
|
|
502
|
-
removeTomlEntry,
|
|
503
|
-
};
|
package/lib/platforms.js
DELETED
|
@@ -1,184 +0,0 @@
|
|
|
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
|
-
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
|
-
|
|
42
|
-
function getJunieMcpPath() {
|
|
43
|
-
const home = os.homedir();
|
|
44
|
-
return path.join(home, ".junie", "mcp", "mcp.json");
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function getCopilotJetBrainsMcpPath() {
|
|
48
|
-
const home = os.homedir();
|
|
49
|
-
if (process.platform === "win32") {
|
|
50
|
-
return path.join(process.env.APPDATA || path.join(home, "AppData", "Roaming"), "github-copilot", "intellij", "mcp.json");
|
|
51
|
-
}
|
|
52
|
-
return path.join(home, ".config", "github-copilot", "intellij", "mcp.json");
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function getCopilotCliMcpPath() {
|
|
56
|
-
const home = os.homedir();
|
|
57
|
-
return path.join(home, ".copilot", "mcp-config.json");
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// ─── Platform Registry ──────────────────────────────────────
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Returns platform definition for manual override.
|
|
64
|
-
* @param {string} platformId
|
|
65
|
-
* @returns {object} Platform object with configPath, rulesPath, rootKey, etc.
|
|
66
|
-
*/
|
|
67
|
-
function createManualPlatform(platformId) {
|
|
68
|
-
const home = os.homedir();
|
|
69
|
-
const configs = {
|
|
70
|
-
"claude-code": {
|
|
71
|
-
configPath: path.join(home, ".claude.json"),
|
|
72
|
-
rulesPath: path.join(home, ".claude", "CLAUDE.md"),
|
|
73
|
-
rootKey: "mcpServers",
|
|
74
|
-
configFormat: "json",
|
|
75
|
-
},
|
|
76
|
-
cursor: {
|
|
77
|
-
configPath: path.join(home, ".cursor", "mcp.json"),
|
|
78
|
-
rulesPath: null,
|
|
79
|
-
rootKey: "mcpServers",
|
|
80
|
-
configFormat: "json",
|
|
81
|
-
},
|
|
82
|
-
windsurf: {
|
|
83
|
-
configPath: path.join(home, ".codeium", "windsurf", "mcp_config.json"),
|
|
84
|
-
rulesPath: path.join(home, ".codeium", "windsurf", "memories", "global_rules.md"),
|
|
85
|
-
rootKey: "mcpServers",
|
|
86
|
-
configFormat: "json",
|
|
87
|
-
},
|
|
88
|
-
vscode: {
|
|
89
|
-
configPath: getVsCodeMcpPath(),
|
|
90
|
-
rulesPath: null,
|
|
91
|
-
rootKey: "servers",
|
|
92
|
-
configFormat: "json",
|
|
93
|
-
},
|
|
94
|
-
cline: {
|
|
95
|
-
configPath: getClineConfigPath(),
|
|
96
|
-
rulesPath: path.join(home, "Documents", "Cline", "Rules"),
|
|
97
|
-
rootKey: "mcpServers",
|
|
98
|
-
configFormat: "json",
|
|
99
|
-
},
|
|
100
|
-
"roo-code": {
|
|
101
|
-
configPath: getRooConfigPath(),
|
|
102
|
-
rulesPath: path.join(home, ".roo", "rules"),
|
|
103
|
-
rootKey: "mcpServers",
|
|
104
|
-
configFormat: "json",
|
|
105
|
-
},
|
|
106
|
-
codex: {
|
|
107
|
-
configPath: getCodexConfigPath(),
|
|
108
|
-
rulesPath: path.join(process.env.CODEX_HOME || path.join(home, ".codex"), "AGENTS.md"),
|
|
109
|
-
rootKey: "mcp_servers",
|
|
110
|
-
configFormat: "toml",
|
|
111
|
-
},
|
|
112
|
-
"gemini-cli": {
|
|
113
|
-
configPath: getGeminiSettingsPath(),
|
|
114
|
-
rulesPath: path.join(home, ".gemini", "GEMINI.md"),
|
|
115
|
-
rootKey: "mcpServers",
|
|
116
|
-
configFormat: "json",
|
|
117
|
-
},
|
|
118
|
-
junie: {
|
|
119
|
-
configPath: getJunieMcpPath(),
|
|
120
|
-
rulesPath: null,
|
|
121
|
-
rootKey: "mcpServers",
|
|
122
|
-
configFormat: "json",
|
|
123
|
-
},
|
|
124
|
-
"copilot-jetbrains": {
|
|
125
|
-
configPath: getCopilotJetBrainsMcpPath(),
|
|
126
|
-
rulesPath: null,
|
|
127
|
-
rootKey: "mcpServers",
|
|
128
|
-
configFormat: "json",
|
|
129
|
-
},
|
|
130
|
-
"copilot-cli": {
|
|
131
|
-
configPath: getCopilotCliMcpPath(),
|
|
132
|
-
rulesPath: null,
|
|
133
|
-
rootKey: "mcpServers",
|
|
134
|
-
configFormat: "json",
|
|
135
|
-
},
|
|
136
|
-
};
|
|
137
|
-
|
|
138
|
-
const def = configs[platformId];
|
|
139
|
-
if (!def) {
|
|
140
|
-
throw new Error(`Unknown platform: ${platformId}. Supported: ${Object.keys(configs).join(", ")}`);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
return { platform: platformId, version: "unknown", hasCli: false, existingMcp: null, ...def };
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* Display name for a platform id.
|
|
148
|
-
*/
|
|
149
|
-
function platformName(id) {
|
|
150
|
-
const names = {
|
|
151
|
-
"claude-code": "Claude Code",
|
|
152
|
-
cursor: "Cursor",
|
|
153
|
-
windsurf: "Windsurf",
|
|
154
|
-
vscode: "VS Code",
|
|
155
|
-
cline: "Cline",
|
|
156
|
-
"roo-code": "Roo Code",
|
|
157
|
-
codex: "Codex",
|
|
158
|
-
"gemini-cli": "Gemini CLI",
|
|
159
|
-
junie: "Junie",
|
|
160
|
-
"copilot-jetbrains": "Copilot (JetBrains)",
|
|
161
|
-
"copilot-cli": "Copilot CLI",
|
|
162
|
-
};
|
|
163
|
-
return names[id] || id;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
/**
|
|
167
|
-
* All known platform IDs.
|
|
168
|
-
*/
|
|
169
|
-
const KNOWN_PLATFORMS = ["claude-code", "cursor", "windsurf", "vscode", "cline", "roo-code", "codex", "gemini-cli", "junie", "copilot-jetbrains", "copilot-cli"];
|
|
170
|
-
|
|
171
|
-
module.exports = {
|
|
172
|
-
getVsCodeUserDir,
|
|
173
|
-
getVsCodeMcpPath,
|
|
174
|
-
getClineConfigPath,
|
|
175
|
-
getRooConfigPath,
|
|
176
|
-
getCodexConfigPath,
|
|
177
|
-
getGeminiSettingsPath,
|
|
178
|
-
getJunieMcpPath,
|
|
179
|
-
getCopilotJetBrainsMcpPath,
|
|
180
|
-
getCopilotCliMcpPath,
|
|
181
|
-
createManualPlatform,
|
|
182
|
-
platformName,
|
|
183
|
-
KNOWN_PLATFORMS,
|
|
184
|
-
};
|