axel-setup 0.2.0
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/CHANGELOG.md +218 -0
- package/CONTRIBUTING.md +58 -0
- package/LICENSE +21 -0
- package/README.md +518 -0
- package/agents/api-design.md +51 -0
- package/agents/bughunter.md +136 -0
- package/agents/changelog.md +89 -0
- package/agents/cleanup.md +126 -0
- package/agents/compare-branch.md +35 -0
- package/agents/cross-repo.md +97 -0
- package/agents/db-check.md +14 -0
- package/agents/debug.md +47 -0
- package/agents/deploy-check.md +100 -0
- package/agents/draft-message.md +19 -0
- package/agents/excelsior-coordinator.md +75 -0
- package/agents/excelsior-verifier.md +94 -0
- package/agents/feature.md +48 -0
- package/agents/harness-optimizer.md +40 -0
- package/agents/incident.md +48 -0
- package/agents/linear-task.md +18 -0
- package/agents/onboard.md +24 -0
- package/agents/perf.md +44 -0
- package/agents/production-validator.md +96 -0
- package/agents/review.md +113 -0
- package/agents/security-check.md +29 -0
- package/agents/sprint-summary.md +15 -0
- package/agents/tdd-mainder.md +178 -0
- package/agents/test-gen.md +39 -0
- package/axel-manifest.json +129 -0
- package/bin/axel-setup.js +597 -0
- package/bootstrap.sh +1087 -0
- package/commands/create-pr.md +13 -0
- package/commands/daily.md +182 -0
- package/commands/deslop.md +13 -0
- package/commands/draft-message.md +23 -0
- package/commands/eod-review.md +154 -0
- package/commands/execute-prp.md +37 -0
- package/commands/generate-prp.md +75 -0
- package/commands/multi-repo-feature.md +60 -0
- package/commands/roadmap.md +31 -0
- package/commands/sprint-status.md +486 -0
- package/commands/style.md +68 -0
- package/commands/visualize.md +17 -0
- package/docs/roadmap/multi-runtime.md +73 -0
- package/docs/superpowers/plans/2026-06-12-setup-hardening-roadmap.md +61 -0
- package/hooks/desktop-notify.sh +26 -0
- package/hooks/enforce-agent-model.jq +14 -0
- package/hooks/gsd-context-monitor.js +156 -0
- package/hooks/linear-lifecycle-sync.sh +112 -0
- package/hooks/memory-dedup.sh +122 -0
- package/hooks/memory-extractor.sh +218 -0
- package/hooks/post-commit-memory-trigger.sh +16 -0
- package/hooks/post-commit-verify.sh +41 -0
- package/hooks/post-edit-lint.sh +43 -0
- package/hooks/precompact-save-context.sh +124 -0
- package/hooks/priority-map-staleness.sh +29 -0
- package/hooks/proactive-resolver.sh +104 -0
- package/hooks/session-auto-title.sh +165 -0
- package/hooks/session-checkpoint.sh +97 -0
- package/hooks/session-cost-log.sh +77 -0
- package/hooks/session-log-action.sh +36 -0
- package/hooks/session-log-prompt.sh +25 -0
- package/hooks/session-restore.sh +45 -0
- package/hooks/session-save.sh +81 -0
- package/hooks/session-summarize.sh +154 -0
- package/hooks/validate-commit-format.sh +38 -0
- package/hooks/weekly-priority-map-review.sh +143 -0
- package/install.sh +46 -0
- package/package.json +67 -0
- package/scripts/ci/bootstrap-dry-run.sh +40 -0
- package/scripts/ci/check.sh +65 -0
- package/scripts/posthog-snapshot-loader.sh +112 -0
- package/skills/context-budget/SKILL.md +86 -0
- package/skills/memory-review/SKILL.md +100 -0
- package/skills/model-routing/SKILL.md +70 -0
- package/skills/posthog-weekly/SKILL.md +271 -0
- package/skills/ui-ux-pro-max/SKILL.md +377 -0
- package/skills/ui-ux-pro-max/data/charts.csv +26 -0
- package/skills/ui-ux-pro-max/data/colors.csv +97 -0
- package/skills/ui-ux-pro-max/data/icons.csv +101 -0
- package/skills/ui-ux-pro-max/data/landing.csv +31 -0
- package/skills/ui-ux-pro-max/data/products.csv +97 -0
- package/skills/ui-ux-pro-max/data/react-performance.csv +45 -0
- package/skills/ui-ux-pro-max/data/stacks/astro.csv +54 -0
- package/skills/ui-ux-pro-max/data/stacks/flutter.csv +53 -0
- package/skills/ui-ux-pro-max/data/stacks/html-tailwind.csv +56 -0
- package/skills/ui-ux-pro-max/data/stacks/jetpack-compose.csv +53 -0
- package/skills/ui-ux-pro-max/data/stacks/nextjs.csv +53 -0
- package/skills/ui-ux-pro-max/data/stacks/nuxt-ui.csv +51 -0
- package/skills/ui-ux-pro-max/data/stacks/nuxtjs.csv +59 -0
- package/skills/ui-ux-pro-max/data/stacks/react-native.csv +52 -0
- package/skills/ui-ux-pro-max/data/stacks/react.csv +54 -0
- package/skills/ui-ux-pro-max/data/stacks/shadcn.csv +61 -0
- package/skills/ui-ux-pro-max/data/stacks/svelte.csv +54 -0
- package/skills/ui-ux-pro-max/data/stacks/swiftui.csv +51 -0
- package/skills/ui-ux-pro-max/data/stacks/vue.csv +50 -0
- package/skills/ui-ux-pro-max/data/styles.csv +68 -0
- package/skills/ui-ux-pro-max/data/typography.csv +58 -0
- package/skills/ui-ux-pro-max/data/ui-reasoning.csv +101 -0
- package/skills/ui-ux-pro-max/data/ux-guidelines.csv +100 -0
- package/skills/ui-ux-pro-max/data/web-interface.csv +31 -0
- package/skills/ui-ux-pro-max/scripts/core.py +253 -0
- package/skills/ui-ux-pro-max/scripts/design_system.py +1067 -0
- package/skills/ui-ux-pro-max/scripts/search.py +114 -0
- package/templates/AGENTS.runtime.md +17 -0
- package/templates/CLAUDE.md +252 -0
- package/templates/claude-monitor.plist +35 -0
- package/templates/keybindings.json +13 -0
- package/templates/merge-settings.jq +53 -0
- package/templates/review-upgrades.md +44 -0
- package/templates/settings.json +255 -0
- package/templates/statusline-command.sh +182 -0
- package/tests/fixtures/hooks/events.json +32 -0
- package/tools/session-costs-view.sh +128 -0
- package/tools/session-dashboard-gen.sh +369 -0
- package/tools/session-live.sh +173 -0
- package/tools/session-server.js +441 -0
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# session-dashboard-gen.sh
|
|
3
|
+
# Generates a self-contained HTML dashboard from session-costs.log
|
|
4
|
+
# Usage: bash ~/.claude/tools/session-dashboard-gen.sh [--open]
|
|
5
|
+
|
|
6
|
+
LOG_FILE="$HOME/.claude/session-costs.log"
|
|
7
|
+
OUT_FILE="$HOME/.claude/session-dashboard.html"
|
|
8
|
+
OPEN_AFTER="${1:-}"
|
|
9
|
+
|
|
10
|
+
if [ ! -f "$LOG_FILE" ]; then
|
|
11
|
+
echo "No hay datos todavía en $LOG_FILE"
|
|
12
|
+
exit 1
|
|
13
|
+
fi
|
|
14
|
+
|
|
15
|
+
# Convert CSV to JSON array
|
|
16
|
+
JSON_DATA=$(tail -n +2 "$LOG_FILE" | awk -F',' '
|
|
17
|
+
NF >= 10 {
|
|
18
|
+
gsub(/"/, "\\\"", $4)
|
|
19
|
+
gsub(/"/, "\\\"", $11)
|
|
20
|
+
printf "{\"date\":\"%s\",\"time\":\"%s\",\"session_id\":\"%s\",\"project\":\"%s\",\"cost\":%s,\"input_tokens\":%s,\"output_tokens\":%s,\"ctx_pct\":%s,\"five_h_end\":%s,\"five_h_delta\":%s,\"model\":\"%s\"}",
|
|
21
|
+
$1,$2,$3,$4,
|
|
22
|
+
($5+0),($6+0),($7+0),($8+0),($9+0),($10+0),
|
|
23
|
+
($11 == "" ? "unknown" : $11)
|
|
24
|
+
printf ","
|
|
25
|
+
}' | sed 's/,$//')
|
|
26
|
+
|
|
27
|
+
GENERATED_AT=$(date '+%Y-%m-%d %H:%M:%S')
|
|
28
|
+
|
|
29
|
+
cat > "$OUT_FILE" << HTMLEOF
|
|
30
|
+
<!DOCTYPE html>
|
|
31
|
+
<html lang="es">
|
|
32
|
+
<head>
|
|
33
|
+
<meta charset="UTF-8">
|
|
34
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
35
|
+
<title>Claude Code — Usage Dashboard</title>
|
|
36
|
+
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
|
37
|
+
<style>
|
|
38
|
+
:root {
|
|
39
|
+
--bg: #0d1117; --bg2: #161b22; --bg3: #21262d;
|
|
40
|
+
--border: #30363d; --text: #e6edf3; --muted: #7d8590;
|
|
41
|
+
--cyan: #39d3f7; --green: #3fb950; --yellow: #d29922;
|
|
42
|
+
--red: #f85149; --purple: #bc8cff; --orange: #f0883e;
|
|
43
|
+
--blue: #58a6ff;
|
|
44
|
+
}
|
|
45
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
46
|
+
body { background: var(--bg); color: var(--text); font-family: 'SF Mono', 'Fira Code', monospace; font-size: 13px; }
|
|
47
|
+
.header { background: var(--bg2); border-bottom: 1px solid var(--border); padding: 16px 24px; display: flex; justify-content: space-between; align-items: center; }
|
|
48
|
+
.header h1 { font-size: 16px; color: var(--cyan); font-weight: 600; }
|
|
49
|
+
.header .meta { color: var(--muted); font-size: 11px; }
|
|
50
|
+
.container { max-width: 1400px; margin: 0 auto; padding: 24px; }
|
|
51
|
+
.cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 12px; margin-bottom: 24px; }
|
|
52
|
+
.card { background: var(--bg2); border: 1px solid var(--border); border-radius: 8px; padding: 16px; }
|
|
53
|
+
.card .label { color: var(--muted); font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 6px; }
|
|
54
|
+
.card .value { font-size: 24px; font-weight: 700; }
|
|
55
|
+
.card .sub { color: var(--muted); font-size: 11px; margin-top: 4px; }
|
|
56
|
+
.charts { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin-bottom: 24px; }
|
|
57
|
+
.charts.full { grid-template-columns: 1fr; }
|
|
58
|
+
.chart-box { background: var(--bg2); border: 1px solid var(--border); border-radius: 8px; padding: 16px; }
|
|
59
|
+
.chart-box h3 { font-size: 12px; color: var(--muted); margin-bottom: 12px; text-transform: uppercase; letter-spacing: 0.5px; }
|
|
60
|
+
.chart-box canvas { max-height: 240px; }
|
|
61
|
+
.table-box { background: var(--bg2); border: 1px solid var(--border); border-radius: 8px; overflow: hidden; margin-bottom: 24px; }
|
|
62
|
+
.table-header { padding: 12px 16px; border-bottom: 1px solid var(--border); display: flex; justify-content: space-between; align-items: center; }
|
|
63
|
+
.table-header h3 { font-size: 12px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.5px; }
|
|
64
|
+
.filters { display: flex; gap: 8px; }
|
|
65
|
+
.filters input, .filters select { background: var(--bg3); border: 1px solid var(--border); color: var(--text); padding: 4px 8px; border-radius: 4px; font-family: inherit; font-size: 12px; outline: none; }
|
|
66
|
+
table { width: 100%; border-collapse: collapse; }
|
|
67
|
+
th { padding: 8px 12px; text-align: left; color: var(--muted); font-size: 11px; font-weight: 500; border-bottom: 1px solid var(--border); cursor: pointer; user-select: none; white-space: nowrap; }
|
|
68
|
+
th:hover { color: var(--text); }
|
|
69
|
+
td { padding: 8px 12px; border-bottom: 1px solid var(--border); white-space: nowrap; }
|
|
70
|
+
tr:last-child td { border-bottom: none; }
|
|
71
|
+
tr:hover td { background: var(--bg3); }
|
|
72
|
+
.pill { display: inline-block; padding: 2px 8px; border-radius: 99px; font-size: 11px; font-weight: 600; }
|
|
73
|
+
.pill-green { background: rgba(63,185,80,.15); color: var(--green); }
|
|
74
|
+
.pill-yellow { background: rgba(210,153,34,.15); color: var(--yellow); }
|
|
75
|
+
.pill-red { background: rgba(248,81,73,.15); color: var(--red); }
|
|
76
|
+
.bar-mini { display: inline-block; height: 6px; border-radius: 3px; vertical-align: middle; }
|
|
77
|
+
.text-muted { color: var(--muted); }
|
|
78
|
+
.text-cyan { color: var(--cyan); }
|
|
79
|
+
.text-green { color: var(--green); }
|
|
80
|
+
.text-yellow { color: var(--yellow); }
|
|
81
|
+
.text-red { color: var(--red); }
|
|
82
|
+
@media (max-width: 900px) { .charts { grid-template-columns: 1fr; } }
|
|
83
|
+
</style>
|
|
84
|
+
</head>
|
|
85
|
+
<body>
|
|
86
|
+
|
|
87
|
+
<div class="header">
|
|
88
|
+
<h1>⚡ Claude Code — Usage Dashboard</h1>
|
|
89
|
+
<div class="meta">generado: $GENERATED_AT | <a href="#" onclick="location.reload()" style="color:var(--cyan);text-decoration:none">regenerar</a></div>
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
<div class="container">
|
|
93
|
+
|
|
94
|
+
<!-- Cards -->
|
|
95
|
+
<div class="cards" id="cards"></div>
|
|
96
|
+
|
|
97
|
+
<!-- Charts row 1 -->
|
|
98
|
+
<div class="charts">
|
|
99
|
+
<div class="chart-box">
|
|
100
|
+
<h3>Costo por día (últimos 30 días)</h3>
|
|
101
|
+
<canvas id="chartCostByDay"></canvas>
|
|
102
|
+
</div>
|
|
103
|
+
<div class="chart-box">
|
|
104
|
+
<h3>% límite de 5h consumido — por sesión (últimas 30)</h3>
|
|
105
|
+
<canvas id="chartFiveH"></canvas>
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
|
|
109
|
+
<!-- Charts row 2 -->
|
|
110
|
+
<div class="charts">
|
|
111
|
+
<div class="chart-box">
|
|
112
|
+
<h3>Tokens por día (in + out, miles)</h3>
|
|
113
|
+
<canvas id="chartTokens"></canvas>
|
|
114
|
+
</div>
|
|
115
|
+
<div class="chart-box">
|
|
116
|
+
<h3>Sesiones por proyecto</h3>
|
|
117
|
+
<canvas id="chartProjects"></canvas>
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
|
|
121
|
+
<!-- Table -->
|
|
122
|
+
<div class="table-box">
|
|
123
|
+
<div class="table-header">
|
|
124
|
+
<h3>Todas las sesiones</h3>
|
|
125
|
+
<div class="filters">
|
|
126
|
+
<input type="text" id="filterText" placeholder="Filtrar..." oninput="renderTable()">
|
|
127
|
+
<select id="filterPeriod" onchange="renderTable()">
|
|
128
|
+
<option value="all">Todo</option>
|
|
129
|
+
<option value="today">Hoy</option>
|
|
130
|
+
<option value="week">Esta semana</option>
|
|
131
|
+
<option value="month">Este mes</option>
|
|
132
|
+
</select>
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
135
|
+
<table id="sessionsTable">
|
|
136
|
+
<thead>
|
|
137
|
+
<tr>
|
|
138
|
+
<th onclick="sortTable('date')">Fecha ↕</th>
|
|
139
|
+
<th onclick="sortTable('time')">Hora ↕</th>
|
|
140
|
+
<th>Session</th>
|
|
141
|
+
<th onclick="sortTable('project')">Proyecto ↕</th>
|
|
142
|
+
<th onclick="sortTable('cost')">Costo ↕</th>
|
|
143
|
+
<th onclick="sortTable('input_tokens')">In-tok ↕</th>
|
|
144
|
+
<th onclick="sortTable('output_tokens')">Out-tok ↕</th>
|
|
145
|
+
<th onclick="sortTable('ctx_pct')">Ctx% ↕</th>
|
|
146
|
+
<th onclick="sortTable('five_h_end')">5h-acum ↕</th>
|
|
147
|
+
<th onclick="sortTable('five_h_delta')">5h-sesión ↕</th>
|
|
148
|
+
<th>Modelo</th>
|
|
149
|
+
</tr>
|
|
150
|
+
</thead>
|
|
151
|
+
<tbody id="tableBody"></tbody>
|
|
152
|
+
</table>
|
|
153
|
+
</div>
|
|
154
|
+
|
|
155
|
+
</div>
|
|
156
|
+
|
|
157
|
+
<script>
|
|
158
|
+
const RAW = [${JSON_DATA}];
|
|
159
|
+
let sortKey = 'date', sortDir = -1;
|
|
160
|
+
|
|
161
|
+
const TODAY = new Date().toISOString().slice(0,10);
|
|
162
|
+
const WEEK_AGO = new Date(Date.now() - 7*24*3600*1000).toISOString().slice(0,10);
|
|
163
|
+
const MONTH_AGO = new Date(Date.now() - 30*24*3600*1000).toISOString().slice(0,10);
|
|
164
|
+
|
|
165
|
+
Chart.defaults.color = '#7d8590';
|
|
166
|
+
Chart.defaults.borderColor = '#30363d';
|
|
167
|
+
Chart.defaults.font.family = "'SF Mono', 'Fira Code', monospace";
|
|
168
|
+
Chart.defaults.font.size = 11;
|
|
169
|
+
|
|
170
|
+
function filteredData() {
|
|
171
|
+
const period = document.getElementById('filterPeriod').value;
|
|
172
|
+
const text = document.getElementById('filterText').value.toLowerCase();
|
|
173
|
+
return RAW.filter(r => {
|
|
174
|
+
if (period === 'today' && r.date !== TODAY) return false;
|
|
175
|
+
if (period === 'week' && r.date < WEEK_AGO) return false;
|
|
176
|
+
if (period === 'month' && r.date < MONTH_AGO) return false;
|
|
177
|
+
if (text && !JSON.stringify(r).toLowerCase().includes(text)) return false;
|
|
178
|
+
return true;
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function sortTable(key) {
|
|
183
|
+
if (sortKey === key) sortDir *= -1; else { sortKey = key; sortDir = -1; }
|
|
184
|
+
renderTable();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function costPill(cost) {
|
|
188
|
+
if (cost > 1) return \`<span class="pill pill-red">\$\${cost.toFixed(2)}</span>\`;
|
|
189
|
+
if (cost > 0.3) return \`<span class="pill pill-yellow">\$\${cost.toFixed(2)}</span>\`;
|
|
190
|
+
return \`<span class="pill pill-green">\$\${cost.toFixed(2)}</span>\`;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function miniBar(pct, color) {
|
|
194
|
+
const w = Math.min(pct, 100);
|
|
195
|
+
return \`<span class="bar-mini" style="width:\${w}px;background:\${color}"></span> \${pct.toFixed(1)}%\`;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function fiveHColor(pct) {
|
|
199
|
+
if (pct >= 20) return '#f85149';
|
|
200
|
+
if (pct >= 10) return '#d29922';
|
|
201
|
+
return '#39d3f7';
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function renderTable() {
|
|
205
|
+
const data = filteredData().sort((a,b) => {
|
|
206
|
+
let av = a[sortKey], bv = b[sortKey];
|
|
207
|
+
if (typeof av === 'string') return sortDir * av.localeCompare(bv);
|
|
208
|
+
return sortDir * (av - bv);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
const tbody = document.getElementById('tableBody');
|
|
212
|
+
if (!data.length) { tbody.innerHTML = '<tr><td colspan="11" style="text-align:center;color:var(--muted);padding:24px">Sin datos</td></tr>'; return; }
|
|
213
|
+
|
|
214
|
+
tbody.innerHTML = data.map(r => \`
|
|
215
|
+
<tr>
|
|
216
|
+
<td class="text-muted">\${r.date}</td>
|
|
217
|
+
<td class="text-muted">\${r.time}</td>
|
|
218
|
+
<td class="text-cyan">\${r.session_id}</td>
|
|
219
|
+
<td>\${r.project}</td>
|
|
220
|
+
<td>\${costPill(r.cost)}</td>
|
|
221
|
+
<td class="text-muted">\${(r.input_tokens/1000).toFixed(0)}k</td>
|
|
222
|
+
<td class="text-muted">\${(r.output_tokens/1000).toFixed(0)}k</td>
|
|
223
|
+
<td>\${miniBar(r.ctx_pct, '#58a6ff')}</td>
|
|
224
|
+
<td>\${miniBar(r.five_h_end, r.five_h_end >= 80 ? '#f85149' : r.five_h_end >= 50 ? '#d29922' : '#3fb950')}</td>
|
|
225
|
+
<td>\${miniBar(r.five_h_delta, fiveHColor(r.five_h_delta))}</td>
|
|
226
|
+
<td class="text-muted">\${r.model}</td>
|
|
227
|
+
</tr>
|
|
228
|
+
\`).join('');
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function renderCards() {
|
|
232
|
+
const data = RAW;
|
|
233
|
+
const totalCost = data.reduce((s,r) => s+r.cost, 0);
|
|
234
|
+
const totalSess = data.length;
|
|
235
|
+
const totalTok = data.reduce((s,r) => s+r.input_tokens+r.output_tokens, 0);
|
|
236
|
+
const total5h = data.reduce((s,r) => s+r.five_h_delta, 0);
|
|
237
|
+
const todayData = data.filter(r => r.date === TODAY);
|
|
238
|
+
const todayCost = todayData.reduce((s,r) => s+r.cost, 0);
|
|
239
|
+
const avg5h = totalSess > 0 ? total5h/totalSess : 0;
|
|
240
|
+
|
|
241
|
+
document.getElementById('cards').innerHTML = \`
|
|
242
|
+
<div class="card">
|
|
243
|
+
<div class="label">Costo total</div>
|
|
244
|
+
<div class="value text-yellow">\$\${totalCost.toFixed(2)}</div>
|
|
245
|
+
<div class="sub">en \${totalSess} sesiones</div>
|
|
246
|
+
</div>
|
|
247
|
+
<div class="card">
|
|
248
|
+
<div class="label">Hoy</div>
|
|
249
|
+
<div class="value text-green">\$\${todayCost.toFixed(2)}</div>
|
|
250
|
+
<div class="sub">\${todayData.length} sesión(es)</div>
|
|
251
|
+
</div>
|
|
252
|
+
<div class="card">
|
|
253
|
+
<div class="label">Tokens totales</div>
|
|
254
|
+
<div class="value text-cyan">\${(totalTok/1000).toFixed(0)}k</div>
|
|
255
|
+
<div class="sub">\${(totalTok/totalSess/1000).toFixed(0)}k promedio/ses</div>
|
|
256
|
+
</div>
|
|
257
|
+
<div class="card">
|
|
258
|
+
<div class="label">5h consumido total</div>
|
|
259
|
+
<div class="value \${total5h > 100 ? 'text-red' : total5h > 50 ? 'text-yellow' : 'text-green'}">\${total5h.toFixed(1)}%</div>
|
|
260
|
+
<div class="sub">\${avg5h.toFixed(1)}% promedio/sesión</div>
|
|
261
|
+
</div>
|
|
262
|
+
<div class="card">
|
|
263
|
+
<div class="label">Sesiones esta semana</div>
|
|
264
|
+
<div class="value text-purple">\${data.filter(r=>r.date>=WEEK_AGO).length}</div>
|
|
265
|
+
<div class="sub">\$\${data.filter(r=>r.date>=WEEK_AGO).reduce((s,r)=>s+r.cost,0).toFixed(2)} esta semana</div>
|
|
266
|
+
</div>
|
|
267
|
+
\`;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function groupBy(data, key, val, agg='sum') {
|
|
271
|
+
const m = {};
|
|
272
|
+
data.forEach(r => {
|
|
273
|
+
const k = r[key];
|
|
274
|
+
if (!m[k]) m[k] = 0;
|
|
275
|
+
m[k] += r[val];
|
|
276
|
+
});
|
|
277
|
+
return m;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function renderCharts() {
|
|
281
|
+
const last30days = RAW.filter(r => r.date >= new Date(Date.now()-30*24*3600*1000).toISOString().slice(0,10));
|
|
282
|
+
const byDay = groupBy(last30days, 'date', 'cost');
|
|
283
|
+
const dayLabels = Object.keys(byDay).sort();
|
|
284
|
+
|
|
285
|
+
// Cost by day
|
|
286
|
+
new Chart(document.getElementById('chartCostByDay'), {
|
|
287
|
+
type: 'bar',
|
|
288
|
+
data: {
|
|
289
|
+
labels: dayLabels,
|
|
290
|
+
datasets: [{
|
|
291
|
+
label: 'Costo USD',
|
|
292
|
+
data: dayLabels.map(d => byDay[d]),
|
|
293
|
+
backgroundColor: 'rgba(57,211,247,0.3)',
|
|
294
|
+
borderColor: '#39d3f7',
|
|
295
|
+
borderWidth: 1,
|
|
296
|
+
borderRadius: 3,
|
|
297
|
+
}]
|
|
298
|
+
},
|
|
299
|
+
options: { plugins: { legend: { display: false } }, scales: { x: { ticks: { maxRotation: 45 } } } }
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
// 5h delta per session (last 30)
|
|
303
|
+
const last30 = RAW.slice(-30);
|
|
304
|
+
new Chart(document.getElementById('chartFiveH'), {
|
|
305
|
+
type: 'bar',
|
|
306
|
+
data: {
|
|
307
|
+
labels: last30.map(r => r.date.slice(5)+' '+r.time),
|
|
308
|
+
datasets: [{
|
|
309
|
+
label: '% 5h esta sesión',
|
|
310
|
+
data: last30.map(r => r.five_h_delta),
|
|
311
|
+
backgroundColor: last30.map(r => r.five_h_delta >= 20 ? 'rgba(248,81,73,0.4)' : r.five_h_delta >= 10 ? 'rgba(210,153,34,0.4)' : 'rgba(57,211,247,0.3)'),
|
|
312
|
+
borderColor: last30.map(r => r.five_h_delta >= 20 ? '#f85149' : r.five_h_delta >= 10 ? '#d29922' : '#39d3f7'),
|
|
313
|
+
borderWidth: 1,
|
|
314
|
+
borderRadius: 3,
|
|
315
|
+
}]
|
|
316
|
+
},
|
|
317
|
+
options: { plugins: { legend: { display: false } }, scales: { x: { ticks: { maxRotation: 45, maxTicksLimit: 15 } }, y: { title: { display: true, text: '%' } } } }
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
// Tokens by day
|
|
321
|
+
const tokByDay = {};
|
|
322
|
+
last30days.forEach(r => {
|
|
323
|
+
if (!tokByDay[r.date]) tokByDay[r.date] = { in: 0, out: 0 };
|
|
324
|
+
tokByDay[r.date].in += r.input_tokens / 1000;
|
|
325
|
+
tokByDay[r.date].out += r.output_tokens / 1000;
|
|
326
|
+
});
|
|
327
|
+
new Chart(document.getElementById('chartTokens'), {
|
|
328
|
+
type: 'bar',
|
|
329
|
+
data: {
|
|
330
|
+
labels: dayLabels,
|
|
331
|
+
datasets: [
|
|
332
|
+
{ label: 'Input', data: dayLabels.map(d => tokByDay[d]?.in || 0), backgroundColor: 'rgba(88,166,255,0.4)', borderColor: '#58a6ff', borderWidth: 1, borderRadius: 3 },
|
|
333
|
+
{ label: 'Output', data: dayLabels.map(d => tokByDay[d]?.out || 0), backgroundColor: 'rgba(188,140,255,0.4)', borderColor: '#bc8cff', borderWidth: 1, borderRadius: 3 },
|
|
334
|
+
]
|
|
335
|
+
},
|
|
336
|
+
options: { plugins: { legend: { position: 'top' } }, scales: { x: { stacked: true, ticks: { maxRotation: 45 } }, y: { stacked: true, title: { display: true, text: 'k tokens' } } } }
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
// Sessions by project (donut)
|
|
340
|
+
const byProj = {};
|
|
341
|
+
RAW.forEach(r => { byProj[r.project] = (byProj[r.project]||0) + 1; });
|
|
342
|
+
const projLabels = Object.keys(byProj).sort((a,b) => byProj[b]-byProj[a]).slice(0,10);
|
|
343
|
+
const palette = ['#39d3f7','#3fb950','#d29922','#f85149','#bc8cff','#f0883e','#58a6ff','#7d8590','#56d364','#e3b341'];
|
|
344
|
+
new Chart(document.getElementById('chartProjects'), {
|
|
345
|
+
type: 'doughnut',
|
|
346
|
+
data: {
|
|
347
|
+
labels: projLabels,
|
|
348
|
+
datasets: [{ data: projLabels.map(p => byProj[p]), backgroundColor: palette.map(c => c+'99'), borderColor: palette, borderWidth: 2 }]
|
|
349
|
+
},
|
|
350
|
+
options: { plugins: { legend: { position: 'right', labels: { boxWidth: 12 } } }, cutout: '60%' }
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Init
|
|
355
|
+
renderCards();
|
|
356
|
+
renderCharts();
|
|
357
|
+
renderTable();
|
|
358
|
+
</script>
|
|
359
|
+
</body>
|
|
360
|
+
</html>
|
|
361
|
+
HTMLEOF
|
|
362
|
+
|
|
363
|
+
echo "Dashboard generado: $OUT_FILE"
|
|
364
|
+
|
|
365
|
+
# Open in browser if requested
|
|
366
|
+
if [ "$OPEN_AFTER" = "--open" ] || [ "$OPEN_AFTER" = "-o" ]; then
|
|
367
|
+
open "$OUT_FILE"
|
|
368
|
+
echo "Abriendo en el browser..."
|
|
369
|
+
fi
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# session-live.sh — live terminal dashboard for Claude Code usage
|
|
3
|
+
# Run with: watch -n 10 -c ~/.claude/tools/session-live.sh
|
|
4
|
+
# Or standalone (loops itself): ~/.claude/tools/session-live.sh --loop
|
|
5
|
+
|
|
6
|
+
LOG_FILE="$HOME/.claude/session-costs.log"
|
|
7
|
+
STATS_DIR="$HOME/.claude"
|
|
8
|
+
TODAY=$(date +%Y-%m-%d)
|
|
9
|
+
NOW=$(date '+%H:%M:%S')
|
|
10
|
+
LOOP_MODE="${1:-}"
|
|
11
|
+
|
|
12
|
+
# ANSI
|
|
13
|
+
BOLD='\033[1m'; DIM='\033[2m'; RESET='\033[0m'
|
|
14
|
+
CYAN='\033[0;36m'; GREEN='\033[0;32m'; YELLOW='\033[0;33m'
|
|
15
|
+
RED='\033[0;31m'; MAGENTA='\033[0;35m'; BLUE='\033[0;34m'; WHITE='\033[0;37m'
|
|
16
|
+
BG_DARK='\033[48;5;235m'
|
|
17
|
+
|
|
18
|
+
bar() {
|
|
19
|
+
local pct="${1:-0}"; local width="${2:-20}"
|
|
20
|
+
local filled=$(awk "BEGIN {printf \"%.0f\", $pct * $width / 100}")
|
|
21
|
+
local empty=$(( width - filled ))
|
|
22
|
+
local color
|
|
23
|
+
if awk "BEGIN {exit !($pct >= 80)}"; then color="$RED"
|
|
24
|
+
elif awk "BEGIN {exit !($pct >= 50)}"; then color="$YELLOW"
|
|
25
|
+
else color="$GREEN"; fi
|
|
26
|
+
printf "${color}"
|
|
27
|
+
printf '█%.0s' $(seq 1 $filled 2>/dev/null); printf '░%.0s' $(seq 1 $empty 2>/dev/null)
|
|
28
|
+
printf "${RESET} ${DIM}%.0f%%${RESET}" "$pct"
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
hr() { printf "${DIM}%s${RESET}\n" "$(printf '─%.0s' $(seq 1 ${1:-70}))"; }
|
|
32
|
+
|
|
33
|
+
clear
|
|
34
|
+
|
|
35
|
+
# ── Header ──────────────────────────────────────────────────────────────────
|
|
36
|
+
printf "${BOLD}${CYAN} Claude Code — Usage Monitor${RESET} ${DIM}actualizado: %s${RESET}\n" "$NOW"
|
|
37
|
+
hr 70
|
|
38
|
+
echo ""
|
|
39
|
+
|
|
40
|
+
# ── Active sessions ──────────────────────────────────────────────────────────
|
|
41
|
+
printf "${BOLD} SESIONES ACTIVAS${RESET}\n"
|
|
42
|
+
echo ""
|
|
43
|
+
|
|
44
|
+
ACTIVE_COUNT=0
|
|
45
|
+
# Find stats files modified in last 2 hours (active sessions)
|
|
46
|
+
while IFS= read -r stats_file; do
|
|
47
|
+
[ -f "$stats_file" ] || continue
|
|
48
|
+
# Skip start files
|
|
49
|
+
[[ "$stats_file" == *"-start.json" ]] && continue
|
|
50
|
+
|
|
51
|
+
session_id=$(jq -r '.session_id // ""' "$stats_file" 2>/dev/null)
|
|
52
|
+
[ -z "$session_id" ] && continue
|
|
53
|
+
|
|
54
|
+
proj=$(jq -r '.cwd // ""' "$stats_file" 2>/dev/null | xargs basename 2>/dev/null)
|
|
55
|
+
cost=$(jq -r '.cost_usd // 0' "$stats_file" 2>/dev/null)
|
|
56
|
+
in_tok=$(jq -r '.total_input_tokens // 0' "$stats_file" 2>/dev/null)
|
|
57
|
+
out_tok=$(jq -r '.total_output_tokens // 0' "$stats_file" 2>/dev/null)
|
|
58
|
+
ctx_pct=$(jq -r '.ctx_used_pct // 0' "$stats_file" 2>/dev/null)
|
|
59
|
+
five_h=$(jq -r '.five_h_pct // 0' "$stats_file" 2>/dev/null)
|
|
60
|
+
|
|
61
|
+
# Per-session 5h delta
|
|
62
|
+
start_file="${stats_file%-start.json}-start.json"
|
|
63
|
+
# Actually: stats file is session-stats-{id}.json, start file is session-stats-{id}-start.json
|
|
64
|
+
start_file="$STATS_DIR/session-stats-${session_id}-start.json"
|
|
65
|
+
five_h_start=0
|
|
66
|
+
[ -f "$start_file" ] && five_h_start=$(jq -r '.five_h_pct_start // 0' "$start_file" 2>/dev/null)
|
|
67
|
+
five_h_delta=$(awk "BEGIN {d=$five_h-$five_h_start; printf \"%.1f\", (d<0?0:d)}")
|
|
68
|
+
|
|
69
|
+
total_tok=$(awk "BEGIN {printf \"%.0f\", ($in_tok+$out_tok)/1000}")
|
|
70
|
+
|
|
71
|
+
printf " ${CYAN}%-18s${RESET} ${DIM}%s${RESET}\n" "$proj" "${session_id:0:8}"
|
|
72
|
+
printf " Costo: ${YELLOW}\$%-8s${RESET} Tokens: ${WHITE}%sk${RESET}\n" "$cost" "$total_tok"
|
|
73
|
+
printf " Ctx: "; bar "$ctx_pct" 18; echo ""
|
|
74
|
+
printf " 5h acum: "; bar "$five_h" 18; echo ""
|
|
75
|
+
printf " 5h esta sesión: ${CYAN}%s%%${RESET}\n" "$five_h_delta"
|
|
76
|
+
echo ""
|
|
77
|
+
ACTIVE_COUNT=$((ACTIVE_COUNT + 1))
|
|
78
|
+
done < <(find "$STATS_DIR" -maxdepth 1 -name "session-stats-*.json" ! -name "*-start.json" -newer "$STATS_DIR/settings.json" -mmin -120 2>/dev/null | sort)
|
|
79
|
+
|
|
80
|
+
if [ "$ACTIVE_COUNT" -eq 0 ]; then
|
|
81
|
+
printf " ${DIM}(sin sesiones activas en las últimas 2h)${RESET}\n"
|
|
82
|
+
echo ""
|
|
83
|
+
fi
|
|
84
|
+
|
|
85
|
+
# ── Today's closed sessions ──────────────────────────────────────────────────
|
|
86
|
+
hr 70
|
|
87
|
+
printf "${BOLD} HOY — %s${RESET}\n" "$TODAY"
|
|
88
|
+
echo ""
|
|
89
|
+
|
|
90
|
+
if [ -f "$LOG_FILE" ]; then
|
|
91
|
+
TODAY_DATA=$(tail -n +2 "$LOG_FILE" | grep "^${TODAY}")
|
|
92
|
+
|
|
93
|
+
if [ -n "$TODAY_DATA" ]; then
|
|
94
|
+
TOTAL_COST_TODAY=0
|
|
95
|
+
TOTAL_5H_TODAY=0
|
|
96
|
+
SESSION_COUNT_TODAY=0
|
|
97
|
+
|
|
98
|
+
while IFS=',' read -r date time sess proj cost in_tok out_tok ctx_pct five_h_end five_h_delta model; do
|
|
99
|
+
cost_cents=$(awk "BEGIN {printf \"%.0f\", ${cost:-0} * 100}")
|
|
100
|
+
[ "${cost_cents:-0}" -gt 30 ] && c_color="$YELLOW" || c_color="$GREEN"
|
|
101
|
+
[ "${cost_cents:-0}" -gt 100 ] && c_color="$RED"
|
|
102
|
+
|
|
103
|
+
in_k=$(awk "BEGIN {printf \"%.0f\", ${in_tok:-0}/1000}")
|
|
104
|
+
out_k=$(awk "BEGIN {printf \"%.0f\", ${out_tok:-0}/1000}")
|
|
105
|
+
|
|
106
|
+
printf " ${DIM}%s${RESET} ${CYAN}%-16s${RESET} ${c_color}\$%s${RESET} %sk+%sk tok ${DIM}5h:+%s%%${RESET}\n" \
|
|
107
|
+
"$time" "${proj:0:16}" "$cost" "$in_k" "$out_k" "$five_h_delta"
|
|
108
|
+
|
|
109
|
+
TOTAL_COST_TODAY=$(awk "BEGIN {printf \"%.2f\", $TOTAL_COST_TODAY + ${cost:-0}}")
|
|
110
|
+
TOTAL_5H_TODAY=$(awk "BEGIN {printf \"%.1f\", $TOTAL_5H_TODAY + ${five_h_delta:-0}}")
|
|
111
|
+
SESSION_COUNT_TODAY=$((SESSION_COUNT_TODAY + 1))
|
|
112
|
+
done <<< "$TODAY_DATA"
|
|
113
|
+
|
|
114
|
+
echo ""
|
|
115
|
+
printf " ${BOLD}Subtotal hoy: ${YELLOW}\$%s${RESET}${BOLD} en %d sesión(es) — %s%% del límite de 5h${RESET}\n" \
|
|
116
|
+
"$TOTAL_COST_TODAY" "$SESSION_COUNT_TODAY" "$TOTAL_5H_TODAY"
|
|
117
|
+
else
|
|
118
|
+
printf " ${DIM}(sin sesiones cerradas hoy todavía)${RESET}\n"
|
|
119
|
+
fi
|
|
120
|
+
else
|
|
121
|
+
printf " ${DIM}(sin datos aún)${RESET}\n"
|
|
122
|
+
fi
|
|
123
|
+
|
|
124
|
+
echo ""
|
|
125
|
+
|
|
126
|
+
# ── 7-day summary ────────────────────────────────────────────────────────────
|
|
127
|
+
hr 70
|
|
128
|
+
printf "${BOLD} ÚLTIMOS 7 DÍAS${RESET}\n"
|
|
129
|
+
echo ""
|
|
130
|
+
|
|
131
|
+
if [ -f "$LOG_FILE" ]; then
|
|
132
|
+
WEEK_AGO=$(date -v-7d +%Y-%m-%d 2>/dev/null || date -d "7 days ago" +%Y-%m-%d 2>/dev/null)
|
|
133
|
+
WEEK_DATA=$(tail -n +2 "$LOG_FILE" | awk -F',' -v cutoff="$WEEK_AGO" '$1 >= cutoff')
|
|
134
|
+
|
|
135
|
+
if [ -n "$WEEK_DATA" ]; then
|
|
136
|
+
echo "$WEEK_DATA" | awk -F',' -v CYAN="$CYAN" -v DIM="$DIM" -v YELLOW="$YELLOW" -v GREEN="$GREEN" -v BOLD="$BOLD" -v RESET="$RESET" '
|
|
137
|
+
{
|
|
138
|
+
day[$1] += $5; sessions[$1]++; tokens[$1] += ($6+$7)/1000; five_h[$1] += $10
|
|
139
|
+
}
|
|
140
|
+
END {
|
|
141
|
+
for (d in day) arr[d]=d
|
|
142
|
+
n = asorti(arr, sorted, "@val_str_desc")
|
|
143
|
+
for (i=1; i<=n; i++) {
|
|
144
|
+
d = sorted[i]
|
|
145
|
+
printf " %s%-12s%s %2d ses %s$%.2f%s %5.0ftok %s%.1f%%%s 5h\n",
|
|
146
|
+
DIM, d, RESET, sessions[d], YELLOW, day[d], RESET, tokens[d], DIM, five_h[d], RESET
|
|
147
|
+
}
|
|
148
|
+
}' 2>/dev/null || \
|
|
149
|
+
echo "$WEEK_DATA" | awk -F',' '{day[$1]+=$5; sessions[$1]++; tokens[$1]+=($6+$7)/1000; five_h[$1]+=$10}
|
|
150
|
+
END {for(d in day) printf " %-12s %2d ses $%.2f %5.0ftok %.1f%% 5h\n", d, sessions[d], day[d], tokens[d], five_h[d]}' | sort -r
|
|
151
|
+
|
|
152
|
+
echo ""
|
|
153
|
+
WEEK_TOTAL=$(echo "$WEEK_DATA" | awk -F',' '{s+=$5} END {printf "%.2f",s}')
|
|
154
|
+
WEEK_5H=$(echo "$WEEK_DATA" | awk -F',' '{s+=$10} END {printf "%.1f",s}')
|
|
155
|
+
WEEK_SESS=$(echo "$WEEK_DATA" | wc -l | tr -d ' ')
|
|
156
|
+
printf " ${BOLD}Total 7d: ${YELLOW}\$%s${RESET}${BOLD} %s sesiones — %s%% del límite de 5h acumulado${RESET}\n" \
|
|
157
|
+
"$WEEK_TOTAL" "$WEEK_SESS" "$WEEK_5H"
|
|
158
|
+
else
|
|
159
|
+
printf " ${DIM}(sin datos esta semana)${RESET}\n"
|
|
160
|
+
fi
|
|
161
|
+
fi
|
|
162
|
+
|
|
163
|
+
echo ""
|
|
164
|
+
hr 70
|
|
165
|
+
printf " ${DIM}Para dashboard web: bash ~/.claude/tools/session-dashboard-gen.sh${RESET}\n"
|
|
166
|
+
printf " ${DIM}Actualiza cada 10s con: watch -n 10 -c ~/.claude/tools/session-live.sh${RESET}\n"
|
|
167
|
+
echo ""
|
|
168
|
+
|
|
169
|
+
# Loop mode
|
|
170
|
+
if [ "$LOOP_MODE" = "--loop" ]; then
|
|
171
|
+
sleep 10
|
|
172
|
+
exec "$0" --loop
|
|
173
|
+
fi
|