clouvel 0.3.1__py3-none-any.whl → 0.6.3__py3-none-any.whl

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.
@@ -0,0 +1,320 @@
1
+ # -*- coding: utf-8 -*-
2
+ """Planning tools (v0.6): init_planning, save_finding, refresh_goals, update_progress"""
3
+
4
+ from pathlib import Path
5
+ from datetime import datetime
6
+ from mcp.types import TextContent
7
+
8
+
9
+ async def init_planning(path: str, task: str, goals: list) -> list[TextContent]:
10
+ """영속적 컨텍스트 초기화"""
11
+ project_path = Path(path)
12
+
13
+ if not project_path.exists():
14
+ return [TextContent(type="text", text=f"❌ 경로가 존재하지 않습니다: {path}")]
15
+
16
+ planning_dir = project_path / ".claude" / "planning"
17
+ planning_dir.mkdir(parents=True, exist_ok=True)
18
+
19
+ # task_plan.md 생성
20
+ goals_md = "\n".join(f"- [ ] {g}" for g in goals) if goals else "- [ ] (목표 정의 필요)"
21
+
22
+ task_plan_content = f"""# Task Plan
23
+
24
+ > 생성일: {datetime.now().strftime('%Y-%m-%d %H:%M')}
25
+
26
+ ---
27
+
28
+ ## 현재 작업
29
+
30
+ {task}
31
+
32
+ ---
33
+
34
+ ## 목표
35
+
36
+ {goals_md}
37
+
38
+ ---
39
+
40
+ ## 접근 방식
41
+
42
+ (작업 시작 전 계획 작성)
43
+
44
+ ---
45
+
46
+ ## 제약 조건
47
+
48
+ - PRD에 명시된 범위 내에서만 작업
49
+ - 테스트 없이 배포 금지
50
+
51
+ ---
52
+
53
+ > 💡 `refresh_goals` 도구로 현재 목표를 리마인드할 수 있습니다.
54
+ """
55
+
56
+ # findings.md 생성
57
+ findings_content = f"""# Findings
58
+
59
+ > 조사 결과 기록
60
+ > 생성일: {datetime.now().strftime('%Y-%m-%d %H:%M')}
61
+
62
+ ---
63
+
64
+ ## 2-Action Rule
65
+
66
+ > view/browser 작업 2개 후 반드시 여기에 기록!
67
+
68
+ ---
69
+
70
+ (아직 기록 없음)
71
+ """
72
+
73
+ # progress.md 생성
74
+ progress_content = f"""# Progress
75
+
76
+ > 마지막 업데이트: {datetime.now().strftime('%Y-%m-%d %H:%M')}
77
+
78
+ ---
79
+
80
+ ## 완료 (Completed)
81
+
82
+ *(아직 없음)*
83
+
84
+ ---
85
+
86
+ ## 진행중 (In Progress)
87
+
88
+ *(없음)*
89
+
90
+ ---
91
+
92
+ ## 블로커 (Blockers)
93
+
94
+ *(없음)*
95
+
96
+ ---
97
+
98
+ ## 다음 할 일 (Next)
99
+
100
+ *(결정 필요)*
101
+
102
+ ---
103
+
104
+ > 💡 업데이트: `update_progress` 도구 호출
105
+ """
106
+
107
+ # 파일 생성
108
+ (planning_dir / "task_plan.md").write_text(task_plan_content, encoding='utf-8')
109
+ (planning_dir / "findings.md").write_text(findings_content, encoding='utf-8')
110
+ (planning_dir / "progress.md").write_text(progress_content, encoding='utf-8')
111
+
112
+ return [TextContent(type="text", text=f"""# 영속적 컨텍스트 초기화 완료
113
+
114
+ ## 생성된 파일
115
+
116
+ | 파일 | 용도 |
117
+ |------|------|
118
+ | `task_plan.md` | 작업 계획 + 목표 |
119
+ | `findings.md` | 조사 결과 기록 |
120
+ | `progress.md` | 진행 상황 추적 |
121
+
122
+ ## 경로
123
+ `{planning_dir}`
124
+
125
+ ## 다음 단계
126
+
127
+ 1. 목표 확인: `refresh_goals`
128
+ 2. 조사 기록: `save_finding`
129
+ 3. 진행 업데이트: `update_progress`
130
+
131
+ **긴 세션에서도 목표를 잃지 마세요!**
132
+ """)]
133
+
134
+
135
+ async def save_finding(path: str, topic: str, question: str, findings: str, source: str, conclusion: str) -> list[TextContent]:
136
+ """조사 결과 저장"""
137
+ project_path = Path(path)
138
+ findings_file = project_path / ".claude" / "planning" / "findings.md"
139
+
140
+ if not findings_file.exists():
141
+ return [TextContent(type="text", text="❌ findings.md가 없습니다. 먼저 `init_planning` 도구로 초기화하세요.")]
142
+
143
+ timestamp = datetime.now().strftime('%Y-%m-%d %H:%M')
144
+ finding_entry = f"""
145
+ ---
146
+
147
+ ## [{timestamp}] {topic}
148
+
149
+ ### 질문
150
+ {question if question else '(명시되지 않음)'}
151
+
152
+ ### 발견
153
+ {findings}
154
+
155
+ ### 소스
156
+ {source if source else '(없음)'}
157
+
158
+ ### 결론
159
+ {conclusion if conclusion else '(추가 조사 필요)'}
160
+
161
+ """
162
+
163
+ existing = findings_file.read_text(encoding='utf-8')
164
+ findings_file.write_text(existing + finding_entry, encoding='utf-8')
165
+
166
+ return [TextContent(type="text", text=f"""# Finding 저장 완료
167
+
168
+ ## 요약
169
+
170
+ | 항목 | 내용 |
171
+ |------|------|
172
+ | 주제 | {topic} |
173
+ | 질문 | {question or '없음'} |
174
+ | 소스 | {source or '없음'} |
175
+
176
+ ## 저장 위치
177
+ `{findings_file}`
178
+
179
+ ---
180
+
181
+ **2-Action Rule 준수!**
182
+ """)]
183
+
184
+
185
+ async def refresh_goals(path: str) -> list[TextContent]:
186
+ """목표 리마인드"""
187
+ project_path = Path(path)
188
+ task_plan_file = project_path / ".claude" / "planning" / "task_plan.md"
189
+ progress_file = project_path / ".claude" / "planning" / "progress.md"
190
+
191
+ if not task_plan_file.exists():
192
+ return [TextContent(type="text", text="❌ task_plan.md가 없습니다. 먼저 `init_planning` 도구로 초기화하세요.")]
193
+
194
+ task_plan = task_plan_file.read_text(encoding='utf-8')
195
+ progress = progress_file.read_text(encoding='utf-8') if progress_file.exists() else "(없음)"
196
+
197
+ # 목표 추출
198
+ goals = []
199
+ in_goals_section = False
200
+ for line in task_plan.split("\n"):
201
+ if "## 목표" in line:
202
+ in_goals_section = True
203
+ elif line.startswith("## "):
204
+ in_goals_section = False
205
+ elif in_goals_section and line.strip().startswith("- "):
206
+ goals.append(line.strip())
207
+
208
+ goals_md = "\n".join(goals) if goals else "*(목표 없음)*"
209
+
210
+ return [TextContent(type="text", text=f"""# 목표 리마인드
211
+
212
+ ## 현재 작업
213
+
214
+ (task_plan.md 참조)
215
+
216
+ ## 목표
217
+
218
+ {goals_md}
219
+
220
+ ---
221
+
222
+ ## 현재 진행 상황
223
+
224
+ {progress[:500]}{'...' if len(progress) > 500 else ''}
225
+
226
+ ---
227
+
228
+ ## 다음 액션
229
+
230
+ 1. 위 목표 중 하나를 선택
231
+ 2. 해당 목표에 집중
232
+ 3. 완료되면 `update_progress`로 기록
233
+
234
+ **"지금 뭐하고 있었지?" → 위 목표를 확인하세요!**
235
+ """)]
236
+
237
+
238
+ async def update_progress(path: str, completed: list, in_progress: str, blockers: list, next_item: str) -> list[TextContent]:
239
+ """진행 상황 업데이트"""
240
+ project_path = Path(path)
241
+ progress_file = project_path / ".claude" / "planning" / "progress.md"
242
+
243
+ if not progress_file.exists():
244
+ return [TextContent(type="text", text="❌ progress.md가 없습니다. 먼저 `init_planning` 도구로 초기화하세요.")]
245
+
246
+ existing = progress_file.read_text(encoding='utf-8')
247
+
248
+ # 기존 완료 항목 파싱
249
+ existing_completed = []
250
+ in_completed_section = False
251
+
252
+ for line in existing.split("\n"):
253
+ if "## 완료" in line:
254
+ in_completed_section = True
255
+ elif line.startswith("## "):
256
+ in_completed_section = False
257
+ elif in_completed_section and line.strip().startswith("- "):
258
+ item = line.strip()[2:]
259
+ if item and item != "*(아직 없음)*":
260
+ existing_completed.append(item)
261
+
262
+ # 새 완료 항목 추가
263
+ all_completed = existing_completed + list(completed)
264
+ completed_md = "\n".join(f"- {c}" for c in all_completed) if all_completed else "*(아직 없음)*"
265
+ blockers_md = "\n".join(f"- {b}" for b in blockers) if blockers else "*(없음)*"
266
+
267
+ # 새 progress.md 생성
268
+ new_progress = f"""# Progress
269
+
270
+ > 마지막 업데이트: {datetime.now().strftime('%Y-%m-%d %H:%M')}
271
+
272
+ ---
273
+
274
+ ## 완료 (Completed)
275
+
276
+ {completed_md}
277
+
278
+ ---
279
+
280
+ ## 진행중 (In Progress)
281
+
282
+ {f"- {in_progress}" if in_progress else "*(없음)*"}
283
+
284
+ ---
285
+
286
+ ## 블로커 (Blockers)
287
+
288
+ {blockers_md}
289
+
290
+ ---
291
+
292
+ ## 다음 할 일 (Next)
293
+
294
+ {next_item if next_item else "*(결정 필요)*"}
295
+
296
+ ---
297
+
298
+ > 💡 업데이트: `update_progress` 도구 호출
299
+ """
300
+
301
+ progress_file.write_text(new_progress, encoding='utf-8')
302
+
303
+ return [TextContent(type="text", text=f"""# Progress 업데이트 완료
304
+
305
+ ## 요약
306
+
307
+ | 항목 | 개수/내용 |
308
+ |------|----------|
309
+ | 완료 | {len(all_completed)}개 |
310
+ | 진행중 | {in_progress if in_progress else '없음'} |
311
+ | 블로커 | {len(blockers)}개 |
312
+ | 다음 | {next_item if next_item else '미정'} |
313
+
314
+ ## 저장 위치
315
+ `{progress_file}`
316
+
317
+ ---
318
+
319
+ **진행 상황이 기록되었습니다!**
320
+ """)]
clouvel/tools/rules.py ADDED
@@ -0,0 +1,223 @@
1
+ # -*- coding: utf-8 -*-
2
+ """Rules tools (v0.5): init_rules, get_rule, add_rule"""
3
+
4
+ import json
5
+ from pathlib import Path
6
+ from datetime import datetime
7
+ from mcp.types import TextContent
8
+
9
+
10
+ async def init_rules(path: str, template: str) -> list[TextContent]:
11
+ """규칙 모듈화 초기화"""
12
+ project_path = Path(path)
13
+
14
+ if not project_path.exists():
15
+ return [TextContent(type="text", text=f"❌ 경로가 존재하지 않습니다: {path}")]
16
+
17
+ rules_dir = project_path / ".claude" / "rules"
18
+ rules_dir.mkdir(parents=True, exist_ok=True)
19
+
20
+ # 템플릿별 규칙 파일
21
+ templates = {
22
+ "minimal": ["global.md", "security.md"],
23
+ "web": ["global.md", "security.md", "frontend.md"],
24
+ "api": ["global.md", "security.md", "api.md", "database.md"],
25
+ "fullstack": ["global.md", "security.md", "frontend.md", "api.md", "database.md"],
26
+ }
27
+
28
+ files_to_create = templates.get(template, templates["minimal"])
29
+ created = []
30
+
31
+ # 규칙 파일 내용
32
+ rule_contents = {
33
+ "global.md": """# Global Rules
34
+
35
+ ## ALWAYS
36
+ - Read before Edit
37
+ - Check PRD before implementing
38
+ - Update progress after completing
39
+
40
+ ## NEVER
41
+ - Skip documentation
42
+ - Implement features not in PRD
43
+ - Commit without tests
44
+ """,
45
+ "security.md": """# Security Rules
46
+
47
+ ## ALWAYS
48
+ - Validate user input
49
+ - Use parameterized queries
50
+ - Escape output for XSS prevention
51
+
52
+ ## NEVER
53
+ - Store passwords in plain text
54
+ - Expose sensitive data in logs
55
+ - Trust client-side validation alone
56
+ """,
57
+ "frontend.md": """# Frontend Rules
58
+
59
+ ## ALWAYS
60
+ - Use semantic HTML
61
+ - Handle loading/error states
62
+ - Support keyboard navigation
63
+
64
+ ## NEVER
65
+ - Use inline styles for complex CSS
66
+ - Mutate props directly
67
+ - Skip accessibility attributes
68
+ """,
69
+ "api.md": """# API Rules
70
+
71
+ ## ALWAYS
72
+ - Return consistent response format
73
+ - Include proper HTTP status codes
74
+ - Document all endpoints
75
+
76
+ ## NEVER
77
+ - Expose internal errors to clients
78
+ - Use GET for state-changing operations
79
+ - Skip rate limiting
80
+ """,
81
+ "database.md": """# Database Rules
82
+
83
+ ## ALWAYS
84
+ - Use migrations for schema changes
85
+ - Index foreign keys
86
+ - Use transactions for multi-step operations
87
+
88
+ ## NEVER
89
+ - Store JSON for relational data
90
+ - Skip backup before migration
91
+ - Use SELECT * in production
92
+ """,
93
+ }
94
+
95
+ for filename in files_to_create:
96
+ file_path = rules_dir / filename
97
+ if not file_path.exists():
98
+ file_path.write_text(rule_contents.get(filename, f"# {filename}\n\n[규칙 작성]"), encoding='utf-8')
99
+ created.append(filename)
100
+
101
+ # rules.index.json 생성
102
+ index_content = {
103
+ "version": "0.5.0",
104
+ "template": template,
105
+ "rules": [
106
+ {"file": f, "scope": "**/*", "priority": 100 - i * 10}
107
+ for i, f in enumerate(files_to_create)
108
+ ]
109
+ }
110
+ index_file = rules_dir / "rules.index.json"
111
+ index_file.write_text(json.dumps(index_content, indent=2, ensure_ascii=False), encoding='utf-8')
112
+
113
+ created_list = "\n".join(f"- {f}" for f in created) if created else "없음 (이미 존재)"
114
+
115
+ return [TextContent(type="text", text=f"""# 규칙 모듈화 완료
116
+
117
+ ## 템플릿
118
+ **{template}**
119
+
120
+ ## 생성된 파일
121
+ {created_list}
122
+
123
+ ## 경로
124
+ `{rules_dir}`
125
+
126
+ ## 사용법
127
+ - `get_rule`로 파일별 규칙 로딩
128
+ - `add_rule`로 새 규칙 추가
129
+
130
+ **컨텍스트 절약 50%+ 효과!**
131
+ """)]
132
+
133
+
134
+ async def get_rule(path: str, context: str) -> list[TextContent]:
135
+ """경로 기반 규칙 로딩"""
136
+ file_path = Path(path)
137
+
138
+ # 프로젝트 루트 찾기
139
+ current = file_path if file_path.is_dir() else file_path.parent
140
+ rules_dir = None
141
+
142
+ for _ in range(10): # 최대 10레벨 상위까지
143
+ potential = current / ".claude" / "rules"
144
+ if potential.exists():
145
+ rules_dir = potential
146
+ break
147
+ if current.parent == current:
148
+ break
149
+ current = current.parent
150
+
151
+ if not rules_dir:
152
+ return [TextContent(type="text", text="❌ .claude/rules/ 폴더를 찾을 수 없습니다. `init_rules`로 먼저 생성하세요.")]
153
+
154
+ # 규칙 파일 로딩
155
+ rules = []
156
+ for rule_file in rules_dir.glob("*.md"):
157
+ rules.append(f"## {rule_file.stem}\n\n{rule_file.read_text(encoding='utf-8')}")
158
+
159
+ if not rules:
160
+ return [TextContent(type="text", text="❌ 규칙 파일이 없습니다.")]
161
+
162
+ context_note = f"\n\n> 컨텍스트: {context}" if context != "coding" else ""
163
+
164
+ return [TextContent(type="text", text=f"""# 적용 규칙
165
+
166
+ 경로: `{path}`{context_note}
167
+
168
+ ---
169
+
170
+ {chr(10).join(rules)}
171
+ """)]
172
+
173
+
174
+ async def add_rule(path: str, rule_type: str, content: str, category: str) -> list[TextContent]:
175
+ """새 규칙 추가"""
176
+ project_path = Path(path)
177
+ rules_dir = project_path / ".claude" / "rules"
178
+
179
+ if not rules_dir.exists():
180
+ return [TextContent(type="text", text="❌ .claude/rules/ 폴더가 없습니다. `init_rules`로 먼저 생성하세요.")]
181
+
182
+ # 카테고리 파일 선택
183
+ category_files = {
184
+ "api": "api.md",
185
+ "frontend": "frontend.md",
186
+ "database": "database.md",
187
+ "security": "security.md",
188
+ "general": "global.md",
189
+ }
190
+ target_file = rules_dir / category_files.get(category, "global.md")
191
+
192
+ # 파일이 없으면 생성
193
+ if not target_file.exists():
194
+ target_file.write_text(f"# {category.title()} Rules\n\n", encoding='utf-8')
195
+
196
+ # 규칙 추가
197
+ existing = target_file.read_text(encoding='utf-8')
198
+ rule_section = f"\n## {rule_type.upper()}\n- {content}\n"
199
+
200
+ # 같은 타입 섹션이 있으면 거기에 추가
201
+ if f"## {rule_type.upper()}" in existing:
202
+ existing = existing.replace(f"## {rule_type.upper()}\n", f"## {rule_type.upper()}\n- {content}\n")
203
+ target_file.write_text(existing, encoding='utf-8')
204
+ else:
205
+ target_file.write_text(existing + rule_section, encoding='utf-8')
206
+
207
+ return [TextContent(type="text", text=f"""# 규칙 추가 완료
208
+
209
+ ## 상세
210
+
211
+ | 항목 | 값 |
212
+ |------|-----|
213
+ | 타입 | {rule_type.upper()} |
214
+ | 카테고리 | {category} |
215
+ | 파일 | {target_file.name} |
216
+
217
+ ## 내용
218
+ {content}
219
+
220
+ ---
221
+
222
+ 규칙이 `{target_file}`에 추가되었습니다.
223
+ """)]
clouvel/tools/setup.py ADDED
@@ -0,0 +1,166 @@
1
+ # -*- coding: utf-8 -*-
2
+ """Setup tools: init_clouvel, setup_cli"""
3
+
4
+ import json
5
+ from pathlib import Path
6
+ from mcp.types import TextContent
7
+
8
+
9
+ async def init_clouvel(platform: str) -> list[TextContent]:
10
+ """Clouvel 온보딩"""
11
+ if platform == "ask":
12
+ return [TextContent(type="text", text="""# Clouvel 온보딩
13
+
14
+ 어떤 환경에서 사용하시나요?
15
+
16
+ 1. **Claude Desktop** - GUI 앱
17
+ 2. **VS Code / Cursor** - IDE 확장
18
+ 3. **Claude Code (CLI)** - 터미널
19
+
20
+ 선택해 주시면 맞춤 설정을 안내해 드립니다.
21
+
22
+ 예: "Claude Code에서 사용할 거야" 또는 "1번"
23
+ """)]
24
+
25
+ guides = {
26
+ "desktop": """# Claude Desktop 설정 완료!
27
+
28
+ MCP 서버가 이미 연결되어 있습니다.
29
+
30
+ ## 사용법
31
+ 대화에서 "코딩해도 돼?" 또는 "can_code로 확인해줘" 라고 말하세요.
32
+
33
+ ## 주요 도구
34
+ - `can_code` - 코딩 가능 여부 확인
35
+ - `init_docs` - 문서 템플릿 생성
36
+ - `get_prd_guide` - PRD 작성 가이드
37
+ """,
38
+ "vscode": """# VS Code / Cursor 설정 안내
39
+
40
+ ## 설치 방법
41
+ 1. 확장 탭에서 "Clouvel" 검색
42
+ 2. 설치
43
+ 3. Command Palette (Ctrl+Shift+P)
44
+ 4. "Clouvel: Setup MCP Server" 실행
45
+
46
+ ## CLI도 설정하시겠어요?
47
+ "CLI도 설정해줘" 라고 말씀하시면 추가 설정을 진행합니다.
48
+ """,
49
+ "cli": """# Claude Code (CLI) 설정 안내
50
+
51
+ ## 자동 설정
52
+ ```bash
53
+ clouvel init
54
+ ```
55
+
56
+ ## 수동 설정
57
+ ```bash
58
+ clouvel init -p /path/to/project -l strict
59
+ ```
60
+
61
+ ## 강제 수준
62
+ - `remind` - 경고만
63
+ - `strict` - 커밋 차단 (추천)
64
+ - `full` - Hooks + 커밋 차단
65
+
66
+ 어떤 수준으로 설정할까요?
67
+ """,
68
+ }
69
+
70
+ return [TextContent(type="text", text=guides.get(platform, guides["cli"]))]
71
+
72
+
73
+ async def setup_cli(path: str, level: str) -> list[TextContent]:
74
+ """CLI 환경 설정"""
75
+ project_path = Path(path).resolve()
76
+
77
+ if not project_path.exists():
78
+ return [TextContent(type="text", text=f"❌ 경로가 존재하지 않습니다: {path}")]
79
+
80
+ created_files = []
81
+
82
+ # 1. .claude 폴더 생성
83
+ claude_dir = project_path / ".claude"
84
+ claude_dir.mkdir(exist_ok=True)
85
+
86
+ # 2. hooks.json (remind, full)
87
+ if level in ["remind", "full"]:
88
+ hooks_content = {
89
+ "hooks": {
90
+ "preToolUse": [
91
+ {
92
+ "matcher": "Edit|Write|NotebookEdit",
93
+ "hooks": [
94
+ {
95
+ "type": "command",
96
+ "command": "echo '[Clouvel] 코드 작성 전 can_code 도구로 문서 상태를 확인하세요!'"
97
+ }
98
+ ]
99
+ }
100
+ ]
101
+ }
102
+ }
103
+ hooks_file = claude_dir / "hooks.json"
104
+ hooks_file.write_text(json.dumps(hooks_content, indent=2, ensure_ascii=False), encoding='utf-8')
105
+ created_files.append(".claude/hooks.json")
106
+
107
+ # 3. CLAUDE.md 규칙
108
+ claude_md = project_path / "CLAUDE.md"
109
+ clouvel_rule = """
110
+ ## Clouvel 규칙 (자동 생성)
111
+
112
+ > 이 규칙은 Clouvel이 자동으로 추가했습니다.
113
+
114
+ ### 필수 준수 사항
115
+ 1. **코드 작성 전 문서 체크**: Edit/Write 도구 사용 전 반드시 `can_code` 도구를 먼저 호출
116
+ 2. **can_code 실패 시 코딩 금지**: 필수 문서가 없으면 PRD 작성부터
117
+ 3. **PRD가 법**: docs/PRD.md에 없는 기능은 구현하지 않음
118
+ """
119
+
120
+ if claude_md.exists():
121
+ existing = claude_md.read_text(encoding='utf-8')
122
+ if "Clouvel 규칙" not in existing:
123
+ claude_md.write_text(existing + "\n" + clouvel_rule, encoding='utf-8')
124
+ created_files.append("CLAUDE.md (규칙 추가)")
125
+ else:
126
+ claude_md.write_text(f"# {project_path.name}\n" + clouvel_rule, encoding='utf-8')
127
+ created_files.append("CLAUDE.md (생성)")
128
+
129
+ # 4. pre-commit hook (strict, full)
130
+ if level in ["strict", "full"]:
131
+ git_hooks_dir = project_path / ".git" / "hooks"
132
+ if git_hooks_dir.exists():
133
+ pre_commit = git_hooks_dir / "pre-commit"
134
+ pre_commit_content = '''#!/bin/sh
135
+ # Clouvel pre-commit hook
136
+ DOCS_DIR="./docs"
137
+ if ! ls "$DOCS_DIR"/*[Pp][Rr][Dd]* 1> /dev/null 2>&1; then
138
+ echo "[Clouvel] BLOCKED: No PRD document found."
139
+ echo "Please create docs/PRD.md first."
140
+ exit 1
141
+ fi
142
+ echo "[Clouvel] Document check passed."
143
+ '''
144
+ pre_commit.write_text(pre_commit_content, encoding='utf-8')
145
+ created_files.append(".git/hooks/pre-commit")
146
+
147
+ files_list = "\n".join(f"- {f}" for f in created_files) if created_files else "없음"
148
+
149
+ return [TextContent(type="text", text=f"""# CLI 설정 완료
150
+
151
+ ## 프로젝트
152
+ `{project_path}`
153
+
154
+ ## 강제 수준
155
+ **{level}**
156
+
157
+ ## 생성/수정된 파일
158
+ {files_list}
159
+
160
+ ## 다음 단계
161
+ 1. `docs/PRD.md` 생성
162
+ 2. Claude에게 "코딩해도 돼?" 질문
163
+ 3. PRD 없으면 코딩 차단됨
164
+
165
+ **PRD 없으면 코딩 없다!**
166
+ """)]