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.
- clouvel/analytics.py +151 -0
- clouvel/server.py +575 -1136
- clouvel/tools/__init__.py +75 -0
- clouvel/tools/agents.py +245 -0
- clouvel/tools/core.py +315 -0
- clouvel/tools/docs.py +247 -0
- clouvel/tools/hooks.py +170 -0
- clouvel/tools/planning.py +320 -0
- clouvel/tools/rules.py +223 -0
- clouvel/tools/setup.py +166 -0
- clouvel/tools/verify.py +212 -0
- clouvel-0.6.3.dist-info/METADATA +269 -0
- clouvel-0.6.3.dist-info/RECORD +16 -0
- clouvel-0.3.1.dist-info/METADATA +0 -80
- clouvel-0.3.1.dist-info/RECORD +0 -6
- {clouvel-0.3.1.dist-info → clouvel-0.6.3.dist-info}/WHEEL +0 -0
- {clouvel-0.3.1.dist-info → clouvel-0.6.3.dist-info}/entry_points.txt +0 -0
|
@@ -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
|
+
""")]
|