@yeongjaeyou/claude-code-config 0.18.6 → 0.18.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.
Files changed (26) hide show
  1. package/.claude/commands/gh/auto-review-loop.md +2 -2
  2. package/.claude/commands/gh/decompose-issue.md +12 -12
  3. package/.claude/commands/gh/resolve-and-review.md +1 -1
  4. package/.claude/guidelines/id-reference.md +2 -2
  5. package/.claude/guidelines/prd-guide.md +13 -13
  6. package/.claude/guidelines/work-guidelines.md +1 -1
  7. package/.claude/hooks/notify_osc.sh +7 -7
  8. package/.claude/skills/code-explorer/SKILL.md +21 -21
  9. package/.claude/skills/midjourney-imagineapi/SKILL.md +260 -0
  10. package/.claude/skills/midjourney-imagineapi/references/imagineapi-integration.md +197 -0
  11. package/.claude/skills/midjourney-imagineapi/references/midjourney-parameters.md +393 -0
  12. package/.claude/skills/midjourney-imagineapi/references/midjourney-style-guide.md +296 -0
  13. package/.claude/skills/midjourney-imagineapi/references/prompt-examples.md +428 -0
  14. package/.claude/skills/notion-md-uploader/scripts/__pycache__/markdown_parser.cpython-311.pyc +0 -0
  15. package/.claude/skills/notion-md-uploader/scripts/__pycache__/notion_client.cpython-311.pyc +0 -0
  16. package/.claude/skills/notion-md-uploader/scripts/__pycache__/notion_converter.cpython-311.pyc +0 -0
  17. package/.claude/skills/notion-md-uploader/scripts/__pycache__/upload_md.cpython-311.pyc +0 -0
  18. package/package.json +1 -1
  19. package/.claude/skills/youtube-collector/README.md +0 -107
  20. package/.claude/skills/youtube-collector/SKILL.md +0 -158
  21. package/.claude/skills/youtube-collector/references/data-schema.md +0 -110
  22. package/.claude/skills/youtube-collector/scripts/collect_videos.py +0 -304
  23. package/.claude/skills/youtube-collector/scripts/fetch_transcript.py +0 -138
  24. package/.claude/skills/youtube-collector/scripts/fetch_videos.py +0 -229
  25. package/.claude/skills/youtube-collector/scripts/register_channel.py +0 -247
  26. package/.claude/skills/youtube-collector/scripts/setup_api_key.py +0 -151
@@ -1,229 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- YouTube 채널의 최신 영상 목록을 가져오는 스크립트
4
-
5
- Usage:
6
- python fetch_videos.py --channel-id UC... [--api-key YOUR_API_KEY] [--max-results 10]
7
- python fetch_videos.py --channel-handle @channelname
8
-
9
- API 키는 다음 위치에서 자동으로 로드됩니다:
10
- - macOS/Linux: ~/.config/youtube-collector/config.yaml
11
- - Windows: %APPDATA%\\youtube-collector\\config.yaml
12
-
13
- Output:
14
- JSON 형식으로 영상 목록 출력
15
-
16
- Requirements:
17
- pip install google-api-python-client pyyaml
18
- """
19
-
20
- import argparse
21
- import json
22
- import os
23
- import platform
24
- import sys
25
- from datetime import datetime
26
-
27
- try:
28
- from googleapiclient.discovery import build
29
- from googleapiclient.errors import HttpError
30
- except ImportError:
31
- print(json.dumps({
32
- "error": "google-api-python-client가 설치되어 있지 않습니다.",
33
- "install": "pip install google-api-python-client"
34
- }))
35
- sys.exit(1)
36
-
37
- try:
38
- import yaml
39
- except ImportError:
40
- print(json.dumps({
41
- "error": "pyyaml이 설치되어 있지 않습니다.",
42
- "install": "pip install pyyaml"
43
- }))
44
- sys.exit(1)
45
-
46
-
47
- def get_api_key_config_path() -> str:
48
- """OS별 API 키 설정 파일 경로 반환"""
49
- system = platform.system()
50
- if system == "Windows":
51
- base = os.environ.get("APPDATA", os.path.expanduser("~"))
52
- return os.path.join(base, "youtube-collector", "config.yaml")
53
- else: # macOS, Linux
54
- xdg_config = os.environ.get("XDG_CONFIG_HOME", os.path.expanduser("~/.config"))
55
- return os.path.join(xdg_config, "youtube-collector", "config.yaml")
56
-
57
-
58
- def load_api_key() -> str:
59
- """설정 파일에서 API 키 로드"""
60
- config_path = get_api_key_config_path()
61
-
62
- if not os.path.exists(config_path):
63
- return None
64
-
65
- try:
66
- with open(config_path, 'r', encoding='utf-8') as f:
67
- config = yaml.safe_load(f)
68
- return config.get('api_key') if config else None
69
- except Exception:
70
- return None
71
-
72
-
73
- def get_channel_id_from_handle(youtube, handle: str) -> str:
74
- """채널 핸들(@username)로 채널 ID 조회"""
75
- # @를 제거
76
- handle = handle.lstrip('@')
77
-
78
- try:
79
- response = youtube.search().list(
80
- part="snippet",
81
- q=f"@{handle}",
82
- type="channel",
83
- maxResults=1
84
- ).execute()
85
-
86
- if response.get('items'):
87
- return response['items'][0]['snippet']['channelId']
88
- return None
89
- except HttpError as e:
90
- return None
91
-
92
-
93
- def get_channel_uploads_playlist_id(youtube, channel_id: str) -> str:
94
- """채널의 업로드 재생목록 ID 조회 (UC... -> UU...)"""
95
- try:
96
- response = youtube.channels().list(
97
- part="contentDetails",
98
- id=channel_id
99
- ).execute()
100
-
101
- if response.get('items'):
102
- return response['items'][0]['contentDetails']['relatedPlaylists']['uploads']
103
- return None
104
- except HttpError:
105
- return None
106
-
107
-
108
- def fetch_videos(youtube, channel_id: str, max_results: int = 10) -> list:
109
- """채널의 최신 영상 목록 조회"""
110
- videos = []
111
-
112
- # 업로드 재생목록 ID 가져오기
113
- uploads_playlist_id = get_channel_uploads_playlist_id(youtube, channel_id)
114
- if not uploads_playlist_id:
115
- return videos
116
-
117
- try:
118
- # 재생목록에서 영상 ID 목록 가져오기
119
- response = youtube.playlistItems().list(
120
- part="snippet,contentDetails",
121
- playlistId=uploads_playlist_id,
122
- maxResults=min(max_results, 50)
123
- ).execute()
124
-
125
- video_ids = [item['contentDetails']['videoId'] for item in response.get('items', [])]
126
-
127
- if not video_ids:
128
- return videos
129
-
130
- # 영상 상세 정보 가져오기
131
- videos_response = youtube.videos().list(
132
- part="snippet,contentDetails",
133
- id=','.join(video_ids)
134
- ).execute()
135
-
136
- for item in videos_response.get('items', []):
137
- snippet = item['snippet']
138
- content_details = item['contentDetails']
139
-
140
- # 썸네일 URL (최대 해상도 우선)
141
- thumbnails = snippet.get('thumbnails', {})
142
- thumbnail_url = (
143
- thumbnails.get('maxres', {}).get('url') or
144
- thumbnails.get('high', {}).get('url') or
145
- thumbnails.get('medium', {}).get('url') or
146
- thumbnails.get('default', {}).get('url', '')
147
- )
148
-
149
- videos.append({
150
- 'video_id': item['id'],
151
- 'title': snippet.get('title', ''),
152
- 'description': snippet.get('description', ''),
153
- 'published_at': snippet.get('publishedAt', ''),
154
- 'channel_id': snippet.get('channelId', ''),
155
- 'channel_title': snippet.get('channelTitle', ''),
156
- 'thumbnail': thumbnail_url,
157
- 'duration': content_details.get('duration', ''),
158
- 'url': f"https://youtube.com/watch?v={item['id']}"
159
- })
160
-
161
- return videos
162
-
163
- except HttpError as e:
164
- print(json.dumps({
165
- "error": f"YouTube API 오류: {e.resp.status}",
166
- "message": str(e)
167
- }), file=sys.stderr)
168
- return videos
169
-
170
-
171
- def main():
172
- parser = argparse.ArgumentParser(description='YouTube 채널의 최신 영상 목록 조회')
173
- parser.add_argument('--channel-id', help='채널 ID (UC...)')
174
- parser.add_argument('--channel-handle', help='채널 핸들 (@username)')
175
- parser.add_argument('--api-key', help='YouTube Data API 키 (미지정시 설정 파일에서 로드)')
176
- parser.add_argument('--max-results', type=int, default=10, help='최대 결과 수 (기본: 10)')
177
-
178
- args = parser.parse_args()
179
-
180
- if not args.channel_id and not args.channel_handle:
181
- print(json.dumps({
182
- "error": "--channel-id 또는 --channel-handle 중 하나를 지정해야 합니다."
183
- }))
184
- sys.exit(1)
185
-
186
- # API 키 결정: 인자 > 설정 파일
187
- api_key = args.api_key or load_api_key()
188
-
189
- if not api_key:
190
- config_path = get_api_key_config_path()
191
- print(json.dumps({
192
- "error": "YouTube Data API 키가 설정되지 않았습니다.",
193
- "help": f"다음 명령으로 API 키를 설정하세요: python3 setup_api_key.py",
194
- "config_path": config_path
195
- }))
196
- sys.exit(1)
197
-
198
- try:
199
- youtube = build('youtube', 'v3', developerKey=api_key)
200
- except Exception as e:
201
- print(json.dumps({
202
- "error": "YouTube API 초기화 실패",
203
- "message": str(e)
204
- }))
205
- sys.exit(1)
206
-
207
- # 채널 ID 결정
208
- channel_id = args.channel_id
209
- if not channel_id and args.channel_handle:
210
- channel_id = get_channel_id_from_handle(youtube, args.channel_handle)
211
- if not channel_id:
212
- print(json.dumps({
213
- "error": f"채널을 찾을 수 없습니다: {args.channel_handle}"
214
- }))
215
- sys.exit(1)
216
-
217
- # 영상 목록 조회
218
- videos = fetch_videos(youtube, channel_id, args.max_results)
219
-
220
- print(json.dumps({
221
- "channel_id": channel_id,
222
- "fetched_at": datetime.utcnow().isoformat() + "Z",
223
- "count": len(videos),
224
- "videos": videos
225
- }, ensure_ascii=False, indent=2))
226
-
227
-
228
- if __name__ == "__main__":
229
- main()
@@ -1,247 +0,0 @@
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()
@@ -1,151 +0,0 @@
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()