@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.
- package/.claude/commands/ask-codex.md +131 -345
- package/.claude/commands/ask-deepwiki.md +15 -15
- package/.claude/commands/ask-gemini.md +134 -352
- package/.claude/commands/code-review.md +41 -40
- package/.claude/commands/commit-and-push.md +35 -36
- package/.claude/commands/council.md +318 -0
- package/.claude/commands/edit-notebook.md +34 -33
- package/.claude/commands/gh/create-issue-label.md +19 -17
- package/.claude/commands/gh/decompose-issue.md +66 -65
- package/.claude/commands/gh/init-project.md +46 -52
- package/.claude/commands/gh/post-merge.md +74 -79
- package/.claude/commands/gh/resolve-issue.md +38 -46
- package/.claude/commands/plan.md +15 -14
- package/.claude/commands/tm/convert-prd.md +53 -53
- package/.claude/commands/tm/post-merge.md +92 -112
- package/.claude/commands/tm/resolve-issue.md +148 -154
- package/.claude/commands/tm/review-prd-with-codex.md +272 -279
- package/.claude/commands/tm/sync-to-github.md +189 -212
- package/.claude/guidelines/cv-guidelines.md +30 -0
- package/.claude/guidelines/id-reference.md +34 -0
- package/.claude/guidelines/work-guidelines.md +17 -0
- package/.claude/skills/notion-md-uploader/SKILL.md +252 -0
- package/.claude/skills/notion-md-uploader/references/notion_block_types.md +323 -0
- package/.claude/skills/notion-md-uploader/references/setup_guide.md +156 -0
- package/.claude/skills/notion-md-uploader/scripts/__pycache__/markdown_parser.cpython-311.pyc +0 -0
- package/.claude/skills/notion-md-uploader/scripts/__pycache__/notion_client.cpython-311.pyc +0 -0
- package/.claude/skills/notion-md-uploader/scripts/__pycache__/notion_converter.cpython-311.pyc +0 -0
- package/.claude/skills/notion-md-uploader/scripts/markdown_parser.py +607 -0
- package/.claude/skills/notion-md-uploader/scripts/notion_client.py +337 -0
- package/.claude/skills/notion-md-uploader/scripts/notion_converter.py +477 -0
- package/.claude/skills/notion-md-uploader/scripts/upload_md.py +298 -0
- package/.claude/skills/skill-creator/LICENSE.txt +202 -0
- package/.claude/skills/skill-creator/SKILL.md +209 -0
- package/.claude/skills/skill-creator/scripts/init_skill.py +303 -0
- package/.claude/skills/skill-creator/scripts/package_skill.py +110 -0
- package/.claude/skills/skill-creator/scripts/quick_validate.py +65 -0
- package/README.md +159 -129
- 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()
|