makecc 0.2.4 → 0.2.7

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.
@@ -5,7 +5,7 @@
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
7
  <title>vite-temp</title>
8
- <script type="module" crossorigin src="/assets/index-c7WYmobg.js"></script>
8
+ <script type="module" crossorigin src="/assets/index-CclsqEDr.js"></script>
9
9
  <link rel="stylesheet" crossorigin href="/assets/index-DLT8bQFx.css">
10
10
  </head>
11
11
  <body>
@@ -10,6 +10,7 @@ import { ClaudeService } from './services/claudeService';
10
10
  import { fileService } from './services/fileService';
11
11
  import { workflowAIService } from './services/workflowAIService';
12
12
  import { skillGeneratorService } from './services/skillGeneratorService';
13
+ import { nodeSyncService } from './services/nodeSyncService';
13
14
  import { workflowExecutionService } from './services/workflowExecutionService';
14
15
  import { executeInTerminal } from './services/terminalService';
15
16
  const __filename = fileURLToPath(import.meta.url);
@@ -90,6 +91,86 @@ app.get('/api/settings/api-key', async (req, res) => {
90
91
  res.status(500).json({ message: errorMessage });
91
92
  }
92
93
  });
94
+ // Sync node to file system
95
+ app.post('/api/sync/node', async (req, res) => {
96
+ try {
97
+ const { node } = req.body;
98
+ if (!node) {
99
+ return res.status(400).json({ message: 'Node data is required' });
100
+ }
101
+ const result = await nodeSyncService.syncNode(node);
102
+ if (result.success) {
103
+ res.json(result);
104
+ }
105
+ else {
106
+ res.status(500).json({ message: result.error });
107
+ }
108
+ }
109
+ catch (error) {
110
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
111
+ res.status(500).json({ message: errorMessage });
112
+ }
113
+ });
114
+ // Delete node from file system
115
+ app.delete('/api/sync/node', async (req, res) => {
116
+ try {
117
+ const { node } = req.body;
118
+ if (!node) {
119
+ return res.status(400).json({ message: 'Node data is required' });
120
+ }
121
+ const result = await nodeSyncService.deleteNode(node);
122
+ if (result.success) {
123
+ res.json(result);
124
+ }
125
+ else {
126
+ res.status(500).json({ message: result.error });
127
+ }
128
+ }
129
+ catch (error) {
130
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
131
+ res.status(500).json({ message: errorMessage });
132
+ }
133
+ });
134
+ // Sync edge (connection) to file system
135
+ app.post('/api/sync/edge', async (req, res) => {
136
+ try {
137
+ const { edge, nodes } = req.body;
138
+ if (!edge || !nodes) {
139
+ return res.status(400).json({ message: 'Edge and nodes data are required' });
140
+ }
141
+ const result = await nodeSyncService.syncEdge(edge, nodes);
142
+ if (result.success) {
143
+ res.json(result);
144
+ }
145
+ else {
146
+ res.status(500).json({ message: result.error });
147
+ }
148
+ }
149
+ catch (error) {
150
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
151
+ res.status(500).json({ message: errorMessage });
152
+ }
153
+ });
154
+ // Remove edge from file system
155
+ app.delete('/api/sync/edge', async (req, res) => {
156
+ try {
157
+ const { edge, nodes } = req.body;
158
+ if (!edge || !nodes) {
159
+ return res.status(400).json({ message: 'Edge and nodes data are required' });
160
+ }
161
+ const result = await nodeSyncService.removeEdge(edge, nodes);
162
+ if (result.success) {
163
+ res.json(result);
164
+ }
165
+ else {
166
+ res.status(500).json({ message: result.error });
167
+ }
168
+ }
169
+ catch (error) {
170
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
171
+ res.status(500).json({ message: errorMessage });
172
+ }
173
+ });
93
174
  // Generate skill using AI
94
175
  app.post('/api/generate/skill', async (req, res) => {
95
176
  try {
@@ -0,0 +1,313 @@
1
+ import * as fs from 'fs/promises';
2
+ import * as path from 'path';
3
+ export class NodeSyncService {
4
+ projectRoot;
5
+ constructor(projectRoot) {
6
+ this.projectRoot = projectRoot || process.env.MAKECC_PROJECT_PATH || process.cwd();
7
+ }
8
+ /**
9
+ * 노드 생성/수정 시 파일 동기화
10
+ */
11
+ async syncNode(node) {
12
+ try {
13
+ switch (node.type) {
14
+ case 'skill':
15
+ return await this.syncSkillNode(node);
16
+ case 'subagent':
17
+ return await this.syncSubagentNode(node);
18
+ case 'command':
19
+ return await this.syncCommandNode(node);
20
+ case 'hook':
21
+ return await this.syncHookNode(node);
22
+ case 'input':
23
+ case 'output':
24
+ // 입력/출력 노드는 파일 저장 불필요
25
+ return { success: true };
26
+ default:
27
+ return { success: false, error: `Unknown node type: ${node.type}` };
28
+ }
29
+ }
30
+ catch (error) {
31
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
32
+ return { success: false, error: errorMessage };
33
+ }
34
+ }
35
+ /**
36
+ * 노드 삭제 시 파일 삭제
37
+ */
38
+ async deleteNode(node) {
39
+ try {
40
+ switch (node.type) {
41
+ case 'skill':
42
+ if (node.skillId) {
43
+ const skillPath = path.join(this.projectRoot, '.claude', 'skills', node.skillId);
44
+ await fs.rm(skillPath, { recursive: true, force: true });
45
+ }
46
+ break;
47
+ case 'subagent':
48
+ const agentName = this.toKebabCase(node.label);
49
+ const agentPath = path.join(this.projectRoot, '.claude', 'agents', `${agentName}.md`);
50
+ await fs.unlink(agentPath).catch(() => { });
51
+ break;
52
+ case 'command':
53
+ const cmdName = node.commandName || this.toKebabCase(node.label);
54
+ const cmdPath = path.join(this.projectRoot, '.claude', 'commands', `${cmdName}.md`);
55
+ await fs.unlink(cmdPath).catch(() => { });
56
+ break;
57
+ case 'hook':
58
+ await this.removeHookFromSettings(node);
59
+ break;
60
+ }
61
+ return { success: true };
62
+ }
63
+ catch (error) {
64
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
65
+ return { success: false, error: errorMessage };
66
+ }
67
+ }
68
+ /**
69
+ * 엣지 연결 시 관계 업데이트
70
+ */
71
+ async syncEdge(edge, nodes) {
72
+ try {
73
+ const sourceNode = nodes.find(n => n.id === edge.source);
74
+ const targetNode = nodes.find(n => n.id === edge.target);
75
+ if (!sourceNode || !targetNode) {
76
+ return { success: false, error: 'Source or target node not found' };
77
+ }
78
+ // 서브에이전트 → 스킬 연결: 서브에이전트의 skills 필드 업데이트
79
+ if (sourceNode.type === 'subagent' && targetNode.type === 'skill') {
80
+ const skills = sourceNode.skills || [];
81
+ const skillId = targetNode.skillId || this.toKebabCase(targetNode.label);
82
+ if (!skills.includes(skillId)) {
83
+ skills.push(skillId);
84
+ sourceNode.skills = skills;
85
+ await this.syncSubagentNode(sourceNode);
86
+ }
87
+ }
88
+ // 스킬 → 서브에이전트 연결: 서브에이전트의 skills 필드 업데이트
89
+ if (sourceNode.type === 'skill' && targetNode.type === 'subagent') {
90
+ const skills = targetNode.skills || [];
91
+ const skillId = sourceNode.skillId || this.toKebabCase(sourceNode.label);
92
+ if (!skills.includes(skillId)) {
93
+ skills.push(skillId);
94
+ targetNode.skills = skills;
95
+ await this.syncSubagentNode(targetNode);
96
+ }
97
+ }
98
+ return { success: true };
99
+ }
100
+ catch (error) {
101
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
102
+ return { success: false, error: errorMessage };
103
+ }
104
+ }
105
+ /**
106
+ * 엣지 삭제 시 관계 업데이트
107
+ */
108
+ async removeEdge(edge, nodes) {
109
+ try {
110
+ const sourceNode = nodes.find(n => n.id === edge.source);
111
+ const targetNode = nodes.find(n => n.id === edge.target);
112
+ if (!sourceNode || !targetNode) {
113
+ return { success: true }; // 노드가 없으면 무시
114
+ }
115
+ // 서브에이전트에서 스킬 제거
116
+ if (sourceNode.type === 'subagent' && targetNode.type === 'skill') {
117
+ const skillId = targetNode.skillId || this.toKebabCase(targetNode.label);
118
+ sourceNode.skills = (sourceNode.skills || []).filter(s => s !== skillId);
119
+ await this.syncSubagentNode(sourceNode);
120
+ }
121
+ if (sourceNode.type === 'skill' && targetNode.type === 'subagent') {
122
+ const skillId = sourceNode.skillId || this.toKebabCase(sourceNode.label);
123
+ targetNode.skills = (targetNode.skills || []).filter(s => s !== skillId);
124
+ await this.syncSubagentNode(targetNode);
125
+ }
126
+ return { success: true };
127
+ }
128
+ catch (error) {
129
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
130
+ return { success: false, error: errorMessage };
131
+ }
132
+ }
133
+ // ===== Private Methods =====
134
+ async syncSkillNode(node) {
135
+ const skillId = node.skillId || this.toKebabCase(node.label);
136
+ const skillPath = path.join(this.projectRoot, '.claude', 'skills', skillId);
137
+ const skillMdPath = path.join(skillPath, 'SKILL.md');
138
+ await fs.mkdir(skillPath, { recursive: true });
139
+ // 기존 파일이 있으면 읽어서 업데이트, 없으면 새로 생성
140
+ let content = '';
141
+ try {
142
+ content = await fs.readFile(skillMdPath, 'utf-8');
143
+ // frontmatter 업데이트
144
+ content = this.updateFrontmatter(content, {
145
+ name: skillId,
146
+ description: node.description || node.label,
147
+ });
148
+ }
149
+ catch {
150
+ // 새로 생성
151
+ content = `---
152
+ name: ${skillId}
153
+ description: ${node.description || node.label}
154
+ ---
155
+
156
+ # ${node.label}
157
+
158
+ ## 사용 시점
159
+ 이 스킬은 다음 상황에서 사용됩니다:
160
+ - ${node.description || '설명을 추가하세요'}
161
+
162
+ ## 사용 방법
163
+
164
+ \`\`\`bash
165
+ # 스킬 사용 방법을 여기에 작성하세요
166
+ \`\`\`
167
+ `;
168
+ }
169
+ await fs.writeFile(skillMdPath, content, 'utf-8');
170
+ return { success: true, path: skillPath };
171
+ }
172
+ async syncSubagentNode(node) {
173
+ const agentName = this.toKebabCase(node.label);
174
+ const agentsDir = path.join(this.projectRoot, '.claude', 'agents');
175
+ const agentPath = path.join(agentsDir, `${agentName}.md`);
176
+ await fs.mkdir(agentsDir, { recursive: true });
177
+ // Frontmatter 구성
178
+ const frontmatter = {
179
+ name: agentName,
180
+ description: node.description || node.label,
181
+ };
182
+ if (node.tools && node.tools.length > 0) {
183
+ frontmatter.tools = node.tools.join(', ');
184
+ }
185
+ if (node.model) {
186
+ frontmatter.model = node.model;
187
+ }
188
+ if (node.skills && node.skills.length > 0) {
189
+ frontmatter.skills = node.skills.join(', ');
190
+ }
191
+ const frontmatterStr = Object.entries(frontmatter)
192
+ .map(([key, value]) => `${key}: ${value}`)
193
+ .join('\n');
194
+ const content = `---
195
+ ${frontmatterStr}
196
+ ---
197
+
198
+ ${node.systemPrompt || `You are ${node.label}.
199
+
200
+ ${node.description || ''}
201
+ `}
202
+ `;
203
+ await fs.writeFile(agentPath, content, 'utf-8');
204
+ return { success: true, path: agentPath };
205
+ }
206
+ async syncCommandNode(node) {
207
+ const cmdName = node.commandName || this.toKebabCase(node.label);
208
+ const commandsDir = path.join(this.projectRoot, '.claude', 'commands');
209
+ const cmdPath = path.join(commandsDir, `${cmdName}.md`);
210
+ await fs.mkdir(commandsDir, { recursive: true });
211
+ const content = node.commandContent || `---
212
+ description: ${node.description || node.label}
213
+ ---
214
+
215
+ ${node.description || '커맨드 내용을 여기에 작성하세요'}
216
+
217
+ $ARGUMENTS
218
+ `;
219
+ await fs.writeFile(cmdPath, content, 'utf-8');
220
+ return { success: true, path: cmdPath };
221
+ }
222
+ async syncHookNode(node) {
223
+ const settingsPath = path.join(this.projectRoot, '.claude', 'settings.json');
224
+ // 기존 settings 읽기
225
+ let settings = {};
226
+ try {
227
+ const content = await fs.readFile(settingsPath, 'utf-8');
228
+ settings = JSON.parse(content);
229
+ }
230
+ catch {
231
+ // 파일 없음
232
+ }
233
+ // hooks 섹션 확인/생성
234
+ if (!settings.hooks) {
235
+ settings.hooks = {};
236
+ }
237
+ const hooks = settings.hooks;
238
+ const event = node.hookEvent || 'PreToolUse';
239
+ if (!hooks[event]) {
240
+ hooks[event] = [];
241
+ }
242
+ // 기존 훅 찾기 (같은 matcher로)
243
+ const eventHooks = hooks[event];
244
+ const existingIndex = eventHooks.findIndex(h => h.matcher === (node.hookMatcher || '*'));
245
+ const hookConfig = {
246
+ matcher: node.hookMatcher || '*',
247
+ hooks: [
248
+ {
249
+ type: 'command',
250
+ command: node.hookCommand || 'echo "Hook triggered"',
251
+ },
252
+ ],
253
+ };
254
+ if (existingIndex >= 0) {
255
+ eventHooks[existingIndex] = hookConfig;
256
+ }
257
+ else {
258
+ eventHooks.push(hookConfig);
259
+ }
260
+ // .claude 디렉토리 생성
261
+ await fs.mkdir(path.dirname(settingsPath), { recursive: true });
262
+ await fs.writeFile(settingsPath, JSON.stringify(settings, null, 2), 'utf-8');
263
+ return { success: true, path: settingsPath };
264
+ }
265
+ async removeHookFromSettings(node) {
266
+ const settingsPath = path.join(this.projectRoot, '.claude', 'settings.json');
267
+ let settings = {};
268
+ try {
269
+ const content = await fs.readFile(settingsPath, 'utf-8');
270
+ settings = JSON.parse(content);
271
+ }
272
+ catch {
273
+ return;
274
+ }
275
+ const hooks = settings.hooks;
276
+ if (!hooks)
277
+ return;
278
+ const event = node.hookEvent || 'PreToolUse';
279
+ if (!hooks[event])
280
+ return;
281
+ const eventHooks = hooks[event];
282
+ hooks[event] = eventHooks.filter(h => h.matcher !== (node.hookMatcher || '*'));
283
+ await fs.writeFile(settingsPath, JSON.stringify(settings, null, 2), 'utf-8');
284
+ }
285
+ updateFrontmatter(content, updates) {
286
+ const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
287
+ if (!frontmatterMatch) {
288
+ // frontmatter 없으면 추가
289
+ const fm = Object.entries(updates)
290
+ .map(([k, v]) => `${k}: ${v}`)
291
+ .join('\n');
292
+ return `---\n${fm}\n---\n\n${content}`;
293
+ }
294
+ let frontmatter = frontmatterMatch[1];
295
+ for (const [key, value] of Object.entries(updates)) {
296
+ const regex = new RegExp(`^${key}:.*$`, 'm');
297
+ if (regex.test(frontmatter)) {
298
+ frontmatter = frontmatter.replace(regex, `${key}: ${value}`);
299
+ }
300
+ else {
301
+ frontmatter += `\n${key}: ${value}`;
302
+ }
303
+ }
304
+ return content.replace(/^---\n[\s\S]*?\n---/, `---\n${frontmatter}\n---`);
305
+ }
306
+ toKebabCase(str) {
307
+ return str
308
+ .toLowerCase()
309
+ .replace(/[^a-z0-9가-힣]+/g, '-')
310
+ .replace(/^-|-$/g, '');
311
+ }
312
+ }
313
+ export const nodeSyncService = new NodeSyncService();
@@ -1,87 +1,63 @@
1
1
  import Anthropic from '@anthropic-ai/sdk';
2
2
  import * as fs from 'fs/promises';
3
3
  import * as path from 'path';
4
- const SYSTEM_PROMPT = `당신은 Claude Code 스킬 생성 전문가입니다.
4
+ const SYSTEM_PROMPT = `You are a Claude Code skill generator. You MUST respond with ONLY a valid JSON object. No markdown, no code blocks, no explanations - just pure JSON.
5
5
 
6
- 사용자의 요청을 분석하여 완전히 작동하는 Claude Code 스킬을 생성합니다.
7
-
8
- ## Claude Code 스킬 구조
9
-
10
- 스킬은 다음 구조로 생성됩니다:
11
- \`\`\`
12
- .claude/skills/[skill-name]/
13
- ├── SKILL.md # 스킬 정의 (필수)
14
- ├── scripts/ # 스크립트 폴더
15
- │ └── main.py # 메인 스크립트
16
- └── requirements.txt # Python 의존성 (필요시)
17
- \`\`\`
18
-
19
- ## SKILL.md 형식
20
-
21
- \`\`\`markdown
22
- ---
23
- name: skill-name
24
- description: 스킬 설명 (한 줄)
25
- ---
26
-
27
- # 스킬 이름
28
-
29
- ## 사용 시점
30
- 이 스킬은 다음 상황에서 사용됩니다:
31
- - 상황 1
32
- - 상황 2
33
-
34
- ## 사용 방법
35
-
36
- \\\`\\\`\\\`bash
37
- ~/.claude/venv/bin/python ~/.claude/skills/[skill-name]/scripts/main.py [인자들]
38
- \\\`\\\`\\\`
39
-
40
- ## 파라미터
41
- - \`param1\`: 설명
42
- - \`param2\`: 설명
43
-
44
- ## 예시
45
- [사용 예시]
46
- \`\`\`
47
-
48
- ## 응답 형식 (JSON)
49
-
50
- 반드시 아래 형식의 JSON으로 응답하세요:
6
+ Your response must follow this exact JSON schema:
51
7
 
52
8
  {
53
- "skillName": "스킬 이름 (한글 가능)",
54
- "skillId": "skill-id-kebab-case",
55
- "description": "스킬 설명",
9
+ "skillName": "Human readable skill name",
10
+ "skillId": "skill-id-in-kebab-case",
11
+ "description": "Brief description of what this skill does",
56
12
  "files": [
57
13
  {
58
14
  "path": "SKILL.md",
59
- "content": "SKILL.md 전체 내용",
15
+ "content": "Full SKILL.md content here",
60
16
  "language": "markdown"
61
17
  },
62
18
  {
63
19
  "path": "scripts/main.py",
64
- "content": "Python 스크립트 전체 내용",
20
+ "content": "Full Python script content here",
65
21
  "language": "python"
66
22
  },
67
23
  {
68
24
  "path": "requirements.txt",
69
- "content": "의존성 목록 (필요한 경우)",
25
+ "content": "package1\\npackage2",
70
26
  "language": "text"
71
27
  }
72
28
  ]
73
29
  }
74
30
 
75
- ## 중요 규칙
31
+ SKILL.md must follow this format:
32
+ ---
33
+ name: skill-id
34
+ description: One line description
35
+ ---
36
+
37
+ # Skill Name
38
+
39
+ ## When to use
40
+ - Use case 1
41
+ - Use case 2
42
+
43
+ ## Usage
44
+ \`\`\`bash
45
+ ~/.claude/venv/bin/python ~/.claude/skills/skill-id/scripts/main.py [args]
46
+ \`\`\`
47
+
48
+ ## Parameters
49
+ - \`--param1\`: Description
50
+
51
+ ## Example
52
+ [Usage example]
76
53
 
77
- 1. **완전한 코드 생성**: 실제로 동작하는 완전한 코드를 작성하세요
78
- 2. **에러 처리 포함**: try-except로 에러 처리를 포함하세요
79
- 3. **한글 지원**: 출력 메시지는 한글로 작성하세요
80
- 4. **환경 고려**:
81
- - Python 스크립트는 \`~/.claude/venv/bin/python\`으로 실행됩니다
82
- - 필요한 패키지는 requirements.txt에 명시하세요
83
- 5. **JSON 반환**: 설명 없이 JSON만 반환하세요
84
- `;
54
+ RULES:
55
+ 1. Generate COMPLETE, WORKING code - no placeholders
56
+ 2. Include proper error handling with try-except
57
+ 3. Use Korean for user-facing messages
58
+ 4. Scripts run with ~/.claude/venv/bin/python
59
+ 5. List all required packages in requirements.txt
60
+ 6. RESPOND WITH JSON ONLY - NO OTHER TEXT`;
85
61
  export class SkillGeneratorService {
86
62
  projectRoot;
87
63
  constructor(projectRoot) {
@@ -105,16 +81,18 @@ export class SkillGeneratorService {
105
81
  }
106
82
  async generate(prompt, settings) {
107
83
  const client = this.getClient(settings);
108
- const userPrompt = `다음 스킬을 생성해주세요: "${prompt}"
84
+ const userPrompt = `Create a skill for: "${prompt}"
109
85
 
110
- 완전히 동작하는 코드를 포함한 스킬을 생성하세요.
111
- 반드시 JSON만 반환하세요.`;
86
+ Generate complete, working code. Respond with JSON only.`;
112
87
  try {
113
88
  const response = await client.messages.create({
114
89
  model: 'claude-sonnet-4-20250514',
115
90
  max_tokens: 8192,
116
91
  system: SYSTEM_PROMPT,
117
- messages: [{ role: 'user', content: userPrompt }],
92
+ messages: [
93
+ { role: 'user', content: userPrompt },
94
+ { role: 'assistant', content: '{' } // Prefill to force JSON
95
+ ],
118
96
  });
119
97
  let responseText = '';
120
98
  for (const block of response.content) {
@@ -125,15 +103,14 @@ export class SkillGeneratorService {
125
103
  if (!responseText) {
126
104
  return { success: false, error: 'AI 응답을 받지 못했습니다.' };
127
105
  }
128
- // JSON 추출
129
- const jsonMatch = responseText.match(/```(?:json)?\s*([\s\S]*?)```/);
130
- const rawJson = jsonMatch ? jsonMatch[1].trim() : responseText.trim();
106
+ // Prefill로 '{'를 보냈으니 응답 앞에 '{'를 붙임
107
+ const fullJson = '{' + responseText;
131
108
  let skill;
132
109
  try {
133
- skill = JSON.parse(rawJson);
110
+ skill = JSON.parse(fullJson);
134
111
  }
135
112
  catch {
136
- console.error('Failed to parse skill response:', rawJson.slice(0, 500));
113
+ console.error('Failed to parse skill response:', fullJson.slice(0, 500));
137
114
  return { success: false, error: 'AI 응답을 파싱할 수 없습니다. 다시 시도해주세요.' };
138
115
  }
139
116
  // 파일 저장
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "makecc",
3
- "version": "0.2.4",
3
+ "version": "0.2.7",
4
4
  "type": "module",
5
5
  "description": "Visual workflow builder for Claude Code agents and skills",
6
6
  "keywords": [
package/server/index.ts CHANGED
@@ -10,6 +10,7 @@ import { ClaudeService } from './services/claudeService';
10
10
  import { fileService } from './services/fileService';
11
11
  import { workflowAIService } from './services/workflowAIService';
12
12
  import { skillGeneratorService } from './services/skillGeneratorService';
13
+ import { nodeSyncService } from './services/nodeSyncService';
13
14
  import { workflowExecutionService } from './services/workflowExecutionService';
14
15
  import { executeInTerminal, getClaudeCommand } from './services/terminalService';
15
16
  import type { WorkflowExecutionRequest, NodeExecutionUpdate } from './types';
@@ -108,6 +109,86 @@ app.get('/api/settings/api-key', async (req, res) => {
108
109
  }
109
110
  });
110
111
 
112
+ // Sync node to file system
113
+ app.post('/api/sync/node', async (req, res) => {
114
+ try {
115
+ const { node } = req.body;
116
+ if (!node) {
117
+ return res.status(400).json({ message: 'Node data is required' });
118
+ }
119
+
120
+ const result = await nodeSyncService.syncNode(node);
121
+ if (result.success) {
122
+ res.json(result);
123
+ } else {
124
+ res.status(500).json({ message: result.error });
125
+ }
126
+ } catch (error) {
127
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
128
+ res.status(500).json({ message: errorMessage });
129
+ }
130
+ });
131
+
132
+ // Delete node from file system
133
+ app.delete('/api/sync/node', async (req, res) => {
134
+ try {
135
+ const { node } = req.body;
136
+ if (!node) {
137
+ return res.status(400).json({ message: 'Node data is required' });
138
+ }
139
+
140
+ const result = await nodeSyncService.deleteNode(node);
141
+ if (result.success) {
142
+ res.json(result);
143
+ } else {
144
+ res.status(500).json({ message: result.error });
145
+ }
146
+ } catch (error) {
147
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
148
+ res.status(500).json({ message: errorMessage });
149
+ }
150
+ });
151
+
152
+ // Sync edge (connection) to file system
153
+ app.post('/api/sync/edge', async (req, res) => {
154
+ try {
155
+ const { edge, nodes } = req.body;
156
+ if (!edge || !nodes) {
157
+ return res.status(400).json({ message: 'Edge and nodes data are required' });
158
+ }
159
+
160
+ const result = await nodeSyncService.syncEdge(edge, nodes);
161
+ if (result.success) {
162
+ res.json(result);
163
+ } else {
164
+ res.status(500).json({ message: result.error });
165
+ }
166
+ } catch (error) {
167
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
168
+ res.status(500).json({ message: errorMessage });
169
+ }
170
+ });
171
+
172
+ // Remove edge from file system
173
+ app.delete('/api/sync/edge', async (req, res) => {
174
+ try {
175
+ const { edge, nodes } = req.body;
176
+ if (!edge || !nodes) {
177
+ return res.status(400).json({ message: 'Edge and nodes data are required' });
178
+ }
179
+
180
+ const result = await nodeSyncService.removeEdge(edge, nodes);
181
+ if (result.success) {
182
+ res.json(result);
183
+ } else {
184
+ res.status(500).json({ message: result.error });
185
+ }
186
+ } catch (error) {
187
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
188
+ res.status(500).json({ message: errorMessage });
189
+ }
190
+ });
191
+
111
192
  // Generate skill using AI
112
193
  app.post('/api/generate/skill', async (req, res) => {
113
194
  try {