@yeongjaeyou/claude-code-config 0.4.0 → 0.5.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 (38) hide show
  1. package/.claude/commands/ask-codex.md +131 -345
  2. package/.claude/commands/ask-deepwiki.md +15 -15
  3. package/.claude/commands/ask-gemini.md +134 -352
  4. package/.claude/commands/code-review.md +41 -40
  5. package/.claude/commands/commit-and-push.md +35 -36
  6. package/.claude/commands/council.md +318 -0
  7. package/.claude/commands/edit-notebook.md +34 -33
  8. package/.claude/commands/gh/create-issue-label.md +19 -17
  9. package/.claude/commands/gh/decompose-issue.md +66 -65
  10. package/.claude/commands/gh/init-project.md +46 -52
  11. package/.claude/commands/gh/post-merge.md +74 -79
  12. package/.claude/commands/gh/resolve-issue.md +38 -46
  13. package/.claude/commands/plan.md +15 -14
  14. package/.claude/commands/tm/convert-prd.md +53 -53
  15. package/.claude/commands/tm/post-merge.md +92 -112
  16. package/.claude/commands/tm/resolve-issue.md +148 -154
  17. package/.claude/commands/tm/review-prd-with-codex.md +272 -279
  18. package/.claude/commands/tm/sync-to-github.md +189 -212
  19. package/.claude/guidelines/cv-guidelines.md +30 -0
  20. package/.claude/guidelines/id-reference.md +34 -0
  21. package/.claude/guidelines/work-guidelines.md +17 -0
  22. package/.claude/skills/notion-md-uploader/SKILL.md +252 -0
  23. package/.claude/skills/notion-md-uploader/references/notion_block_types.md +323 -0
  24. package/.claude/skills/notion-md-uploader/references/setup_guide.md +156 -0
  25. package/.claude/skills/notion-md-uploader/scripts/__pycache__/markdown_parser.cpython-311.pyc +0 -0
  26. package/.claude/skills/notion-md-uploader/scripts/__pycache__/notion_client.cpython-311.pyc +0 -0
  27. package/.claude/skills/notion-md-uploader/scripts/__pycache__/notion_converter.cpython-311.pyc +0 -0
  28. package/.claude/skills/notion-md-uploader/scripts/markdown_parser.py +607 -0
  29. package/.claude/skills/notion-md-uploader/scripts/notion_client.py +337 -0
  30. package/.claude/skills/notion-md-uploader/scripts/notion_converter.py +477 -0
  31. package/.claude/skills/notion-md-uploader/scripts/upload_md.py +298 -0
  32. package/.claude/skills/skill-creator/LICENSE.txt +202 -0
  33. package/.claude/skills/skill-creator/SKILL.md +209 -0
  34. package/.claude/skills/skill-creator/scripts/init_skill.py +303 -0
  35. package/.claude/skills/skill-creator/scripts/package_skill.py +110 -0
  36. package/.claude/skills/skill-creator/scripts/quick_validate.py +65 -0
  37. package/README.md +159 -129
  38. package/package.json +1 -1
@@ -0,0 +1,337 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Notion API Client for file uploads and page creation.
4
+
5
+ This module provides a wrapper around the Notion API for:
6
+ - Creating pages with content blocks
7
+ - Uploading files (images, documents)
8
+ - Appending blocks to existing pages
9
+ """
10
+
11
+ import json
12
+ import mimetypes
13
+ import os
14
+ from dataclasses import dataclass
15
+ from pathlib import Path
16
+ from typing import Any
17
+
18
+ import requests
19
+
20
+
21
+ @dataclass
22
+ class NotionConfig:
23
+ """Notion API configuration."""
24
+
25
+ api_key: str
26
+ api_version: str = "2022-06-28"
27
+ base_url: str = "https://api.notion.com/v1"
28
+
29
+ @classmethod
30
+ def from_env(cls) -> "NotionConfig":
31
+ """Create config from environment variables."""
32
+ api_key = os.getenv("NOTION_API_KEY")
33
+ if not api_key:
34
+ raise ValueError("NOTION_API_KEY environment variable is not set")
35
+ return cls(api_key=api_key)
36
+
37
+
38
+ class NotionAPIError(Exception):
39
+ """Custom exception for Notion API errors."""
40
+
41
+ def __init__(self, status_code: int, message: str, response_body: str = ""):
42
+ self.status_code = status_code
43
+ self.message = message
44
+ self.response_body = response_body
45
+ super().__init__(f"Notion API Error ({status_code}): {message}")
46
+
47
+
48
+ class NotionClient:
49
+ """Client for interacting with Notion API."""
50
+
51
+ def __init__(self, config: NotionConfig | None = None):
52
+ """Initialize the Notion client.
53
+
54
+ Args:
55
+ config: NotionConfig instance. If None, loads from environment.
56
+ """
57
+ self.config = config or NotionConfig.from_env()
58
+ self._session = requests.Session()
59
+ self._session.headers.update(self._default_headers())
60
+
61
+ def _default_headers(self) -> dict[str, str]:
62
+ """Return default headers for API requests."""
63
+ return {
64
+ "Authorization": f"Bearer {self.config.api_key}",
65
+ "Notion-Version": self.config.api_version,
66
+ "Content-Type": "application/json",
67
+ }
68
+
69
+ def _request(
70
+ self,
71
+ method: str,
72
+ endpoint: str,
73
+ json_data: dict[str, Any] | None = None,
74
+ **kwargs: Any,
75
+ ) -> dict[str, Any]:
76
+ """Make an API request to Notion.
77
+
78
+ Args:
79
+ method: HTTP method (GET, POST, PATCH, DELETE)
80
+ endpoint: API endpoint (without base URL)
81
+ json_data: JSON payload for the request
82
+ **kwargs: Additional arguments for requests
83
+
84
+ Returns:
85
+ JSON response from the API
86
+
87
+ Raises:
88
+ NotionAPIError: If the API returns an error
89
+ """
90
+ url = f"{self.config.base_url}/{endpoint.lstrip('/')}"
91
+
92
+ response = self._session.request(
93
+ method=method,
94
+ url=url,
95
+ json=json_data,
96
+ **kwargs,
97
+ )
98
+
99
+ if not response.ok:
100
+ raise NotionAPIError(
101
+ status_code=response.status_code,
102
+ message=response.reason,
103
+ response_body=response.text,
104
+ )
105
+
106
+ return response.json()
107
+
108
+ def create_page(
109
+ self,
110
+ parent_page_id: str,
111
+ title: str,
112
+ children: list[dict[str, Any]] | None = None,
113
+ icon: dict[str, Any] | None = None,
114
+ cover: dict[str, Any] | None = None,
115
+ ) -> dict[str, Any]:
116
+ """Create a new page under a parent page.
117
+
118
+ Args:
119
+ parent_page_id: ID of the parent page
120
+ title: Page title
121
+ children: List of block objects for page content
122
+ icon: Page icon (emoji or file)
123
+ cover: Page cover image
124
+
125
+ Returns:
126
+ Created page object
127
+ """
128
+ payload: dict[str, Any] = {
129
+ "parent": {"type": "page_id", "page_id": parent_page_id},
130
+ "properties": {
131
+ "title": {
132
+ "title": [{"type": "text", "text": {"content": title}}]
133
+ }
134
+ },
135
+ }
136
+
137
+ if children:
138
+ payload["children"] = children
139
+ if icon:
140
+ payload["icon"] = icon
141
+ if cover:
142
+ payload["cover"] = cover
143
+
144
+ return self._request("POST", "/pages", json_data=payload)
145
+
146
+ def append_blocks(
147
+ self,
148
+ block_id: str,
149
+ children: list[dict[str, Any]],
150
+ ) -> dict[str, Any]:
151
+ """Append blocks to an existing page or block.
152
+
153
+ Note: Notion API has a limit of 100 blocks per request.
154
+ For larger content, use append_blocks_chunked.
155
+
156
+ Args:
157
+ block_id: ID of the page or block to append to
158
+ children: List of block objects to append
159
+
160
+ Returns:
161
+ Response containing the appended blocks
162
+ """
163
+ return self._request(
164
+ "PATCH",
165
+ f"/blocks/{block_id}/children",
166
+ json_data={"children": children},
167
+ )
168
+
169
+ def append_blocks_chunked(
170
+ self,
171
+ block_id: str,
172
+ children: list[dict[str, Any]],
173
+ chunk_size: int = 100,
174
+ ) -> list[dict[str, Any]]:
175
+ """Append blocks in chunks to handle Notion's 100 block limit.
176
+
177
+ Args:
178
+ block_id: ID of the page or block to append to
179
+ children: List of block objects to append
180
+ chunk_size: Maximum blocks per request (default 100)
181
+
182
+ Returns:
183
+ List of responses from each chunk
184
+ """
185
+ responses = []
186
+ for i in range(0, len(children), chunk_size):
187
+ chunk = children[i : i + chunk_size]
188
+ response = self.append_blocks(block_id, chunk)
189
+ responses.append(response)
190
+ return responses
191
+
192
+ def create_file_upload(
193
+ self,
194
+ filename: str | None = None,
195
+ content_type: str | None = None,
196
+ ) -> dict[str, Any]:
197
+ """Create a file upload object.
198
+
199
+ Args:
200
+ filename: Optional filename
201
+ content_type: Optional MIME type
202
+
203
+ Returns:
204
+ File upload object with id and upload_url
205
+ """
206
+ payload = {}
207
+ if filename:
208
+ payload["filename"] = filename
209
+ if content_type:
210
+ payload["content_type"] = content_type
211
+
212
+ return self._request("POST", "/file_uploads", json_data=payload)
213
+
214
+ def send_file_upload(
215
+ self,
216
+ file_upload_id: str,
217
+ file_path: str | Path,
218
+ ) -> dict[str, Any]:
219
+ """Upload file contents to a file upload object.
220
+
221
+ Args:
222
+ file_upload_id: ID from create_file_upload
223
+ file_path: Path to the file to upload
224
+
225
+ Returns:
226
+ Updated file upload object with status 'uploaded'
227
+ """
228
+ file_path = Path(file_path)
229
+ if not file_path.exists():
230
+ raise FileNotFoundError(f"File not found: {file_path}")
231
+
232
+ # Detect MIME type
233
+ mime_type, _ = mimetypes.guess_type(str(file_path))
234
+ if mime_type is None:
235
+ mime_type = "application/octet-stream"
236
+
237
+ url = f"{self.config.base_url}/file_uploads/{file_upload_id}/send"
238
+
239
+ with open(file_path, "rb") as f:
240
+ files = {"file": (file_path.name, f, mime_type)}
241
+ # Remove Content-Type header for multipart/form-data
242
+ headers = {
243
+ "Authorization": f"Bearer {self.config.api_key}",
244
+ "Notion-Version": self.config.api_version,
245
+ }
246
+ response = requests.post(url, headers=headers, files=files)
247
+
248
+ if not response.ok:
249
+ raise NotionAPIError(
250
+ status_code=response.status_code,
251
+ message=response.reason,
252
+ response_body=response.text,
253
+ )
254
+
255
+ return response.json()
256
+
257
+ def upload_file(self, file_path: str | Path) -> str:
258
+ """Upload a file to Notion and return the file_upload ID.
259
+
260
+ This is a convenience method that combines create_file_upload
261
+ and send_file_upload.
262
+
263
+ Args:
264
+ file_path: Path to the file to upload
265
+
266
+ Returns:
267
+ File upload ID that can be used in blocks
268
+ """
269
+ file_path = Path(file_path)
270
+ mime_type, _ = mimetypes.guess_type(str(file_path))
271
+
272
+ # Step 1: Create file upload object
273
+ file_upload = self.create_file_upload(
274
+ filename=file_path.name,
275
+ content_type=mime_type,
276
+ )
277
+ file_upload_id = file_upload["id"]
278
+
279
+ # Step 2: Send file contents
280
+ self.send_file_upload(file_upload_id, file_path)
281
+
282
+ return file_upload_id
283
+
284
+ def get_page(self, page_id: str) -> dict[str, Any]:
285
+ """Retrieve a page by ID.
286
+
287
+ Args:
288
+ page_id: The page ID
289
+
290
+ Returns:
291
+ Page object
292
+ """
293
+ return self._request("GET", f"/pages/{page_id}")
294
+
295
+ def search(
296
+ self,
297
+ query: str = "",
298
+ filter_type: str | None = None,
299
+ page_size: int = 100,
300
+ ) -> dict[str, Any]:
301
+ """Search for pages and databases.
302
+
303
+ Args:
304
+ query: Search query string
305
+ filter_type: Filter by 'page' or 'database'
306
+ page_size: Number of results per page
307
+
308
+ Returns:
309
+ Search results
310
+ """
311
+ payload: dict[str, Any] = {"page_size": page_size}
312
+ if query:
313
+ payload["query"] = query
314
+ if filter_type:
315
+ payload["filter"] = {"property": "object", "value": filter_type}
316
+
317
+ return self._request("POST", "/search", json_data=payload)
318
+
319
+
320
+ def main():
321
+ """Test the Notion client."""
322
+ try:
323
+ client = NotionClient()
324
+ print("Notion client initialized successfully")
325
+
326
+ # Test search
327
+ results = client.search(query="", page_size=5)
328
+ print(f"Found {len(results.get('results', []))} items")
329
+
330
+ except NotionAPIError as e:
331
+ print(f"API Error: {e}")
332
+ except ValueError as e:
333
+ print(f"Configuration Error: {e}")
334
+
335
+
336
+ if __name__ == "__main__":
337
+ main()