makecc 0.2.14 → 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.
@@ -67,6 +67,8 @@ export class NodeSyncService {
67
67
  }
68
68
  /**
69
69
  * 엣지 연결 시 관계 업데이트
70
+ * - source의 downstream에 target 추가
71
+ * - target의 upstream에 source 추가
70
72
  */
71
73
  async syncEdge(edge, nodes) {
72
74
  try {
@@ -75,25 +77,68 @@ export class NodeSyncService {
75
77
  if (!sourceNode || !targetNode) {
76
78
  return { success: false, error: 'Source or target node not found' };
77
79
  }
78
- // 서브에이전트 스킬 연결: 서브에이전트의 skills 필드 업데이트
80
+ const sourceId = this.getNodeIdentifier(sourceNode);
81
+ const targetId = this.getNodeIdentifier(targetNode);
82
+ // 서브에이전트 → 스킬 연결
79
83
  if (sourceNode.type === 'subagent' && targetNode.type === 'skill') {
84
+ // 에이전트의 skills 필드 업데이트
80
85
  const skills = sourceNode.skills || [];
81
- const skillId = targetNode.skillId || this.toKebabCase(targetNode.label);
82
- if (!skills.includes(skillId)) {
83
- skills.push(skillId);
86
+ if (!skills.includes(targetId)) {
87
+ skills.push(targetId);
84
88
  sourceNode.skills = skills;
85
89
  await this.syncSubagentNode(sourceNode);
86
90
  }
91
+ // 스킬의 upstream 필드 업데이트
92
+ const upstream = targetNode.upstream || [];
93
+ if (!upstream.includes(sourceId)) {
94
+ upstream.push(sourceId);
95
+ targetNode.upstream = upstream;
96
+ await this.syncSkillNode(targetNode);
97
+ }
98
+ }
99
+ // 스킬 → 스킬 연결
100
+ if (sourceNode.type === 'skill' && targetNode.type === 'skill') {
101
+ // source의 downstream 업데이트
102
+ const downstream = sourceNode.downstream || [];
103
+ if (!downstream.includes(targetId)) {
104
+ downstream.push(targetId);
105
+ sourceNode.downstream = downstream;
106
+ await this.syncSkillNode(sourceNode);
107
+ }
108
+ // target의 upstream 업데이트
109
+ const upstream = targetNode.upstream || [];
110
+ if (!upstream.includes(sourceId)) {
111
+ upstream.push(sourceId);
112
+ targetNode.upstream = upstream;
113
+ await this.syncSkillNode(targetNode);
114
+ }
87
115
  }
88
- // 스킬 → 서브에이전트 연결: 서브에이전트의 skills 필드 업데이트
116
+ // 스킬 → 서브에이전트 연결
89
117
  if (sourceNode.type === 'skill' && targetNode.type === 'subagent') {
118
+ // 에이전트의 upstream skills 필드 업데이트
90
119
  const skills = targetNode.skills || [];
91
- const skillId = sourceNode.skillId || this.toKebabCase(sourceNode.label);
92
- if (!skills.includes(skillId)) {
93
- skills.push(skillId);
120
+ if (!skills.includes(sourceId)) {
121
+ skills.push(sourceId);
94
122
  targetNode.skills = skills;
95
123
  await this.syncSubagentNode(targetNode);
96
124
  }
125
+ // 스킬의 downstream 필드 업데이트
126
+ const downstream = sourceNode.downstream || [];
127
+ if (!downstream.includes(targetId)) {
128
+ downstream.push(targetId);
129
+ sourceNode.downstream = downstream;
130
+ await this.syncSkillNode(sourceNode);
131
+ }
132
+ }
133
+ // 서브에이전트 → 서브에이전트 연결
134
+ if (sourceNode.type === 'subagent' && targetNode.type === 'subagent') {
135
+ // source의 downstream agents
136
+ const sourceDownstream = sourceNode.skills || [];
137
+ if (!sourceDownstream.includes(targetId)) {
138
+ sourceDownstream.push(targetId);
139
+ sourceNode.skills = sourceDownstream;
140
+ await this.syncSubagentNode(sourceNode);
141
+ }
97
142
  }
98
143
  return { success: true };
99
144
  }
@@ -102,6 +147,15 @@ export class NodeSyncService {
102
147
  return { success: false, error: errorMessage };
103
148
  }
104
149
  }
150
+ /**
151
+ * 노드의 식별자 반환 (skillId 또는 kebab-case label)
152
+ */
153
+ getNodeIdentifier(node) {
154
+ if (node.type === 'skill') {
155
+ return node.skillId || this.toKebabCase(node.label);
156
+ }
157
+ return this.toKebabCase(node.label);
158
+ }
105
159
  /**
106
160
  * 엣지 삭제 시 관계 업데이트
107
161
  */
@@ -112,16 +166,35 @@ export class NodeSyncService {
112
166
  if (!sourceNode || !targetNode) {
113
167
  return { success: true }; // 노드가 없으면 무시
114
168
  }
115
- // 서브에이전트에서 스킬 제거
169
+ const sourceId = this.getNodeIdentifier(sourceNode);
170
+ const targetId = this.getNodeIdentifier(targetNode);
171
+ // 서브에이전트 → 스킬 연결 해제
116
172
  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);
173
+ // 에이전트에서 스킬 제거
174
+ sourceNode.skills = (sourceNode.skills || []).filter(s => s !== targetId);
119
175
  await this.syncSubagentNode(sourceNode);
176
+ // 스킬에서 upstream 제거
177
+ targetNode.upstream = (targetNode.upstream || []).filter(s => s !== sourceId);
178
+ await this.syncSkillNode(targetNode);
179
+ }
180
+ // 스킬 → 스킬 연결 해제
181
+ if (sourceNode.type === 'skill' && targetNode.type === 'skill') {
182
+ sourceNode.downstream = (sourceNode.downstream || []).filter(s => s !== targetId);
183
+ await this.syncSkillNode(sourceNode);
184
+ targetNode.upstream = (targetNode.upstream || []).filter(s => s !== sourceId);
185
+ await this.syncSkillNode(targetNode);
120
186
  }
187
+ // 스킬 → 서브에이전트 연결 해제
121
188
  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);
189
+ targetNode.skills = (targetNode.skills || []).filter(s => s !== sourceId);
124
190
  await this.syncSubagentNode(targetNode);
191
+ sourceNode.downstream = (sourceNode.downstream || []).filter(s => s !== targetId);
192
+ await this.syncSkillNode(sourceNode);
193
+ }
194
+ // 서브에이전트 → 서브에이전트 연결 해제
195
+ if (sourceNode.type === 'subagent' && targetNode.type === 'subagent') {
196
+ sourceNode.skills = (sourceNode.skills || []).filter(s => s !== targetId);
197
+ await this.syncSubagentNode(sourceNode);
125
198
  }
126
199
  return { success: true };
127
200
  }
@@ -136,21 +209,35 @@ export class NodeSyncService {
136
209
  const skillPath = path.join(this.projectRoot, '.claude', 'skills', skillId);
137
210
  const skillMdPath = path.join(skillPath, 'SKILL.md');
138
211
  await fs.mkdir(skillPath, { recursive: true });
212
+ // Frontmatter 구성
213
+ const frontmatter = {
214
+ name: skillId,
215
+ description: node.description || node.label,
216
+ };
217
+ // upstream 연결 추가
218
+ if (node.upstream && node.upstream.length > 0) {
219
+ frontmatter.upstream = node.upstream.join(', ');
220
+ }
221
+ // downstream 연결 추가
222
+ if (node.downstream && node.downstream.length > 0) {
223
+ frontmatter.downstream = node.downstream.join(', ');
224
+ }
139
225
  // 기존 파일이 있으면 읽어서 업데이트, 없으면 새로 생성
140
226
  let content = '';
141
227
  try {
142
228
  content = await fs.readFile(skillMdPath, 'utf-8');
143
229
  // frontmatter 업데이트
144
- content = this.updateFrontmatter(content, {
145
- name: skillId,
146
- description: node.description || node.label,
147
- });
230
+ for (const [key, value] of Object.entries(frontmatter)) {
231
+ content = this.updateFrontmatter(content, { [key]: value });
232
+ }
148
233
  }
149
234
  catch {
150
235
  // 새로 생성
236
+ const frontmatterStr = Object.entries(frontmatter)
237
+ .map(([key, value]) => `${key}: ${value}`)
238
+ .join('\n');
151
239
  content = `---
152
- name: ${skillId}
153
- description: ${node.description || node.label}
240
+ ${frontmatterStr}
154
241
  ---
155
242
 
156
243
  # ${node.label}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "makecc",
3
- "version": "0.2.14",
3
+ "version": "0.2.15",
4
4
  "type": "module",
5
5
  "description": "Visual workflow builder for Claude Code agents and skills",
6
6
  "keywords": [
@@ -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}