makecc 0.2.4 → 0.2.7
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-c7WYmobg.js → index-CclsqEDr.js} +42 -42
- package/dist/client/index.html +1 -1
- package/dist/server/index.js +81 -0
- package/dist/server/services/nodeSyncService.js +313 -0
- package/dist/server/services/skillGeneratorService.js +47 -70
- package/package.json +1 -1
- package/server/index.ts +81 -0
- package/server/services/nodeSyncService.ts +384 -0
- package/server/services/skillGeneratorService.ts +47 -70
|
@@ -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();
|
|
@@ -21,87 +21,63 @@ export interface SkillGenerationResult {
|
|
|
21
21
|
error?: string;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
const SYSTEM_PROMPT =
|
|
24
|
+
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
25
|
|
|
26
|
-
|
|
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으로 응답하세요:
|
|
26
|
+
Your response must follow this exact JSON schema:
|
|
71
27
|
|
|
72
28
|
{
|
|
73
|
-
"skillName": "
|
|
74
|
-
"skillId": "skill-id-kebab-case",
|
|
75
|
-
"description": "
|
|
29
|
+
"skillName": "Human readable skill name",
|
|
30
|
+
"skillId": "skill-id-in-kebab-case",
|
|
31
|
+
"description": "Brief description of what this skill does",
|
|
76
32
|
"files": [
|
|
77
33
|
{
|
|
78
34
|
"path": "SKILL.md",
|
|
79
|
-
"content": "SKILL.md
|
|
35
|
+
"content": "Full SKILL.md content here",
|
|
80
36
|
"language": "markdown"
|
|
81
37
|
},
|
|
82
38
|
{
|
|
83
39
|
"path": "scripts/main.py",
|
|
84
|
-
"content": "Python
|
|
40
|
+
"content": "Full Python script content here",
|
|
85
41
|
"language": "python"
|
|
86
42
|
},
|
|
87
43
|
{
|
|
88
44
|
"path": "requirements.txt",
|
|
89
|
-
"content": "
|
|
45
|
+
"content": "package1\\npackage2",
|
|
90
46
|
"language": "text"
|
|
91
47
|
}
|
|
92
48
|
]
|
|
93
49
|
}
|
|
94
50
|
|
|
95
|
-
|
|
51
|
+
SKILL.md must follow this format:
|
|
52
|
+
---
|
|
53
|
+
name: skill-id
|
|
54
|
+
description: One line description
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
# Skill Name
|
|
58
|
+
|
|
59
|
+
## When to use
|
|
60
|
+
- Use case 1
|
|
61
|
+
- Use case 2
|
|
62
|
+
|
|
63
|
+
## Usage
|
|
64
|
+
\`\`\`bash
|
|
65
|
+
~/.claude/venv/bin/python ~/.claude/skills/skill-id/scripts/main.py [args]
|
|
66
|
+
\`\`\`
|
|
67
|
+
|
|
68
|
+
## Parameters
|
|
69
|
+
- \`--param1\`: Description
|
|
70
|
+
|
|
71
|
+
## Example
|
|
72
|
+
[Usage example]
|
|
96
73
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
`;
|
|
74
|
+
RULES:
|
|
75
|
+
1. Generate COMPLETE, WORKING code - no placeholders
|
|
76
|
+
2. Include proper error handling with try-except
|
|
77
|
+
3. Use Korean for user-facing messages
|
|
78
|
+
4. Scripts run with ~/.claude/venv/bin/python
|
|
79
|
+
5. List all required packages in requirements.txt
|
|
80
|
+
6. RESPOND WITH JSON ONLY - NO OTHER TEXT`;
|
|
105
81
|
|
|
106
82
|
export class SkillGeneratorService {
|
|
107
83
|
private projectRoot: string;
|
|
@@ -133,17 +109,19 @@ export class SkillGeneratorService {
|
|
|
133
109
|
async generate(prompt: string, settings?: ApiSettings): Promise<SkillGenerationResult> {
|
|
134
110
|
const client = this.getClient(settings);
|
|
135
111
|
|
|
136
|
-
const userPrompt =
|
|
112
|
+
const userPrompt = `Create a skill for: "${prompt}"
|
|
137
113
|
|
|
138
|
-
|
|
139
|
-
반드시 JSON만 반환하세요.`;
|
|
114
|
+
Generate complete, working code. Respond with JSON only.`;
|
|
140
115
|
|
|
141
116
|
try {
|
|
142
117
|
const response = await client.messages.create({
|
|
143
118
|
model: 'claude-sonnet-4-20250514',
|
|
144
119
|
max_tokens: 8192,
|
|
145
120
|
system: SYSTEM_PROMPT,
|
|
146
|
-
messages: [
|
|
121
|
+
messages: [
|
|
122
|
+
{ role: 'user', content: userPrompt },
|
|
123
|
+
{ role: 'assistant', content: '{' } // Prefill to force JSON
|
|
124
|
+
],
|
|
147
125
|
});
|
|
148
126
|
|
|
149
127
|
let responseText = '';
|
|
@@ -157,15 +135,14 @@ export class SkillGeneratorService {
|
|
|
157
135
|
return { success: false, error: 'AI 응답을 받지 못했습니다.' };
|
|
158
136
|
}
|
|
159
137
|
|
|
160
|
-
//
|
|
161
|
-
const
|
|
162
|
-
const rawJson = jsonMatch ? jsonMatch[1].trim() : responseText.trim();
|
|
138
|
+
// Prefill로 '{'를 보냈으니 응답 앞에 '{'를 붙임
|
|
139
|
+
const fullJson = '{' + responseText;
|
|
163
140
|
|
|
164
141
|
let skill: GeneratedSkill;
|
|
165
142
|
try {
|
|
166
|
-
skill = JSON.parse(
|
|
143
|
+
skill = JSON.parse(fullJson);
|
|
167
144
|
} catch {
|
|
168
|
-
console.error('Failed to parse skill response:',
|
|
145
|
+
console.error('Failed to parse skill response:', fullJson.slice(0, 500));
|
|
169
146
|
return { success: false, error: 'AI 응답을 파싱할 수 없습니다. 다시 시도해주세요.' };
|
|
170
147
|
}
|
|
171
148
|
|