@yeongjaeyou/claude-code-config 0.16.0 → 0.17.1

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 +79 -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 +178 -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,247 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ YouTube 채널을 channels.yaml에 등록하는 스크립트
4
+
5
+ Usage:
6
+ python register_channel.py --channel-handle @channelname --output-dir .reference/
7
+ python register_channel.py --channel-url https://youtube.com/@channelname --output-dir .reference/
8
+
9
+ Output:
10
+ JSON 형식으로 등록 결과 출력
11
+
12
+ Requirements:
13
+ pip install google-api-python-client pyyaml
14
+ """
15
+
16
+ import argparse
17
+ import json
18
+ import os
19
+ import re
20
+ import sys
21
+ from datetime import datetime
22
+ from pathlib import Path
23
+ from urllib.parse import urlparse, unquote
24
+
25
+ # 같은 디렉토리의 모듈 import
26
+ script_dir = Path(__file__).parent
27
+ sys.path.insert(0, str(script_dir))
28
+
29
+ try:
30
+ from fetch_videos import (
31
+ load_api_key,
32
+ get_channel_id_from_handle,
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
+ import yaml
45
+ except ImportError:
46
+ print(json.dumps({
47
+ "error": "pyyaml이 설치되어 있지 않습니다.",
48
+ "install": "pip install pyyaml"
49
+ }))
50
+ sys.exit(1)
51
+
52
+
53
+ def extract_handle_from_url(url: str) -> str:
54
+ """YouTube URL에서 채널 핸들 추출"""
55
+ # URL 디코딩
56
+ url = unquote(url)
57
+
58
+ # URL 파싱
59
+ parsed = urlparse(url)
60
+ path = parsed.path
61
+
62
+ # @username 형식 (예: youtube.com/@channelname)
63
+ match = re.match(r'^/@([^/]+)', path)
64
+ if match:
65
+ return '@' + match.group(1)
66
+
67
+ # /channel/UC... 형식
68
+ match = re.match(r'^/channel/(UC[a-zA-Z0-9_-]+)', path)
69
+ if match:
70
+ return match.group(1) # 채널 ID 반환
71
+
72
+ # /c/customname 형식
73
+ match = re.match(r'^/c/([^/]+)', path)
74
+ if match:
75
+ return '@' + match.group(1)
76
+
77
+ # /user/username 형식
78
+ match = re.match(r'^/user/([^/]+)', path)
79
+ if match:
80
+ return '@' + match.group(1)
81
+
82
+ return None
83
+
84
+
85
+ def get_channel_info(youtube, channel_id: str) -> dict:
86
+ """채널 ID로 채널 상세 정보 조회"""
87
+ try:
88
+ response = youtube.channels().list(
89
+ part="snippet",
90
+ id=channel_id
91
+ ).execute()
92
+
93
+ if response.get('items'):
94
+ snippet = response['items'][0]['snippet']
95
+ return {
96
+ 'id': channel_id,
97
+ 'name': snippet.get('title', ''),
98
+ 'description': snippet.get('description', ''),
99
+ 'custom_url': snippet.get('customUrl', '')
100
+ }
101
+ return None
102
+ except Exception:
103
+ return None
104
+
105
+
106
+ def load_channels(channels_file: Path) -> dict:
107
+ """channels.yaml 로드"""
108
+ if not channels_file.exists():
109
+ return {'channels': []}
110
+
111
+ try:
112
+ with open(channels_file, 'r', encoding='utf-8') as f:
113
+ data = yaml.safe_load(f)
114
+ return data if data else {'channels': []}
115
+ except Exception:
116
+ return {'channels': []}
117
+
118
+
119
+ def save_channels(channels_file: Path, data: dict):
120
+ """channels.yaml 저장"""
121
+ # 부모 폴더 생성
122
+ channels_file.parent.mkdir(parents=True, exist_ok=True)
123
+
124
+ with open(channels_file, 'w', encoding='utf-8') as f:
125
+ yaml.dump(data, f, allow_unicode=True, default_flow_style=False, sort_keys=False)
126
+
127
+
128
+ def main():
129
+ parser = argparse.ArgumentParser(description='YouTube 채널 등록')
130
+ parser.add_argument('--channel-handle', help='채널 핸들 (@username)')
131
+ parser.add_argument('--channel-url', help='채널 URL')
132
+ parser.add_argument('--channel-id', help='채널 ID (UC...)')
133
+ parser.add_argument('--output-dir', default='.reference', help='저장 디렉토리 (기본: .reference)')
134
+ parser.add_argument('--api-key', help='YouTube Data API 키 (미지정시 설정 파일에서 로드)')
135
+
136
+ args = parser.parse_args()
137
+
138
+ # 옵션 검증
139
+ if not args.channel_handle and not args.channel_url and not args.channel_id:
140
+ print(json.dumps({
141
+ "error": "--channel-handle, --channel-url, --channel-id 중 하나를 지정해야 합니다."
142
+ }))
143
+ sys.exit(1)
144
+
145
+ # API 키 로드
146
+ api_key = args.api_key or load_api_key()
147
+ if not api_key:
148
+ print(json.dumps({
149
+ "error": "YouTube Data API 키가 설정되지 않았습니다.",
150
+ "help": "python3 scripts/setup_api_key.py로 설정해주세요."
151
+ }))
152
+ sys.exit(1)
153
+
154
+ # YouTube API 초기화
155
+ try:
156
+ youtube = build('youtube', 'v3', developerKey=api_key)
157
+ except Exception as e:
158
+ print(json.dumps({
159
+ "error": "YouTube API 초기화 실패",
160
+ "message": str(e)
161
+ }))
162
+ sys.exit(1)
163
+
164
+ # 채널 핸들/ID 결정
165
+ channel_handle = args.channel_handle
166
+ channel_id = args.channel_id
167
+
168
+ if args.channel_url:
169
+ extracted = extract_handle_from_url(args.channel_url)
170
+ if not extracted:
171
+ print(json.dumps({
172
+ "error": "URL에서 채널 정보를 추출할 수 없습니다.",
173
+ "url": args.channel_url
174
+ }))
175
+ sys.exit(1)
176
+
177
+ if extracted.startswith('UC'):
178
+ channel_id = extracted
179
+ else:
180
+ channel_handle = extracted
181
+
182
+ # 채널 ID 조회
183
+ if not channel_id and channel_handle:
184
+ channel_id = get_channel_id_from_handle(youtube, channel_handle)
185
+ if not channel_id:
186
+ print(json.dumps({
187
+ "error": f"채널을 찾을 수 없습니다: {channel_handle}"
188
+ }))
189
+ sys.exit(1)
190
+
191
+ # 채널 상세 정보 조회
192
+ channel_info = get_channel_info(youtube, channel_id)
193
+ if not channel_info:
194
+ print(json.dumps({
195
+ "error": f"채널 정보를 가져올 수 없습니다: {channel_id}"
196
+ }))
197
+ sys.exit(1)
198
+
199
+ # 핸들 결정 (없으면 custom_url 사용)
200
+ if not channel_handle:
201
+ custom_url = channel_info.get('custom_url', '')
202
+ if custom_url:
203
+ channel_handle = custom_url if custom_url.startswith('@') else '@' + custom_url
204
+ else:
205
+ channel_handle = '@' + channel_id # fallback
206
+
207
+ # channels.yaml 로드
208
+ channels_file = Path(args.output_dir) / "channels.yaml"
209
+ data = load_channels(channels_file)
210
+
211
+ # 중복 체크
212
+ existing_ids = {ch.get('id') for ch in data.get('channels', [])}
213
+ if channel_id in existing_ids:
214
+ print(json.dumps({
215
+ "status": "already_registered",
216
+ "message": f"채널이 이미 등록되어 있습니다: {channel_info['name']}",
217
+ "channel": {
218
+ "id": channel_id,
219
+ "handle": channel_handle,
220
+ "name": channel_info['name']
221
+ }
222
+ }))
223
+ sys.exit(0)
224
+
225
+ # 채널 추가
226
+ new_channel = {
227
+ 'id': channel_id,
228
+ 'handle': channel_handle,
229
+ 'name': channel_info['name'],
230
+ 'added_at': datetime.now().strftime('%Y-%m-%d')
231
+ }
232
+
233
+ data['channels'].append(new_channel)
234
+
235
+ # 저장
236
+ save_channels(channels_file, data)
237
+
238
+ print(json.dumps({
239
+ "status": "registered",
240
+ "message": f"채널이 등록되었습니다: {channel_info['name']}",
241
+ "channel": new_channel,
242
+ "file": str(channels_file)
243
+ }, ensure_ascii=False, indent=2))
244
+
245
+
246
+ if __name__ == "__main__":
247
+ main()
@@ -0,0 +1,151 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ YouTube Data API 키를 설정하는 스크립트
4
+
5
+ 사용자로부터 API 키를 입력받아 OS별 적절한 경로에 저장합니다.
6
+
7
+ 저장 경로:
8
+ - macOS/Linux: ~/.config/youtube-collector/config.yaml
9
+ - Windows: %APPDATA%\\youtube-collector\\config.yaml
10
+
11
+ Usage:
12
+ python setup_api_key.py # 대화형 입력
13
+ python setup_api_key.py --api-key KEY # 직접 지정
14
+ python setup_api_key.py --show # 현재 설정 확인
15
+ python setup_api_key.py --path # 설정 파일 경로 출력
16
+ """
17
+
18
+ import argparse
19
+ import os
20
+ import platform
21
+ import sys
22
+
23
+
24
+ def get_config_path() -> str:
25
+ """OS별 설정 파일 경로 반환"""
26
+ system = platform.system()
27
+ if system == "Windows":
28
+ base = os.environ.get("APPDATA", os.path.expanduser("~"))
29
+ return os.path.join(base, "youtube-collector", "config.yaml")
30
+ else: # macOS, Linux
31
+ xdg_config = os.environ.get("XDG_CONFIG_HOME", os.path.expanduser("~/.config"))
32
+ return os.path.join(xdg_config, "youtube-collector", "config.yaml")
33
+
34
+
35
+ def load_config(config_path: str) -> dict:
36
+ """기존 설정 로드"""
37
+ if not os.path.exists(config_path):
38
+ return {}
39
+
40
+ try:
41
+ import yaml
42
+ with open(config_path, 'r', encoding='utf-8') as f:
43
+ return yaml.safe_load(f) or {}
44
+ except ImportError:
45
+ # yaml 없으면 간단히 파싱
46
+ config = {}
47
+ with open(config_path, 'r', encoding='utf-8') as f:
48
+ for line in f:
49
+ line = line.strip()
50
+ if line and not line.startswith('#') and ':' in line:
51
+ key, value = line.split(':', 1)
52
+ config[key.strip()] = value.strip().strip('"\'')
53
+ return config
54
+ except Exception:
55
+ return {}
56
+
57
+
58
+ def save_config(config_path: str, config: dict):
59
+ """설정 저장"""
60
+ # 디렉토리 생성
61
+ config_dir = os.path.dirname(config_path)
62
+ os.makedirs(config_dir, exist_ok=True)
63
+
64
+ try:
65
+ import yaml
66
+ with open(config_path, 'w', encoding='utf-8') as f:
67
+ f.write("# YouTube Collector API 설정\n")
68
+ f.write("# 이 파일은 자동 생성되었습니다.\n\n")
69
+ yaml.dump(config, f, default_flow_style=False, allow_unicode=True)
70
+ except ImportError:
71
+ # yaml 없으면 수동 작성
72
+ with open(config_path, 'w', encoding='utf-8') as f:
73
+ f.write("# YouTube Collector API 설정\n")
74
+ f.write("# 이 파일은 자동 생성되었습니다.\n\n")
75
+ for key, value in config.items():
76
+ f.write(f'{key}: "{value}"\n')
77
+
78
+
79
+ def mask_api_key(key: str) -> str:
80
+ """API 키 마스킹 (앞 8자, 뒤 4자만 표시)"""
81
+ if not key or len(key) < 16:
82
+ return "***"
83
+ return f"{key[:8]}...{key[-4:]}"
84
+
85
+
86
+ def main():
87
+ parser = argparse.ArgumentParser(description='YouTube Data API 키 설정')
88
+ parser.add_argument('--api-key', help='설정할 API 키')
89
+ parser.add_argument('--show', action='store_true', help='현재 설정 확인')
90
+ parser.add_argument('--path', action='store_true', help='설정 파일 경로 출력')
91
+
92
+ args = parser.parse_args()
93
+
94
+ config_path = get_config_path()
95
+
96
+ # 경로만 출력
97
+ if args.path:
98
+ print(config_path)
99
+ return
100
+
101
+ # 현재 설정 확인
102
+ if args.show:
103
+ if os.path.exists(config_path):
104
+ config = load_config(config_path)
105
+ api_key = config.get('api_key', '')
106
+ print(f"설정 파일: {config_path}")
107
+ print(f"API 키: {mask_api_key(api_key) if api_key else '(설정되지 않음)'}")
108
+ else:
109
+ print(f"설정 파일이 없습니다: {config_path}")
110
+ return
111
+
112
+ # API 키 설정
113
+ api_key = args.api_key
114
+
115
+ if not api_key:
116
+ # 대화형 입력
117
+ print("YouTube Data API 키 설정")
118
+ print("-" * 40)
119
+ print(f"설정 파일 경로: {config_path}")
120
+ print()
121
+ print("API 키 발급 방법:")
122
+ print("1. https://console.cloud.google.com/ 접속")
123
+ print("2. 프로젝트 생성/선택")
124
+ print("3. YouTube Data API v3 활성화")
125
+ print("4. 사용자 인증 정보 > API 키 생성")
126
+ print()
127
+
128
+ try:
129
+ api_key = input("API 키를 입력하세요: ").strip()
130
+ except (KeyboardInterrupt, EOFError):
131
+ print("\n취소되었습니다.")
132
+ sys.exit(1)
133
+
134
+ if not api_key:
135
+ print("API 키가 입력되지 않았습니다.")
136
+ sys.exit(1)
137
+
138
+ # 기존 설정 로드 및 업데이트
139
+ config = load_config(config_path)
140
+ config['api_key'] = api_key
141
+
142
+ # 저장
143
+ save_config(config_path, config)
144
+
145
+ print()
146
+ print(f"API 키가 저장되었습니다: {config_path}")
147
+ print(f"저장된 키: {mask_api_key(api_key)}")
148
+
149
+
150
+ if __name__ == "__main__":
151
+ main()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yeongjaeyou/claude-code-config",
3
- "version": "0.16.0",
3
+ "version": "0.17.1",
4
4
  "description": "Claude Code CLI custom commands, agents, and skills",
5
5
  "bin": {
6
6
  "claude-code-config": "./bin/cli.js"