makecc 0.1.0
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/bin/cli.js +111 -0
- package/dist/assets/index-BcaHeFM8.css +1 -0
- package/dist/assets/index-CryYaA6W.js +57 -0
- package/dist/client/assets/index-BkAJX_uj.js +133 -0
- package/dist/client/assets/index-CRHGYTA2.css +1 -0
- package/dist/client/index.html +14 -0
- package/dist/client/vite.svg +1 -0
- package/dist/index.html +14 -0
- package/dist/server/index.js +207 -0
- package/dist/server/services/claudeService.js +153 -0
- package/dist/server/services/fileService.js +193 -0
- package/dist/server/services/skillExecutionService.js +622 -0
- package/dist/server/services/terminalService.js +216 -0
- package/dist/server/services/workflowAIService.js +191 -0
- package/dist/server/services/workflowExecutionService.js +342 -0
- package/dist/server/types.js +1 -0
- package/dist/vite.svg +1 -0
- package/package.json +84 -0
- package/server/index.ts +253 -0
- package/server/services/claudeService.ts +222 -0
- package/server/services/fileService.ts +277 -0
- package/server/services/skillExecutionService.ts +732 -0
- package/server/services/terminalService.ts +266 -0
- package/server/services/workflowAIService.ts +240 -0
- package/server/services/workflowExecutionService.ts +471 -0
- package/server/types.ts +110 -0
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
import Anthropic from '@anthropic-ai/sdk';
|
|
2
|
+
import { writeFile, mkdir } from 'fs/promises';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import { existsSync } from 'fs';
|
|
5
|
+
import { skillExecutionService } from './skillExecutionService';
|
|
6
|
+
/**
|
|
7
|
+
* Anthropic API를 사용한 워크플로우 실행 서비스
|
|
8
|
+
*/
|
|
9
|
+
export class WorkflowExecutionService {
|
|
10
|
+
client;
|
|
11
|
+
results = new Map();
|
|
12
|
+
outputDir = '';
|
|
13
|
+
constructor() {
|
|
14
|
+
this.client = new Anthropic({
|
|
15
|
+
apiKey: process.env.ANTHROPIC_API_KEY,
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* 워크플로우 전체 실행
|
|
20
|
+
*/
|
|
21
|
+
async execute(context, onProgress, onLog) {
|
|
22
|
+
this.results.clear();
|
|
23
|
+
this.outputDir = context.outputDir;
|
|
24
|
+
// 출력 디렉토리 생성
|
|
25
|
+
if (!existsSync(this.outputDir)) {
|
|
26
|
+
await mkdir(this.outputDir, { recursive: true });
|
|
27
|
+
}
|
|
28
|
+
const executionOrder = this.topologicalSort(context.nodes, context.edges);
|
|
29
|
+
onLog?.('info', `워크플로우 "${context.workflowName}" 실행 시작 (${executionOrder.length}개 노드)`);
|
|
30
|
+
for (const node of executionOrder) {
|
|
31
|
+
try {
|
|
32
|
+
onProgress?.({ nodeId: node.id, status: 'running', progress: 0 });
|
|
33
|
+
onLog?.('info', `노드 "${node.data.label}" 실행 중...`);
|
|
34
|
+
const result = await this.executeNode(node, context, onProgress, onLog);
|
|
35
|
+
this.results.set(node.id, result);
|
|
36
|
+
if (result.success) {
|
|
37
|
+
onProgress?.({ nodeId: node.id, status: 'completed', progress: 100, result: result.result });
|
|
38
|
+
onLog?.('info', `노드 "${node.data.label}" 완료`);
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
onProgress?.({ nodeId: node.id, status: 'error', error: result.error });
|
|
42
|
+
onLog?.('error', `노드 "${node.data.label}" 실패: ${result.error}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
47
|
+
this.results.set(node.id, { nodeId: node.id, success: false, error: errorMessage });
|
|
48
|
+
onProgress?.({ nodeId: node.id, status: 'error', error: errorMessage });
|
|
49
|
+
onLog?.('error', `노드 "${node.data.label}" 실행 오류: ${errorMessage}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return this.results;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* 개별 노드 실행
|
|
56
|
+
*/
|
|
57
|
+
async executeNode(node, context, onProgress, onLog) {
|
|
58
|
+
// 이전 노드 결과 수집
|
|
59
|
+
const previousResults = this.collectPreviousResults(node, context.edges);
|
|
60
|
+
switch (node.type) {
|
|
61
|
+
case 'input':
|
|
62
|
+
return this.executeInputNode(node, context.inputs);
|
|
63
|
+
case 'subagent':
|
|
64
|
+
return this.executeSubagentNode(node, previousResults, onProgress, onLog);
|
|
65
|
+
case 'skill':
|
|
66
|
+
return this.executeSkillNode(node, previousResults, onProgress, onLog);
|
|
67
|
+
case 'mcp':
|
|
68
|
+
return this.executeMcpNode(node, previousResults, onProgress, onLog);
|
|
69
|
+
case 'output':
|
|
70
|
+
return this.executeOutputNode(node, previousResults, onLog);
|
|
71
|
+
default:
|
|
72
|
+
return { nodeId: node.id, success: false, error: `Unknown node type: ${node.type}` };
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Input 노드 실행 - 사용자 입력값 반환
|
|
77
|
+
*/
|
|
78
|
+
async executeInputNode(node, inputs) {
|
|
79
|
+
const data = node.data;
|
|
80
|
+
const value = inputs?.[node.id] || data.value || '';
|
|
81
|
+
return {
|
|
82
|
+
nodeId: node.id,
|
|
83
|
+
success: true,
|
|
84
|
+
result: value,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Subagent 노드 실행 - Claude API로 작업 수행
|
|
89
|
+
*/
|
|
90
|
+
async executeSubagentNode(node, previousResults, onProgress, onLog) {
|
|
91
|
+
const data = node.data;
|
|
92
|
+
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}) 호출 중...`);
|
|
111
|
+
try {
|
|
112
|
+
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
|
+
],
|
|
121
|
+
});
|
|
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
|
+
onProgress?.({ nodeId: node.id, status: 'running', progress: 100 });
|
|
129
|
+
return {
|
|
130
|
+
nodeId: node.id,
|
|
131
|
+
success: true,
|
|
132
|
+
result: resultText,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
const errorMsg = error instanceof Error ? error.message : 'Claude API 호출 실패';
|
|
137
|
+
onLog?.('error', `Subagent 오류: ${errorMsg}`);
|
|
138
|
+
return {
|
|
139
|
+
nodeId: node.id,
|
|
140
|
+
success: false,
|
|
141
|
+
error: errorMsg,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Skill 노드 실행 - skillExecutionService 사용
|
|
147
|
+
*/
|
|
148
|
+
async executeSkillNode(node, previousResults, onProgress, onLog) {
|
|
149
|
+
const data = node.data;
|
|
150
|
+
const skillId = data.skillId || 'generic';
|
|
151
|
+
onProgress?.({ nodeId: node.id, status: 'running', progress: 10 });
|
|
152
|
+
try {
|
|
153
|
+
// skillExecutionService를 사용하여 실제 파일 생성
|
|
154
|
+
const result = await skillExecutionService.execute(skillId, previousResults, this.outputDir, onLog);
|
|
155
|
+
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
|
+
};
|
|
163
|
+
}
|
|
164
|
+
catch (error) {
|
|
165
|
+
const errorMsg = error instanceof Error ? error.message : '스킬 실행 실패';
|
|
166
|
+
onLog?.('error', errorMsg);
|
|
167
|
+
return {
|
|
168
|
+
nodeId: node.id,
|
|
169
|
+
success: false,
|
|
170
|
+
error: errorMsg,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* MCP 노드 실행 - 외부 도구/서비스 연결
|
|
176
|
+
*/
|
|
177
|
+
async executeMcpNode(node, previousResults, onProgress, onLog) {
|
|
178
|
+
const data = node.data;
|
|
179
|
+
onProgress?.({ nodeId: node.id, status: 'running', progress: 10 });
|
|
180
|
+
onLog?.('info', `MCP 서버 "${data.serverName}" 연결 중...`);
|
|
181
|
+
// MCP 서버 타입별 처리
|
|
182
|
+
const mcpPrompt = `당신은 MCP (Model Context Protocol) 서버와 상호작용하는 전문가입니다.
|
|
183
|
+
|
|
184
|
+
## MCP 서버 정보
|
|
185
|
+
- 서버 이름: ${data.serverName}
|
|
186
|
+
- 서버 타입: ${data.serverType}
|
|
187
|
+
- 설정: ${JSON.stringify(data.serverConfig, null, 2)}
|
|
188
|
+
|
|
189
|
+
## 이전 단계 결과
|
|
190
|
+
${previousResults}
|
|
191
|
+
|
|
192
|
+
## 작업
|
|
193
|
+
위 MCP 서버를 사용하여 이전 단계의 결과를 처리하세요.
|
|
194
|
+
|
|
195
|
+
다음 MCP 서버 유형에 따라 적절한 작업을 수행하세요:
|
|
196
|
+
- PostgreSQL/데이터베이스: 데이터 조회 또는 저장
|
|
197
|
+
- Notion/Google Drive: 문서 생성 또는 업데이트
|
|
198
|
+
- Slack/Discord: 메시지 전송 시뮬레이션
|
|
199
|
+
- GitHub/Jira: 이슈 또는 PR 관련 작업 시뮬레이션
|
|
200
|
+
|
|
201
|
+
작업 결과를 상세히 설명해주세요.`;
|
|
202
|
+
try {
|
|
203
|
+
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 }],
|
|
208
|
+
});
|
|
209
|
+
const result = response.content
|
|
210
|
+
.filter((block) => block.type === 'text')
|
|
211
|
+
.map((block) => block.text)
|
|
212
|
+
.join('\n');
|
|
213
|
+
onProgress?.({ nodeId: node.id, status: 'running', progress: 100 });
|
|
214
|
+
return {
|
|
215
|
+
nodeId: node.id,
|
|
216
|
+
success: true,
|
|
217
|
+
result,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
catch (error) {
|
|
221
|
+
return {
|
|
222
|
+
nodeId: node.id,
|
|
223
|
+
success: false,
|
|
224
|
+
error: error instanceof Error ? error.message : 'MCP 노드 실행 실패',
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Output 노드 실행 - 결과 수집 및 파일 저장
|
|
230
|
+
*/
|
|
231
|
+
async executeOutputNode(node, previousResults, onLog) {
|
|
232
|
+
const data = node.data;
|
|
233
|
+
onLog?.('info', '최종 결과 수집 및 저장 중...');
|
|
234
|
+
// 모든 이전 결과와 파일 수집
|
|
235
|
+
const allFiles = [];
|
|
236
|
+
for (const [, result] of this.results) {
|
|
237
|
+
if (result.files) {
|
|
238
|
+
allFiles.push(...result.files);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
// 결과 요약 파일 생성
|
|
242
|
+
const summaryPath = join(this.outputDir, 'result-summary.md');
|
|
243
|
+
const summaryContent = `# 워크플로우 실행 결과
|
|
244
|
+
|
|
245
|
+
## 생성된 컨텐츠
|
|
246
|
+
|
|
247
|
+
${previousResults}
|
|
248
|
+
|
|
249
|
+
## 생성된 파일 목록
|
|
250
|
+
${allFiles.map((f) => `- **${f.name}**: \`${f.path}\``).join('\n') || '없음'}
|
|
251
|
+
|
|
252
|
+
---
|
|
253
|
+
생성 시간: ${new Date().toLocaleString('ko-KR')}
|
|
254
|
+
`;
|
|
255
|
+
try {
|
|
256
|
+
await writeFile(summaryPath, summaryContent, 'utf-8');
|
|
257
|
+
return {
|
|
258
|
+
nodeId: node.id,
|
|
259
|
+
success: true,
|
|
260
|
+
result: summaryContent,
|
|
261
|
+
files: [
|
|
262
|
+
{ path: summaryPath, type: 'markdown', name: '결과 요약' },
|
|
263
|
+
...allFiles,
|
|
264
|
+
],
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
catch (error) {
|
|
268
|
+
return {
|
|
269
|
+
nodeId: node.id,
|
|
270
|
+
success: true,
|
|
271
|
+
result: previousResults,
|
|
272
|
+
files: allFiles,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
}
|
|
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
|
+
/**
|
|
290
|
+
* 이전 노드 결과 수집
|
|
291
|
+
*/
|
|
292
|
+
collectPreviousResults(node, edges) {
|
|
293
|
+
const incomingEdges = edges.filter((e) => e.target === node.id);
|
|
294
|
+
const previousResults = [];
|
|
295
|
+
for (const edge of incomingEdges) {
|
|
296
|
+
const result = this.results.get(edge.source);
|
|
297
|
+
if (result?.result) {
|
|
298
|
+
previousResults.push(result.result);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return previousResults.join('\n\n---\n\n');
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* 위상 정렬 (실행 순서 결정)
|
|
305
|
+
*/
|
|
306
|
+
topologicalSort(nodes, edges) {
|
|
307
|
+
const nodeMap = new Map(nodes.map((n) => [n.id, n]));
|
|
308
|
+
const inDegree = new Map();
|
|
309
|
+
const adjList = new Map();
|
|
310
|
+
nodes.forEach((node) => {
|
|
311
|
+
inDegree.set(node.id, 0);
|
|
312
|
+
adjList.set(node.id, []);
|
|
313
|
+
});
|
|
314
|
+
edges.forEach((edge) => {
|
|
315
|
+
adjList.get(edge.source)?.push(edge.target);
|
|
316
|
+
inDegree.set(edge.target, (inDegree.get(edge.target) || 0) + 1);
|
|
317
|
+
});
|
|
318
|
+
const queue = [];
|
|
319
|
+
inDegree.forEach((degree, nodeId) => {
|
|
320
|
+
if (degree === 0) {
|
|
321
|
+
queue.push(nodeId);
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
const result = [];
|
|
325
|
+
while (queue.length > 0) {
|
|
326
|
+
const nodeId = queue.shift();
|
|
327
|
+
const node = nodeMap.get(nodeId);
|
|
328
|
+
if (node) {
|
|
329
|
+
result.push(node);
|
|
330
|
+
}
|
|
331
|
+
adjList.get(nodeId)?.forEach((neighborId) => {
|
|
332
|
+
const newDegree = (inDegree.get(neighborId) || 0) - 1;
|
|
333
|
+
inDegree.set(neighborId, newDegree);
|
|
334
|
+
if (newDegree === 0) {
|
|
335
|
+
queue.push(neighborId);
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
return result;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
export const workflowExecutionService = new WorkflowExecutionService();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/vite.svg
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
package/package.json
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "makecc",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Visual workflow builder for Claude Code agents and skills",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"claude",
|
|
8
|
+
"claude-code",
|
|
9
|
+
"workflow",
|
|
10
|
+
"agent",
|
|
11
|
+
"ai",
|
|
12
|
+
"automation"
|
|
13
|
+
],
|
|
14
|
+
"author": "",
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": ""
|
|
19
|
+
},
|
|
20
|
+
"bin": {
|
|
21
|
+
"makecc": "./bin/cli.js"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"bin",
|
|
25
|
+
"dist",
|
|
26
|
+
"server"
|
|
27
|
+
],
|
|
28
|
+
"scripts": {
|
|
29
|
+
"dev": "vite",
|
|
30
|
+
"dev:server": "tsx watch server/index.ts",
|
|
31
|
+
"dev:all": "concurrently \"npm run dev\" \"npm run dev:server\"",
|
|
32
|
+
"build": "tsc -b && vite build --outDir dist/client",
|
|
33
|
+
"start": "tsx server/index.ts",
|
|
34
|
+
"start:server": "tsx server/index.ts",
|
|
35
|
+
"lint": "eslint .",
|
|
36
|
+
"preview": "vite preview",
|
|
37
|
+
"prepublishOnly": "npm run build"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@anthropic-ai/claude-code": "^2.1.19",
|
|
41
|
+
"@anthropic-ai/sdk": "^0.71.2",
|
|
42
|
+
"@google/generative-ai": "^0.24.1",
|
|
43
|
+
"@tailwindcss/postcss": "^4.1.18",
|
|
44
|
+
"@xyflow/react": "^12.10.0",
|
|
45
|
+
"clsx": "^2.1.1",
|
|
46
|
+
"concurrently": "^9.2.1",
|
|
47
|
+
"cors": "^2.8.6",
|
|
48
|
+
"dotenv": "^17.2.3",
|
|
49
|
+
"exceljs": "^4.4.0",
|
|
50
|
+
"express": "^5.2.1",
|
|
51
|
+
"lucide-react": "^0.563.0",
|
|
52
|
+
"nanoid": "^5.1.6",
|
|
53
|
+
"pptxgenjs": "^4.0.1",
|
|
54
|
+
"react": "^19.2.0",
|
|
55
|
+
"react-dom": "^19.2.0",
|
|
56
|
+
"react-markdown": "^10.1.0",
|
|
57
|
+
"remark-gfm": "^4.0.1",
|
|
58
|
+
"sharp": "^0.34.5",
|
|
59
|
+
"socket.io": "^4.8.3",
|
|
60
|
+
"socket.io-client": "^4.8.3",
|
|
61
|
+
"tsx": "^4.21.0",
|
|
62
|
+
"zustand": "^5.0.10"
|
|
63
|
+
},
|
|
64
|
+
"devDependencies": {
|
|
65
|
+
"@eslint/js": "^9.39.1",
|
|
66
|
+
"@tailwindcss/typography": "^0.5.19",
|
|
67
|
+
"@types/cors": "^2.8.19",
|
|
68
|
+
"@types/express": "^5.0.6",
|
|
69
|
+
"@types/node": "^24.10.1",
|
|
70
|
+
"@types/react": "^19.2.5",
|
|
71
|
+
"@types/react-dom": "^19.2.3",
|
|
72
|
+
"@vitejs/plugin-react": "^5.1.1",
|
|
73
|
+
"autoprefixer": "^10.4.23",
|
|
74
|
+
"eslint": "^9.39.1",
|
|
75
|
+
"eslint-plugin-react-hooks": "^7.0.1",
|
|
76
|
+
"eslint-plugin-react-refresh": "^0.4.24",
|
|
77
|
+
"globals": "^16.5.0",
|
|
78
|
+
"postcss": "^8.5.6",
|
|
79
|
+
"tailwindcss": "^4.1.18",
|
|
80
|
+
"typescript": "~5.9.3",
|
|
81
|
+
"typescript-eslint": "^8.46.4",
|
|
82
|
+
"vite": "^7.2.4"
|
|
83
|
+
}
|
|
84
|
+
}
|
package/server/index.ts
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import 'dotenv/config';
|
|
2
|
+
import express from 'express';
|
|
3
|
+
import { createServer } from 'http';
|
|
4
|
+
import { Server } from 'socket.io';
|
|
5
|
+
import cors from 'cors';
|
|
6
|
+
import { join, dirname } from 'path';
|
|
7
|
+
import { fileURLToPath } from 'url';
|
|
8
|
+
import { existsSync } from 'fs';
|
|
9
|
+
import { ClaudeService } from './services/claudeService';
|
|
10
|
+
import { fileService } from './services/fileService';
|
|
11
|
+
import { workflowAIService } from './services/workflowAIService';
|
|
12
|
+
import { workflowExecutionService } from './services/workflowExecutionService';
|
|
13
|
+
import { executeInTerminal, getClaudeCommand } from './services/terminalService';
|
|
14
|
+
import type { WorkflowExecutionRequest, NodeExecutionUpdate } from './types';
|
|
15
|
+
import type { ClaudeConfigExport, SaveOptions } from './services/fileService';
|
|
16
|
+
|
|
17
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
18
|
+
const __dirname = dirname(__filename);
|
|
19
|
+
|
|
20
|
+
const app = express();
|
|
21
|
+
const httpServer = createServer(app);
|
|
22
|
+
|
|
23
|
+
const isProduction = process.env.NODE_ENV === 'production';
|
|
24
|
+
|
|
25
|
+
const io = new Server(httpServer, {
|
|
26
|
+
cors: {
|
|
27
|
+
origin: isProduction
|
|
28
|
+
? undefined
|
|
29
|
+
: ['http://localhost:5173', 'http://localhost:5174', 'http://localhost:5175'],
|
|
30
|
+
methods: ['GET', 'POST'],
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
app.use(cors());
|
|
35
|
+
app.use(express.json());
|
|
36
|
+
|
|
37
|
+
// Serve static files in production
|
|
38
|
+
if (isProduction) {
|
|
39
|
+
const clientDistPath = join(__dirname, '..', 'dist', 'client');
|
|
40
|
+
if (existsSync(clientDistPath)) {
|
|
41
|
+
app.use(express.static(clientDistPath));
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const claudeService = new ClaudeService();
|
|
46
|
+
|
|
47
|
+
// REST API endpoints
|
|
48
|
+
app.get('/api/health', (req, res) => {
|
|
49
|
+
res.json({ status: 'ok', timestamp: new Date().toISOString() });
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// Get project path
|
|
53
|
+
app.get('/api/project-path', (req, res) => {
|
|
54
|
+
res.json({ path: fileService.getProjectPath() });
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// Save workflow as Claude Code configuration
|
|
58
|
+
app.post('/api/save/workflow', async (req, res) => {
|
|
59
|
+
try {
|
|
60
|
+
const { config, options } = req.body as {
|
|
61
|
+
config: ClaudeConfigExport;
|
|
62
|
+
options: SaveOptions;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
if (!config || !options) {
|
|
66
|
+
return res.status(400).json({ message: 'Missing config or options' });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const result = await fileService.saveWorkflow(config, options);
|
|
70
|
+
res.json(result);
|
|
71
|
+
} catch (error) {
|
|
72
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
73
|
+
console.error('Save workflow error:', errorMessage);
|
|
74
|
+
res.status(500).json({ message: errorMessage });
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// Generate workflow using AI
|
|
79
|
+
app.post('/api/generate/workflow', async (req, res) => {
|
|
80
|
+
try {
|
|
81
|
+
const { prompt } = req.body as { prompt: string };
|
|
82
|
+
|
|
83
|
+
if (!prompt || typeof prompt !== 'string') {
|
|
84
|
+
return res.status(400).json({ message: 'Prompt is required' });
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
console.log('Generating workflow for prompt:', prompt);
|
|
88
|
+
const result = await workflowAIService.generate(prompt);
|
|
89
|
+
console.log('Workflow generated:', result.workflowName);
|
|
90
|
+
|
|
91
|
+
res.json(result);
|
|
92
|
+
} catch (error) {
|
|
93
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
94
|
+
console.error('Generate workflow error:', errorMessage);
|
|
95
|
+
res.status(500).json({ message: errorMessage });
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// Socket.io connection handling
|
|
100
|
+
io.on('connection', (socket) => {
|
|
101
|
+
console.log('Client connected:', socket.id);
|
|
102
|
+
|
|
103
|
+
// Execute workflow - Actually executes the workflow using Claude Code SDK
|
|
104
|
+
socket.on('execute:workflow', async (data: WorkflowExecutionRequest) => {
|
|
105
|
+
console.log('Executing workflow:', data.workflowId);
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
// Emit start event
|
|
109
|
+
socket.emit('workflow:started', { workflowId: data.workflowId });
|
|
110
|
+
|
|
111
|
+
socket.emit('console:log', {
|
|
112
|
+
type: 'info',
|
|
113
|
+
message: `워크플로우 "${data.workflowName}" 실행 시작...`,
|
|
114
|
+
timestamp: new Date().toISOString(),
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// 출력 디렉토리 설정
|
|
118
|
+
const outputDir = join(fileService.getProjectPath(), 'output', data.workflowId);
|
|
119
|
+
|
|
120
|
+
// 실제 워크플로우 실행
|
|
121
|
+
const results = await workflowExecutionService.execute(
|
|
122
|
+
{
|
|
123
|
+
workflowId: data.workflowId,
|
|
124
|
+
workflowName: data.workflowName,
|
|
125
|
+
nodes: data.nodes,
|
|
126
|
+
edges: data.edges,
|
|
127
|
+
inputs: data.inputs,
|
|
128
|
+
outputDir,
|
|
129
|
+
},
|
|
130
|
+
// Progress callback
|
|
131
|
+
(update: NodeExecutionUpdate) => {
|
|
132
|
+
socket.emit('node:update', update);
|
|
133
|
+
},
|
|
134
|
+
// Log callback
|
|
135
|
+
(type, message) => {
|
|
136
|
+
socket.emit('console:log', {
|
|
137
|
+
type,
|
|
138
|
+
message,
|
|
139
|
+
timestamp: new Date().toISOString(),
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
// 결과 수집
|
|
145
|
+
const allResults: Array<{
|
|
146
|
+
nodeId: string;
|
|
147
|
+
label: string;
|
|
148
|
+
success: boolean;
|
|
149
|
+
result?: string;
|
|
150
|
+
files?: Array<{ path: string; type: string; name: string }>;
|
|
151
|
+
error?: string;
|
|
152
|
+
}> = [];
|
|
153
|
+
|
|
154
|
+
for (const [nodeId, result] of results) {
|
|
155
|
+
const node = data.nodes.find((n) => n.id === nodeId);
|
|
156
|
+
allResults.push({
|
|
157
|
+
nodeId,
|
|
158
|
+
label: node?.data.label || nodeId,
|
|
159
|
+
success: result.success,
|
|
160
|
+
result: result.result,
|
|
161
|
+
files: result.files,
|
|
162
|
+
error: result.error,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// 최종 결과 전송
|
|
167
|
+
socket.emit('workflow:completed', {
|
|
168
|
+
workflowId: data.workflowId,
|
|
169
|
+
results: allResults,
|
|
170
|
+
outputDir,
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
socket.emit('console:log', {
|
|
174
|
+
type: 'info',
|
|
175
|
+
message: `워크플로우 실행 완료! 결과가 ${outputDir}에 저장되었습니다.`,
|
|
176
|
+
timestamp: new Date().toISOString(),
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
} catch (error) {
|
|
180
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
181
|
+
socket.emit('console:log', {
|
|
182
|
+
type: 'error',
|
|
183
|
+
message: `실행 오류: ${errorMessage}`,
|
|
184
|
+
timestamp: new Date().toISOString(),
|
|
185
|
+
});
|
|
186
|
+
socket.emit('workflow:error', { workflowId: data.workflowId, error: errorMessage });
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// Execute workflow in Terminal (alternative mode)
|
|
191
|
+
socket.on('execute:workflow:terminal', async (data: WorkflowExecutionRequest) => {
|
|
192
|
+
console.log('Executing workflow in Terminal:', data.workflowId);
|
|
193
|
+
|
|
194
|
+
try {
|
|
195
|
+
socket.emit('workflow:started', { workflowId: data.workflowId });
|
|
196
|
+
|
|
197
|
+
socket.emit('console:log', {
|
|
198
|
+
type: 'info',
|
|
199
|
+
message: 'Opening Terminal with Claude Code...',
|
|
200
|
+
timestamp: new Date().toISOString(),
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
const result = await executeInTerminal({
|
|
204
|
+
workflowName: data.workflowName,
|
|
205
|
+
nodes: data.nodes,
|
|
206
|
+
edges: data.edges,
|
|
207
|
+
inputs: data.inputs,
|
|
208
|
+
workingDirectory: fileService.getProjectPath(),
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
if (result.success) {
|
|
212
|
+
socket.emit('console:log', {
|
|
213
|
+
type: 'info',
|
|
214
|
+
message: result.message,
|
|
215
|
+
timestamp: new Date().toISOString(),
|
|
216
|
+
});
|
|
217
|
+
socket.emit('workflow:completed', { workflowId: data.workflowId });
|
|
218
|
+
} else {
|
|
219
|
+
socket.emit('workflow:error', {
|
|
220
|
+
workflowId: data.workflowId,
|
|
221
|
+
error: result.message
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
} catch (error) {
|
|
225
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
226
|
+
socket.emit('workflow:error', { workflowId: data.workflowId, error: errorMessage });
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
// Cancel workflow execution
|
|
231
|
+
socket.on('execute:cancel', () => {
|
|
232
|
+
claudeService.cancelExecution();
|
|
233
|
+
socket.emit('workflow:cancelled');
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
socket.on('disconnect', () => {
|
|
237
|
+
console.log('Client disconnected:', socket.id);
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
// SPA fallback - serve index.html for all non-API routes in production
|
|
242
|
+
if (isProduction) {
|
|
243
|
+
const clientDistPath = join(__dirname, '..', 'dist', 'client');
|
|
244
|
+
app.get('/{*path}', (req, res) => {
|
|
245
|
+
res.sendFile(join(clientDistPath, 'index.html'));
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const PORT = process.env.PORT || 3001;
|
|
250
|
+
|
|
251
|
+
httpServer.listen(PORT, () => {
|
|
252
|
+
console.log(`Server running on http://localhost:${PORT}`);
|
|
253
|
+
});
|