makecc 0.2.12 → 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.
- package/dist/client/assets/index-CXXgu628.css +1 -0
- package/dist/client/assets/index-PJfSWqWr.js +150 -0
- package/dist/client/index.html +3 -3
- package/dist/server/index.js +30 -0
- package/dist/server/services/claudeCliService.js +210 -0
- package/dist/server/services/workflowExecutionService.js +79 -97
- package/package.json +1 -1
- package/server/index.ts +39 -0
- package/server/services/claudeCliService.ts +258 -0
- package/server/services/workflowExecutionService.ts +77 -110
- package/dist/client/assets/index-oO8rpFoq.js +0 -150
- package/dist/client/assets/index-v9IFpWkA.css +0 -1
package/dist/client/index.html
CHANGED
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
<meta charset="UTF-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
|
-
<title>
|
|
8
|
-
<script type="module" crossorigin src="/assets/index-
|
|
9
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
7
|
+
<title>makecc</title>
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-PJfSWqWr.js"></script>
|
|
9
|
+
<link rel="stylesheet" crossorigin href="/assets/index-CXXgu628.css">
|
|
10
10
|
</head>
|
|
11
11
|
<body>
|
|
12
12
|
<div id="root"></div>
|
package/dist/server/index.js
CHANGED
|
@@ -314,6 +314,36 @@ app.post('/api/skill/test', async (req, res) => {
|
|
|
314
314
|
res.status(500).json({ message: errorMessage });
|
|
315
315
|
}
|
|
316
316
|
});
|
|
317
|
+
// Save workflow output to local project
|
|
318
|
+
app.post('/api/save/workflow-output', async (req, res) => {
|
|
319
|
+
try {
|
|
320
|
+
const { workflowName, files } = req.body;
|
|
321
|
+
if (!workflowName || !files || files.length === 0) {
|
|
322
|
+
return res.status(400).json({ message: 'Workflow name and files are required' });
|
|
323
|
+
}
|
|
324
|
+
// output/워크플로우명/ 폴더 생성
|
|
325
|
+
const sanitizedName = workflowName.replace(/[/\\?%*:|"<>]/g, '_').replace(/\s+/g, '_');
|
|
326
|
+
const outputDir = join(fileService.getProjectPath(), 'output', sanitizedName);
|
|
327
|
+
await fs.mkdir(outputDir, { recursive: true });
|
|
328
|
+
const savedFiles = [];
|
|
329
|
+
for (const file of files) {
|
|
330
|
+
const filePath = join(outputDir, file.name);
|
|
331
|
+
await fs.writeFile(filePath, file.content, 'utf-8');
|
|
332
|
+
savedFiles.push({ name: file.name, path: filePath });
|
|
333
|
+
console.log(`Saved workflow output: ${filePath}`);
|
|
334
|
+
}
|
|
335
|
+
res.json({
|
|
336
|
+
success: true,
|
|
337
|
+
outputDir,
|
|
338
|
+
files: savedFiles,
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
catch (error) {
|
|
342
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
343
|
+
console.error('Save workflow output error:', errorMessage);
|
|
344
|
+
res.status(500).json({ message: errorMessage });
|
|
345
|
+
}
|
|
346
|
+
});
|
|
317
347
|
// Read skill files for preview
|
|
318
348
|
app.get('/api/skill/files', async (req, res) => {
|
|
319
349
|
try {
|
|
@@ -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
package/server/index.ts
CHANGED
|
@@ -355,6 +355,45 @@ app.post('/api/skill/test', async (req, res) => {
|
|
|
355
355
|
}
|
|
356
356
|
});
|
|
357
357
|
|
|
358
|
+
// Save workflow output to local project
|
|
359
|
+
app.post('/api/save/workflow-output', async (req, res) => {
|
|
360
|
+
try {
|
|
361
|
+
const { workflowName, files } = req.body as {
|
|
362
|
+
workflowName: string;
|
|
363
|
+
files: Array<{ name: string; content: string }>;
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
if (!workflowName || !files || files.length === 0) {
|
|
367
|
+
return res.status(400).json({ message: 'Workflow name and files are required' });
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// output/워크플로우명/ 폴더 생성
|
|
371
|
+
const sanitizedName = workflowName.replace(/[/\\?%*:|"<>]/g, '_').replace(/\s+/g, '_');
|
|
372
|
+
const outputDir = join(fileService.getProjectPath(), 'output', sanitizedName);
|
|
373
|
+
|
|
374
|
+
await fs.mkdir(outputDir, { recursive: true });
|
|
375
|
+
|
|
376
|
+
const savedFiles: Array<{ name: string; path: string }> = [];
|
|
377
|
+
|
|
378
|
+
for (const file of files) {
|
|
379
|
+
const filePath = join(outputDir, file.name);
|
|
380
|
+
await fs.writeFile(filePath, file.content, 'utf-8');
|
|
381
|
+
savedFiles.push({ name: file.name, path: filePath });
|
|
382
|
+
console.log(`Saved workflow output: ${filePath}`);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
res.json({
|
|
386
|
+
success: true,
|
|
387
|
+
outputDir,
|
|
388
|
+
files: savedFiles,
|
|
389
|
+
});
|
|
390
|
+
} catch (error) {
|
|
391
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
392
|
+
console.error('Save workflow output error:', errorMessage);
|
|
393
|
+
res.status(500).json({ message: errorMessage });
|
|
394
|
+
}
|
|
395
|
+
});
|
|
396
|
+
|
|
358
397
|
// Read skill files for preview
|
|
359
398
|
app.get('/api/skill/files', async (req, res) => {
|
|
360
399
|
try {
|