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 +2 -1
- package/src/editors.mjs +300 -147
- package/src/setup.mjs +28 -4
- package/src/skill.mjs +26 -112
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spritecook-mcp",
|
|
3
|
-
"version": "0.2.
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
const
|
|
18
|
-
if (
|
|
19
|
-
|
|
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
|
-
|
|
23
|
+
return null;
|
|
24
|
+
};
|
|
26
25
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
//
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
//
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
//
|
|
73
|
+
// Codex (TOML - just search for the key string)
|
|
66
74
|
try {
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
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
|
|
144
|
+
const inVSCode = isRunningInVSCode() && !inCursor && !isRunningInAntigravity() && !isRunningInWindsurf();
|
|
145
|
+
const inAntigravity = isRunningInAntigravity();
|
|
146
|
+
const inWindsurf = isRunningInWindsurf();
|
|
128
147
|
|
|
129
|
-
// Cursor
|
|
130
|
-
const cursorDir = join(cwd, '.cursor');
|
|
148
|
+
// ── Cursor ──────────────────────────────────────────────────
|
|
131
149
|
editors.push({
|
|
132
150
|
name: 'Cursor',
|
|
133
|
-
detected: existsSync(
|
|
134
|
-
|
|
135
|
-
|
|
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
|
|
139
|
-
|
|
167
|
+
// ── VS Code ─────────────────────────────────────────────────
|
|
168
|
+
// Skills: project .github/skills/, global ~/.copilot/skills/
|
|
140
169
|
editors.push({
|
|
141
170
|
name: 'VS Code',
|
|
142
|
-
detected: existsSync(
|
|
143
|
-
|
|
144
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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)
|
|
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:
|
|
174
|
-
|
|
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.
|
|
192
|
-
|
|
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
|
|
232
|
-
|
|
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
|
-
|
|
246
|
-
|
|
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
|
-
|
|
263
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
71
|
-
const
|
|
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) =>
|
|
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(
|
|
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 {
|
|
2
|
-
import {
|
|
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
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
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
|
|
55
|
-
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
89
|
-
const anyExist = targets.some(t => t.exists);
|
|
24
|
+
info('Installing skill via npx skills add SpriteCook/skills...');
|
|
90
25
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
104
|
-
|
|
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
|
}
|