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/server.py CHANGED
@@ -1,1207 +1,646 @@
1
- import re
2
- from pathlib import Path
3
- from datetime import datetime
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Clouvel MCP Server v0.8.0
4
+ 바이브코딩 프로세스를 강제하는 MCP 서버
5
+
6
+ Free 버전 - Pro 기능은 clouvel-pro 패키지 참조
7
+ """
8
+
4
9
  from mcp.server import Server
5
10
  from mcp.server.stdio import stdio_server
6
11
  from mcp.types import Tool, TextContent
7
12
 
13
+ from .analytics import log_tool_call, get_stats, format_stats
14
+ from .tools import (
15
+ # core
16
+ can_code, scan_docs, analyze_docs, init_docs, REQUIRED_DOCS,
17
+ # docs
18
+ get_prd_template, write_prd_section, get_prd_guide, get_verify_checklist, get_setup_guide,
19
+ # setup
20
+ init_clouvel, setup_cli,
21
+ # rules (v0.5)
22
+ init_rules, get_rule, add_rule,
23
+ # verify (v0.5)
24
+ verify, gate, handoff,
25
+ # planning (v0.6)
26
+ init_planning, save_finding, refresh_goals, update_progress,
27
+ # agents (v0.7)
28
+ spawn_explore, spawn_librarian,
29
+ # hooks (v0.8)
30
+ hook_design, hook_verify,
31
+ )
32
+
8
33
  server = Server("clouvel")
9
34
 
10
- # 필수 문서 정의
11
- REQUIRED_DOCS = [
12
- {"type": "prd", "name": "PRD", "patterns": [r"prd", r"product.?requirement"], "priority": "critical"},
13
- {"type": "architecture", "name": "아키텍처", "patterns": [r"architect", r"module"], "priority": "critical"},
14
- {"type": "api_spec", "name": "API 스펙", "patterns": [r"api", r"swagger", r"openapi"], "priority": "critical"},
15
- {"type": "db_schema", "name": "DB 스키마", "patterns": [r"schema", r"database", r"db"], "priority": "critical"},
16
- {"type": "verification", "name": "검증 계획", "patterns": [r"verif", r"test.?plan"], "priority": "critical"},
35
+
36
+ # ============================================================
37
+ # Tool Definitions (Free - v0.8까지)
38
+ # ============================================================
39
+
40
+ TOOL_DEFINITIONS = [
41
+ # === Core Tools ===
42
+ Tool(
43
+ name="can_code",
44
+ description="코드 작성 전 반드시 호출. 문서 상태 확인 후 코딩 가능 여부 판단.",
45
+ inputSchema={
46
+ "type": "object",
47
+ "properties": {"path": {"type": "string", "description": "프로젝트 docs 폴더 경로"}},
48
+ "required": ["path"]
49
+ }
50
+ ),
51
+ Tool(
52
+ name="scan_docs",
53
+ description="프로젝트 docs 폴더 스캔. 파일 목록 반환.",
54
+ inputSchema={
55
+ "type": "object",
56
+ "properties": {"path": {"type": "string", "description": "docs 폴더 경로"}},
57
+ "required": ["path"]
58
+ }
59
+ ),
60
+ Tool(
61
+ name="analyze_docs",
62
+ description="docs 폴더 분석. 필수 문서 체크.",
63
+ inputSchema={
64
+ "type": "object",
65
+ "properties": {"path": {"type": "string", "description": "docs 폴더 경로"}},
66
+ "required": ["path"]
67
+ }
68
+ ),
69
+ Tool(
70
+ name="init_docs",
71
+ description="docs 폴더 초기화 + 템플릿 생성.",
72
+ inputSchema={
73
+ "type": "object",
74
+ "properties": {
75
+ "path": {"type": "string", "description": "프로젝트 루트 경로"},
76
+ "project_name": {"type": "string", "description": "프로젝트 이름"}
77
+ },
78
+ "required": ["path", "project_name"]
79
+ }
80
+ ),
81
+
82
+ # === Docs Tools ===
83
+ Tool(
84
+ name="get_prd_template",
85
+ description="PRD 템플릿 생성.",
86
+ inputSchema={
87
+ "type": "object",
88
+ "properties": {
89
+ "project_name": {"type": "string", "description": "프로젝트 이름"},
90
+ "output_path": {"type": "string", "description": "출력 경로"}
91
+ },
92
+ "required": ["project_name", "output_path"]
93
+ }
94
+ ),
95
+ Tool(
96
+ name="write_prd_section",
97
+ description="PRD 섹션별 작성 가이드.",
98
+ inputSchema={
99
+ "type": "object",
100
+ "properties": {
101
+ "section": {"type": "string", "enum": ["summary", "principles", "input_spec", "output_spec", "errors", "state_machine", "api_endpoints", "db_schema"]},
102
+ "content": {"type": "string", "description": "섹션 내용"}
103
+ },
104
+ "required": ["section"]
105
+ }
106
+ ),
107
+ Tool(name="get_prd_guide", description="PRD 작성 가이드.", inputSchema={"type": "object", "properties": {}}),
108
+ Tool(name="get_verify_checklist", description="검증 체크리스트.", inputSchema={"type": "object", "properties": {}}),
109
+ Tool(
110
+ name="get_setup_guide",
111
+ description="설치/설정 가이드.",
112
+ inputSchema={
113
+ "type": "object",
114
+ "properties": {"platform": {"type": "string", "enum": ["desktop", "code", "vscode", "cursor", "all"]}}
115
+ }
116
+ ),
117
+ Tool(
118
+ name="get_analytics",
119
+ description="도구 사용량 통계.",
120
+ inputSchema={
121
+ "type": "object",
122
+ "properties": {
123
+ "path": {"type": "string", "description": "프로젝트 경로"},
124
+ "days": {"type": "integer", "description": "조회 기간 (기본: 30일)"}
125
+ }
126
+ }
127
+ ),
128
+
129
+ # === Setup Tools ===
130
+ Tool(
131
+ name="init_clouvel",
132
+ description="Clouvel 온보딩. 플랫폼 선택 후 맞춤 설정.",
133
+ inputSchema={
134
+ "type": "object",
135
+ "properties": {"platform": {"type": "string", "enum": ["desktop", "vscode", "cli", "ask"]}}
136
+ }
137
+ ),
138
+ Tool(
139
+ name="setup_cli",
140
+ description="CLI 환경 설정. hooks, CLAUDE.md, pre-commit 생성.",
141
+ inputSchema={
142
+ "type": "object",
143
+ "properties": {
144
+ "path": {"type": "string", "description": "프로젝트 루트 경로"},
145
+ "level": {"type": "string", "enum": ["remind", "strict", "full"]}
146
+ },
147
+ "required": ["path"]
148
+ }
149
+ ),
150
+
151
+ # === Rules Tools (v0.5) ===
152
+ Tool(
153
+ name="init_rules",
154
+ description="v0.5: 규칙 모듈화 초기화.",
155
+ inputSchema={
156
+ "type": "object",
157
+ "properties": {
158
+ "path": {"type": "string", "description": "프로젝트 루트 경로"},
159
+ "template": {"type": "string", "enum": ["web", "api", "fullstack", "minimal"]}
160
+ },
161
+ "required": ["path"]
162
+ }
163
+ ),
164
+ Tool(
165
+ name="get_rule",
166
+ description="v0.5: 경로 기반 규칙 로딩.",
167
+ inputSchema={
168
+ "type": "object",
169
+ "properties": {
170
+ "path": {"type": "string", "description": "파일 경로"},
171
+ "context": {"type": "string", "enum": ["coding", "review", "debug", "test"]}
172
+ },
173
+ "required": ["path"]
174
+ }
175
+ ),
176
+ Tool(
177
+ name="add_rule",
178
+ description="v0.5: 새 규칙 추가.",
179
+ inputSchema={
180
+ "type": "object",
181
+ "properties": {
182
+ "path": {"type": "string", "description": "프로젝트 루트 경로"},
183
+ "rule_type": {"type": "string", "enum": ["never", "always", "prefer"]},
184
+ "content": {"type": "string", "description": "규칙 내용"},
185
+ "category": {"type": "string", "enum": ["api", "frontend", "database", "security", "general"]}
186
+ },
187
+ "required": ["path", "rule_type", "content"]
188
+ }
189
+ ),
190
+
191
+ # === Verify Tools (v0.5) ===
192
+ Tool(
193
+ name="verify",
194
+ description="v0.5: Context Bias 제거 검증.",
195
+ inputSchema={
196
+ "type": "object",
197
+ "properties": {
198
+ "path": {"type": "string", "description": "검증 대상 경로"},
199
+ "scope": {"type": "string", "enum": ["file", "feature", "full"]},
200
+ "checklist": {"type": "array", "items": {"type": "string"}}
201
+ },
202
+ "required": ["path"]
203
+ }
204
+ ),
205
+ Tool(
206
+ name="gate",
207
+ description="v0.5: lint → test → build 자동화.",
208
+ inputSchema={
209
+ "type": "object",
210
+ "properties": {
211
+ "path": {"type": "string", "description": "프로젝트 루트 경로"},
212
+ "steps": {"type": "array", "items": {"type": "string"}},
213
+ "fix": {"type": "boolean"}
214
+ },
215
+ "required": ["path"]
216
+ }
217
+ ),
218
+ Tool(
219
+ name="handoff",
220
+ description="v0.5: 의도 기록.",
221
+ inputSchema={
222
+ "type": "object",
223
+ "properties": {
224
+ "path": {"type": "string", "description": "프로젝트 루트 경로"},
225
+ "feature": {"type": "string", "description": "완료한 기능"},
226
+ "decisions": {"type": "string"},
227
+ "warnings": {"type": "string"},
228
+ "next_steps": {"type": "string"}
229
+ },
230
+ "required": ["path", "feature"]
231
+ }
232
+ ),
233
+
234
+ # === Planning Tools (v0.6) ===
235
+ Tool(
236
+ name="init_planning",
237
+ description="v0.6: 영속적 컨텍스트 초기화.",
238
+ inputSchema={
239
+ "type": "object",
240
+ "properties": {
241
+ "path": {"type": "string", "description": "프로젝트 루트 경로"},
242
+ "task": {"type": "string", "description": "현재 작업"},
243
+ "goals": {"type": "array", "items": {"type": "string"}}
244
+ },
245
+ "required": ["path", "task"]
246
+ }
247
+ ),
248
+ Tool(
249
+ name="save_finding",
250
+ description="v0.6: 조사 결과 저장.",
251
+ inputSchema={
252
+ "type": "object",
253
+ "properties": {
254
+ "path": {"type": "string", "description": "프로젝트 루트 경로"},
255
+ "topic": {"type": "string"},
256
+ "question": {"type": "string"},
257
+ "findings": {"type": "string"},
258
+ "source": {"type": "string"},
259
+ "conclusion": {"type": "string"}
260
+ },
261
+ "required": ["path", "topic", "findings"]
262
+ }
263
+ ),
264
+ Tool(
265
+ name="refresh_goals",
266
+ description="v0.6: 목표 리마인드.",
267
+ inputSchema={
268
+ "type": "object",
269
+ "properties": {"path": {"type": "string", "description": "프로젝트 루트 경로"}},
270
+ "required": ["path"]
271
+ }
272
+ ),
273
+ Tool(
274
+ name="update_progress",
275
+ description="v0.6: 진행 상황 업데이트.",
276
+ inputSchema={
277
+ "type": "object",
278
+ "properties": {
279
+ "path": {"type": "string", "description": "프로젝트 루트 경로"},
280
+ "completed": {"type": "array", "items": {"type": "string"}},
281
+ "in_progress": {"type": "string"},
282
+ "blockers": {"type": "array", "items": {"type": "string"}},
283
+ "next": {"type": "string"}
284
+ },
285
+ "required": ["path"]
286
+ }
287
+ ),
288
+
289
+ # === Agent Tools (v0.7) ===
290
+ Tool(
291
+ name="spawn_explore",
292
+ description="v0.7: 탐색 전문 에이전트.",
293
+ inputSchema={
294
+ "type": "object",
295
+ "properties": {
296
+ "path": {"type": "string", "description": "프로젝트 루트 경로"},
297
+ "query": {"type": "string", "description": "탐색 질문"},
298
+ "scope": {"type": "string", "enum": ["file", "folder", "project", "deep"]},
299
+ "save_findings": {"type": "boolean"}
300
+ },
301
+ "required": ["path", "query"]
302
+ }
303
+ ),
304
+ Tool(
305
+ name="spawn_librarian",
306
+ description="v0.7: 라이브러리언 에이전트.",
307
+ inputSchema={
308
+ "type": "object",
309
+ "properties": {
310
+ "path": {"type": "string", "description": "프로젝트 루트 경로"},
311
+ "topic": {"type": "string", "description": "조사 주제"},
312
+ "type": {"type": "string", "enum": ["library", "api", "migration", "best_practice"]},
313
+ "depth": {"type": "string", "enum": ["quick", "standard", "thorough"]}
314
+ },
315
+ "required": ["path", "topic"]
316
+ }
317
+ ),
318
+
319
+ # === Hook Tools (v0.8) ===
320
+ Tool(
321
+ name="hook_design",
322
+ description="v0.8: 설계 훅 생성.",
323
+ inputSchema={
324
+ "type": "object",
325
+ "properties": {
326
+ "path": {"type": "string", "description": "프로젝트 루트 경로"},
327
+ "trigger": {"type": "string", "enum": ["pre_code", "pre_feature", "pre_refactor", "pre_api"]},
328
+ "checks": {"type": "array", "items": {"type": "string"}},
329
+ "block_on_fail": {"type": "boolean"}
330
+ },
331
+ "required": ["path", "trigger"]
332
+ }
333
+ ),
334
+ Tool(
335
+ name="hook_verify",
336
+ description="v0.8: 검증 훅 생성.",
337
+ inputSchema={
338
+ "type": "object",
339
+ "properties": {
340
+ "path": {"type": "string", "description": "프로젝트 루트 경로"},
341
+ "trigger": {"type": "string", "enum": ["post_code", "post_feature", "pre_commit", "pre_push"]},
342
+ "steps": {"type": "array", "items": {"type": "string"}},
343
+ "parallel": {"type": "boolean"},
344
+ "continue_on_error": {"type": "boolean"}
345
+ },
346
+ "required": ["path", "trigger"]
347
+ }
348
+ ),
349
+
350
+ # === Pro 안내 ===
351
+ Tool(
352
+ name="upgrade_pro",
353
+ description="Clouvel Pro 안내. Shovel 자동 설치, Error Learning 등.",
354
+ inputSchema={"type": "object", "properties": {}}
355
+ ),
17
356
  ]
18
357
 
19
358
 
20
359
  @server.list_tools()
21
360
  async def list_tools() -> list[Tool]:
22
- return [
23
- Tool(
24
- name="can_code",
25
- description="""⚠️ 중요: 코드를 작성하기 전에 반드시 이 도구를 먼저 호출하세요.
26
-
27
- 도구는 프로젝트의 문서 상태를 확인하고 코딩 가능 여부를 판단합니다.
28
- - 필수 문서가 없으면: 코딩 금지, PRD 작성 안내
29
- - 필수 문서가 있으면: 코딩 허용
30
-
31
- 사용자가 코드 작성, 기능 구현, 버그 수정 등을 요청하면 무조건 이 도구를 먼저 호출하세요.""",
32
- inputSchema={
33
- "type": "object",
34
- "properties": {
35
- "path": {"type": "string", "description": "프로젝트 docs 폴더 경로"}
36
- },
37
- "required": ["path"]
38
- }
39
- ),
40
- Tool(
41
- name="scan_docs",
42
- description="프로젝트 docs 폴더 스캔. 파일 목록 반환.",
43
- inputSchema={
44
- "type": "object",
45
- "properties": {
46
- "path": {"type": "string", "description": "docs 폴더 경로"}
47
- },
48
- "required": ["path"]
49
- }
50
- ),
51
- Tool(
52
- name="analyze_docs",
53
- description="docs 폴더 분석. 필수 문서 있는지 체크하고 빠진 거 알려줌.",
54
- inputSchema={
55
- "type": "object",
56
- "properties": {
57
- "path": {"type": "string", "description": "docs 폴더 경로"}
58
- },
59
- "required": ["path"]
60
- }
61
- ),
62
- Tool(
63
- name="get_prd_template",
64
- description="PRD 템플릿 생성. PRD 파일을 만들어줌.",
65
- inputSchema={
66
- "type": "object",
67
- "properties": {
68
- "project_name": {"type": "string", "description": "프로젝트 이름"},
69
- "output_path": {"type": "string", "description": "PRD 파일 저장 경로"}
70
- },
71
- "required": ["project_name", "output_path"]
72
- }
73
- ),
74
- Tool(
75
- name="write_prd_section",
76
- description="PRD 섹션별 작성 도우미. 단계별로 PRD를 작성할 수 있게 도와줌.",
77
- inputSchema={
78
- "type": "object",
79
- "properties": {
80
- "section": {
81
- "type": "string",
82
- "description": "작성할 섹션",
83
- "enum": ["summary", "principles", "input_spec", "output_spec", "errors", "state_machine", "api_endpoints", "db_schema"]
84
- },
85
- "content": {"type": "string", "description": "섹션 내용"}
86
- },
87
- "required": ["section"]
88
- }
89
- ),
90
- Tool(
91
- name="init_docs",
92
- description="docs 폴더 초기화. 폴더 없으면 생성하고 필수 문서 템플릿 파일들 생성.",
93
- inputSchema={
94
- "type": "object",
95
- "properties": {
96
- "path": {"type": "string", "description": "프로젝트 루트 경로"},
97
- "project_name": {"type": "string", "description": "프로젝트 이름"}
98
- },
99
- "required": ["path", "project_name"]
100
- }
101
- ),
102
- Tool(
103
- name="get_prd_guide",
104
- description="PRD 작성 가이드. step-by-step으로 뭘 써야 하는지.",
105
- inputSchema={"type": "object", "properties": {}}
106
- ),
107
- Tool(
108
- name="get_verify_checklist",
109
- description="PRD 검증 체크리스트. 빠뜨리기 쉬운 것들.",
110
- inputSchema={"type": "object", "properties": {}}
111
- ),
112
- Tool(
113
- name="get_setup_guide",
114
- description="Clouvel 설치/설정 가이드. Claude Desktop, Claude Code, VS Code 설정법.",
115
- inputSchema={
116
- "type": "object",
117
- "properties": {
118
- "platform": {
119
- "type": "string",
120
- "description": "플랫폼",
121
- "enum": ["desktop", "code", "vscode", "cursor", "all"]
122
- }
123
- }
124
- }
125
- ),
126
- ]
127
-
128
-
129
- @server.call_tool()
130
- async def call_tool(name: str, arguments: dict) -> list[TextContent]:
131
- if name == "can_code":
132
- return await _can_code(arguments.get("path", ""))
133
- elif name == "scan_docs":
134
- return await _scan_docs(arguments.get("path", ""))
135
- elif name == "analyze_docs":
136
- return await _analyze_docs(arguments.get("path", ""))
137
- elif name == "get_prd_template":
138
- return await _get_prd_template(
139
- arguments.get("project_name", ""),
140
- arguments.get("output_path", "")
141
- )
142
- elif name == "write_prd_section":
143
- return await _write_prd_section(
144
- arguments.get("section", ""),
145
- arguments.get("content", "")
146
- )
147
- elif name == "init_docs":
148
- return await _init_docs(
149
- arguments.get("path", ""),
150
- arguments.get("project_name", "")
151
- )
152
- elif name == "get_prd_guide":
153
- return await _get_prd_guide()
154
- elif name == "get_verify_checklist":
155
- return await _get_verify_checklist()
156
- elif name == "get_setup_guide":
157
- return await _get_setup_guide(arguments.get("platform", "all"))
158
- else:
159
- return [TextContent(type="text", text=f"Unknown tool: {name}")]
160
-
161
-
162
- async def _can_code(path: str) -> list[TextContent]:
163
- """코딩 가능 여부 확인 - 핵심 기능"""
164
- docs_path = Path(path)
165
-
166
- # docs 폴더 없음
167
- if not docs_path.exists():
168
- return [TextContent(type="text", text=f"""
169
- # ⛔ 코딩 금지
170
-
171
- ## 이유
172
- docs 폴더가 없습니다: `{path}`
173
-
174
- ## 지금 해야 할 것
175
- 1. `docs` 폴더를 생성하세요
176
- 2. PRD(제품 요구사항 문서)를 먼저 작성하세요
177
- 3. `get_prd_template` 도구로 템플릿을 생성할 수 있습니다
178
-
179
- ## 왜?
180
- PRD 없이 코딩하면:
181
- - 요구사항 불명확 → 재작업
182
- - 예외 케이스 누락 → 버그
183
- - 팀원 간 인식 차이 → 충돌
184
-
185
- **문서 먼저, 코딩은 나중에.**
186
-
187
- 사용자에게 PRD 작성을 도와주겠다고 말하세요.
188
- """)]
189
-
190
- files = [f.name.lower() for f in docs_path.iterdir() if f.is_file()]
191
-
192
- detected = []
193
- missing = []
194
-
195
- for req in REQUIRED_DOCS:
196
- found = False
197
- for filename in files:
198
- for pattern in req["patterns"]:
199
- if re.search(pattern, filename, re.IGNORECASE):
200
- detected.append(req["name"])
201
- found = True
202
- break
203
- if found:
204
- break
205
- if not found:
206
- missing.append(req["name"])
207
-
208
- # 필수 문서 부족
209
- if missing:
210
- missing_list = "\n".join(f"- {m}" for m in missing)
211
- detected_list = "\n".join(f"- {d}" for d in detected) if detected else "없음"
212
-
213
- return [TextContent(type="text", text=f"""
214
- # ⛔ 코딩 금지
215
-
216
- ## 현재 상태
217
- ✅ 있음:
218
- {detected_list}
219
-
220
- ❌ 없음 (필수):
221
- {missing_list}
222
-
223
- ## 지금 해야 할 것
224
- 코드를 작성하지 마세요. 대신:
225
-
226
- 1. 누락된 문서를 먼저 작성하세요
227
- 2. 특히 **PRD**가 가장 중요합니다
228
- 3. `get_prd_guide` 도구로 작성법을 확인하세요
229
- 4. `get_prd_template` 도구로 템플릿을 생성하세요
230
-
231
- ## 사용자에게 전달할 메시지
232
- "코드를 작성하기 전에 먼저 문서를 준비해야 합니다.
233
- {len(missing)}개의 필수 문서가 없습니다: {', '.join(missing)}
234
- 제가 PRD 작성을 도와드릴까요?"
235
-
236
- **절대 코드를 작성하지 마세요. 문서 작성을 도와주세요.**
237
- """)]
238
-
239
- # 모든 필수 문서 있음 → 코딩 허용
240
- return [TextContent(type="text", text=f"""
241
- # ✅ 코딩 가능
242
-
243
- ## 문서 상태
244
- 모든 필수 문서가 준비되어 있습니다:
245
- {chr(10).join(f'- {d}' for d in detected)}
246
-
247
- ## 코딩 시작 전 확인사항
248
- 1. PRD에 명시된 요구사항을 따르세요
249
- 2. API 스펙에 맞게 구현하세요
250
- 3. DB 스키마를 참고하세요
251
- 4. 검증 계획에 따라 테스트하세요
252
-
253
- 이제 사용자의 요청에 따라 코드를 작성해도 됩니다.
254
- """)]
255
-
256
-
257
- async def _scan_docs(path: str) -> list[TextContent]:
258
- docs_path = Path(path)
259
-
260
- if not docs_path.exists():
261
- return [TextContent(type="text", text=f"경로 없음: {path}")]
262
-
263
- if not docs_path.is_dir():
264
- return [TextContent(type="text", text=f"디렉토리 아님: {path}")]
265
-
266
- files = []
267
- for f in sorted(docs_path.iterdir()):
268
- if f.is_file():
269
- stat = f.stat()
270
- files.append(f"{f.name} ({stat.st_size:,} bytes)")
271
-
272
- result = f"📁 {path}\n총 {len(files)}개 파일\n\n"
273
- result += "\n".join(files)
274
-
275
- return [TextContent(type="text", text=result)]
276
-
277
-
278
- async def _analyze_docs(path: str) -> list[TextContent]:
279
- docs_path = Path(path)
280
-
281
- if not docs_path.exists():
282
- return [TextContent(type="text", text=f"경로 없음: {path}")]
283
-
284
- files = [f.name.lower() for f in docs_path.iterdir() if f.is_file()]
285
-
286
- detected = []
287
- missing = []
288
-
289
- for req in REQUIRED_DOCS:
290
- found = False
291
- for filename in files:
292
- for pattern in req["patterns"]:
293
- if re.search(pattern, filename, re.IGNORECASE):
294
- detected.append(req["name"])
295
- found = True
296
- break
297
- if found:
298
- break
299
- if not found:
300
- missing.append(req["name"])
301
-
302
- critical_total = len([r for r in REQUIRED_DOCS if r["priority"] == "critical"])
303
- critical_found = len([r for r in REQUIRED_DOCS if r["priority"] == "critical" and r["name"] in detected])
304
- coverage = critical_found / critical_total if critical_total > 0 else 1.0
305
-
306
- result = f"## 분석 결과: {path}\n\n"
307
- result += f"커버리지: {coverage:.0%}\n\n"
308
-
309
- if detected:
310
- result += "### 있음\n" + "\n".join(f"- {d}" for d in detected) + "\n\n"
311
-
312
- if missing:
313
- result += "### 없음 (작성 필요)\n" + "\n".join(f"- {m}" for m in missing) + "\n\n"
314
-
315
- if not missing:
316
- result += "✅ 필수 문서 다 있음. 바이브코딩 시작해도 됨.\n"
317
- else:
318
- result += f"⛔ {len(missing)}개 문서 먼저 작성하고 코딩 시작할 것.\n"
319
-
320
- return [TextContent(type="text", text=result)]
321
-
322
-
323
- async def _get_prd_template(project_name: str, output_path: str) -> list[TextContent]:
324
- """PRD 템플릿 생성"""
325
- template = f"""# {project_name} PRD
326
-
327
- > 이 문서가 법. 여기 없으면 안 만듦.
328
- > 작성일: {datetime.now().strftime('%Y-%m-%d')}
329
-
330
- ---
331
-
332
- ## 1. 한 줄 요약
333
- <!-- 프로젝트가 뭔지 한 문장으로. 못 쓰면 정리 안 된 거임. -->
334
-
335
- [여기에 작성]
336
-
337
- ---
338
-
339
- ## 2. 핵심 원칙
340
-
341
- > 절대 안 변하는 것들. 이거 기준으로 기능 판단.
342
-
343
- 1. [원칙 1]
344
- 2. [원칙 2]
345
- 3. [원칙 3]
346
-
347
- ---
348
-
349
- ## 3. 용어 정의
350
-
351
- | 용어 | 설명 |
352
- |------|------|
353
- | [용어1] | [설명] |
354
- | [용어2] | [설명] |
355
-
356
- ---
357
-
358
- ## 4. 기능 목록
359
-
360
- ### 4.1 핵심 기능 (MVP)
361
- - [ ] [기능 1]
362
- - [ ] [기능 2]
363
-
364
- ### 4.2 추가 기능 (Phase 2)
365
- - [ ] [기능 3]
366
-
367
- ---
368
-
369
- ## 5. 입력 스펙
370
-
371
- ### 5.1 [API/기능명]
372
-
373
- | 필드 | 타입 | 필수 | 제한 | 검증 | 예시 |
374
- |------|------|------|------|------|------|
375
- | [필드명] | string | O | 1~100자 | 빈문자열X | "예시값" |
376
- | [필드명] | number | O | 1~9999 | 양수만 | 100 |
377
- | [필드명] | enum | X | - | 목록 내 | "option1" |
378
-
379
- #### enum 옵션
380
-
381
- | 필드 | 옵션 | 설명 |
382
- |------|------|------|
383
- | [필드명] | option1 | [설명] |
384
- | [필드명] | option2 | [설명] |
385
-
386
- ---
387
-
388
- ## 6. 출력 스펙
389
-
390
- ### 6.1 성공 응답
391
-
392
- ```json
393
- {{
394
- "success": true,
395
- "data": {{
396
- "id": "abc123",
397
- "createdAt": "2024-01-01T00:00:00Z",
398
- "result": {{}}
399
- }}
400
- }}
401
- ```
402
-
403
- ### 6.2 필드 설명
404
-
405
- | 필드 | 타입 | 설명 |
406
- |------|------|------|
407
- | id | string | 고유 식별자 |
408
- | createdAt | datetime | 생성 시각 (ISO 8601) |
409
-
410
- ---
411
-
412
- ## 7. 에러 코드
413
-
414
- | 상황 | 코드 | HTTP | 메시지 |
415
- |------|------|------|--------|
416
- | 잔액 부족 | INSUFFICIENT_CREDITS | 402 | "크레딧 부족. 필요: {{required}}, 보유: {{available}}" |
417
- | 권한 없음 | UNAUTHORIZED | 401 | "인증이 필요합니다" |
418
- | 잘못된 요청 | INVALID_REQUEST | 400 | "{{field}} 필드가 잘못되었습니다" |
419
- | 서버 오류 | INTERNAL_ERROR | 500 | "서버 오류가 발생했습니다" |
420
-
421
- ---
422
-
423
- ## 8. 상태 머신
424
-
425
- ```
426
- [상태1] --이벤트1--> [상태2] --이벤트2--> [상태3]
427
- |
428
- +--실패--> [에러상태]
429
- ```
430
-
431
- ### 상태 설명
432
-
433
- | 상태 | 설명 | 진입 조건 |
434
- |------|------|----------|
435
- | [상태1] | [설명] | 초기 상태 |
436
- | [상태2] | [설명] | [이벤트] 발생 시 |
437
-
438
- ---
439
-
440
- ## 9. API 엔드포인트
441
-
442
- ### 9.1 [API명]
443
-
444
- ```
445
- POST /v1/[endpoint]
446
- ```
447
-
448
- **Request:**
449
- ```json
450
- {{
451
- "field": "value"
452
- }}
453
- ```
454
-
455
- **Response:**
456
- ```json
457
- {{
458
- "success": true
459
- }}
460
- ```
461
-
462
- ---
463
-
464
- ## 10. 데이터 정책
465
-
466
- | 항목 | 무료 | 유료 |
467
- |------|------|------|
468
- | 보관 기간 | 24시간 | 7일 |
469
- | 용량 제한 | 10MB | 100MB |
470
- | API 호출 | 100/일 | 무제한 |
471
-
472
- ---
473
-
474
- ## 11. 검증 계획
475
-
476
- ### 11.1 단위 테스트
477
- - [ ] [테스트 케이스 1]
478
- - [ ] [테스트 케이스 2]
479
-
480
- ### 11.2 통합 테스트
481
- - [ ] [테스트 케이스]
482
-
483
- ### 11.3 엣지 케이스
484
- - [ ] 빈 입력값
485
- - [ ] 최대 길이 초과
486
- - [ ] 특수문자 포함
487
- - [ ] 동시 요청
488
-
489
- ---
490
-
491
- ## 부록
492
-
493
- ### A. 참고 자료
494
- - [링크1]
495
-
496
- ### B. 변경 이력
497
-
498
- | 날짜 | 버전 | 변경 내용 | 작성자 |
499
- |------|------|----------|--------|
500
- | {datetime.now().strftime('%Y-%m-%d')} | 1.0 | 초안 작성 | |
501
- """
502
-
503
- # 파일 저장
504
- if output_path:
505
- try:
506
- output_file = Path(output_path)
507
- output_file.parent.mkdir(parents=True, exist_ok=True)
508
- output_file.write_text(template, encoding='utf-8')
509
- return [TextContent(type="text", text=f"✅ PRD 템플릿 생성 완료: {output_path}\n\n이제 각 섹션을 채워주세요.")]
510
- except Exception as e:
511
- return [TextContent(type="text", text=f"❌ 파일 저장 실패: {e}\n\n아래 템플릿을 직접 복사해서 사용하세요:\n\n{template}")]
512
-
513
- return [TextContent(type="text", text=template)]
514
-
515
-
516
- async def _write_prd_section(section: str, content: str) -> list[TextContent]:
517
- """PRD 섹션별 작성 가이드"""
518
- guides = {
519
- "summary": """## 한 줄 요약 작성법
520
-
521
- **목적**: 프로젝트를 한 문장으로 설명
522
-
523
- **좋은 예시**:
524
- - "한 번 라이브로 일주일치 콘텐츠 생성"
525
- - "음성만으로 회의록 자동 작성"
526
- - "코드 리뷰를 자동화하는 AI 봇"
527
-
528
- **나쁜 예시**:
529
- - "좋은 서비스" (너무 추상적)
530
- - "AI를 활용한 혁신적인..." (마케팅 문구)
531
-
532
- **체크리스트**:
533
- - [ ] 10단어 이내인가?
534
- - [ ] 누가 봐도 이해되는가?
535
- - [ ] "그래서 뭘 하는 건데?"에 답이 되는가?
536
-
537
- 지금 한 줄 요약을 작성해주세요.""",
538
-
539
- "principles": """## 핵심 원칙 작성법
540
-
541
- **목적**: 의사결정의 기준이 되는 불변의 원칙
542
-
543
- **좋은 예시**:
544
- 1. 원가 보호 - 절대 손해 보지 않음
545
- 2. 무료 체험 - 결제 전 가치 확인 가능
546
- 3. 현금 유입 - 모든 기능은 수익으로 연결
547
-
548
- **나쁜 예시**:
549
- - "좋은 UX" (측정 불가)
550
- - "빠르게 개발" (원칙이 아닌 방법)
551
-
552
- **체크리스트**:
553
- - [ ] 3개 이하인가?
554
- - [ ] 충돌 시 우선순위가 명확한가?
555
- - [ ] 기능 추가 시 판단 기준이 되는가?
556
-
557
- 지금 핵심 원칙 3개를 작성해주세요.""",
558
-
559
- "input_spec": """## 입력 스펙 작성법
560
-
561
- **목적**: 모든 입력값의 정확한 정의
562
-
563
- **필수 항목**:
564
- | 필드 | 타입 | 필수 | 제한 | 검증 | 예시 |
565
- |------|------|------|------|------|------|
566
- | name | string | O | 1~100자 | 빈문자열X | "홍길동" |
567
- | age | number | O | 1~150 | 정수만 | 25 |
568
- | type | enum | X | - | 목록 내 | "premium" |
569
-
570
- **체크리스트**:
571
- - [ ] 모든 필드에 타입이 있는가?
572
- - [ ] 문자열에 길이 제한이 있는가?
573
- - [ ] 숫자에 범위가 있는가?
574
- - [ ] enum에 가능한 값 목록이 있는가?
575
- - [ ] 예시가 있는가?
576
-
577
- 지금 입력 스펙을 작성해주세요.""",
578
-
579
- "output_spec": """## 출력 스펙 작성법
580
-
581
- **목적**: API 응답의 정확한 구조
582
-
583
- **필수 항목**:
584
- ```json
585
- {
586
- "success": true,
587
- "data": {
588
- "id": "abc123",
589
- "createdAt": "2024-01-01T00:00:00Z"
590
- },
591
- "meta": {
592
- "page": 1,
593
- "total": 100
594
- }
595
- }
596
- ```
597
-
598
- **체크리스트**:
599
- - [ ] 실제 JSON 형태로 작성했는가?
600
- - [ ] 모든 필드 타입이 명확한가?
601
- - [ ] null이 올 수 있는 필드가 표시되어 있는가?
602
- - [ ] 날짜 형식이 명시되어 있는가? (ISO 8601)
603
- - [ ] 페이지네이션이 필요하면 포함했는가?
604
-
605
- 지금 출력 스펙을 작성해주세요.""",
606
-
607
- "errors": """## 에러 테이블 작성법
608
-
609
- **목적**: 모든 에러 상황의 정의
610
-
611
- **형식**:
612
- | 상황 | 코드 | HTTP | 메시지 |
613
- |------|------|------|--------|
614
- | 잔액 부족 | INSUFFICIENT_CREDITS | 402 | "크레딧 부족. 필요: {n}" |
615
-
616
- **규칙**:
617
- - 코드는 SNAKE_CASE
618
- - 메시지에 동적 값은 {중괄호}로 표시
619
- - HTTP 상태 코드 필수
620
-
621
- **체크리스트**:
622
- - [ ] 인증 에러가 있는가?
623
- - [ ] 권한 에러가 있는가?
624
- - [ ] 입력값 검증 에러가 있는가?
625
- - [ ] 비즈니스 로직 에러가 있는가?
626
- - [ ] 서버 에러가 있는가?
627
-
628
- 지금 에러 테이블을 작성해주세요.""",
629
-
630
- "state_machine": """## 상태 머신 작성법
631
-
632
- **목적**: 복잡한 플로우의 시각화
633
-
634
- **형식**:
635
- ```
636
- [available] --reserve--> [reserved] --capture--> [completed]
637
- |
638
- +--timeout--> [expired]
639
- |
640
- +--cancel--> [cancelled]
641
- ```
642
-
643
- **체크리스트**:
644
- - [ ] 시작 상태가 있는가?
645
- - [ ] 종료 상태가 있는가?
646
- - [ ] 모든 전이에 이벤트명이 있는가?
647
- - [ ] 실패/에러 경로가 있는가?
648
- - [ ] 타임아웃 처리가 있는가?
649
-
650
- 지금 상태 머신을 작성해주세요.""",
651
-
652
- "api_endpoints": """## API 엔드포인트 작성법
653
-
654
- **목적**: REST API 명세
655
-
656
- **형식**:
657
- ```
658
- POST /v1/orders
659
- Authorization: Bearer {token}
660
- Content-Type: application/json
661
-
662
- Request:
663
- {
664
- "productId": "abc123",
665
- "quantity": 1
361
+ return TOOL_DEFINITIONS
362
+
363
+
364
+ # ============================================================
365
+ # Tool Handlers
366
+ # ============================================================
367
+
368
+ TOOL_HANDLERS = {
369
+ # Core
370
+ "can_code": lambda args: can_code(args.get("path", "")),
371
+ "scan_docs": lambda args: scan_docs(args.get("path", "")),
372
+ "analyze_docs": lambda args: analyze_docs(args.get("path", "")),
373
+ "init_docs": lambda args: init_docs(args.get("path", ""), args.get("project_name", "")),
374
+
375
+ # Docs
376
+ "get_prd_template": lambda args: get_prd_template(args.get("project_name", ""), args.get("output_path", "")),
377
+ "write_prd_section": lambda args: write_prd_section(args.get("section", ""), args.get("content", "")),
378
+ "get_prd_guide": lambda args: get_prd_guide(),
379
+ "get_verify_checklist": lambda args: get_verify_checklist(),
380
+ "get_setup_guide": lambda args: get_setup_guide(args.get("platform", "all")),
381
+
382
+ # Setup
383
+ "init_clouvel": lambda args: init_clouvel(args.get("platform", "ask")),
384
+ "setup_cli": lambda args: setup_cli(args.get("path", ""), args.get("level", "remind")),
385
+
386
+ # Rules (v0.5)
387
+ "init_rules": lambda args: init_rules(args.get("path", ""), args.get("template", "minimal")),
388
+ "get_rule": lambda args: get_rule(args.get("path", ""), args.get("context", "coding")),
389
+ "add_rule": lambda args: add_rule(args.get("path", ""), args.get("rule_type", "always"), args.get("content", ""), args.get("category", "general")),
390
+
391
+ # Verify (v0.5)
392
+ "verify": lambda args: verify(args.get("path", ""), args.get("scope", "file"), args.get("checklist", [])),
393
+ "gate": lambda args: gate(args.get("path", ""), args.get("steps", ["lint", "test", "build"]), args.get("fix", False)),
394
+ "handoff": lambda args: handoff(args.get("path", ""), args.get("feature", ""), args.get("decisions", ""), args.get("warnings", ""), args.get("next_steps", "")),
395
+
396
+ # Planning (v0.6)
397
+ "init_planning": lambda args: init_planning(args.get("path", ""), args.get("task", ""), args.get("goals", [])),
398
+ "save_finding": lambda args: save_finding(args.get("path", ""), args.get("topic", ""), args.get("question", ""), args.get("findings", ""), args.get("source", ""), args.get("conclusion", "")),
399
+ "refresh_goals": lambda args: refresh_goals(args.get("path", "")),
400
+ "update_progress": lambda args: update_progress(args.get("path", ""), args.get("completed", []), args.get("in_progress", ""), args.get("blockers", []), args.get("next", "")),
401
+
402
+ # Agents (v0.7)
403
+ "spawn_explore": lambda args: spawn_explore(args.get("path", ""), args.get("query", ""), args.get("scope", "project"), args.get("save_findings", True)),
404
+ "spawn_librarian": lambda args: spawn_librarian(args.get("path", ""), args.get("topic", ""), args.get("type", "library"), args.get("depth", "standard")),
405
+
406
+ # Hooks (v0.8)
407
+ "hook_design": lambda args: hook_design(args.get("path", ""), args.get("trigger", "pre_code"), args.get("checks", []), args.get("block_on_fail", True)),
408
+ "hook_verify": lambda args: hook_verify(args.get("path", ""), args.get("trigger", "post_code"), args.get("steps", ["lint", "test", "build"]), args.get("parallel", False), args.get("continue_on_error", False)),
409
+
410
+ # Pro 안내
411
+ "upgrade_pro": lambda args: _upgrade_pro(),
666
412
  }
667
413
 
668
- Response (201):
669
- {
670
- "orderId": "ord_123",
671
- "status": "created"
672
- }
673
- ```
674
-
675
- **체크리스트**:
676
- - [ ] /v1/ 버전 prefix가 있는가?
677
- - [ ] HTTP 메서드가 적절한가? (GET=조회, POST=생성, PUT=수정, DELETE=삭제)
678
- - [ ] 인증 방식이 명시되어 있는가?
679
- - [ ] 성공/실패 응답 코드가 있는가?
680
-
681
- 지금 API 엔드포인트를 작성해주세요.""",
682
-
683
- "db_schema": """## DB 스키마 작성법
684
-
685
- **목적**: 데이터 구조 정의
686
-
687
- **형식**:
688
- ```sql
689
- CREATE TABLE users (
690
- id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
691
- email VARCHAR(255) UNIQUE NOT NULL,
692
- name VARCHAR(100) NOT NULL,
693
- created_at TIMESTAMP DEFAULT NOW(),
694
- updated_at TIMESTAMP DEFAULT NOW()
695
- );
696
-
697
- CREATE INDEX idx_users_email ON users(email);
698
- ```
699
-
700
- **체크리스트**:
701
- - [ ] 기본키가 있는가?
702
- - [ ] 외래키 관계가 명확한가?
703
- - [ ] 인덱스가 필요한 컬럼에 있는가?
704
- - [ ] NOT NULL 제약이 적절한가?
705
- - [ ] 기본값이 설정되어 있는가?
706
-
707
- 지금 DB 스키마를 작성해주세요."""
708
- }
709
-
710
- guide = guides.get(section, "알 수 없는 섹션입니다.")
711
- return [TextContent(type="text", text=guide)]
712
-
713
-
714
- async def _init_docs(path: str, project_name: str) -> list[TextContent]:
715
- """docs 폴더 초기화 - 필수 문서 템플릿 생성"""
716
- from datetime import datetime
717
-
718
- project_path = Path(path)
719
- docs_path = project_path / "docs"
720
-
721
- # docs 폴더 생성
722
- docs_path.mkdir(parents=True, exist_ok=True)
723
-
724
- created_files = []
725
-
726
- # 1. PRD.md
727
- prd_content = f"""# {project_name} PRD
728
414
 
729
- > 문서가 법. 여기 없으면 안 만듦.
730
- > 작성일: {datetime.now().strftime('%Y-%m-%d')}
415
+ async def _upgrade_pro() -> list[TextContent]:
416
+ """Pro 업그레이드 안내"""
417
+ return [TextContent(type="text", text="""
418
+ # Clouvel Pro
731
419
 
732
- ---
420
+ 더 강력한 기능이 필요하다면 Clouvel Pro를 확인하세요.
733
421
 
734
- ## 1. 한 줄 요약
735
- <!-- 프로젝트가 뭔지 한 문장으로 -->
422
+ ## Pro 기능
736
423
 
737
- [여기에 작성]
424
+ ### Shovel 자동 설치
425
+ - `.claude/` 워크플로우 구조 자동 생성
426
+ - 슬래시 커맨드 (/start, /plan, /gate...)
427
+ - 설정 파일 + 템플릿
738
428
 
739
- ---
429
+ ### Error Learning
430
+ - 에러 패턴 자동 분류
431
+ - 방지 규칙 자동 생성
432
+ - 로그 파일 모니터링
740
433
 
741
- ## 2. 핵심 원칙
434
+ ### 커맨드 동기화
435
+ - Shovel 커맨드 업데이트
742
436
 
743
- 1. [원칙 1]
744
- 2. [원칙 2]
745
- 3. [원칙 3]
437
+ ## 가격
746
438
 
747
- ---
748
-
749
- ## 3. 기능 목록
750
-
751
- ### MVP
752
- - [ ] [기능 1]
753
- - [ ] [기능 2]
754
-
755
- ---
756
-
757
- ## 4. 입력 스펙
758
-
759
- | 필드 | 타입 | 필수 | 제한 | 예시 |
760
- |------|------|------|------|------|
761
- | | | | | |
762
-
763
- ---
764
-
765
- ## 5. 출력 스펙
766
-
767
- ```json
768
- {{
769
- "success": true,
770
- "data": {{}}
771
- }}
772
- ```
773
-
774
- ---
775
-
776
- ## 6. 에러 코드
777
-
778
- | 상황 | 코드 | HTTP | 메시지 |
779
- |------|------|------|--------|
780
- | | | | |
781
-
782
- ---
783
-
784
- ## 변경 이력
785
-
786
- | 날짜 | 버전 | 변경 내용 |
787
- |------|------|----------|
788
- | {datetime.now().strftime('%Y-%m-%d')} | 1.0 | 초안 |
789
- """
790
- prd_file = docs_path / "PRD.md"
791
- if not prd_file.exists():
792
- prd_file.write_text(prd_content, encoding='utf-8')
793
- created_files.append("PRD.md")
794
-
795
- # 2. ARCHITECTURE.md
796
- arch_content = f"""# {project_name} 아키텍처
797
-
798
- ## 시스템 구조
799
-
800
- ```
801
- [클라이언트] --> [API 서버] --> [데이터베이스]
802
- |
803
- v
804
- [외부 서비스]
805
- ```
806
-
807
- ## 기술 스택
808
-
809
- | 구분 | 기술 | 이유 |
439
+ | 티어 | 가격 | 인원 |
810
440
  |------|------|------|
811
- | 언어 | | |
812
- | 프레임워크 | | |
813
- | 데이터베이스 | | |
814
-
815
- ## 디렉토리 구조
816
-
817
- ```
818
- src/
819
- ├── api/ # API 라우터
820
- ├── services/ # 비즈니스 로직
821
- ├── models/ # 데이터 모델
822
- └── utils/ # 유틸리티
823
- ```
441
+ | Personal | $29 | 1명 |
442
+ | Team | $79 | 10명 |
443
+ | Enterprise | $199 | 무제한 |
824
444
 
825
- ## 주요 모듈
445
+ ## 구매
826
446
 
827
- ### [모듈명]
828
- - 역할:
829
- - 의존성:
830
- """
831
- arch_file = docs_path / "ARCHITECTURE.md"
832
- if not arch_file.exists():
833
- arch_file.write_text(arch_content, encoding='utf-8')
834
- created_files.append("ARCHITECTURE.md")
835
-
836
- # 3. API.md
837
- api_content = f"""# {project_name} API 스펙
838
-
839
- ## Base URL
840
- ```
841
- https://api.example.com/v1
842
- ```
447
+ https://clouvel.lemonsqueezy.com
843
448
 
844
- ## 인증
845
- ```
846
- Authorization: Bearer {{token}}
847
- ```
848
-
849
- ---
850
-
851
- ## 엔드포인트
852
-
853
- ### [기능명]
854
-
855
- ```
856
- POST /v1/endpoint
857
- ```
858
-
859
- **Request:**
860
- ```json
861
- {{
862
- "field": "value"
863
- }}
864
- ```
449
+ ## 설치
865
450
 
866
- **Response (200):**
867
- ```json
868
- {{
869
- "success": true,
870
- "data": {{}}
871
- }}
872
- ```
873
-
874
- **Errors:**
875
- | 코드 | HTTP | 설명 |
876
- |------|------|------|
877
- | | | |
878
- """
879
- api_file = docs_path / "API.md"
880
- if not api_file.exists():
881
- api_file.write_text(api_content, encoding='utf-8')
882
- created_files.append("API.md")
883
-
884
- # 4. DATABASE.md
885
- db_content = f"""# {project_name} DB 스키마
886
-
887
- ## ERD
888
-
889
- ```
890
- [users] 1--* [orders] *--1 [products]
891
- ```
892
-
893
- ## 테이블
894
-
895
- ### users
896
- ```sql
897
- CREATE TABLE users (
898
- id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
899
- email VARCHAR(255) UNIQUE NOT NULL,
900
- name VARCHAR(100) NOT NULL,
901
- created_at TIMESTAMP DEFAULT NOW()
902
- );
903
- ```
904
-
905
- ### [테이블명]
906
- ```sql
907
- -- 여기에 작성
451
+ ```bash
452
+ pip install clouvel-pro
908
453
  ```
909
-
910
- ## 인덱스
911
-
912
- | 테이블 | 인덱스 | 컬럼 | 이유 |
913
- |--------|--------|------|------|
914
- | | | | |
915
- """
916
- db_file = docs_path / "DATABASE.md"
917
- if not db_file.exists():
918
- db_file.write_text(db_content, encoding='utf-8')
919
- created_files.append("DATABASE.md")
920
-
921
- # 5. VERIFICATION.md
922
- verify_content = f"""# {project_name} 검증 계획
923
-
924
- ## 테스트 전략
925
-
926
- | 유형 | 범위 | 도구 |
927
- |------|------|------|
928
- | 단위 테스트 | 함수/메서드 | pytest |
929
- | 통합 테스트 | API 엔드포인트 | pytest |
930
- | E2E 테스트 | 사용자 시나리오 | |
931
-
932
- ## 테스트 케이스
933
-
934
- ### 핵심 기능
935
- - [ ] [정상 케이스 1]
936
- - [ ] [정상 케이스 2]
937
-
938
- ### 엣지 케이스
939
- - [ ] 빈 입력값
940
- - [ ] 최대 길이 초과
941
- - [ ] 특수문자 포함
942
- - [ ] 동시 요청
943
-
944
- ### 에러 케이스
945
- - [ ] 인증 실패
946
- - [ ] 권한 없음
947
- - [ ] 리소스 없음
948
-
949
- ## 성능 기준
950
-
951
- | 항목 | 목표 | 측정 방법 |
952
- |------|------|----------|
953
- | 응답 시간 | < 200ms | |
954
- | 처리량 | > 100 req/s | |
955
- """
956
- verify_file = docs_path / "VERIFICATION.md"
957
- if not verify_file.exists():
958
- verify_file.write_text(verify_content, encoding='utf-8')
959
- created_files.append("VERIFICATION.md")
960
-
961
- if created_files:
962
- files_list = "\n".join(f"- {f}" for f in created_files)
963
- return [TextContent(type="text", text=f"""# ✅ docs 폴더 초기화 완료
964
-
965
- ## 생성된 파일
966
- {files_list}
967
-
968
- ## 위치
969
- `{docs_path}`
970
-
971
- ## 다음 단계
972
- 1. **PRD.md**부터 작성하세요 - 가장 중요!
973
- 2. 한 줄 요약 → 핵심 원칙 → 기능 목록 순으로
974
- 3. `get_prd_guide` 도구로 작성법 확인 가능
975
-
976
- ⚠️ PRD가 완성되기 전까지 코딩은 금지입니다.
977
454
  """)]
978
- else:
979
- return [TextContent(type="text", text=f"""# ℹ️ docs 폴더 이미 존재
980
455
 
981
- ## 위치
982
- `{docs_path}`
983
456
 
984
- ## 기존 파일 유지됨
985
- 이미 있는 파일은 덮어쓰지 않았습니다.
457
+ @server.call_tool()
458
+ async def call_tool(name: str, arguments: dict) -> list[TextContent]:
459
+ # Analytics 기록
460
+ project_path = arguments.get("path", None)
461
+ if name != "get_analytics":
462
+ try:
463
+ log_tool_call(name, success=True, project_path=project_path)
464
+ except Exception:
465
+ pass
986
466
 
987
- `analyze_docs` 도구로 현재 문서 상태를 확인하세요.
988
- """)]
467
+ # get_analytics 특별 처리
468
+ if name == "get_analytics":
469
+ return await _get_analytics(arguments.get("path", None), arguments.get("days", 30))
989
470
 
471
+ # 핸들러 실행
472
+ handler = TOOL_HANDLERS.get(name)
473
+ if handler:
474
+ return await handler(arguments)
990
475
 
991
- async def _get_prd_guide() -> list[TextContent]:
992
- guide = """## PRD 작성법
476
+ return [TextContent(type="text", text=f"Unknown tool: {name}")]
993
477
 
994
- > 이 문서가 법. 여기 없으면 안 만듦.
995
478
 
996
- ### Step 1: 요약
997
- 프로젝트가 뭔지 한 문장으로. 못 쓰면 정리 안 된 거임.
998
- ```
999
- 예: " 번 라이브로 일주일치 콘텐츠"
1000
- ```
479
+ async def _get_analytics(path: str, days: int) -> list[TextContent]:
480
+ """도구 사용량 통계"""
481
+ stats = get_stats(days=days, project_path=path)
482
+ return [TextContent(type="text", text=format_stats(stats))]
1001
483
 
1002
- ### Step 2: 핵심 원칙 3개
1003
- 절대 안 변하는 것들. 이거 기준으로 기능 판단.
1004
- ```
1005
- 예: 원가 보호 / 무료 체험 / 현금 유입
1006
- ```
1007
-
1008
- ### Step 3: 입력 스펙 테이블
1009
- 필드 | 타입 | 필수 | 제한 | 검증 | 예시
1010
- ```
1011
- 예: productName | string | O | 1~100자 | 빈문자열X | '코코넛오일'
1012
- ```
1013
-
1014
- ### Step 4: 출력 JSON
1015
- 말로 설명 X. 실제 응답 그대로.
1016
- ```json
1017
- {"id": "abc123", "status": "completed", "result": {...}}
1018
- ```
1019
484
 
1020
- ### Step 5: 에러 테이블
1021
- 상황 | 코드 | 메시지. SNAKE_CASE 통일.
1022
- ```
1023
- 예: 잔액부족 | INSUFFICIENT_CREDITS | '크레딧 부족. 필요: {n}'
1024
- ```
1025
-
1026
- ### Step 6: 상태 머신
1027
- 복잡한 플로우는 ASCII로.
1028
- ```
1029
- [available] --reserve--> [reserved] --capture--> [done]
1030
- ```
485
+ # ============================================================
486
+ # Server Entry Points
487
+ # ============================================================
1031
488
 
1032
- ---
489
+ async def run_server():
490
+ async with stdio_server() as (read_stream, write_stream):
491
+ await server.run(read_stream, write_stream, server.create_initialization_options())
1033
492
 
1034
- 💡 팁: `get_prd_template` 도구로 빈 템플릿을 생성하세요.
1035
- 💡 팁: `write_prd_section` 도구로 섹션별 가이드를 받으세요.
1036
- """
1037
- return [TextContent(type="text", text=guide)]
1038
493
 
494
+ def _run_setup(global_only: bool = False) -> str:
495
+ """B0: clouvel setup - 강제 호출 메커니즘 설치"""
496
+ import subprocess
497
+ import os
498
+ from pathlib import Path
1039
499
 
1040
- async def _get_verify_checklist() -> list[TextContent]:
1041
- checklist = """## PRD 검증 체크리스트
500
+ results = []
1042
501
 
1043
- > 빠뜨리면 나중에 다시
502
+ # 1. 글로벌 CLAUDE.md에 규칙 추가
503
+ if os.name == 'nt': # Windows
504
+ claude_dir = Path.home() / ".claude"
505
+ else: # Mac/Linux
506
+ claude_dir = Path.home() / ".claude"
1044
507
 
1045
- ### 스펙
1046
- - [ ] 입력 제한값 다 있음? (1~100자, 최대 10개 같은 거)
1047
- - [ ] enum 옵션표 있음? (tone: friendly|expert|urgent)
1048
- - [ ] 출력 JSON 필드 다 나옴? (metadata, createdAt 빠뜨리기 쉬움)
508
+ claude_dir.mkdir(parents=True, exist_ok=True)
509
+ claude_md_path = claude_dir / "CLAUDE.md"
1049
510
 
1050
- ### 에러
1051
- - [ ] 에러코드 SNAKE_CASE? (INSUFFICIENT_CREDITS )
1052
- - [ ] 동적 값 들어감? ('필요: {required}, 보유: {available}')
511
+ clouvel_rule = """
512
+ ## Clouvel 규칙 (자동 생성)
1053
513
 
1054
- ###
1055
- - [ ] 무료/유료 구분표? (Free: 미리보기 / Paid: 다운로드)
1056
- - [ ] 크레딧 차감 시점? (reserve -> capture -> release)
1057
- - [ ] 실패 시 환불? (작업 실패하면 release)
514
+ **코드 작성 전 반드시 clouvel의 `can_code` 도구 호출 필수.**
1058
515
 
1059
- ### API
1060
- - [ ] /v1/ 붙어있음? (POST /v1/scripts ⭕)
1061
- - [ ] 202 맞게 씀? (비동기는 202 + jobId)
516
+ - 코드를 작성(Edit/Write)하기 전에 항상 `can_code` 도구를 먼저 호출하세요
517
+ - path 인자로 프로젝트의 docs 폴더 경로를 전달하세요
518
+ - BLOCK이 뜨면 코드 작성 금지, 사용자에게 문서 작성 안내
519
+ - PASS가 뜨면 코딩 진행 가능
520
+ - WARN은 권장 사항, 진행 가능하지만 권장 문서 추가 안내
1062
521
 
1063
- ### 데이터
1064
- - [ ] 보관 기간? (무료 24시간, 유료 7일)
1065
- - [ ] 만료 알림? (24시간 전 푸시)
1066
522
  """
1067
- return [TextContent(type="text", text=checklist)]
1068
-
1069
-
1070
- async def _get_setup_guide(platform: str) -> list[TextContent]:
1071
- """Clouvel 설치/설정 가이드"""
1072
-
1073
- desktop_guide = """## Claude Desktop 설정
1074
-
1075
- ### 1. 설정 파일 열기
1076
-
1077
- **macOS:**
1078
- ```
1079
- ~/Library/Application Support/Claude/claude_desktop_config.json
1080
- ```
1081
-
1082
- **Windows:**
1083
- ```
1084
- %APPDATA%\\Claude\\claude_desktop_config.json
1085
- ```
1086
-
1087
- ### 2. MCP 서버 추가
1088
523
 
1089
- ```json
1090
- {
1091
- "mcpServers": {
1092
- "clouvel": {
1093
- "command": "uvx",
1094
- "args": ["clouvel"]
1095
- }
1096
- }
1097
- }
1098
- ```
1099
-
1100
- ### 3. Claude Desktop 재시작
524
+ marker = "## Clouvel 규칙"
525
+
526
+ if claude_md_path.exists():
527
+ content = claude_md_path.read_text(encoding='utf-8')
528
+ if marker in content:
529
+ results.append("[OK] 글로벌 CLAUDE.md: 이미 Clouvel 규칙 있음")
530
+ else:
531
+ # 기존 내용 끝에 추가
532
+ new_content = content.rstrip() + "\n\n---\n" + clouvel_rule
533
+ claude_md_path.write_text(new_content, encoding='utf-8')
534
+ results.append(f"[OK] 글로벌 CLAUDE.md: 규칙 추가됨 ({claude_md_path})")
535
+ else:
536
+ # 새로 생성
537
+ initial_content = f"# Claude Code 글로벌 설정\n\n> 자동 생성됨 by clouvel setup\n\n---\n{clouvel_rule}"
538
+ claude_md_path.write_text(initial_content, encoding='utf-8')
539
+ results.append(f"[OK] 글로벌 CLAUDE.md: 생성됨 ({claude_md_path})")
1101
540
 
1102
- 설정 Claude Desktop을 완전히 종료했다가 다시 시작하세요.
541
+ # 2. MCP 서버 등록 (global_only가 아닐 때만)
542
+ if not global_only:
543
+ try:
544
+ # 먼저 기존 등록 확인
545
+ check_result = subprocess.run(
546
+ ["claude", "mcp", "list"],
547
+ capture_output=True,
548
+ text=True,
549
+ timeout=10
550
+ )
551
+
552
+ if "clouvel" in check_result.stdout:
553
+ results.append("[OK] MCP 서버: 이미 등록됨")
554
+ else:
555
+ # 등록
556
+ add_result = subprocess.run(
557
+ ["claude", "mcp", "add", "clouvel", "-s", "user", "--", "clouvel"],
558
+ capture_output=True,
559
+ text=True,
560
+ timeout=30
561
+ )
562
+
563
+ if add_result.returncode == 0:
564
+ results.append("[OK] MCP 서버: 등록 완료")
565
+ else:
566
+ results.append(f"[WARN] MCP 서버: 등록 실패 - {add_result.stderr.strip()}")
567
+ results.append(" 수동 등록: claude mcp add clouvel -s user -- clouvel")
568
+ except FileNotFoundError:
569
+ results.append("[WARN] MCP 서버: claude 명령어 없음")
570
+ results.append(" Claude Code 설치 후 다시 실행하세요")
571
+ except subprocess.TimeoutExpired:
572
+ results.append("[WARN] MCP 서버: 타임아웃")
573
+ results.append(" 수동 등록: claude mcp add clouvel -s user -- clouvel")
574
+ except Exception as e:
575
+ results.append(f"[WARN] MCP 서버: 오류 - {str(e)}")
576
+ results.append(" 수동 등록: claude mcp add clouvel -s user -- clouvel")
1103
577
 
1104
- ### 4. 확인
578
+ # 결과 출력
579
+ output = """
580
+ ================================================================
581
+ Clouvel Setup 완료
582
+ ================================================================
1105
583
 
1106
- Claude에게 "clouvel 도구 목록 보여줘"라고 말하면 도구들이 보입니다.
1107
584
  """
585
+ output += "\n".join(results)
586
+ output += """
1108
587
 
1109
- code_guide = """## Claude Code (CLI) 설정
588
+ ----------------------------------------------------------------
1110
589
 
1111
- ### 1. 프로젝트 루트에 .mcp.json 생성
590
+ ## 작동 방식
1112
591
 
1113
- ```json
1114
- {
1115
- "mcpServers": {
1116
- "clouvel": {
1117
- "command": "uvx",
1118
- "args": ["clouvel"]
1119
- }
1120
- }
1121
- }
1122
- ```
592
+ 1. Claude Code 실행
593
+ 2. "로그인 기능 만들어줘" 요청
594
+ 3. Claude가 자동으로 can_code 먼저 호출
595
+ 4. PRD 없으면 → [BLOCK] BLOCK (코딩 금지)
596
+ 5. PRD 있으면 → [OK] PASS (코딩 진행)
1123
597
 
1124
- ### 2. Claude Code 실행
598
+ ## 테스트
1125
599
 
1126
600
  ```bash
601
+ # PRD 없는 폴더에서 테스트
602
+ mkdir test-project && cd test-project
1127
603
  claude
604
+ > "코드 짜줘"
605
+ # → BLOCK 메시지 확인
1128
606
  ```
1129
607
 
1130
- ### 3. 확인
1131
-
1132
- ```
1133
- > clouvel 도구 목록 보여줘
1134
- ```
608
+ ----------------------------------------------------------------
1135
609
  """
1136
610
 
1137
- vscode_guide = """## VS Code 설정
1138
-
1139
- ### 1. 확장 설치
1140
-
1141
- 1. VS Code 열기
1142
- 2. 확장(Extensions) 탭 열기 (Ctrl+Shift+X)
1143
- 3. "Clouvel" 검색
1144
- 4. "Clouvel" 확장 설치 (whitening.clouvel)
1145
-
1146
- ### 2. MCP 서버 설정
1147
-
1148
- 1. 명령 팔레트 열기 (Ctrl+Shift+P)
1149
- 2. "Clouvel: MCP 서버 설정" 선택
1150
- 3. Claude Desktop 또는 Claude Code 선택
1151
-
1152
- ### 3. 사이드바에서 문서 상태 확인
1153
-
1154
- 왼쪽 사이드바에 Clouvel 아이콘이 생깁니다.
1155
- 문서 상태를 실시간으로 확인할 수 있습니다.
1156
- """
1157
-
1158
- cursor_guide = """## Cursor 설정
1159
-
1160
- ### 1. 확장 설치
1161
-
1162
- 1. Cursor 열기
1163
- 2. 확장(Extensions) 탭 열기 (Ctrl+Shift+X)
1164
- 3. "Clouvel" 검색
1165
- 4. "Clouvel for Cursor" 확장 설치 (whitening.clouvel-cursor)
1166
-
1167
- ### 2. MCP 서버 설정
1168
-
1169
- 1. 명령 팔레트 열기 (Ctrl+Shift+P)
1170
- 2. "Clouvel: MCP 서버 설정" 선택
1171
- 3. Claude Desktop 또는 Claude Code 선택
1172
-
1173
- ### 3. 사이드바에서 문서 상태 확인
1174
-
1175
- 왼쪽 사이드바에 Clouvel 아이콘이 생깁니다.
1176
- """
1177
-
1178
- guides = {
1179
- "desktop": desktop_guide,
1180
- "code": code_guide,
1181
- "vscode": vscode_guide,
1182
- "cursor": cursor_guide,
1183
- }
1184
-
1185
- if platform == "all":
1186
- result = "# Clouvel 설치/설정 가이드\n\n"
1187
- result += desktop_guide + "\n---\n\n"
1188
- result += code_guide + "\n---\n\n"
1189
- result += vscode_guide + "\n---\n\n"
1190
- result += cursor_guide
1191
- else:
1192
- result = guides.get(platform, "알 수 없는 플랫폼입니다.")
1193
-
1194
- return [TextContent(type="text", text=result)]
1195
-
1196
-
1197
- async def run_server():
1198
- async with stdio_server() as (read_stream, write_stream):
1199
- await server.run(read_stream, write_stream, server.create_initialization_options())
611
+ return output
1200
612
 
1201
613
 
1202
614
  def main():
615
+ import sys
1203
616
  import asyncio
1204
- asyncio.run(run_server())
617
+ import argparse
618
+ from pathlib import Path
619
+
620
+ parser = argparse.ArgumentParser(description="Clouvel - 바이브코딩 프로세스 강제 도구")
621
+ subparsers = parser.add_subparsers(dest="command")
622
+
623
+ # init 명령
624
+ init_parser = subparsers.add_parser("init", help="프로젝트 초기화")
625
+ init_parser.add_argument("-p", "--path", default=".", help="프로젝트 경로")
626
+ init_parser.add_argument("-l", "--level", choices=["remind", "strict", "full"], default="strict")
627
+
628
+ # setup 명령 (B0)
629
+ setup_parser = subparsers.add_parser("setup", help="Clouvel 강제 호출 메커니즘 설치 (글로벌)")
630
+ setup_parser.add_argument("--global-only", action="store_true", help="CLAUDE.md만 설정 (MCP 등록 제외)")
631
+
632
+ args = parser.parse_args()
633
+
634
+ if args.command == "init":
635
+ from .tools.setup import setup_cli as sync_setup
636
+ import asyncio
637
+ result = asyncio.run(sync_setup(args.path, args.level))
638
+ print(result[0].text)
639
+ elif args.command == "setup":
640
+ result = _run_setup(global_only=args.global_only if hasattr(args, 'global_only') else False)
641
+ print(result)
642
+ else:
643
+ asyncio.run(run_server())
1205
644
 
1206
645
 
1207
646
  if __name__ == "__main__":