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