ac-framework 1.9.1 → 1.9.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ac-framework",
3
- "version": "1.9.1",
3
+ "version": "1.9.2",
4
4
  "description": "Agentic Coding Framework - Multi-assistant configuration system with OpenSpec workflows",
5
5
  "main": "src/index.js",
6
6
  "exports": {
@@ -2,12 +2,15 @@
2
2
  * mcp-installer.js — MCP config installer for AC Framework
3
3
  *
4
4
  * Detects installed AI assistants and injects the ac-framework-memory
5
- * MCP server into their config files.
5
+ * MCP server into their config files using the CORRECT format for each.
6
6
  *
7
- * Best-practice config format per MCP spec:
8
- * { "mcpServers": { "ac-framework-memory": { "command": "node", "args": [<absPath>] } } }
9
- *
10
- * Claude Code uses a different top-level key format (mcpServers inside claude.json).
7
+ * Verified formats (Mar 2026):
8
+ * opencode → ~/.config/opencode/opencode.json key:"mcp" type:"local" command:array
9
+ * claude → ~/.claude.json key:"mcpServers" type:"stdio"
10
+ * cursor → ~/.cursor/mcp.json key:"mcpServers"
11
+ * windsurf → ~/.codeium/windsurf/mcp_config.json key:"mcpServers"
12
+ * gemini → ~/.gemini/settings.json key:"mcpServers"
13
+ * codex → ~/.codex/config.toml TOML format
11
14
  */
12
15
 
13
16
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
@@ -17,61 +20,149 @@ import { fileURLToPath } from 'node:url';
17
20
 
18
21
  const __dirname = dirname(fileURLToPath(import.meta.url));
19
22
 
20
- // Absolute path to the MCP server entry point (ESM, referenced by node)
23
+ /** Absolute path to the MCP server entry point */
21
24
  export function getMCPServerPath() {
22
- const srcPath = join(__dirname, '../mcp/server.js');
23
- return srcPath;
25
+ return join(__dirname, '../mcp/server.js');
24
26
  }
25
27
 
26
- // ── Supported assistants ──────────────────────────────────────────
27
-
28
28
  const home = homedir();
29
- const IS_WIN = platform() === 'win32';
30
29
 
31
- /**
32
- * Each assistant entry:
33
- * configPath – absolute path to the JSON config file
34
- * configKey – top-level key that holds server map ("mcpServers" | "servers")
35
- * detectDir – directory whose existence signals the assistant is present
36
- */
37
- const ASSISTANTS = [
30
+ // ── Assistant definitions ─────────────────────────────────────────
31
+ //
32
+ // Each entry may have a custom `install` / `uninstall` function if its
33
+ // format differs from the standard JSON mcpServers pattern.
34
+
35
+ export const ASSISTANTS = [
36
+ // ── OpenCode ───────────────────────────────────────────────────
37
+ // Config: ~/.config/opencode/opencode.json
38
+ // Schema: { "mcp": { "<name>": { "type": "local", "command": ["node", "path"] } } }
38
39
  {
39
40
  name: 'opencode',
40
- configPath: join(home, '.opencode', 'mcp.json'),
41
- configKey: 'mcpServers',
42
- detectDir: join(home, '.opencode'),
41
+ configPath: join(home, '.config', 'opencode', 'opencode.json'),
42
+ detectDir: join(home, '.config', 'opencode'),
43
+ install(serverPath) {
44
+ const configDir = dirname(this.configPath);
45
+ if (!existsSync(configDir)) mkdirSync(configDir, { recursive: true });
46
+
47
+ let config = {};
48
+ if (existsSync(this.configPath)) {
49
+ try { config = JSON.parse(readFileSync(this.configPath, 'utf8')); } catch { config = {}; }
50
+ }
51
+
52
+ if (!config.mcp) config.mcp = {};
53
+ config.mcp['ac-framework-memory'] = {
54
+ type: 'local',
55
+ command: ['node', serverPath],
56
+ };
57
+
58
+ writeFileSync(this.configPath, JSON.stringify(config, null, 2));
59
+ return true;
60
+ },
61
+ uninstall() {
62
+ if (!existsSync(this.configPath)) return true;
63
+ let config = {};
64
+ try { config = JSON.parse(readFileSync(this.configPath, 'utf8')); } catch { return true; }
65
+ if (config.mcp?.['ac-framework-memory']) {
66
+ delete config.mcp['ac-framework-memory'];
67
+ writeFileSync(this.configPath, JSON.stringify(config, null, 2));
68
+ }
69
+ return true;
70
+ },
43
71
  },
72
+
73
+ // ── Claude Code CLI ────────────────────────────────────────────
74
+ // Config: ~/.claude.json (top-level mcpServers key, merges with existing data)
75
+ // Schema: { "mcpServers": { "<name>": { "type": "stdio", "command": "node", "args": ["path"] } } }
44
76
  {
45
77
  name: 'claude',
46
- // Claude Code CLI uses ~/.claude.json (mcpServers key)
47
78
  configPath: join(home, '.claude.json'),
48
- configKey: 'mcpServers',
49
- detectDir: home,
50
79
  detectFile: join(home, '.claude.json'),
80
+ install(serverPath) {
81
+ let config = {};
82
+ if (existsSync(this.configPath)) {
83
+ try { config = JSON.parse(readFileSync(this.configPath, 'utf8')); } catch { config = {}; }
84
+ }
85
+
86
+ if (!config.mcpServers) config.mcpServers = {};
87
+ config.mcpServers['ac-framework-memory'] = {
88
+ type: 'stdio',
89
+ command: 'node',
90
+ args: [serverPath],
91
+ };
92
+
93
+ writeFileSync(this.configPath, JSON.stringify(config, null, 2));
94
+ return true;
95
+ },
96
+ uninstall() {
97
+ if (!existsSync(this.configPath)) return true;
98
+ let config = {};
99
+ try { config = JSON.parse(readFileSync(this.configPath, 'utf8')); } catch { return true; }
100
+ if (config.mcpServers?.['ac-framework-memory']) {
101
+ delete config.mcpServers['ac-framework-memory'];
102
+ writeFileSync(this.configPath, JSON.stringify(config, null, 2));
103
+ }
104
+ return true;
105
+ },
51
106
  },
107
+
108
+ // ── Cursor IDE ─────────────────────────────────────────────────
109
+ // Config: ~/.cursor/mcp.json
110
+ // Schema: { "mcpServers": { "<name>": { "command": "node", "args": ["path"] } } }
52
111
  {
53
112
  name: 'cursor',
54
113
  configPath: join(home, '.cursor', 'mcp.json'),
55
- configKey: 'mcpServers',
56
114
  detectDir: join(home, '.cursor'),
115
+ install(serverPath) {
116
+ return installJsonMcpServers(this.configPath, serverPath);
117
+ },
118
+ uninstall() {
119
+ return uninstallJsonMcpServers(this.configPath);
120
+ },
57
121
  },
122
+
123
+ // ── Windsurf IDE ───────────────────────────────────────────────
124
+ // Config: ~/.codeium/windsurf/mcp_config.json (NOT ~/.windsurf/mcp.json)
125
+ // Schema: { "mcpServers": { "<name>": { "command": "node", "args": ["path"] } } }
58
126
  {
59
127
  name: 'windsurf',
60
- configPath: join(home, '.windsurf', 'mcp.json'),
61
- configKey: 'mcpServers',
62
- detectDir: join(home, '.windsurf'),
128
+ configPath: join(home, '.codeium', 'windsurf', 'mcp_config.json'),
129
+ detectDir: join(home, '.codeium', 'windsurf'),
130
+ install(serverPath) {
131
+ return installJsonMcpServers(this.configPath, serverPath);
132
+ },
133
+ uninstall() {
134
+ return uninstallJsonMcpServers(this.configPath);
135
+ },
63
136
  },
137
+
138
+ // ── Google Gemini CLI ──────────────────────────────────────────
139
+ // Config: ~/.gemini/settings.json (NOT ~/.gemini/mcp.json)
140
+ // Schema: { "mcpServers": { "<name>": { "command": "node", "args": ["path"] } } }
64
141
  {
65
142
  name: 'gemini',
66
- configPath: join(home, '.gemini', 'mcp.json'),
67
- configKey: 'mcpServers',
143
+ configPath: join(home, '.gemini', 'settings.json'),
68
144
  detectDir: join(home, '.gemini'),
145
+ install(serverPath) {
146
+ return installJsonMcpServers(this.configPath, serverPath);
147
+ },
148
+ uninstall() {
149
+ return uninstallJsonMcpServers(this.configPath);
150
+ },
69
151
  },
152
+
153
+ // ── OpenAI Codex CLI ───────────────────────────────────────────
154
+ // Config: ~/.codex/config.toml (TOML format, NOT JSON)
155
+ // Schema: [mcp_servers.ac-framework-memory]\ncommand = "node"\nargs = ["/path"]
70
156
  {
71
157
  name: 'codex',
72
- configPath: join(home, '.codex', 'mcp.json'),
73
- configKey: 'mcpServers',
158
+ configPath: join(home, '.codex', 'config.toml'),
74
159
  detectDir: join(home, '.codex'),
160
+ install(serverPath) {
161
+ return installTomlMcpServer(this.configPath, serverPath);
162
+ },
163
+ uninstall() {
164
+ return uninstallTomlMcpServer(this.configPath);
165
+ },
75
166
  },
76
167
  ];
77
168
 
@@ -79,7 +170,6 @@ const ASSISTANTS = [
79
170
 
80
171
  export function isAssistantInstalled(assistant) {
81
172
  try {
82
- // If a specific file is the detection signal, use that
83
173
  if (assistant.detectFile) return existsSync(assistant.detectFile);
84
174
  return existsSync(assistant.detectDir);
85
175
  } catch {
@@ -87,39 +177,115 @@ export function isAssistantInstalled(assistant) {
87
177
  }
88
178
  }
89
179
 
90
- // ── Install / Uninstall ───────────────────────────────────────────
180
+ // ── Generic JSON mcpServers helpers ───────────────────────────────
91
181
 
92
- export function installMCPForAssistant(assistant) {
93
- try {
94
- const configDir = dirname(assistant.configPath);
182
+ function installJsonMcpServers(configPath, serverPath) {
183
+ const configDir = dirname(configPath);
184
+ if (!existsSync(configDir)) mkdirSync(configDir, { recursive: true });
95
185
 
96
- if (!existsSync(configDir)) {
97
- mkdirSync(configDir, { recursive: true });
98
- }
186
+ let config = {};
187
+ if (existsSync(configPath)) {
188
+ try { config = JSON.parse(readFileSync(configPath, 'utf8')); } catch { config = {}; }
189
+ }
99
190
 
100
- // Read existing config or start fresh
101
- let config = {};
102
- if (existsSync(assistant.configPath)) {
103
- try {
104
- config = JSON.parse(readFileSync(assistant.configPath, 'utf8'));
105
- } catch {
106
- config = {};
107
- }
108
- }
191
+ if (!config.mcpServers) config.mcpServers = {};
192
+ config.mcpServers['ac-framework-memory'] = {
193
+ command: 'node',
194
+ args: [serverPath],
195
+ };
196
+
197
+ writeFileSync(configPath, JSON.stringify(config, null, 2));
198
+ return true;
199
+ }
109
200
 
110
- // Ensure the server map key exists
111
- if (!config[assistant.configKey]) {
112
- config[assistant.configKey] = {};
201
+ function uninstallJsonMcpServers(configPath) {
202
+ if (!existsSync(configPath)) return true;
203
+ let config = {};
204
+ try { config = JSON.parse(readFileSync(configPath, 'utf8')); } catch { return true; }
205
+ if (config.mcpServers?.['ac-framework-memory']) {
206
+ delete config.mcpServers['ac-framework-memory'];
207
+ writeFileSync(configPath, JSON.stringify(config, null, 2));
208
+ }
209
+ return true;
210
+ }
211
+
212
+ // ── TOML helpers (Codex) ──────────────────────────────────────────
213
+ //
214
+ // We write TOML manually (no external dep). The section to add/replace:
215
+ //
216
+ // [mcp_servers.ac-framework-memory]
217
+ // command = "node"
218
+ // args = ["/abs/path/to/server.js"]
219
+
220
+ const TOML_SECTION = 'mcp_servers.ac-framework-memory';
221
+
222
+ function installTomlMcpServer(configPath, serverPath) {
223
+ const configDir = dirname(configPath);
224
+ if (!existsSync(configDir)) mkdirSync(configDir, { recursive: true });
225
+
226
+ // Escape backslashes for Windows paths
227
+ const escapedPath = serverPath.replace(/\\/g, '\\\\');
228
+ const newBlock = [
229
+ `[${TOML_SECTION}]`,
230
+ `command = "node"`,
231
+ `args = ["${escapedPath}"]`,
232
+ ].join('\n');
233
+
234
+ let existing = '';
235
+ if (existsSync(configPath)) {
236
+ try { existing = readFileSync(configPath, 'utf8'); } catch { existing = ''; }
237
+ }
238
+
239
+ if (existing.includes(`[${TOML_SECTION}]`)) {
240
+ // Replace existing block — remove old section, append new one
241
+ existing = removeTomlSection(existing, TOML_SECTION);
242
+ }
243
+
244
+ const separator = existing.trim() ? '\n\n' : '';
245
+ writeFileSync(configPath, existing.trimEnd() + separator + newBlock + '\n');
246
+ return true;
247
+ }
248
+
249
+ function uninstallTomlMcpServer(configPath) {
250
+ if (!existsSync(configPath)) return true;
251
+ let content = '';
252
+ try { content = readFileSync(configPath, 'utf8'); } catch { return true; }
253
+ if (content.includes(`[${TOML_SECTION}]`)) {
254
+ content = removeTomlSection(content, TOML_SECTION);
255
+ writeFileSync(configPath, content);
256
+ }
257
+ return true;
258
+ }
259
+
260
+ /**
261
+ * Removes a TOML section (and its key-value lines) from a TOML string.
262
+ * Stops at the next [section] header or end of file.
263
+ */
264
+ function removeTomlSection(toml, sectionKey) {
265
+ const lines = toml.split('\n');
266
+ const result = [];
267
+ let inSection = false;
268
+
269
+ for (const line of lines) {
270
+ const trimmed = line.trim();
271
+ if (trimmed === `[${sectionKey}]`) {
272
+ inSection = true;
273
+ continue;
113
274
  }
275
+ if (inSection && trimmed.startsWith('[')) {
276
+ inSection = false;
277
+ }
278
+ if (!inSection) result.push(line);
279
+ }
114
280
 
115
- // Add / overwrite our server entry
116
- config[assistant.configKey]['ac-framework-memory'] = {
117
- command: 'node',
118
- args: [getMCPServerPath()],
119
- };
281
+ return result.join('\n');
282
+ }
283
+
284
+ // ── Install / Uninstall per assistant ────────────────────────────
120
285
 
121
- writeFileSync(assistant.configPath, JSON.stringify(config, null, 2));
122
- return true;
286
+ export function installMCPForAssistant(assistant) {
287
+ try {
288
+ return assistant.install(getMCPServerPath());
123
289
  } catch (error) {
124
290
  console.error(` Failed to install MCP for ${assistant.name}: ${error.message}`);
125
291
  return false;
@@ -128,21 +294,7 @@ export function installMCPForAssistant(assistant) {
128
294
 
129
295
  export function uninstallMCPForAssistant(assistant) {
130
296
  try {
131
- if (!existsSync(assistant.configPath)) return true;
132
-
133
- let config = {};
134
- try {
135
- config = JSON.parse(readFileSync(assistant.configPath, 'utf8'));
136
- } catch {
137
- return true;
138
- }
139
-
140
- if (config[assistant.configKey]?.['ac-framework-memory']) {
141
- delete config[assistant.configKey]['ac-framework-memory'];
142
- writeFileSync(assistant.configPath, JSON.stringify(config, null, 2));
143
- }
144
-
145
- return true;
297
+ return assistant.uninstall();
146
298
  } catch (error) {
147
299
  console.error(` Failed to uninstall MCP for ${assistant.name}: ${error.message}`);
148
300
  return false;
@@ -190,5 +342,3 @@ export function uninstallAllMCPs() {
190
342
  }
191
343
  return { success };
192
344
  }
193
-
194
- export { ASSISTANTS };