ai-agent-config 2.7.3 → 2.8.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/README.md CHANGED
@@ -61,14 +61,16 @@ ai-agent update
61
61
 
62
62
  ## Supported Platforms
63
63
 
64
- | Platform | Skills Path | MCP Support |
65
- |----------|-------------|-------------|
66
- | Claude Code | `~/.claude/skills/` | `claude_desktop_config.json` |
67
- | Antigravity IDE | `~/.gemini/antigravity/skills/` | `mcp_config.json` |
68
- | Cursor | `~/.cursor/skills/` | - |
69
- | Windsurf | `~/.windsurf/skills/` | - |
70
- | Codex CLI | `~/.codex/skills/` | - |
71
- | GitHub Copilot | `~/.github/copilot-instructions.md` | - |
64
+ | Platform | Skills Path | MCP Support | Format |
65
+ |----------|-------------|-------------|--------|
66
+ | Claude Code | `~/.claude/skills/` | `claude_desktop_config.json` | JSON |
67
+ | Antigravity IDE | `~/.gemini/antigravity/skills/` | `mcp_config.json` | JSON |
68
+ | **Cursor** | `~/.cursor/skills/` | **`~/.cursor/mcp.json`** | JSON |
69
+ | **Windsurf** | `~/.windsurf/skills/` | **`~/.codeium/windsurf/mcp_config.json`** | JSON |
70
+ | **Codex CLI** | `~/.codex/skills/` | **`~/.codex/config.toml`** | **TOML** |
71
+ | GitHub Copilot | `~/.github/copilot-instructions.md` | ❌ | - |
72
+
73
+ **New in v2.8.0:** MCP server support for Cursor, Windsurf, and Codex CLI!
72
74
 
73
75
  ## Secret Management
74
76
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-agent-config",
3
- "version": "2.7.3",
3
+ "version": "2.8.0",
4
4
  "description": "Universal skill & workflow manager for AI coding assistants with bi-directional GitHub sync",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -40,6 +40,7 @@
40
40
  "node": ">=18.0.0"
41
41
  },
42
42
  "dependencies": {
43
+ "@iarna/toml": "^2.2.5",
43
44
  "inquirer": "^9.2.12"
44
45
  },
45
46
  "files": [
@@ -50,4 +51,4 @@
50
51
  "index.js",
51
52
  "README.md"
52
53
  ]
53
- }
54
+ }
@@ -5,11 +5,18 @@
5
5
 
6
6
  const fs = require("fs");
7
7
  const path = require("path");
8
+ const toml = require("@iarna/toml");
8
9
  const platforms = require("./platforms");
9
10
  const configManager = require("./config-manager");
10
11
 
11
12
  const SKIP_FOLDERS = ["bitwarden"];
12
13
 
14
+ // Config format constants
15
+ const FORMAT_JSON = "json";
16
+ const FORMAT_TOML = "toml";
17
+ const TOML_MCP_KEY = "mcp_servers"; // Underscore! Not dot
18
+ const JSON_MCP_KEY = "mcpServers"; // CamelCase
19
+
13
20
  /**
14
21
  * Get the MCP servers directory from the user's sync repo
15
22
  * @returns {string|null} Path to .agent/mcp-servers/ or null
@@ -85,6 +92,143 @@ function getAvailableMcpServers() {
85
92
  return servers;
86
93
  }
87
94
 
95
+ /**
96
+ * Get config format for a platform
97
+ * @param {string} platformName - Platform name
98
+ * @returns {string} "json" or "toml"
99
+ */
100
+ function getConfigFormat(platformName) {
101
+ const platform = platforms.getByName(platformName);
102
+ return platform?.mcpConfigFormat || FORMAT_JSON;
103
+ }
104
+
105
+ /**
106
+ * Read platform config file (supports JSON and TOML)
107
+ * @param {string} configPath - Path to config file
108
+ * @param {string} format - "json" or "toml"
109
+ * @returns {Object} Parsed config or empty object
110
+ */
111
+ function readPlatformConfig(configPath, format) {
112
+ if (!fs.existsSync(configPath)) {
113
+ return {};
114
+ }
115
+
116
+ try {
117
+ const content = fs.readFileSync(configPath, "utf-8");
118
+
119
+ if (format === FORMAT_TOML) {
120
+ return toml.parse(content);
121
+ } else {
122
+ return JSON.parse(content);
123
+ }
124
+ } catch (error) {
125
+ console.warn(`⚠️ Failed to parse config at ${configPath}: ${error.message}`);
126
+ return {};
127
+ }
128
+ }
129
+
130
+ /**
131
+ * Write platform config file (supports JSON and TOML)
132
+ * @param {string} configPath - Path to config file
133
+ * @param {Object} config - Config object to write
134
+ * @param {string} format - "json" or "toml"
135
+ */
136
+ function writePlatformConfig(configPath, config, format) {
137
+ let content;
138
+
139
+ if (format === FORMAT_TOML) {
140
+ content = toml.stringify(config);
141
+ } else {
142
+ content = JSON.stringify(config, null, 2) + "\n";
143
+ }
144
+
145
+ // Create directory if needed
146
+ const configDir = path.dirname(configPath);
147
+ if (!fs.existsSync(configDir)) {
148
+ fs.mkdirSync(configDir, { recursive: true });
149
+ }
150
+
151
+ fs.writeFileSync(configPath, content, "utf-8");
152
+
153
+ // Set restrictive permissions (owner read/write only) to protect secrets
154
+ // Only on Unix-like systems (macOS, Linux) - Windows uses ACL instead
155
+ if (process.platform !== "win32") {
156
+ try {
157
+ fs.chmodSync(configPath, 0o600);
158
+ } catch (e) {
159
+ console.warn(`⚠️ Warning: Could not set file permissions on ${configPath}: ${e.message}`);
160
+ }
161
+ }
162
+ }
163
+
164
+ /**
165
+ * Build server config object with platform-specific fields
166
+ * @param {Object} server - Server config from repo
167
+ * @param {string} platformName - Platform name
168
+ * @param {Object} existing - Existing server config (for preservation)
169
+ * @returns {Object} Platform-specific server config
170
+ */
171
+ function buildServerConfig(server, platformName, existing = {}) {
172
+ const config = {
173
+ command: server.command,
174
+ args: server.args,
175
+ };
176
+
177
+ // Preserve existing env vars
178
+ if (existing.env) {
179
+ config.env = existing.env;
180
+ }
181
+
182
+ // Platform-specific field handling
183
+ switch (platformName) {
184
+ case "antigravity":
185
+ // Antigravity supports disabledTools
186
+ // Preserve existing disabledTools, or use server's if present
187
+ if (existing.disabledTools) {
188
+ config.disabledTools = existing.disabledTools;
189
+ } else if (server.disabledTools && server.disabledTools.length > 0) {
190
+ config.disabledTools = server.disabledTools;
191
+ }
192
+ break;
193
+
194
+ case "windsurf":
195
+ // Windsurf uses "disabled" boolean field
196
+ // Preserve user's manual setting, otherwise use repo default
197
+ if (existing.disabled !== undefined) {
198
+ config.disabled = existing.disabled;
199
+ } else if (server.enabled !== undefined) {
200
+ config.disabled = !server.enabled;
201
+ }
202
+ break;
203
+
204
+ case "codex":
205
+ // Codex supports enabled, enabled_tools, disabled_tools
206
+ // Preserve user's manual settings, otherwise use repo defaults
207
+ if (existing.enabled !== undefined) {
208
+ config.enabled = existing.enabled;
209
+ } else if (server.enabled !== undefined) {
210
+ config.enabled = server.enabled;
211
+ }
212
+
213
+ if (existing.disabled_tools) {
214
+ config.disabled_tools = existing.disabled_tools;
215
+ } else if (server.disabledTools && server.disabledTools.length > 0) {
216
+ config.disabled_tools = server.disabledTools; // Note: snake_case
217
+ }
218
+ break;
219
+
220
+ case "claude":
221
+ // Claude doesn't support disabledTools - skip
222
+ break;
223
+
224
+ case "cursor":
225
+ // Cursor doesn't support disabledTools - skip
226
+ break;
227
+ }
228
+
229
+ return config;
230
+ }
231
+
88
232
  /**
89
233
  * Collect all bitwardenEnv entries from MCP server configs
90
234
  * @returns {Array<{ serverName: string, envVar: string, bitwardenItem: string }>}
@@ -119,63 +263,39 @@ function collectBitwardenEnvs() {
119
263
  function writeMcpToPlatformConfig(configPath, servers, options = {}) {
120
264
  const { force = false, platformName = "" } = options;
121
265
 
122
- // Read existing config — preserve ALL existing keys (e.g. Claude's "preferences")
123
- let config = { mcpServers: {} };
124
- if (fs.existsSync(configPath)) {
125
- try {
126
- config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
127
- if (!config.mcpServers) config.mcpServers = {};
128
- } catch {
129
- config = { mcpServers: {} };
130
- }
266
+ // Determine config format
267
+ const format = getConfigFormat(platformName);
268
+
269
+ // Read existing config — preserve ALL existing keys
270
+ let config = readPlatformConfig(configPath, format);
271
+
272
+ // Initialize MCP servers section if not exists
273
+ const mcpKey = format === FORMAT_TOML ? TOML_MCP_KEY : JSON_MCP_KEY;
274
+ if (!config[mcpKey]) {
275
+ config[mcpKey] = {};
131
276
  }
132
277
 
133
278
  let added = 0;
134
279
  let skipped = 0;
135
280
 
136
281
  for (const server of servers) {
137
- const existing = config.mcpServers[server.name];
282
+ const existing = config[mcpKey][server.name];
138
283
 
139
284
  if (existing && !force) {
140
285
  skipped++;
141
286
  continue;
142
287
  }
143
288
 
144
- const entry = {
145
- command: server.command,
146
- args: server.args,
147
- };
148
-
149
- // Preserve existing env
150
- if (existing && existing.env) {
151
- entry.env = existing.env;
152
- }
289
+ // Build platform-specific config
290
+ const entry = buildServerConfig(server, platformName, existing);
153
291
 
154
- // disabledTools: only add for platforms that support it (not Claude)
155
- if (platformName !== "claude" && server.disabledTools && server.disabledTools.length > 0) {
156
- entry.disabledTools = server.disabledTools;
157
- }
158
-
159
- config.mcpServers[server.name] = entry;
292
+ config[mcpKey][server.name] = entry;
160
293
  added++;
161
294
  }
162
295
 
163
- // Write back (create directory if needed)
296
+ // Write back only if changes were made
164
297
  if (added > 0) {
165
- const configDir = path.dirname(configPath);
166
- if (!fs.existsSync(configDir)) {
167
- fs.mkdirSync(configDir, { recursive: true });
168
- }
169
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
170
- // Set restrictive permissions (owner read/write only) to protect secrets
171
- // Only on Unix-like systems (macOS, Linux) - Windows uses ACL instead
172
- if (process.platform !== "win32") {
173
- try {
174
- fs.chmodSync(configPath, 0o600);
175
- } catch (e) {
176
- console.warn(`⚠️ Warning: Could not set file permissions on ${configPath}: ${e.message}`);
177
- }
178
- }
298
+ writePlatformConfig(configPath, config, format);
179
299
  }
180
300
 
181
301
  return { added, skipped };
@@ -233,31 +353,30 @@ function installMcpServers(options = {}) {
233
353
  * @param {string} configPath - Path to platform's MCP config file
234
354
  * @param {Array} servers - MCP server configs
235
355
  * @param {Object} resolvedSecrets - Map of bitwardenItem → resolvedValue
236
- * @param {string} platformName - Platform name for disabledTools handling
356
+ * @param {string} platformName - Platform name for format and field handling
237
357
  * @returns {{ installed: number, servers: Array<{ name: string, secretsCount: number }> }}
238
358
  */
239
359
  function writeMcpWithSecretsToPlatformConfig(configPath, servers, resolvedSecrets, platformName) {
360
+ // Determine config format
361
+ const format = getConfigFormat(platformName);
362
+
240
363
  // Read existing config — preserve ALL existing keys
241
- let config = { mcpServers: {} };
242
- if (fs.existsSync(configPath)) {
243
- try {
244
- config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
245
- if (!config.mcpServers) config.mcpServers = {};
246
- } catch {
247
- config = { mcpServers: {} };
248
- }
364
+ let config = readPlatformConfig(configPath, format);
365
+
366
+ // Initialize MCP servers section if not exists
367
+ const mcpKey = format === FORMAT_TOML ? TOML_MCP_KEY : JSON_MCP_KEY;
368
+ if (!config[mcpKey]) {
369
+ config[mcpKey] = {};
249
370
  }
250
371
 
251
372
  let installed = 0;
252
373
  const serverResults = [];
253
374
 
254
375
  for (const server of servers) {
255
- const existing = config.mcpServers[server.name] || {};
376
+ const existing = config[mcpKey][server.name] || {};
256
377
 
257
- const entry = {
258
- command: server.command,
259
- args: server.args,
260
- };
378
+ // Build platform-specific config
379
+ const entry = buildServerConfig(server, platformName, existing);
261
380
 
262
381
  // Build env from resolved secrets
263
382
  if (server.bitwardenEnv) {
@@ -280,35 +399,13 @@ function writeMcpWithSecretsToPlatformConfig(configPath, servers, resolvedSecret
280
399
  serverResults.push({ name: server.name, secretsCount: 0 });
281
400
  }
282
401
 
283
- // disabledTools: only for platforms that support it (not Claude)
284
- if (platformName !== "claude") {
285
- if (existing.disabledTools) {
286
- entry.disabledTools = existing.disabledTools;
287
- } else if (server.disabledTools && server.disabledTools.length > 0) {
288
- entry.disabledTools = server.disabledTools;
289
- }
290
- }
291
-
292
- config.mcpServers[server.name] = entry;
402
+ config[mcpKey][server.name] = entry;
293
403
  installed++;
294
404
  }
295
405
 
296
406
  // Write back
297
407
  if (installed > 0) {
298
- const configDir = path.dirname(configPath);
299
- if (!fs.existsSync(configDir)) {
300
- fs.mkdirSync(configDir, { recursive: true });
301
- }
302
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
303
- // Set restrictive permissions (owner read/write only) to protect secrets
304
- // Only on Unix-like systems (macOS, Linux) - Windows uses ACL instead
305
- if (process.platform !== "win32") {
306
- try {
307
- fs.chmodSync(configPath, 0o600);
308
- } catch (e) {
309
- console.warn(`⚠️ Warning: Could not set file permissions on ${configPath}: ${e.message}`);
310
- }
311
- }
408
+ writePlatformConfig(configPath, config, format);
312
409
  }
313
410
 
314
411
  return { installed, servers: serverResults };
@@ -362,5 +459,10 @@ module.exports = {
362
459
  installMcpServers,
363
460
  installMcpServersWithSecrets,
364
461
  writeMcpToPlatformConfig,
462
+ // New helper functions (for testing)
463
+ getConfigFormat,
464
+ readPlatformConfig,
465
+ writePlatformConfig,
466
+ buildServerConfig,
365
467
  SKIP_FOLDERS,
366
468
  };
@@ -79,6 +79,7 @@ const SUPPORTED = [
79
79
  configDir: ".cursor",
80
80
  skillsDir: "skills",
81
81
  rulesDir: "rules",
82
+ mcpConfigFile: "mcp.json",
82
83
  get configPath() {
83
84
  return path.join(HOME, this.configDir);
84
85
  },
@@ -88,6 +89,9 @@ const SUPPORTED = [
88
89
  get rulesPath() {
89
90
  return path.join(HOME, this.configDir, this.rulesDir);
90
91
  },
92
+ get mcpConfigPath() {
93
+ return path.join(HOME, this.configDir, this.mcpConfigFile);
94
+ },
91
95
  detect() {
92
96
  return (
93
97
  fs.existsSync(this.configPath) ||
@@ -101,12 +105,17 @@ const SUPPORTED = [
101
105
  displayName: "Windsurf",
102
106
  configDir: ".windsurf",
103
107
  skillsDir: "skills",
108
+ mcpConfigFile: "mcp_config.json",
104
109
  get configPath() {
105
110
  return path.join(HOME, this.configDir);
106
111
  },
107
112
  get skillsPath() {
108
113
  return path.join(HOME, this.configDir, this.skillsDir);
109
114
  },
115
+ get mcpConfigPath() {
116
+ // Windsurf stores config in ~/.codeium/windsurf/
117
+ return path.join(HOME, ".codeium", "windsurf", this.mcpConfigFile);
118
+ },
110
119
  detect() {
111
120
  return (
112
121
  fs.existsSync(this.configPath) ||
@@ -119,12 +128,17 @@ const SUPPORTED = [
119
128
  displayName: "Codex CLI",
120
129
  configDir: ".codex",
121
130
  skillsDir: "skills",
131
+ mcpConfigFile: "config.toml",
132
+ mcpConfigFormat: "toml", // TOML format instead of JSON
122
133
  get configPath() {
123
134
  return path.join(HOME, this.configDir);
124
135
  },
125
136
  get skillsPath() {
126
137
  return path.join(HOME, this.configDir, this.skillsDir);
127
138
  },
139
+ get mcpConfigPath() {
140
+ return path.join(HOME, this.configDir, this.mcpConfigFile);
141
+ },
128
142
  detect() {
129
143
  return fs.existsSync(this.configPath);
130
144
  },