makecc 0.2.2 → 0.2.4

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.
@@ -5,8 +5,8 @@
5
5
  <link rel="icon" type="image/svg+xml" href="/vite.svg" />
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
7
  <title>vite-temp</title>
8
- <script type="module" crossorigin src="/assets/index--RFba-Yw.js"></script>
9
- <link rel="stylesheet" crossorigin href="/assets/index-COYRelWD.css">
8
+ <script type="module" crossorigin src="/assets/index-c7WYmobg.js"></script>
9
+ <link rel="stylesheet" crossorigin href="/assets/index-DLT8bQFx.css">
10
10
  </head>
11
11
  <body>
12
12
  <div id="root"></div>
@@ -9,6 +9,7 @@ import { existsSync } from 'fs';
9
9
  import { ClaudeService } from './services/claudeService';
10
10
  import { fileService } from './services/fileService';
11
11
  import { workflowAIService } from './services/workflowAIService';
12
+ import { skillGeneratorService } from './services/skillGeneratorService';
12
13
  import { workflowExecutionService } from './services/workflowExecutionService';
13
14
  import { executeInTerminal } from './services/terminalService';
14
15
  const __filename = fileURLToPath(import.meta.url);
@@ -58,6 +59,67 @@ app.post('/api/save/workflow', async (req, res) => {
58
59
  res.status(500).json({ message: errorMessage });
59
60
  }
60
61
  });
62
+ // Save API key to .env
63
+ app.post('/api/settings/api-key', async (req, res) => {
64
+ try {
65
+ const { apiKey } = req.body;
66
+ if (!apiKey) {
67
+ return res.status(400).json({ message: 'API key is required' });
68
+ }
69
+ const result = await fileService.saveApiKey(apiKey);
70
+ if (result.success) {
71
+ res.json({ success: true, message: 'API 키가 .env 파일에 저장되었습니다.' });
72
+ }
73
+ else {
74
+ res.status(500).json({ success: false, message: result.error });
75
+ }
76
+ }
77
+ catch (error) {
78
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
79
+ res.status(500).json({ message: errorMessage });
80
+ }
81
+ });
82
+ // Get API settings
83
+ app.get('/api/settings/api-key', async (req, res) => {
84
+ try {
85
+ const settings = await fileService.getApiSettings();
86
+ res.json(settings);
87
+ }
88
+ catch (error) {
89
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
90
+ res.status(500).json({ message: errorMessage });
91
+ }
92
+ });
93
+ // Generate skill using AI
94
+ app.post('/api/generate/skill', async (req, res) => {
95
+ try {
96
+ const { prompt } = req.body;
97
+ if (!prompt || typeof prompt !== 'string') {
98
+ return res.status(400).json({ message: 'Prompt is required' });
99
+ }
100
+ const apiMode = req.headers['x-api-mode'] || 'proxy';
101
+ const apiKey = req.headers['x-api-key'];
102
+ const proxyUrl = req.headers['x-proxy-url'];
103
+ console.log('Generating skill for prompt:', prompt);
104
+ const result = await skillGeneratorService.generate(prompt, {
105
+ apiMode: apiMode,
106
+ apiKey,
107
+ proxyUrl,
108
+ });
109
+ if (result.success) {
110
+ console.log('Skill generated:', result.skill?.skillName, 'at', result.savedPath);
111
+ res.json(result);
112
+ }
113
+ else {
114
+ res.status(500).json({ message: result.error });
115
+ }
116
+ }
117
+ catch (error) {
118
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
119
+ console.error('Generate skill error:', errorMessage);
120
+ res.status(500).json({ message: errorMessage });
121
+ }
122
+ });
61
123
  // Generate workflow using AI
62
124
  app.post('/api/generate/workflow', async (req, res) => {
63
125
  try {
@@ -190,5 +190,58 @@ export class FileService {
190
190
  getProjectPath() {
191
191
  return this.projectRoot;
192
192
  }
193
+ /**
194
+ * Saves API key to .env file
195
+ */
196
+ async saveApiKey(apiKey) {
197
+ try {
198
+ const envPath = path.join(this.projectRoot, '.env');
199
+ // Read existing .env content
200
+ let envContent = '';
201
+ try {
202
+ envContent = await fs.readFile(envPath, 'utf-8');
203
+ }
204
+ catch {
205
+ // File doesn't exist, will create new
206
+ }
207
+ // Parse existing env variables
208
+ const envLines = envContent.split('\n');
209
+ let found = false;
210
+ const updatedLines = envLines.map(line => {
211
+ if (line.startsWith('ANTHROPIC_API_KEY=')) {
212
+ found = true;
213
+ return `ANTHROPIC_API_KEY=${apiKey}`;
214
+ }
215
+ return line;
216
+ });
217
+ // Add if not found
218
+ if (!found) {
219
+ // Remove empty lines at the end before adding
220
+ while (updatedLines.length > 0 && updatedLines[updatedLines.length - 1].trim() === '') {
221
+ updatedLines.pop();
222
+ }
223
+ updatedLines.push(`ANTHROPIC_API_KEY=${apiKey}`);
224
+ }
225
+ // Write back
226
+ await fs.writeFile(envPath, updatedLines.join('\n') + '\n', 'utf-8');
227
+ // Update process.env for current session
228
+ process.env.ANTHROPIC_API_KEY = apiKey;
229
+ return { success: true };
230
+ }
231
+ catch (error) {
232
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
233
+ return { success: false, error: errorMessage };
234
+ }
235
+ }
236
+ /**
237
+ * Gets current API settings
238
+ */
239
+ async getApiSettings() {
240
+ const envKey = process.env.ANTHROPIC_API_KEY;
241
+ return {
242
+ apiKey: envKey ? `${envKey.slice(0, 10)}...${envKey.slice(-4)}` : null,
243
+ hasEnvKey: !!envKey,
244
+ };
245
+ }
193
246
  }
194
247
  export const fileService = new FileService();
@@ -0,0 +1,168 @@
1
+ import Anthropic from '@anthropic-ai/sdk';
2
+ import * as fs from 'fs/promises';
3
+ import * as path from 'path';
4
+ const SYSTEM_PROMPT = `당신은 Claude Code 스킬 생성 전문가입니다.
5
+
6
+ 사용자의 요청을 분석하여 완전히 작동하는 Claude Code 스킬을 생성합니다.
7
+
8
+ ## Claude Code 스킬 구조
9
+
10
+ 스킬은 다음 구조로 생성됩니다:
11
+ \`\`\`
12
+ .claude/skills/[skill-name]/
13
+ ├── SKILL.md # 스킬 정의 (필수)
14
+ ├── scripts/ # 스크립트 폴더
15
+ │ └── main.py # 메인 스크립트
16
+ └── requirements.txt # Python 의존성 (필요시)
17
+ \`\`\`
18
+
19
+ ## SKILL.md 형식
20
+
21
+ \`\`\`markdown
22
+ ---
23
+ name: skill-name
24
+ description: 스킬 설명 (한 줄)
25
+ ---
26
+
27
+ # 스킬 이름
28
+
29
+ ## 사용 시점
30
+ 이 스킬은 다음 상황에서 사용됩니다:
31
+ - 상황 1
32
+ - 상황 2
33
+
34
+ ## 사용 방법
35
+
36
+ \\\`\\\`\\\`bash
37
+ ~/.claude/venv/bin/python ~/.claude/skills/[skill-name]/scripts/main.py [인자들]
38
+ \\\`\\\`\\\`
39
+
40
+ ## 파라미터
41
+ - \`param1\`: 설명
42
+ - \`param2\`: 설명
43
+
44
+ ## 예시
45
+ [사용 예시]
46
+ \`\`\`
47
+
48
+ ## 응답 형식 (JSON)
49
+
50
+ 반드시 아래 형식의 JSON으로 응답하세요:
51
+
52
+ {
53
+ "skillName": "스킬 이름 (한글 가능)",
54
+ "skillId": "skill-id-kebab-case",
55
+ "description": "스킬 설명",
56
+ "files": [
57
+ {
58
+ "path": "SKILL.md",
59
+ "content": "SKILL.md 전체 내용",
60
+ "language": "markdown"
61
+ },
62
+ {
63
+ "path": "scripts/main.py",
64
+ "content": "Python 스크립트 전체 내용",
65
+ "language": "python"
66
+ },
67
+ {
68
+ "path": "requirements.txt",
69
+ "content": "의존성 목록 (필요한 경우)",
70
+ "language": "text"
71
+ }
72
+ ]
73
+ }
74
+
75
+ ## 중요 규칙
76
+
77
+ 1. **완전한 코드 생성**: 실제로 동작하는 완전한 코드를 작성하세요
78
+ 2. **에러 처리 포함**: try-except로 에러 처리를 포함하세요
79
+ 3. **한글 지원**: 출력 메시지는 한글로 작성하세요
80
+ 4. **환경 고려**:
81
+ - Python 스크립트는 \`~/.claude/venv/bin/python\`으로 실행됩니다
82
+ - 필요한 패키지는 requirements.txt에 명시하세요
83
+ 5. **JSON만 반환**: 설명 없이 JSON만 반환하세요
84
+ `;
85
+ export class SkillGeneratorService {
86
+ projectRoot;
87
+ constructor(projectRoot) {
88
+ this.projectRoot = projectRoot || process.env.MAKECC_PROJECT_PATH || process.cwd();
89
+ }
90
+ getClient(settings) {
91
+ if (settings?.apiMode === 'direct' && settings.apiKey) {
92
+ return new Anthropic({ apiKey: settings.apiKey });
93
+ }
94
+ if (settings?.apiMode === 'proxy' && settings.proxyUrl) {
95
+ return new Anthropic({
96
+ baseURL: settings.proxyUrl,
97
+ apiKey: process.env.ANTHROPIC_API_KEY || 'proxy-mode',
98
+ });
99
+ }
100
+ const envApiKey = process.env.ANTHROPIC_API_KEY;
101
+ if (!envApiKey) {
102
+ throw new Error('API 키가 설정되지 않았습니다. 설정에서 API 키를 입력하세요.');
103
+ }
104
+ return new Anthropic({ apiKey: envApiKey });
105
+ }
106
+ async generate(prompt, settings) {
107
+ const client = this.getClient(settings);
108
+ const userPrompt = `다음 스킬을 생성해주세요: "${prompt}"
109
+
110
+ 완전히 동작하는 코드를 포함한 스킬을 생성하세요.
111
+ 반드시 JSON만 반환하세요.`;
112
+ try {
113
+ const response = await client.messages.create({
114
+ model: 'claude-sonnet-4-20250514',
115
+ max_tokens: 8192,
116
+ system: SYSTEM_PROMPT,
117
+ messages: [{ role: 'user', content: userPrompt }],
118
+ });
119
+ let responseText = '';
120
+ for (const block of response.content) {
121
+ if (block.type === 'text') {
122
+ responseText += block.text;
123
+ }
124
+ }
125
+ if (!responseText) {
126
+ return { success: false, error: 'AI 응답을 받지 못했습니다.' };
127
+ }
128
+ // JSON 추출
129
+ const jsonMatch = responseText.match(/```(?:json)?\s*([\s\S]*?)```/);
130
+ const rawJson = jsonMatch ? jsonMatch[1].trim() : responseText.trim();
131
+ let skill;
132
+ try {
133
+ skill = JSON.parse(rawJson);
134
+ }
135
+ catch {
136
+ console.error('Failed to parse skill response:', rawJson.slice(0, 500));
137
+ return { success: false, error: 'AI 응답을 파싱할 수 없습니다. 다시 시도해주세요.' };
138
+ }
139
+ // 파일 저장
140
+ const skillPath = path.join(this.projectRoot, '.claude', 'skills', skill.skillId);
141
+ await this.saveSkillFiles(skillPath, skill.files);
142
+ return {
143
+ success: true,
144
+ skill,
145
+ savedPath: skillPath,
146
+ };
147
+ }
148
+ catch (error) {
149
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
150
+ console.error('Skill generation error:', errorMessage);
151
+ return { success: false, error: errorMessage };
152
+ }
153
+ }
154
+ async saveSkillFiles(skillPath, files) {
155
+ // 스킬 디렉토리 생성
156
+ await fs.mkdir(skillPath, { recursive: true });
157
+ for (const file of files) {
158
+ const filePath = path.join(skillPath, file.path);
159
+ const dirPath = path.dirname(filePath);
160
+ // 하위 디렉토리 생성
161
+ await fs.mkdir(dirPath, { recursive: true });
162
+ // 파일 저장
163
+ await fs.writeFile(filePath, file.content, 'utf-8');
164
+ console.log(`Saved: ${filePath}`);
165
+ }
166
+ }
167
+ }
168
+ export const skillGeneratorService = new SkillGeneratorService();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "makecc",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "type": "module",
5
5
  "description": "Visual workflow builder for Claude Code agents and skills",
6
6
  "keywords": [
package/server/index.ts CHANGED
@@ -9,6 +9,7 @@ import { existsSync } from 'fs';
9
9
  import { ClaudeService } from './services/claudeService';
10
10
  import { fileService } from './services/fileService';
11
11
  import { workflowAIService } from './services/workflowAIService';
12
+ import { skillGeneratorService } from './services/skillGeneratorService';
12
13
  import { workflowExecutionService } from './services/workflowExecutionService';
13
14
  import { executeInTerminal, getClaudeCommand } from './services/terminalService';
14
15
  import type { WorkflowExecutionRequest, NodeExecutionUpdate } from './types';
@@ -75,6 +76,72 @@ app.post('/api/save/workflow', async (req, res) => {
75
76
  }
76
77
  });
77
78
 
79
+ // Save API key to .env
80
+ app.post('/api/settings/api-key', async (req, res) => {
81
+ try {
82
+ const { apiKey } = req.body as { apiKey: string };
83
+
84
+ if (!apiKey) {
85
+ return res.status(400).json({ message: 'API key is required' });
86
+ }
87
+
88
+ const result = await fileService.saveApiKey(apiKey);
89
+ if (result.success) {
90
+ res.json({ success: true, message: 'API 키가 .env 파일에 저장되었습니다.' });
91
+ } else {
92
+ res.status(500).json({ success: false, message: result.error });
93
+ }
94
+ } catch (error) {
95
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
96
+ res.status(500).json({ message: errorMessage });
97
+ }
98
+ });
99
+
100
+ // Get API settings
101
+ app.get('/api/settings/api-key', async (req, res) => {
102
+ try {
103
+ const settings = await fileService.getApiSettings();
104
+ res.json(settings);
105
+ } catch (error) {
106
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
107
+ res.status(500).json({ message: errorMessage });
108
+ }
109
+ });
110
+
111
+ // Generate skill using AI
112
+ app.post('/api/generate/skill', async (req, res) => {
113
+ try {
114
+ const { prompt } = req.body as { prompt: string };
115
+
116
+ if (!prompt || typeof prompt !== 'string') {
117
+ return res.status(400).json({ message: 'Prompt is required' });
118
+ }
119
+
120
+ const apiMode = req.headers['x-api-mode'] as string || 'proxy';
121
+ const apiKey = req.headers['x-api-key'] as string;
122
+ const proxyUrl = req.headers['x-proxy-url'] as string;
123
+
124
+ console.log('Generating skill for prompt:', prompt);
125
+
126
+ const result = await skillGeneratorService.generate(prompt, {
127
+ apiMode: apiMode as 'proxy' | 'direct',
128
+ apiKey,
129
+ proxyUrl,
130
+ });
131
+
132
+ if (result.success) {
133
+ console.log('Skill generated:', result.skill?.skillName, 'at', result.savedPath);
134
+ res.json(result);
135
+ } else {
136
+ res.status(500).json({ message: result.error });
137
+ }
138
+ } catch (error) {
139
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
140
+ console.error('Generate skill error:', errorMessage);
141
+ res.status(500).json({ message: errorMessage });
142
+ }
143
+ });
144
+
78
145
  // Generate workflow using AI
79
146
  app.post('/api/generate/workflow', async (req, res) => {
80
147
  try {
@@ -273,6 +273,65 @@ export class FileService {
273
273
  getProjectPath(): string {
274
274
  return this.projectRoot;
275
275
  }
276
+
277
+ /**
278
+ * Saves API key to .env file
279
+ */
280
+ async saveApiKey(apiKey: string): Promise<{ success: boolean; error?: string }> {
281
+ try {
282
+ const envPath = path.join(this.projectRoot, '.env');
283
+
284
+ // Read existing .env content
285
+ let envContent = '';
286
+ try {
287
+ envContent = await fs.readFile(envPath, 'utf-8');
288
+ } catch {
289
+ // File doesn't exist, will create new
290
+ }
291
+
292
+ // Parse existing env variables
293
+ const envLines = envContent.split('\n');
294
+ let found = false;
295
+ const updatedLines = envLines.map(line => {
296
+ if (line.startsWith('ANTHROPIC_API_KEY=')) {
297
+ found = true;
298
+ return `ANTHROPIC_API_KEY=${apiKey}`;
299
+ }
300
+ return line;
301
+ });
302
+
303
+ // Add if not found
304
+ if (!found) {
305
+ // Remove empty lines at the end before adding
306
+ while (updatedLines.length > 0 && updatedLines[updatedLines.length - 1].trim() === '') {
307
+ updatedLines.pop();
308
+ }
309
+ updatedLines.push(`ANTHROPIC_API_KEY=${apiKey}`);
310
+ }
311
+
312
+ // Write back
313
+ await fs.writeFile(envPath, updatedLines.join('\n') + '\n', 'utf-8');
314
+
315
+ // Update process.env for current session
316
+ process.env.ANTHROPIC_API_KEY = apiKey;
317
+
318
+ return { success: true };
319
+ } catch (error) {
320
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
321
+ return { success: false, error: errorMessage };
322
+ }
323
+ }
324
+
325
+ /**
326
+ * Gets current API settings
327
+ */
328
+ async getApiSettings(): Promise<{ apiKey: string | null; hasEnvKey: boolean }> {
329
+ const envKey = process.env.ANTHROPIC_API_KEY;
330
+ return {
331
+ apiKey: envKey ? `${envKey.slice(0, 10)}...${envKey.slice(-4)}` : null,
332
+ hasEnvKey: !!envKey,
333
+ };
334
+ }
276
335
  }
277
336
 
278
337
  export const fileService = new FileService();
@@ -0,0 +1,209 @@
1
+ import Anthropic from '@anthropic-ai/sdk';
2
+ import * as fs from 'fs/promises';
3
+ import * as path from 'path';
4
+ import type { ApiSettings } from './workflowAIService';
5
+
6
+ export interface GeneratedSkill {
7
+ skillName: string;
8
+ skillId: string;
9
+ description: string;
10
+ files: Array<{
11
+ path: string;
12
+ content: string;
13
+ language: string;
14
+ }>;
15
+ }
16
+
17
+ export interface SkillGenerationResult {
18
+ success: boolean;
19
+ skill?: GeneratedSkill;
20
+ savedPath?: string;
21
+ error?: string;
22
+ }
23
+
24
+ const SYSTEM_PROMPT = `당신은 Claude Code 스킬 생성 전문가입니다.
25
+
26
+ 사용자의 요청을 분석하여 완전히 작동하는 Claude Code 스킬을 생성합니다.
27
+
28
+ ## Claude Code 스킬 구조
29
+
30
+ 스킬은 다음 구조로 생성됩니다:
31
+ \`\`\`
32
+ .claude/skills/[skill-name]/
33
+ ├── SKILL.md # 스킬 정의 (필수)
34
+ ├── scripts/ # 스크립트 폴더
35
+ │ └── main.py # 메인 스크립트
36
+ └── requirements.txt # Python 의존성 (필요시)
37
+ \`\`\`
38
+
39
+ ## SKILL.md 형식
40
+
41
+ \`\`\`markdown
42
+ ---
43
+ name: skill-name
44
+ description: 스킬 설명 (한 줄)
45
+ ---
46
+
47
+ # 스킬 이름
48
+
49
+ ## 사용 시점
50
+ 이 스킬은 다음 상황에서 사용됩니다:
51
+ - 상황 1
52
+ - 상황 2
53
+
54
+ ## 사용 방법
55
+
56
+ \\\`\\\`\\\`bash
57
+ ~/.claude/venv/bin/python ~/.claude/skills/[skill-name]/scripts/main.py [인자들]
58
+ \\\`\\\`\\\`
59
+
60
+ ## 파라미터
61
+ - \`param1\`: 설명
62
+ - \`param2\`: 설명
63
+
64
+ ## 예시
65
+ [사용 예시]
66
+ \`\`\`
67
+
68
+ ## 응답 형식 (JSON)
69
+
70
+ 반드시 아래 형식의 JSON으로 응답하세요:
71
+
72
+ {
73
+ "skillName": "스킬 이름 (한글 가능)",
74
+ "skillId": "skill-id-kebab-case",
75
+ "description": "스킬 설명",
76
+ "files": [
77
+ {
78
+ "path": "SKILL.md",
79
+ "content": "SKILL.md 전체 내용",
80
+ "language": "markdown"
81
+ },
82
+ {
83
+ "path": "scripts/main.py",
84
+ "content": "Python 스크립트 전체 내용",
85
+ "language": "python"
86
+ },
87
+ {
88
+ "path": "requirements.txt",
89
+ "content": "의존성 목록 (필요한 경우)",
90
+ "language": "text"
91
+ }
92
+ ]
93
+ }
94
+
95
+ ## 중요 규칙
96
+
97
+ 1. **완전한 코드 생성**: 실제로 동작하는 완전한 코드를 작성하세요
98
+ 2. **에러 처리 포함**: try-except로 에러 처리를 포함하세요
99
+ 3. **한글 지원**: 출력 메시지는 한글로 작성하세요
100
+ 4. **환경 고려**:
101
+ - Python 스크립트는 \`~/.claude/venv/bin/python\`으로 실행됩니다
102
+ - 필요한 패키지는 requirements.txt에 명시하세요
103
+ 5. **JSON만 반환**: 설명 없이 JSON만 반환하세요
104
+ `;
105
+
106
+ export class SkillGeneratorService {
107
+ private projectRoot: string;
108
+
109
+ constructor(projectRoot?: string) {
110
+ this.projectRoot = projectRoot || process.env.MAKECC_PROJECT_PATH || process.cwd();
111
+ }
112
+
113
+ private getClient(settings?: ApiSettings): Anthropic {
114
+ if (settings?.apiMode === 'direct' && settings.apiKey) {
115
+ return new Anthropic({ apiKey: settings.apiKey });
116
+ }
117
+
118
+ if (settings?.apiMode === 'proxy' && settings.proxyUrl) {
119
+ return new Anthropic({
120
+ baseURL: settings.proxyUrl,
121
+ apiKey: process.env.ANTHROPIC_API_KEY || 'proxy-mode',
122
+ });
123
+ }
124
+
125
+ const envApiKey = process.env.ANTHROPIC_API_KEY;
126
+ if (!envApiKey) {
127
+ throw new Error('API 키가 설정되지 않았습니다. 설정에서 API 키를 입력하세요.');
128
+ }
129
+
130
+ return new Anthropic({ apiKey: envApiKey });
131
+ }
132
+
133
+ async generate(prompt: string, settings?: ApiSettings): Promise<SkillGenerationResult> {
134
+ const client = this.getClient(settings);
135
+
136
+ const userPrompt = `다음 스킬을 생성해주세요: "${prompt}"
137
+
138
+ 완전히 동작하는 코드를 포함한 스킬을 생성하세요.
139
+ 반드시 JSON만 반환하세요.`;
140
+
141
+ try {
142
+ const response = await client.messages.create({
143
+ model: 'claude-sonnet-4-20250514',
144
+ max_tokens: 8192,
145
+ system: SYSTEM_PROMPT,
146
+ messages: [{ role: 'user', content: userPrompt }],
147
+ });
148
+
149
+ let responseText = '';
150
+ for (const block of response.content) {
151
+ if (block.type === 'text') {
152
+ responseText += block.text;
153
+ }
154
+ }
155
+
156
+ if (!responseText) {
157
+ return { success: false, error: 'AI 응답을 받지 못했습니다.' };
158
+ }
159
+
160
+ // JSON 추출
161
+ const jsonMatch = responseText.match(/```(?:json)?\s*([\s\S]*?)```/);
162
+ const rawJson = jsonMatch ? jsonMatch[1].trim() : responseText.trim();
163
+
164
+ let skill: GeneratedSkill;
165
+ try {
166
+ skill = JSON.parse(rawJson);
167
+ } catch {
168
+ console.error('Failed to parse skill response:', rawJson.slice(0, 500));
169
+ return { success: false, error: 'AI 응답을 파싱할 수 없습니다. 다시 시도해주세요.' };
170
+ }
171
+
172
+ // 파일 저장
173
+ const skillPath = path.join(this.projectRoot, '.claude', 'skills', skill.skillId);
174
+ await this.saveSkillFiles(skillPath, skill.files);
175
+
176
+ return {
177
+ success: true,
178
+ skill,
179
+ savedPath: skillPath,
180
+ };
181
+ } catch (error) {
182
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
183
+ console.error('Skill generation error:', errorMessage);
184
+ return { success: false, error: errorMessage };
185
+ }
186
+ }
187
+
188
+ private async saveSkillFiles(
189
+ skillPath: string,
190
+ files: Array<{ path: string; content: string }>
191
+ ): Promise<void> {
192
+ // 스킬 디렉토리 생성
193
+ await fs.mkdir(skillPath, { recursive: true });
194
+
195
+ for (const file of files) {
196
+ const filePath = path.join(skillPath, file.path);
197
+ const dirPath = path.dirname(filePath);
198
+
199
+ // 하위 디렉토리 생성
200
+ await fs.mkdir(dirPath, { recursive: true });
201
+
202
+ // 파일 저장
203
+ await fs.writeFile(filePath, file.content, 'utf-8');
204
+ console.log(`Saved: ${filePath}`);
205
+ }
206
+ }
207
+ }
208
+
209
+ export const skillGeneratorService = new SkillGeneratorService();