@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 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 JSON file
16
- * @param {string} rootKey - Root key ("mcpServers" or "servers")
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
- ...buildHttpConfig(serverUrl, platform),
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
- // JSON write (all platforms, fallback for CLI failures)
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.1.1",
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",