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.
Files changed (117) hide show
  1. package/CHANGELOG.md +218 -0
  2. package/CONTRIBUTING.md +58 -0
  3. package/LICENSE +21 -0
  4. package/README.md +518 -0
  5. package/agents/api-design.md +51 -0
  6. package/agents/bughunter.md +136 -0
  7. package/agents/changelog.md +89 -0
  8. package/agents/cleanup.md +126 -0
  9. package/agents/compare-branch.md +35 -0
  10. package/agents/cross-repo.md +97 -0
  11. package/agents/db-check.md +14 -0
  12. package/agents/debug.md +47 -0
  13. package/agents/deploy-check.md +100 -0
  14. package/agents/draft-message.md +19 -0
  15. package/agents/excelsior-coordinator.md +75 -0
  16. package/agents/excelsior-verifier.md +94 -0
  17. package/agents/feature.md +48 -0
  18. package/agents/harness-optimizer.md +40 -0
  19. package/agents/incident.md +48 -0
  20. package/agents/linear-task.md +18 -0
  21. package/agents/onboard.md +24 -0
  22. package/agents/perf.md +44 -0
  23. package/agents/production-validator.md +96 -0
  24. package/agents/review.md +113 -0
  25. package/agents/security-check.md +29 -0
  26. package/agents/sprint-summary.md +15 -0
  27. package/agents/tdd-mainder.md +178 -0
  28. package/agents/test-gen.md +39 -0
  29. package/axel-manifest.json +129 -0
  30. package/bin/axel-setup.js +597 -0
  31. package/bootstrap.sh +1087 -0
  32. package/commands/create-pr.md +13 -0
  33. package/commands/daily.md +182 -0
  34. package/commands/deslop.md +13 -0
  35. package/commands/draft-message.md +23 -0
  36. package/commands/eod-review.md +154 -0
  37. package/commands/execute-prp.md +37 -0
  38. package/commands/generate-prp.md +75 -0
  39. package/commands/multi-repo-feature.md +60 -0
  40. package/commands/roadmap.md +31 -0
  41. package/commands/sprint-status.md +486 -0
  42. package/commands/style.md +68 -0
  43. package/commands/visualize.md +17 -0
  44. package/docs/roadmap/multi-runtime.md +73 -0
  45. package/docs/superpowers/plans/2026-06-12-setup-hardening-roadmap.md +61 -0
  46. package/hooks/desktop-notify.sh +26 -0
  47. package/hooks/enforce-agent-model.jq +14 -0
  48. package/hooks/gsd-context-monitor.js +156 -0
  49. package/hooks/linear-lifecycle-sync.sh +112 -0
  50. package/hooks/memory-dedup.sh +122 -0
  51. package/hooks/memory-extractor.sh +218 -0
  52. package/hooks/post-commit-memory-trigger.sh +16 -0
  53. package/hooks/post-commit-verify.sh +41 -0
  54. package/hooks/post-edit-lint.sh +43 -0
  55. package/hooks/precompact-save-context.sh +124 -0
  56. package/hooks/priority-map-staleness.sh +29 -0
  57. package/hooks/proactive-resolver.sh +104 -0
  58. package/hooks/session-auto-title.sh +165 -0
  59. package/hooks/session-checkpoint.sh +97 -0
  60. package/hooks/session-cost-log.sh +77 -0
  61. package/hooks/session-log-action.sh +36 -0
  62. package/hooks/session-log-prompt.sh +25 -0
  63. package/hooks/session-restore.sh +45 -0
  64. package/hooks/session-save.sh +81 -0
  65. package/hooks/session-summarize.sh +154 -0
  66. package/hooks/validate-commit-format.sh +38 -0
  67. package/hooks/weekly-priority-map-review.sh +143 -0
  68. package/install.sh +46 -0
  69. package/package.json +67 -0
  70. package/scripts/ci/bootstrap-dry-run.sh +40 -0
  71. package/scripts/ci/check.sh +65 -0
  72. package/scripts/posthog-snapshot-loader.sh +112 -0
  73. package/skills/context-budget/SKILL.md +86 -0
  74. package/skills/memory-review/SKILL.md +100 -0
  75. package/skills/model-routing/SKILL.md +70 -0
  76. package/skills/posthog-weekly/SKILL.md +271 -0
  77. package/skills/ui-ux-pro-max/SKILL.md +377 -0
  78. package/skills/ui-ux-pro-max/data/charts.csv +26 -0
  79. package/skills/ui-ux-pro-max/data/colors.csv +97 -0
  80. package/skills/ui-ux-pro-max/data/icons.csv +101 -0
  81. package/skills/ui-ux-pro-max/data/landing.csv +31 -0
  82. package/skills/ui-ux-pro-max/data/products.csv +97 -0
  83. package/skills/ui-ux-pro-max/data/react-performance.csv +45 -0
  84. package/skills/ui-ux-pro-max/data/stacks/astro.csv +54 -0
  85. package/skills/ui-ux-pro-max/data/stacks/flutter.csv +53 -0
  86. package/skills/ui-ux-pro-max/data/stacks/html-tailwind.csv +56 -0
  87. package/skills/ui-ux-pro-max/data/stacks/jetpack-compose.csv +53 -0
  88. package/skills/ui-ux-pro-max/data/stacks/nextjs.csv +53 -0
  89. package/skills/ui-ux-pro-max/data/stacks/nuxt-ui.csv +51 -0
  90. package/skills/ui-ux-pro-max/data/stacks/nuxtjs.csv +59 -0
  91. package/skills/ui-ux-pro-max/data/stacks/react-native.csv +52 -0
  92. package/skills/ui-ux-pro-max/data/stacks/react.csv +54 -0
  93. package/skills/ui-ux-pro-max/data/stacks/shadcn.csv +61 -0
  94. package/skills/ui-ux-pro-max/data/stacks/svelte.csv +54 -0
  95. package/skills/ui-ux-pro-max/data/stacks/swiftui.csv +51 -0
  96. package/skills/ui-ux-pro-max/data/stacks/vue.csv +50 -0
  97. package/skills/ui-ux-pro-max/data/styles.csv +68 -0
  98. package/skills/ui-ux-pro-max/data/typography.csv +58 -0
  99. package/skills/ui-ux-pro-max/data/ui-reasoning.csv +101 -0
  100. package/skills/ui-ux-pro-max/data/ux-guidelines.csv +100 -0
  101. package/skills/ui-ux-pro-max/data/web-interface.csv +31 -0
  102. package/skills/ui-ux-pro-max/scripts/core.py +253 -0
  103. package/skills/ui-ux-pro-max/scripts/design_system.py +1067 -0
  104. package/skills/ui-ux-pro-max/scripts/search.py +114 -0
  105. package/templates/AGENTS.runtime.md +17 -0
  106. package/templates/CLAUDE.md +252 -0
  107. package/templates/claude-monitor.plist +35 -0
  108. package/templates/keybindings.json +13 -0
  109. package/templates/merge-settings.jq +53 -0
  110. package/templates/review-upgrades.md +44 -0
  111. package/templates/settings.json +255 -0
  112. package/templates/statusline-command.sh +182 -0
  113. package/tests/fixtures/hooks/events.json +32 -0
  114. package/tools/session-costs-view.sh +128 -0
  115. package/tools/session-dashboard-gen.sh +369 -0
  116. package/tools/session-live.sh +173 -0
  117. 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 &nbsp;|&nbsp; <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