makecc 0.2.5 → 0.2.8
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/dist/client/assets/index-C2Yw-VLu.js +150 -0
- package/dist/client/assets/index-v9IFpWkA.css +1 -0
- package/dist/client/index.html +2 -2
- package/dist/server/index.js +226 -1
- package/dist/server/services/configLoaderService.js +185 -0
- package/dist/server/services/nodeSyncService.js +313 -0
- package/dist/server/services/skillGeneratorService.js +159 -1
- package/package.json +1 -1
- package/server/index.ts +257 -2
- package/server/services/configLoaderService.ts +260 -0
- package/server/services/nodeSyncService.ts +384 -0
- package/server/services/skillGeneratorService.ts +200 -1
- package/dist/client/assets/index-DLT8bQFx.css +0 -1
- package/dist/client/assets/index-c7WYmobg.js +0 -147
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
import * as fs from 'fs/promises';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
|
|
4
|
+
interface NodeData {
|
|
5
|
+
id: string;
|
|
6
|
+
type: 'skill' | 'subagent' | 'command' | 'hook' | 'input' | 'output';
|
|
7
|
+
label: string;
|
|
8
|
+
description?: string;
|
|
9
|
+
// Skill specific
|
|
10
|
+
skillId?: string;
|
|
11
|
+
skillPath?: string;
|
|
12
|
+
// Subagent specific
|
|
13
|
+
tools?: string[];
|
|
14
|
+
model?: string;
|
|
15
|
+
skills?: string[]; // Connected skills
|
|
16
|
+
systemPrompt?: string;
|
|
17
|
+
// Command specific
|
|
18
|
+
commandName?: string;
|
|
19
|
+
commandContent?: string;
|
|
20
|
+
// Hook specific
|
|
21
|
+
hookEvent?: string;
|
|
22
|
+
hookMatcher?: string;
|
|
23
|
+
hookCommand?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface EdgeData {
|
|
27
|
+
source: string;
|
|
28
|
+
target: string;
|
|
29
|
+
sourceType: string;
|
|
30
|
+
targetType: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export class NodeSyncService {
|
|
34
|
+
private projectRoot: string;
|
|
35
|
+
|
|
36
|
+
constructor(projectRoot?: string) {
|
|
37
|
+
this.projectRoot = projectRoot || process.env.MAKECC_PROJECT_PATH || process.cwd();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* 노드 생성/수정 시 파일 동기화
|
|
42
|
+
*/
|
|
43
|
+
async syncNode(node: NodeData): Promise<{ success: boolean; path?: string; error?: string }> {
|
|
44
|
+
try {
|
|
45
|
+
switch (node.type) {
|
|
46
|
+
case 'skill':
|
|
47
|
+
return await this.syncSkillNode(node);
|
|
48
|
+
case 'subagent':
|
|
49
|
+
return await this.syncSubagentNode(node);
|
|
50
|
+
case 'command':
|
|
51
|
+
return await this.syncCommandNode(node);
|
|
52
|
+
case 'hook':
|
|
53
|
+
return await this.syncHookNode(node);
|
|
54
|
+
case 'input':
|
|
55
|
+
case 'output':
|
|
56
|
+
// 입력/출력 노드는 파일 저장 불필요
|
|
57
|
+
return { success: true };
|
|
58
|
+
default:
|
|
59
|
+
return { success: false, error: `Unknown node type: ${node.type}` };
|
|
60
|
+
}
|
|
61
|
+
} catch (error) {
|
|
62
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
63
|
+
return { success: false, error: errorMessage };
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* 노드 삭제 시 파일 삭제
|
|
69
|
+
*/
|
|
70
|
+
async deleteNode(node: NodeData): Promise<{ success: boolean; error?: string }> {
|
|
71
|
+
try {
|
|
72
|
+
switch (node.type) {
|
|
73
|
+
case 'skill':
|
|
74
|
+
if (node.skillId) {
|
|
75
|
+
const skillPath = path.join(this.projectRoot, '.claude', 'skills', node.skillId);
|
|
76
|
+
await fs.rm(skillPath, { recursive: true, force: true });
|
|
77
|
+
}
|
|
78
|
+
break;
|
|
79
|
+
case 'subagent':
|
|
80
|
+
const agentName = this.toKebabCase(node.label);
|
|
81
|
+
const agentPath = path.join(this.projectRoot, '.claude', 'agents', `${agentName}.md`);
|
|
82
|
+
await fs.unlink(agentPath).catch(() => {});
|
|
83
|
+
break;
|
|
84
|
+
case 'command':
|
|
85
|
+
const cmdName = node.commandName || this.toKebabCase(node.label);
|
|
86
|
+
const cmdPath = path.join(this.projectRoot, '.claude', 'commands', `${cmdName}.md`);
|
|
87
|
+
await fs.unlink(cmdPath).catch(() => {});
|
|
88
|
+
break;
|
|
89
|
+
case 'hook':
|
|
90
|
+
await this.removeHookFromSettings(node);
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
return { success: true };
|
|
94
|
+
} catch (error) {
|
|
95
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
96
|
+
return { success: false, error: errorMessage };
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* 엣지 연결 시 관계 업데이트
|
|
102
|
+
*/
|
|
103
|
+
async syncEdge(edge: EdgeData, nodes: NodeData[]): Promise<{ success: boolean; error?: string }> {
|
|
104
|
+
try {
|
|
105
|
+
const sourceNode = nodes.find(n => n.id === edge.source);
|
|
106
|
+
const targetNode = nodes.find(n => n.id === edge.target);
|
|
107
|
+
|
|
108
|
+
if (!sourceNode || !targetNode) {
|
|
109
|
+
return { success: false, error: 'Source or target node not found' };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// 서브에이전트 → 스킬 연결: 서브에이전트의 skills 필드 업데이트
|
|
113
|
+
if (sourceNode.type === 'subagent' && targetNode.type === 'skill') {
|
|
114
|
+
const skills = sourceNode.skills || [];
|
|
115
|
+
const skillId = targetNode.skillId || this.toKebabCase(targetNode.label);
|
|
116
|
+
if (!skills.includes(skillId)) {
|
|
117
|
+
skills.push(skillId);
|
|
118
|
+
sourceNode.skills = skills;
|
|
119
|
+
await this.syncSubagentNode(sourceNode);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// 스킬 → 서브에이전트 연결: 서브에이전트의 skills 필드 업데이트
|
|
124
|
+
if (sourceNode.type === 'skill' && targetNode.type === 'subagent') {
|
|
125
|
+
const skills = targetNode.skills || [];
|
|
126
|
+
const skillId = sourceNode.skillId || this.toKebabCase(sourceNode.label);
|
|
127
|
+
if (!skills.includes(skillId)) {
|
|
128
|
+
skills.push(skillId);
|
|
129
|
+
targetNode.skills = skills;
|
|
130
|
+
await this.syncSubagentNode(targetNode);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return { success: true };
|
|
135
|
+
} catch (error) {
|
|
136
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
137
|
+
return { success: false, error: errorMessage };
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* 엣지 삭제 시 관계 업데이트
|
|
143
|
+
*/
|
|
144
|
+
async removeEdge(edge: EdgeData, nodes: NodeData[]): Promise<{ success: boolean; error?: string }> {
|
|
145
|
+
try {
|
|
146
|
+
const sourceNode = nodes.find(n => n.id === edge.source);
|
|
147
|
+
const targetNode = nodes.find(n => n.id === edge.target);
|
|
148
|
+
|
|
149
|
+
if (!sourceNode || !targetNode) {
|
|
150
|
+
return { success: true }; // 노드가 없으면 무시
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// 서브에이전트에서 스킬 제거
|
|
154
|
+
if (sourceNode.type === 'subagent' && targetNode.type === 'skill') {
|
|
155
|
+
const skillId = targetNode.skillId || this.toKebabCase(targetNode.label);
|
|
156
|
+
sourceNode.skills = (sourceNode.skills || []).filter(s => s !== skillId);
|
|
157
|
+
await this.syncSubagentNode(sourceNode);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (sourceNode.type === 'skill' && targetNode.type === 'subagent') {
|
|
161
|
+
const skillId = sourceNode.skillId || this.toKebabCase(sourceNode.label);
|
|
162
|
+
targetNode.skills = (targetNode.skills || []).filter(s => s !== skillId);
|
|
163
|
+
await this.syncSubagentNode(targetNode);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return { success: true };
|
|
167
|
+
} catch (error) {
|
|
168
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
169
|
+
return { success: false, error: errorMessage };
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ===== Private Methods =====
|
|
174
|
+
|
|
175
|
+
private async syncSkillNode(node: NodeData): Promise<{ success: boolean; path?: string; error?: string }> {
|
|
176
|
+
const skillId = node.skillId || this.toKebabCase(node.label);
|
|
177
|
+
const skillPath = path.join(this.projectRoot, '.claude', 'skills', skillId);
|
|
178
|
+
const skillMdPath = path.join(skillPath, 'SKILL.md');
|
|
179
|
+
|
|
180
|
+
await fs.mkdir(skillPath, { recursive: true });
|
|
181
|
+
|
|
182
|
+
// 기존 파일이 있으면 읽어서 업데이트, 없으면 새로 생성
|
|
183
|
+
let content = '';
|
|
184
|
+
try {
|
|
185
|
+
content = await fs.readFile(skillMdPath, 'utf-8');
|
|
186
|
+
// frontmatter 업데이트
|
|
187
|
+
content = this.updateFrontmatter(content, {
|
|
188
|
+
name: skillId,
|
|
189
|
+
description: node.description || node.label,
|
|
190
|
+
});
|
|
191
|
+
} catch {
|
|
192
|
+
// 새로 생성
|
|
193
|
+
content = `---
|
|
194
|
+
name: ${skillId}
|
|
195
|
+
description: ${node.description || node.label}
|
|
196
|
+
---
|
|
197
|
+
|
|
198
|
+
# ${node.label}
|
|
199
|
+
|
|
200
|
+
## 사용 시점
|
|
201
|
+
이 스킬은 다음 상황에서 사용됩니다:
|
|
202
|
+
- ${node.description || '설명을 추가하세요'}
|
|
203
|
+
|
|
204
|
+
## 사용 방법
|
|
205
|
+
|
|
206
|
+
\`\`\`bash
|
|
207
|
+
# 스킬 사용 방법을 여기에 작성하세요
|
|
208
|
+
\`\`\`
|
|
209
|
+
`;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
await fs.writeFile(skillMdPath, content, 'utf-8');
|
|
213
|
+
return { success: true, path: skillPath };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
private async syncSubagentNode(node: NodeData): Promise<{ success: boolean; path?: string; error?: string }> {
|
|
217
|
+
const agentName = this.toKebabCase(node.label);
|
|
218
|
+
const agentsDir = path.join(this.projectRoot, '.claude', 'agents');
|
|
219
|
+
const agentPath = path.join(agentsDir, `${agentName}.md`);
|
|
220
|
+
|
|
221
|
+
await fs.mkdir(agentsDir, { recursive: true });
|
|
222
|
+
|
|
223
|
+
// Frontmatter 구성
|
|
224
|
+
const frontmatter: Record<string, string> = {
|
|
225
|
+
name: agentName,
|
|
226
|
+
description: node.description || node.label,
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
if (node.tools && node.tools.length > 0) {
|
|
230
|
+
frontmatter.tools = node.tools.join(', ');
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (node.model) {
|
|
234
|
+
frontmatter.model = node.model;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (node.skills && node.skills.length > 0) {
|
|
238
|
+
frontmatter.skills = node.skills.join(', ');
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const frontmatterStr = Object.entries(frontmatter)
|
|
242
|
+
.map(([key, value]) => `${key}: ${value}`)
|
|
243
|
+
.join('\n');
|
|
244
|
+
|
|
245
|
+
const content = `---
|
|
246
|
+
${frontmatterStr}
|
|
247
|
+
---
|
|
248
|
+
|
|
249
|
+
${node.systemPrompt || `You are ${node.label}.
|
|
250
|
+
|
|
251
|
+
${node.description || ''}
|
|
252
|
+
`}
|
|
253
|
+
`;
|
|
254
|
+
|
|
255
|
+
await fs.writeFile(agentPath, content, 'utf-8');
|
|
256
|
+
return { success: true, path: agentPath };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
private async syncCommandNode(node: NodeData): Promise<{ success: boolean; path?: string; error?: string }> {
|
|
260
|
+
const cmdName = node.commandName || this.toKebabCase(node.label);
|
|
261
|
+
const commandsDir = path.join(this.projectRoot, '.claude', 'commands');
|
|
262
|
+
const cmdPath = path.join(commandsDir, `${cmdName}.md`);
|
|
263
|
+
|
|
264
|
+
await fs.mkdir(commandsDir, { recursive: true });
|
|
265
|
+
|
|
266
|
+
const content = node.commandContent || `---
|
|
267
|
+
description: ${node.description || node.label}
|
|
268
|
+
---
|
|
269
|
+
|
|
270
|
+
${node.description || '커맨드 내용을 여기에 작성하세요'}
|
|
271
|
+
|
|
272
|
+
$ARGUMENTS
|
|
273
|
+
`;
|
|
274
|
+
|
|
275
|
+
await fs.writeFile(cmdPath, content, 'utf-8');
|
|
276
|
+
return { success: true, path: cmdPath };
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
private async syncHookNode(node: NodeData): Promise<{ success: boolean; path?: string; error?: string }> {
|
|
280
|
+
const settingsPath = path.join(this.projectRoot, '.claude', 'settings.json');
|
|
281
|
+
|
|
282
|
+
// 기존 settings 읽기
|
|
283
|
+
let settings: Record<string, unknown> = {};
|
|
284
|
+
try {
|
|
285
|
+
const content = await fs.readFile(settingsPath, 'utf-8');
|
|
286
|
+
settings = JSON.parse(content);
|
|
287
|
+
} catch {
|
|
288
|
+
// 파일 없음
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// hooks 섹션 확인/생성
|
|
292
|
+
if (!settings.hooks) {
|
|
293
|
+
settings.hooks = {};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const hooks = settings.hooks as Record<string, unknown[]>;
|
|
297
|
+
const event = node.hookEvent || 'PreToolUse';
|
|
298
|
+
|
|
299
|
+
if (!hooks[event]) {
|
|
300
|
+
hooks[event] = [];
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// 기존 훅 찾기 (같은 matcher로)
|
|
304
|
+
const eventHooks = hooks[event] as Array<{ matcher: string; hooks: Array<{ type: string; command: string }> }>;
|
|
305
|
+
const existingIndex = eventHooks.findIndex(h => h.matcher === (node.hookMatcher || '*'));
|
|
306
|
+
|
|
307
|
+
const hookConfig = {
|
|
308
|
+
matcher: node.hookMatcher || '*',
|
|
309
|
+
hooks: [
|
|
310
|
+
{
|
|
311
|
+
type: 'command',
|
|
312
|
+
command: node.hookCommand || 'echo "Hook triggered"',
|
|
313
|
+
},
|
|
314
|
+
],
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
if (existingIndex >= 0) {
|
|
318
|
+
eventHooks[existingIndex] = hookConfig;
|
|
319
|
+
} else {
|
|
320
|
+
eventHooks.push(hookConfig);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// .claude 디렉토리 생성
|
|
324
|
+
await fs.mkdir(path.dirname(settingsPath), { recursive: true });
|
|
325
|
+
await fs.writeFile(settingsPath, JSON.stringify(settings, null, 2), 'utf-8');
|
|
326
|
+
|
|
327
|
+
return { success: true, path: settingsPath };
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
private async removeHookFromSettings(node: NodeData): Promise<void> {
|
|
331
|
+
const settingsPath = path.join(this.projectRoot, '.claude', 'settings.json');
|
|
332
|
+
|
|
333
|
+
let settings: Record<string, unknown> = {};
|
|
334
|
+
try {
|
|
335
|
+
const content = await fs.readFile(settingsPath, 'utf-8');
|
|
336
|
+
settings = JSON.parse(content);
|
|
337
|
+
} catch {
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const hooks = settings.hooks as Record<string, unknown[]> | undefined;
|
|
342
|
+
if (!hooks) return;
|
|
343
|
+
|
|
344
|
+
const event = node.hookEvent || 'PreToolUse';
|
|
345
|
+
if (!hooks[event]) return;
|
|
346
|
+
|
|
347
|
+
const eventHooks = hooks[event] as Array<{ matcher: string }>;
|
|
348
|
+
hooks[event] = eventHooks.filter(h => h.matcher !== (node.hookMatcher || '*'));
|
|
349
|
+
|
|
350
|
+
await fs.writeFile(settingsPath, JSON.stringify(settings, null, 2), 'utf-8');
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
private updateFrontmatter(content: string, updates: Record<string, string>): string {
|
|
354
|
+
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
355
|
+
if (!frontmatterMatch) {
|
|
356
|
+
// frontmatter 없으면 추가
|
|
357
|
+
const fm = Object.entries(updates)
|
|
358
|
+
.map(([k, v]) => `${k}: ${v}`)
|
|
359
|
+
.join('\n');
|
|
360
|
+
return `---\n${fm}\n---\n\n${content}`;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
let frontmatter = frontmatterMatch[1];
|
|
364
|
+
for (const [key, value] of Object.entries(updates)) {
|
|
365
|
+
const regex = new RegExp(`^${key}:.*$`, 'm');
|
|
366
|
+
if (regex.test(frontmatter)) {
|
|
367
|
+
frontmatter = frontmatter.replace(regex, `${key}: ${value}`);
|
|
368
|
+
} else {
|
|
369
|
+
frontmatter += `\n${key}: ${value}`;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
return content.replace(/^---\n[\s\S]*?\n---/, `---\n${frontmatter}\n---`);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
private toKebabCase(str: string): string {
|
|
377
|
+
return str
|
|
378
|
+
.toLowerCase()
|
|
379
|
+
.replace(/[^a-z0-9가-힣]+/g, '-')
|
|
380
|
+
.replace(/^-|-$/g, '');
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
export const nodeSyncService = new NodeSyncService();
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import Anthropic from '@anthropic-ai/sdk';
|
|
2
2
|
import * as fs from 'fs/promises';
|
|
3
3
|
import * as path from 'path';
|
|
4
|
+
import { existsSync } from 'fs';
|
|
5
|
+
import { spawn } from 'child_process';
|
|
4
6
|
import type { ApiSettings } from './workflowAIService';
|
|
5
7
|
|
|
6
8
|
export interface GeneratedSkill {
|
|
@@ -21,6 +23,24 @@ export interface SkillGenerationResult {
|
|
|
21
23
|
error?: string;
|
|
22
24
|
}
|
|
23
25
|
|
|
26
|
+
export type SkillProgressStep =
|
|
27
|
+
| 'started'
|
|
28
|
+
| 'analyzing'
|
|
29
|
+
| 'designing'
|
|
30
|
+
| 'generating'
|
|
31
|
+
| 'saving'
|
|
32
|
+
| 'installing'
|
|
33
|
+
| 'completed'
|
|
34
|
+
| 'error';
|
|
35
|
+
|
|
36
|
+
export interface SkillProgressEvent {
|
|
37
|
+
step: SkillProgressStep;
|
|
38
|
+
message: string;
|
|
39
|
+
detail?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export type SkillProgressCallback = (event: SkillProgressEvent) => void;
|
|
43
|
+
|
|
24
44
|
const SYSTEM_PROMPT = `You are a Claude Code skill generator. You MUST respond with ONLY a valid JSON object. No markdown, no code blocks, no explanations - just pure JSON.
|
|
25
45
|
|
|
26
46
|
Your response must follow this exact JSON schema:
|
|
@@ -106,14 +126,44 @@ export class SkillGeneratorService {
|
|
|
106
126
|
return new Anthropic({ apiKey: envApiKey });
|
|
107
127
|
}
|
|
108
128
|
|
|
109
|
-
async generate(
|
|
129
|
+
async generate(
|
|
130
|
+
prompt: string,
|
|
131
|
+
settings?: ApiSettings,
|
|
132
|
+
onProgress?: SkillProgressCallback
|
|
133
|
+
): Promise<SkillGenerationResult> {
|
|
134
|
+
const progress = onProgress || (() => {});
|
|
135
|
+
|
|
136
|
+
progress({
|
|
137
|
+
step: 'started',
|
|
138
|
+
message: '✨ 스킬 생성을 시작합니다',
|
|
139
|
+
detail: `요청: "${prompt.slice(0, 50)}${prompt.length > 50 ? '...' : ''}"`,
|
|
140
|
+
});
|
|
141
|
+
|
|
110
142
|
const client = this.getClient(settings);
|
|
111
143
|
|
|
144
|
+
progress({
|
|
145
|
+
step: 'analyzing',
|
|
146
|
+
message: '🔍 요청을 분석하고 있어요',
|
|
147
|
+
detail: '어떤 스킬이 필요한지 파악 중...',
|
|
148
|
+
});
|
|
149
|
+
|
|
112
150
|
const userPrompt = `Create a skill for: "${prompt}"
|
|
113
151
|
|
|
114
152
|
Generate complete, working code. Respond with JSON only.`;
|
|
115
153
|
|
|
116
154
|
try {
|
|
155
|
+
progress({
|
|
156
|
+
step: 'designing',
|
|
157
|
+
message: '📝 스킬 구조를 설계하고 있어요',
|
|
158
|
+
detail: 'AI가 최적의 스킬 구조를 결정 중...',
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
progress({
|
|
162
|
+
step: 'generating',
|
|
163
|
+
message: '⚙️ 코드를 생성하고 있어요',
|
|
164
|
+
detail: 'Python 스크립트와 설정 파일 작성 중...',
|
|
165
|
+
});
|
|
166
|
+
|
|
117
167
|
const response = await client.messages.create({
|
|
118
168
|
model: 'claude-sonnet-4-20250514',
|
|
119
169
|
max_tokens: 8192,
|
|
@@ -132,6 +182,10 @@ Generate complete, working code. Respond with JSON only.`;
|
|
|
132
182
|
}
|
|
133
183
|
|
|
134
184
|
if (!responseText) {
|
|
185
|
+
progress({
|
|
186
|
+
step: 'error',
|
|
187
|
+
message: '❌ AI 응답을 받지 못했습니다',
|
|
188
|
+
});
|
|
135
189
|
return { success: false, error: 'AI 응답을 받지 못했습니다.' };
|
|
136
190
|
}
|
|
137
191
|
|
|
@@ -143,13 +197,52 @@ Generate complete, working code. Respond with JSON only.`;
|
|
|
143
197
|
skill = JSON.parse(fullJson);
|
|
144
198
|
} catch {
|
|
145
199
|
console.error('Failed to parse skill response:', fullJson.slice(0, 500));
|
|
200
|
+
progress({
|
|
201
|
+
step: 'error',
|
|
202
|
+
message: '❌ AI 응답을 파싱할 수 없습니다',
|
|
203
|
+
detail: '다시 시도해주세요',
|
|
204
|
+
});
|
|
146
205
|
return { success: false, error: 'AI 응답을 파싱할 수 없습니다. 다시 시도해주세요.' };
|
|
147
206
|
}
|
|
148
207
|
|
|
208
|
+
progress({
|
|
209
|
+
step: 'saving',
|
|
210
|
+
message: '💾 파일을 저장하고 있어요',
|
|
211
|
+
detail: `${skill.files.length}개 파일 저장 중...`,
|
|
212
|
+
});
|
|
213
|
+
|
|
149
214
|
// 파일 저장
|
|
150
215
|
const skillPath = path.join(this.projectRoot, '.claude', 'skills', skill.skillId);
|
|
151
216
|
await this.saveSkillFiles(skillPath, skill.files);
|
|
152
217
|
|
|
218
|
+
// requirements.txt가 있으면 의존성 설치
|
|
219
|
+
const requirementsPath = path.join(skillPath, 'requirements.txt');
|
|
220
|
+
if (existsSync(requirementsPath)) {
|
|
221
|
+
progress({
|
|
222
|
+
step: 'installing',
|
|
223
|
+
message: '📦 패키지를 설치하고 있어요',
|
|
224
|
+
detail: 'pip install 실행 중...',
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
try {
|
|
228
|
+
await this.installDependencies(requirementsPath, progress);
|
|
229
|
+
} catch (installError) {
|
|
230
|
+
// 설치 실패해도 스킬 생성은 성공으로 처리
|
|
231
|
+
console.error('Dependency installation failed:', installError);
|
|
232
|
+
progress({
|
|
233
|
+
step: 'installing',
|
|
234
|
+
message: '⚠️ 일부 패키지 설치에 실패했어요',
|
|
235
|
+
detail: '나중에 수동으로 설치해주세요',
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
progress({
|
|
241
|
+
step: 'completed',
|
|
242
|
+
message: '✅ 스킬이 생성되었습니다!',
|
|
243
|
+
detail: `${skill.skillName} → ${skillPath}`,
|
|
244
|
+
});
|
|
245
|
+
|
|
153
246
|
return {
|
|
154
247
|
success: true,
|
|
155
248
|
skill,
|
|
@@ -158,6 +251,11 @@ Generate complete, working code. Respond with JSON only.`;
|
|
|
158
251
|
} catch (error) {
|
|
159
252
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
160
253
|
console.error('Skill generation error:', errorMessage);
|
|
254
|
+
progress({
|
|
255
|
+
step: 'error',
|
|
256
|
+
message: '❌ 스킬 생성에 실패했습니다',
|
|
257
|
+
detail: errorMessage,
|
|
258
|
+
});
|
|
161
259
|
return { success: false, error: errorMessage };
|
|
162
260
|
}
|
|
163
261
|
}
|
|
@@ -181,6 +279,107 @@ Generate complete, working code. Respond with JSON only.`;
|
|
|
181
279
|
console.log(`Saved: ${filePath}`);
|
|
182
280
|
}
|
|
183
281
|
}
|
|
282
|
+
|
|
283
|
+
private async installDependencies(
|
|
284
|
+
requirementsPath: string,
|
|
285
|
+
progress: SkillProgressCallback
|
|
286
|
+
): Promise<void> {
|
|
287
|
+
return new Promise((resolve, reject) => {
|
|
288
|
+
const homeDir = process.env.HOME || process.env.USERPROFILE || '';
|
|
289
|
+
const venvPythonPath = path.join(homeDir, '.claude', 'venv', 'bin', 'python');
|
|
290
|
+
|
|
291
|
+
let command: string;
|
|
292
|
+
let args: string[];
|
|
293
|
+
|
|
294
|
+
// uv를 우선 사용 (10-100x 빠름)
|
|
295
|
+
// uv pip install --python <venv-python> -r requirements.txt
|
|
296
|
+
const useUv = this.checkCommandExists('uv');
|
|
297
|
+
|
|
298
|
+
if (useUv && existsSync(venvPythonPath)) {
|
|
299
|
+
command = 'uv';
|
|
300
|
+
args = ['pip', 'install', '--python', venvPythonPath, '-r', requirementsPath];
|
|
301
|
+
progress({
|
|
302
|
+
step: 'installing',
|
|
303
|
+
message: '⚡ uv로 패키지 설치 중 (고속)',
|
|
304
|
+
detail: 'uv pip install 실행 중...',
|
|
305
|
+
});
|
|
306
|
+
} else if (existsSync(path.join(homeDir, '.claude', 'venv', 'bin', 'pip'))) {
|
|
307
|
+
// fallback: 전역 venv pip 사용
|
|
308
|
+
command = path.join(homeDir, '.claude', 'venv', 'bin', 'pip');
|
|
309
|
+
args = ['install', '-r', requirementsPath];
|
|
310
|
+
progress({
|
|
311
|
+
step: 'installing',
|
|
312
|
+
message: '📦 pip으로 패키지 설치 중',
|
|
313
|
+
detail: 'pip install 실행 중...',
|
|
314
|
+
});
|
|
315
|
+
} else {
|
|
316
|
+
// fallback: 시스템 pip 사용
|
|
317
|
+
command = 'pip3';
|
|
318
|
+
args = ['install', '-r', requirementsPath];
|
|
319
|
+
progress({
|
|
320
|
+
step: 'installing',
|
|
321
|
+
message: '📦 pip으로 패키지 설치 중',
|
|
322
|
+
detail: 'pip install 실행 중...',
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
console.log(`Installing dependencies: ${command} ${args.join(' ')}`);
|
|
327
|
+
|
|
328
|
+
const proc = spawn(command, args, {
|
|
329
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
let output = '';
|
|
333
|
+
let errorOutput = '';
|
|
334
|
+
|
|
335
|
+
proc.stdout?.on('data', (data) => {
|
|
336
|
+
output += data.toString();
|
|
337
|
+
const lines = data.toString().trim().split('\n');
|
|
338
|
+
for (const line of lines) {
|
|
339
|
+
if (line.includes('Successfully installed') || line.includes('Requirement already satisfied') || line.includes('Installed')) {
|
|
340
|
+
progress({
|
|
341
|
+
step: 'installing',
|
|
342
|
+
message: '📦 패키지 설치 중',
|
|
343
|
+
detail: line.slice(0, 60) + (line.length > 60 ? '...' : ''),
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
proc.stderr?.on('data', (data) => {
|
|
350
|
+
errorOutput += data.toString();
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
proc.on('close', (code) => {
|
|
354
|
+
if (code === 0) {
|
|
355
|
+
progress({
|
|
356
|
+
step: 'installing',
|
|
357
|
+
message: '✅ 패키지 설치 완료',
|
|
358
|
+
detail: '모든 의존성이 설치되었습니다',
|
|
359
|
+
});
|
|
360
|
+
resolve();
|
|
361
|
+
} else {
|
|
362
|
+
console.error('Package install failed:', errorOutput);
|
|
363
|
+
reject(new Error(`Package install failed with code ${code}`));
|
|
364
|
+
}
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
proc.on('error', (err) => {
|
|
368
|
+
console.error('Failed to start package installer:', err);
|
|
369
|
+
reject(err);
|
|
370
|
+
});
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
private checkCommandExists(cmd: string): boolean {
|
|
375
|
+
try {
|
|
376
|
+
const { execSync } = require('child_process');
|
|
377
|
+
execSync(`which ${cmd}`, { stdio: 'ignore' });
|
|
378
|
+
return true;
|
|
379
|
+
} catch {
|
|
380
|
+
return false;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
184
383
|
}
|
|
185
384
|
|
|
186
385
|
export const skillGeneratorService = new SkillGeneratorService();
|