makecc 0.2.13 → 0.2.15
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/server/services/claudeCliService.js +210 -0
- package/dist/server/services/nodeSyncService.js +106 -19
- package/dist/server/services/workflowExecutionService.js +79 -97
- package/package.json +1 -1
- package/server/services/claudeCliService.ts +258 -0
- package/server/services/nodeSyncService.ts +126 -20
- package/server/services/workflowExecutionService.ts +77 -110
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import { join, basename } from 'path';
|
|
3
|
+
import { existsSync } from 'fs';
|
|
4
|
+
import { mkdir, copyFile, readdir, stat } from 'fs/promises';
|
|
5
|
+
/**
|
|
6
|
+
* claude -c 명령어를 백그라운드로 실행하고 결과를 캡처합니다.
|
|
7
|
+
*/
|
|
8
|
+
export async function executeClaudeCli(options) {
|
|
9
|
+
const { prompt, workingDirectory, outputDirectory, timeoutMs = 300000 } = options; // 기본 5분 타임아웃
|
|
10
|
+
// 출력 디렉토리 생성
|
|
11
|
+
if (!existsSync(outputDirectory)) {
|
|
12
|
+
await mkdir(outputDirectory, { recursive: true });
|
|
13
|
+
}
|
|
14
|
+
// 실행 전 working directory 파일 목록 스냅샷
|
|
15
|
+
const beforeFiles = await getDirectorySnapshot(workingDirectory);
|
|
16
|
+
return new Promise((resolve) => {
|
|
17
|
+
// claude -c --print 옵션으로 결과만 출력 (인터랙티브 모드 없이)
|
|
18
|
+
const proc = spawn('claude', ['-c', '--print', prompt], {
|
|
19
|
+
cwd: workingDirectory,
|
|
20
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
21
|
+
env: {
|
|
22
|
+
...process.env,
|
|
23
|
+
// TERM 설정으로 색상 코드 방지
|
|
24
|
+
TERM: 'dumb',
|
|
25
|
+
NO_COLOR: '1',
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
let stdout = '';
|
|
29
|
+
let stderr = '';
|
|
30
|
+
let timedOut = false;
|
|
31
|
+
// 타임아웃 설정
|
|
32
|
+
const timer = setTimeout(() => {
|
|
33
|
+
timedOut = true;
|
|
34
|
+
proc.kill('SIGTERM');
|
|
35
|
+
}, timeoutMs);
|
|
36
|
+
proc.stdout?.on('data', (data) => {
|
|
37
|
+
stdout += data.toString();
|
|
38
|
+
});
|
|
39
|
+
proc.stderr?.on('data', (data) => {
|
|
40
|
+
stderr += data.toString();
|
|
41
|
+
});
|
|
42
|
+
proc.on('close', async (code) => {
|
|
43
|
+
clearTimeout(timer);
|
|
44
|
+
// 실행 후 새로 생성된 파일 찾기
|
|
45
|
+
const afterFiles = await getDirectorySnapshot(workingDirectory);
|
|
46
|
+
const newFiles = findNewFiles(beforeFiles, afterFiles);
|
|
47
|
+
// 새 파일들을 output 디렉토리로 복사
|
|
48
|
+
const generatedFiles = [];
|
|
49
|
+
for (const filePath of newFiles) {
|
|
50
|
+
const fileName = basename(filePath);
|
|
51
|
+
const destPath = join(outputDirectory, fileName);
|
|
52
|
+
const fileType = getFileType(fileName);
|
|
53
|
+
try {
|
|
54
|
+
await copyFile(filePath, destPath);
|
|
55
|
+
generatedFiles.push({ name: fileName, path: destPath, type: fileType });
|
|
56
|
+
console.log(`Copied generated file: ${filePath} -> ${destPath}`);
|
|
57
|
+
}
|
|
58
|
+
catch (err) {
|
|
59
|
+
console.error(`Failed to copy file ${filePath}:`, err);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// stdout 결과도 파일로 저장 (결과가 있으면)
|
|
63
|
+
if (stdout.trim()) {
|
|
64
|
+
const { writeFile } = await import('fs/promises');
|
|
65
|
+
const resultPath = join(outputDirectory, 'claude-output.md');
|
|
66
|
+
await writeFile(resultPath, stdout, 'utf-8');
|
|
67
|
+
generatedFiles.push({ name: 'claude-output.md', path: resultPath, type: 'markdown' });
|
|
68
|
+
}
|
|
69
|
+
resolve({
|
|
70
|
+
success: code === 0 && !timedOut,
|
|
71
|
+
stdout: timedOut ? stdout + '\n[Execution timed out]' : stdout,
|
|
72
|
+
stderr,
|
|
73
|
+
exitCode: code,
|
|
74
|
+
generatedFiles,
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
proc.on('error', (err) => {
|
|
78
|
+
clearTimeout(timer);
|
|
79
|
+
resolve({
|
|
80
|
+
success: false,
|
|
81
|
+
stdout: '',
|
|
82
|
+
stderr: err.message,
|
|
83
|
+
exitCode: null,
|
|
84
|
+
generatedFiles: [],
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* 디렉토리의 파일 스냅샷 (재귀적으로 1단계까지만)
|
|
91
|
+
*/
|
|
92
|
+
async function getDirectorySnapshot(dir) {
|
|
93
|
+
const snapshot = new Map();
|
|
94
|
+
if (!existsSync(dir)) {
|
|
95
|
+
return snapshot;
|
|
96
|
+
}
|
|
97
|
+
try {
|
|
98
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
99
|
+
for (const entry of entries) {
|
|
100
|
+
// 숨김 파일 및 특정 폴더 제외
|
|
101
|
+
if (entry.name.startsWith('.') || entry.name === 'node_modules' || entry.name === '__pycache__') {
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
const fullPath = join(dir, entry.name);
|
|
105
|
+
if (entry.isFile()) {
|
|
106
|
+
const stats = await stat(fullPath);
|
|
107
|
+
snapshot.set(fullPath, stats.mtimeMs);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
catch (err) {
|
|
112
|
+
console.error('Error reading directory:', err);
|
|
113
|
+
}
|
|
114
|
+
return snapshot;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* 파일 확장자로 타입 결정
|
|
118
|
+
*/
|
|
119
|
+
function getFileType(fileName) {
|
|
120
|
+
const ext = fileName.split('.').pop()?.toLowerCase() || '';
|
|
121
|
+
const typeMap = {
|
|
122
|
+
md: 'markdown',
|
|
123
|
+
txt: 'text',
|
|
124
|
+
json: 'json',
|
|
125
|
+
js: 'javascript',
|
|
126
|
+
ts: 'typescript',
|
|
127
|
+
py: 'python',
|
|
128
|
+
html: 'html',
|
|
129
|
+
css: 'css',
|
|
130
|
+
png: 'image',
|
|
131
|
+
jpg: 'image',
|
|
132
|
+
jpeg: 'image',
|
|
133
|
+
gif: 'image',
|
|
134
|
+
svg: 'image',
|
|
135
|
+
pdf: 'pdf',
|
|
136
|
+
xlsx: 'excel',
|
|
137
|
+
xls: 'excel',
|
|
138
|
+
pptx: 'powerpoint',
|
|
139
|
+
ppt: 'powerpoint',
|
|
140
|
+
docx: 'word',
|
|
141
|
+
doc: 'word',
|
|
142
|
+
};
|
|
143
|
+
return typeMap[ext] || 'file';
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* 새로 생성되거나 수정된 파일 찾기
|
|
147
|
+
*/
|
|
148
|
+
function findNewFiles(before, after) {
|
|
149
|
+
const newFiles = [];
|
|
150
|
+
for (const [path, mtime] of after) {
|
|
151
|
+
const beforeMtime = before.get(path);
|
|
152
|
+
// 새 파일이거나 수정된 파일
|
|
153
|
+
if (beforeMtime === undefined || mtime > beforeMtime) {
|
|
154
|
+
newFiles.push(path);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return newFiles;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* 노드별 프롬프트 생성
|
|
161
|
+
*/
|
|
162
|
+
export function buildNodePrompt(nodeType, nodeData, previousResults) {
|
|
163
|
+
const lines = [];
|
|
164
|
+
if (nodeType === 'subagent') {
|
|
165
|
+
const role = nodeData.role || 'assistant';
|
|
166
|
+
const description = nodeData.description || '';
|
|
167
|
+
const systemPrompt = nodeData.systemPrompt || '';
|
|
168
|
+
lines.push(`You are a ${role}.`);
|
|
169
|
+
if (systemPrompt) {
|
|
170
|
+
lines.push(systemPrompt);
|
|
171
|
+
}
|
|
172
|
+
lines.push('');
|
|
173
|
+
lines.push('## Task');
|
|
174
|
+
lines.push(description || 'Complete the following task based on the input.');
|
|
175
|
+
lines.push('');
|
|
176
|
+
if (previousResults) {
|
|
177
|
+
lines.push('## Input from previous steps');
|
|
178
|
+
lines.push(previousResults);
|
|
179
|
+
lines.push('');
|
|
180
|
+
}
|
|
181
|
+
lines.push('Please complete this task and provide the result.');
|
|
182
|
+
}
|
|
183
|
+
else if (nodeType === 'skill') {
|
|
184
|
+
const skillId = nodeData.skillId || '';
|
|
185
|
+
const description = nodeData.description || '';
|
|
186
|
+
if (skillId) {
|
|
187
|
+
lines.push(`Execute the skill: /${skillId}`);
|
|
188
|
+
}
|
|
189
|
+
lines.push('');
|
|
190
|
+
lines.push('## Task');
|
|
191
|
+
lines.push(description || 'Execute this skill with the provided input.');
|
|
192
|
+
lines.push('');
|
|
193
|
+
if (previousResults) {
|
|
194
|
+
lines.push('## Input');
|
|
195
|
+
lines.push(previousResults);
|
|
196
|
+
lines.push('');
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
// Generic
|
|
201
|
+
lines.push('## Task');
|
|
202
|
+
lines.push(nodeData.description || 'Process the following input.');
|
|
203
|
+
lines.push('');
|
|
204
|
+
if (previousResults) {
|
|
205
|
+
lines.push('## Input');
|
|
206
|
+
lines.push(previousResults);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return lines.join('\n');
|
|
210
|
+
}
|
|
@@ -67,6 +67,8 @@ export class NodeSyncService {
|
|
|
67
67
|
}
|
|
68
68
|
/**
|
|
69
69
|
* 엣지 연결 시 관계 업데이트
|
|
70
|
+
* - source의 downstream에 target 추가
|
|
71
|
+
* - target의 upstream에 source 추가
|
|
70
72
|
*/
|
|
71
73
|
async syncEdge(edge, nodes) {
|
|
72
74
|
try {
|
|
@@ -75,25 +77,68 @@ export class NodeSyncService {
|
|
|
75
77
|
if (!sourceNode || !targetNode) {
|
|
76
78
|
return { success: false, error: 'Source or target node not found' };
|
|
77
79
|
}
|
|
78
|
-
|
|
80
|
+
const sourceId = this.getNodeIdentifier(sourceNode);
|
|
81
|
+
const targetId = this.getNodeIdentifier(targetNode);
|
|
82
|
+
// 서브에이전트 → 스킬 연결
|
|
79
83
|
if (sourceNode.type === 'subagent' && targetNode.type === 'skill') {
|
|
84
|
+
// 에이전트의 skills 필드 업데이트
|
|
80
85
|
const skills = sourceNode.skills || [];
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
skills.push(skillId);
|
|
86
|
+
if (!skills.includes(targetId)) {
|
|
87
|
+
skills.push(targetId);
|
|
84
88
|
sourceNode.skills = skills;
|
|
85
89
|
await this.syncSubagentNode(sourceNode);
|
|
86
90
|
}
|
|
91
|
+
// 스킬의 upstream 필드 업데이트
|
|
92
|
+
const upstream = targetNode.upstream || [];
|
|
93
|
+
if (!upstream.includes(sourceId)) {
|
|
94
|
+
upstream.push(sourceId);
|
|
95
|
+
targetNode.upstream = upstream;
|
|
96
|
+
await this.syncSkillNode(targetNode);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
// 스킬 → 스킬 연결
|
|
100
|
+
if (sourceNode.type === 'skill' && targetNode.type === 'skill') {
|
|
101
|
+
// source의 downstream 업데이트
|
|
102
|
+
const downstream = sourceNode.downstream || [];
|
|
103
|
+
if (!downstream.includes(targetId)) {
|
|
104
|
+
downstream.push(targetId);
|
|
105
|
+
sourceNode.downstream = downstream;
|
|
106
|
+
await this.syncSkillNode(sourceNode);
|
|
107
|
+
}
|
|
108
|
+
// target의 upstream 업데이트
|
|
109
|
+
const upstream = targetNode.upstream || [];
|
|
110
|
+
if (!upstream.includes(sourceId)) {
|
|
111
|
+
upstream.push(sourceId);
|
|
112
|
+
targetNode.upstream = upstream;
|
|
113
|
+
await this.syncSkillNode(targetNode);
|
|
114
|
+
}
|
|
87
115
|
}
|
|
88
|
-
// 스킬 → 서브에이전트
|
|
116
|
+
// 스킬 → 서브에이전트 연결
|
|
89
117
|
if (sourceNode.type === 'skill' && targetNode.type === 'subagent') {
|
|
118
|
+
// 에이전트의 upstream skills 필드 업데이트
|
|
90
119
|
const skills = targetNode.skills || [];
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
skills.push(skillId);
|
|
120
|
+
if (!skills.includes(sourceId)) {
|
|
121
|
+
skills.push(sourceId);
|
|
94
122
|
targetNode.skills = skills;
|
|
95
123
|
await this.syncSubagentNode(targetNode);
|
|
96
124
|
}
|
|
125
|
+
// 스킬의 downstream 필드 업데이트
|
|
126
|
+
const downstream = sourceNode.downstream || [];
|
|
127
|
+
if (!downstream.includes(targetId)) {
|
|
128
|
+
downstream.push(targetId);
|
|
129
|
+
sourceNode.downstream = downstream;
|
|
130
|
+
await this.syncSkillNode(sourceNode);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
// 서브에이전트 → 서브에이전트 연결
|
|
134
|
+
if (sourceNode.type === 'subagent' && targetNode.type === 'subagent') {
|
|
135
|
+
// source의 downstream agents
|
|
136
|
+
const sourceDownstream = sourceNode.skills || [];
|
|
137
|
+
if (!sourceDownstream.includes(targetId)) {
|
|
138
|
+
sourceDownstream.push(targetId);
|
|
139
|
+
sourceNode.skills = sourceDownstream;
|
|
140
|
+
await this.syncSubagentNode(sourceNode);
|
|
141
|
+
}
|
|
97
142
|
}
|
|
98
143
|
return { success: true };
|
|
99
144
|
}
|
|
@@ -102,6 +147,15 @@ export class NodeSyncService {
|
|
|
102
147
|
return { success: false, error: errorMessage };
|
|
103
148
|
}
|
|
104
149
|
}
|
|
150
|
+
/**
|
|
151
|
+
* 노드의 식별자 반환 (skillId 또는 kebab-case label)
|
|
152
|
+
*/
|
|
153
|
+
getNodeIdentifier(node) {
|
|
154
|
+
if (node.type === 'skill') {
|
|
155
|
+
return node.skillId || this.toKebabCase(node.label);
|
|
156
|
+
}
|
|
157
|
+
return this.toKebabCase(node.label);
|
|
158
|
+
}
|
|
105
159
|
/**
|
|
106
160
|
* 엣지 삭제 시 관계 업데이트
|
|
107
161
|
*/
|
|
@@ -112,16 +166,35 @@ export class NodeSyncService {
|
|
|
112
166
|
if (!sourceNode || !targetNode) {
|
|
113
167
|
return { success: true }; // 노드가 없으면 무시
|
|
114
168
|
}
|
|
115
|
-
|
|
169
|
+
const sourceId = this.getNodeIdentifier(sourceNode);
|
|
170
|
+
const targetId = this.getNodeIdentifier(targetNode);
|
|
171
|
+
// 서브에이전트 → 스킬 연결 해제
|
|
116
172
|
if (sourceNode.type === 'subagent' && targetNode.type === 'skill') {
|
|
117
|
-
|
|
118
|
-
sourceNode.skills = (sourceNode.skills || []).filter(s => s !==
|
|
173
|
+
// 에이전트에서 스킬 제거
|
|
174
|
+
sourceNode.skills = (sourceNode.skills || []).filter(s => s !== targetId);
|
|
119
175
|
await this.syncSubagentNode(sourceNode);
|
|
176
|
+
// 스킬에서 upstream 제거
|
|
177
|
+
targetNode.upstream = (targetNode.upstream || []).filter(s => s !== sourceId);
|
|
178
|
+
await this.syncSkillNode(targetNode);
|
|
179
|
+
}
|
|
180
|
+
// 스킬 → 스킬 연결 해제
|
|
181
|
+
if (sourceNode.type === 'skill' && targetNode.type === 'skill') {
|
|
182
|
+
sourceNode.downstream = (sourceNode.downstream || []).filter(s => s !== targetId);
|
|
183
|
+
await this.syncSkillNode(sourceNode);
|
|
184
|
+
targetNode.upstream = (targetNode.upstream || []).filter(s => s !== sourceId);
|
|
185
|
+
await this.syncSkillNode(targetNode);
|
|
120
186
|
}
|
|
187
|
+
// 스킬 → 서브에이전트 연결 해제
|
|
121
188
|
if (sourceNode.type === 'skill' && targetNode.type === 'subagent') {
|
|
122
|
-
|
|
123
|
-
targetNode.skills = (targetNode.skills || []).filter(s => s !== skillId);
|
|
189
|
+
targetNode.skills = (targetNode.skills || []).filter(s => s !== sourceId);
|
|
124
190
|
await this.syncSubagentNode(targetNode);
|
|
191
|
+
sourceNode.downstream = (sourceNode.downstream || []).filter(s => s !== targetId);
|
|
192
|
+
await this.syncSkillNode(sourceNode);
|
|
193
|
+
}
|
|
194
|
+
// 서브에이전트 → 서브에이전트 연결 해제
|
|
195
|
+
if (sourceNode.type === 'subagent' && targetNode.type === 'subagent') {
|
|
196
|
+
sourceNode.skills = (sourceNode.skills || []).filter(s => s !== targetId);
|
|
197
|
+
await this.syncSubagentNode(sourceNode);
|
|
125
198
|
}
|
|
126
199
|
return { success: true };
|
|
127
200
|
}
|
|
@@ -136,21 +209,35 @@ export class NodeSyncService {
|
|
|
136
209
|
const skillPath = path.join(this.projectRoot, '.claude', 'skills', skillId);
|
|
137
210
|
const skillMdPath = path.join(skillPath, 'SKILL.md');
|
|
138
211
|
await fs.mkdir(skillPath, { recursive: true });
|
|
212
|
+
// Frontmatter 구성
|
|
213
|
+
const frontmatter = {
|
|
214
|
+
name: skillId,
|
|
215
|
+
description: node.description || node.label,
|
|
216
|
+
};
|
|
217
|
+
// upstream 연결 추가
|
|
218
|
+
if (node.upstream && node.upstream.length > 0) {
|
|
219
|
+
frontmatter.upstream = node.upstream.join(', ');
|
|
220
|
+
}
|
|
221
|
+
// downstream 연결 추가
|
|
222
|
+
if (node.downstream && node.downstream.length > 0) {
|
|
223
|
+
frontmatter.downstream = node.downstream.join(', ');
|
|
224
|
+
}
|
|
139
225
|
// 기존 파일이 있으면 읽어서 업데이트, 없으면 새로 생성
|
|
140
226
|
let content = '';
|
|
141
227
|
try {
|
|
142
228
|
content = await fs.readFile(skillMdPath, 'utf-8');
|
|
143
229
|
// frontmatter 업데이트
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
});
|
|
230
|
+
for (const [key, value] of Object.entries(frontmatter)) {
|
|
231
|
+
content = this.updateFrontmatter(content, { [key]: value });
|
|
232
|
+
}
|
|
148
233
|
}
|
|
149
234
|
catch {
|
|
150
235
|
// 새로 생성
|
|
236
|
+
const frontmatterStr = Object.entries(frontmatter)
|
|
237
|
+
.map(([key, value]) => `${key}: ${value}`)
|
|
238
|
+
.join('\n');
|
|
151
239
|
content = `---
|
|
152
|
-
|
|
153
|
-
description: ${node.description || node.label}
|
|
240
|
+
${frontmatterStr}
|
|
154
241
|
---
|
|
155
242
|
|
|
156
243
|
# ${node.label}
|
|
@@ -1,19 +1,16 @@
|
|
|
1
|
-
import Anthropic from '@anthropic-ai/sdk';
|
|
2
1
|
import { writeFile, mkdir } from 'fs/promises';
|
|
3
2
|
import { join } from 'path';
|
|
4
3
|
import { existsSync } from 'fs';
|
|
5
|
-
import {
|
|
4
|
+
import { executeClaudeCli, buildNodePrompt } from './claudeCliService';
|
|
6
5
|
/**
|
|
7
|
-
*
|
|
6
|
+
* Claude CLI를 사용한 워크플로우 실행 서비스
|
|
8
7
|
*/
|
|
9
8
|
export class WorkflowExecutionService {
|
|
10
|
-
client;
|
|
11
9
|
results = new Map();
|
|
12
10
|
outputDir = '';
|
|
11
|
+
projectRoot = '';
|
|
13
12
|
constructor() {
|
|
14
|
-
this.
|
|
15
|
-
apiKey: process.env.ANTHROPIC_API_KEY,
|
|
16
|
-
});
|
|
13
|
+
this.projectRoot = process.env.MAKECC_PROJECT_PATH || process.cwd();
|
|
17
14
|
}
|
|
18
15
|
/**
|
|
19
16
|
* 워크플로우 전체 실행
|
|
@@ -85,55 +82,41 @@ export class WorkflowExecutionService {
|
|
|
85
82
|
};
|
|
86
83
|
}
|
|
87
84
|
/**
|
|
88
|
-
* Subagent 노드 실행 - Claude
|
|
85
|
+
* Subagent 노드 실행 - Claude CLI로 작업 수행
|
|
89
86
|
*/
|
|
90
87
|
async executeSubagentNode(node, previousResults, onProgress, onLog) {
|
|
91
88
|
const data = node.data;
|
|
92
89
|
onProgress?.({ nodeId: node.id, status: 'running', progress: 20 });
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
writer: `당신은 전문 작가입니다. 명확하고 매력적인 콘텐츠를 작성합니다. 사용자의 요구에 맞는 톤과 스타일로 글을 작성합니다.`,
|
|
97
|
-
analyst: `당신은 데이터 분석가입니다. 정보를 분석하고 패턴을 파악하여 인사이트를 도출합니다.`,
|
|
98
|
-
coder: `당신은 전문 개발자입니다. 깔끔하고 효율적인 코드를 작성하며, 모범 사례를 따릅니다.`,
|
|
99
|
-
designer: `당신은 디자인 전문가입니다. 상세페이지, 배너, UI 등을 위한 디자인 가이드와 컨셉을 제안합니다.`,
|
|
100
|
-
custom: `당신은 AI 어시스턴트입니다. 주어진 작업을 최선을 다해 수행합니다.`,
|
|
101
|
-
};
|
|
102
|
-
const systemPrompt = data.systemPrompt || rolePrompts[data.role] || rolePrompts.custom;
|
|
103
|
-
const userMessage = `## 작업 설명
|
|
104
|
-
${data.description || '주어진 작업을 수행해주세요.'}
|
|
105
|
-
|
|
106
|
-
## 이전 단계 결과
|
|
107
|
-
${previousResults || '(없음)'}
|
|
108
|
-
|
|
109
|
-
위 내용을 바탕으로 작업을 수행하고 결과를 제공해주세요.`;
|
|
110
|
-
onLog?.('debug', `Subagent "${data.label}" (${data.role}) 호출 중...`);
|
|
90
|
+
onLog?.('info', `claude -c 실행 중: ${data.label} (${data.role})`);
|
|
91
|
+
// 프롬프트 생성
|
|
92
|
+
const prompt = buildNodePrompt('subagent', data, previousResults);
|
|
111
93
|
try {
|
|
112
94
|
onProgress?.({ nodeId: node.id, status: 'running', progress: 40 });
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
messages: [
|
|
119
|
-
{ role: 'user', content: userMessage }
|
|
120
|
-
],
|
|
95
|
+
const result = await executeClaudeCli({
|
|
96
|
+
prompt,
|
|
97
|
+
workingDirectory: this.projectRoot,
|
|
98
|
+
outputDirectory: this.outputDir,
|
|
99
|
+
timeoutMs: 300000, // 5분
|
|
121
100
|
});
|
|
122
|
-
onProgress?.({ nodeId: node.id, status: 'running', progress: 80 });
|
|
123
|
-
// 응답 텍스트 추출
|
|
124
|
-
const resultText = response.content
|
|
125
|
-
.filter((block) => block.type === 'text')
|
|
126
|
-
.map((block) => block.text)
|
|
127
|
-
.join('\n');
|
|
128
101
|
onProgress?.({ nodeId: node.id, status: 'running', progress: 100 });
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
102
|
+
if (result.success) {
|
|
103
|
+
return {
|
|
104
|
+
nodeId: node.id,
|
|
105
|
+
success: true,
|
|
106
|
+
result: result.stdout,
|
|
107
|
+
files: result.generatedFiles,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
return {
|
|
112
|
+
nodeId: node.id,
|
|
113
|
+
success: false,
|
|
114
|
+
error: result.stderr || 'Claude CLI 실행 실패',
|
|
115
|
+
};
|
|
116
|
+
}
|
|
134
117
|
}
|
|
135
118
|
catch (error) {
|
|
136
|
-
const errorMsg = error instanceof Error ? error.message : 'Claude
|
|
119
|
+
const errorMsg = error instanceof Error ? error.message : 'Claude CLI 호출 실패';
|
|
137
120
|
onLog?.('error', `Subagent 오류: ${errorMsg}`);
|
|
138
121
|
return {
|
|
139
122
|
nodeId: node.id,
|
|
@@ -143,23 +126,38 @@ ${previousResults || '(없음)'}
|
|
|
143
126
|
}
|
|
144
127
|
}
|
|
145
128
|
/**
|
|
146
|
-
* Skill 노드 실행 -
|
|
129
|
+
* Skill 노드 실행 - Claude CLI로 스킬 실행
|
|
147
130
|
*/
|
|
148
131
|
async executeSkillNode(node, previousResults, onProgress, onLog) {
|
|
149
132
|
const data = node.data;
|
|
150
133
|
const skillId = data.skillId || 'generic';
|
|
151
134
|
onProgress?.({ nodeId: node.id, status: 'running', progress: 10 });
|
|
135
|
+
onLog?.('info', `claude -c 실행 중: /${skillId}`);
|
|
136
|
+
// 프롬프트 생성 - 스킬 호출 형태
|
|
137
|
+
const prompt = buildNodePrompt('skill', data, previousResults);
|
|
152
138
|
try {
|
|
153
|
-
|
|
154
|
-
|
|
139
|
+
const result = await executeClaudeCli({
|
|
140
|
+
prompt,
|
|
141
|
+
workingDirectory: this.projectRoot,
|
|
142
|
+
outputDirectory: this.outputDir,
|
|
143
|
+
timeoutMs: 300000,
|
|
144
|
+
});
|
|
155
145
|
onProgress?.({ nodeId: node.id, status: 'running', progress: 100 });
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
146
|
+
if (result.success) {
|
|
147
|
+
return {
|
|
148
|
+
nodeId: node.id,
|
|
149
|
+
success: true,
|
|
150
|
+
result: result.stdout,
|
|
151
|
+
files: result.generatedFiles,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
return {
|
|
156
|
+
nodeId: node.id,
|
|
157
|
+
success: false,
|
|
158
|
+
error: result.stderr || '스킬 실행 실패',
|
|
159
|
+
};
|
|
160
|
+
}
|
|
163
161
|
}
|
|
164
162
|
catch (error) {
|
|
165
163
|
const errorMsg = error instanceof Error ? error.message : '스킬 실행 실패';
|
|
@@ -172,50 +170,47 @@ ${previousResults || '(없음)'}
|
|
|
172
170
|
}
|
|
173
171
|
}
|
|
174
172
|
/**
|
|
175
|
-
* MCP 노드 실행 -
|
|
173
|
+
* MCP 노드 실행 - Claude CLI로 MCP 서버 연동
|
|
176
174
|
*/
|
|
177
175
|
async executeMcpNode(node, previousResults, onProgress, onLog) {
|
|
178
176
|
const data = node.data;
|
|
179
177
|
onProgress?.({ nodeId: node.id, status: 'running', progress: 10 });
|
|
180
|
-
onLog?.('info', `MCP 서버 "${data.serverName}"
|
|
181
|
-
|
|
182
|
-
const mcpPrompt = `당신은 MCP (Model Context Protocol) 서버와 상호작용하는 전문가입니다.
|
|
178
|
+
onLog?.('info', `claude -c 실행 중: MCP 서버 "${data.serverName}"`);
|
|
179
|
+
const prompt = `MCP 서버를 사용하여 작업을 수행해주세요.
|
|
183
180
|
|
|
184
181
|
## MCP 서버 정보
|
|
185
182
|
- 서버 이름: ${data.serverName}
|
|
186
183
|
- 서버 타입: ${data.serverType}
|
|
187
|
-
- 설정: ${JSON.stringify(data.serverConfig, null, 2)}
|
|
188
184
|
|
|
189
185
|
## 이전 단계 결과
|
|
190
|
-
${previousResults}
|
|
186
|
+
${previousResults || '(없음)'}
|
|
191
187
|
|
|
192
188
|
## 작업
|
|
193
|
-
위 MCP 서버를 사용하여 이전 단계의 결과를
|
|
194
|
-
|
|
195
|
-
다음 MCP 서버 유형에 따라 적절한 작업을 수행하세요:
|
|
196
|
-
- PostgreSQL/데이터베이스: 데이터 조회 또는 저장
|
|
197
|
-
- Notion/Google Drive: 문서 생성 또는 업데이트
|
|
198
|
-
- Slack/Discord: 메시지 전송 시뮬레이션
|
|
199
|
-
- GitHub/Jira: 이슈 또는 PR 관련 작업 시뮬레이션
|
|
200
|
-
|
|
201
|
-
작업 결과를 상세히 설명해주세요.`;
|
|
189
|
+
위 MCP 서버를 사용하여 이전 단계의 결과를 처리하세요.`;
|
|
202
190
|
try {
|
|
203
191
|
onProgress?.({ nodeId: node.id, status: 'running', progress: 50 });
|
|
204
|
-
const
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
192
|
+
const result = await executeClaudeCli({
|
|
193
|
+
prompt,
|
|
194
|
+
workingDirectory: this.projectRoot,
|
|
195
|
+
outputDirectory: this.outputDir,
|
|
196
|
+
timeoutMs: 300000,
|
|
208
197
|
});
|
|
209
|
-
const result = response.content
|
|
210
|
-
.filter((block) => block.type === 'text')
|
|
211
|
-
.map((block) => block.text)
|
|
212
|
-
.join('\n');
|
|
213
198
|
onProgress?.({ nodeId: node.id, status: 'running', progress: 100 });
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
199
|
+
if (result.success) {
|
|
200
|
+
return {
|
|
201
|
+
nodeId: node.id,
|
|
202
|
+
success: true,
|
|
203
|
+
result: result.stdout,
|
|
204
|
+
files: result.generatedFiles,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
return {
|
|
209
|
+
nodeId: node.id,
|
|
210
|
+
success: false,
|
|
211
|
+
error: result.stderr || 'MCP 노드 실행 실패',
|
|
212
|
+
};
|
|
213
|
+
}
|
|
219
214
|
}
|
|
220
215
|
catch (error) {
|
|
221
216
|
return {
|
|
@@ -273,19 +268,6 @@ ${allFiles.map((f) => `- **${f.name}**: \`${f.path}\``).join('\n') || '없음'}
|
|
|
273
268
|
};
|
|
274
269
|
}
|
|
275
270
|
}
|
|
276
|
-
/**
|
|
277
|
-
* 모델 ID 변환
|
|
278
|
-
*/
|
|
279
|
-
getModelId(model) {
|
|
280
|
-
switch (model) {
|
|
281
|
-
case 'opus':
|
|
282
|
-
return 'claude-opus-4-20250514';
|
|
283
|
-
case 'haiku':
|
|
284
|
-
return 'claude-3-5-haiku-20241022';
|
|
285
|
-
default:
|
|
286
|
-
return 'claude-sonnet-4-20250514';
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
271
|
/**
|
|
290
272
|
* 이전 노드 결과 수집
|
|
291
273
|
*/
|