cc-skills-usage 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +54 -0
- package/dist/cli.js +298 -0
- package/package.json +19 -0
package/README.md
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# cc-skills-usage
|
|
2
|
+
|
|
3
|
+
Claude Code のスキル利用状況を分析・可視化する CLI ツール。
|
|
4
|
+
|
|
5
|
+
`~/.claude/projects/` に保存された JSONL 形式の会話履歴をスキャンし、スキルの呼び出し回数・プロジェクト別利用状況・トークン消費量・日別トレンドなどを集計します。
|
|
6
|
+
|
|
7
|
+
## 必要環境
|
|
8
|
+
|
|
9
|
+
- [Bun](https://bun.sh/)
|
|
10
|
+
|
|
11
|
+
## 使い方
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
bun src/index.ts
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
### オプション
|
|
18
|
+
|
|
19
|
+
| フラグ | 短縮 | 説明 |
|
|
20
|
+
|---|---|---|
|
|
21
|
+
| `--output <mode>` | `-o` | 出力モード: `terminal`(デフォルト)または `web` |
|
|
22
|
+
| `--from <date>` | | 開始日フィルタ(YYYY-MM-DD) |
|
|
23
|
+
| `--to <date>` | | 終了日フィルタ(YYYY-MM-DD) |
|
|
24
|
+
| `--project <name>` | `-p` | プロジェクト名の部分一致フィルタ |
|
|
25
|
+
| `--skill <name>` | `-s` | スキル名フィルタ |
|
|
26
|
+
| `--port <number>` | | Web サーバーのポート(デフォルト: 3939) |
|
|
27
|
+
| `--claude-dir <path>` | | `~/.claude` のパスを上書き |
|
|
28
|
+
| `--limit <number>` | `-n` | 直近の呼び出し表示件数(デフォルト: 50) |
|
|
29
|
+
| `--help` | `-h` | ヘルプを表示 |
|
|
30
|
+
|
|
31
|
+
### 例
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
# ターミナルで表示
|
|
35
|
+
bun src/index.ts
|
|
36
|
+
|
|
37
|
+
# Web ダッシュボードで表示(ブラウザが自動で開きます)
|
|
38
|
+
bun src/index.ts --output web
|
|
39
|
+
|
|
40
|
+
# 日付範囲とスキル名で絞り込み
|
|
41
|
+
bun src/index.ts --from 2025-06-01 --to 2025-06-30 --skill review-pr
|
|
42
|
+
|
|
43
|
+
# 特定プロジェクトの利用状況を確認
|
|
44
|
+
bun src/index.ts --project my-app
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## スキル検出
|
|
48
|
+
|
|
49
|
+
以下の 2 つの方法でスキル呼び出しを検出します:
|
|
50
|
+
|
|
51
|
+
1. **Skill tool_use** — アシスタントメッセージ内の `tool_use` ブロック(`name: "Skill"`)
|
|
52
|
+
2. **スラッシュコマンド** — ユーザーメッセージ内の `<command-name>` タグ(例: `/devg`, `/review-pr`)
|
|
53
|
+
|
|
54
|
+
ビルトイン CLI コマンド(`/help`, `/clear` など)は除外されます。同一スキルが両方の方法で検出された場合は重複排除されます。
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// @bun
|
|
3
|
+
var G=Object.defineProperty;var Q=(t,e)=>{for(var s in e)G(t,s,{get:e[s],enumerable:!0,configurable:!0,set:(o)=>e[s]=()=>o})};var _=(t,e)=>()=>(t&&(e=t(t=0)),e);function Y(t){return`<!DOCTYPE html>
|
|
4
|
+
<html lang="en">
|
|
5
|
+
<head>
|
|
6
|
+
<meta charset="utf-8">
|
|
7
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
8
|
+
<title>cc-skills-usage Dashboard</title>
|
|
9
|
+
<script src="https://cdn.jsdelivr.net/npm/chart.js@4"></script>
|
|
10
|
+
<style>
|
|
11
|
+
:root {
|
|
12
|
+
--bg: #0d1117;
|
|
13
|
+
--bg2: #161b22;
|
|
14
|
+
--bg3: #21262d;
|
|
15
|
+
--border: #30363d;
|
|
16
|
+
--text: #e6edf3;
|
|
17
|
+
--text2: #8b949e;
|
|
18
|
+
--accent: #58a6ff;
|
|
19
|
+
--accent2: #3fb950;
|
|
20
|
+
--accent3: #d2a8ff;
|
|
21
|
+
--accent4: #f78166;
|
|
22
|
+
--accent5: #79c0ff;
|
|
23
|
+
}
|
|
24
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
25
|
+
body {
|
|
26
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
|
|
27
|
+
background: var(--bg);
|
|
28
|
+
color: var(--text);
|
|
29
|
+
line-height: 1.5;
|
|
30
|
+
padding: 24px;
|
|
31
|
+
}
|
|
32
|
+
h1 { font-size: 24px; margin-bottom: 8px; }
|
|
33
|
+
h2 { font-size: 18px; color: var(--accent); margin-bottom: 12px; }
|
|
34
|
+
.subtitle { color: var(--text2); margin-bottom: 24px; }
|
|
35
|
+
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 16px; margin-bottom: 24px; }
|
|
36
|
+
.card {
|
|
37
|
+
background: var(--bg2);
|
|
38
|
+
border: 1px solid var(--border);
|
|
39
|
+
border-radius: 8px;
|
|
40
|
+
padding: 20px;
|
|
41
|
+
}
|
|
42
|
+
.card-full { grid-column: 1 / -1; }
|
|
43
|
+
.stat-value { font-size: 36px; font-weight: 700; color: var(--accent); }
|
|
44
|
+
.stat-label { color: var(--text2); font-size: 14px; }
|
|
45
|
+
.chart-container { position: relative; height: 300px; }
|
|
46
|
+
table { width: 100%; border-collapse: collapse; font-size: 14px; }
|
|
47
|
+
th { text-align: left; padding: 8px 12px; border-bottom: 2px solid var(--border); color: var(--text2); font-weight: 600; }
|
|
48
|
+
td { padding: 8px 12px; border-bottom: 1px solid var(--border); }
|
|
49
|
+
tr:hover td { background: var(--bg3); }
|
|
50
|
+
.tag {
|
|
51
|
+
display: inline-block;
|
|
52
|
+
padding: 2px 8px;
|
|
53
|
+
border-radius: 12px;
|
|
54
|
+
font-size: 12px;
|
|
55
|
+
background: var(--bg3);
|
|
56
|
+
color: var(--accent);
|
|
57
|
+
margin: 2px;
|
|
58
|
+
}
|
|
59
|
+
.unused-tag {
|
|
60
|
+
background: rgba(247, 129, 102, 0.15);
|
|
61
|
+
color: var(--accent4);
|
|
62
|
+
}
|
|
63
|
+
.filter-bar {
|
|
64
|
+
display: flex;
|
|
65
|
+
gap: 12px;
|
|
66
|
+
margin-bottom: 16px;
|
|
67
|
+
flex-wrap: wrap;
|
|
68
|
+
}
|
|
69
|
+
.filter-bar input, .filter-bar select {
|
|
70
|
+
background: var(--bg3);
|
|
71
|
+
border: 1px solid var(--border);
|
|
72
|
+
border-radius: 6px;
|
|
73
|
+
padding: 6px 12px;
|
|
74
|
+
color: var(--text);
|
|
75
|
+
font-size: 14px;
|
|
76
|
+
}
|
|
77
|
+
.filter-bar input:focus, .filter-bar select:focus {
|
|
78
|
+
outline: none;
|
|
79
|
+
border-color: var(--accent);
|
|
80
|
+
}
|
|
81
|
+
.num { text-align: right; font-variant-numeric: tabular-nums; }
|
|
82
|
+
.dim { color: var(--text2); }
|
|
83
|
+
.truncate { max-width: 300px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
84
|
+
</style>
|
|
85
|
+
</head>
|
|
86
|
+
<body>
|
|
87
|
+
<h1>cc-skills-usage</h1>
|
|
88
|
+
<p class="subtitle">Claude Code Skill Usage Dashboard</p>
|
|
89
|
+
|
|
90
|
+
<div class="grid">
|
|
91
|
+
<div class="card">
|
|
92
|
+
<div class="stat-label">Total Calls</div>
|
|
93
|
+
<div class="stat-value" id="totalCalls"></div>
|
|
94
|
+
</div>
|
|
95
|
+
<div class="card">
|
|
96
|
+
<div class="stat-label">Unique Skills</div>
|
|
97
|
+
<div class="stat-value" id="uniqueSkills"></div>
|
|
98
|
+
</div>
|
|
99
|
+
<div class="card">
|
|
100
|
+
<div class="stat-label">Projects</div>
|
|
101
|
+
<div class="stat-value" id="projectCount"></div>
|
|
102
|
+
</div>
|
|
103
|
+
<div class="card">
|
|
104
|
+
<div class="stat-label">Period</div>
|
|
105
|
+
<div class="stat-value" id="period" style="font-size:20px"></div>
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
|
|
109
|
+
<div class="grid">
|
|
110
|
+
<div class="card">
|
|
111
|
+
<h2>Skill Usage</h2>
|
|
112
|
+
<div class="chart-container"><canvas id="skillChart"></canvas></div>
|
|
113
|
+
</div>
|
|
114
|
+
<div class="card">
|
|
115
|
+
<h2>Token Usage</h2>
|
|
116
|
+
<div class="chart-container"><canvas id="tokenChart"></canvas></div>
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
|
|
120
|
+
<div class="grid">
|
|
121
|
+
<div class="card card-full">
|
|
122
|
+
<h2>Daily Timeline</h2>
|
|
123
|
+
<div class="chart-container" style="height:250px"><canvas id="dailyChart"></canvas></div>
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
|
|
127
|
+
<div class="grid">
|
|
128
|
+
<div class="card">
|
|
129
|
+
<h2>Project Breakdown</h2>
|
|
130
|
+
<table>
|
|
131
|
+
<thead><tr><th>Project</th><th class="num">Calls</th><th>Skills</th></tr></thead>
|
|
132
|
+
<tbody id="projectTable"></tbody>
|
|
133
|
+
</table>
|
|
134
|
+
</div>
|
|
135
|
+
<div class="card">
|
|
136
|
+
<h2>Unused Skills</h2>
|
|
137
|
+
<div id="unusedSkills"></div>
|
|
138
|
+
</div>
|
|
139
|
+
</div>
|
|
140
|
+
|
|
141
|
+
<div class="grid">
|
|
142
|
+
<div class="card card-full">
|
|
143
|
+
<h2>Recent Calls</h2>
|
|
144
|
+
<div class="filter-bar">
|
|
145
|
+
<input id="filterText" type="text" placeholder="Filter by skill or project...">
|
|
146
|
+
</div>
|
|
147
|
+
<table>
|
|
148
|
+
<thead><tr><th>Time</th><th>Skill</th><th>Project</th><th>Args</th><th>Trigger</th></tr></thead>
|
|
149
|
+
<tbody id="recentTable"></tbody>
|
|
150
|
+
</table>
|
|
151
|
+
</div>
|
|
152
|
+
</div>
|
|
153
|
+
|
|
154
|
+
<script>
|
|
155
|
+
window.__DATA__ = ${JSON.stringify(t)};
|
|
156
|
+
|
|
157
|
+
const D = window.__DATA__;
|
|
158
|
+
const COLORS = ['#58a6ff','#3fb950','#d2a8ff','#f78166','#79c0ff','#ffa657','#ff7b72','#7ee787','#a5d6ff','#d5a5ff'];
|
|
159
|
+
|
|
160
|
+
// Summary
|
|
161
|
+
document.getElementById('totalCalls').textContent = D.totalCalls.toLocaleString();
|
|
162
|
+
document.getElementById('uniqueSkills').textContent = D.skillStats.length;
|
|
163
|
+
document.getElementById('projectCount').textContent = D.projectStats.length;
|
|
164
|
+
document.getElementById('period').textContent = (D.dateRange.from || 'N/A') + ' \u2192 ' + (D.dateRange.to || 'N/A');
|
|
165
|
+
|
|
166
|
+
// Skill chart
|
|
167
|
+
new Chart(document.getElementById('skillChart'), {
|
|
168
|
+
type: 'bar',
|
|
169
|
+
data: {
|
|
170
|
+
labels: D.skillStats.map(s => s.name),
|
|
171
|
+
datasets: [{
|
|
172
|
+
label: 'Calls',
|
|
173
|
+
data: D.skillStats.map(s => s.count),
|
|
174
|
+
backgroundColor: D.skillStats.map((_, i) => COLORS[i % COLORS.length]),
|
|
175
|
+
borderRadius: 4,
|
|
176
|
+
}]
|
|
177
|
+
},
|
|
178
|
+
options: {
|
|
179
|
+
indexAxis: 'y',
|
|
180
|
+
responsive: true,
|
|
181
|
+
maintainAspectRatio: false,
|
|
182
|
+
plugins: { legend: { display: false } },
|
|
183
|
+
scales: {
|
|
184
|
+
x: { grid: { color: '#30363d' }, ticks: { color: '#8b949e' } },
|
|
185
|
+
y: { grid: { display: false }, ticks: { color: '#e6edf3' } },
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// Token chart
|
|
191
|
+
if (D.tokenStats.length > 0) {
|
|
192
|
+
new Chart(document.getElementById('tokenChart'), {
|
|
193
|
+
type: 'bar',
|
|
194
|
+
data: {
|
|
195
|
+
labels: D.tokenStats.map(t => t.skillName),
|
|
196
|
+
datasets: [
|
|
197
|
+
{ label: 'Input', data: D.tokenStats.map(t => t.inputTokens), backgroundColor: '#58a6ff', borderRadius: 4 },
|
|
198
|
+
{ label: 'Output', data: D.tokenStats.map(t => t.outputTokens), backgroundColor: '#3fb950', borderRadius: 4 },
|
|
199
|
+
{ label: 'Cache Create', data: D.tokenStats.map(t => t.cacheCreateTokens), backgroundColor: '#d2a8ff', borderRadius: 4 },
|
|
200
|
+
{ label: 'Cache Read', data: D.tokenStats.map(t => t.cacheReadTokens), backgroundColor: '#ffa657', borderRadius: 4 },
|
|
201
|
+
]
|
|
202
|
+
},
|
|
203
|
+
options: {
|
|
204
|
+
responsive: true,
|
|
205
|
+
maintainAspectRatio: false,
|
|
206
|
+
plugins: { legend: { labels: { color: '#8b949e' } } },
|
|
207
|
+
scales: {
|
|
208
|
+
x: { stacked: true, grid: { display: false }, ticks: { color: '#e6edf3' } },
|
|
209
|
+
y: { stacked: true, grid: { color: '#30363d' }, ticks: { color: '#8b949e' } },
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Daily chart
|
|
216
|
+
if (D.dailyStats.length > 0) {
|
|
217
|
+
const allSkills = [...new Set(D.dailyStats.flatMap(d => Object.keys(d.skills)))];
|
|
218
|
+
new Chart(document.getElementById('dailyChart'), {
|
|
219
|
+
type: 'bar',
|
|
220
|
+
data: {
|
|
221
|
+
labels: D.dailyStats.map(d => d.date),
|
|
222
|
+
datasets: allSkills.map((skill, i) => ({
|
|
223
|
+
label: skill,
|
|
224
|
+
data: D.dailyStats.map(d => d.skills[skill] || 0),
|
|
225
|
+
backgroundColor: COLORS[i % COLORS.length],
|
|
226
|
+
borderRadius: 2,
|
|
227
|
+
}))
|
|
228
|
+
},
|
|
229
|
+
options: {
|
|
230
|
+
responsive: true,
|
|
231
|
+
maintainAspectRatio: false,
|
|
232
|
+
plugins: { legend: { labels: { color: '#8b949e' } } },
|
|
233
|
+
scales: {
|
|
234
|
+
x: { stacked: true, grid: { display: false }, ticks: { color: '#8b949e' } },
|
|
235
|
+
y: { stacked: true, grid: { color: '#30363d' }, ticks: { color: '#8b949e' } },
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Project table
|
|
242
|
+
const ptBody = document.getElementById('projectTable');
|
|
243
|
+
D.projectStats.forEach(p => {
|
|
244
|
+
const tags = p.skills.map(s => '<span class="tag">' + s.name + ':' + s.count + '</span>').join('');
|
|
245
|
+
ptBody.innerHTML += '<tr><td>' + p.projectName + '</td><td class="num">' + p.totalCalls + '</td><td>' + tags + '</td></tr>';
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
// Unused skills
|
|
249
|
+
const unusedDiv = document.getElementById('unusedSkills');
|
|
250
|
+
if (D.unusedSkills.length === 0) {
|
|
251
|
+
unusedDiv.innerHTML = '<p class="dim">All registered skills have been used.</p>';
|
|
252
|
+
} else {
|
|
253
|
+
unusedDiv.innerHTML = D.unusedSkills.map(s => '<span class="tag unused-tag">' + s + '</span>').join(' ');
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Recent calls table
|
|
257
|
+
function renderRecent(filter) {
|
|
258
|
+
const tbody = document.getElementById('recentTable');
|
|
259
|
+
const lc = (filter || '').toLowerCase();
|
|
260
|
+
const rows = D.recentCalls.filter(c =>
|
|
261
|
+
!lc || c.skillName.toLowerCase().includes(lc) || c.projectPath.toLowerCase().includes(lc)
|
|
262
|
+
);
|
|
263
|
+
tbody.innerHTML = rows.map(c => {
|
|
264
|
+
const ts = c.timestamp.replace('T', ' ').slice(0, 19);
|
|
265
|
+
const args = c.args ? '<span class="dim">' + escHtml(c.args.slice(0, 80)) + '</span>' : '';
|
|
266
|
+
const trigger = c.triggerMessage ? '<span class="dim">' + escHtml(c.triggerMessage.slice(0, 100)) + '</span>' : '';
|
|
267
|
+
return '<tr><td class="dim">' + ts + '</td><td>' + c.skillName + '</td><td>' + c.projectPath + '</td><td class="truncate">' + args + '</td><td class="truncate">' + trigger + '</td></tr>';
|
|
268
|
+
}).join('');
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function escHtml(s) {
|
|
272
|
+
return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
renderRecent('');
|
|
276
|
+
document.getElementById('filterText').addEventListener('input', e => renderRecent(e.target.value));
|
|
277
|
+
</script>
|
|
278
|
+
</body>
|
|
279
|
+
</html>`}import{exec as lt}from"child_process";async function $(t,e){let s=Y(t),o=JSON.stringify(t),n=`http://localhost:${Bun.serve({port:e,fetch(r){if(new URL(r.url).pathname==="/api/data")return new Response(o,{headers:{"Content-Type":"application/json"}});return new Response(s,{headers:{"Content-Type":"text/html; charset=utf-8"}})}}).port}`;console.log(`
|
|
280
|
+
Dashboard running at: \x1B[1;36m${n}\x1B[0m`),console.log(` Press Ctrl+C to stop
|
|
281
|
+
`),lt(`open "${n}"`)}var q=()=>{};var U={};Q(U,{renderWeb:()=>$});var W=_(()=>{q()});import{homedir as nt}from"os";import{join as rt}from"path";import{parseArgs as dt}from"util";import{readdir as V,stat as X}from"fs/promises";import{join as B}from"path";async function A(t){let e=B(t,"skills");try{let s=await V(e),o=[];for(let l of s){if(l.startsWith("."))continue;let n=B(e,l);try{if((await X(n)).isDirectory())o.push({name:l,dirPath:n})}catch{}}return o}catch{return[]}}import{readdir as Z}from"fs/promises";import{join as M,basename as I}from"path";import{execSync as F}from"child_process";var K=new Set(["/clear","/compact","/config","/cost","/doctor","/help","/init","/login","/logout","/memory","/model","/permissions","/plugin","/resume","/skills","/status","/terminal-setup","/vim"]);function H(t){let e=t.indexOf("/Documents/");if(e!==-1)return t.slice(e+11);return t}function tt(t,e){let s=e;for(let o=0;o<20&&s;o++){let l=t.get(s);if(!l)break;if(l.type==="user"&&l.message){let n=l.message.content;if(typeof n==="string"&&n.length>0)return n.length>200?n.slice(0,200)+"\u2026":n}s=l.parentUuid}return}function et(t){let e=t.message?.usage;if(!e)return;return{input_tokens:e.input_tokens??0,output_tokens:e.output_tokens??0,cache_creation_input_tokens:e.cache_creation_input_tokens??0,cache_read_input_tokens:e.cache_read_input_tokens??0}}async function E(t){let e=M(t,"projects"),s=[],o;try{o=(await Z(e,{withFileTypes:!0})).filter((r)=>r.isDirectory()).map((r)=>M(e,r.name))}catch{return[]}let l=new Set;try{let n=F(`grep -rl -e '"name":"Skill"' -e '<command-name>' ${e}/*/*.jsonl 2>/dev/null`,{encoding:"utf-8",maxBuffer:10485760});for(let r of n.trim().split(`
|
|
282
|
+
`))if(r)l.add(r)}catch{}if(l.size===0)return[];for(let n of l){let r=await at(n);s.push(...r)}return s.sort((n,r)=>new Date(r.timestamp).getTime()-new Date(n.timestamp).getTime()),s}async function at(t){let e=[],s=new Map,o=new Set,l=I(M(t,"..")),n=I(t).replace(".jsonl",""),T=await Bun.file(t).text();for(let c of T.split(`
|
|
283
|
+
`)){if(!c)continue;try{let u=JSON.parse(c);if(!u.uuid)continue;let d={uuid:u.uuid,parentUuid:u.parentUuid??null,type:u.type,timestamp:u.timestamp,cwd:u.cwd,sessionId:u.sessionId,message:u.message,toolUseResult:u.toolUseResult};if(s.set(d.uuid,d),d.type==="assistant"&&d.message?.content){let g=d.message.content;if(!Array.isArray(g))continue;for(let m of g)if(typeof m==="object"&&m!==null&&m.type==="tool_use"&&m.name==="Skill"){let b=m.input;if(!b?.skill)continue;let a=d.cwd??"",i={skillName:b.skill,args:b.args,timestamp:d.timestamp,sessionId:d.sessionId??n,projectDir:l,projectPath:H(a),cwd:a,triggerMessage:tt(s,d.parentUuid),usage:et(d)};e.push(i)}}if(d.type==="user"&&d.message?.content){let g=d.message.content,m=[];if(typeof g==="string")m.push(g);else if(Array.isArray(g)){for(let b of g)if(typeof b==="object"&&b!==null&&typeof b.text==="string")m.push(b.text)}for(let b of m){let a=b.match(/<command-name>(\/[^<]+)<\/command-name>/);if(!a)continue;let i=a[1];if(K.has(i))continue;let p=i.slice(1);if(o.has(`${d.uuid}:${p}`))continue;o.add(`${d.uuid}:${p}`);let f;for(let J of m){let L=J.match(/<command-message>(.*?)<\/command-message>/s);if(L&&L[1]!==p)f=L[1].trim()||void 0}let k=d.cwd??"",w={skillName:p,args:f,timestamp:d.timestamp,sessionId:d.sessionId??n,projectDir:l,projectPath:H(k),cwd:k,triggerMessage:`/${p}${f?" "+f:""}`};e.push(w)}}}catch{}}s.clear();let h=[],j=e.filter((c)=>!c.triggerMessage?.startsWith("/")),v=e.filter((c)=>c.triggerMessage?.startsWith("/")),O=new Set(v.map((c)=>`${c.sessionId}:${c.skillName}`));for(let c of j){let u=`${c.sessionId}:${c.skillName}`;if(O.has(u))continue;h.push(c)}return h.push(...v),h}function x(t){return t.slice(0,10)}function z(t,e,s){let o=t;if(s.from)o=o.filter((a)=>x(a.timestamp)>=s.from);if(s.to)o=o.filter((a)=>x(a.timestamp)<=s.to);if(s.project){let a=s.project.toLowerCase();o=o.filter((i)=>i.projectPath.toLowerCase().includes(a)||i.projectDir.toLowerCase().includes(a))}if(s.skill){let a=s.skill.toLowerCase();o=o.filter((i)=>i.skillName.toLowerCase().includes(a))}let l=new Map;for(let a of o)l.set(a.skillName,(l.get(a.skillName)??0)+1);let n=[...l.entries()].map(([a,i])=>({name:a,count:i})).sort((a,i)=>i.count-a.count),r=new Map;for(let a of o){if(!r.has(a.projectPath))r.set(a.projectPath,new Map);let i=r.get(a.projectPath);i.set(a.skillName,(i.get(a.skillName)??0)+1)}let T=[...r.entries()].map(([a,i])=>{let p=[...i.entries()].map(([f,k])=>({name:f,count:k})).sort((f,k)=>k.count-f.count);return{projectName:a,skills:p,totalCalls:p.reduce((f,k)=>f+k.count,0)}}).sort((a,i)=>i.totalCalls-a.totalCalls),h=new Map;for(let a of o){let i=x(a.timestamp);if(!h.has(i))h.set(i,new Map);let p=h.get(i);p.set(a.skillName,(p.get(a.skillName)??0)+1)}let j=[...h.entries()].map(([a,i])=>{let p={},f=0;for(let[k,w]of i)p[k]=w,f+=w;return{date:a,skills:p,total:f}}).sort((a,i)=>a.date.localeCompare(i.date)),v=new Map;for(let a of o){if(!a.usage)continue;if(!v.has(a.skillName))v.set(a.skillName,{inputTokens:0,outputTokens:0,cacheCreateTokens:0,cacheReadTokens:0,callCount:0});let i=v.get(a.skillName);i.inputTokens+=a.usage.input_tokens??0,i.outputTokens+=a.usage.output_tokens??0,i.cacheCreateTokens+=a.usage.cache_creation_input_tokens??0,i.cacheReadTokens+=a.usage.cache_read_input_tokens??0,i.callCount+=1}let O=[...v.entries()].map(([a,i])=>({skillName:a,...i})).sort((a,i)=>i.inputTokens+i.outputTokens-(a.inputTokens+a.outputTokens)),c=new Set(o.map((a)=>a.skillName)),u=e.map((a)=>a.name).filter((a)=>!c.has(a)).sort(),d=o.slice(0,s.limit),g=o.map((a)=>a.timestamp),m=g.length?x(g[g.length-1]):"",b=g.length?x(g[0]):"";return{totalCalls:o.length,skillStats:n,projectStats:T,dailyStats:j,tokenStats:O,unusedSkills:u,recentCalls:d,dateRange:{from:m,to:b}}}var st=["\u258F","\u258E","\u258D","\u258C","\u258B","\u258A","\u2589","\u2588"];function N(t,e){if(e===0)return"";let s=t/e,o=Math.floor(s*30),l=(s*30-o)*8,n=l>0?st[Math.floor(l)-1]??"":"";return"\u2588".repeat(o)+n}function y(t){return t.toLocaleString()}function S(t){console.log(),console.log(`\x1B[1;36m\u2500\u2500 ${t} \u2500\u2500\x1B[0m`),console.log()}function C(t){return`\x1B[2m${t}\x1B[0m`}function R(t){return`\x1B[1m${t}\x1B[0m`}function D(t){return`\x1B[36m${t}\x1B[0m`}function ot(t){return`\x1B[33m${t}\x1B[0m`}function P(t){if(S("Summary"),console.log(` Total skill calls: ${R(y(t.totalCalls))} Period: ${D(t.dateRange.from||"N/A")} \u2192 ${D(t.dateRange.to||"N/A")}`),console.log(` Unique skills used: ${R(String(t.skillStats.length))} Projects: ${R(String(t.projectStats.length))}`),t.skillStats.length>0){S("Skill Usage");let e=t.skillStats[0].count,s=Math.max(...t.skillStats.map((o)=>o.name.length),10);for(let o of t.skillStats){let l=o.name.padEnd(s),n=String(o.count).padStart(4),r=N(o.count,e);console.log(` ${D(l)} ${n} ${r}`)}}if(t.projectStats.length>0){S("Project Breakdown");for(let e of t.projectStats){console.log(` ${R(e.projectName)} (${e.totalCalls} calls)`);for(let s of e.skills)console.log(` ${s.name.padEnd(25)} ${String(s.count).padStart(4)}`)}}if(t.dailyStats.length>0){S("Daily Timeline");let e=Math.max(...t.dailyStats.map((s)=>s.total));for(let s of t.dailyStats){let o=Object.entries(s.skills).sort((n,r)=>r[1]-n[1]).map(([n,r])=>`${n}:${r}`).join(", "),l=N(s.total,e);console.log(` ${C(s.date)} ${String(s.total).padStart(3)} ${l} ${C(o)}`)}}if(t.tokenStats.length>0){S("Token Usage (per skill)");let e={name:Math.max(...t.tokenStats.map((s)=>s.skillName.length),10)};console.log(` ${"Skill".padEnd(e.name)} ${"Calls".padStart(5)} ${"Input".padStart(10)} ${"Output".padStart(10)} ${"Cache Create".padStart(12)} ${"Cache Read".padStart(12)}`),console.log(` ${"\u2500".repeat(e.name+5+10+10+12+12+10)}`);for(let s of t.tokenStats)console.log(` ${s.skillName.padEnd(e.name)} ${String(s.callCount).padStart(5)} ${y(s.inputTokens).padStart(10)} ${y(s.outputTokens).padStart(10)} ${y(s.cacheCreateTokens).padStart(12)} ${y(s.cacheReadTokens).padStart(12)}`)}if(t.unusedSkills.length>0){S("Unused Skills (registered but never called)");for(let e of t.unusedSkills)console.log(` ${ot("\u25CB")} ${e}`)}if(t.recentCalls.length>0){S(`Recent Calls (last ${t.recentCalls.length})`);for(let e of t.recentCalls){let s=e.timestamp.replace("T"," ").slice(0,19),o=e.args?C(` args="${e.args.slice(0,60)}"`):"",l=e.triggerMessage?C(` \u2190 "${e.triggerMessage.slice(0,80)}"`):"";console.log(` ${C(s)} ${D(e.skillName.padEnd(22))} ${e.projectPath}${o}${l}`)}}console.log()}function ct(){console.log(`
|
|
284
|
+
cc-skills-usage \u2014 Analyze Claude Code skill usage
|
|
285
|
+
|
|
286
|
+
Usage: bun src/index.ts [options]
|
|
287
|
+
|
|
288
|
+
Options:
|
|
289
|
+
--output, -o <mode> "terminal" (default) or "web"
|
|
290
|
+
--from <date> Start date filter (YYYY-MM-DD)
|
|
291
|
+
--to <date> End date filter (YYYY-MM-DD)
|
|
292
|
+
--project, -p <name> Project path partial match filter
|
|
293
|
+
--skill, -s <name> Skill name filter
|
|
294
|
+
--port <number> Web server port (default: 3939)
|
|
295
|
+
--claude-dir <path> Override ~/.claude location
|
|
296
|
+
--limit, -n <number> Number of recent calls to show (default: 50)
|
|
297
|
+
--help, -h Show this help
|
|
298
|
+
`)}function pt(){let{values:t}=dt({options:{output:{type:"string",short:"o",default:"terminal"},from:{type:"string"},to:{type:"string"},project:{type:"string",short:"p"},skill:{type:"string",short:"s"},port:{type:"string",default:"3939"},"claude-dir":{type:"string"},limit:{type:"string",short:"n",default:"50"},help:{type:"boolean",short:"h",default:!1}},strict:!0});if(t.help)ct(),process.exit(0);let e=t.output;if(e!=="terminal"&&e!=="web")console.error(`Invalid output mode: ${e}. Use "terminal" or "web".`),process.exit(1);return{output:e,from:t.from,to:t.to,project:t.project,skill:t.skill,port:parseInt(t.port,10),claudeDir:t["claude-dir"]??rt(nt(),".claude"),limit:parseInt(t.limit,10)}}async function ut(){let t=pt();console.log("\x1B[2mScanning skill calls...\x1B[0m");let[e,s]=await Promise.all([A(t.claudeDir),E(t.claudeDir)]);console.log(`\x1B[2mFound ${s.length} skill calls across ${new Set(s.map((l)=>l.sessionId)).size} sessions\x1B[0m`);let o=z(s,e,t);if(t.output==="terminal")P(o);else{let{renderWeb:l}=await Promise.resolve().then(() => (W(),U));await l(o,t.port)}}ut().catch((t)=>{console.error(t),process.exit(1)});
|
package/package.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "cc-skills-usage",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"bin": {
|
|
6
|
+
"cc-skills-usage": "./dist/cli.js"
|
|
7
|
+
},
|
|
8
|
+
"files": [
|
|
9
|
+
"dist"
|
|
10
|
+
],
|
|
11
|
+
"workspaces": [
|
|
12
|
+
"packages/*"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"start": "bun packages/cli/src/index.ts",
|
|
16
|
+
"dev": "bun --watch packages/cli/src/index.ts",
|
|
17
|
+
"build": "bun build packages/cli/src/index.ts --outfile dist/cli.js --target bun --minify"
|
|
18
|
+
}
|
|
19
|
+
}
|