@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,477 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Notion Block Converter.
4
+
5
+ Converts parsed Markdown blocks to Notion API block objects.
6
+ """
7
+
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ from markdown_parser import BlockType, InlineStyle, MarkdownBlock
12
+
13
+
14
+ class NotionBlockConverter:
15
+ """Converts MarkdownBlock objects to Notion API block format."""
16
+
17
+ # Emoji mapping for callout types
18
+ CALLOUT_ICONS = {
19
+ "note": "information_source",
20
+ "warning": "warning",
21
+ "tip": "bulb",
22
+ "important": "star",
23
+ "caution": "warning",
24
+ }
25
+
26
+ # Language mapping for code blocks
27
+ LANGUAGE_MAP = {
28
+ "py": "python",
29
+ "js": "javascript",
30
+ "ts": "typescript",
31
+ "sh": "shell",
32
+ "bash": "shell",
33
+ "yml": "yaml",
34
+ "": "plain text",
35
+ }
36
+
37
+ def __init__(
38
+ self,
39
+ image_uploader: Any | None = None,
40
+ base_path: str = "",
41
+ ):
42
+ """Initialize the converter.
43
+
44
+ Args:
45
+ image_uploader: Optional callable that uploads images and returns file_upload_id
46
+ base_path: Base path for resolving relative image paths
47
+ """
48
+ self.image_uploader = image_uploader
49
+ self.base_path = Path(base_path) if base_path else Path.cwd()
50
+
51
+ def convert_blocks(
52
+ self,
53
+ blocks: list[MarkdownBlock],
54
+ ) -> list[dict[str, Any]]:
55
+ """Convert a list of MarkdownBlocks to Notion blocks.
56
+
57
+ Args:
58
+ blocks: List of parsed MarkdownBlock objects
59
+
60
+ Returns:
61
+ List of Notion block objects
62
+ """
63
+ notion_blocks = []
64
+ for block in blocks:
65
+ converted = self.convert_block(block)
66
+ if converted:
67
+ if isinstance(converted, list):
68
+ notion_blocks.extend(converted)
69
+ else:
70
+ notion_blocks.append(converted)
71
+ return notion_blocks
72
+
73
+ def convert_block(
74
+ self,
75
+ block: MarkdownBlock,
76
+ ) -> dict[str, Any] | list[dict[str, Any]] | None:
77
+ """Convert a single MarkdownBlock to Notion block(s).
78
+
79
+ Args:
80
+ block: Parsed MarkdownBlock object
81
+
82
+ Returns:
83
+ Notion block object, list of blocks, or None
84
+ """
85
+ converters = {
86
+ BlockType.HEADING1: self._convert_heading,
87
+ BlockType.HEADING2: self._convert_heading,
88
+ BlockType.HEADING3: self._convert_heading,
89
+ BlockType.PARAGRAPH: self._convert_paragraph,
90
+ BlockType.BULLETED_LIST: self._convert_bulleted_list,
91
+ BlockType.NUMBERED_LIST: self._convert_numbered_list,
92
+ BlockType.CODE_BLOCK: self._convert_code_block,
93
+ BlockType.QUOTE: self._convert_quote,
94
+ BlockType.DIVIDER: self._convert_divider,
95
+ BlockType.IMAGE: self._convert_image,
96
+ BlockType.TABLE: self._convert_table,
97
+ BlockType.TODO: self._convert_todo,
98
+ BlockType.CALLOUT: self._convert_callout,
99
+ }
100
+
101
+ converter = converters.get(block.block_type)
102
+ if converter:
103
+ return converter(block)
104
+ return None
105
+
106
+ def _convert_rich_text(
107
+ self,
108
+ content: list[InlineStyle] | str,
109
+ ) -> list[dict[str, Any]]:
110
+ """Convert inline content to Notion rich_text array.
111
+
112
+ Args:
113
+ content: InlineStyle list or plain string
114
+
115
+ Returns:
116
+ Notion rich_text array
117
+ """
118
+ if isinstance(content, str):
119
+ return [{"type": "text", "text": {"content": content}}]
120
+
121
+ rich_text = []
122
+ for style in content:
123
+ text_obj: dict[str, Any] = {
124
+ "type": "text",
125
+ "text": {"content": style.text},
126
+ "annotations": {
127
+ "bold": style.bold,
128
+ "italic": style.italic,
129
+ "strikethrough": style.strikethrough,
130
+ "underline": False,
131
+ "code": style.code,
132
+ "color": "default",
133
+ },
134
+ }
135
+ # Only add link if it's a valid HTTP(S) URL
136
+ if style.link and style.link.startswith(("http://", "https://")):
137
+ text_obj["text"]["link"] = {"url": style.link}
138
+
139
+ rich_text.append(text_obj)
140
+
141
+ return rich_text if rich_text else [{"type": "text", "text": {"content": ""}}]
142
+
143
+ def _convert_heading(
144
+ self,
145
+ block: MarkdownBlock,
146
+ ) -> dict[str, Any]:
147
+ """Convert heading block."""
148
+ heading_map = {
149
+ BlockType.HEADING1: "heading_1",
150
+ BlockType.HEADING2: "heading_2",
151
+ BlockType.HEADING3: "heading_3",
152
+ }
153
+ heading_type = heading_map[block.block_type]
154
+
155
+ return {
156
+ "object": "block",
157
+ "type": heading_type,
158
+ heading_type: {
159
+ "rich_text": self._convert_rich_text(block.content),
160
+ "color": "default",
161
+ "is_toggleable": False,
162
+ },
163
+ }
164
+
165
+ def _convert_paragraph(
166
+ self,
167
+ block: MarkdownBlock,
168
+ ) -> dict[str, Any]:
169
+ """Convert paragraph block."""
170
+ return {
171
+ "object": "block",
172
+ "type": "paragraph",
173
+ "paragraph": {
174
+ "rich_text": self._convert_rich_text(block.content),
175
+ "color": "default",
176
+ },
177
+ }
178
+
179
+ def _convert_bulleted_list(
180
+ self,
181
+ block: MarkdownBlock,
182
+ ) -> dict[str, Any]:
183
+ """Convert bulleted list item."""
184
+ return {
185
+ "object": "block",
186
+ "type": "bulleted_list_item",
187
+ "bulleted_list_item": {
188
+ "rich_text": self._convert_rich_text(block.content),
189
+ "color": "default",
190
+ },
191
+ }
192
+
193
+ def _convert_numbered_list(
194
+ self,
195
+ block: MarkdownBlock,
196
+ ) -> dict[str, Any]:
197
+ """Convert numbered list item."""
198
+ return {
199
+ "object": "block",
200
+ "type": "numbered_list_item",
201
+ "numbered_list_item": {
202
+ "rich_text": self._convert_rich_text(block.content),
203
+ "color": "default",
204
+ },
205
+ }
206
+
207
+ def _convert_code_block(
208
+ self,
209
+ block: MarkdownBlock,
210
+ ) -> dict[str, Any]:
211
+ """Convert code block."""
212
+ language = block.metadata.get("language", "plain text")
213
+ language = self.LANGUAGE_MAP.get(language, language)
214
+
215
+ # Notion has specific language values
216
+ valid_languages = {
217
+ "abap", "arduino", "bash", "basic", "c", "clojure", "coffeescript",
218
+ "c++", "c#", "css", "dart", "diff", "docker", "elixir", "elm",
219
+ "erlang", "flow", "fortran", "f#", "gherkin", "glsl", "go", "graphql",
220
+ "groovy", "haskell", "html", "java", "javascript", "json", "julia",
221
+ "kotlin", "latex", "less", "lisp", "livescript", "lua", "makefile",
222
+ "markdown", "markup", "matlab", "mermaid", "nix", "objective-c",
223
+ "ocaml", "pascal", "perl", "php", "plain text", "powershell",
224
+ "prolog", "protobuf", "python", "r", "reason", "ruby", "rust",
225
+ "sass", "scala", "scheme", "scss", "shell", "sql", "swift",
226
+ "typescript", "vb.net", "verilog", "vhdl", "visual basic",
227
+ "webassembly", "xml", "yaml", "java/c/c++/c#",
228
+ }
229
+
230
+ if language.lower() not in valid_languages:
231
+ language = "plain text"
232
+
233
+ return {
234
+ "object": "block",
235
+ "type": "code",
236
+ "code": {
237
+ "rich_text": [
238
+ {"type": "text", "text": {"content": block.content}}
239
+ ],
240
+ "language": language.lower(),
241
+ "caption": [],
242
+ },
243
+ }
244
+
245
+ def _convert_quote(
246
+ self,
247
+ block: MarkdownBlock,
248
+ ) -> dict[str, Any]:
249
+ """Convert quote block."""
250
+ return {
251
+ "object": "block",
252
+ "type": "quote",
253
+ "quote": {
254
+ "rich_text": self._convert_rich_text(block.content),
255
+ "color": "default",
256
+ },
257
+ }
258
+
259
+ def _convert_divider(
260
+ self,
261
+ block: MarkdownBlock,
262
+ ) -> dict[str, Any]:
263
+ """Convert divider block."""
264
+ return {
265
+ "object": "block",
266
+ "type": "divider",
267
+ "divider": {},
268
+ }
269
+
270
+ def _convert_image(
271
+ self,
272
+ block: MarkdownBlock,
273
+ ) -> dict[str, Any]:
274
+ """Convert image block."""
275
+ url = block.metadata.get("url", "")
276
+ alt_text = block.content if isinstance(block.content, str) else ""
277
+
278
+ # Check if it's a local file or URL
279
+ if url.startswith(("http://", "https://")):
280
+ # External URL
281
+ return {
282
+ "object": "block",
283
+ "type": "image",
284
+ "image": {
285
+ "type": "external",
286
+ "external": {"url": url},
287
+ "caption": [
288
+ {"type": "text", "text": {"content": alt_text}}
289
+ ] if alt_text else [],
290
+ },
291
+ }
292
+ else:
293
+ # Local file - needs upload
294
+ local_path = self.base_path / url
295
+ if self.image_uploader and local_path.exists():
296
+ try:
297
+ file_upload_id = self.image_uploader(str(local_path))
298
+ return {
299
+ "object": "block",
300
+ "type": "image",
301
+ "image": {
302
+ "type": "file_upload",
303
+ "file_upload": {"id": file_upload_id},
304
+ "caption": [
305
+ {"type": "text", "text": {"content": alt_text}}
306
+ ] if alt_text else [],
307
+ },
308
+ }
309
+ except Exception as e:
310
+ # Fall back to paragraph with error message
311
+ return {
312
+ "object": "block",
313
+ "type": "paragraph",
314
+ "paragraph": {
315
+ "rich_text": [
316
+ {
317
+ "type": "text",
318
+ "text": {"content": f"[Image upload failed: {url}] - {e}"},
319
+ "annotations": {"italic": True, "color": "red"},
320
+ }
321
+ ],
322
+ },
323
+ }
324
+ else:
325
+ # No uploader or file not found
326
+ return {
327
+ "object": "block",
328
+ "type": "paragraph",
329
+ "paragraph": {
330
+ "rich_text": [
331
+ {
332
+ "type": "text",
333
+ "text": {"content": f"[Image: {url}]"},
334
+ "annotations": {"italic": True, "bold": False, "strikethrough": False, "underline": False, "code": False, "color": "gray"},
335
+ }
336
+ ],
337
+ },
338
+ }
339
+
340
+ def _convert_table(
341
+ self,
342
+ block: MarkdownBlock,
343
+ ) -> list[dict[str, Any]]:
344
+ """Convert table block to Notion table blocks."""
345
+ rows = block.metadata.get("rows", [])
346
+ has_header = block.metadata.get("has_header", True)
347
+ column_count = block.metadata.get("column_count", 0)
348
+
349
+ if not rows or column_count == 0:
350
+ return []
351
+
352
+ # Create table block with children
353
+ table_rows = []
354
+ for row in rows:
355
+ # Ensure row has correct number of cells
356
+ cells = row[:column_count]
357
+ while len(cells) < column_count:
358
+ cells.append("")
359
+
360
+ table_row = {
361
+ "object": "block",
362
+ "type": "table_row",
363
+ "table_row": {
364
+ "cells": [
365
+ [{"type": "text", "text": {"content": cell}}]
366
+ for cell in cells
367
+ ]
368
+ },
369
+ }
370
+ table_rows.append(table_row)
371
+
372
+ table_block = {
373
+ "object": "block",
374
+ "type": "table",
375
+ "table": {
376
+ "table_width": column_count,
377
+ "has_column_header": has_header,
378
+ "has_row_header": False,
379
+ "children": table_rows,
380
+ },
381
+ }
382
+
383
+ return [table_block]
384
+
385
+ def _convert_todo(
386
+ self,
387
+ block: MarkdownBlock,
388
+ ) -> dict[str, Any]:
389
+ """Convert todo/checkbox block."""
390
+ checked = block.metadata.get("checked", False)
391
+
392
+ return {
393
+ "object": "block",
394
+ "type": "to_do",
395
+ "to_do": {
396
+ "rich_text": self._convert_rich_text(block.content),
397
+ "checked": checked,
398
+ "color": "default",
399
+ },
400
+ }
401
+
402
+ def _convert_callout(
403
+ self,
404
+ block: MarkdownBlock,
405
+ ) -> dict[str, Any]:
406
+ """Convert callout block."""
407
+ callout_type = block.metadata.get("type", "note")
408
+
409
+ # Emoji mapping
410
+ emoji_map = {
411
+ "note": "information_source",
412
+ "warning": "warning",
413
+ "tip": "bulb",
414
+ "important": "star",
415
+ "caution": "construction",
416
+ }
417
+ emoji = emoji_map.get(callout_type, "memo")
418
+
419
+ return {
420
+ "object": "block",
421
+ "type": "callout",
422
+ "callout": {
423
+ "rich_text": self._convert_rich_text(block.content),
424
+ "icon": {"type": "emoji", "emoji": self._get_emoji(emoji)},
425
+ "color": "default",
426
+ },
427
+ }
428
+
429
+ def _get_emoji(self, name: str) -> str:
430
+ """Get emoji character from name."""
431
+ emoji_map = {
432
+ "information_source": "ℹ️",
433
+ "warning": "⚠️",
434
+ "bulb": "💡",
435
+ "star": "⭐",
436
+ "construction": "🚧",
437
+ "memo": "📝",
438
+ }
439
+ return emoji_map.get(name, "ℹ️")
440
+
441
+
442
+ def main():
443
+ """Test the converter."""
444
+ from markdown_parser import MarkdownParser
445
+
446
+ test_md = """# Test Document
447
+
448
+ This is a **bold** paragraph with *italic* text.
449
+
450
+ ## Features
451
+
452
+ - Item 1
453
+ - Item 2
454
+
455
+ ```python
456
+ print("Hello")
457
+ ```
458
+
459
+ > Quote text
460
+
461
+ | A | B |
462
+ |---|---|
463
+ | 1 | 2 |
464
+ """
465
+
466
+ parser = MarkdownParser()
467
+ blocks = parser.parse(test_md)
468
+
469
+ converter = NotionBlockConverter()
470
+ notion_blocks = converter.convert_blocks(blocks)
471
+
472
+ import json
473
+ print(json.dumps(notion_blocks, indent=2))
474
+
475
+
476
+ if __name__ == "__main__":
477
+ main()