spritecook-mcp 0.2.10 → 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/src/editors.mjs CHANGED
@@ -1,527 +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
- const claudeCodePath2 = join(home, '.claude', 'settings.json');
203
- const claudeCodePath1 = join(home, '.claude.json');
204
- const claudeCodeDetected = existsSync(claudeCodePath1) || existsSync(claudeCodePath2);
205
- const claudeCodeConfigPath = existsSync(claudeCodePath2) ? claudeCodePath2 : claudeCodePath1;
206
- editors.push({
207
- name: 'Claude Code',
208
- detected: claudeCodeDetected,
209
- scopes: ['global'],
210
- defaultScope: 'global',
211
- configPath: () => claudeCodeConfigPath,
212
- write: (apiKey) => writeClaudeCodeConfig(claudeCodeConfigPath, apiKey),
213
- skillDirs: {
214
- project: join(cwd, '.claude', 'skills', 'spritecook'),
215
- global: join(home, '.claude', 'skills', 'spritecook'),
216
- },
217
- });
218
-
219
- // ── Antigravity (Google) ────────────────────────────────────
220
- const antigravityPath = getAntigravityConfigPath();
221
- editors.push({
222
- name: 'Antigravity',
223
- detected: inAntigravity || (antigravityPath ? existsSync(join(antigravityPath, '..')) : false),
224
- scopes: ['global'],
225
- defaultScope: 'global',
226
- configPath: () => antigravityPath,
227
- write: (apiKey) => writeAntigravityConfig(antigravityPath, apiKey),
228
- skillDirs: {
229
- project: join(cwd, '.agent', 'skills', 'spritecook'),
230
- global: join(home, '.gemini', 'antigravity', 'skills', 'spritecook'),
231
- },
232
- });
233
-
234
- // ── Windsurf (Codeium) ──────────────────────────────────────
235
- const windsurfPath = getWindsurfConfigPath();
236
- editors.push({
237
- name: 'Windsurf',
238
- detected: inWindsurf || (windsurfPath ? existsSync(join(windsurfPath, '..')) : false),
239
- scopes: ['global'],
240
- defaultScope: 'global',
241
- configPath: () => windsurfPath,
242
- write: (apiKey) => writeWindsurfConfig(windsurfPath, apiKey),
243
- skillDirs: {
244
- project: null, // Windsurf uses Cascade / AGENTS.md, no standard skills dir
245
- global: null,
246
- },
247
- });
248
-
249
- // ── Codex (OpenAI) ──────────────────────────────────────────
250
- // Always write to global ~/.codex/config.toml regardless of scope choice.
251
- // Project-level config requires Codex "trusted project" which is unreliable
252
- // for new projects, and global keeps the API key out of git.
253
- const codexGlobalPath = join(home, '.codex', 'config.toml');
254
- const codexProjectPath = join(cwd, '.codex', 'config.toml');
255
- editors.push({
256
- name: 'Codex',
257
- detected: existsSync(codexGlobalPath) || existsSync(codexProjectPath),
258
- scopes: ['global'],
259
- defaultScope: 'global',
260
- configPath: () => codexGlobalPath,
261
- write: (apiKey) => writeCodexConfig(codexGlobalPath, apiKey),
262
- skillDirs: {
263
- project: join(cwd, '.agents', 'skills', 'spritecook'),
264
- global: join(home, '.agents', 'skills', 'spritecook'),
265
- },
266
- });
267
-
268
- return editors;
269
- }
270
-
271
- /**
272
- * Write MCP config for all selected editors.
273
- * Creates config directories if they don't exist.
274
- * Returns the count of configs successfully written.
275
- */
276
- export function writeConfigs(editors, apiKey) {
277
- let written = 0;
278
-
279
- for (const editor of editors) {
280
- try {
281
- const scope = editor._chosenScope || editor.defaultScope;
282
- editor.write(apiKey, scope);
283
- const path = typeof editor.configPath === 'function' ? editor.configPath(scope) : editor.configPath;
284
- success(`${path}`);
285
- written++;
286
- } catch (err) {
287
- warn(`Failed to write ${editor.name} config: ${err.message}`);
288
- }
289
- }
290
-
291
- return written;
292
- }
293
-
294
- // ── Writer Functions ────────────────────────────────────────────────────
295
-
296
- function writeCursorConfig(cursorDir, apiKey) {
297
- const configPath = join(cursorDir, 'mcp.json');
298
- const mcpUrl = getMcpUrl();
299
-
300
- let config = {};
301
- if (existsSync(configPath)) {
302
- try { config = JSON.parse(readFileSync(configPath, 'utf-8')); } catch { /* start fresh */ }
303
- }
304
-
305
- if (!config.mcpServers) config.mcpServers = {};
306
- config.mcpServers.spritecook = {
307
- url: mcpUrl,
308
- headers: { Authorization: `Bearer ${apiKey}` },
309
- };
310
-
311
- ensureDir(cursorDir);
312
- writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
313
- }
314
-
315
- function writeVSCodeProjectConfig(vscodeDir, apiKey) {
316
- // VS Code now uses .vscode/mcp.json with root key "servers"
317
- const configPath = join(vscodeDir, 'mcp.json');
318
- const mcpUrl = getMcpUrl();
319
-
320
- let config = {};
321
- if (existsSync(configPath)) {
322
- try { config = JSON.parse(readFileSync(configPath, 'utf-8')); } catch { /* start fresh */ }
323
- }
324
-
325
- if (!config.servers) config.servers = {};
326
- config.servers.spritecook = {
327
- type: 'http',
328
- url: mcpUrl,
329
- headers: { Authorization: `Bearer ${apiKey}` },
330
- };
331
-
332
- ensureDir(vscodeDir);
333
- writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
334
- }
335
-
336
- function writeVSCodeGlobalConfig(apiKey) {
337
- const configPath = getVSCodeGlobalMcpPath();
338
- const mcpUrl = getMcpUrl();
339
-
340
- let config = {};
341
- if (existsSync(configPath)) {
342
- try { config = JSON.parse(readFileSync(configPath, 'utf-8')); } catch { /* start fresh */ }
343
- }
344
-
345
- if (!config.servers) config.servers = {};
346
- config.servers.spritecook = {
347
- type: 'http',
348
- url: mcpUrl,
349
- headers: { Authorization: `Bearer ${apiKey}` },
350
- };
351
-
352
- ensureDir(join(configPath, '..'));
353
- writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
354
- }
355
-
356
- function writeClaudeDesktopConfig(configPath, apiKey) {
357
- if (!configPath) { warn('Claude Desktop config path not found.'); return; }
358
- const mcpUrl = getMcpUrl();
359
-
360
- let config = {};
361
- if (existsSync(configPath)) {
362
- try { config = JSON.parse(readFileSync(configPath, 'utf-8')); } catch { /* start fresh */ }
363
- }
364
-
365
- if (!config.mcpServers) config.mcpServers = {};
366
- config.mcpServers.spritecook = {
367
- url: mcpUrl,
368
- headers: { Authorization: `Bearer ${apiKey}` },
369
- };
370
-
371
- ensureDir(join(configPath, '..'));
372
- writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
373
- }
374
-
375
- function writeClaudeCodeConfig(configPath, apiKey) {
376
- const mcpUrl = getMcpUrl();
377
-
378
- let config = {};
379
- if (existsSync(configPath)) {
380
- try { config = JSON.parse(readFileSync(configPath, 'utf-8')); } catch { /* start fresh */ }
381
- }
382
-
383
- if (!config.mcpServers) config.mcpServers = {};
384
- config.mcpServers.spritecook = {
385
- url: mcpUrl,
386
- headers: { Authorization: `Bearer ${apiKey}` },
387
- };
388
-
389
- ensureDir(join(configPath, '..'));
390
- writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
391
- }
392
-
393
- function writeAntigravityConfig(configPath, apiKey) {
394
- if (!configPath) { warn('Antigravity config path not found.'); return; }
395
- const mcpUrl = getMcpUrl();
396
-
397
- let config = {};
398
- if (existsSync(configPath)) {
399
- try { config = JSON.parse(readFileSync(configPath, 'utf-8')); } catch { /* start fresh */ }
400
- }
401
-
402
- if (!config.mcpServers) config.mcpServers = {};
403
- config.mcpServers.spritecook = {
404
- serverUrl: mcpUrl,
405
- headers: { Authorization: `Bearer ${apiKey}` },
406
- };
407
-
408
- ensureDir(join(configPath, '..'));
409
- writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
410
- }
411
-
412
- function writeWindsurfConfig(configPath, apiKey) {
413
- if (!configPath) { warn('Windsurf config path not found.'); return; }
414
- const mcpUrl = getMcpUrl();
415
-
416
- let config = {};
417
- if (existsSync(configPath)) {
418
- try { config = JSON.parse(readFileSync(configPath, 'utf-8')); } catch { /* start fresh */ }
419
- }
420
-
421
- if (!config.mcpServers) config.mcpServers = {};
422
- config.mcpServers.spritecook = {
423
- url: mcpUrl,
424
- headers: { Authorization: `Bearer ${apiKey}` },
425
- };
426
-
427
- ensureDir(join(configPath, '..'));
428
- writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
429
- }
430
-
431
- function writeCodexConfig(configPath, apiKey) {
432
- const mcpUrl = getMcpUrl();
433
-
434
- // Read existing or start fresh TOML
435
- let content = '';
436
- if (existsSync(configPath)) {
437
- try { content = readFileSync(configPath, 'utf-8'); } catch { /* start fresh */ }
438
- }
439
-
440
- // Remove any existing [mcp_servers.spritecook] block (including sub-tables)
441
- content = content.replace(/\[mcp_servers\.spritecook[^\]]*\][^\[]*/g, '');
442
-
443
- // Codex uses Streamable HTTP format per https://developers.openai.com/codex/mcp/
444
- // - url: required, the server address
445
- // - http_headers: map of header names to static values (for auth)
446
- const block = [
447
- '',
448
- '[mcp_servers.spritecook]',
449
- `url = "${mcpUrl}"`,
450
- '',
451
- '[mcp_servers.spritecook.http_headers]',
452
- `Authorization = "Bearer ${apiKey}"`,
453
- '',
454
- ].join('\n');
455
-
456
- content = content.trimEnd() + '\n' + block;
457
-
458
- ensureDir(join(configPath, '..'));
459
- writeFileSync(configPath, content, 'utf-8');
460
- }
461
-
462
- // ── Path Helpers ────────────────────────────────────────────────────────
463
-
464
- function getAntigravityConfigPath() {
465
- return join(homedir(), '.gemini', 'antigravity', 'mcp_config.json');
466
- }
467
-
468
- function getClaudeDesktopConfigPath() {
469
- const platform = process.platform;
470
- const home = homedir();
471
-
472
- if (platform === 'darwin') {
473
- return join(home, 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json');
474
- }
475
- if (platform === 'win32') {
476
- const appData = process.env.APPDATA || join(home, 'AppData', 'Roaming');
477
- return join(appData, 'Claude', 'claude_desktop_config.json');
478
- }
479
- if (platform === 'linux') {
480
- return join(home, '.config', 'Claude', 'claude_desktop_config.json');
481
- }
482
- return null;
483
- }
484
-
485
- function getWindsurfConfigPath() {
486
- const platform = process.platform;
487
- const home = homedir();
488
-
489
- if (platform === 'darwin') {
490
- return join(home, '.codeium', 'windsurf', 'mcp_config.json');
491
- }
492
- if (platform === 'win32') {
493
- const appData = process.env.APPDATA || join(home, 'AppData', 'Roaming');
494
- return join(appData, 'Codeium', 'Windsurf', 'mcp_config.json');
495
- }
496
- if (platform === 'linux') {
497
- // Try both common paths
498
- const p1 = join(home, '.codeium', 'windsurf', 'mcp_config.json');
499
- const p2 = join(home, '.config', 'Codeium', 'Windsurf', 'mcp_config.json');
500
- if (existsSync(p2)) return p2;
501
- return p1;
502
- }
503
- return null;
504
- }
505
-
506
- function getVSCodeGlobalMcpPath() {
507
- const platform = process.platform;
508
- const home = homedir();
509
-
510
- if (platform === 'darwin') {
511
- return join(home, 'Library', 'Application Support', 'Code', 'User', 'mcp.json');
512
- }
513
- if (platform === 'win32') {
514
- const appData = process.env.APPDATA || join(home, 'AppData', 'Roaming');
515
- return join(appData, 'Code', 'User', 'mcp.json');
516
- }
517
- if (platform === 'linux') {
518
- return join(home, '.config', 'Code', 'User', 'mcp.json');
519
- }
520
- return join(home, '.vscode', 'mcp.json'); // fallback
521
- }
522
-
523
- function ensureDir(dir) {
524
- if (!existsSync(dir)) {
525
- mkdirSync(dir, { recursive: true });
526
- }
527
- }
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
+ }