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
clouvel/analytics.py
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Clouvel Analytics - 도구 사용량 로컬 추적
|
|
3
|
+
|
|
4
|
+
저장 위치: .clouvel/analytics.json (프로젝트 로컬)
|
|
5
|
+
개인정보 없음, 순수 사용량 통계만 기록
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from datetime import datetime, timedelta
|
|
11
|
+
from typing import Optional
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def get_analytics_path(project_path: Optional[str] = None) -> Path:
|
|
15
|
+
"""analytics.json 경로 반환"""
|
|
16
|
+
if project_path:
|
|
17
|
+
base = Path(project_path)
|
|
18
|
+
else:
|
|
19
|
+
base = Path.cwd()
|
|
20
|
+
|
|
21
|
+
clouvel_dir = base / ".clouvel"
|
|
22
|
+
clouvel_dir.mkdir(parents=True, exist_ok=True)
|
|
23
|
+
return clouvel_dir / "analytics.json"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def load_analytics(project_path: Optional[str] = None) -> dict:
|
|
27
|
+
"""analytics 데이터 로드"""
|
|
28
|
+
path = get_analytics_path(project_path)
|
|
29
|
+
if path.exists():
|
|
30
|
+
try:
|
|
31
|
+
return json.loads(path.read_text(encoding='utf-8'))
|
|
32
|
+
except (json.JSONDecodeError, IOError):
|
|
33
|
+
return {"events": [], "version": "1.0"}
|
|
34
|
+
return {"events": [], "version": "1.0"}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def save_analytics(data: dict, project_path: Optional[str] = None) -> None:
|
|
38
|
+
"""analytics 데이터 저장"""
|
|
39
|
+
path = get_analytics_path(project_path)
|
|
40
|
+
path.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding='utf-8')
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def log_tool_call(tool_name: str, success: bool = True, project_path: Optional[str] = None) -> None:
|
|
44
|
+
"""도구 호출 기록"""
|
|
45
|
+
data = load_analytics(project_path)
|
|
46
|
+
|
|
47
|
+
event = {
|
|
48
|
+
"tool": tool_name,
|
|
49
|
+
"ts": datetime.now().isoformat(),
|
|
50
|
+
"success": success
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
data["events"].append(event)
|
|
54
|
+
|
|
55
|
+
# 최근 1000개만 유지 (메모리 관리)
|
|
56
|
+
if len(data["events"]) > 1000:
|
|
57
|
+
data["events"] = data["events"][-1000:]
|
|
58
|
+
|
|
59
|
+
save_analytics(data, project_path)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def get_stats(project_path: Optional[str] = None, days: int = 30) -> dict:
|
|
63
|
+
"""사용량 통계 반환"""
|
|
64
|
+
data = load_analytics(project_path)
|
|
65
|
+
events = data.get("events", [])
|
|
66
|
+
|
|
67
|
+
if not events:
|
|
68
|
+
return {
|
|
69
|
+
"total_calls": 0,
|
|
70
|
+
"by_tool": {},
|
|
71
|
+
"by_date": {},
|
|
72
|
+
"success_rate": 0,
|
|
73
|
+
"period_days": days
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
# 기간 필터
|
|
77
|
+
cutoff = datetime.now() - timedelta(days=days)
|
|
78
|
+
filtered = []
|
|
79
|
+
for e in events:
|
|
80
|
+
try:
|
|
81
|
+
ts = datetime.fromisoformat(e["ts"])
|
|
82
|
+
if ts >= cutoff:
|
|
83
|
+
filtered.append(e)
|
|
84
|
+
except (KeyError, ValueError):
|
|
85
|
+
continue
|
|
86
|
+
|
|
87
|
+
# 도구별 집계
|
|
88
|
+
by_tool = {}
|
|
89
|
+
by_date = {}
|
|
90
|
+
success_count = 0
|
|
91
|
+
|
|
92
|
+
for e in filtered:
|
|
93
|
+
tool = e.get("tool", "unknown")
|
|
94
|
+
by_tool[tool] = by_tool.get(tool, 0) + 1
|
|
95
|
+
|
|
96
|
+
try:
|
|
97
|
+
date = datetime.fromisoformat(e["ts"]).strftime("%Y-%m-%d")
|
|
98
|
+
by_date[date] = by_date.get(date, 0) + 1
|
|
99
|
+
except (KeyError, ValueError):
|
|
100
|
+
pass
|
|
101
|
+
|
|
102
|
+
if e.get("success", True):
|
|
103
|
+
success_count += 1
|
|
104
|
+
|
|
105
|
+
total = len(filtered)
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
"total_calls": total,
|
|
109
|
+
"by_tool": dict(sorted(by_tool.items(), key=lambda x: x[1], reverse=True)),
|
|
110
|
+
"by_date": dict(sorted(by_date.items())),
|
|
111
|
+
"success_rate": round(success_count / total * 100, 1) if total > 0 else 0,
|
|
112
|
+
"period_days": days
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def format_stats(stats: dict) -> str:
|
|
117
|
+
"""통계를 읽기 좋은 문자열로 변환"""
|
|
118
|
+
lines = [
|
|
119
|
+
f"# Clouvel 사용량 통계 (최근 {stats['period_days']}일)",
|
|
120
|
+
"",
|
|
121
|
+
f"## 요약",
|
|
122
|
+
f"- 총 호출: {stats['total_calls']}회",
|
|
123
|
+
f"- 성공률: {stats['success_rate']}%",
|
|
124
|
+
"",
|
|
125
|
+
]
|
|
126
|
+
|
|
127
|
+
if stats["by_tool"]:
|
|
128
|
+
lines.append("## 도구별 사용량")
|
|
129
|
+
lines.append("")
|
|
130
|
+
lines.append("| 도구 | 횟수 | 비율 |")
|
|
131
|
+
lines.append("|------|------|------|")
|
|
132
|
+
total = stats["total_calls"]
|
|
133
|
+
for tool, count in stats["by_tool"].items():
|
|
134
|
+
pct = round(count / total * 100, 1) if total > 0 else 0
|
|
135
|
+
lines.append(f"| {tool} | {count} | {pct}% |")
|
|
136
|
+
lines.append("")
|
|
137
|
+
|
|
138
|
+
if stats["by_date"]:
|
|
139
|
+
lines.append("## 일별 사용량")
|
|
140
|
+
lines.append("")
|
|
141
|
+
# 최근 7일만 표시
|
|
142
|
+
recent_dates = list(stats["by_date"].items())[-7:]
|
|
143
|
+
for date, count in recent_dates:
|
|
144
|
+
bar = "█" * min(count, 20)
|
|
145
|
+
lines.append(f"- {date}: {bar} {count}")
|
|
146
|
+
lines.append("")
|
|
147
|
+
|
|
148
|
+
if stats["total_calls"] == 0:
|
|
149
|
+
lines.append("아직 기록된 사용량이 없습니다.")
|
|
150
|
+
|
|
151
|
+
return "\n".join(lines)
|