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.
@@ -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
- // 서브에이전트 스킬 연결: 서브에이전트의 skills 필드 업데이트
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
- const skillId = targetNode.skillId || this.toKebabCase(targetNode.label);
82
- if (!skills.includes(skillId)) {
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
- // 스킬 → 서브에이전트 연결: 서브에이전트의 skills 필드 업데이트
116
+ // 스킬 → 서브에이전트 연결
89
117
  if (sourceNode.type === 'skill' && targetNode.type === 'subagent') {
118
+ // 에이전트의 upstream skills 필드 업데이트
90
119
  const skills = targetNode.skills || [];
91
- const skillId = sourceNode.skillId || this.toKebabCase(sourceNode.label);
92
- if (!skills.includes(skillId)) {
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
- const skillId = targetNode.skillId || this.toKebabCase(targetNode.label);
118
- sourceNode.skills = (sourceNode.skills || []).filter(s => s !== skillId);
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
- const skillId = sourceNode.skillId || this.toKebabCase(sourceNode.label);
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
- content = this.updateFrontmatter(content, {
145
- name: skillId,
146
- description: node.description || node.label,
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
- name: ${skillId}
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 { skillExecutionService } from './skillExecutionService';
4
+ import { executeClaudeCli, buildNodePrompt } from './claudeCliService';
6
5
  /**
7
- * Anthropic API를 사용한 워크플로우 실행 서비스
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.client = new Anthropic({
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 API로 작업 수행
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
- const rolePrompts = {
95
- researcher: `당신은 전문 리서처입니다. 주어진 주제에 대해 깊이 있는 조사를 수행하고, 핵심 정보를 정리하여 제공합니다.`,
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 modelId = this.getModelId(data.model);
114
- const response = await this.client.messages.create({
115
- model: modelId,
116
- max_tokens: 4096,
117
- system: systemPrompt,
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
- return {
130
- nodeId: node.id,
131
- success: true,
132
- result: resultText,
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 API 호출 실패';
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 노드 실행 - skillExecutionService 사용
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
- // skillExecutionService를 사용하여 실제 파일 생성
154
- const result = await skillExecutionService.execute(skillId, previousResults, this.outputDir, onLog);
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
- return {
157
- nodeId: node.id,
158
- success: result.success,
159
- result: result.result,
160
- files: result.files,
161
- error: result.error,
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
- // MCP 서버 타입별 처리
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 response = await this.client.messages.create({
205
- model: 'claude-sonnet-4-20250514',
206
- max_tokens: 4096,
207
- messages: [{ role: 'user', content: mcpPrompt }],
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
- return {
215
- nodeId: node.id,
216
- success: true,
217
- result,
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "makecc",
3
- "version": "0.2.13",
3
+ "version": "0.2.15",
4
4
  "type": "module",
5
5
  "description": "Visual workflow builder for Claude Code agents and skills",
6
6
  "keywords": [