makecc 0.2.7 → 0.2.10

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.
@@ -1,6 +1,8 @@
1
1
  import Anthropic from '@anthropic-ai/sdk';
2
2
  import * as fs from 'fs/promises';
3
3
  import * as path from 'path';
4
+ import { existsSync } from 'fs';
5
+ import { spawn } from 'child_process';
4
6
  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
7
 
6
8
  Your response must follow this exact JSON schema:
@@ -79,12 +81,33 @@ export class SkillGeneratorService {
79
81
  }
80
82
  return new Anthropic({ apiKey: envApiKey });
81
83
  }
82
- async generate(prompt, settings) {
84
+ async generate(prompt, settings, onProgress) {
85
+ const progress = onProgress || (() => { });
86
+ progress({
87
+ step: 'started',
88
+ message: '✨ 스킬 생성을 시작합니다',
89
+ detail: `요청: "${prompt.slice(0, 50)}${prompt.length > 50 ? '...' : ''}"`,
90
+ });
83
91
  const client = this.getClient(settings);
92
+ progress({
93
+ step: 'analyzing',
94
+ message: '🔍 요청을 분석하고 있어요',
95
+ detail: '어떤 스킬이 필요한지 파악 중...',
96
+ });
84
97
  const userPrompt = `Create a skill for: "${prompt}"
85
98
 
86
99
  Generate complete, working code. Respond with JSON only.`;
87
100
  try {
101
+ progress({
102
+ step: 'designing',
103
+ message: '📝 스킬 구조를 설계하고 있어요',
104
+ detail: 'AI가 최적의 스킬 구조를 결정 중...',
105
+ });
106
+ progress({
107
+ step: 'generating',
108
+ message: '⚙️ 코드를 생성하고 있어요',
109
+ detail: 'Python 스크립트와 설정 파일 작성 중...',
110
+ });
88
111
  const response = await client.messages.create({
89
112
  model: 'claude-sonnet-4-20250514',
90
113
  max_tokens: 8192,
@@ -101,6 +124,10 @@ Generate complete, working code. Respond with JSON only.`;
101
124
  }
102
125
  }
103
126
  if (!responseText) {
127
+ progress({
128
+ step: 'error',
129
+ message: '❌ AI 응답을 받지 못했습니다',
130
+ });
104
131
  return { success: false, error: 'AI 응답을 받지 못했습니다.' };
105
132
  }
106
133
  // Prefill로 '{'를 보냈으니 응답 앞에 '{'를 붙임
@@ -111,11 +138,47 @@ Generate complete, working code. Respond with JSON only.`;
111
138
  }
112
139
  catch {
113
140
  console.error('Failed to parse skill response:', fullJson.slice(0, 500));
141
+ progress({
142
+ step: 'error',
143
+ message: '❌ AI 응답을 파싱할 수 없습니다',
144
+ detail: '다시 시도해주세요',
145
+ });
114
146
  return { success: false, error: 'AI 응답을 파싱할 수 없습니다. 다시 시도해주세요.' };
115
147
  }
148
+ progress({
149
+ step: 'saving',
150
+ message: '💾 파일을 저장하고 있어요',
151
+ detail: `${skill.files.length}개 파일 저장 중...`,
152
+ });
116
153
  // 파일 저장
117
154
  const skillPath = path.join(this.projectRoot, '.claude', 'skills', skill.skillId);
118
155
  await this.saveSkillFiles(skillPath, skill.files);
156
+ // requirements.txt가 있으면 의존성 설치
157
+ const requirementsPath = path.join(skillPath, 'requirements.txt');
158
+ if (existsSync(requirementsPath)) {
159
+ progress({
160
+ step: 'installing',
161
+ message: '📦 패키지를 설치하고 있어요',
162
+ detail: 'pip install 실행 중...',
163
+ });
164
+ try {
165
+ await this.installDependencies(requirementsPath, progress);
166
+ }
167
+ catch (installError) {
168
+ // 설치 실패해도 스킬 생성은 성공으로 처리
169
+ console.error('Dependency installation failed:', installError);
170
+ progress({
171
+ step: 'installing',
172
+ message: '⚠️ 일부 패키지 설치에 실패했어요',
173
+ detail: '나중에 수동으로 설치해주세요',
174
+ });
175
+ }
176
+ }
177
+ progress({
178
+ step: 'completed',
179
+ message: '✅ 스킬이 생성되었습니다!',
180
+ detail: `${skill.skillName} → ${skillPath}`,
181
+ });
119
182
  return {
120
183
  success: true,
121
184
  skill,
@@ -125,6 +188,11 @@ Generate complete, working code. Respond with JSON only.`;
125
188
  catch (error) {
126
189
  const errorMessage = error instanceof Error ? error.message : 'Unknown error';
127
190
  console.error('Skill generation error:', errorMessage);
191
+ progress({
192
+ step: 'error',
193
+ message: '❌ 스킬 생성에 실패했습니다',
194
+ detail: errorMessage,
195
+ });
128
196
  return { success: false, error: errorMessage };
129
197
  }
130
198
  }
@@ -141,5 +209,95 @@ Generate complete, working code. Respond with JSON only.`;
141
209
  console.log(`Saved: ${filePath}`);
142
210
  }
143
211
  }
212
+ async installDependencies(requirementsPath, progress) {
213
+ return new Promise((resolve, reject) => {
214
+ const homeDir = process.env.HOME || process.env.USERPROFILE || '';
215
+ const venvPythonPath = path.join(homeDir, '.claude', 'venv', 'bin', 'python');
216
+ let command;
217
+ let args;
218
+ // uv를 우선 사용 (10-100x 빠름)
219
+ // uv pip install --python <venv-python> -r requirements.txt
220
+ const useUv = this.checkCommandExists('uv');
221
+ if (useUv && existsSync(venvPythonPath)) {
222
+ command = 'uv';
223
+ args = ['pip', 'install', '--python', venvPythonPath, '-r', requirementsPath];
224
+ progress({
225
+ step: 'installing',
226
+ message: '⚡ uv로 패키지 설치 중 (고속)',
227
+ detail: 'uv pip install 실행 중...',
228
+ });
229
+ }
230
+ else if (existsSync(path.join(homeDir, '.claude', 'venv', 'bin', 'pip'))) {
231
+ // fallback: 전역 venv pip 사용
232
+ command = path.join(homeDir, '.claude', 'venv', 'bin', 'pip');
233
+ args = ['install', '-r', requirementsPath];
234
+ progress({
235
+ step: 'installing',
236
+ message: '📦 pip으로 패키지 설치 중',
237
+ detail: 'pip install 실행 중...',
238
+ });
239
+ }
240
+ else {
241
+ // fallback: 시스템 pip 사용
242
+ command = 'pip3';
243
+ args = ['install', '-r', requirementsPath];
244
+ progress({
245
+ step: 'installing',
246
+ message: '📦 pip으로 패키지 설치 중',
247
+ detail: 'pip install 실행 중...',
248
+ });
249
+ }
250
+ console.log(`Installing dependencies: ${command} ${args.join(' ')}`);
251
+ const proc = spawn(command, args, {
252
+ stdio: ['ignore', 'pipe', 'pipe'],
253
+ });
254
+ let output = '';
255
+ let errorOutput = '';
256
+ proc.stdout?.on('data', (data) => {
257
+ output += data.toString();
258
+ const lines = data.toString().trim().split('\n');
259
+ for (const line of lines) {
260
+ if (line.includes('Successfully installed') || line.includes('Requirement already satisfied') || line.includes('Installed')) {
261
+ progress({
262
+ step: 'installing',
263
+ message: '📦 패키지 설치 중',
264
+ detail: line.slice(0, 60) + (line.length > 60 ? '...' : ''),
265
+ });
266
+ }
267
+ }
268
+ });
269
+ proc.stderr?.on('data', (data) => {
270
+ errorOutput += data.toString();
271
+ });
272
+ proc.on('close', (code) => {
273
+ if (code === 0) {
274
+ progress({
275
+ step: 'installing',
276
+ message: '✅ 패키지 설치 완료',
277
+ detail: '모든 의존성이 설치되었습니다',
278
+ });
279
+ resolve();
280
+ }
281
+ else {
282
+ console.error('Package install failed:', errorOutput);
283
+ reject(new Error(`Package install failed with code ${code}`));
284
+ }
285
+ });
286
+ proc.on('error', (err) => {
287
+ console.error('Failed to start package installer:', err);
288
+ reject(err);
289
+ });
290
+ });
291
+ }
292
+ checkCommandExists(cmd) {
293
+ try {
294
+ const { execSync } = require('child_process');
295
+ execSync(`which ${cmd}`, { stdio: 'ignore' });
296
+ return true;
297
+ }
298
+ catch {
299
+ return false;
300
+ }
301
+ }
144
302
  }
145
303
  export const skillGeneratorService = new SkillGeneratorService();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "makecc",
3
- "version": "0.2.7",
3
+ "version": "0.2.10",
4
4
  "type": "module",
5
5
  "description": "Visual workflow builder for Claude Code agents and skills",
6
6
  "keywords": [
package/server/index.ts CHANGED
@@ -5,12 +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
13
  import { nodeSyncService } from './services/nodeSyncService';
14
+ import { configLoaderService } from './services/configLoaderService';
14
15
  import { workflowExecutionService } from './services/workflowExecutionService';
15
16
  import { executeInTerminal, getClaudeCommand } from './services/terminalService';
16
17
  import type { WorkflowExecutionRequest, NodeExecutionUpdate } from './types';
@@ -223,6 +224,22 @@ app.post('/api/generate/skill', async (req, res) => {
223
224
  }
224
225
  });
225
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
+
226
243
  // Generate workflow using AI
227
244
  app.post('/api/generate/workflow', async (req, res) => {
228
245
  try {
@@ -254,6 +271,124 @@ app.post('/api/generate/workflow', async (req, res) => {
254
271
  }
255
272
  });
256
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
+
257
392
  // Socket.io connection handling
258
393
  io.on('connection', (socket) => {
259
394
  console.log('Client connected:', socket.id);
@@ -391,6 +526,45 @@ io.on('connection', (socket) => {
391
526
  socket.emit('workflow:cancelled');
392
527
  });
393
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
+
394
568
  socket.on('disconnect', () => {
395
569
  console.log('Client disconnected:', socket.id);
396
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();