spritecook-mcp 0.2.4 → 0.2.6

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,7 @@
1
1
  {
2
2
  "name": "spritecook-mcp",
3
- "version": "0.2.4",
3
+ "version": "0.2.6",
4
+ "mcpName": "ai.spritecook/generate",
4
5
  "description": "SpriteCook MCP Server - Connect your AI agent (Cursor, VS Code, Claude) to SpriteCook for pixel art and game asset generation.",
5
6
  "keywords": [
6
7
  "spritecook",
package/src/editors.mjs CHANGED
@@ -4,6 +4,8 @@ import { homedir } from 'node:os';
4
4
  import { success, warn, info } from './ui.mjs';
5
5
  import { getMcpUrl } from './config.mjs';
6
6
 
7
+ // ── Read existing key ───────────────────────────────────────────────────
8
+
7
9
  /**
8
10
  * Try to read an existing SpriteCook API key from any detected editor config.
9
11
  * Returns the key string or null if none found.
@@ -12,64 +14,71 @@ export function readExistingKey() {
12
14
  const cwd = process.cwd();
13
15
  const home = homedir();
14
16
 
15
- // Check Cursor .cursor/mcp.json
16
- try {
17
- const cursorConfig = join(cwd, '.cursor', 'mcp.json');
18
- if (existsSync(cursorConfig)) {
19
- const config = JSON.parse(readFileSync(cursorConfig, 'utf-8'));
20
- const auth = config?.mcpServers?.spritecook?.headers?.Authorization;
21
- if (auth && auth.startsWith('Bearer sc_live_')) {
22
- return auth.replace('Bearer ', '');
23
- }
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 ', '');
24
22
  }
25
- } catch { /* ignore */ }
23
+ return null;
24
+ };
26
25
 
27
- // Check VS Code .vscode/settings.json
28
- try {
29
- const vscodeConfig = join(cwd, '.vscode', 'settings.json');
30
- if (existsSync(vscodeConfig)) {
31
- const config = JSON.parse(readFileSync(vscodeConfig, 'utf-8'));
32
- const auth = config?.mcp?.servers?.spritecook?.headers?.Authorization;
33
- if (auth && auth.startsWith('Bearer sc_live_')) {
34
- return auth.replace('Bearer ', '');
35
- }
36
- }
37
- } catch { /* ignore */ }
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
+ };
38
33
 
39
- // Check Claude Desktop
40
- try {
41
- const claudePath = getClaudeDesktopConfigPath();
42
- if (claudePath && existsSync(claudePath)) {
43
- const config = JSON.parse(readFileSync(claudePath, 'utf-8'));
44
- const auth = config?.mcpServers?.spritecook?.headers?.Authorization;
45
- if (auth && auth.startsWith('Bearer sc_live_')) {
46
- return auth.replace('Bearer ', '');
47
- }
48
- }
49
- } catch { /* ignore */ }
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
+ }
50
54
 
51
- // Check Claude Code
52
- try {
53
- const claudeCode1 = join(home, '.claude.json');
54
- const claudeCode2 = join(home, '.claude', 'settings.json');
55
- const path = existsSync(claudeCode2) ? claudeCode2 : claudeCode1;
56
- if (existsSync(path)) {
57
- const config = JSON.parse(readFileSync(path, 'utf-8'));
58
- const auth = config?.mcpServers?.spritecook?.headers?.Authorization;
59
- if (auth && auth.startsWith('Bearer sc_live_')) {
60
- return auth.replace('Bearer ', '');
61
- }
62
- }
63
- } catch { /* ignore */ }
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
+ }
64
72
 
65
- // Check Antigravity
73
+ // Codex (TOML - just search for the key string)
66
74
  try {
67
- const antigravityPath = getAntigravityConfigPath();
68
- if (antigravityPath && existsSync(antigravityPath)) {
69
- const config = JSON.parse(readFileSync(antigravityPath, 'utf-8'));
70
- const auth = config?.mcpServers?.spritecook?.headers?.Authorization;
71
- if (auth && auth.startsWith('Bearer sc_live_')) {
72
- return auth.replace('Bearer ', '');
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];
73
82
  }
74
83
  }
75
84
  } catch { /* ignore */ }
@@ -77,15 +86,12 @@ export function readExistingKey() {
77
86
  return null;
78
87
  }
79
88
 
80
- /**
81
- * Detect if the terminal is running inside a specific editor via env vars.
82
- */
89
+ // ── Environment detection ───────────────────────────────────────────────
90
+
83
91
  function isRunningInCursor() {
84
- // Cursor sets CURSOR_CHANNEL or has "cursor" in TERM_PROGRAM / VSCODE_GIT_IPC_HANDLE
85
92
  const env = process.env;
86
93
  if (env.CURSOR_CHANNEL) return true;
87
94
  if (env.TERM_PROGRAM === 'cursor') return true;
88
- // Cursor is a VS Code fork - check for cursor-specific paths in common env vars
89
95
  const ipcHandle = env.VSCODE_GIT_IPC_HANDLE || env.VSCODE_IPC_HOOK_CLI || '';
90
96
  if (ipcHandle.toLowerCase().includes('cursor')) return true;
91
97
  return false;
@@ -93,10 +99,8 @@ function isRunningInCursor() {
93
99
 
94
100
  function isRunningInAntigravity() {
95
101
  const env = process.env;
96
- // Antigravity may set its own env vars or identify via TERM_PROGRAM
97
102
  if (env.ANTIGRAVITY_CHANNEL) return true;
98
103
  if (env.TERM_PROGRAM === 'antigravity') return true;
99
- // Antigravity is a VS Code fork - check for antigravity-specific paths
100
104
  const ipcHandle = env.VSCODE_GIT_IPC_HANDLE || env.VSCODE_IPC_HOOK_CLI || '';
101
105
  if (ipcHandle.toLowerCase().includes('antigravity')) return true;
102
106
  return false;
@@ -111,12 +115,25 @@ function isRunningInVSCode() {
111
115
  return false;
112
116
  }
113
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
+
114
128
  /**
115
- * Detect which editors/IDEs are available and return a list of
116
- * { name, detected, configPath, write(apiKey) } objects.
117
- *
118
- * Detection uses both folder existence AND environment variables,
119
- * so editors are found even in fresh projects without config folders.
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
120
137
  */
121
138
  export function detectEditors() {
122
139
  const cwd = process.cwd();
@@ -124,55 +141,128 @@ export function detectEditors() {
124
141
  const editors = [];
125
142
 
126
143
  const inCursor = isRunningInCursor();
127
- const inVSCode = isRunningInVSCode() && !inCursor; // Cursor is a VS Code fork
144
+ const inVSCode = isRunningInVSCode() && !inCursor && !isRunningInAntigravity() && !isRunningInWindsurf();
145
+ const inAntigravity = isRunningInAntigravity();
146
+ const inWindsurf = isRunningInWindsurf();
128
147
 
129
- // Cursor - project-level .cursor/mcp.json
130
- const cursorDir = join(cwd, '.cursor');
148
+ // ── Cursor ──────────────────────────────────────────────────
131
149
  editors.push({
132
150
  name: 'Cursor',
133
- detected: existsSync(cursorDir) || inCursor,
134
- configPath: join(cursorDir, 'mcp.json'),
135
- write: (apiKey) => writeCursorConfig(cursorDir, apiKey),
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
+ },
136
165
  });
137
166
 
138
- // VS Code - project-level .vscode/settings.json
139
- const vscodeDir = join(cwd, '.vscode');
167
+ // ── VS Code ─────────────────────────────────────────────────
168
+ // Skills: project .github/skills/, global ~/.copilot/skills/
140
169
  editors.push({
141
170
  name: 'VS Code',
142
- detected: existsSync(vscodeDir) || inVSCode,
143
- configPath: join(vscodeDir, 'settings.json'),
144
- write: (apiKey) => writeVSCodeConfig(vscodeDir, apiKey),
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
+ },
145
184
  });
146
185
 
147
- // Claude Desktop - platform-specific
186
+ // ── Claude Desktop ──────────────────────────────────────────
148
187
  const claudeDesktopPath = getClaudeDesktopConfigPath();
149
188
  editors.push({
150
189
  name: 'Claude Desktop',
151
190
  detected: claudeDesktopPath ? existsSync(join(claudeDesktopPath, '..')) : false,
152
- configPath: claudeDesktopPath,
191
+ scopes: ['global'],
192
+ defaultScope: 'global',
193
+ configPath: () => claudeDesktopPath,
153
194
  write: (apiKey) => writeClaudeDesktopConfig(claudeDesktopPath, apiKey),
195
+ skillDirs: {
196
+ project: join(cwd, '.claude', 'skills', 'spritecook'),
197
+ global: join(home, '.claude', 'skills', 'spritecook'),
198
+ },
154
199
  });
155
200
 
156
- // Claude Code - ~/.claude.json or ~/.claude/settings.json
157
- const claudeCodePath1 = join(home, '.claude.json');
201
+ // ── Claude Code ─────────────────────────────────────────────
158
202
  const claudeCodePath2 = join(home, '.claude', 'settings.json');
203
+ const claudeCodePath1 = join(home, '.claude.json');
159
204
  const claudeCodeDetected = existsSync(claudeCodePath1) || existsSync(claudeCodePath2);
160
205
  const claudeCodeConfigPath = existsSync(claudeCodePath2) ? claudeCodePath2 : claudeCodePath1;
161
206
  editors.push({
162
207
  name: 'Claude Code',
163
208
  detected: claudeCodeDetected,
164
- configPath: claudeCodeConfigPath,
209
+ scopes: ['global'],
210
+ defaultScope: 'global',
211
+ configPath: () => claudeCodeConfigPath,
165
212
  write: (apiKey) => writeClaudeCodeConfig(claudeCodeConfigPath, apiKey),
213
+ skillDirs: {
214
+ project: join(cwd, '.claude', 'skills', 'spritecook'),
215
+ global: join(home, '.claude', 'skills', 'spritecook'),
216
+ },
166
217
  });
167
218
 
168
- // Antigravity (Google) - user-level mcp_config.json
219
+ // ── Antigravity (Google) ────────────────────────────────────
169
220
  const antigravityPath = getAntigravityConfigPath();
170
- const antigravityDetected = isRunningInAntigravity() || (antigravityPath ? existsSync(join(antigravityPath, '..')) : false);
171
221
  editors.push({
172
222
  name: 'Antigravity',
173
- detected: antigravityDetected,
174
- configPath: antigravityPath,
223
+ detected: inAntigravity || (antigravityPath ? existsSync(join(antigravityPath, '..')) : false),
224
+ scopes: ['global'],
225
+ defaultScope: 'global',
226
+ configPath: () => antigravityPath,
175
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
+ const codexGlobalPath = join(home, '.codex', 'config.toml');
251
+ const codexProjectPath = join(cwd, '.codex', 'config.toml');
252
+ editors.push({
253
+ name: 'Codex',
254
+ detected: existsSync(codexGlobalPath) || existsSync(codexProjectPath),
255
+ scopes: ['project', 'global'],
256
+ defaultScope: 'global',
257
+ configPath: (scope) => scope === 'project' ? codexProjectPath : codexGlobalPath,
258
+ write: (apiKey, scope) => writeCodexConfig(
259
+ scope === 'project' ? codexProjectPath : codexGlobalPath,
260
+ apiKey,
261
+ ),
262
+ skillDirs: {
263
+ project: join(cwd, '.agents', 'skills', 'spritecook'),
264
+ global: join(home, '.agents', 'skills', 'spritecook'),
265
+ },
176
266
  });
177
267
 
178
268
  return editors;
@@ -188,8 +278,10 @@ export function writeConfigs(editors, apiKey) {
188
278
 
189
279
  for (const editor of editors) {
190
280
  try {
191
- editor.write(apiKey);
192
- success(`${editor.configPath}`);
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}`);
193
285
  written++;
194
286
  } catch (err) {
195
287
  warn(`Failed to write ${editor.name} config: ${err.message}`);
@@ -205,80 +297,93 @@ function writeCursorConfig(cursorDir, apiKey) {
205
297
  const configPath = join(cursorDir, 'mcp.json');
206
298
  const mcpUrl = getMcpUrl();
207
299
 
208
- // Read existing config or start fresh
209
300
  let config = {};
210
301
  if (existsSync(configPath)) {
211
- try {
212
- config = JSON.parse(readFileSync(configPath, 'utf-8'));
213
- } catch {
214
- // Corrupted file, start fresh
215
- }
302
+ try { config = JSON.parse(readFileSync(configPath, 'utf-8')); } catch { /* start fresh */ }
216
303
  }
217
304
 
218
- // Merge the spritecook server entry
219
305
  if (!config.mcpServers) config.mcpServers = {};
220
306
  config.mcpServers.spritecook = {
221
307
  url: mcpUrl,
222
- headers: {
223
- Authorization: `Bearer ${apiKey}`,
224
- },
308
+ headers: { Authorization: `Bearer ${apiKey}` },
225
309
  };
226
310
 
227
311
  ensureDir(cursorDir);
228
312
  writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
229
313
  }
230
314
 
231
- function writeVSCodeConfig(vscodeDir, apiKey) {
232
- const configPath = join(vscodeDir, 'settings.json');
315
+ function writeVSCodeProjectConfig(vscodeDir, apiKey) {
316
+ // VS Code now uses .vscode/mcp.json with root key "servers"
317
+ const configPath = join(vscodeDir, 'mcp.json');
233
318
  const mcpUrl = getMcpUrl();
234
319
 
235
- // Read existing config or start fresh
236
320
  let config = {};
237
321
  if (existsSync(configPath)) {
238
- try {
239
- config = JSON.parse(readFileSync(configPath, 'utf-8'));
240
- } catch {
241
- // Corrupted file, start fresh
242
- }
322
+ try { config = JSON.parse(readFileSync(configPath, 'utf-8')); } catch { /* start fresh */ }
243
323
  }
244
324
 
245
- // Merge into mcp.servers without overwriting other servers
246
- if (!config.mcp) config.mcp = {};
247
- if (!config.mcp.servers) config.mcp.servers = {};
248
- config.mcp.servers.spritecook = {
325
+ if (!config.servers) config.servers = {};
326
+ config.servers.spritecook = {
249
327
  type: 'http',
250
328
  url: mcpUrl,
251
- headers: {
252
- Authorization: `Bearer ${apiKey}`,
253
- },
329
+ headers: { Authorization: `Bearer ${apiKey}` },
254
330
  };
255
331
 
256
332
  ensureDir(vscodeDir);
257
333
  writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
258
334
  }
259
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
+
260
356
  function writeClaudeDesktopConfig(configPath, apiKey) {
261
- if (!configPath) {
262
- warn('Claude Desktop config path not found for this platform.');
263
- return;
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 */ }
264
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) {
265
376
  const mcpUrl = getMcpUrl();
266
377
 
267
378
  let config = {};
268
379
  if (existsSync(configPath)) {
269
- try {
270
- config = JSON.parse(readFileSync(configPath, 'utf-8'));
271
- } catch {
272
- // Start fresh
273
- }
380
+ try { config = JSON.parse(readFileSync(configPath, 'utf-8')); } catch { /* start fresh */ }
274
381
  }
275
382
 
276
383
  if (!config.mcpServers) config.mcpServers = {};
277
384
  config.mcpServers.spritecook = {
278
385
  url: mcpUrl,
279
- headers: {
280
- Authorization: `Bearer ${apiKey}`,
281
- },
386
+ headers: { Authorization: `Bearer ${apiKey}` },
282
387
  };
283
388
 
284
389
  ensureDir(join(configPath, '..'));
@@ -286,63 +391,74 @@ function writeClaudeDesktopConfig(configPath, apiKey) {
286
391
  }
287
392
 
288
393
  function writeAntigravityConfig(configPath, apiKey) {
289
- if (!configPath) {
290
- warn('Antigravity config path not found for this platform.');
291
- return;
292
- }
394
+ if (!configPath) { warn('Antigravity config path not found.'); return; }
293
395
  const mcpUrl = getMcpUrl();
294
396
 
295
397
  let config = {};
296
398
  if (existsSync(configPath)) {
297
- try {
298
- config = JSON.parse(readFileSync(configPath, 'utf-8'));
299
- } catch {
300
- // Start fresh
301
- }
399
+ try { config = JSON.parse(readFileSync(configPath, 'utf-8')); } catch { /* start fresh */ }
302
400
  }
303
401
 
304
402
  if (!config.mcpServers) config.mcpServers = {};
305
403
  config.mcpServers.spritecook = {
306
404
  serverUrl: mcpUrl,
307
- headers: {
308
- Authorization: `Bearer ${apiKey}`,
309
- },
405
+ headers: { Authorization: `Bearer ${apiKey}` },
310
406
  };
311
407
 
312
408
  ensureDir(join(configPath, '..'));
313
409
  writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
314
410
  }
315
411
 
316
- function writeClaudeCodeConfig(configPath, apiKey) {
412
+ function writeWindsurfConfig(configPath, apiKey) {
413
+ if (!configPath) { warn('Windsurf config path not found.'); return; }
317
414
  const mcpUrl = getMcpUrl();
318
415
 
319
416
  let config = {};
320
417
  if (existsSync(configPath)) {
321
- try {
322
- config = JSON.parse(readFileSync(configPath, 'utf-8'));
323
- } catch {
324
- // Start fresh
325
- }
418
+ try { config = JSON.parse(readFileSync(configPath, 'utf-8')); } catch { /* start fresh */ }
326
419
  }
327
420
 
328
421
  if (!config.mcpServers) config.mcpServers = {};
329
422
  config.mcpServers.spritecook = {
330
423
  url: mcpUrl,
331
- headers: {
332
- Authorization: `Bearer ${apiKey}`,
333
- },
424
+ headers: { Authorization: `Bearer ${apiKey}` },
334
425
  };
335
426
 
336
427
  ensureDir(join(configPath, '..'));
337
428
  writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
338
429
  }
339
430
 
340
- // ── Helpers ─────────────────────────────────────────────────────────────
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
441
+ content = content.replace(/\[mcp_servers\.spritecook\][^\[]*/, '');
442
+
443
+ // Append the new block
444
+ const block = [
445
+ '',
446
+ '[mcp_servers.spritecook]',
447
+ `url = "${mcpUrl}"`,
448
+ `bearer_token = "${apiKey}"`,
449
+ '',
450
+ ].join('\n');
451
+
452
+ content = content.trimEnd() + '\n' + block;
453
+
454
+ ensureDir(join(configPath, '..'));
455
+ writeFileSync(configPath, content, 'utf-8');
456
+ }
457
+
458
+ // ── Path Helpers ────────────────────────────────────────────────────────
341
459
 
342
460
  function getAntigravityConfigPath() {
343
- const home = homedir();
344
- // Antigravity stores MCP config at ~/.gemini/antigravity/mcp_config.json on all platforms
345
- return join(home, '.gemini', 'antigravity', 'mcp_config.json');
461
+ return join(homedir(), '.gemini', 'antigravity', 'mcp_config.json');
346
462
  }
347
463
 
348
464
  function getClaudeDesktopConfigPath() {
@@ -359,10 +475,47 @@ function getClaudeDesktopConfigPath() {
359
475
  if (platform === 'linux') {
360
476
  return join(home, '.config', 'Claude', 'claude_desktop_config.json');
361
477
  }
478
+ return null;
479
+ }
480
+
481
+ function getWindsurfConfigPath() {
482
+ const platform = process.platform;
483
+ const home = homedir();
362
484
 
485
+ if (platform === 'darwin') {
486
+ return join(home, '.codeium', 'windsurf', 'mcp_config.json');
487
+ }
488
+ if (platform === 'win32') {
489
+ const appData = process.env.APPDATA || join(home, 'AppData', 'Roaming');
490
+ return join(appData, 'Codeium', 'Windsurf', 'mcp_config.json');
491
+ }
492
+ if (platform === 'linux') {
493
+ // Try both common paths
494
+ const p1 = join(home, '.codeium', 'windsurf', 'mcp_config.json');
495
+ const p2 = join(home, '.config', 'Codeium', 'Windsurf', 'mcp_config.json');
496
+ if (existsSync(p2)) return p2;
497
+ return p1;
498
+ }
363
499
  return null;
364
500
  }
365
501
 
502
+ function getVSCodeGlobalMcpPath() {
503
+ const platform = process.platform;
504
+ const home = homedir();
505
+
506
+ if (platform === 'darwin') {
507
+ return join(home, 'Library', 'Application Support', 'Code', 'User', 'mcp.json');
508
+ }
509
+ if (platform === 'win32') {
510
+ const appData = process.env.APPDATA || join(home, 'AppData', 'Roaming');
511
+ return join(appData, 'Code', 'User', 'mcp.json');
512
+ }
513
+ if (platform === 'linux') {
514
+ return join(home, '.config', 'Code', 'User', 'mcp.json');
515
+ }
516
+ return join(home, '.vscode', 'mcp.json'); // fallback
517
+ }
518
+
366
519
  function ensureDir(dir) {
367
520
  if (!existsSync(dir)) {
368
521
  mkdirSync(dir, { recursive: true });
package/src/setup.mjs CHANGED
@@ -67,8 +67,8 @@ export async function run() {
67
67
 
68
68
  const editors = detectEditors();
69
69
 
70
- // Always show all editors - detected ones are pre-selected, others are selectable too
71
- const response = await prompts({
70
+ // Let user select which editors to configure
71
+ const editorResponse = await prompts({
72
72
  type: 'multiselect',
73
73
  name: 'editors',
74
74
  message: 'Configure MCP for:',
@@ -81,7 +81,7 @@ export async function run() {
81
81
  instructions: false,
82
82
  });
83
83
 
84
- const selected = editors.filter((e) => response.editors?.includes(e.name));
84
+ const selected = editors.filter((e) => editorResponse.editors?.includes(e.name));
85
85
 
86
86
  if (selected.length === 0) {
87
87
  warn('No editors selected. Skipping config.');
@@ -89,6 +89,30 @@ export async function run() {
89
89
  info('You can configure manually later. Your API key:');
90
90
  console.log(` ${chalk.dim(apiKey.slice(0, 16) + '...')}`);
91
91
  } else {
92
+ // Ask about scope for editors that support both project and global
93
+ const multiScopeEditors = selected.filter((e) => e.scopes.length > 1);
94
+ if (multiScopeEditors.length > 0) {
95
+ console.log();
96
+ const scopeResponse = await prompts({
97
+ type: 'select',
98
+ name: 'scope',
99
+ message: 'Install MCP config:',
100
+ choices: [
101
+ { title: 'This project only (recommended)', value: 'project', description: 'Writes to project config files' },
102
+ { title: 'Global (all projects)', value: 'global', description: 'Writes to user-level config' },
103
+ ],
104
+ initial: 0,
105
+ });
106
+
107
+ const chosenScope = scopeResponse.scope || 'project';
108
+ for (const e of selected) {
109
+ if (e.scopes.includes(chosenScope)) {
110
+ e._chosenScope = chosenScope;
111
+ }
112
+ // Editors with only 1 scope keep their default
113
+ }
114
+ }
115
+
92
116
  console.log();
93
117
  info('Writing MCP config...');
94
118
  const written = writeConfigs(selected, apiKey);
@@ -100,7 +124,7 @@ export async function run() {
100
124
 
101
125
  // ── Step 4: Optional agent skill ─────────────────────────────────
102
126
  step(4, 'Agent Skill (optional)');
103
- await maybeInstallSkill(selected);
127
+ await maybeInstallSkill();
104
128
 
105
129
  // ── Done ──────────────────────────────────────────────────────────
106
130
  const configuredNames = selected.map((e) => e.name);
package/src/skill.mjs CHANGED
@@ -1,123 +1,37 @@
1
- import { writeFileSync, readFileSync, existsSync, mkdirSync } from 'node:fs';
2
- import { join } from 'node:path';
1
+ import { execSync } from 'node:child_process';
2
+ import { platform } from 'node:os';
3
3
  import prompts from 'prompts';
4
4
  import { success, info, warn } from './ui.mjs';
5
- import { getApiBase } from './config.mjs';
6
-
7
- /** Fetch the latest skill document from the SpriteCook API. */
8
- async function fetchLatestSkill() {
9
- try {
10
- const res = await fetch(`${getApiBase()}/v1/public/agent-skill`);
11
- if (res.ok) {
12
- const text = await res.text();
13
- if (text && text.length > 50) return text;
14
- }
15
- } catch {
16
- // Network error, fall through to bundled fallback
17
- }
18
- return null;
19
- }
20
-
21
- // Bundled fallback (used when the API is unreachable)
22
- const FALLBACK_SKILL = `# SpriteCook - AI Game Asset Generator
23
-
24
- Use SpriteCook MCP tools when the user needs pixel art, detailed sprites, game assets, icons, tilesets, textures, or UI elements for a game project.
25
-
26
- ## Available Tools
27
-
28
- ### generate_pixel_art
29
- Generate game art from a text prompt. Key params: prompt (required), width, height, pixel, bg_mode, theme, style, mode, reference_asset_id, edit_asset_id.
30
-
31
- ### check_job_status
32
- Check progress of a generation job by job_id.
33
-
34
- ### get_credit_balance
35
- Check remaining credits and subscription tier.
36
-
37
- For full documentation visit: https://spritecook.ai/agents
38
- `;
39
-
40
- // Map editor names to their project-level skill directory (.<editor>/skills/spritecook/)
41
- const EDITOR_SKILL_DIRS = {
42
- 'Cursor': '.cursor',
43
- 'VS Code': '.vscode',
44
- 'Antigravity': '.antigravity',
45
- 'Claude Desktop': '.claude',
46
- 'Claude Code': '.claude',
47
- };
48
5
 
49
6
  /**
50
- * Determine which skill install targets are relevant based on selected editors.
51
- * Each selected editor gets a local project skill at .<editor>/skills/spritecook/SKILL.md.
52
- * Returns an array of { name, dir, path, exists } objects (deduplicated by path).
7
+ * Install or update the SpriteCook agent skill using the standard
8
+ * `npx skills add` CLI. This handles editor detection and correct
9
+ * file placement for all supported editors automatically.
53
10
  */
54
- function getSkillTargets(selectedEditors) {
55
- const cwd = process.cwd();
56
- const targets = [];
57
- const seenPaths = new Set();
58
-
59
- for (const editor of (selectedEditors || [])) {
60
- const dotDir = EDITOR_SKILL_DIRS[editor.name];
61
- if (!dotDir) continue;
62
-
63
- const dir = join(cwd, dotDir, 'skills', 'spritecook');
64
- const path = join(dir, 'SKILL.md');
65
-
66
- // Deduplicate (e.g. Claude Desktop & Claude Code both use .claude)
67
- if (seenPaths.has(path)) continue;
68
- seenPaths.add(path);
69
-
70
- targets.push({ name: editor.name, dir, path, exists: existsSync(path) });
71
- }
72
-
73
- return targets;
74
- }
75
-
76
- /**
77
- * Optionally install or update the SpriteCook agent skill.
78
- * Supports Cursor (.cursor/skills/) and Antigravity (.antigravity/skills/).
79
- * Always fetches the latest version from the API.
80
- */
81
- export async function maybeInstallSkill(selectedEditors) {
82
- const targets = getSkillTargets(selectedEditors);
83
-
84
- if (targets.length === 0) {
85
- return; // No supported editors for skills
11
+ export async function maybeInstallSkill() {
12
+ info('The agent skill teaches your AI how to generate sprites autonomously.');
13
+ const response = await prompts({
14
+ type: 'confirm',
15
+ name: 'install',
16
+ message: 'Install SpriteCook agent skill? (highly recommended)',
17
+ initial: true,
18
+ });
19
+
20
+ if (!response.install) {
21
+ return;
86
22
  }
87
23
 
88
- const allExist = targets.every(t => t.exists);
89
- const anyExist = targets.some(t => t.exists);
24
+ info('Installing skill via npx skills add SpriteCook/skills...');
90
25
 
91
- if (allExist) {
92
- // Silently update all
93
- info('Updating agent skill to latest version...');
94
- } else {
95
- const editorNames = targets.filter(t => !t.exists).map(t => t.name).join(' & ');
96
- const response = await prompts({
97
- type: 'confirm',
98
- name: 'install',
99
- message: `Install SpriteCook agent skill for ${editorNames}? (helps AI use SpriteCook proactively)`,
100
- initial: true,
26
+ try {
27
+ const npxCmd = platform() === 'win32' ? 'npx.cmd' : 'npx';
28
+ execSync(`${npxCmd} -y skills add SpriteCook/skills`, {
29
+ stdio: 'inherit',
30
+ timeout: 60_000,
101
31
  });
102
-
103
- if (!response.install) {
104
- return;
105
- }
106
- }
107
-
108
- // Fetch latest from API, fall back to bundled version
109
- const content = await fetchLatestSkill() || FALLBACK_SKILL;
110
-
111
- for (const target of targets) {
112
- if (!existsSync(target.dir)) {
113
- mkdirSync(target.dir, { recursive: true });
114
- }
115
- writeFileSync(target.path, content, 'utf-8');
116
-
117
- if (target.exists) {
118
- success(`${target.name} skill updated.`);
119
- } else {
120
- success(`${target.name} skill installed at ${target.path}`);
121
- }
32
+ success('Agent skill installed.');
33
+ } catch (err) {
34
+ warn('Skill install failed. You can install it manually later:');
35
+ console.log(' npx skills add SpriteCook/skills');
122
36
  }
123
37
  }