makecc 0.2.15 → 0.2.17
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-1V3GQ_8q.css +1 -0
- package/dist/client/assets/index-CzDuVmB7.js +67 -0
- package/dist/client/index.html +2 -2
- package/dist/server/index.js +3 -3
- package/dist/server/services/claudeCliService.js +1 -1
- package/dist/server/services/claudeService.js +1 -1
- package/dist/server/services/configLoaderService.js +7 -7
- package/dist/server/services/nodeSyncService.js +101 -46
- package/dist/server/services/terminalService.js +1 -1
- package/dist/server/services/workflowAIService.js +5 -5
- package/dist/server/services/workflowExecutionService.js +2 -2
- package/package.json +1 -1
- package/server/index.ts +3 -3
- package/server/services/claudeCliService.ts +1 -1
- package/server/services/claudeService.ts +1 -1
- package/server/services/configLoaderService.ts +9 -9
- package/server/services/nodeSyncService.ts +107 -53
- package/server/services/terminalService.ts +1 -1
- package/server/services/workflowAIService.ts +7 -7
- package/server/services/workflowExecutionService.ts +2 -2
- package/server/types.ts +1 -1
- package/dist/client/assets/index-CXXgu628.css +0 -1
- package/dist/client/assets/index-PJfSWqWr.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-CzDuVmB7.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
|
@@ -128,11 +128,11 @@ app.post('/api/sync/node', async (req, res) => {
|
|
|
128
128
|
// Delete node from file system
|
|
129
129
|
app.delete('/api/sync/node', async (req, res) => {
|
|
130
130
|
try {
|
|
131
|
-
const { node } = req.body;
|
|
131
|
+
const { node, nodes } = req.body;
|
|
132
132
|
if (!node) {
|
|
133
133
|
return res.status(400).json({ message: 'Node data is required' });
|
|
134
134
|
}
|
|
135
|
-
const result = await nodeSyncService.deleteNode(node);
|
|
135
|
+
const result = await nodeSyncService.deleteNode(node, nodes);
|
|
136
136
|
if (result.success) {
|
|
137
137
|
res.json(result);
|
|
138
138
|
}
|
|
@@ -219,7 +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.
|
|
222
|
+
console.log(`Loaded config: ${config.skills.length} skills, ${config.agents.length} agents, ` +
|
|
223
223
|
`${config.commands.length} commands, ${config.hooks.length} hooks`);
|
|
224
224
|
res.json(config);
|
|
225
225
|
}
|
|
@@ -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,13 @@ export class ConfigLoaderService {
|
|
|
9
9
|
* .claude/ 디렉토리에서 모든 설정 로드
|
|
10
10
|
*/
|
|
11
11
|
async loadAll() {
|
|
12
|
-
const [skills,
|
|
12
|
+
const [skills, agents, commands, hooks] = await Promise.all([
|
|
13
13
|
this.loadSkills(),
|
|
14
14
|
this.loadSubagents(),
|
|
15
15
|
this.loadCommands(),
|
|
16
16
|
this.loadHooks(),
|
|
17
17
|
]);
|
|
18
|
-
return { skills,
|
|
18
|
+
return { skills, agents, commands, hooks };
|
|
19
19
|
}
|
|
20
20
|
/**
|
|
21
21
|
* .claude/skills/ 에서 스킬 로드
|
|
@@ -56,7 +56,7 @@ export class ConfigLoaderService {
|
|
|
56
56
|
*/
|
|
57
57
|
async loadSubagents() {
|
|
58
58
|
const agentsDir = path.join(this.projectRoot, '.claude', 'agents');
|
|
59
|
-
const
|
|
59
|
+
const agents = [];
|
|
60
60
|
try {
|
|
61
61
|
const entries = await fs.readdir(agentsDir, { withFileTypes: true });
|
|
62
62
|
for (const entry of entries) {
|
|
@@ -66,9 +66,9 @@ export class ConfigLoaderService {
|
|
|
66
66
|
const content = await fs.readFile(agentPath, 'utf-8');
|
|
67
67
|
const parsed = this.parseFrontmatter(content);
|
|
68
68
|
const agentName = entry.name.replace('.md', '');
|
|
69
|
-
|
|
70
|
-
id: `
|
|
71
|
-
type: '
|
|
69
|
+
agents.push({
|
|
70
|
+
id: `agent-${agentName}`,
|
|
71
|
+
type: 'agent',
|
|
72
72
|
label: parsed.frontmatter.name || agentName,
|
|
73
73
|
description: parsed.frontmatter.description || '',
|
|
74
74
|
tools: this.parseList(parsed.frontmatter.tools),
|
|
@@ -86,7 +86,7 @@ export class ConfigLoaderService {
|
|
|
86
86
|
catch {
|
|
87
87
|
// agents 디렉토리가 없으면 빈 배열 반환
|
|
88
88
|
}
|
|
89
|
-
return
|
|
89
|
+
return agents;
|
|
90
90
|
}
|
|
91
91
|
/**
|
|
92
92
|
* .claude/commands/ 에서 커맨드 로드
|
|
@@ -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':
|
|
@@ -33,10 +31,16 @@ export class NodeSyncService {
|
|
|
33
31
|
}
|
|
34
32
|
}
|
|
35
33
|
/**
|
|
36
|
-
* 노드 삭제 시 파일 삭제
|
|
34
|
+
* 노드 삭제 시 파일 삭제 및 관련 참조 정리
|
|
37
35
|
*/
|
|
38
|
-
async deleteNode(node) {
|
|
36
|
+
async deleteNode(node, allNodes) {
|
|
39
37
|
try {
|
|
38
|
+
const nodeId = this.getNodeIdentifier(node);
|
|
39
|
+
// 먼저 관련 노드들에서 참조 제거
|
|
40
|
+
if (allNodes) {
|
|
41
|
+
await this.removeReferencesToNode(nodeId, node.type, allNodes);
|
|
42
|
+
}
|
|
43
|
+
// 파일 삭제
|
|
40
44
|
switch (node.type) {
|
|
41
45
|
case 'skill':
|
|
42
46
|
if (node.skillId) {
|
|
@@ -44,16 +48,11 @@ export class NodeSyncService {
|
|
|
44
48
|
await fs.rm(skillPath, { recursive: true, force: true });
|
|
45
49
|
}
|
|
46
50
|
break;
|
|
47
|
-
case '
|
|
51
|
+
case 'agent':
|
|
48
52
|
const agentName = this.toKebabCase(node.label);
|
|
49
53
|
const agentPath = path.join(this.projectRoot, '.claude', 'agents', `${agentName}.md`);
|
|
50
54
|
await fs.unlink(agentPath).catch(() => { });
|
|
51
55
|
break;
|
|
52
|
-
case 'command':
|
|
53
|
-
const cmdName = node.commandName || this.toKebabCase(node.label);
|
|
54
|
-
const cmdPath = path.join(this.projectRoot, '.claude', 'commands', `${cmdName}.md`);
|
|
55
|
-
await fs.unlink(cmdPath).catch(() => { });
|
|
56
|
-
break;
|
|
57
56
|
case 'hook':
|
|
58
57
|
await this.removeHookFromSettings(node);
|
|
59
58
|
break;
|
|
@@ -65,6 +64,36 @@ export class NodeSyncService {
|
|
|
65
64
|
return { success: false, error: errorMessage };
|
|
66
65
|
}
|
|
67
66
|
}
|
|
67
|
+
/**
|
|
68
|
+
* 삭제되는 노드에 대한 모든 참조를 관련 노드에서 제거
|
|
69
|
+
*/
|
|
70
|
+
async removeReferencesToNode(nodeId, nodeType, allNodes) {
|
|
71
|
+
for (const relatedNode of allNodes) {
|
|
72
|
+
if (relatedNode.type === 'agent') {
|
|
73
|
+
// 서브에이전트의 skills 배열에서 삭제된 노드 제거
|
|
74
|
+
if (relatedNode.skills?.includes(nodeId)) {
|
|
75
|
+
relatedNode.skills = relatedNode.skills.filter(s => s !== nodeId);
|
|
76
|
+
await this.syncAgentNode(relatedNode);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
else if (relatedNode.type === 'skill') {
|
|
80
|
+
let updated = false;
|
|
81
|
+
// 스킬의 upstream에서 삭제된 노드 제거
|
|
82
|
+
if (relatedNode.upstream?.includes(nodeId)) {
|
|
83
|
+
relatedNode.upstream = relatedNode.upstream.filter(s => s !== nodeId);
|
|
84
|
+
updated = true;
|
|
85
|
+
}
|
|
86
|
+
// 스킬의 downstream에서 삭제된 노드 제거
|
|
87
|
+
if (relatedNode.downstream?.includes(nodeId)) {
|
|
88
|
+
relatedNode.downstream = relatedNode.downstream.filter(s => s !== nodeId);
|
|
89
|
+
updated = true;
|
|
90
|
+
}
|
|
91
|
+
if (updated) {
|
|
92
|
+
await this.syncSkillNode(relatedNode);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
68
97
|
/**
|
|
69
98
|
* 엣지 연결 시 관계 업데이트
|
|
70
99
|
* - source의 downstream에 target 추가
|
|
@@ -80,13 +109,13 @@ export class NodeSyncService {
|
|
|
80
109
|
const sourceId = this.getNodeIdentifier(sourceNode);
|
|
81
110
|
const targetId = this.getNodeIdentifier(targetNode);
|
|
82
111
|
// 서브에이전트 → 스킬 연결
|
|
83
|
-
if (sourceNode.type === '
|
|
112
|
+
if (sourceNode.type === 'agent' && targetNode.type === 'skill') {
|
|
84
113
|
// 에이전트의 skills 필드 업데이트
|
|
85
114
|
const skills = sourceNode.skills || [];
|
|
86
115
|
if (!skills.includes(targetId)) {
|
|
87
116
|
skills.push(targetId);
|
|
88
117
|
sourceNode.skills = skills;
|
|
89
|
-
await this.
|
|
118
|
+
await this.syncAgentNode(sourceNode);
|
|
90
119
|
}
|
|
91
120
|
// 스킬의 upstream 필드 업데이트
|
|
92
121
|
const upstream = targetNode.upstream || [];
|
|
@@ -114,13 +143,13 @@ export class NodeSyncService {
|
|
|
114
143
|
}
|
|
115
144
|
}
|
|
116
145
|
// 스킬 → 서브에이전트 연결
|
|
117
|
-
if (sourceNode.type === 'skill' && targetNode.type === '
|
|
146
|
+
if (sourceNode.type === 'skill' && targetNode.type === 'agent') {
|
|
118
147
|
// 에이전트의 upstream skills 필드 업데이트
|
|
119
148
|
const skills = targetNode.skills || [];
|
|
120
149
|
if (!skills.includes(sourceId)) {
|
|
121
150
|
skills.push(sourceId);
|
|
122
151
|
targetNode.skills = skills;
|
|
123
|
-
await this.
|
|
152
|
+
await this.syncAgentNode(targetNode);
|
|
124
153
|
}
|
|
125
154
|
// 스킬의 downstream 필드 업데이트
|
|
126
155
|
const downstream = sourceNode.downstream || [];
|
|
@@ -131,13 +160,13 @@ export class NodeSyncService {
|
|
|
131
160
|
}
|
|
132
161
|
}
|
|
133
162
|
// 서브에이전트 → 서브에이전트 연결
|
|
134
|
-
if (sourceNode.type === '
|
|
163
|
+
if (sourceNode.type === 'agent' && targetNode.type === 'agent') {
|
|
135
164
|
// source의 downstream agents
|
|
136
165
|
const sourceDownstream = sourceNode.skills || [];
|
|
137
166
|
if (!sourceDownstream.includes(targetId)) {
|
|
138
167
|
sourceDownstream.push(targetId);
|
|
139
168
|
sourceNode.skills = sourceDownstream;
|
|
140
|
-
await this.
|
|
169
|
+
await this.syncAgentNode(sourceNode);
|
|
141
170
|
}
|
|
142
171
|
}
|
|
143
172
|
return { success: true };
|
|
@@ -169,10 +198,10 @@ export class NodeSyncService {
|
|
|
169
198
|
const sourceId = this.getNodeIdentifier(sourceNode);
|
|
170
199
|
const targetId = this.getNodeIdentifier(targetNode);
|
|
171
200
|
// 서브에이전트 → 스킬 연결 해제
|
|
172
|
-
if (sourceNode.type === '
|
|
201
|
+
if (sourceNode.type === 'agent' && targetNode.type === 'skill') {
|
|
173
202
|
// 에이전트에서 스킬 제거
|
|
174
203
|
sourceNode.skills = (sourceNode.skills || []).filter(s => s !== targetId);
|
|
175
|
-
await this.
|
|
204
|
+
await this.syncAgentNode(sourceNode);
|
|
176
205
|
// 스킬에서 upstream 제거
|
|
177
206
|
targetNode.upstream = (targetNode.upstream || []).filter(s => s !== sourceId);
|
|
178
207
|
await this.syncSkillNode(targetNode);
|
|
@@ -185,16 +214,16 @@ export class NodeSyncService {
|
|
|
185
214
|
await this.syncSkillNode(targetNode);
|
|
186
215
|
}
|
|
187
216
|
// 스킬 → 서브에이전트 연결 해제
|
|
188
|
-
if (sourceNode.type === 'skill' && targetNode.type === '
|
|
217
|
+
if (sourceNode.type === 'skill' && targetNode.type === 'agent') {
|
|
189
218
|
targetNode.skills = (targetNode.skills || []).filter(s => s !== sourceId);
|
|
190
|
-
await this.
|
|
219
|
+
await this.syncAgentNode(targetNode);
|
|
191
220
|
sourceNode.downstream = (sourceNode.downstream || []).filter(s => s !== targetId);
|
|
192
221
|
await this.syncSkillNode(sourceNode);
|
|
193
222
|
}
|
|
194
223
|
// 서브에이전트 → 서브에이전트 연결 해제
|
|
195
|
-
if (sourceNode.type === '
|
|
224
|
+
if (sourceNode.type === 'agent' && targetNode.type === 'agent') {
|
|
196
225
|
sourceNode.skills = (sourceNode.skills || []).filter(s => s !== targetId);
|
|
197
|
-
await this.
|
|
226
|
+
await this.syncAgentNode(sourceNode);
|
|
198
227
|
}
|
|
199
228
|
return { success: true };
|
|
200
229
|
}
|
|
@@ -256,15 +285,29 @@ ${frontmatterStr}
|
|
|
256
285
|
await fs.writeFile(skillMdPath, content, 'utf-8');
|
|
257
286
|
return { success: true, path: skillPath };
|
|
258
287
|
}
|
|
259
|
-
async
|
|
288
|
+
async syncAgentNode(node) {
|
|
260
289
|
const agentName = this.toKebabCase(node.label);
|
|
261
290
|
const agentsDir = path.join(this.projectRoot, '.claude', 'agents');
|
|
262
291
|
const agentPath = path.join(agentsDir, `${agentName}.md`);
|
|
263
292
|
await fs.mkdir(agentsDir, { recursive: true });
|
|
264
|
-
//
|
|
293
|
+
// 기존 파일 읽기 시도
|
|
294
|
+
let existingContent = '';
|
|
295
|
+
let existingFrontmatter = {};
|
|
296
|
+
let existingBody = '';
|
|
297
|
+
try {
|
|
298
|
+
existingContent = await fs.readFile(agentPath, 'utf-8');
|
|
299
|
+
const parsed = this.parseFrontmatter(existingContent);
|
|
300
|
+
existingFrontmatter = parsed.frontmatter;
|
|
301
|
+
existingBody = parsed.body;
|
|
302
|
+
}
|
|
303
|
+
catch {
|
|
304
|
+
// 파일 없음 - 새로 생성
|
|
305
|
+
}
|
|
306
|
+
// Frontmatter 구성 - 기존 값 유지하면서 업데이트
|
|
265
307
|
const frontmatter = {
|
|
308
|
+
...existingFrontmatter,
|
|
266
309
|
name: agentName,
|
|
267
|
-
description: node.description || node.label,
|
|
310
|
+
description: node.description || existingFrontmatter.description || node.label,
|
|
268
311
|
};
|
|
269
312
|
if (node.tools && node.tools.length > 0) {
|
|
270
313
|
frontmatter.tools = node.tools.join(', ');
|
|
@@ -272,39 +315,51 @@ ${frontmatterStr}
|
|
|
272
315
|
if (node.model) {
|
|
273
316
|
frontmatter.model = node.model;
|
|
274
317
|
}
|
|
318
|
+
// skills 필드 처리 - 기존 값과 새 값 병합
|
|
275
319
|
if (node.skills && node.skills.length > 0) {
|
|
276
320
|
frontmatter.skills = node.skills.join(', ');
|
|
277
321
|
}
|
|
322
|
+
else if (node.skills && node.skills.length === 0) {
|
|
323
|
+
// 명시적으로 빈 배열이면 skills 제거
|
|
324
|
+
delete frontmatter.skills;
|
|
325
|
+
}
|
|
278
326
|
const frontmatterStr = Object.entries(frontmatter)
|
|
279
327
|
.map(([key, value]) => `${key}: ${value}`)
|
|
280
328
|
.join('\n');
|
|
329
|
+
// body 처리 - systemPrompt가 명시되면 교체, 아니면 기존 유지
|
|
330
|
+
const body = node.systemPrompt || existingBody || `You are ${node.label}.
|
|
331
|
+
|
|
332
|
+
${node.description || ''}
|
|
333
|
+
`;
|
|
281
334
|
const content = `---
|
|
282
335
|
${frontmatterStr}
|
|
283
336
|
---
|
|
284
337
|
|
|
285
|
-
${
|
|
286
|
-
|
|
287
|
-
${node.description || ''}
|
|
288
|
-
`}
|
|
338
|
+
${body}
|
|
289
339
|
`;
|
|
290
340
|
await fs.writeFile(agentPath, content, 'utf-8');
|
|
291
341
|
return { success: true, path: agentPath };
|
|
292
342
|
}
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
343
|
+
/**
|
|
344
|
+
* Frontmatter 파싱
|
|
345
|
+
*/
|
|
346
|
+
parseFrontmatter(content) {
|
|
347
|
+
const frontmatter = {};
|
|
348
|
+
let body = content;
|
|
349
|
+
const match = content.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
|
|
350
|
+
if (match) {
|
|
351
|
+
const fmContent = match[1];
|
|
352
|
+
body = match[2].trim();
|
|
353
|
+
for (const line of fmContent.split('\n')) {
|
|
354
|
+
const colonIndex = line.indexOf(':');
|
|
355
|
+
if (colonIndex > 0) {
|
|
356
|
+
const key = line.slice(0, colonIndex).trim();
|
|
357
|
+
const value = line.slice(colonIndex + 1).trim();
|
|
358
|
+
frontmatter[key] = value;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
return { frontmatter, body };
|
|
308
363
|
}
|
|
309
364
|
async syncHookNode(node) {
|
|
310
365
|
const settingsPath = path.join(this.projectRoot, '.claude', 'settings.json');
|
|
@@ -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) {
|
|
@@ -22,7 +22,7 @@ const SYSTEM_PROMPT = `당신은 Claude Code 워크플로우 설계 전문가입
|
|
|
22
22
|
## 사용 가능한 공식 스킬
|
|
23
23
|
${AVAILABLE_SKILLS.map(s => `- ${s.id}: ${s.description}`).join('\n')}
|
|
24
24
|
|
|
25
|
-
## 사용 가능한 도구 (
|
|
25
|
+
## 사용 가능한 도구 (agent의 tools에 사용)
|
|
26
26
|
${AVAILABLE_TOOLS.join(', ')}
|
|
27
27
|
|
|
28
28
|
## 응답 형식 (JSON)
|
|
@@ -42,7 +42,7 @@ ${AVAILABLE_TOOLS.join(', ')}
|
|
|
42
42
|
}
|
|
43
43
|
},
|
|
44
44
|
{
|
|
45
|
-
"type": "
|
|
45
|
+
"type": "agent",
|
|
46
46
|
"label": "에이전트 이름",
|
|
47
47
|
"description": "에이전트 역할 설명",
|
|
48
48
|
"config": {
|
|
@@ -90,7 +90,7 @@ ${AVAILABLE_TOOLS.join(', ')}
|
|
|
90
90
|
1. 항상 input 노드로 시작하고 output 노드로 종료
|
|
91
91
|
2. 기존 공식 스킬로 가능하면 official 스킬 사용
|
|
92
92
|
3. 새로운 기능이 필요하면 custom 스킬 생성 (skillContent에 SKILL.md 형식)
|
|
93
|
-
4.
|
|
93
|
+
4. agent는 복잡한 추론이나 다단계 작업에 사용
|
|
94
94
|
5. edges의 from/to는 nodes 배열의 인덱스 (0부터 시작)
|
|
95
95
|
6. 순차적으로 연결되지 않아도 됨 (병렬 처리, 합류 가능)
|
|
96
96
|
7. systemPrompt는 구체적이고 실행 가능한 지시사항으로 작성
|
|
@@ -115,8 +115,8 @@ ${AVAILABLE_TOOLS.join(', ')}
|
|
|
115
115
|
"description": "데이터를 분석하고 보고서를 작성하는 워크플로우",
|
|
116
116
|
"nodes": [
|
|
117
117
|
{ "type": "input", "label": "데이터 파일", "description": "분석할 데이터 파일", "config": { "inputType": "file" } },
|
|
118
|
-
{ "type": "
|
|
119
|
-
{ "type": "
|
|
118
|
+
{ "type": "agent", "label": "데이터 분석가", "description": "데이터를 분석하고 인사이트 도출", "config": { "role": "analyst", "tools": ["Read", "Grep", "Glob"], "model": "sonnet", "systemPrompt": "주어진 데이터를 분석하여 핵심 인사이트를 도출하세요. 통계적 요약, 트렌드, 이상치를 파악하세요." } },
|
|
119
|
+
{ "type": "agent", "label": "보고서 작성자", "description": "분석 결과로 보고서 작성", "config": { "role": "writer", "tools": ["Read", "Write"], "model": "opus", "systemPrompt": "분석 결과를 바탕으로 경영진을 위한 간결하고 명확한 보고서를 작성하세요." } },
|
|
120
120
|
{ "type": "output", "label": "분석 보고서", "description": "최종 분석 보고서", "config": { "outputType": "document" } }
|
|
121
121
|
],
|
|
122
122
|
"edges": [{ "from": 0, "to": 1 }, { "from": 1, "to": 2 }, { "from": 2, "to": 3 }]
|
|
@@ -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({
|
package/package.json
CHANGED
package/server/index.ts
CHANGED
|
@@ -146,12 +146,12 @@ app.post('/api/sync/node', async (req, res) => {
|
|
|
146
146
|
// Delete node from file system
|
|
147
147
|
app.delete('/api/sync/node', async (req, res) => {
|
|
148
148
|
try {
|
|
149
|
-
const { node } = req.body;
|
|
149
|
+
const { node, nodes } = req.body;
|
|
150
150
|
if (!node) {
|
|
151
151
|
return res.status(400).json({ message: 'Node data is required' });
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
-
const result = await nodeSyncService.deleteNode(node);
|
|
154
|
+
const result = await nodeSyncService.deleteNode(node, nodes);
|
|
155
155
|
if (result.success) {
|
|
156
156
|
res.json(result);
|
|
157
157
|
} else {
|
|
@@ -242,7 +242,7 @@ app.get('/api/load/claude-config', async (req, res) => {
|
|
|
242
242
|
try {
|
|
243
243
|
const config = await configLoaderService.loadAll();
|
|
244
244
|
console.log(
|
|
245
|
-
`Loaded config: ${config.skills.length} skills, ${config.
|
|
245
|
+
`Loaded config: ${config.skills.length} skills, ${config.agents.length} agents, ` +
|
|
246
246
|
`${config.commands.length} commands, ${config.hooks.length} hooks`
|
|
247
247
|
);
|
|
248
248
|
res.json(config);
|
|
@@ -208,7 +208,7 @@ export function buildNodePrompt(
|
|
|
208
208
|
): string {
|
|
209
209
|
const lines: string[] = [];
|
|
210
210
|
|
|
211
|
-
if (nodeType === '
|
|
211
|
+
if (nodeType === 'agent') {
|
|
212
212
|
const role = nodeData.role as string || 'assistant';
|
|
213
213
|
const description = nodeData.description as string || '';
|
|
214
214
|
const systemPrompt = nodeData.systemPrompt as string || '';
|
|
@@ -12,7 +12,7 @@ interface LoadedSkill {
|
|
|
12
12
|
|
|
13
13
|
interface LoadedSubagent {
|
|
14
14
|
id: string;
|
|
15
|
-
type: '
|
|
15
|
+
type: 'agent';
|
|
16
16
|
label: string;
|
|
17
17
|
description: string;
|
|
18
18
|
tools: string[];
|
|
@@ -44,7 +44,7 @@ export type LoadedNode = LoadedSkill | LoadedSubagent | LoadedCommand | LoadedHo
|
|
|
44
44
|
|
|
45
45
|
export interface ClaudeConfig {
|
|
46
46
|
skills: LoadedSkill[];
|
|
47
|
-
|
|
47
|
+
agents: LoadedSubagent[];
|
|
48
48
|
commands: LoadedCommand[];
|
|
49
49
|
hooks: LoadedHook[];
|
|
50
50
|
}
|
|
@@ -60,14 +60,14 @@ export class ConfigLoaderService {
|
|
|
60
60
|
* .claude/ 디렉토리에서 모든 설정 로드
|
|
61
61
|
*/
|
|
62
62
|
async loadAll(): Promise<ClaudeConfig> {
|
|
63
|
-
const [skills,
|
|
63
|
+
const [skills, agents, commands, hooks] = await Promise.all([
|
|
64
64
|
this.loadSkills(),
|
|
65
65
|
this.loadSubagents(),
|
|
66
66
|
this.loadCommands(),
|
|
67
67
|
this.loadHooks(),
|
|
68
68
|
]);
|
|
69
69
|
|
|
70
|
-
return { skills,
|
|
70
|
+
return { skills, agents, commands, hooks };
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
/**
|
|
@@ -112,7 +112,7 @@ export class ConfigLoaderService {
|
|
|
112
112
|
*/
|
|
113
113
|
async loadSubagents(): Promise<LoadedSubagent[]> {
|
|
114
114
|
const agentsDir = path.join(this.projectRoot, '.claude', 'agents');
|
|
115
|
-
const
|
|
115
|
+
const agents: LoadedSubagent[] = [];
|
|
116
116
|
|
|
117
117
|
try {
|
|
118
118
|
const entries = await fs.readdir(agentsDir, { withFileTypes: true });
|
|
@@ -125,9 +125,9 @@ export class ConfigLoaderService {
|
|
|
125
125
|
const parsed = this.parseFrontmatter(content);
|
|
126
126
|
const agentName = entry.name.replace('.md', '');
|
|
127
127
|
|
|
128
|
-
|
|
129
|
-
id: `
|
|
130
|
-
type: '
|
|
128
|
+
agents.push({
|
|
129
|
+
id: `agent-${agentName}`,
|
|
130
|
+
type: 'agent',
|
|
131
131
|
label: parsed.frontmatter.name || agentName,
|
|
132
132
|
description: parsed.frontmatter.description || '',
|
|
133
133
|
tools: this.parseList(parsed.frontmatter.tools),
|
|
@@ -144,7 +144,7 @@ export class ConfigLoaderService {
|
|
|
144
144
|
// agents 디렉토리가 없으면 빈 배열 반환
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
-
return
|
|
147
|
+
return agents;
|
|
148
148
|
}
|
|
149
149
|
|
|
150
150
|
/**
|