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.
@@ -0,0 +1,471 @@
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
+ import type {
7
+ ExecutionNode,
8
+ SubagentNodeData,
9
+ SkillNodeData,
10
+ InputNodeData,
11
+ OutputNodeData,
12
+ McpNodeData,
13
+ NodeExecutionUpdate,
14
+ } from '../types';
15
+
16
+ export interface WorkflowEdge {
17
+ id: string;
18
+ source: string;
19
+ target: string;
20
+ }
21
+
22
+ export interface ExecutionContext {
23
+ workflowId: string;
24
+ workflowName: string;
25
+ nodes: ExecutionNode[];
26
+ edges: WorkflowEdge[];
27
+ inputs?: Record<string, string>;
28
+ outputDir: string;
29
+ }
30
+
31
+ export interface ExecutionResult {
32
+ nodeId: string;
33
+ success: boolean;
34
+ result?: string;
35
+ files?: Array<{ path: string; type: string; name: string }>;
36
+ error?: string;
37
+ }
38
+
39
+ type ProgressCallback = (update: NodeExecutionUpdate) => void;
40
+ type LogCallback = (type: 'info' | 'warn' | 'error' | 'debug', message: string) => void;
41
+
42
+ /**
43
+ * Anthropic API를 사용한 워크플로우 실행 서비스
44
+ */
45
+ export class WorkflowExecutionService {
46
+ private client: Anthropic;
47
+ private results: Map<string, ExecutionResult> = new Map();
48
+ private outputDir: string = '';
49
+
50
+ constructor() {
51
+ this.client = new Anthropic({
52
+ apiKey: process.env.ANTHROPIC_API_KEY,
53
+ });
54
+ }
55
+
56
+ /**
57
+ * 워크플로우 전체 실행
58
+ */
59
+ async execute(
60
+ context: ExecutionContext,
61
+ onProgress?: ProgressCallback,
62
+ onLog?: LogCallback
63
+ ): Promise<Map<string, ExecutionResult>> {
64
+ this.results.clear();
65
+ this.outputDir = context.outputDir;
66
+
67
+ // 출력 디렉토리 생성
68
+ if (!existsSync(this.outputDir)) {
69
+ await mkdir(this.outputDir, { recursive: true });
70
+ }
71
+
72
+ const executionOrder = this.topologicalSort(context.nodes, context.edges);
73
+
74
+ onLog?.('info', `워크플로우 "${context.workflowName}" 실행 시작 (${executionOrder.length}개 노드)`);
75
+
76
+ for (const node of executionOrder) {
77
+ try {
78
+ onProgress?.({ nodeId: node.id, status: 'running', progress: 0 });
79
+ onLog?.('info', `노드 "${node.data.label}" 실행 중...`);
80
+
81
+ const result = await this.executeNode(node, context, onProgress, onLog);
82
+ this.results.set(node.id, result);
83
+
84
+ if (result.success) {
85
+ onProgress?.({ nodeId: node.id, status: 'completed', progress: 100, result: result.result });
86
+ onLog?.('info', `노드 "${node.data.label}" 완료`);
87
+ } else {
88
+ onProgress?.({ nodeId: node.id, status: 'error', error: result.error });
89
+ onLog?.('error', `노드 "${node.data.label}" 실패: ${result.error}`);
90
+ }
91
+ } catch (error) {
92
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
93
+ this.results.set(node.id, { nodeId: node.id, success: false, error: errorMessage });
94
+ onProgress?.({ nodeId: node.id, status: 'error', error: errorMessage });
95
+ onLog?.('error', `노드 "${node.data.label}" 실행 오류: ${errorMessage}`);
96
+ }
97
+ }
98
+
99
+ return this.results;
100
+ }
101
+
102
+ /**
103
+ * 개별 노드 실행
104
+ */
105
+ private async executeNode(
106
+ node: ExecutionNode,
107
+ context: ExecutionContext,
108
+ onProgress?: ProgressCallback,
109
+ onLog?: LogCallback
110
+ ): Promise<ExecutionResult> {
111
+ // 이전 노드 결과 수집
112
+ const previousResults = this.collectPreviousResults(node, context.edges);
113
+
114
+ switch (node.type) {
115
+ case 'input':
116
+ return this.executeInputNode(node, context.inputs);
117
+
118
+ case 'subagent':
119
+ return this.executeSubagentNode(node, previousResults, onProgress, onLog);
120
+
121
+ case 'skill':
122
+ return this.executeSkillNode(node, previousResults, onProgress, onLog);
123
+
124
+ case 'mcp':
125
+ return this.executeMcpNode(node, previousResults, onProgress, onLog);
126
+
127
+ case 'output':
128
+ return this.executeOutputNode(node, previousResults, onLog);
129
+
130
+ default:
131
+ return { nodeId: node.id, success: false, error: `Unknown node type: ${node.type}` };
132
+ }
133
+ }
134
+
135
+ /**
136
+ * Input 노드 실행 - 사용자 입력값 반환
137
+ */
138
+ private async executeInputNode(
139
+ node: ExecutionNode,
140
+ inputs?: Record<string, string>
141
+ ): Promise<ExecutionResult> {
142
+ const data = node.data as InputNodeData;
143
+ const value = inputs?.[node.id] || data.value || '';
144
+
145
+ return {
146
+ nodeId: node.id,
147
+ success: true,
148
+ result: value,
149
+ };
150
+ }
151
+
152
+ /**
153
+ * Subagent 노드 실행 - Claude API로 작업 수행
154
+ */
155
+ private async executeSubagentNode(
156
+ node: ExecutionNode,
157
+ previousResults: string,
158
+ onProgress?: ProgressCallback,
159
+ onLog?: LogCallback
160
+ ): Promise<ExecutionResult> {
161
+ const data = node.data as SubagentNodeData;
162
+
163
+ onProgress?.({ nodeId: node.id, status: 'running', progress: 20 });
164
+
165
+ // 역할별 시스템 프롬프트
166
+ const rolePrompts: Record<string, string> = {
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}) 호출 중...`);
186
+
187
+ try {
188
+ onProgress?.({ nodeId: node.id, status: 'running', progress: 40 });
189
+
190
+ const modelId = this.getModelId(data.model);
191
+
192
+ const response = await this.client.messages.create({
193
+ model: modelId,
194
+ max_tokens: 4096,
195
+ system: systemPrompt,
196
+ messages: [
197
+ { role: 'user', content: userMessage }
198
+ ],
199
+ });
200
+
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
+ onProgress?.({ nodeId: node.id, status: 'running', progress: 100 });
210
+
211
+ return {
212
+ nodeId: node.id,
213
+ success: true,
214
+ result: resultText,
215
+ };
216
+ } catch (error) {
217
+ const errorMsg = error instanceof Error ? error.message : 'Claude API 호출 실패';
218
+ onLog?.('error', `Subagent 오류: ${errorMsg}`);
219
+ return {
220
+ nodeId: node.id,
221
+ success: false,
222
+ error: errorMsg,
223
+ };
224
+ }
225
+ }
226
+
227
+ /**
228
+ * Skill 노드 실행 - skillExecutionService 사용
229
+ */
230
+ private async executeSkillNode(
231
+ node: ExecutionNode,
232
+ previousResults: string,
233
+ onProgress?: ProgressCallback,
234
+ onLog?: LogCallback
235
+ ): Promise<ExecutionResult> {
236
+ const data = node.data as SkillNodeData;
237
+ const skillId = data.skillId || 'generic';
238
+
239
+ onProgress?.({ nodeId: node.id, status: 'running', progress: 10 });
240
+
241
+ try {
242
+ // skillExecutionService를 사용하여 실제 파일 생성
243
+ const result = await skillExecutionService.execute(
244
+ skillId,
245
+ previousResults,
246
+ this.outputDir,
247
+ onLog
248
+ );
249
+
250
+ onProgress?.({ nodeId: node.id, status: 'running', progress: 100 });
251
+
252
+ return {
253
+ nodeId: node.id,
254
+ success: result.success,
255
+ result: result.result,
256
+ files: result.files,
257
+ error: result.error,
258
+ };
259
+ } catch (error) {
260
+ const errorMsg = error instanceof Error ? error.message : '스킬 실행 실패';
261
+ onLog?.('error', errorMsg);
262
+ return {
263
+ nodeId: node.id,
264
+ success: false,
265
+ error: errorMsg,
266
+ };
267
+ }
268
+ }
269
+
270
+ /**
271
+ * MCP 노드 실행 - 외부 도구/서비스 연결
272
+ */
273
+ private async executeMcpNode(
274
+ node: ExecutionNode,
275
+ previousResults: string,
276
+ onProgress?: ProgressCallback,
277
+ onLog?: LogCallback
278
+ ): Promise<ExecutionResult> {
279
+ const data = node.data as McpNodeData;
280
+
281
+ onProgress?.({ nodeId: node.id, status: 'running', progress: 10 });
282
+ onLog?.('info', `MCP 서버 "${data.serverName}" 연결 중...`);
283
+
284
+ // MCP 서버 타입별 처리
285
+ const mcpPrompt = `당신은 MCP (Model Context Protocol) 서버와 상호작용하는 전문가입니다.
286
+
287
+ ## MCP 서버 정보
288
+ - 서버 이름: ${data.serverName}
289
+ - 서버 타입: ${data.serverType}
290
+ - 설정: ${JSON.stringify(data.serverConfig, null, 2)}
291
+
292
+ ## 이전 단계 결과
293
+ ${previousResults}
294
+
295
+ ## 작업
296
+ 위 MCP 서버를 사용하여 이전 단계의 결과를 처리하세요.
297
+
298
+ 다음 MCP 서버 유형에 따라 적절한 작업을 수행하세요:
299
+ - PostgreSQL/데이터베이스: 데이터 조회 또는 저장
300
+ - Notion/Google Drive: 문서 생성 또는 업데이트
301
+ - Slack/Discord: 메시지 전송 시뮬레이션
302
+ - GitHub/Jira: 이슈 또는 PR 관련 작업 시뮬레이션
303
+
304
+ 작업 결과를 상세히 설명해주세요.`;
305
+
306
+ try {
307
+ onProgress?.({ nodeId: node.id, status: 'running', progress: 50 });
308
+
309
+ const response = await this.client.messages.create({
310
+ model: 'claude-sonnet-4-20250514',
311
+ max_tokens: 4096,
312
+ messages: [{ role: 'user', content: mcpPrompt }],
313
+ });
314
+
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
+ onProgress?.({ nodeId: node.id, status: 'running', progress: 100 });
321
+
322
+ return {
323
+ nodeId: node.id,
324
+ success: true,
325
+ result,
326
+ };
327
+ } catch (error) {
328
+ return {
329
+ nodeId: node.id,
330
+ success: false,
331
+ error: error instanceof Error ? error.message : 'MCP 노드 실행 실패',
332
+ };
333
+ }
334
+ }
335
+
336
+ /**
337
+ * Output 노드 실행 - 결과 수집 및 파일 저장
338
+ */
339
+ private async executeOutputNode(
340
+ node: ExecutionNode,
341
+ previousResults: string,
342
+ onLog?: LogCallback
343
+ ): Promise<ExecutionResult> {
344
+ const data = node.data as OutputNodeData;
345
+
346
+ onLog?.('info', '최종 결과 수집 및 저장 중...');
347
+
348
+ // 모든 이전 결과와 파일 수집
349
+ const allFiles: Array<{ path: string; type: string; name: string }> = [];
350
+
351
+ for (const [, result] of this.results) {
352
+ if (result.files) {
353
+ allFiles.push(...result.files);
354
+ }
355
+ }
356
+
357
+ // 결과 요약 파일 생성
358
+ const summaryPath = join(this.outputDir, 'result-summary.md');
359
+ const summaryContent = `# 워크플로우 실행 결과
360
+
361
+ ## 생성된 컨텐츠
362
+
363
+ ${previousResults}
364
+
365
+ ## 생성된 파일 목록
366
+ ${allFiles.map((f) => `- **${f.name}**: \`${f.path}\``).join('\n') || '없음'}
367
+
368
+ ---
369
+ 생성 시간: ${new Date().toLocaleString('ko-KR')}
370
+ `;
371
+
372
+ try {
373
+ await writeFile(summaryPath, summaryContent, 'utf-8');
374
+
375
+ return {
376
+ nodeId: node.id,
377
+ success: true,
378
+ result: summaryContent,
379
+ files: [
380
+ { path: summaryPath, type: 'markdown', name: '결과 요약' },
381
+ ...allFiles,
382
+ ],
383
+ };
384
+ } catch (error) {
385
+ return {
386
+ nodeId: node.id,
387
+ success: true,
388
+ result: previousResults,
389
+ files: allFiles,
390
+ };
391
+ }
392
+ }
393
+
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
+ /**
409
+ * 이전 노드 결과 수집
410
+ */
411
+ private collectPreviousResults(node: ExecutionNode, edges: WorkflowEdge[]): string {
412
+ const incomingEdges = edges.filter((e) => e.target === node.id);
413
+ const previousResults: string[] = [];
414
+
415
+ for (const edge of incomingEdges) {
416
+ const result = this.results.get(edge.source);
417
+ if (result?.result) {
418
+ previousResults.push(result.result);
419
+ }
420
+ }
421
+
422
+ return previousResults.join('\n\n---\n\n');
423
+ }
424
+
425
+ /**
426
+ * 위상 정렬 (실행 순서 결정)
427
+ */
428
+ private topologicalSort(nodes: ExecutionNode[], edges: WorkflowEdge[]): ExecutionNode[] {
429
+ const nodeMap = new Map(nodes.map((n) => [n.id, n]));
430
+ const inDegree = new Map<string, number>();
431
+ const adjList = new Map<string, string[]>();
432
+
433
+ nodes.forEach((node) => {
434
+ inDegree.set(node.id, 0);
435
+ adjList.set(node.id, []);
436
+ });
437
+
438
+ edges.forEach((edge) => {
439
+ adjList.get(edge.source)?.push(edge.target);
440
+ inDegree.set(edge.target, (inDegree.get(edge.target) || 0) + 1);
441
+ });
442
+
443
+ const queue: string[] = [];
444
+ inDegree.forEach((degree, nodeId) => {
445
+ if (degree === 0) {
446
+ queue.push(nodeId);
447
+ }
448
+ });
449
+
450
+ const result: ExecutionNode[] = [];
451
+ while (queue.length > 0) {
452
+ const nodeId = queue.shift()!;
453
+ const node = nodeMap.get(nodeId);
454
+ if (node) {
455
+ result.push(node);
456
+ }
457
+
458
+ adjList.get(nodeId)?.forEach((neighborId) => {
459
+ const newDegree = (inDegree.get(neighborId) || 0) - 1;
460
+ inDegree.set(neighborId, newDegree);
461
+ if (newDegree === 0) {
462
+ queue.push(neighborId);
463
+ }
464
+ });
465
+ }
466
+
467
+ return result;
468
+ }
469
+ }
470
+
471
+ export const workflowExecutionService = new WorkflowExecutionService();
@@ -0,0 +1,110 @@
1
+ // Node status type
2
+ export type NodeStatus = 'idle' | 'pending' | 'running' | 'completed' | 'error';
3
+
4
+ // Base node data
5
+ export interface BaseNodeData {
6
+ label: string;
7
+ description?: string;
8
+ status: NodeStatus;
9
+ progress?: number;
10
+ error?: string;
11
+ [key: string]: unknown;
12
+ }
13
+
14
+ // Node types
15
+ export type InputType = 'text' | 'file' | 'select' | 'multi';
16
+ export type AgentRole = 'researcher' | 'writer' | 'analyst' | 'coder' | 'custom';
17
+ export type SkillType = 'official' | 'custom';
18
+ export type McpServerType = 'stdio' | 'sse' | 'http';
19
+ export type OutputType = 'markdown' | 'document' | 'image' | 'webpage' | 'link' | 'auto';
20
+
21
+ export interface InputNodeData extends BaseNodeData {
22
+ inputType: InputType;
23
+ value?: string;
24
+ placeholder?: string;
25
+ options?: string[];
26
+ fileTypes?: string[];
27
+ }
28
+
29
+ export interface SubagentNodeData extends BaseNodeData {
30
+ role: AgentRole;
31
+ tools: string[];
32
+ mdContent?: string;
33
+ systemPrompt?: string;
34
+ model?: 'sonnet' | 'opus' | 'haiku';
35
+ usedInputs?: string[];
36
+ }
37
+
38
+ export interface SkillNodeData extends BaseNodeData {
39
+ skillType: SkillType;
40
+ skillId?: string;
41
+ skillCategory?: string;
42
+ mdContent?: string;
43
+ skillContent?: string; // AI가 생성한 커스텀 스킬 SKILL.md 내용
44
+ usedInputs?: string[];
45
+ }
46
+
47
+ export interface McpNodeData extends BaseNodeData {
48
+ serverType: McpServerType;
49
+ serverName: string;
50
+ serverConfig: {
51
+ command?: string;
52
+ args?: string[];
53
+ url?: string;
54
+ env?: Record<string, string>;
55
+ };
56
+ usedInputs?: string[];
57
+ }
58
+
59
+ export interface OutputNodeData extends BaseNodeData {
60
+ outputType: OutputType;
61
+ layoutType?: 'manual' | 'auto' | 'google-docs' | 'google-slides' | 'google-sheets';
62
+ result?: string;
63
+ filePath?: string;
64
+ usedInputs?: string[];
65
+ }
66
+
67
+ export type WorkflowNodeData =
68
+ | InputNodeData
69
+ | SubagentNodeData
70
+ | SkillNodeData
71
+ | McpNodeData
72
+ | OutputNodeData;
73
+
74
+ // Node structure for execution
75
+ export interface ExecutionNode {
76
+ id: string;
77
+ type: 'input' | 'subagent' | 'skill' | 'mcp' | 'output';
78
+ data: WorkflowNodeData;
79
+ position: { x: number; y: number };
80
+ }
81
+
82
+ // Workflow execution request
83
+ export interface WorkflowExecutionRequest {
84
+ workflowId: string;
85
+ workflowName: string;
86
+ nodes: ExecutionNode[];
87
+ edges: Array<{
88
+ id: string;
89
+ source: string;
90
+ target: string;
91
+ }>;
92
+ inputs?: Record<string, string>;
93
+ }
94
+
95
+ // Node execution update
96
+ export interface NodeExecutionUpdate {
97
+ nodeId: string;
98
+ status: NodeStatus;
99
+ progress?: number;
100
+ result?: string;
101
+ error?: string;
102
+ }
103
+
104
+ // Console log entry
105
+ export interface ConsoleLogEntry {
106
+ type: 'info' | 'warn' | 'error' | 'debug';
107
+ message: string;
108
+ timestamp: string;
109
+ nodeId?: string;
110
+ }