ac-framework 1.9.0 → 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.0",
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": {
@@ -54,9 +54,7 @@ const acGradient = gradient(['#6C5CE7', '#00CEC9', '#0984E3']);
54
54
  */
55
55
  async function setupPersistentMemory() {
56
56
  const memoryDbPath = join(homedir(), '.acfm', 'memory.db');
57
-
58
- // Only ask on first run
59
- if (existsSync(memoryDbPath)) return;
57
+ const alreadyExists = existsSync(memoryDbPath);
60
58
 
61
59
  console.log();
62
60
  await animatedSeparator(60);
@@ -93,16 +91,20 @@ async function setupPersistentMemory() {
93
91
  }
94
92
 
95
93
  console.log();
96
- console.log(chalk.hex('#B2BEC3')(' Initializing NexusVault...'));
94
+ console.log(chalk.hex('#B2BEC3')(alreadyExists ? ' Reconnecting NexusVault...' : ' Initializing NexusVault...'));
97
95
 
98
- // Init the SQLite database
96
+ // Init the SQLite database (idempotent — skips if already exists)
99
97
  const { initDatabase, isDatabaseInitialized } = await import('../memory/database.js');
100
98
  if (!isDatabaseInitialized()) {
101
99
  initDatabase();
102
100
  }
103
101
  console.log(
104
102
  chalk.hex('#00CEC9')(' ◆ ') +
105
- chalk.hex('#DFE6E9')('NexusVault database created at ~/.acfm/memory.db')
103
+ chalk.hex('#DFE6E9')(
104
+ alreadyExists
105
+ ? 'NexusVault database found at ~/.acfm/memory.db'
106
+ : 'NexusVault database created at ~/.acfm/memory.db'
107
+ )
106
108
  );
107
109
 
108
110
  // Install MCP server into detected assistants
@@ -284,7 +286,7 @@ export async function initCommand(options = {}) {
284
286
 
285
287
  // Dynamic step counting: +1 step when downloading from GitHub
286
288
  const stepOffset = useLatest ? 1 : 0;
287
- const totalSteps = 4 + stepOffset;
289
+ const totalSteps = 5 + stepOffset;
288
290
 
289
291
  // Framework source: bundled by default, overridden by --latest
290
292
  let frameworkPath = FRAMEWORK_PATH;
@@ -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 };