spritecook-mcp 0.2.11 → 0.2.12
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 +3 -3
- package/src/editors.mjs +531 -531
- package/src/setup.mjs +160 -160
- package/src/skill.mjs +124 -102
package/src/editors.mjs
CHANGED
|
@@ -1,531 +1,531 @@
|
|
|
1
|
-
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
2
|
-
import { join } from 'node:path';
|
|
3
|
-
import { homedir } from 'node:os';
|
|
4
|
-
import { success, warn, info } from './ui.mjs';
|
|
5
|
-
import { getMcpUrl } from './config.mjs';
|
|
6
|
-
|
|
7
|
-
// ── Read existing key ───────────────────────────────────────────────────
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Try to read an existing SpriteCook API key from any detected editor config.
|
|
11
|
-
* Returns the key string or null if none found.
|
|
12
|
-
*/
|
|
13
|
-
export function readExistingKey() {
|
|
14
|
-
const cwd = process.cwd();
|
|
15
|
-
const home = homedir();
|
|
16
|
-
|
|
17
|
-
const keyFrom = (config, ...path) => {
|
|
18
|
-
let obj = config;
|
|
19
|
-
for (const p of path) { obj = obj?.[p]; }
|
|
20
|
-
if (typeof obj === 'string' && obj.startsWith('Bearer sc_live_')) {
|
|
21
|
-
return obj.replace('Bearer ', '');
|
|
22
|
-
}
|
|
23
|
-
return null;
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
const tryFile = (filePath, ...keyPath) => {
|
|
27
|
-
try {
|
|
28
|
-
if (!existsSync(filePath)) return null;
|
|
29
|
-
const config = JSON.parse(readFileSync(filePath, 'utf-8'));
|
|
30
|
-
return keyFrom(config, ...keyPath);
|
|
31
|
-
} catch { return null; }
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
// Cursor project
|
|
35
|
-
const k1 = tryFile(join(cwd, '.cursor', 'mcp.json'), 'mcpServers', 'spritecook', 'headers', 'Authorization');
|
|
36
|
-
if (k1) return k1;
|
|
37
|
-
// Cursor global
|
|
38
|
-
const k1g = tryFile(join(home, '.cursor', 'mcp.json'), 'mcpServers', 'spritecook', 'headers', 'Authorization');
|
|
39
|
-
if (k1g) return k1g;
|
|
40
|
-
|
|
41
|
-
// VS Code project (new mcp.json format)
|
|
42
|
-
const k2 = tryFile(join(cwd, '.vscode', 'mcp.json'), 'servers', 'spritecook', 'headers', 'Authorization');
|
|
43
|
-
if (k2) return k2;
|
|
44
|
-
// VS Code legacy (settings.json)
|
|
45
|
-
const k2b = tryFile(join(cwd, '.vscode', 'settings.json'), 'mcp', 'servers', 'spritecook', 'headers', 'Authorization');
|
|
46
|
-
if (k2b) return k2b;
|
|
47
|
-
|
|
48
|
-
// Claude Desktop
|
|
49
|
-
const claudeDesktop = getClaudeDesktopConfigPath();
|
|
50
|
-
if (claudeDesktop) {
|
|
51
|
-
const k3 = tryFile(claudeDesktop, 'mcpServers', 'spritecook', 'headers', 'Authorization');
|
|
52
|
-
if (k3) return k3;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// Claude Code
|
|
56
|
-
const k4a = tryFile(join(home, '.claude', 'settings.json'), 'mcpServers', 'spritecook', 'headers', 'Authorization');
|
|
57
|
-
if (k4a) return k4a;
|
|
58
|
-
const k4b = tryFile(join(home, '.claude.json'), 'mcpServers', 'spritecook', 'headers', 'Authorization');
|
|
59
|
-
if (k4b) return k4b;
|
|
60
|
-
|
|
61
|
-
// Antigravity
|
|
62
|
-
const antigravity = getAntigravityConfigPath();
|
|
63
|
-
const k5 = tryFile(antigravity, 'mcpServers', 'spritecook', 'headers', 'Authorization');
|
|
64
|
-
if (k5) return k5;
|
|
65
|
-
|
|
66
|
-
// Windsurf
|
|
67
|
-
const windsurf = getWindsurfConfigPath();
|
|
68
|
-
if (windsurf) {
|
|
69
|
-
const k6 = tryFile(windsurf, 'mcpServers', 'spritecook', 'headers', 'Authorization');
|
|
70
|
-
if (k6) return k6;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// Codex (TOML - just search for the key string)
|
|
74
|
-
try {
|
|
75
|
-
const codexGlobal = join(home, '.codex', 'config.toml');
|
|
76
|
-
const codexProject = join(cwd, '.codex', 'config.toml');
|
|
77
|
-
for (const p of [codexProject, codexGlobal]) {
|
|
78
|
-
if (existsSync(p)) {
|
|
79
|
-
const text = readFileSync(p, 'utf-8');
|
|
80
|
-
const match = text.match(/bearer_token\s*=\s*"(sc_live_[^"]+)"/);
|
|
81
|
-
if (match) return match[1];
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
} catch { /* ignore */ }
|
|
85
|
-
|
|
86
|
-
return null;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// ── Environment detection ───────────────────────────────────────────────
|
|
90
|
-
|
|
91
|
-
function isRunningInCursor() {
|
|
92
|
-
const env = process.env;
|
|
93
|
-
if (env.CURSOR_CHANNEL) return true;
|
|
94
|
-
if (env.TERM_PROGRAM === 'cursor') return true;
|
|
95
|
-
const ipcHandle = env.VSCODE_GIT_IPC_HANDLE || env.VSCODE_IPC_HOOK_CLI || '';
|
|
96
|
-
if (ipcHandle.toLowerCase().includes('cursor')) return true;
|
|
97
|
-
return false;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function isRunningInAntigravity() {
|
|
101
|
-
const env = process.env;
|
|
102
|
-
if (env.ANTIGRAVITY_CHANNEL) return true;
|
|
103
|
-
if (env.TERM_PROGRAM === 'antigravity') return true;
|
|
104
|
-
const ipcHandle = env.VSCODE_GIT_IPC_HANDLE || env.VSCODE_IPC_HOOK_CLI || '';
|
|
105
|
-
if (ipcHandle.toLowerCase().includes('antigravity')) return true;
|
|
106
|
-
return false;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function isRunningInVSCode() {
|
|
110
|
-
const env = process.env;
|
|
111
|
-
if (env.TERM_PROGRAM === 'vscode') return true;
|
|
112
|
-
if (env.VSCODE_PID) return true;
|
|
113
|
-
if (env.VSCODE_GIT_IPC_HANDLE) return true;
|
|
114
|
-
if (env.VSCODE_IPC_HOOK_CLI) return true;
|
|
115
|
-
return false;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
function isRunningInWindsurf() {
|
|
119
|
-
const env = process.env;
|
|
120
|
-
if (env.TERM_PROGRAM === 'windsurf') return true;
|
|
121
|
-
const ipcHandle = env.VSCODE_GIT_IPC_HANDLE || env.VSCODE_IPC_HOOK_CLI || '';
|
|
122
|
-
if (ipcHandle.toLowerCase().includes('windsurf') || ipcHandle.toLowerCase().includes('codeium')) return true;
|
|
123
|
-
return false;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// ── Editor definitions ──────────────────────────────────────────────────
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* Each editor defines:
|
|
130
|
-
* - name: display name
|
|
131
|
-
* - detected: boolean heuristic
|
|
132
|
-
* - scopes: available MCP scopes ('project' and/or 'global')
|
|
133
|
-
* - defaultScope: recommended default scope
|
|
134
|
-
* - write(apiKey, scope): writer function
|
|
135
|
-
* - configPath(scope): return the path that will be written
|
|
136
|
-
* - skillDirs: { project, global } paths for skill installation
|
|
137
|
-
*/
|
|
138
|
-
export function detectEditors() {
|
|
139
|
-
const cwd = process.cwd();
|
|
140
|
-
const home = homedir();
|
|
141
|
-
const editors = [];
|
|
142
|
-
|
|
143
|
-
const inCursor = isRunningInCursor();
|
|
144
|
-
const inVSCode = isRunningInVSCode() && !inCursor && !isRunningInAntigravity() && !isRunningInWindsurf();
|
|
145
|
-
const inAntigravity = isRunningInAntigravity();
|
|
146
|
-
const inWindsurf = isRunningInWindsurf();
|
|
147
|
-
|
|
148
|
-
// ── Cursor ──────────────────────────────────────────────────
|
|
149
|
-
editors.push({
|
|
150
|
-
name: 'Cursor',
|
|
151
|
-
detected: existsSync(join(cwd, '.cursor')) || inCursor,
|
|
152
|
-
scopes: ['project', 'global'],
|
|
153
|
-
defaultScope: 'project',
|
|
154
|
-
configPath: (scope) => scope === 'global'
|
|
155
|
-
? join(home, '.cursor', 'mcp.json')
|
|
156
|
-
: join(cwd, '.cursor', 'mcp.json'),
|
|
157
|
-
write: (apiKey, scope) => writeCursorConfig(
|
|
158
|
-
scope === 'global' ? join(home, '.cursor') : join(cwd, '.cursor'),
|
|
159
|
-
apiKey
|
|
160
|
-
),
|
|
161
|
-
skillDirs: {
|
|
162
|
-
project: join(cwd, '.cursor', 'skills', 'spritecook'),
|
|
163
|
-
global: join(home, '.cursor', 'skills', 'spritecook'),
|
|
164
|
-
},
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
// ── VS Code ─────────────────────────────────────────────────
|
|
168
|
-
// Skills: project .github/skills/, global ~/.copilot/skills/
|
|
169
|
-
editors.push({
|
|
170
|
-
name: 'VS Code',
|
|
171
|
-
detected: existsSync(join(cwd, '.vscode')) || inVSCode,
|
|
172
|
-
scopes: ['project', 'global'],
|
|
173
|
-
defaultScope: 'project',
|
|
174
|
-
configPath: (scope) => scope === 'global'
|
|
175
|
-
? getVSCodeGlobalMcpPath()
|
|
176
|
-
: join(cwd, '.vscode', 'mcp.json'),
|
|
177
|
-
write: (apiKey, scope) => scope === 'global'
|
|
178
|
-
? writeVSCodeGlobalConfig(apiKey)
|
|
179
|
-
: writeVSCodeProjectConfig(join(cwd, '.vscode'), apiKey),
|
|
180
|
-
skillDirs: {
|
|
181
|
-
project: join(cwd, '.github', 'skills', 'spritecook'),
|
|
182
|
-
global: join(home, '.copilot', 'skills', 'spritecook'),
|
|
183
|
-
},
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
// ── Claude Desktop ──────────────────────────────────────────
|
|
187
|
-
const claudeDesktopPath = getClaudeDesktopConfigPath();
|
|
188
|
-
editors.push({
|
|
189
|
-
name: 'Claude Desktop',
|
|
190
|
-
detected: claudeDesktopPath ? existsSync(join(claudeDesktopPath, '..')) : false,
|
|
191
|
-
scopes: ['global'],
|
|
192
|
-
defaultScope: 'global',
|
|
193
|
-
configPath: () => claudeDesktopPath,
|
|
194
|
-
write: (apiKey) => writeClaudeDesktopConfig(claudeDesktopPath, apiKey),
|
|
195
|
-
skillDirs: {
|
|
196
|
-
project: join(cwd, '.claude', 'skills', 'spritecook'),
|
|
197
|
-
global: join(home, '.claude', 'skills', 'spritecook'),
|
|
198
|
-
},
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
// ── Claude Code ─────────────────────────────────────────────
|
|
202
|
-
// MCP servers are always stored in ~/.claude.json (not ~/.claude/settings.json).
|
|
203
|
-
// Detection: check both files, but always write MCP config to ~/.claude.json.
|
|
204
|
-
const claudeCodeMcpPath = join(home, '.claude.json');
|
|
205
|
-
const claudeCodeSettingsPath = join(home, '.claude', 'settings.json');
|
|
206
|
-
const claudeCodeDetected = existsSync(claudeCodeMcpPath) || existsSync(claudeCodeSettingsPath);
|
|
207
|
-
editors.push({
|
|
208
|
-
name: 'Claude Code',
|
|
209
|
-
detected: claudeCodeDetected,
|
|
210
|
-
scopes: ['global'],
|
|
211
|
-
defaultScope: 'global',
|
|
212
|
-
configPath: () => claudeCodeMcpPath,
|
|
213
|
-
write: (apiKey) => writeClaudeCodeConfig(claudeCodeMcpPath, apiKey),
|
|
214
|
-
skillDirs: {
|
|
215
|
-
project: join(cwd, '.claude', 'skills', 'spritecook'),
|
|
216
|
-
global: join(home, '.claude', 'skills', 'spritecook'),
|
|
217
|
-
},
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
// ── Antigravity (Google) ────────────────────────────────────
|
|
221
|
-
const antigravityPath = getAntigravityConfigPath();
|
|
222
|
-
editors.push({
|
|
223
|
-
name: 'Antigravity',
|
|
224
|
-
detected: inAntigravity || (antigravityPath ? existsSync(join(antigravityPath, '..')) : false),
|
|
225
|
-
scopes: ['global'],
|
|
226
|
-
defaultScope: 'global',
|
|
227
|
-
configPath: () => antigravityPath,
|
|
228
|
-
write: (apiKey) => writeAntigravityConfig(antigravityPath, apiKey),
|
|
229
|
-
skillDirs: {
|
|
230
|
-
project: join(cwd, '.agent', 'skills', 'spritecook'),
|
|
231
|
-
global: join(home, '.gemini', 'antigravity', 'skills', 'spritecook'),
|
|
232
|
-
},
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
// ── Windsurf (Codeium) ──────────────────────────────────────
|
|
236
|
-
const windsurfPath = getWindsurfConfigPath();
|
|
237
|
-
editors.push({
|
|
238
|
-
name: 'Windsurf',
|
|
239
|
-
detected: inWindsurf || (windsurfPath ? existsSync(join(windsurfPath, '..')) : false),
|
|
240
|
-
scopes: ['global'],
|
|
241
|
-
defaultScope: 'global',
|
|
242
|
-
configPath: () => windsurfPath,
|
|
243
|
-
write: (apiKey) => writeWindsurfConfig(windsurfPath, apiKey),
|
|
244
|
-
skillDirs: {
|
|
245
|
-
project: null, // Windsurf uses Cascade / AGENTS.md, no standard skills dir
|
|
246
|
-
global: null,
|
|
247
|
-
},
|
|
248
|
-
});
|
|
249
|
-
|
|
250
|
-
// ── Codex (OpenAI) ──────────────────────────────────────────
|
|
251
|
-
// Always write to global ~/.codex/config.toml regardless of scope choice.
|
|
252
|
-
// Project-level config requires Codex "trusted project" which is unreliable
|
|
253
|
-
// for new projects, and global keeps the API key out of git.
|
|
254
|
-
const codexGlobalPath = join(home, '.codex', 'config.toml');
|
|
255
|
-
const codexProjectPath = join(cwd, '.codex', 'config.toml');
|
|
256
|
-
editors.push({
|
|
257
|
-
name: 'Codex',
|
|
258
|
-
detected: existsSync(codexGlobalPath) || existsSync(codexProjectPath),
|
|
259
|
-
scopes: ['global'],
|
|
260
|
-
defaultScope: 'global',
|
|
261
|
-
configPath: () => codexGlobalPath,
|
|
262
|
-
write: (apiKey) => writeCodexConfig(codexGlobalPath, apiKey),
|
|
263
|
-
skillDirs: {
|
|
264
|
-
project: join(cwd, '.agents', 'skills', 'spritecook'),
|
|
265
|
-
global: join(home, '.agents', 'skills', 'spritecook'),
|
|
266
|
-
},
|
|
267
|
-
});
|
|
268
|
-
|
|
269
|
-
return editors;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
/**
|
|
273
|
-
* Write MCP config for all selected editors.
|
|
274
|
-
* Creates config directories if they don't exist.
|
|
275
|
-
* Returns the count of configs successfully written.
|
|
276
|
-
*/
|
|
277
|
-
export function writeConfigs(editors, apiKey) {
|
|
278
|
-
let written = 0;
|
|
279
|
-
|
|
280
|
-
for (const editor of editors) {
|
|
281
|
-
try {
|
|
282
|
-
const scope = editor._chosenScope || editor.defaultScope;
|
|
283
|
-
editor.write(apiKey, scope);
|
|
284
|
-
const path = typeof editor.configPath === 'function' ? editor.configPath(scope) : editor.configPath;
|
|
285
|
-
success(`${path}`);
|
|
286
|
-
written++;
|
|
287
|
-
} catch (err) {
|
|
288
|
-
warn(`Failed to write ${editor.name} config: ${err.message}`);
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
return written;
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
// ── Writer Functions ────────────────────────────────────────────────────
|
|
296
|
-
|
|
297
|
-
function writeCursorConfig(cursorDir, apiKey) {
|
|
298
|
-
const configPath = join(cursorDir, 'mcp.json');
|
|
299
|
-
const mcpUrl = getMcpUrl();
|
|
300
|
-
|
|
301
|
-
let config = {};
|
|
302
|
-
if (existsSync(configPath)) {
|
|
303
|
-
try { config = JSON.parse(readFileSync(configPath, 'utf-8')); } catch { /* start fresh */ }
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
if (!config.mcpServers) config.mcpServers = {};
|
|
307
|
-
config.mcpServers.spritecook = {
|
|
308
|
-
url: mcpUrl,
|
|
309
|
-
headers: { Authorization: `Bearer ${apiKey}` },
|
|
310
|
-
};
|
|
311
|
-
|
|
312
|
-
ensureDir(cursorDir);
|
|
313
|
-
writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
function writeVSCodeProjectConfig(vscodeDir, apiKey) {
|
|
317
|
-
// VS Code now uses .vscode/mcp.json with root key "servers"
|
|
318
|
-
const configPath = join(vscodeDir, 'mcp.json');
|
|
319
|
-
const mcpUrl = getMcpUrl();
|
|
320
|
-
|
|
321
|
-
let config = {};
|
|
322
|
-
if (existsSync(configPath)) {
|
|
323
|
-
try { config = JSON.parse(readFileSync(configPath, 'utf-8')); } catch { /* start fresh */ }
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
if (!config.servers) config.servers = {};
|
|
327
|
-
config.servers.spritecook = {
|
|
328
|
-
type: 'http',
|
|
329
|
-
url: mcpUrl,
|
|
330
|
-
headers: { Authorization: `Bearer ${apiKey}` },
|
|
331
|
-
};
|
|
332
|
-
|
|
333
|
-
ensureDir(vscodeDir);
|
|
334
|
-
writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
function writeVSCodeGlobalConfig(apiKey) {
|
|
338
|
-
const configPath = getVSCodeGlobalMcpPath();
|
|
339
|
-
const mcpUrl = getMcpUrl();
|
|
340
|
-
|
|
341
|
-
let config = {};
|
|
342
|
-
if (existsSync(configPath)) {
|
|
343
|
-
try { config = JSON.parse(readFileSync(configPath, 'utf-8')); } catch { /* start fresh */ }
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
if (!config.servers) config.servers = {};
|
|
347
|
-
config.servers.spritecook = {
|
|
348
|
-
type: 'http',
|
|
349
|
-
url: mcpUrl,
|
|
350
|
-
headers: { Authorization: `Bearer ${apiKey}` },
|
|
351
|
-
};
|
|
352
|
-
|
|
353
|
-
ensureDir(join(configPath, '..'));
|
|
354
|
-
writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
function writeClaudeDesktopConfig(configPath, apiKey) {
|
|
358
|
-
if (!configPath) { warn('Claude Desktop config path not found.'); return; }
|
|
359
|
-
const mcpUrl = getMcpUrl();
|
|
360
|
-
|
|
361
|
-
let config = {};
|
|
362
|
-
if (existsSync(configPath)) {
|
|
363
|
-
try { config = JSON.parse(readFileSync(configPath, 'utf-8')); } catch { /* start fresh */ }
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
if (!config.mcpServers) config.mcpServers = {};
|
|
367
|
-
config.mcpServers.spritecook = {
|
|
368
|
-
url: mcpUrl,
|
|
369
|
-
headers: { Authorization: `Bearer ${apiKey}` },
|
|
370
|
-
};
|
|
371
|
-
|
|
372
|
-
ensureDir(join(configPath, '..'));
|
|
373
|
-
writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
function writeClaudeCodeConfig(configPath, apiKey) {
|
|
377
|
-
const mcpUrl = getMcpUrl();
|
|
378
|
-
|
|
379
|
-
let config = {};
|
|
380
|
-
if (existsSync(configPath)) {
|
|
381
|
-
try { config = JSON.parse(readFileSync(configPath, 'utf-8')); } catch { /* start fresh */ }
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
// Claude Code requires "type": "http" for remote servers
|
|
385
|
-
// Config lives in ~/.claude.json per https://docs.anthropic.com/en/docs/claude-code/mcp
|
|
386
|
-
if (!config.mcpServers) config.mcpServers = {};
|
|
387
|
-
config.mcpServers.spritecook = {
|
|
388
|
-
type: 'http',
|
|
389
|
-
url: mcpUrl,
|
|
390
|
-
headers: { Authorization: `Bearer ${apiKey}` },
|
|
391
|
-
};
|
|
392
|
-
|
|
393
|
-
ensureDir(join(configPath, '..'));
|
|
394
|
-
writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
function writeAntigravityConfig(configPath, apiKey) {
|
|
398
|
-
if (!configPath) { warn('Antigravity config path not found.'); return; }
|
|
399
|
-
const mcpUrl = getMcpUrl();
|
|
400
|
-
|
|
401
|
-
let config = {};
|
|
402
|
-
if (existsSync(configPath)) {
|
|
403
|
-
try { config = JSON.parse(readFileSync(configPath, 'utf-8')); } catch { /* start fresh */ }
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
if (!config.mcpServers) config.mcpServers = {};
|
|
407
|
-
config.mcpServers.spritecook = {
|
|
408
|
-
serverUrl: mcpUrl,
|
|
409
|
-
headers: { Authorization: `Bearer ${apiKey}` },
|
|
410
|
-
};
|
|
411
|
-
|
|
412
|
-
ensureDir(join(configPath, '..'));
|
|
413
|
-
writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
function writeWindsurfConfig(configPath, apiKey) {
|
|
417
|
-
if (!configPath) { warn('Windsurf config path not found.'); return; }
|
|
418
|
-
const mcpUrl = getMcpUrl();
|
|
419
|
-
|
|
420
|
-
let config = {};
|
|
421
|
-
if (existsSync(configPath)) {
|
|
422
|
-
try { config = JSON.parse(readFileSync(configPath, 'utf-8')); } catch { /* start fresh */ }
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
if (!config.mcpServers) config.mcpServers = {};
|
|
426
|
-
config.mcpServers.spritecook = {
|
|
427
|
-
url: mcpUrl,
|
|
428
|
-
headers: { Authorization: `Bearer ${apiKey}` },
|
|
429
|
-
};
|
|
430
|
-
|
|
431
|
-
ensureDir(join(configPath, '..'));
|
|
432
|
-
writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
function writeCodexConfig(configPath, apiKey) {
|
|
436
|
-
const mcpUrl = getMcpUrl();
|
|
437
|
-
|
|
438
|
-
// Read existing or start fresh TOML
|
|
439
|
-
let content = '';
|
|
440
|
-
if (existsSync(configPath)) {
|
|
441
|
-
try { content = readFileSync(configPath, 'utf-8'); } catch { /* start fresh */ }
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
// Remove any existing [mcp_servers.spritecook] block (including sub-tables)
|
|
445
|
-
content = content.replace(/\[mcp_servers\.spritecook[^\]]*\][^\[]*/g, '');
|
|
446
|
-
|
|
447
|
-
// Codex uses Streamable HTTP format per https://developers.openai.com/codex/mcp/
|
|
448
|
-
// - url: required, the server address
|
|
449
|
-
// - http_headers: map of header names to static values (for auth)
|
|
450
|
-
const block = [
|
|
451
|
-
'',
|
|
452
|
-
'[mcp_servers.spritecook]',
|
|
453
|
-
`url = "${mcpUrl}"`,
|
|
454
|
-
'',
|
|
455
|
-
'[mcp_servers.spritecook.http_headers]',
|
|
456
|
-
`Authorization = "Bearer ${apiKey}"`,
|
|
457
|
-
'',
|
|
458
|
-
].join('\n');
|
|
459
|
-
|
|
460
|
-
content = content.trimEnd() + '\n' + block;
|
|
461
|
-
|
|
462
|
-
ensureDir(join(configPath, '..'));
|
|
463
|
-
writeFileSync(configPath, content, 'utf-8');
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
// ── Path Helpers ────────────────────────────────────────────────────────
|
|
467
|
-
|
|
468
|
-
function getAntigravityConfigPath() {
|
|
469
|
-
return join(homedir(), '.gemini', 'antigravity', 'mcp_config.json');
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
function getClaudeDesktopConfigPath() {
|
|
473
|
-
const platform = process.platform;
|
|
474
|
-
const home = homedir();
|
|
475
|
-
|
|
476
|
-
if (platform === 'darwin') {
|
|
477
|
-
return join(home, 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json');
|
|
478
|
-
}
|
|
479
|
-
if (platform === 'win32') {
|
|
480
|
-
const appData = process.env.APPDATA || join(home, 'AppData', 'Roaming');
|
|
481
|
-
return join(appData, 'Claude', 'claude_desktop_config.json');
|
|
482
|
-
}
|
|
483
|
-
if (platform === 'linux') {
|
|
484
|
-
return join(home, '.config', 'Claude', 'claude_desktop_config.json');
|
|
485
|
-
}
|
|
486
|
-
return null;
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
function getWindsurfConfigPath() {
|
|
490
|
-
const platform = process.platform;
|
|
491
|
-
const home = homedir();
|
|
492
|
-
|
|
493
|
-
if (platform === 'darwin') {
|
|
494
|
-
return join(home, '.codeium', 'windsurf', 'mcp_config.json');
|
|
495
|
-
}
|
|
496
|
-
if (platform === 'win32') {
|
|
497
|
-
const appData = process.env.APPDATA || join(home, 'AppData', 'Roaming');
|
|
498
|
-
return join(appData, 'Codeium', 'Windsurf', 'mcp_config.json');
|
|
499
|
-
}
|
|
500
|
-
if (platform === 'linux') {
|
|
501
|
-
// Try both common paths
|
|
502
|
-
const p1 = join(home, '.codeium', 'windsurf', 'mcp_config.json');
|
|
503
|
-
const p2 = join(home, '.config', 'Codeium', 'Windsurf', 'mcp_config.json');
|
|
504
|
-
if (existsSync(p2)) return p2;
|
|
505
|
-
return p1;
|
|
506
|
-
}
|
|
507
|
-
return null;
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
function getVSCodeGlobalMcpPath() {
|
|
511
|
-
const platform = process.platform;
|
|
512
|
-
const home = homedir();
|
|
513
|
-
|
|
514
|
-
if (platform === 'darwin') {
|
|
515
|
-
return join(home, 'Library', 'Application Support', 'Code', 'User', 'mcp.json');
|
|
516
|
-
}
|
|
517
|
-
if (platform === 'win32') {
|
|
518
|
-
const appData = process.env.APPDATA || join(home, 'AppData', 'Roaming');
|
|
519
|
-
return join(appData, 'Code', 'User', 'mcp.json');
|
|
520
|
-
}
|
|
521
|
-
if (platform === 'linux') {
|
|
522
|
-
return join(home, '.config', 'Code', 'User', 'mcp.json');
|
|
523
|
-
}
|
|
524
|
-
return join(home, '.vscode', 'mcp.json'); // fallback
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
function ensureDir(dir) {
|
|
528
|
-
if (!existsSync(dir)) {
|
|
529
|
-
mkdirSync(dir, { recursive: true });
|
|
530
|
-
}
|
|
531
|
-
}
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { success, warn, info } from './ui.mjs';
|
|
5
|
+
import { getMcpUrl } from './config.mjs';
|
|
6
|
+
|
|
7
|
+
// ── Read existing key ───────────────────────────────────────────────────
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Try to read an existing SpriteCook API key from any detected editor config.
|
|
11
|
+
* Returns the key string or null if none found.
|
|
12
|
+
*/
|
|
13
|
+
export function readExistingKey() {
|
|
14
|
+
const cwd = process.cwd();
|
|
15
|
+
const home = homedir();
|
|
16
|
+
|
|
17
|
+
const keyFrom = (config, ...path) => {
|
|
18
|
+
let obj = config;
|
|
19
|
+
for (const p of path) { obj = obj?.[p]; }
|
|
20
|
+
if (typeof obj === 'string' && obj.startsWith('Bearer sc_live_')) {
|
|
21
|
+
return obj.replace('Bearer ', '');
|
|
22
|
+
}
|
|
23
|
+
return null;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const tryFile = (filePath, ...keyPath) => {
|
|
27
|
+
try {
|
|
28
|
+
if (!existsSync(filePath)) return null;
|
|
29
|
+
const config = JSON.parse(readFileSync(filePath, 'utf-8'));
|
|
30
|
+
return keyFrom(config, ...keyPath);
|
|
31
|
+
} catch { return null; }
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// Cursor project
|
|
35
|
+
const k1 = tryFile(join(cwd, '.cursor', 'mcp.json'), 'mcpServers', 'spritecook', 'headers', 'Authorization');
|
|
36
|
+
if (k1) return k1;
|
|
37
|
+
// Cursor global
|
|
38
|
+
const k1g = tryFile(join(home, '.cursor', 'mcp.json'), 'mcpServers', 'spritecook', 'headers', 'Authorization');
|
|
39
|
+
if (k1g) return k1g;
|
|
40
|
+
|
|
41
|
+
// VS Code project (new mcp.json format)
|
|
42
|
+
const k2 = tryFile(join(cwd, '.vscode', 'mcp.json'), 'servers', 'spritecook', 'headers', 'Authorization');
|
|
43
|
+
if (k2) return k2;
|
|
44
|
+
// VS Code legacy (settings.json)
|
|
45
|
+
const k2b = tryFile(join(cwd, '.vscode', 'settings.json'), 'mcp', 'servers', 'spritecook', 'headers', 'Authorization');
|
|
46
|
+
if (k2b) return k2b;
|
|
47
|
+
|
|
48
|
+
// Claude Desktop
|
|
49
|
+
const claudeDesktop = getClaudeDesktopConfigPath();
|
|
50
|
+
if (claudeDesktop) {
|
|
51
|
+
const k3 = tryFile(claudeDesktop, 'mcpServers', 'spritecook', 'headers', 'Authorization');
|
|
52
|
+
if (k3) return k3;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Claude Code
|
|
56
|
+
const k4a = tryFile(join(home, '.claude', 'settings.json'), 'mcpServers', 'spritecook', 'headers', 'Authorization');
|
|
57
|
+
if (k4a) return k4a;
|
|
58
|
+
const k4b = tryFile(join(home, '.claude.json'), 'mcpServers', 'spritecook', 'headers', 'Authorization');
|
|
59
|
+
if (k4b) return k4b;
|
|
60
|
+
|
|
61
|
+
// Antigravity
|
|
62
|
+
const antigravity = getAntigravityConfigPath();
|
|
63
|
+
const k5 = tryFile(antigravity, 'mcpServers', 'spritecook', 'headers', 'Authorization');
|
|
64
|
+
if (k5) return k5;
|
|
65
|
+
|
|
66
|
+
// Windsurf
|
|
67
|
+
const windsurf = getWindsurfConfigPath();
|
|
68
|
+
if (windsurf) {
|
|
69
|
+
const k6 = tryFile(windsurf, 'mcpServers', 'spritecook', 'headers', 'Authorization');
|
|
70
|
+
if (k6) return k6;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Codex (TOML - just search for the key string)
|
|
74
|
+
try {
|
|
75
|
+
const codexGlobal = join(home, '.codex', 'config.toml');
|
|
76
|
+
const codexProject = join(cwd, '.codex', 'config.toml');
|
|
77
|
+
for (const p of [codexProject, codexGlobal]) {
|
|
78
|
+
if (existsSync(p)) {
|
|
79
|
+
const text = readFileSync(p, 'utf-8');
|
|
80
|
+
const match = text.match(/bearer_token\s*=\s*"(sc_live_[^"]+)"/);
|
|
81
|
+
if (match) return match[1];
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
} catch { /* ignore */ }
|
|
85
|
+
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ── Environment detection ───────────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
function isRunningInCursor() {
|
|
92
|
+
const env = process.env;
|
|
93
|
+
if (env.CURSOR_CHANNEL) return true;
|
|
94
|
+
if (env.TERM_PROGRAM === 'cursor') return true;
|
|
95
|
+
const ipcHandle = env.VSCODE_GIT_IPC_HANDLE || env.VSCODE_IPC_HOOK_CLI || '';
|
|
96
|
+
if (ipcHandle.toLowerCase().includes('cursor')) return true;
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function isRunningInAntigravity() {
|
|
101
|
+
const env = process.env;
|
|
102
|
+
if (env.ANTIGRAVITY_CHANNEL) return true;
|
|
103
|
+
if (env.TERM_PROGRAM === 'antigravity') return true;
|
|
104
|
+
const ipcHandle = env.VSCODE_GIT_IPC_HANDLE || env.VSCODE_IPC_HOOK_CLI || '';
|
|
105
|
+
if (ipcHandle.toLowerCase().includes('antigravity')) return true;
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function isRunningInVSCode() {
|
|
110
|
+
const env = process.env;
|
|
111
|
+
if (env.TERM_PROGRAM === 'vscode') return true;
|
|
112
|
+
if (env.VSCODE_PID) return true;
|
|
113
|
+
if (env.VSCODE_GIT_IPC_HANDLE) return true;
|
|
114
|
+
if (env.VSCODE_IPC_HOOK_CLI) return true;
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function isRunningInWindsurf() {
|
|
119
|
+
const env = process.env;
|
|
120
|
+
if (env.TERM_PROGRAM === 'windsurf') return true;
|
|
121
|
+
const ipcHandle = env.VSCODE_GIT_IPC_HANDLE || env.VSCODE_IPC_HOOK_CLI || '';
|
|
122
|
+
if (ipcHandle.toLowerCase().includes('windsurf') || ipcHandle.toLowerCase().includes('codeium')) return true;
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ── Editor definitions ──────────────────────────────────────────────────
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Each editor defines:
|
|
130
|
+
* - name: display name
|
|
131
|
+
* - detected: boolean heuristic
|
|
132
|
+
* - scopes: available MCP scopes ('project' and/or 'global')
|
|
133
|
+
* - defaultScope: recommended default scope
|
|
134
|
+
* - write(apiKey, scope): writer function
|
|
135
|
+
* - configPath(scope): return the path that will be written
|
|
136
|
+
* - skillDirs: { project, global } paths for skill installation
|
|
137
|
+
*/
|
|
138
|
+
export function detectEditors() {
|
|
139
|
+
const cwd = process.cwd();
|
|
140
|
+
const home = homedir();
|
|
141
|
+
const editors = [];
|
|
142
|
+
|
|
143
|
+
const inCursor = isRunningInCursor();
|
|
144
|
+
const inVSCode = isRunningInVSCode() && !inCursor && !isRunningInAntigravity() && !isRunningInWindsurf();
|
|
145
|
+
const inAntigravity = isRunningInAntigravity();
|
|
146
|
+
const inWindsurf = isRunningInWindsurf();
|
|
147
|
+
|
|
148
|
+
// ── Cursor ──────────────────────────────────────────────────
|
|
149
|
+
editors.push({
|
|
150
|
+
name: 'Cursor',
|
|
151
|
+
detected: existsSync(join(cwd, '.cursor')) || inCursor,
|
|
152
|
+
scopes: ['project', 'global'],
|
|
153
|
+
defaultScope: 'project',
|
|
154
|
+
configPath: (scope) => scope === 'global'
|
|
155
|
+
? join(home, '.cursor', 'mcp.json')
|
|
156
|
+
: join(cwd, '.cursor', 'mcp.json'),
|
|
157
|
+
write: (apiKey, scope) => writeCursorConfig(
|
|
158
|
+
scope === 'global' ? join(home, '.cursor') : join(cwd, '.cursor'),
|
|
159
|
+
apiKey
|
|
160
|
+
),
|
|
161
|
+
skillDirs: {
|
|
162
|
+
project: join(cwd, '.cursor', 'skills', 'spritecook'),
|
|
163
|
+
global: join(home, '.cursor', 'skills', 'spritecook'),
|
|
164
|
+
},
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// ── VS Code ─────────────────────────────────────────────────
|
|
168
|
+
// Skills: project .github/skills/, global ~/.copilot/skills/
|
|
169
|
+
editors.push({
|
|
170
|
+
name: 'VS Code',
|
|
171
|
+
detected: existsSync(join(cwd, '.vscode')) || inVSCode,
|
|
172
|
+
scopes: ['project', 'global'],
|
|
173
|
+
defaultScope: 'project',
|
|
174
|
+
configPath: (scope) => scope === 'global'
|
|
175
|
+
? getVSCodeGlobalMcpPath()
|
|
176
|
+
: join(cwd, '.vscode', 'mcp.json'),
|
|
177
|
+
write: (apiKey, scope) => scope === 'global'
|
|
178
|
+
? writeVSCodeGlobalConfig(apiKey)
|
|
179
|
+
: writeVSCodeProjectConfig(join(cwd, '.vscode'), apiKey),
|
|
180
|
+
skillDirs: {
|
|
181
|
+
project: join(cwd, '.github', 'skills', 'spritecook'),
|
|
182
|
+
global: join(home, '.copilot', 'skills', 'spritecook'),
|
|
183
|
+
},
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// ── Claude Desktop ──────────────────────────────────────────
|
|
187
|
+
const claudeDesktopPath = getClaudeDesktopConfigPath();
|
|
188
|
+
editors.push({
|
|
189
|
+
name: 'Claude Desktop',
|
|
190
|
+
detected: claudeDesktopPath ? existsSync(join(claudeDesktopPath, '..')) : false,
|
|
191
|
+
scopes: ['global'],
|
|
192
|
+
defaultScope: 'global',
|
|
193
|
+
configPath: () => claudeDesktopPath,
|
|
194
|
+
write: (apiKey) => writeClaudeDesktopConfig(claudeDesktopPath, apiKey),
|
|
195
|
+
skillDirs: {
|
|
196
|
+
project: join(cwd, '.claude', 'skills', 'spritecook'),
|
|
197
|
+
global: join(home, '.claude', 'skills', 'spritecook'),
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// ── Claude Code ─────────────────────────────────────────────
|
|
202
|
+
// MCP servers are always stored in ~/.claude.json (not ~/.claude/settings.json).
|
|
203
|
+
// Detection: check both files, but always write MCP config to ~/.claude.json.
|
|
204
|
+
const claudeCodeMcpPath = join(home, '.claude.json');
|
|
205
|
+
const claudeCodeSettingsPath = join(home, '.claude', 'settings.json');
|
|
206
|
+
const claudeCodeDetected = existsSync(claudeCodeMcpPath) || existsSync(claudeCodeSettingsPath);
|
|
207
|
+
editors.push({
|
|
208
|
+
name: 'Claude Code',
|
|
209
|
+
detected: claudeCodeDetected,
|
|
210
|
+
scopes: ['global'],
|
|
211
|
+
defaultScope: 'global',
|
|
212
|
+
configPath: () => claudeCodeMcpPath,
|
|
213
|
+
write: (apiKey) => writeClaudeCodeConfig(claudeCodeMcpPath, apiKey),
|
|
214
|
+
skillDirs: {
|
|
215
|
+
project: join(cwd, '.claude', 'skills', 'spritecook'),
|
|
216
|
+
global: join(home, '.claude', 'skills', 'spritecook'),
|
|
217
|
+
},
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
// ── Antigravity (Google) ────────────────────────────────────
|
|
221
|
+
const antigravityPath = getAntigravityConfigPath();
|
|
222
|
+
editors.push({
|
|
223
|
+
name: 'Antigravity',
|
|
224
|
+
detected: inAntigravity || (antigravityPath ? existsSync(join(antigravityPath, '..')) : false),
|
|
225
|
+
scopes: ['global'],
|
|
226
|
+
defaultScope: 'global',
|
|
227
|
+
configPath: () => antigravityPath,
|
|
228
|
+
write: (apiKey) => writeAntigravityConfig(antigravityPath, apiKey),
|
|
229
|
+
skillDirs: {
|
|
230
|
+
project: join(cwd, '.agent', 'skills', 'spritecook'),
|
|
231
|
+
global: join(home, '.gemini', 'antigravity', 'skills', 'spritecook'),
|
|
232
|
+
},
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
// ── Windsurf (Codeium) ──────────────────────────────────────
|
|
236
|
+
const windsurfPath = getWindsurfConfigPath();
|
|
237
|
+
editors.push({
|
|
238
|
+
name: 'Windsurf',
|
|
239
|
+
detected: inWindsurf || (windsurfPath ? existsSync(join(windsurfPath, '..')) : false),
|
|
240
|
+
scopes: ['global'],
|
|
241
|
+
defaultScope: 'global',
|
|
242
|
+
configPath: () => windsurfPath,
|
|
243
|
+
write: (apiKey) => writeWindsurfConfig(windsurfPath, apiKey),
|
|
244
|
+
skillDirs: {
|
|
245
|
+
project: null, // Windsurf uses Cascade / AGENTS.md, no standard skills dir
|
|
246
|
+
global: null,
|
|
247
|
+
},
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
// ── Codex (OpenAI) ──────────────────────────────────────────
|
|
251
|
+
// Always write to global ~/.codex/config.toml regardless of scope choice.
|
|
252
|
+
// Project-level config requires Codex "trusted project" which is unreliable
|
|
253
|
+
// for new projects, and global keeps the API key out of git.
|
|
254
|
+
const codexGlobalPath = join(home, '.codex', 'config.toml');
|
|
255
|
+
const codexProjectPath = join(cwd, '.codex', 'config.toml');
|
|
256
|
+
editors.push({
|
|
257
|
+
name: 'Codex',
|
|
258
|
+
detected: existsSync(codexGlobalPath) || existsSync(codexProjectPath),
|
|
259
|
+
scopes: ['global'],
|
|
260
|
+
defaultScope: 'global',
|
|
261
|
+
configPath: () => codexGlobalPath,
|
|
262
|
+
write: (apiKey) => writeCodexConfig(codexGlobalPath, apiKey),
|
|
263
|
+
skillDirs: {
|
|
264
|
+
project: join(cwd, '.agents', 'skills', 'spritecook'),
|
|
265
|
+
global: join(home, '.agents', 'skills', 'spritecook'),
|
|
266
|
+
},
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
return editors;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Write MCP config for all selected editors.
|
|
274
|
+
* Creates config directories if they don't exist.
|
|
275
|
+
* Returns the count of configs successfully written.
|
|
276
|
+
*/
|
|
277
|
+
export function writeConfigs(editors, apiKey) {
|
|
278
|
+
let written = 0;
|
|
279
|
+
|
|
280
|
+
for (const editor of editors) {
|
|
281
|
+
try {
|
|
282
|
+
const scope = editor._chosenScope || editor.defaultScope;
|
|
283
|
+
editor.write(apiKey, scope);
|
|
284
|
+
const path = typeof editor.configPath === 'function' ? editor.configPath(scope) : editor.configPath;
|
|
285
|
+
success(`${path}`);
|
|
286
|
+
written++;
|
|
287
|
+
} catch (err) {
|
|
288
|
+
warn(`Failed to write ${editor.name} config: ${err.message}`);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return written;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// ── Writer Functions ────────────────────────────────────────────────────
|
|
296
|
+
|
|
297
|
+
function writeCursorConfig(cursorDir, apiKey) {
|
|
298
|
+
const configPath = join(cursorDir, 'mcp.json');
|
|
299
|
+
const mcpUrl = getMcpUrl();
|
|
300
|
+
|
|
301
|
+
let config = {};
|
|
302
|
+
if (existsSync(configPath)) {
|
|
303
|
+
try { config = JSON.parse(readFileSync(configPath, 'utf-8')); } catch { /* start fresh */ }
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (!config.mcpServers) config.mcpServers = {};
|
|
307
|
+
config.mcpServers.spritecook = {
|
|
308
|
+
url: mcpUrl,
|
|
309
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
ensureDir(cursorDir);
|
|
313
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function writeVSCodeProjectConfig(vscodeDir, apiKey) {
|
|
317
|
+
// VS Code now uses .vscode/mcp.json with root key "servers"
|
|
318
|
+
const configPath = join(vscodeDir, 'mcp.json');
|
|
319
|
+
const mcpUrl = getMcpUrl();
|
|
320
|
+
|
|
321
|
+
let config = {};
|
|
322
|
+
if (existsSync(configPath)) {
|
|
323
|
+
try { config = JSON.parse(readFileSync(configPath, 'utf-8')); } catch { /* start fresh */ }
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (!config.servers) config.servers = {};
|
|
327
|
+
config.servers.spritecook = {
|
|
328
|
+
type: 'http',
|
|
329
|
+
url: mcpUrl,
|
|
330
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
ensureDir(vscodeDir);
|
|
334
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function writeVSCodeGlobalConfig(apiKey) {
|
|
338
|
+
const configPath = getVSCodeGlobalMcpPath();
|
|
339
|
+
const mcpUrl = getMcpUrl();
|
|
340
|
+
|
|
341
|
+
let config = {};
|
|
342
|
+
if (existsSync(configPath)) {
|
|
343
|
+
try { config = JSON.parse(readFileSync(configPath, 'utf-8')); } catch { /* start fresh */ }
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (!config.servers) config.servers = {};
|
|
347
|
+
config.servers.spritecook = {
|
|
348
|
+
type: 'http',
|
|
349
|
+
url: mcpUrl,
|
|
350
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
ensureDir(join(configPath, '..'));
|
|
354
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function writeClaudeDesktopConfig(configPath, apiKey) {
|
|
358
|
+
if (!configPath) { warn('Claude Desktop config path not found.'); return; }
|
|
359
|
+
const mcpUrl = getMcpUrl();
|
|
360
|
+
|
|
361
|
+
let config = {};
|
|
362
|
+
if (existsSync(configPath)) {
|
|
363
|
+
try { config = JSON.parse(readFileSync(configPath, 'utf-8')); } catch { /* start fresh */ }
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if (!config.mcpServers) config.mcpServers = {};
|
|
367
|
+
config.mcpServers.spritecook = {
|
|
368
|
+
url: mcpUrl,
|
|
369
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
ensureDir(join(configPath, '..'));
|
|
373
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function writeClaudeCodeConfig(configPath, apiKey) {
|
|
377
|
+
const mcpUrl = getMcpUrl();
|
|
378
|
+
|
|
379
|
+
let config = {};
|
|
380
|
+
if (existsSync(configPath)) {
|
|
381
|
+
try { config = JSON.parse(readFileSync(configPath, 'utf-8')); } catch { /* start fresh */ }
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Claude Code requires "type": "http" for remote servers
|
|
385
|
+
// Config lives in ~/.claude.json per https://docs.anthropic.com/en/docs/claude-code/mcp
|
|
386
|
+
if (!config.mcpServers) config.mcpServers = {};
|
|
387
|
+
config.mcpServers.spritecook = {
|
|
388
|
+
type: 'http',
|
|
389
|
+
url: mcpUrl,
|
|
390
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
ensureDir(join(configPath, '..'));
|
|
394
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function writeAntigravityConfig(configPath, apiKey) {
|
|
398
|
+
if (!configPath) { warn('Antigravity config path not found.'); return; }
|
|
399
|
+
const mcpUrl = getMcpUrl();
|
|
400
|
+
|
|
401
|
+
let config = {};
|
|
402
|
+
if (existsSync(configPath)) {
|
|
403
|
+
try { config = JSON.parse(readFileSync(configPath, 'utf-8')); } catch { /* start fresh */ }
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
if (!config.mcpServers) config.mcpServers = {};
|
|
407
|
+
config.mcpServers.spritecook = {
|
|
408
|
+
serverUrl: mcpUrl,
|
|
409
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
ensureDir(join(configPath, '..'));
|
|
413
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function writeWindsurfConfig(configPath, apiKey) {
|
|
417
|
+
if (!configPath) { warn('Windsurf config path not found.'); return; }
|
|
418
|
+
const mcpUrl = getMcpUrl();
|
|
419
|
+
|
|
420
|
+
let config = {};
|
|
421
|
+
if (existsSync(configPath)) {
|
|
422
|
+
try { config = JSON.parse(readFileSync(configPath, 'utf-8')); } catch { /* start fresh */ }
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
if (!config.mcpServers) config.mcpServers = {};
|
|
426
|
+
config.mcpServers.spritecook = {
|
|
427
|
+
url: mcpUrl,
|
|
428
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
429
|
+
};
|
|
430
|
+
|
|
431
|
+
ensureDir(join(configPath, '..'));
|
|
432
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function writeCodexConfig(configPath, apiKey) {
|
|
436
|
+
const mcpUrl = getMcpUrl();
|
|
437
|
+
|
|
438
|
+
// Read existing or start fresh TOML
|
|
439
|
+
let content = '';
|
|
440
|
+
if (existsSync(configPath)) {
|
|
441
|
+
try { content = readFileSync(configPath, 'utf-8'); } catch { /* start fresh */ }
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Remove any existing [mcp_servers.spritecook] block (including sub-tables)
|
|
445
|
+
content = content.replace(/\[mcp_servers\.spritecook[^\]]*\][^\[]*/g, '');
|
|
446
|
+
|
|
447
|
+
// Codex uses Streamable HTTP format per https://developers.openai.com/codex/mcp/
|
|
448
|
+
// - url: required, the server address
|
|
449
|
+
// - http_headers: map of header names to static values (for auth)
|
|
450
|
+
const block = [
|
|
451
|
+
'',
|
|
452
|
+
'[mcp_servers.spritecook]',
|
|
453
|
+
`url = "${mcpUrl}"`,
|
|
454
|
+
'',
|
|
455
|
+
'[mcp_servers.spritecook.http_headers]',
|
|
456
|
+
`Authorization = "Bearer ${apiKey}"`,
|
|
457
|
+
'',
|
|
458
|
+
].join('\n');
|
|
459
|
+
|
|
460
|
+
content = content.trimEnd() + '\n' + block;
|
|
461
|
+
|
|
462
|
+
ensureDir(join(configPath, '..'));
|
|
463
|
+
writeFileSync(configPath, content, 'utf-8');
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// ── Path Helpers ────────────────────────────────────────────────────────
|
|
467
|
+
|
|
468
|
+
function getAntigravityConfigPath() {
|
|
469
|
+
return join(homedir(), '.gemini', 'antigravity', 'mcp_config.json');
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function getClaudeDesktopConfigPath() {
|
|
473
|
+
const platform = process.platform;
|
|
474
|
+
const home = homedir();
|
|
475
|
+
|
|
476
|
+
if (platform === 'darwin') {
|
|
477
|
+
return join(home, 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json');
|
|
478
|
+
}
|
|
479
|
+
if (platform === 'win32') {
|
|
480
|
+
const appData = process.env.APPDATA || join(home, 'AppData', 'Roaming');
|
|
481
|
+
return join(appData, 'Claude', 'claude_desktop_config.json');
|
|
482
|
+
}
|
|
483
|
+
if (platform === 'linux') {
|
|
484
|
+
return join(home, '.config', 'Claude', 'claude_desktop_config.json');
|
|
485
|
+
}
|
|
486
|
+
return null;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function getWindsurfConfigPath() {
|
|
490
|
+
const platform = process.platform;
|
|
491
|
+
const home = homedir();
|
|
492
|
+
|
|
493
|
+
if (platform === 'darwin') {
|
|
494
|
+
return join(home, '.codeium', 'windsurf', 'mcp_config.json');
|
|
495
|
+
}
|
|
496
|
+
if (platform === 'win32') {
|
|
497
|
+
const appData = process.env.APPDATA || join(home, 'AppData', 'Roaming');
|
|
498
|
+
return join(appData, 'Codeium', 'Windsurf', 'mcp_config.json');
|
|
499
|
+
}
|
|
500
|
+
if (platform === 'linux') {
|
|
501
|
+
// Try both common paths
|
|
502
|
+
const p1 = join(home, '.codeium', 'windsurf', 'mcp_config.json');
|
|
503
|
+
const p2 = join(home, '.config', 'Codeium', 'Windsurf', 'mcp_config.json');
|
|
504
|
+
if (existsSync(p2)) return p2;
|
|
505
|
+
return p1;
|
|
506
|
+
}
|
|
507
|
+
return null;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function getVSCodeGlobalMcpPath() {
|
|
511
|
+
const platform = process.platform;
|
|
512
|
+
const home = homedir();
|
|
513
|
+
|
|
514
|
+
if (platform === 'darwin') {
|
|
515
|
+
return join(home, 'Library', 'Application Support', 'Code', 'User', 'mcp.json');
|
|
516
|
+
}
|
|
517
|
+
if (platform === 'win32') {
|
|
518
|
+
const appData = process.env.APPDATA || join(home, 'AppData', 'Roaming');
|
|
519
|
+
return join(appData, 'Code', 'User', 'mcp.json');
|
|
520
|
+
}
|
|
521
|
+
if (platform === 'linux') {
|
|
522
|
+
return join(home, '.config', 'Code', 'User', 'mcp.json');
|
|
523
|
+
}
|
|
524
|
+
return join(home, '.vscode', 'mcp.json'); // fallback
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function ensureDir(dir) {
|
|
528
|
+
if (!existsSync(dir)) {
|
|
529
|
+
mkdirSync(dir, { recursive: true });
|
|
530
|
+
}
|
|
531
|
+
}
|