@yeongjaeyou/claude-code-config 0.16.0 → 0.17.0

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.
Files changed (44) hide show
  1. package/.claude/agents/code-review-handler.md +203 -0
  2. package/.claude/agents/issue-resolver.md +123 -0
  3. package/.claude/agents/python-pro.md +7 -2
  4. package/.claude/agents/web-researcher.md +5 -1
  5. package/.claude/commands/ask-deepwiki.md +46 -11
  6. package/.claude/commands/gh/auto-review-loop.md +201 -0
  7. package/.claude/commands/gh/create-issue-label.md +4 -0
  8. package/.claude/commands/gh/decompose-issue.md +24 -2
  9. package/.claude/commands/gh/post-merge.md +52 -10
  10. package/.claude/commands/gh/resolve-and-review.md +69 -0
  11. package/.claude/commands/gh/resolve-issue.md +3 -0
  12. package/.claude/commands/tm/convert-prd.md +4 -0
  13. package/.claude/commands/tm/post-merge.md +7 -1
  14. package/.claude/commands/tm/resolve-issue.md +4 -0
  15. package/.claude/commands/tm/sync-to-github.md +4 -0
  16. package/.claude/settings.json +15 -0
  17. package/.claude/skills/claude-md-generator/SKILL.md +130 -0
  18. package/.claude/skills/claude-md-generator/references/examples.md +261 -0
  19. package/.claude/skills/claude-md-generator/references/templates.md +156 -0
  20. package/.claude/skills/hook-creator/SKILL.md +88 -0
  21. package/.claude/skills/hook-creator/references/examples.md +339 -0
  22. package/.claude/skills/hook-creator/references/hook-events.md +193 -0
  23. package/.claude/skills/skill-creator/SKILL.md +160 -13
  24. package/.claude/skills/skill-creator/references/output-patterns.md +82 -0
  25. package/.claude/skills/skill-creator/references/workflows.md +28 -0
  26. package/.claude/skills/skill-creator/scripts/package_skill.py +10 -10
  27. package/.claude/skills/skill-creator/scripts/quick_validate.py +45 -15
  28. package/.claude/skills/slash-command-creator/SKILL.md +108 -0
  29. package/.claude/skills/slash-command-creator/references/examples.md +161 -0
  30. package/.claude/skills/slash-command-creator/references/frontmatter.md +74 -0
  31. package/.claude/skills/slash-command-creator/scripts/init_command.py +221 -0
  32. package/.claude/skills/subagent-creator/SKILL.md +127 -0
  33. package/.claude/skills/subagent-creator/assets/subagent-template.md +31 -0
  34. package/.claude/skills/subagent-creator/references/available-tools.md +63 -0
  35. package/.claude/skills/subagent-creator/references/examples.md +213 -0
  36. package/.claude/skills/youtube-collector/README.md +107 -0
  37. package/.claude/skills/youtube-collector/SKILL.md +158 -0
  38. package/.claude/skills/youtube-collector/references/data-schema.md +110 -0
  39. package/.claude/skills/youtube-collector/scripts/collect_videos.py +304 -0
  40. package/.claude/skills/youtube-collector/scripts/fetch_transcript.py +138 -0
  41. package/.claude/skills/youtube-collector/scripts/fetch_videos.py +229 -0
  42. package/.claude/skills/youtube-collector/scripts/register_channel.py +247 -0
  43. package/.claude/skills/youtube-collector/scripts/setup_api_key.py +151 -0
  44. package/package.json +1 -1
@@ -0,0 +1,110 @@
1
+ # YouTube Collector 데이터 스키마
2
+
3
+ ## 설정 파일 구조
4
+
5
+ ### API 키 (사용자 홈 디렉토리)
6
+
7
+ 보안을 위해 API 키는 코드베이스 외부에 저장됨.
8
+
9
+ **경로:**
10
+ - macOS/Linux: `~/.config/youtube-collector/config.yaml`
11
+ - Windows: `%APPDATA%\youtube-collector\config.yaml`
12
+
13
+ ```yaml
14
+ api_key: "YOUR_YOUTUBE_DATA_API_KEY"
15
+ ```
16
+
17
+ **설정 명령:**
18
+ ```bash
19
+ python3 scripts/setup_api_key.py
20
+ ```
21
+
22
+ ### 프로젝트 설정 (.reference/)
23
+
24
+ ```
25
+ .reference/
26
+ ├── youtube-config.yaml # 프로젝트 설정 (API 키 제외)
27
+ ├── channels.yaml # 등록된 채널 목록
28
+ └── contents/
29
+ └── {channel_handle}/ # 채널별 폴더 (@ 제외)
30
+ └── {video_id}.yaml # 영상별 데이터
31
+ ```
32
+
33
+ ## youtube-config.yaml
34
+
35
+ ```yaml
36
+ # 자막 우선 언어 (기본: ko)
37
+ default_language: "ko"
38
+
39
+ # 채널당 최대 수집 개수 (기본: 10)
40
+ max_results: 10
41
+ ```
42
+
43
+ ## channels.yaml
44
+
45
+ ```yaml
46
+ channels:
47
+ - id: "UCxxxxxxxxxxxxxxxxxxxxxx" # 채널 ID (UC로 시작)
48
+ handle: "@channelname" # 채널 핸들
49
+ name: "채널 표시 이름" # 사람이 읽기 쉬운 이름
50
+ added_at: "2025-12-13" # 등록일
51
+ ```
52
+
53
+ ## contents/{channel_handle}/{video_id}.yaml
54
+
55
+ ```yaml
56
+ # 기본 정보
57
+ video_id: "abc123xyz"
58
+ title: "영상 제목"
59
+ published_at: "2025-12-10T10:00:00Z"
60
+ url: "https://youtube.com/watch?v=abc123xyz"
61
+ thumbnail: "https://i.ytimg.com/vi/abc123xyz/maxresdefault.jpg"
62
+ description: |
63
+ 영상 설명 전체 텍스트
64
+ duration: "PT10M30S" # ISO 8601 형식
65
+ collected_at: "2025-12-13T15:00:00Z" # 수집 시점
66
+
67
+ # 자막 정보
68
+ transcript:
69
+ available: true # 자막 존재 여부
70
+ language: "ko" # 자막 언어
71
+ text: |
72
+ 자막 전체 텍스트...
73
+
74
+ # 요약 정보 (초기값: null, AI가 생성하여 추가)
75
+ summary:
76
+ source: "transcript" # "transcript" 또는 "description"
77
+ content: |
78
+ ## 서론
79
+ - 문제 제기 또는 주제 소개
80
+ - 영상의 목적/배경
81
+
82
+ ## 본론
83
+ - 핵심 내용 상세 설명
84
+ - 해결책, 방법론, 예시 등
85
+ - 주요 포인트별 정리
86
+
87
+ ## 결론
88
+ - 핵심 요약
89
+ - 시사점 또는 다음 단계
90
+ ```
91
+
92
+ ### 자막이 없는 경우
93
+
94
+ ```yaml
95
+ transcript:
96
+ available: false
97
+ language: null
98
+ text: null
99
+
100
+ summary:
101
+ source: "description"
102
+ content: |
103
+ (설명 기반 요약 내용)
104
+ ```
105
+
106
+ ## 중복 방지 규칙
107
+
108
+ - 파일명이 `{video_id}.yaml` 형식
109
+ - 수집 전 해당 파일 존재 여부로 중복 체크
110
+ - 이미 존재하는 video_id는 스킵
@@ -0,0 +1,304 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ 채널의 영상을 수집하고 .reference/ 폴더에 YAML 파일로 저장하는 통합 스크립트
4
+
5
+ Usage:
6
+ python collect_videos.py --channel-id UC... --output-dir .reference/
7
+ python collect_videos.py --channel-handle @channelname --output-dir .reference/
8
+ python collect_videos.py --all --output-dir .reference/ # channels.yaml의 모든 채널
9
+
10
+ Output:
11
+ JSON 형식으로 처리 결과 요약 출력
12
+
13
+ Requirements:
14
+ pip install google-api-python-client youtube-transcript-api pyyaml
15
+ """
16
+
17
+ import argparse
18
+ import json
19
+ import os
20
+ import sys
21
+ from datetime import datetime
22
+ from pathlib import Path
23
+
24
+ # 같은 디렉토리의 모듈 import
25
+ script_dir = Path(__file__).parent
26
+ sys.path.insert(0, str(script_dir))
27
+
28
+ try:
29
+ from fetch_videos import (
30
+ load_api_key,
31
+ get_channel_id_from_handle,
32
+ fetch_videos,
33
+ )
34
+ from googleapiclient.discovery import build
35
+ except ImportError as e:
36
+ print(json.dumps({
37
+ "error": "필요한 모듈을 import할 수 없습니다.",
38
+ "detail": str(e),
39
+ "install": "pip install google-api-python-client pyyaml"
40
+ }))
41
+ sys.exit(1)
42
+
43
+ try:
44
+ from fetch_transcript import fetch_transcript
45
+ except ImportError as e:
46
+ print(json.dumps({
47
+ "error": "fetch_transcript 모듈을 import할 수 없습니다.",
48
+ "detail": str(e),
49
+ "install": "pip install youtube-transcript-api"
50
+ }))
51
+ sys.exit(1)
52
+
53
+ try:
54
+ import yaml
55
+ except ImportError:
56
+ print(json.dumps({
57
+ "error": "pyyaml이 설치되어 있지 않습니다.",
58
+ "install": "pip install pyyaml"
59
+ }))
60
+ sys.exit(1)
61
+
62
+
63
+ def load_channels(output_dir: str) -> list:
64
+ """channels.yaml에서 등록된 채널 목록 로드"""
65
+ channels_file = Path(output_dir) / "channels.yaml"
66
+
67
+ if not channels_file.exists():
68
+ return []
69
+
70
+ try:
71
+ with open(channels_file, 'r', encoding='utf-8') as f:
72
+ data = yaml.safe_load(f)
73
+ return data.get('channels', []) if data else []
74
+ except Exception:
75
+ return []
76
+
77
+
78
+ def get_existing_video_ids(output_dir: str, channel_handle: str) -> set:
79
+ """채널 폴더에서 이미 수집된 video_id 목록 반환"""
80
+ # @ 제거
81
+ handle_clean = channel_handle.lstrip('@')
82
+ channel_dir = Path(output_dir) / "contents" / handle_clean
83
+
84
+ if not channel_dir.exists():
85
+ return set()
86
+
87
+ video_ids = set()
88
+ for yaml_file in channel_dir.glob("*.yaml"):
89
+ # 파일명이 video_id.yaml 형식
90
+ video_ids.add(yaml_file.stem)
91
+
92
+ return video_ids
93
+
94
+
95
+ def save_video_yaml(output_dir: str, channel_handle: str, video_data: dict, transcript_data: dict) -> str:
96
+ """영상 데이터를 YAML 파일로 저장"""
97
+ # @ 제거
98
+ handle_clean = channel_handle.lstrip('@')
99
+ channel_dir = Path(output_dir) / "contents" / handle_clean
100
+
101
+ # 폴더 생성
102
+ channel_dir.mkdir(parents=True, exist_ok=True)
103
+
104
+ # YAML 데이터 구성
105
+ yaml_data = {
106
+ 'video_id': video_data['video_id'],
107
+ 'title': video_data['title'],
108
+ 'published_at': video_data['published_at'],
109
+ 'url': video_data['url'],
110
+ 'thumbnail': video_data['thumbnail'],
111
+ 'description': video_data['description'],
112
+ 'duration': video_data['duration'],
113
+ 'collected_at': datetime.utcnow().isoformat() + "Z",
114
+ 'transcript': {
115
+ 'available': transcript_data['available'],
116
+ 'language': transcript_data.get('language'),
117
+ 'text': transcript_data.get('text')
118
+ },
119
+ 'summary': None # AI가 나중에 추가
120
+ }
121
+
122
+ # 파일 저장
123
+ file_path = channel_dir / f"{video_data['video_id']}.yaml"
124
+
125
+ with open(file_path, 'w', encoding='utf-8') as f:
126
+ yaml.dump(yaml_data, f, allow_unicode=True, default_flow_style=False, sort_keys=False)
127
+
128
+ return str(file_path)
129
+
130
+
131
+ def collect_channel_videos(
132
+ youtube,
133
+ channel_id: str,
134
+ channel_handle: str,
135
+ output_dir: str,
136
+ max_results: int = 10,
137
+ language: str = 'ko',
138
+ skip_existing: bool = True
139
+ ) -> dict:
140
+ """단일 채널의 영상을 수집하고 저장"""
141
+ result = {
142
+ 'channel': channel_handle,
143
+ 'channel_id': channel_id,
144
+ 'new_videos': [],
145
+ 'skipped': 0,
146
+ 'errors': []
147
+ }
148
+
149
+ # 기존 video_id 목록
150
+ existing_ids = get_existing_video_ids(output_dir, channel_handle) if skip_existing else set()
151
+
152
+ # 영상 목록 조회
153
+ videos = fetch_videos(youtube, channel_id, max_results)
154
+
155
+ for video in videos:
156
+ video_id = video['video_id']
157
+
158
+ # 중복 체크
159
+ if video_id in existing_ids:
160
+ result['skipped'] += 1
161
+ continue
162
+
163
+ try:
164
+ # 자막 수집
165
+ transcript = fetch_transcript(video_id, language)
166
+
167
+ # YAML 저장
168
+ file_path = save_video_yaml(output_dir, channel_handle, video, transcript)
169
+
170
+ result['new_videos'].append({
171
+ 'video_id': video_id,
172
+ 'title': video['title'],
173
+ 'file_path': file_path,
174
+ 'transcript_available': transcript['available']
175
+ })
176
+
177
+ except Exception as e:
178
+ result['errors'].append({
179
+ 'video_id': video_id,
180
+ 'error': str(e)
181
+ })
182
+
183
+ return result
184
+
185
+
186
+ def main():
187
+ parser = argparse.ArgumentParser(description='YouTube 채널 영상 수집 및 저장')
188
+ parser.add_argument('--channel-id', help='채널 ID (UC...)')
189
+ parser.add_argument('--channel-handle', help='채널 핸들 (@username)')
190
+ parser.add_argument('--all', action='store_true', help='channels.yaml의 모든 채널 처리')
191
+ parser.add_argument('--output-dir', default='.reference', help='저장 디렉토리 (기본: .reference)')
192
+ parser.add_argument('--max-results', type=int, default=10, help='채널당 최대 수집 개수 (기본: 10)')
193
+ parser.add_argument('--language', default='ko', help='자막 우선 언어 (기본: ko)')
194
+ parser.add_argument('--no-skip-existing', action='store_true', help='기존 파일도 덮어쓰기')
195
+ parser.add_argument('--api-key', help='YouTube Data API 키 (미지정시 설정 파일에서 로드)')
196
+
197
+ args = parser.parse_args()
198
+
199
+ # 옵션 검증
200
+ if not args.all and not args.channel_id and not args.channel_handle:
201
+ print(json.dumps({
202
+ "error": "--all, --channel-id, --channel-handle 중 하나를 지정해야 합니다."
203
+ }))
204
+ sys.exit(1)
205
+
206
+ # API 키 로드
207
+ api_key = args.api_key or load_api_key()
208
+ if not api_key:
209
+ print(json.dumps({
210
+ "error": "YouTube Data API 키가 설정되지 않았습니다.",
211
+ "help": "python3 scripts/setup_api_key.py로 설정해주세요."
212
+ }))
213
+ sys.exit(1)
214
+
215
+ # YouTube API 초기화
216
+ try:
217
+ youtube = build('youtube', 'v3', developerKey=api_key)
218
+ except Exception as e:
219
+ print(json.dumps({
220
+ "error": "YouTube API 초기화 실패",
221
+ "message": str(e)
222
+ }))
223
+ sys.exit(1)
224
+
225
+ # 처리할 채널 목록 결정
226
+ channels_to_process = []
227
+
228
+ if args.all:
229
+ # channels.yaml에서 모든 채널 로드
230
+ channels = load_channels(args.output_dir)
231
+ if not channels:
232
+ print(json.dumps({
233
+ "error": "등록된 채널이 없습니다.",
234
+ "help": "먼저 register_channel.py로 채널을 등록해주세요."
235
+ }))
236
+ sys.exit(1)
237
+
238
+ for ch in channels:
239
+ channels_to_process.append({
240
+ 'id': ch.get('id'),
241
+ 'handle': ch.get('handle', '')
242
+ })
243
+ else:
244
+ # 단일 채널 처리
245
+ channel_id = args.channel_id
246
+ channel_handle = args.channel_handle or ''
247
+
248
+ if not channel_id and channel_handle:
249
+ channel_id = get_channel_id_from_handle(youtube, channel_handle)
250
+ if not channel_id:
251
+ print(json.dumps({
252
+ "error": f"채널을 찾을 수 없습니다: {channel_handle}"
253
+ }))
254
+ sys.exit(1)
255
+
256
+ channels_to_process.append({
257
+ 'id': channel_id,
258
+ 'handle': channel_handle
259
+ })
260
+
261
+ # 결과 수집
262
+ all_results = {
263
+ 'processed_at': datetime.utcnow().isoformat() + "Z",
264
+ 'output_dir': args.output_dir,
265
+ 'channels_processed': 0,
266
+ 'total_new_videos': 0,
267
+ 'total_skipped': 0,
268
+ 'results': [],
269
+ 'errors': []
270
+ }
271
+
272
+ skip_existing = not args.no_skip_existing
273
+
274
+ for channel in channels_to_process:
275
+ try:
276
+ result = collect_channel_videos(
277
+ youtube=youtube,
278
+ channel_id=channel['id'],
279
+ channel_handle=channel['handle'],
280
+ output_dir=args.output_dir,
281
+ max_results=args.max_results,
282
+ language=args.language,
283
+ skip_existing=skip_existing
284
+ )
285
+
286
+ all_results['channels_processed'] += 1
287
+ all_results['total_new_videos'] += len(result['new_videos'])
288
+ all_results['total_skipped'] += result['skipped']
289
+ all_results['results'].append(result)
290
+
291
+ if result['errors']:
292
+ all_results['errors'].extend(result['errors'])
293
+
294
+ except Exception as e:
295
+ all_results['errors'].append({
296
+ 'channel': channel.get('handle') or channel.get('id'),
297
+ 'error': str(e)
298
+ })
299
+
300
+ print(json.dumps(all_results, ensure_ascii=False, indent=2))
301
+
302
+
303
+ if __name__ == "__main__":
304
+ main()
@@ -0,0 +1,138 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ YouTube 영상의 자막(transcript)을 가져오는 스크립트
4
+
5
+ Usage:
6
+ python fetch_transcript.py --video-id VIDEO_ID [--language ko]
7
+
8
+ Output:
9
+ JSON 형식으로 자막 출력
10
+
11
+ Requirements:
12
+ pip install youtube-transcript-api
13
+ """
14
+
15
+ import argparse
16
+ import json
17
+ import sys
18
+
19
+ try:
20
+ from youtube_transcript_api import YouTubeTranscriptApi
21
+ from youtube_transcript_api._errors import (
22
+ TranscriptsDisabled,
23
+ NoTranscriptFound,
24
+ VideoUnavailable,
25
+ CouldNotRetrieveTranscript
26
+ )
27
+ except ImportError:
28
+ print(json.dumps({
29
+ "error": "youtube-transcript-api가 설치되어 있지 않습니다.",
30
+ "install": "pip install youtube-transcript-api"
31
+ }))
32
+ sys.exit(1)
33
+
34
+
35
+ def fetch_transcript(video_id: str, preferred_language: str = 'ko') -> dict:
36
+ """
37
+ 영상 자막 가져오기
38
+
39
+ Args:
40
+ video_id: YouTube 영상 ID
41
+ preferred_language: 우선 언어 코드 (기본: ko)
42
+
43
+ Returns:
44
+ dict: {
45
+ 'available': bool,
46
+ 'language': str or None,
47
+ 'text': str or None,
48
+ 'segments': list or None,
49
+ 'error': str or None
50
+ }
51
+ """
52
+ result = {
53
+ 'available': False,
54
+ 'language': None,
55
+ 'text': None,
56
+ 'segments': None,
57
+ 'error': None
58
+ }
59
+
60
+ try:
61
+ # 사용 가능한 자막 목록 조회
62
+ transcript_list = YouTubeTranscriptApi.list_transcripts(video_id)
63
+
64
+ transcript = None
65
+
66
+ # 1. 우선 언어의 수동 자막 시도
67
+ try:
68
+ transcript = transcript_list.find_manually_created_transcript([preferred_language])
69
+ except NoTranscriptFound:
70
+ pass
71
+
72
+ # 2. 우선 언어의 자동 생성 자막 시도
73
+ if not transcript:
74
+ try:
75
+ transcript = transcript_list.find_generated_transcript([preferred_language])
76
+ except NoTranscriptFound:
77
+ pass
78
+
79
+ # 3. 영어 자막 시도 (한국어 없을 경우)
80
+ if not transcript and preferred_language != 'en':
81
+ try:
82
+ transcript = transcript_list.find_manually_created_transcript(['en'])
83
+ except NoTranscriptFound:
84
+ try:
85
+ transcript = transcript_list.find_generated_transcript(['en'])
86
+ except NoTranscriptFound:
87
+ pass
88
+
89
+ # 4. 아무 자막이나 가져오기
90
+ if not transcript:
91
+ try:
92
+ for t in transcript_list:
93
+ transcript = t
94
+ break
95
+ except:
96
+ pass
97
+
98
+ if transcript:
99
+ segments = transcript.fetch()
100
+ full_text = ' '.join([segment['text'] for segment in segments])
101
+
102
+ result['available'] = True
103
+ result['language'] = transcript.language_code
104
+ result['text'] = full_text
105
+ result['segments'] = segments
106
+
107
+ except TranscriptsDisabled:
108
+ result['error'] = "이 영상은 자막이 비활성화되어 있습니다."
109
+ except CouldNotRetrieveTranscript:
110
+ result['error'] = "이 영상에는 사용 가능한 자막이 없습니다."
111
+ except VideoUnavailable:
112
+ result['error'] = "영상을 찾을 수 없습니다."
113
+ except Exception as e:
114
+ result['error'] = str(e)
115
+
116
+ return result
117
+
118
+
119
+ def main():
120
+ parser = argparse.ArgumentParser(description='YouTube 영상 자막 가져오기')
121
+ parser.add_argument('--video-id', required=True, help='영상 ID')
122
+ parser.add_argument('--language', default='ko', help='우선 언어 (기본: ko)')
123
+ parser.add_argument('--include-segments', action='store_true',
124
+ help='타임스탬프가 포함된 세그먼트 정보 포함')
125
+
126
+ args = parser.parse_args()
127
+
128
+ result = fetch_transcript(args.video_id, args.language)
129
+
130
+ # 세그먼트 정보 제외 옵션
131
+ if not args.include_segments and result.get('segments'):
132
+ del result['segments']
133
+
134
+ print(json.dumps(result, ensure_ascii=False, indent=2))
135
+
136
+
137
+ if __name__ == "__main__":
138
+ main()