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.
@@ -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>vite-temp</title>
8
- <script type="module" crossorigin src="/assets/index-oO8rpFoq.js"></script>
9
- <link rel="stylesheet" crossorigin href="/assets/index-v9IFpWkA.css">
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>
@@ -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 { 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.12",
3
+ "version": "0.2.14",
4
4
  "type": "module",
5
5
  "description": "Visual workflow builder for Claude Code agents and skills",
6
6
  "keywords": [
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 {