makecc 0.2.5 → 0.2.8

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/server/index.ts CHANGED
@@ -5,11 +5,13 @@ import { Server } from 'socket.io';
5
5
  import cors from 'cors';
6
6
  import { join, dirname } from 'path';
7
7
  import { fileURLToPath } from 'url';
8
- import { existsSync } from 'fs';
8
+ import { existsSync, promises as fs } from 'fs';
9
9
  import { ClaudeService } from './services/claudeService';
10
10
  import { fileService } from './services/fileService';
11
11
  import { workflowAIService } from './services/workflowAIService';
12
- import { skillGeneratorService } from './services/skillGeneratorService';
12
+ import { skillGeneratorService, type SkillProgressEvent } from './services/skillGeneratorService';
13
+ import { nodeSyncService } from './services/nodeSyncService';
14
+ import { configLoaderService } from './services/configLoaderService';
13
15
  import { workflowExecutionService } from './services/workflowExecutionService';
14
16
  import { executeInTerminal, getClaudeCommand } from './services/terminalService';
15
17
  import type { WorkflowExecutionRequest, NodeExecutionUpdate } from './types';
@@ -108,6 +110,86 @@ app.get('/api/settings/api-key', async (req, res) => {
108
110
  }
109
111
  });
110
112
 
113
+ // Sync node to file system
114
+ app.post('/api/sync/node', async (req, res) => {
115
+ try {
116
+ const { node } = req.body;
117
+ if (!node) {
118
+ return res.status(400).json({ message: 'Node data is required' });
119
+ }
120
+
121
+ const result = await nodeSyncService.syncNode(node);
122
+ if (result.success) {
123
+ res.json(result);
124
+ } else {
125
+ res.status(500).json({ message: result.error });
126
+ }
127
+ } catch (error) {
128
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
129
+ res.status(500).json({ message: errorMessage });
130
+ }
131
+ });
132
+
133
+ // Delete node from file system
134
+ app.delete('/api/sync/node', async (req, res) => {
135
+ try {
136
+ const { node } = req.body;
137
+ if (!node) {
138
+ return res.status(400).json({ message: 'Node data is required' });
139
+ }
140
+
141
+ const result = await nodeSyncService.deleteNode(node);
142
+ if (result.success) {
143
+ res.json(result);
144
+ } else {
145
+ res.status(500).json({ message: result.error });
146
+ }
147
+ } catch (error) {
148
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
149
+ res.status(500).json({ message: errorMessage });
150
+ }
151
+ });
152
+
153
+ // Sync edge (connection) to file system
154
+ app.post('/api/sync/edge', async (req, res) => {
155
+ try {
156
+ const { edge, nodes } = req.body;
157
+ if (!edge || !nodes) {
158
+ return res.status(400).json({ message: 'Edge and nodes data are required' });
159
+ }
160
+
161
+ const result = await nodeSyncService.syncEdge(edge, nodes);
162
+ if (result.success) {
163
+ res.json(result);
164
+ } else {
165
+ res.status(500).json({ message: result.error });
166
+ }
167
+ } catch (error) {
168
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
169
+ res.status(500).json({ message: errorMessage });
170
+ }
171
+ });
172
+
173
+ // Remove edge from file system
174
+ app.delete('/api/sync/edge', async (req, res) => {
175
+ try {
176
+ const { edge, nodes } = req.body;
177
+ if (!edge || !nodes) {
178
+ return res.status(400).json({ message: 'Edge and nodes data are required' });
179
+ }
180
+
181
+ const result = await nodeSyncService.removeEdge(edge, nodes);
182
+ if (result.success) {
183
+ res.json(result);
184
+ } else {
185
+ res.status(500).json({ message: result.error });
186
+ }
187
+ } catch (error) {
188
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
189
+ res.status(500).json({ message: errorMessage });
190
+ }
191
+ });
192
+
111
193
  // Generate skill using AI
112
194
  app.post('/api/generate/skill', async (req, res) => {
113
195
  try {
@@ -142,6 +224,22 @@ app.post('/api/generate/skill', async (req, res) => {
142
224
  }
143
225
  });
144
226
 
227
+ // Load existing Claude config from .claude/ directory
228
+ app.get('/api/load/claude-config', async (req, res) => {
229
+ try {
230
+ const config = await configLoaderService.loadAll();
231
+ console.log(
232
+ `Loaded config: ${config.skills.length} skills, ${config.subagents.length} agents, ` +
233
+ `${config.commands.length} commands, ${config.hooks.length} hooks`
234
+ );
235
+ res.json(config);
236
+ } catch (error) {
237
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
238
+ console.error('Load config error:', errorMessage);
239
+ res.status(500).json({ message: errorMessage });
240
+ }
241
+ });
242
+
145
243
  // Generate workflow using AI
146
244
  app.post('/api/generate/workflow', async (req, res) => {
147
245
  try {
@@ -173,6 +271,124 @@ app.post('/api/generate/workflow', async (req, res) => {
173
271
  }
174
272
  });
175
273
 
274
+ // Test skill execution
275
+ app.post('/api/skill/test', async (req, res) => {
276
+ const { spawn } = await import('child_process');
277
+
278
+ try {
279
+ const { skillPath, args } = req.body as { skillPath: string; args?: string[] };
280
+
281
+ if (!skillPath) {
282
+ return res.status(400).json({ message: 'Skill path is required' });
283
+ }
284
+
285
+ // 보안: 프로젝트 경로 내의 파일만 접근 허용
286
+ const projectRoot = fileService.getProjectPath();
287
+ if (!skillPath.startsWith(projectRoot)) {
288
+ return res.status(403).json({ message: 'Access denied' });
289
+ }
290
+
291
+ const mainPyPath = join(skillPath, 'scripts', 'main.py');
292
+ if (!existsSync(mainPyPath)) {
293
+ return res.status(404).json({ message: 'main.py not found' });
294
+ }
295
+
296
+ // 전역 venv의 python 사용
297
+ const homeDir = process.env.HOME || process.env.USERPROFILE || '';
298
+ const pythonPath = join(homeDir, '.claude', 'venv', 'bin', 'python');
299
+ const command = existsSync(pythonPath) ? pythonPath : 'python3';
300
+
301
+ console.log(`Testing skill: ${command} ${mainPyPath}`);
302
+
303
+ return new Promise<void>((resolve) => {
304
+ const proc = spawn(command, [mainPyPath, ...(args || [])], {
305
+ cwd: skillPath,
306
+ stdio: ['ignore', 'pipe', 'pipe'],
307
+ timeout: 30000, // 30초 타임아웃
308
+ });
309
+
310
+ let stdout = '';
311
+ let stderr = '';
312
+
313
+ proc.stdout?.on('data', (data) => {
314
+ stdout += data.toString();
315
+ });
316
+
317
+ proc.stderr?.on('data', (data) => {
318
+ stderr += data.toString();
319
+ });
320
+
321
+ proc.on('close', (code) => {
322
+ res.json({
323
+ success: code === 0,
324
+ exitCode: code,
325
+ stdout: stdout.trim(),
326
+ stderr: stderr.trim(),
327
+ });
328
+ resolve();
329
+ });
330
+
331
+ proc.on('error', (err) => {
332
+ res.status(500).json({
333
+ success: false,
334
+ error: err.message,
335
+ });
336
+ resolve();
337
+ });
338
+ });
339
+ } catch (error) {
340
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
341
+ console.error('Skill test error:', errorMessage);
342
+ res.status(500).json({ message: errorMessage });
343
+ }
344
+ });
345
+
346
+ // Read skill files for preview
347
+ app.get('/api/skill/files', async (req, res) => {
348
+ try {
349
+ const skillPath = req.query.path as string;
350
+
351
+ if (!skillPath) {
352
+ return res.status(400).json({ message: 'Skill path is required' });
353
+ }
354
+
355
+ // 보안: 프로젝트 경로 내의 파일만 접근 허용
356
+ const projectRoot = fileService.getProjectPath();
357
+ if (!skillPath.startsWith(projectRoot)) {
358
+ return res.status(403).json({ message: 'Access denied' });
359
+ }
360
+
361
+ const files: Array<{ name: string; content: string; language: string }> = [];
362
+
363
+ // SKILL.md 읽기
364
+ const skillMdPath = join(skillPath, 'SKILL.md');
365
+ if (existsSync(skillMdPath)) {
366
+ const content = await fs.readFile(skillMdPath, 'utf-8');
367
+ files.push({ name: 'SKILL.md', content, language: 'markdown' });
368
+ }
369
+
370
+ // scripts/main.py 읽기
371
+ const mainPyPath = join(skillPath, 'scripts', 'main.py');
372
+ if (existsSync(mainPyPath)) {
373
+ const content = await fs.readFile(mainPyPath, 'utf-8');
374
+ files.push({ name: 'scripts/main.py', content, language: 'python' });
375
+ }
376
+
377
+ // requirements.txt 읽기
378
+ const requirementsPath = join(skillPath, 'requirements.txt');
379
+ if (existsSync(requirementsPath)) {
380
+ const content = await fs.readFile(requirementsPath, 'utf-8');
381
+ files.push({ name: 'requirements.txt', content, language: 'text' });
382
+ }
383
+
384
+ res.json({ files });
385
+ } catch (error) {
386
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
387
+ console.error('Read skill files error:', errorMessage);
388
+ res.status(500).json({ message: errorMessage });
389
+ }
390
+ });
391
+
176
392
  // Socket.io connection handling
177
393
  io.on('connection', (socket) => {
178
394
  console.log('Client connected:', socket.id);
@@ -310,6 +526,45 @@ io.on('connection', (socket) => {
310
526
  socket.emit('workflow:cancelled');
311
527
  });
312
528
 
529
+ // Generate skill with real-time progress
530
+ socket.on('generate:skill', async (data: {
531
+ prompt: string;
532
+ apiMode?: 'proxy' | 'direct';
533
+ apiKey?: string;
534
+ proxyUrl?: string;
535
+ }) => {
536
+ console.log('Generating skill via Socket.IO:', data.prompt);
537
+
538
+ try {
539
+ const result = await skillGeneratorService.generate(
540
+ data.prompt,
541
+ {
542
+ apiMode: data.apiMode || 'proxy',
543
+ apiKey: data.apiKey,
544
+ proxyUrl: data.proxyUrl,
545
+ },
546
+ // Progress callback
547
+ (event: SkillProgressEvent) => {
548
+ socket.emit('skill:progress', event);
549
+ }
550
+ );
551
+
552
+ if (result.success) {
553
+ socket.emit('skill:completed', {
554
+ skill: result.skill,
555
+ savedPath: result.savedPath,
556
+ });
557
+ } else {
558
+ socket.emit('skill:error', {
559
+ error: result.error,
560
+ });
561
+ }
562
+ } catch (error) {
563
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
564
+ socket.emit('skill:error', { error: errorMessage });
565
+ }
566
+ });
567
+
313
568
  socket.on('disconnect', () => {
314
569
  console.log('Client disconnected:', socket.id);
315
570
  });
@@ -0,0 +1,260 @@
1
+ import * as fs from 'fs/promises';
2
+ import * as path from 'path';
3
+
4
+ interface LoadedSkill {
5
+ id: string;
6
+ type: 'skill';
7
+ label: string;
8
+ description: string;
9
+ skillId: string;
10
+ skillPath: string;
11
+ }
12
+
13
+ interface LoadedSubagent {
14
+ id: string;
15
+ type: 'subagent';
16
+ label: string;
17
+ description: string;
18
+ tools: string[];
19
+ model?: string;
20
+ skills: string[];
21
+ systemPrompt: string;
22
+ }
23
+
24
+ interface LoadedCommand {
25
+ id: string;
26
+ type: 'command';
27
+ label: string;
28
+ description: string;
29
+ commandName: string;
30
+ commandContent: string;
31
+ }
32
+
33
+ interface LoadedHook {
34
+ id: string;
35
+ type: 'hook';
36
+ label: string;
37
+ description: string;
38
+ hookEvent: string;
39
+ hookMatcher: string;
40
+ hookCommand: string;
41
+ }
42
+
43
+ export type LoadedNode = LoadedSkill | LoadedSubagent | LoadedCommand | LoadedHook;
44
+
45
+ export interface ClaudeConfig {
46
+ skills: LoadedSkill[];
47
+ subagents: LoadedSubagent[];
48
+ commands: LoadedCommand[];
49
+ hooks: LoadedHook[];
50
+ }
51
+
52
+ export class ConfigLoaderService {
53
+ private projectRoot: string;
54
+
55
+ constructor(projectRoot?: string) {
56
+ this.projectRoot = projectRoot || process.env.MAKECC_PROJECT_PATH || process.cwd();
57
+ }
58
+
59
+ /**
60
+ * .claude/ 디렉토리에서 모든 설정 로드
61
+ */
62
+ async loadAll(): Promise<ClaudeConfig> {
63
+ const [skills, subagents, commands, hooks] = await Promise.all([
64
+ this.loadSkills(),
65
+ this.loadSubagents(),
66
+ this.loadCommands(),
67
+ this.loadHooks(),
68
+ ]);
69
+
70
+ return { skills, subagents, commands, hooks };
71
+ }
72
+
73
+ /**
74
+ * .claude/skills/ 에서 스킬 로드
75
+ */
76
+ async loadSkills(): Promise<LoadedSkill[]> {
77
+ const skillsDir = path.join(this.projectRoot, '.claude', 'skills');
78
+ const skills: LoadedSkill[] = [];
79
+
80
+ try {
81
+ const entries = await fs.readdir(skillsDir, { withFileTypes: true });
82
+
83
+ for (const entry of entries) {
84
+ if (entry.isDirectory()) {
85
+ const skillMdPath = path.join(skillsDir, entry.name, 'SKILL.md');
86
+ try {
87
+ const content = await fs.readFile(skillMdPath, 'utf-8');
88
+ const parsed = this.parseFrontmatter(content);
89
+
90
+ skills.push({
91
+ id: `skill-${entry.name}`,
92
+ type: 'skill',
93
+ label: parsed.frontmatter.name || entry.name,
94
+ description: parsed.frontmatter.description || '',
95
+ skillId: entry.name,
96
+ skillPath: path.join(skillsDir, entry.name),
97
+ });
98
+ } catch {
99
+ // SKILL.md가 없거나 읽을 수 없으면 스킵
100
+ }
101
+ }
102
+ }
103
+ } catch {
104
+ // skills 디렉토리가 없으면 빈 배열 반환
105
+ }
106
+
107
+ return skills;
108
+ }
109
+
110
+ /**
111
+ * .claude/agents/ 에서 서브에이전트 로드
112
+ */
113
+ async loadSubagents(): Promise<LoadedSubagent[]> {
114
+ const agentsDir = path.join(this.projectRoot, '.claude', 'agents');
115
+ const subagents: LoadedSubagent[] = [];
116
+
117
+ try {
118
+ const entries = await fs.readdir(agentsDir, { withFileTypes: true });
119
+
120
+ for (const entry of entries) {
121
+ if (entry.isFile() && entry.name.endsWith('.md')) {
122
+ const agentPath = path.join(agentsDir, entry.name);
123
+ try {
124
+ const content = await fs.readFile(agentPath, 'utf-8');
125
+ const parsed = this.parseFrontmatter(content);
126
+ const agentName = entry.name.replace('.md', '');
127
+
128
+ subagents.push({
129
+ id: `subagent-${agentName}`,
130
+ type: 'subagent',
131
+ label: parsed.frontmatter.name || agentName,
132
+ description: parsed.frontmatter.description || '',
133
+ tools: this.parseList(parsed.frontmatter.tools),
134
+ model: parsed.frontmatter.model,
135
+ skills: this.parseList(parsed.frontmatter.skills),
136
+ systemPrompt: parsed.body,
137
+ });
138
+ } catch {
139
+ // 읽을 수 없으면 스킵
140
+ }
141
+ }
142
+ }
143
+ } catch {
144
+ // agents 디렉토리가 없으면 빈 배열 반환
145
+ }
146
+
147
+ return subagents;
148
+ }
149
+
150
+ /**
151
+ * .claude/commands/ 에서 커맨드 로드
152
+ */
153
+ async loadCommands(): Promise<LoadedCommand[]> {
154
+ const commandsDir = path.join(this.projectRoot, '.claude', 'commands');
155
+ const commands: LoadedCommand[] = [];
156
+
157
+ try {
158
+ const entries = await fs.readdir(commandsDir, { withFileTypes: true });
159
+
160
+ for (const entry of entries) {
161
+ if (entry.isFile() && entry.name.endsWith('.md')) {
162
+ const cmdPath = path.join(commandsDir, entry.name);
163
+ try {
164
+ const content = await fs.readFile(cmdPath, 'utf-8');
165
+ const parsed = this.parseFrontmatter(content);
166
+ const cmdName = entry.name.replace('.md', '');
167
+
168
+ commands.push({
169
+ id: `command-${cmdName}`,
170
+ type: 'command',
171
+ label: parsed.frontmatter.name || cmdName,
172
+ description: parsed.frontmatter.description || '',
173
+ commandName: cmdName,
174
+ commandContent: content,
175
+ });
176
+ } catch {
177
+ // 읽을 수 없으면 스킵
178
+ }
179
+ }
180
+ }
181
+ } catch {
182
+ // commands 디렉토리가 없으면 빈 배열 반환
183
+ }
184
+
185
+ return commands;
186
+ }
187
+
188
+ /**
189
+ * .claude/settings.json 에서 훅 로드
190
+ */
191
+ async loadHooks(): Promise<LoadedHook[]> {
192
+ const settingsPath = path.join(this.projectRoot, '.claude', 'settings.json');
193
+ const hooks: LoadedHook[] = [];
194
+
195
+ try {
196
+ const content = await fs.readFile(settingsPath, 'utf-8');
197
+ const settings = JSON.parse(content);
198
+
199
+ if (settings.hooks) {
200
+ for (const [event, eventHooks] of Object.entries(settings.hooks)) {
201
+ if (Array.isArray(eventHooks)) {
202
+ for (let i = 0; i < eventHooks.length; i++) {
203
+ const hookConfig = eventHooks[i] as {
204
+ matcher?: string;
205
+ hooks?: Array<{ type: string; command: string }>;
206
+ };
207
+
208
+ const command = hookConfig.hooks?.[0]?.command || '';
209
+
210
+ hooks.push({
211
+ id: `hook-${event}-${i}`,
212
+ type: 'hook',
213
+ label: `${event} Hook`,
214
+ description: `Matcher: ${hookConfig.matcher || '*'}`,
215
+ hookEvent: event,
216
+ hookMatcher: hookConfig.matcher || '*',
217
+ hookCommand: command,
218
+ });
219
+ }
220
+ }
221
+ }
222
+ }
223
+ } catch {
224
+ // settings.json이 없거나 파싱 에러면 빈 배열 반환
225
+ }
226
+
227
+ return hooks;
228
+ }
229
+
230
+ // ===== Private Methods =====
231
+
232
+ private parseFrontmatter(content: string): { frontmatter: Record<string, string>; body: string } {
233
+ const frontmatter: Record<string, string> = {};
234
+ let body = content;
235
+
236
+ const match = content.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
237
+ if (match) {
238
+ const fmContent = match[1];
239
+ body = match[2].trim();
240
+
241
+ for (const line of fmContent.split('\n')) {
242
+ const colonIndex = line.indexOf(':');
243
+ if (colonIndex > 0) {
244
+ const key = line.slice(0, colonIndex).trim();
245
+ const value = line.slice(colonIndex + 1).trim();
246
+ frontmatter[key] = value;
247
+ }
248
+ }
249
+ }
250
+
251
+ return { frontmatter, body };
252
+ }
253
+
254
+ private parseList(value: string | undefined): string[] {
255
+ if (!value) return [];
256
+ return value.split(',').map(s => s.trim()).filter(Boolean);
257
+ }
258
+ }
259
+
260
+ export const configLoaderService = new ConfigLoaderService();