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,258 @@
1
+ import { spawn } from 'child_process';
2
+ import { join, basename } from 'path';
3
+ import { existsSync, readdirSync, statSync } from 'fs';
4
+ import { mkdir, copyFile, readdir, stat } from 'fs/promises';
5
+
6
+ export interface ClaudeCliResult {
7
+ success: boolean;
8
+ stdout: string;
9
+ stderr: string;
10
+ exitCode: number | null;
11
+ generatedFiles: Array<{ name: string; path: string; type: string }>;
12
+ }
13
+
14
+ export interface ClaudeCliOptions {
15
+ prompt: string;
16
+ workingDirectory: string;
17
+ outputDirectory: string;
18
+ timeoutMs?: number;
19
+ }
20
+
21
+ /**
22
+ * claude -c 명령어를 백그라운드로 실행하고 결과를 캡처합니다.
23
+ */
24
+ export async function executeClaudeCli(options: ClaudeCliOptions): Promise<ClaudeCliResult> {
25
+ const { prompt, workingDirectory, outputDirectory, timeoutMs = 300000 } = options; // 기본 5분 타임아웃
26
+
27
+ // 출력 디렉토리 생성
28
+ if (!existsSync(outputDirectory)) {
29
+ await mkdir(outputDirectory, { recursive: true });
30
+ }
31
+
32
+ // 실행 전 working directory 파일 목록 스냅샷
33
+ const beforeFiles = await getDirectorySnapshot(workingDirectory);
34
+
35
+ return new Promise((resolve) => {
36
+ // claude -c --print 옵션으로 결과만 출력 (인터랙티브 모드 없이)
37
+ const proc = spawn('claude', ['-c', '--print', prompt], {
38
+ cwd: workingDirectory,
39
+ stdio: ['ignore', 'pipe', 'pipe'],
40
+ env: {
41
+ ...process.env,
42
+ // TERM 설정으로 색상 코드 방지
43
+ TERM: 'dumb',
44
+ NO_COLOR: '1',
45
+ },
46
+ });
47
+
48
+ let stdout = '';
49
+ let stderr = '';
50
+ let timedOut = false;
51
+
52
+ // 타임아웃 설정
53
+ const timer = setTimeout(() => {
54
+ timedOut = true;
55
+ proc.kill('SIGTERM');
56
+ }, timeoutMs);
57
+
58
+ proc.stdout?.on('data', (data) => {
59
+ stdout += data.toString();
60
+ });
61
+
62
+ proc.stderr?.on('data', (data) => {
63
+ stderr += data.toString();
64
+ });
65
+
66
+ proc.on('close', async (code) => {
67
+ clearTimeout(timer);
68
+
69
+ // 실행 후 새로 생성된 파일 찾기
70
+ const afterFiles = await getDirectorySnapshot(workingDirectory);
71
+ const newFiles = findNewFiles(beforeFiles, afterFiles);
72
+
73
+ // 새 파일들을 output 디렉토리로 복사
74
+ const generatedFiles: Array<{ name: string; path: string; type: string }> = [];
75
+
76
+ for (const filePath of newFiles) {
77
+ const fileName = basename(filePath);
78
+ const destPath = join(outputDirectory, fileName);
79
+ const fileType = getFileType(fileName);
80
+
81
+ try {
82
+ await copyFile(filePath, destPath);
83
+ generatedFiles.push({ name: fileName, path: destPath, type: fileType });
84
+ console.log(`Copied generated file: ${filePath} -> ${destPath}`);
85
+ } catch (err) {
86
+ console.error(`Failed to copy file ${filePath}:`, err);
87
+ }
88
+ }
89
+
90
+ // stdout 결과도 파일로 저장 (결과가 있으면)
91
+ if (stdout.trim()) {
92
+ const { writeFile } = await import('fs/promises');
93
+ const resultPath = join(outputDirectory, 'claude-output.md');
94
+ await writeFile(resultPath, stdout, 'utf-8');
95
+ generatedFiles.push({ name: 'claude-output.md', path: resultPath, type: 'markdown' });
96
+ }
97
+
98
+ resolve({
99
+ success: code === 0 && !timedOut,
100
+ stdout: timedOut ? stdout + '\n[Execution timed out]' : stdout,
101
+ stderr,
102
+ exitCode: code,
103
+ generatedFiles,
104
+ });
105
+ });
106
+
107
+ proc.on('error', (err) => {
108
+ clearTimeout(timer);
109
+ resolve({
110
+ success: false,
111
+ stdout: '',
112
+ stderr: err.message,
113
+ exitCode: null,
114
+ generatedFiles: [],
115
+ });
116
+ });
117
+ });
118
+ }
119
+
120
+ /**
121
+ * 디렉토리의 파일 스냅샷 (재귀적으로 1단계까지만)
122
+ */
123
+ async function getDirectorySnapshot(dir: string): Promise<Map<string, number>> {
124
+ const snapshot = new Map<string, number>();
125
+
126
+ if (!existsSync(dir)) {
127
+ return snapshot;
128
+ }
129
+
130
+ try {
131
+ const entries = await readdir(dir, { withFileTypes: true });
132
+
133
+ for (const entry of entries) {
134
+ // 숨김 파일 및 특정 폴더 제외
135
+ if (entry.name.startsWith('.') || entry.name === 'node_modules' || entry.name === '__pycache__') {
136
+ continue;
137
+ }
138
+
139
+ const fullPath = join(dir, entry.name);
140
+
141
+ if (entry.isFile()) {
142
+ const stats = await stat(fullPath);
143
+ snapshot.set(fullPath, stats.mtimeMs);
144
+ }
145
+ }
146
+ } catch (err) {
147
+ console.error('Error reading directory:', err);
148
+ }
149
+
150
+ return snapshot;
151
+ }
152
+
153
+ /**
154
+ * 파일 확장자로 타입 결정
155
+ */
156
+ function getFileType(fileName: string): string {
157
+ const ext = fileName.split('.').pop()?.toLowerCase() || '';
158
+ const typeMap: Record<string, string> = {
159
+ md: 'markdown',
160
+ txt: 'text',
161
+ json: 'json',
162
+ js: 'javascript',
163
+ ts: 'typescript',
164
+ py: 'python',
165
+ html: 'html',
166
+ css: 'css',
167
+ png: 'image',
168
+ jpg: 'image',
169
+ jpeg: 'image',
170
+ gif: 'image',
171
+ svg: 'image',
172
+ pdf: 'pdf',
173
+ xlsx: 'excel',
174
+ xls: 'excel',
175
+ pptx: 'powerpoint',
176
+ ppt: 'powerpoint',
177
+ docx: 'word',
178
+ doc: 'word',
179
+ };
180
+ return typeMap[ext] || 'file';
181
+ }
182
+
183
+ /**
184
+ * 새로 생성되거나 수정된 파일 찾기
185
+ */
186
+ function findNewFiles(before: Map<string, number>, after: Map<string, number>): string[] {
187
+ const newFiles: string[] = [];
188
+
189
+ for (const [path, mtime] of after) {
190
+ const beforeMtime = before.get(path);
191
+
192
+ // 새 파일이거나 수정된 파일
193
+ if (beforeMtime === undefined || mtime > beforeMtime) {
194
+ newFiles.push(path);
195
+ }
196
+ }
197
+
198
+ return newFiles;
199
+ }
200
+
201
+ /**
202
+ * 노드별 프롬프트 생성
203
+ */
204
+ export function buildNodePrompt(
205
+ nodeType: string,
206
+ nodeData: Record<string, unknown>,
207
+ previousResults: string
208
+ ): string {
209
+ const lines: string[] = [];
210
+
211
+ if (nodeType === 'subagent') {
212
+ const role = nodeData.role as string || 'assistant';
213
+ const description = nodeData.description as string || '';
214
+ const systemPrompt = nodeData.systemPrompt as string || '';
215
+
216
+ lines.push(`You are a ${role}.`);
217
+ if (systemPrompt) {
218
+ lines.push(systemPrompt);
219
+ }
220
+ lines.push('');
221
+ lines.push('## Task');
222
+ lines.push(description || 'Complete the following task based on the input.');
223
+ lines.push('');
224
+ if (previousResults) {
225
+ lines.push('## Input from previous steps');
226
+ lines.push(previousResults);
227
+ lines.push('');
228
+ }
229
+ lines.push('Please complete this task and provide the result.');
230
+ } else if (nodeType === 'skill') {
231
+ const skillId = nodeData.skillId as string || '';
232
+ const description = nodeData.description as string || '';
233
+
234
+ if (skillId) {
235
+ lines.push(`Execute the skill: /${skillId}`);
236
+ }
237
+ lines.push('');
238
+ lines.push('## Task');
239
+ lines.push(description || 'Execute this skill with the provided input.');
240
+ lines.push('');
241
+ if (previousResults) {
242
+ lines.push('## Input');
243
+ lines.push(previousResults);
244
+ lines.push('');
245
+ }
246
+ } else {
247
+ // Generic
248
+ lines.push('## Task');
249
+ lines.push(nodeData.description as string || 'Process the following input.');
250
+ lines.push('');
251
+ if (previousResults) {
252
+ lines.push('## Input');
253
+ lines.push(previousResults);
254
+ }
255
+ }
256
+
257
+ return lines.join('\n');
258
+ }
@@ -9,10 +9,12 @@ interface NodeData {
9
9
  // Skill specific
10
10
  skillId?: string;
11
11
  skillPath?: string;
12
+ upstream?: string[]; // Connected upstream agents/skills
13
+ downstream?: string[]; // Connected downstream agents/skills
12
14
  // Subagent specific
13
15
  tools?: string[];
14
16
  model?: string;
15
- skills?: string[]; // Connected skills
17
+ skills?: string[]; // Connected downstream skills
16
18
  systemPrompt?: string;
17
19
  // Command specific
18
20
  commandName?: string;
@@ -99,6 +101,8 @@ export class NodeSyncService {
99
101
 
100
102
  /**
101
103
  * 엣지 연결 시 관계 업데이트
104
+ * - source의 downstream에 target 추가
105
+ * - target의 upstream에 source 추가
102
106
  */
103
107
  async syncEdge(edge: EdgeData, nodes: NodeData[]): Promise<{ success: boolean; error?: string }> {
104
108
  try {
@@ -109,26 +113,75 @@ export class NodeSyncService {
109
113
  return { success: false, error: 'Source or target node not found' };
110
114
  }
111
115
 
112
- // 서브에이전트 스킬 연결: 서브에이전트의 skills 필드 업데이트
116
+ const sourceId = this.getNodeIdentifier(sourceNode);
117
+ const targetId = this.getNodeIdentifier(targetNode);
118
+
119
+ // 서브에이전트 → 스킬 연결
113
120
  if (sourceNode.type === 'subagent' && targetNode.type === 'skill') {
121
+ // 에이전트의 skills 필드 업데이트
114
122
  const skills = sourceNode.skills || [];
115
- const skillId = targetNode.skillId || this.toKebabCase(targetNode.label);
116
- if (!skills.includes(skillId)) {
117
- skills.push(skillId);
123
+ if (!skills.includes(targetId)) {
124
+ skills.push(targetId);
118
125
  sourceNode.skills = skills;
119
126
  await this.syncSubagentNode(sourceNode);
120
127
  }
128
+
129
+ // 스킬의 upstream 필드 업데이트
130
+ const upstream = targetNode.upstream || [];
131
+ if (!upstream.includes(sourceId)) {
132
+ upstream.push(sourceId);
133
+ targetNode.upstream = upstream;
134
+ await this.syncSkillNode(targetNode);
135
+ }
121
136
  }
122
137
 
123
- // 스킬 → 서브에이전트 연결: 서브에이전트의 skills 필드 업데이트
138
+ // 스킬 → 스킬 연결
139
+ if (sourceNode.type === 'skill' && targetNode.type === 'skill') {
140
+ // source의 downstream 업데이트
141
+ const downstream = sourceNode.downstream || [];
142
+ if (!downstream.includes(targetId)) {
143
+ downstream.push(targetId);
144
+ sourceNode.downstream = downstream;
145
+ await this.syncSkillNode(sourceNode);
146
+ }
147
+
148
+ // target의 upstream 업데이트
149
+ const upstream = targetNode.upstream || [];
150
+ if (!upstream.includes(sourceId)) {
151
+ upstream.push(sourceId);
152
+ targetNode.upstream = upstream;
153
+ await this.syncSkillNode(targetNode);
154
+ }
155
+ }
156
+
157
+ // 스킬 → 서브에이전트 연결
124
158
  if (sourceNode.type === 'skill' && targetNode.type === 'subagent') {
159
+ // 에이전트의 upstream skills 필드 업데이트
125
160
  const skills = targetNode.skills || [];
126
- const skillId = sourceNode.skillId || this.toKebabCase(sourceNode.label);
127
- if (!skills.includes(skillId)) {
128
- skills.push(skillId);
161
+ if (!skills.includes(sourceId)) {
162
+ skills.push(sourceId);
129
163
  targetNode.skills = skills;
130
164
  await this.syncSubagentNode(targetNode);
131
165
  }
166
+
167
+ // 스킬의 downstream 필드 업데이트
168
+ const downstream = sourceNode.downstream || [];
169
+ if (!downstream.includes(targetId)) {
170
+ downstream.push(targetId);
171
+ sourceNode.downstream = downstream;
172
+ await this.syncSkillNode(sourceNode);
173
+ }
174
+ }
175
+
176
+ // 서브에이전트 → 서브에이전트 연결
177
+ if (sourceNode.type === 'subagent' && targetNode.type === 'subagent') {
178
+ // source의 downstream agents
179
+ const sourceDownstream = sourceNode.skills || [];
180
+ if (!sourceDownstream.includes(targetId)) {
181
+ sourceDownstream.push(targetId);
182
+ sourceNode.skills = sourceDownstream;
183
+ await this.syncSubagentNode(sourceNode);
184
+ }
132
185
  }
133
186
 
134
187
  return { success: true };
@@ -138,6 +191,16 @@ export class NodeSyncService {
138
191
  }
139
192
  }
140
193
 
194
+ /**
195
+ * 노드의 식별자 반환 (skillId 또는 kebab-case label)
196
+ */
197
+ private getNodeIdentifier(node: NodeData): string {
198
+ if (node.type === 'skill') {
199
+ return node.skillId || this.toKebabCase(node.label);
200
+ }
201
+ return this.toKebabCase(node.label);
202
+ }
203
+
141
204
  /**
142
205
  * 엣지 삭제 시 관계 업데이트
143
206
  */
@@ -150,17 +213,42 @@ export class NodeSyncService {
150
213
  return { success: true }; // 노드가 없으면 무시
151
214
  }
152
215
 
153
- // 서브에이전트에서 스킬 제거
216
+ const sourceId = this.getNodeIdentifier(sourceNode);
217
+ const targetId = this.getNodeIdentifier(targetNode);
218
+
219
+ // 서브에이전트 → 스킬 연결 해제
154
220
  if (sourceNode.type === 'subagent' && targetNode.type === 'skill') {
155
- const skillId = targetNode.skillId || this.toKebabCase(targetNode.label);
156
- sourceNode.skills = (sourceNode.skills || []).filter(s => s !== skillId);
221
+ // 에이전트에서 스킬 제거
222
+ sourceNode.skills = (sourceNode.skills || []).filter(s => s !== targetId);
157
223
  await this.syncSubagentNode(sourceNode);
224
+
225
+ // 스킬에서 upstream 제거
226
+ targetNode.upstream = (targetNode.upstream || []).filter(s => s !== sourceId);
227
+ await this.syncSkillNode(targetNode);
158
228
  }
159
229
 
230
+ // 스킬 → 스킬 연결 해제
231
+ if (sourceNode.type === 'skill' && targetNode.type === 'skill') {
232
+ sourceNode.downstream = (sourceNode.downstream || []).filter(s => s !== targetId);
233
+ await this.syncSkillNode(sourceNode);
234
+
235
+ targetNode.upstream = (targetNode.upstream || []).filter(s => s !== sourceId);
236
+ await this.syncSkillNode(targetNode);
237
+ }
238
+
239
+ // 스킬 → 서브에이전트 연결 해제
160
240
  if (sourceNode.type === 'skill' && targetNode.type === 'subagent') {
161
- const skillId = sourceNode.skillId || this.toKebabCase(sourceNode.label);
162
- targetNode.skills = (targetNode.skills || []).filter(s => s !== skillId);
241
+ targetNode.skills = (targetNode.skills || []).filter(s => s !== sourceId);
163
242
  await this.syncSubagentNode(targetNode);
243
+
244
+ sourceNode.downstream = (sourceNode.downstream || []).filter(s => s !== targetId);
245
+ await this.syncSkillNode(sourceNode);
246
+ }
247
+
248
+ // 서브에이전트 → 서브에이전트 연결 해제
249
+ if (sourceNode.type === 'subagent' && targetNode.type === 'subagent') {
250
+ sourceNode.skills = (sourceNode.skills || []).filter(s => s !== targetId);
251
+ await this.syncSubagentNode(sourceNode);
164
252
  }
165
253
 
166
254
  return { success: true };
@@ -179,20 +267,38 @@ export class NodeSyncService {
179
267
 
180
268
  await fs.mkdir(skillPath, { recursive: true });
181
269
 
270
+ // Frontmatter 구성
271
+ const frontmatter: Record<string, string> = {
272
+ name: skillId,
273
+ description: node.description || node.label,
274
+ };
275
+
276
+ // upstream 연결 추가
277
+ if (node.upstream && node.upstream.length > 0) {
278
+ frontmatter.upstream = node.upstream.join(', ');
279
+ }
280
+
281
+ // downstream 연결 추가
282
+ if (node.downstream && node.downstream.length > 0) {
283
+ frontmatter.downstream = node.downstream.join(', ');
284
+ }
285
+
182
286
  // 기존 파일이 있으면 읽어서 업데이트, 없으면 새로 생성
183
287
  let content = '';
184
288
  try {
185
289
  content = await fs.readFile(skillMdPath, 'utf-8');
186
290
  // frontmatter 업데이트
187
- content = this.updateFrontmatter(content, {
188
- name: skillId,
189
- description: node.description || node.label,
190
- });
291
+ for (const [key, value] of Object.entries(frontmatter)) {
292
+ content = this.updateFrontmatter(content, { [key]: value });
293
+ }
191
294
  } catch {
192
295
  // 새로 생성
296
+ const frontmatterStr = Object.entries(frontmatter)
297
+ .map(([key, value]) => `${key}: ${value}`)
298
+ .join('\n');
299
+
193
300
  content = `---
194
- name: ${skillId}
195
- description: ${node.description || node.label}
301
+ ${frontmatterStr}
196
302
  ---
197
303
 
198
304
  # ${node.label}