makecc 0.2.16 → 0.2.18
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-Dlx6ST3a.css → index-1V3GQ_8q.css} +1 -1
- package/dist/client/assets/index-j_MJWOjN.js +67 -0
- package/dist/client/index.html +2 -2
- package/dist/server/index.js +11 -6
- package/dist/server/services/claudeCliService.js +1 -1
- package/dist/server/services/claudeService.js +1 -1
- package/dist/server/services/configLoaderService.js +10 -46
- package/dist/server/services/nodeSyncService.js +18 -41
- package/dist/server/services/terminalService.js +1 -1
- package/dist/server/services/workflowAIService.js +169 -25
- package/dist/server/services/workflowExecutionService.js +2 -2
- package/package.json +1 -1
- package/server/index.ts +13 -6
- package/server/services/claudeCliService.ts +1 -1
- package/server/services/claudeService.ts +1 -1
- package/server/services/configLoaderService.ts +13 -62
- package/server/services/nodeSyncService.ts +20 -50
- package/server/services/terminalService.ts +1 -1
- package/server/services/workflowAIService.ts +213 -27
- package/server/services/workflowExecutionService.ts +2 -2
- package/server/types.ts +1 -1
- package/dist/client/assets/index-Bir6nP2a.js +0 -150
package/dist/client/index.html
CHANGED
|
@@ -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>makecc</title>
|
|
8
|
-
<script type="module" crossorigin src="/assets/index-
|
|
9
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-j_MJWOjN.js"></script>
|
|
9
|
+
<link rel="stylesheet" crossorigin href="/assets/index-1V3GQ_8q.css">
|
|
10
10
|
</head>
|
|
11
11
|
<body>
|
|
12
12
|
<div id="root"></div>
|
package/dist/server/index.js
CHANGED
|
@@ -219,8 +219,7 @@ app.post('/api/generate/skill', async (req, res) => {
|
|
|
219
219
|
app.get('/api/load/claude-config', async (req, res) => {
|
|
220
220
|
try {
|
|
221
221
|
const config = await configLoaderService.loadAll();
|
|
222
|
-
console.log(`Loaded config: ${config.skills.length} skills, ${config.
|
|
223
|
-
`${config.commands.length} commands, ${config.hooks.length} hooks`);
|
|
222
|
+
console.log(`Loaded config: ${config.skills.length} skills, ${config.agents.length} agents, ${config.hooks.length} hooks`);
|
|
224
223
|
res.json(config);
|
|
225
224
|
}
|
|
226
225
|
catch (error) {
|
|
@@ -232,7 +231,7 @@ app.get('/api/load/claude-config', async (req, res) => {
|
|
|
232
231
|
// Generate workflow using AI
|
|
233
232
|
app.post('/api/generate/workflow', async (req, res) => {
|
|
234
233
|
try {
|
|
235
|
-
const { prompt } = req.body;
|
|
234
|
+
const { prompt, expand = true } = req.body;
|
|
236
235
|
if (!prompt || typeof prompt !== 'string') {
|
|
237
236
|
return res.status(400).json({ message: 'Prompt is required' });
|
|
238
237
|
}
|
|
@@ -240,12 +239,18 @@ app.post('/api/generate/workflow', async (req, res) => {
|
|
|
240
239
|
const apiMode = req.headers['x-api-mode'] || 'proxy';
|
|
241
240
|
const apiKey = req.headers['x-api-key'];
|
|
242
241
|
const proxyUrl = req.headers['x-proxy-url'];
|
|
243
|
-
console.log('Generating workflow for prompt:', prompt, 'mode:', apiMode);
|
|
244
|
-
const
|
|
242
|
+
console.log('Generating workflow for prompt:', prompt, 'mode:', apiMode, 'expand:', expand);
|
|
243
|
+
const settings = {
|
|
245
244
|
apiMode: apiMode,
|
|
246
245
|
apiKey,
|
|
247
246
|
proxyUrl,
|
|
248
|
-
}
|
|
247
|
+
};
|
|
248
|
+
// expand=true (기본값)이면 재귀적으로 스킬/에이전트 상세 생성
|
|
249
|
+
const result = expand
|
|
250
|
+
? await workflowAIService.generateWithExpansion(prompt, settings, (event) => {
|
|
251
|
+
console.log(`[Workflow] ${event.step}: ${event.message}`);
|
|
252
|
+
})
|
|
253
|
+
: await workflowAIService.generate(prompt, settings);
|
|
249
254
|
console.log('Workflow generated:', result.workflowName);
|
|
250
255
|
res.json(result);
|
|
251
256
|
}
|
|
@@ -161,7 +161,7 @@ function findNewFiles(before, after) {
|
|
|
161
161
|
*/
|
|
162
162
|
export function buildNodePrompt(nodeType, nodeData, previousResults) {
|
|
163
163
|
const lines = [];
|
|
164
|
-
if (nodeType === '
|
|
164
|
+
if (nodeType === 'agent') {
|
|
165
165
|
const role = nodeData.role || 'assistant';
|
|
166
166
|
const description = nodeData.description || '';
|
|
167
167
|
const systemPrompt = nodeData.systemPrompt || '';
|
|
@@ -21,7 +21,7 @@ export class ClaudeService {
|
|
|
21
21
|
switch (node.type) {
|
|
22
22
|
case 'input':
|
|
23
23
|
return this.executeInputNode(node.id, node.data);
|
|
24
|
-
case '
|
|
24
|
+
case 'agent':
|
|
25
25
|
return this.executeSubagentNode(node.id, node.data, onProgress);
|
|
26
26
|
case 'skill':
|
|
27
27
|
return this.executeSkillNode(node.id, node.data);
|
|
@@ -9,13 +9,12 @@ export class ConfigLoaderService {
|
|
|
9
9
|
* .claude/ 디렉토리에서 모든 설정 로드
|
|
10
10
|
*/
|
|
11
11
|
async loadAll() {
|
|
12
|
-
const [skills,
|
|
12
|
+
const [skills, agents, hooks] = await Promise.all([
|
|
13
13
|
this.loadSkills(),
|
|
14
|
-
this.
|
|
15
|
-
this.loadCommands(),
|
|
14
|
+
this.loadAgents(),
|
|
16
15
|
this.loadHooks(),
|
|
17
16
|
]);
|
|
18
|
-
return { skills,
|
|
17
|
+
return { skills, agents, hooks };
|
|
19
18
|
}
|
|
20
19
|
/**
|
|
21
20
|
* .claude/skills/ 에서 스킬 로드
|
|
@@ -52,11 +51,11 @@ export class ConfigLoaderService {
|
|
|
52
51
|
return skills;
|
|
53
52
|
}
|
|
54
53
|
/**
|
|
55
|
-
* .claude/agents/ 에서
|
|
54
|
+
* .claude/agents/ 에서 에이전트 로드
|
|
56
55
|
*/
|
|
57
|
-
async
|
|
56
|
+
async loadAgents() {
|
|
58
57
|
const agentsDir = path.join(this.projectRoot, '.claude', 'agents');
|
|
59
|
-
const
|
|
58
|
+
const agents = [];
|
|
60
59
|
try {
|
|
61
60
|
const entries = await fs.readdir(agentsDir, { withFileTypes: true });
|
|
62
61
|
for (const entry of entries) {
|
|
@@ -66,9 +65,9 @@ export class ConfigLoaderService {
|
|
|
66
65
|
const content = await fs.readFile(agentPath, 'utf-8');
|
|
67
66
|
const parsed = this.parseFrontmatter(content);
|
|
68
67
|
const agentName = entry.name.replace('.md', '');
|
|
69
|
-
|
|
70
|
-
id: `
|
|
71
|
-
type: '
|
|
68
|
+
agents.push({
|
|
69
|
+
id: `agent-${agentName}`,
|
|
70
|
+
type: 'agent',
|
|
72
71
|
label: parsed.frontmatter.name || agentName,
|
|
73
72
|
description: parsed.frontmatter.description || '',
|
|
74
73
|
tools: this.parseList(parsed.frontmatter.tools),
|
|
@@ -86,42 +85,7 @@ export class ConfigLoaderService {
|
|
|
86
85
|
catch {
|
|
87
86
|
// agents 디렉토리가 없으면 빈 배열 반환
|
|
88
87
|
}
|
|
89
|
-
return
|
|
90
|
-
}
|
|
91
|
-
/**
|
|
92
|
-
* .claude/commands/ 에서 커맨드 로드
|
|
93
|
-
*/
|
|
94
|
-
async loadCommands() {
|
|
95
|
-
const commandsDir = path.join(this.projectRoot, '.claude', 'commands');
|
|
96
|
-
const commands = [];
|
|
97
|
-
try {
|
|
98
|
-
const entries = await fs.readdir(commandsDir, { withFileTypes: true });
|
|
99
|
-
for (const entry of entries) {
|
|
100
|
-
if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
101
|
-
const cmdPath = path.join(commandsDir, entry.name);
|
|
102
|
-
try {
|
|
103
|
-
const content = await fs.readFile(cmdPath, 'utf-8');
|
|
104
|
-
const parsed = this.parseFrontmatter(content);
|
|
105
|
-
const cmdName = entry.name.replace('.md', '');
|
|
106
|
-
commands.push({
|
|
107
|
-
id: `command-${cmdName}`,
|
|
108
|
-
type: 'command',
|
|
109
|
-
label: parsed.frontmatter.name || cmdName,
|
|
110
|
-
description: parsed.frontmatter.description || '',
|
|
111
|
-
commandName: cmdName,
|
|
112
|
-
commandContent: content,
|
|
113
|
-
});
|
|
114
|
-
}
|
|
115
|
-
catch {
|
|
116
|
-
// 읽을 수 없으면 스킵
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
catch {
|
|
122
|
-
// commands 디렉토리가 없으면 빈 배열 반환
|
|
123
|
-
}
|
|
124
|
-
return commands;
|
|
88
|
+
return agents;
|
|
125
89
|
}
|
|
126
90
|
/**
|
|
127
91
|
* .claude/settings.json 에서 훅 로드
|
|
@@ -13,10 +13,8 @@ export class NodeSyncService {
|
|
|
13
13
|
switch (node.type) {
|
|
14
14
|
case 'skill':
|
|
15
15
|
return await this.syncSkillNode(node);
|
|
16
|
-
case '
|
|
17
|
-
return await this.
|
|
18
|
-
case 'command':
|
|
19
|
-
return await this.syncCommandNode(node);
|
|
16
|
+
case 'agent':
|
|
17
|
+
return await this.syncAgentNode(node);
|
|
20
18
|
case 'hook':
|
|
21
19
|
return await this.syncHookNode(node);
|
|
22
20
|
case 'input':
|
|
@@ -50,16 +48,11 @@ export class NodeSyncService {
|
|
|
50
48
|
await fs.rm(skillPath, { recursive: true, force: true });
|
|
51
49
|
}
|
|
52
50
|
break;
|
|
53
|
-
case '
|
|
51
|
+
case 'agent':
|
|
54
52
|
const agentName = this.toKebabCase(node.label);
|
|
55
53
|
const agentPath = path.join(this.projectRoot, '.claude', 'agents', `${agentName}.md`);
|
|
56
54
|
await fs.unlink(agentPath).catch(() => { });
|
|
57
55
|
break;
|
|
58
|
-
case 'command':
|
|
59
|
-
const cmdName = node.commandName || this.toKebabCase(node.label);
|
|
60
|
-
const cmdPath = path.join(this.projectRoot, '.claude', 'commands', `${cmdName}.md`);
|
|
61
|
-
await fs.unlink(cmdPath).catch(() => { });
|
|
62
|
-
break;
|
|
63
56
|
case 'hook':
|
|
64
57
|
await this.removeHookFromSettings(node);
|
|
65
58
|
break;
|
|
@@ -76,11 +69,11 @@ export class NodeSyncService {
|
|
|
76
69
|
*/
|
|
77
70
|
async removeReferencesToNode(nodeId, nodeType, allNodes) {
|
|
78
71
|
for (const relatedNode of allNodes) {
|
|
79
|
-
if (relatedNode.type === '
|
|
72
|
+
if (relatedNode.type === 'agent') {
|
|
80
73
|
// 서브에이전트의 skills 배열에서 삭제된 노드 제거
|
|
81
74
|
if (relatedNode.skills?.includes(nodeId)) {
|
|
82
75
|
relatedNode.skills = relatedNode.skills.filter(s => s !== nodeId);
|
|
83
|
-
await this.
|
|
76
|
+
await this.syncAgentNode(relatedNode);
|
|
84
77
|
}
|
|
85
78
|
}
|
|
86
79
|
else if (relatedNode.type === 'skill') {
|
|
@@ -116,13 +109,13 @@ export class NodeSyncService {
|
|
|
116
109
|
const sourceId = this.getNodeIdentifier(sourceNode);
|
|
117
110
|
const targetId = this.getNodeIdentifier(targetNode);
|
|
118
111
|
// 서브에이전트 → 스킬 연결
|
|
119
|
-
if (sourceNode.type === '
|
|
112
|
+
if (sourceNode.type === 'agent' && targetNode.type === 'skill') {
|
|
120
113
|
// 에이전트의 skills 필드 업데이트
|
|
121
114
|
const skills = sourceNode.skills || [];
|
|
122
115
|
if (!skills.includes(targetId)) {
|
|
123
116
|
skills.push(targetId);
|
|
124
117
|
sourceNode.skills = skills;
|
|
125
|
-
await this.
|
|
118
|
+
await this.syncAgentNode(sourceNode);
|
|
126
119
|
}
|
|
127
120
|
// 스킬의 upstream 필드 업데이트
|
|
128
121
|
const upstream = targetNode.upstream || [];
|
|
@@ -150,13 +143,13 @@ export class NodeSyncService {
|
|
|
150
143
|
}
|
|
151
144
|
}
|
|
152
145
|
// 스킬 → 서브에이전트 연결
|
|
153
|
-
if (sourceNode.type === 'skill' && targetNode.type === '
|
|
146
|
+
if (sourceNode.type === 'skill' && targetNode.type === 'agent') {
|
|
154
147
|
// 에이전트의 upstream skills 필드 업데이트
|
|
155
148
|
const skills = targetNode.skills || [];
|
|
156
149
|
if (!skills.includes(sourceId)) {
|
|
157
150
|
skills.push(sourceId);
|
|
158
151
|
targetNode.skills = skills;
|
|
159
|
-
await this.
|
|
152
|
+
await this.syncAgentNode(targetNode);
|
|
160
153
|
}
|
|
161
154
|
// 스킬의 downstream 필드 업데이트
|
|
162
155
|
const downstream = sourceNode.downstream || [];
|
|
@@ -167,13 +160,13 @@ export class NodeSyncService {
|
|
|
167
160
|
}
|
|
168
161
|
}
|
|
169
162
|
// 서브에이전트 → 서브에이전트 연결
|
|
170
|
-
if (sourceNode.type === '
|
|
163
|
+
if (sourceNode.type === 'agent' && targetNode.type === 'agent') {
|
|
171
164
|
// source의 downstream agents
|
|
172
165
|
const sourceDownstream = sourceNode.skills || [];
|
|
173
166
|
if (!sourceDownstream.includes(targetId)) {
|
|
174
167
|
sourceDownstream.push(targetId);
|
|
175
168
|
sourceNode.skills = sourceDownstream;
|
|
176
|
-
await this.
|
|
169
|
+
await this.syncAgentNode(sourceNode);
|
|
177
170
|
}
|
|
178
171
|
}
|
|
179
172
|
return { success: true };
|
|
@@ -205,10 +198,10 @@ export class NodeSyncService {
|
|
|
205
198
|
const sourceId = this.getNodeIdentifier(sourceNode);
|
|
206
199
|
const targetId = this.getNodeIdentifier(targetNode);
|
|
207
200
|
// 서브에이전트 → 스킬 연결 해제
|
|
208
|
-
if (sourceNode.type === '
|
|
201
|
+
if (sourceNode.type === 'agent' && targetNode.type === 'skill') {
|
|
209
202
|
// 에이전트에서 스킬 제거
|
|
210
203
|
sourceNode.skills = (sourceNode.skills || []).filter(s => s !== targetId);
|
|
211
|
-
await this.
|
|
204
|
+
await this.syncAgentNode(sourceNode);
|
|
212
205
|
// 스킬에서 upstream 제거
|
|
213
206
|
targetNode.upstream = (targetNode.upstream || []).filter(s => s !== sourceId);
|
|
214
207
|
await this.syncSkillNode(targetNode);
|
|
@@ -221,16 +214,16 @@ export class NodeSyncService {
|
|
|
221
214
|
await this.syncSkillNode(targetNode);
|
|
222
215
|
}
|
|
223
216
|
// 스킬 → 서브에이전트 연결 해제
|
|
224
|
-
if (sourceNode.type === 'skill' && targetNode.type === '
|
|
217
|
+
if (sourceNode.type === 'skill' && targetNode.type === 'agent') {
|
|
225
218
|
targetNode.skills = (targetNode.skills || []).filter(s => s !== sourceId);
|
|
226
|
-
await this.
|
|
219
|
+
await this.syncAgentNode(targetNode);
|
|
227
220
|
sourceNode.downstream = (sourceNode.downstream || []).filter(s => s !== targetId);
|
|
228
221
|
await this.syncSkillNode(sourceNode);
|
|
229
222
|
}
|
|
230
223
|
// 서브에이전트 → 서브에이전트 연결 해제
|
|
231
|
-
if (sourceNode.type === '
|
|
224
|
+
if (sourceNode.type === 'agent' && targetNode.type === 'agent') {
|
|
232
225
|
sourceNode.skills = (sourceNode.skills || []).filter(s => s !== targetId);
|
|
233
|
-
await this.
|
|
226
|
+
await this.syncAgentNode(sourceNode);
|
|
234
227
|
}
|
|
235
228
|
return { success: true };
|
|
236
229
|
}
|
|
@@ -292,7 +285,7 @@ ${frontmatterStr}
|
|
|
292
285
|
await fs.writeFile(skillMdPath, content, 'utf-8');
|
|
293
286
|
return { success: true, path: skillPath };
|
|
294
287
|
}
|
|
295
|
-
async
|
|
288
|
+
async syncAgentNode(node) {
|
|
296
289
|
const agentName = this.toKebabCase(node.label);
|
|
297
290
|
const agentsDir = path.join(this.projectRoot, '.claude', 'agents');
|
|
298
291
|
const agentPath = path.join(agentsDir, `${agentName}.md`);
|
|
@@ -368,22 +361,6 @@ ${body}
|
|
|
368
361
|
}
|
|
369
362
|
return { frontmatter, body };
|
|
370
363
|
}
|
|
371
|
-
async syncCommandNode(node) {
|
|
372
|
-
const cmdName = node.commandName || this.toKebabCase(node.label);
|
|
373
|
-
const commandsDir = path.join(this.projectRoot, '.claude', 'commands');
|
|
374
|
-
const cmdPath = path.join(commandsDir, `${cmdName}.md`);
|
|
375
|
-
await fs.mkdir(commandsDir, { recursive: true });
|
|
376
|
-
const content = node.commandContent || `---
|
|
377
|
-
description: ${node.description || node.label}
|
|
378
|
-
---
|
|
379
|
-
|
|
380
|
-
${node.description || '커맨드 내용을 여기에 작성하세요'}
|
|
381
|
-
|
|
382
|
-
$ARGUMENTS
|
|
383
|
-
`;
|
|
384
|
-
await fs.writeFile(cmdPath, content, 'utf-8');
|
|
385
|
-
return { success: true, path: cmdPath };
|
|
386
|
-
}
|
|
387
364
|
async syncHookNode(node) {
|
|
388
365
|
const settingsPath = path.join(this.projectRoot, '.claude', 'settings.json');
|
|
389
366
|
// 기존 settings 읽기
|
|
@@ -28,7 +28,7 @@ function generateClaudePrompt(options) {
|
|
|
28
28
|
executionOrder.forEach((node) => {
|
|
29
29
|
if (node.type === 'input')
|
|
30
30
|
return;
|
|
31
|
-
if (node.type === '
|
|
31
|
+
if (node.type === 'agent') {
|
|
32
32
|
const data = node.data;
|
|
33
33
|
lines.push(`${stepNum}. **${data.label}** (${data.role})`);
|
|
34
34
|
if (data.description) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import Anthropic from '@anthropic-ai/sdk';
|
|
2
|
+
import { skillGeneratorService } from './skillGeneratorService';
|
|
2
3
|
// 사용 가능한 공식 스킬 목록
|
|
3
4
|
const AVAILABLE_SKILLS = [
|
|
4
5
|
{ id: 'image-gen-nanobanana', name: 'Image Generator', description: 'Google Gemini 기반 AI 이미지 생성' },
|
|
@@ -22,29 +23,29 @@ const SYSTEM_PROMPT = `당신은 Claude Code 워크플로우 설계 전문가입
|
|
|
22
23
|
## 사용 가능한 공식 스킬
|
|
23
24
|
${AVAILABLE_SKILLS.map(s => `- ${s.id}: ${s.description}`).join('\n')}
|
|
24
25
|
|
|
25
|
-
## 사용 가능한 도구 (
|
|
26
|
+
## 사용 가능한 도구 (agent의 tools에 사용)
|
|
26
27
|
${AVAILABLE_TOOLS.join(', ')}
|
|
27
28
|
|
|
28
29
|
## 응답 형식 (JSON)
|
|
29
30
|
반드시 아래 형식의 유효한 JSON으로 응답하세요. 다른 텍스트 없이 JSON만 반환하세요.
|
|
30
31
|
|
|
31
32
|
{
|
|
32
|
-
"workflowName": "
|
|
33
|
-
"description": "워크플로우 설명",
|
|
33
|
+
"workflowName": "Workflow Name in English",
|
|
34
|
+
"description": "워크플로우 설명 (한글 가능)",
|
|
34
35
|
"nodes": [
|
|
35
36
|
{
|
|
36
37
|
"type": "input",
|
|
37
|
-
"label": "
|
|
38
|
-
"description": "입력 설명",
|
|
38
|
+
"label": "input-name-in-english",
|
|
39
|
+
"description": "입력 설명 (한글 가능)",
|
|
39
40
|
"config": {
|
|
40
41
|
"inputType": "text",
|
|
41
42
|
"placeholder": "입력 안내"
|
|
42
43
|
}
|
|
43
44
|
},
|
|
44
45
|
{
|
|
45
|
-
"type": "
|
|
46
|
-
"label": "
|
|
47
|
-
"description": "에이전트 역할 설명",
|
|
46
|
+
"type": "agent",
|
|
47
|
+
"label": "agent-name-in-english",
|
|
48
|
+
"description": "에이전트 역할 설명 (한글 가능)",
|
|
48
49
|
"config": {
|
|
49
50
|
"role": "researcher|writer|analyst|coder|custom",
|
|
50
51
|
"tools": ["Read", "Write"],
|
|
@@ -54,8 +55,8 @@ ${AVAILABLE_TOOLS.join(', ')}
|
|
|
54
55
|
},
|
|
55
56
|
{
|
|
56
57
|
"type": "skill",
|
|
57
|
-
"label": "
|
|
58
|
-
"description": "스킬 설명",
|
|
58
|
+
"label": "skill-name-in-english",
|
|
59
|
+
"description": "스킬 설명 (한글 가능)",
|
|
59
60
|
"config": {
|
|
60
61
|
"skillType": "official",
|
|
61
62
|
"skillId": "image-gen-nanobanana"
|
|
@@ -63,18 +64,18 @@ ${AVAILABLE_TOOLS.join(', ')}
|
|
|
63
64
|
},
|
|
64
65
|
{
|
|
65
66
|
"type": "skill",
|
|
66
|
-
"label": "
|
|
67
|
-
"description": "커스텀 스킬 설명",
|
|
67
|
+
"label": "custom-skill-name",
|
|
68
|
+
"description": "커스텀 스킬 설명 (한글 가능)",
|
|
68
69
|
"config": {
|
|
69
70
|
"skillType": "custom",
|
|
70
71
|
"skillId": "my-custom-skill",
|
|
71
|
-
"skillContent": "---\\nname: my-custom-skill\\ndescription:
|
|
72
|
+
"skillContent": "---\\nname: my-custom-skill\\ndescription: Custom skill description\\n---\\n\\n# Skill Instructions\\n\\nSpecific instructions..."
|
|
72
73
|
}
|
|
73
74
|
},
|
|
74
75
|
{
|
|
75
76
|
"type": "output",
|
|
76
|
-
"label": "
|
|
77
|
-
"description": "출력 설명",
|
|
77
|
+
"label": "output-name-in-english",
|
|
78
|
+
"description": "출력 설명 (한글 가능)",
|
|
78
79
|
"config": {
|
|
79
80
|
"outputType": "auto|markdown|document|image"
|
|
80
81
|
}
|
|
@@ -90,34 +91,36 @@ ${AVAILABLE_TOOLS.join(', ')}
|
|
|
90
91
|
1. 항상 input 노드로 시작하고 output 노드로 종료
|
|
91
92
|
2. 기존 공식 스킬로 가능하면 official 스킬 사용
|
|
92
93
|
3. 새로운 기능이 필요하면 custom 스킬 생성 (skillContent에 SKILL.md 형식)
|
|
93
|
-
4.
|
|
94
|
+
4. agent는 복잡한 추론이나 다단계 작업에 사용
|
|
94
95
|
5. edges의 from/to는 nodes 배열의 인덱스 (0부터 시작)
|
|
95
96
|
6. 순차적으로 연결되지 않아도 됨 (병렬 처리, 합류 가능)
|
|
96
97
|
7. systemPrompt는 구체적이고 실행 가능한 지시사항으로 작성
|
|
98
|
+
8. **중요: label은 반드시 영어로, kebab-case 형식으로 작성 (예: blog-writer, data-analyzer)**
|
|
99
|
+
9. workflowName도 영어로 작성
|
|
97
100
|
|
|
98
101
|
## 예시
|
|
99
102
|
|
|
100
103
|
### 예시 1: "이미지 3개 만들어줘"
|
|
101
104
|
{
|
|
102
|
-
"workflowName": "
|
|
105
|
+
"workflowName": "Image Generation",
|
|
103
106
|
"description": "3개의 이미지를 생성하는 워크플로우",
|
|
104
107
|
"nodes": [
|
|
105
|
-
{ "type": "input", "label": "
|
|
106
|
-
{ "type": "skill", "label": "
|
|
107
|
-
{ "type": "output", "label": "
|
|
108
|
+
{ "type": "input", "label": "image-prompt", "description": "생성할 이미지에 대한 설명", "config": { "inputType": "text", "placeholder": "이미지 프롬프트 입력" } },
|
|
109
|
+
{ "type": "skill", "label": "image-generator", "description": "AI로 이미지 생성", "config": { "skillType": "official", "skillId": "image-gen-nanobanana" } },
|
|
110
|
+
{ "type": "output", "label": "generated-images", "description": "생성된 이미지 결과", "config": { "outputType": "image" } }
|
|
108
111
|
],
|
|
109
112
|
"edges": [{ "from": 0, "to": 1 }, { "from": 1, "to": 2 }]
|
|
110
113
|
}
|
|
111
114
|
|
|
112
115
|
### 예시 2: "데이터 분석해서 보고서 만들어줘"
|
|
113
116
|
{
|
|
114
|
-
"workflowName": "
|
|
117
|
+
"workflowName": "Data Analysis Report",
|
|
115
118
|
"description": "데이터를 분석하고 보고서를 작성하는 워크플로우",
|
|
116
119
|
"nodes": [
|
|
117
|
-
{ "type": "input", "label": "
|
|
118
|
-
{ "type": "
|
|
119
|
-
{ "type": "
|
|
120
|
-
{ "type": "output", "label": "
|
|
120
|
+
{ "type": "input", "label": "data-file", "description": "분석할 데이터 파일", "config": { "inputType": "file" } },
|
|
121
|
+
{ "type": "agent", "label": "data-analyzer", "description": "데이터를 분석하고 인사이트 도출", "config": { "role": "analyst", "tools": ["Read", "Grep", "Glob"], "model": "sonnet", "systemPrompt": "주어진 데이터를 분석하여 핵심 인사이트를 도출하세요. 통계적 요약, 트렌드, 이상치를 파악하세요." } },
|
|
122
|
+
{ "type": "agent", "label": "report-writer", "description": "분석 결과로 보고서 작성", "config": { "role": "writer", "tools": ["Read", "Write"], "model": "opus", "systemPrompt": "분석 결과를 바탕으로 경영진을 위한 간결하고 명확한 보고서를 작성하세요." } },
|
|
123
|
+
{ "type": "output", "label": "analysis-report", "description": "최종 분석 보고서", "config": { "outputType": "document" } }
|
|
121
124
|
],
|
|
122
125
|
"edges": [{ "from": 0, "to": 1 }, { "from": 1, "to": 2 }, { "from": 2, "to": 3 }]
|
|
123
126
|
}
|
|
@@ -211,5 +214,146 @@ export class WorkflowAIService {
|
|
|
211
214
|
edge.to < result.nodes.length);
|
|
212
215
|
return result;
|
|
213
216
|
}
|
|
217
|
+
/**
|
|
218
|
+
* 워크플로우 생성 + 재귀적 노드 확장
|
|
219
|
+
* 각 custom 스킬과 에이전트에 대해 상세 내용 생성
|
|
220
|
+
*/
|
|
221
|
+
async generateWithExpansion(prompt, settings, onProgress) {
|
|
222
|
+
// 1. 워크플로우 구조 생성
|
|
223
|
+
onProgress?.({ step: 'workflow', message: '워크플로우 구조를 생성하고 있습니다...' });
|
|
224
|
+
const result = await this.generate(prompt, settings);
|
|
225
|
+
// 2. 확장이 필요한 노드 식별
|
|
226
|
+
const customSkills = result.nodes.filter((n) => n.type === 'skill' && n.config.skillType === 'custom');
|
|
227
|
+
const agents = result.nodes.filter((n) => n.type === 'agent');
|
|
228
|
+
const totalExpansions = customSkills.length + agents.length;
|
|
229
|
+
let current = 0;
|
|
230
|
+
// 3. 각 custom 스킬 확장
|
|
231
|
+
for (const skill of customSkills) {
|
|
232
|
+
current++;
|
|
233
|
+
onProgress?.({
|
|
234
|
+
step: 'skill',
|
|
235
|
+
message: `스킬 "${skill.label}" 상세 생성 중...`,
|
|
236
|
+
current,
|
|
237
|
+
total: totalExpansions,
|
|
238
|
+
});
|
|
239
|
+
try {
|
|
240
|
+
// skillGeneratorService를 사용하여 완전한 스킬 생성
|
|
241
|
+
const skillPrompt = this.buildSkillPrompt(skill, result);
|
|
242
|
+
const skillResult = await skillGeneratorService.generate(skillPrompt, settings);
|
|
243
|
+
if (skillResult.success && skillResult.skill) {
|
|
244
|
+
// 생성된 스킬 정보로 노드 업데이트
|
|
245
|
+
skill.config.skillId = skillResult.skill.skillId;
|
|
246
|
+
skill.config.skillContent = undefined; // 파일로 저장되었으므로 제거
|
|
247
|
+
// savedPath는 로그로만 출력 (타입에 없음)
|
|
248
|
+
console.log(`Skill saved to: ${skillResult.savedPath}`);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
catch (error) {
|
|
252
|
+
console.error(`Failed to expand skill ${skill.label}:`, error);
|
|
253
|
+
// 실패해도 계속 진행
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
// 4. 각 에이전트 확장 (상세 systemPrompt 생성)
|
|
257
|
+
for (const agent of agents) {
|
|
258
|
+
current++;
|
|
259
|
+
onProgress?.({
|
|
260
|
+
step: 'agent',
|
|
261
|
+
message: `에이전트 "${agent.label}" 상세 생성 중...`,
|
|
262
|
+
current,
|
|
263
|
+
total: totalExpansions,
|
|
264
|
+
});
|
|
265
|
+
try {
|
|
266
|
+
const expandedPrompt = await this.expandAgentPrompt(agent, result, settings);
|
|
267
|
+
agent.config.systemPrompt = expandedPrompt;
|
|
268
|
+
}
|
|
269
|
+
catch (error) {
|
|
270
|
+
console.error(`Failed to expand agent ${agent.label}:`, error);
|
|
271
|
+
// 실패해도 계속 진행
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
onProgress?.({ step: 'completed', message: '워크플로우 생성 완료!' });
|
|
275
|
+
return result;
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* 스킬 생성을 위한 상세 프롬프트 빌드
|
|
279
|
+
*/
|
|
280
|
+
buildSkillPrompt(skill, workflow) {
|
|
281
|
+
// 워크플로우에서 이 스킬의 연결 관계 파악
|
|
282
|
+
const skillIndex = workflow.nodes.indexOf(skill);
|
|
283
|
+
const upstreamNodes = workflow.edges
|
|
284
|
+
.filter((e) => e.to === skillIndex)
|
|
285
|
+
.map((e) => workflow.nodes[e.from]);
|
|
286
|
+
const downstreamNodes = workflow.edges
|
|
287
|
+
.filter((e) => e.from === skillIndex)
|
|
288
|
+
.map((e) => workflow.nodes[e.to]);
|
|
289
|
+
const contextParts = [
|
|
290
|
+
`스킬 이름: ${skill.label}`,
|
|
291
|
+
`설명: ${skill.description}`,
|
|
292
|
+
`워크플로우: ${workflow.workflowName}`,
|
|
293
|
+
];
|
|
294
|
+
if (upstreamNodes.length > 0) {
|
|
295
|
+
contextParts.push(`이전 단계: ${upstreamNodes.map((n) => `${n.label} (${n.type})`).join(', ')}`);
|
|
296
|
+
}
|
|
297
|
+
if (downstreamNodes.length > 0) {
|
|
298
|
+
contextParts.push(`다음 단계: ${downstreamNodes.map((n) => `${n.label} (${n.type})`).join(', ')}`);
|
|
299
|
+
}
|
|
300
|
+
if (skill.config.skillContent) {
|
|
301
|
+
contextParts.push(`기본 내용:\n${skill.config.skillContent}`);
|
|
302
|
+
}
|
|
303
|
+
return `다음 스킬을 생성해주세요:\n\n${contextParts.join('\n')}`;
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* 에이전트의 systemPrompt를 상세하게 확장
|
|
307
|
+
*/
|
|
308
|
+
async expandAgentPrompt(agent, workflow, settings) {
|
|
309
|
+
const client = this.getClient(settings);
|
|
310
|
+
// 워크플로우에서 이 에이전트의 연결 관계 파악
|
|
311
|
+
const agentIndex = workflow.nodes.indexOf(agent);
|
|
312
|
+
const upstreamNodes = workflow.edges
|
|
313
|
+
.filter((e) => e.to === agentIndex)
|
|
314
|
+
.map((e) => workflow.nodes[e.from]);
|
|
315
|
+
const downstreamNodes = workflow.edges
|
|
316
|
+
.filter((e) => e.from === agentIndex)
|
|
317
|
+
.map((e) => workflow.nodes[e.to]);
|
|
318
|
+
const systemPrompt = `You are an expert at writing detailed system prompts for AI agents.
|
|
319
|
+
Given an agent's context, generate a comprehensive system prompt that:
|
|
320
|
+
1. Clearly defines the agent's role and responsibilities
|
|
321
|
+
2. Specifies input/output expectations
|
|
322
|
+
3. Provides step-by-step instructions
|
|
323
|
+
4. Includes best practices and constraints
|
|
324
|
+
5. Is written in Korean for user-facing parts
|
|
325
|
+
|
|
326
|
+
Respond with ONLY the system prompt text, no explanations or formatting.`;
|
|
327
|
+
const userPrompt = `워크플로우: ${workflow.workflowName}
|
|
328
|
+
워크플로우 설명: ${workflow.description}
|
|
329
|
+
|
|
330
|
+
에이전트 정보:
|
|
331
|
+
- 이름: ${agent.label}
|
|
332
|
+
- 설명: ${agent.description}
|
|
333
|
+
- 역할: ${agent.config.role || 'custom'}
|
|
334
|
+
- 도구: ${(agent.config.tools || []).join(', ')}
|
|
335
|
+
- 모델: ${agent.config.model || 'sonnet'}
|
|
336
|
+
|
|
337
|
+
${upstreamNodes.length > 0 ? `이전 단계에서 받는 입력:\n${upstreamNodes.map((n) => `- ${n.label}: ${n.description}`).join('\n')}` : ''}
|
|
338
|
+
|
|
339
|
+
${downstreamNodes.length > 0 ? `다음 단계로 전달할 출력:\n${downstreamNodes.map((n) => `- ${n.label}: ${n.description}`).join('\n')}` : ''}
|
|
340
|
+
|
|
341
|
+
${agent.config.systemPrompt ? `기존 프롬프트 (확장 필요):\n${agent.config.systemPrompt}` : ''}
|
|
342
|
+
|
|
343
|
+
이 에이전트를 위한 상세하고 실행 가능한 system prompt를 작성해주세요.`;
|
|
344
|
+
const response = await client.messages.create({
|
|
345
|
+
model: 'claude-sonnet-4-20250514',
|
|
346
|
+
max_tokens: 2048,
|
|
347
|
+
system: systemPrompt,
|
|
348
|
+
messages: [{ role: 'user', content: userPrompt }],
|
|
349
|
+
});
|
|
350
|
+
let result = '';
|
|
351
|
+
for (const block of response.content) {
|
|
352
|
+
if (block.type === 'text') {
|
|
353
|
+
result += block.text;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
return result.trim() || agent.config.systemPrompt || agent.description;
|
|
357
|
+
}
|
|
214
358
|
}
|
|
215
359
|
export const workflowAIService = new WorkflowAIService();
|
|
@@ -57,7 +57,7 @@ export class WorkflowExecutionService {
|
|
|
57
57
|
switch (node.type) {
|
|
58
58
|
case 'input':
|
|
59
59
|
return this.executeInputNode(node, context.inputs);
|
|
60
|
-
case '
|
|
60
|
+
case 'agent':
|
|
61
61
|
return this.executeSubagentNode(node, previousResults, onProgress, onLog);
|
|
62
62
|
case 'skill':
|
|
63
63
|
return this.executeSkillNode(node, previousResults, onProgress, onLog);
|
|
@@ -89,7 +89,7 @@ export class WorkflowExecutionService {
|
|
|
89
89
|
onProgress?.({ nodeId: node.id, status: 'running', progress: 20 });
|
|
90
90
|
onLog?.('info', `claude -c 실행 중: ${data.label} (${data.role})`);
|
|
91
91
|
// 프롬프트 생성
|
|
92
|
-
const prompt = buildNodePrompt('
|
|
92
|
+
const prompt = buildNodePrompt('agent', data, previousResults);
|
|
93
93
|
try {
|
|
94
94
|
onProgress?.({ nodeId: node.id, status: 'running', progress: 40 });
|
|
95
95
|
const result = await executeClaudeCli({
|