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,441 @@
1
+ #!/usr/bin/env node
2
+ // Claude Code Usage Monitor — HTTP server
3
+ // Runs as a launchd service, accessible at http://localhost:9119
4
+ // No npm dependencies — pure Node.js built-ins only
5
+
6
+ const http = require('http');
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+ const os = require('os');
10
+
11
+ const PORT = 9119;
12
+ const BASE_DIR = path.join(os.homedir(), '.claude');
13
+ const LOG_FILE = path.join(BASE_DIR, 'session-costs.log');
14
+
15
+ // ── Data helpers ─────────────────────────────────────────────────────────────
16
+
17
+ function readLog() {
18
+ if (!fs.existsSync(LOG_FILE)) return [];
19
+ const lines = fs.readFileSync(LOG_FILE, 'utf8').trim().split('\n');
20
+ // Skip header
21
+ return lines.slice(1).filter(Boolean).map(line => {
22
+ const [date, time, session_id, project, cost, input_tokens, output_tokens,
23
+ ctx_pct, five_h_end, five_h_delta, ...modelParts] = line.split(',');
24
+ return {
25
+ date, time, session_id, project,
26
+ cost: parseFloat(cost) || 0,
27
+ input_tokens: parseInt(input_tokens) || 0,
28
+ output_tokens: parseInt(output_tokens) || 0,
29
+ ctx_pct: parseFloat(ctx_pct) || 0,
30
+ five_h_end: parseFloat(five_h_end) || 0,
31
+ five_h_delta: parseFloat(five_h_delta) || 0,
32
+ model: modelParts.join(',').trim() || 'unknown',
33
+ };
34
+ });
35
+ }
36
+
37
+ function readActiveSessions() {
38
+ const files = fs.readdirSync(BASE_DIR).filter(f =>
39
+ f.startsWith('session-stats-') && !f.endsWith('-start.json') && f.endsWith('.json')
40
+ );
41
+ const cutoff = Date.now() - 2 * 60 * 60 * 1000; // 2h
42
+ const result = [];
43
+ for (const f of files) {
44
+ const fp = path.join(BASE_DIR, f);
45
+ try {
46
+ const stat = fs.statSync(fp);
47
+ if (stat.mtimeMs < cutoff) continue;
48
+ const data = JSON.parse(fs.readFileSync(fp, 'utf8'));
49
+ const sid = data.session_id || f.replace('session-stats-', '').replace('.json', '');
50
+ // Try to get start 5h%
51
+ const startFp = path.join(BASE_DIR, `session-stats-${sid}-start.json`);
52
+ let five_h_start = 0;
53
+ if (fs.existsSync(startFp)) {
54
+ try { five_h_start = JSON.parse(fs.readFileSync(startFp, 'utf8')).five_h_pct_start || 0; } catch {}
55
+ }
56
+ data.five_h_delta = Math.max(0, (data.five_h_pct || 0) - five_h_start);
57
+ result.push(data);
58
+ } catch {}
59
+ }
60
+ return result;
61
+ }
62
+
63
+ // ── HTML dashboard ────────────────────────────────────────────────────────────
64
+
65
+ function dashboardHTML() {
66
+ return `<!DOCTYPE html>
67
+ <html lang="es">
68
+ <head>
69
+ <meta charset="UTF-8">
70
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
71
+ <title>Claude Monitor</title>
72
+ <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
73
+ <style>
74
+ :root {
75
+ --bg:#0d1117;--bg2:#161b22;--bg3:#21262d;--border:#30363d;
76
+ --text:#e6edf3;--muted:#7d8590;
77
+ --cyan:#39d3f7;--green:#3fb950;--yellow:#d29922;
78
+ --red:#f85149;--purple:#bc8cff;--blue:#58a6ff;
79
+ }
80
+ *{box-sizing:border-box;margin:0;padding:0}
81
+ body{background:var(--bg);color:var(--text);font-family:'SF Mono','Fira Code',monospace;font-size:13px}
82
+ .header{background:var(--bg2);border-bottom:1px solid var(--border);padding:14px 24px;display:flex;justify-content:space-between;align-items:center;position:sticky;top:0;z-index:10}
83
+ .header h1{font-size:15px;color:var(--cyan);font-weight:700}
84
+ .header .meta{color:var(--muted);font-size:11px;display:flex;align-items:center;gap:12px}
85
+ .dot{width:8px;height:8px;border-radius:50%;background:var(--green);display:inline-block;animation:pulse 2s infinite}
86
+ @keyframes pulse{0%,100%{opacity:1}50%{opacity:.4}}
87
+ .container{max-width:1400px;margin:0 auto;padding:20px}
88
+ .cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:12px;margin-bottom:20px}
89
+ .card{background:var(--bg2);border:1px solid var(--border);border-radius:8px;padding:14px}
90
+ .card .label{color:var(--muted);font-size:10px;text-transform:uppercase;letter-spacing:.5px;margin-bottom:6px}
91
+ .card .value{font-size:22px;font-weight:700}
92
+ .card .sub{color:var(--muted);font-size:10px;margin-top:4px}
93
+ .section{background:var(--bg2);border:1px solid var(--border);border-radius:8px;margin-bottom:16px;overflow:hidden}
94
+ .section-header{padding:10px 16px;border-bottom:1px solid var(--border);display:flex;justify-content:space-between;align-items:center}
95
+ .section-header h3{font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:.5px}
96
+ .active-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:0;padding:0}
97
+ .active-card{padding:14px 16px;border-right:1px solid var(--border);border-bottom:1px solid var(--border)}
98
+ .active-card:nth-child(n){border-right:1px solid var(--border)}
99
+ .active-card .proj{font-weight:600;color:var(--cyan);margin-bottom:8px;font-size:13px}
100
+ .active-card .sess{color:var(--muted);font-size:10px;margin-bottom:10px}
101
+ .bar-row{display:flex;align-items:center;gap:8px;margin-bottom:6px;font-size:11px}
102
+ .bar-row .bar-label{width:70px;color:var(--muted);flex-shrink:0}
103
+ .bar-track{flex:1;height:6px;background:var(--bg3);border-radius:3px;overflow:hidden}
104
+ .bar-fill{height:100%;border-radius:3px;transition:width .3s}
105
+ .bar-row .bar-val{width:50px;text-align:right;color:var(--text)}
106
+ .charts-grid{display:grid;grid-template-columns:1fr 1fr;gap:0}
107
+ .chart-box{padding:16px;border-right:1px solid var(--border)}
108
+ .chart-box:nth-child(2n){border-right:none}
109
+ .chart-box:nth-child(n+3){border-top:1px solid var(--border)}
110
+ .chart-box h3{font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:.5px;margin-bottom:12px}
111
+ .chart-box canvas{max-height:220px}
112
+ .filters{display:flex;gap:8px}
113
+ .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}
114
+ table{width:100%;border-collapse:collapse}
115
+ 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}
116
+ th:hover{color:var(--text)}
117
+ td{padding:7px 12px;border-bottom:1px solid var(--border);white-space:nowrap}
118
+ tr:last-child td{border-bottom:none}
119
+ tr:hover td{background:var(--bg3)}
120
+ .pill{display:inline-block;padding:1px 7px;border-radius:99px;font-size:11px;font-weight:600}
121
+ .pg{background:rgba(63,185,80,.15);color:var(--green)}
122
+ .py{background:rgba(210,153,34,.15);color:var(--yellow)}
123
+ .pr{background:rgba(248,81,73,.15);color:var(--red)}
124
+ .mini-bar{display:inline-flex;align-items:center;gap:6px;font-size:11px}
125
+ .mini-bar .track{width:50px;height:5px;background:var(--bg3);border-radius:3px;overflow:hidden;display:inline-block}
126
+ .mini-bar .fill{height:100%;border-radius:3px}
127
+ .empty-state{padding:32px;text-align:center;color:var(--muted)}
128
+ @media(max-width:900px){.charts-grid{grid-template-columns:1fr}.chart-box{border-right:none;border-top:1px solid var(--border)}}
129
+ </style>
130
+ </head>
131
+ <body>
132
+
133
+ <div class="header">
134
+ <h1>⚡ Claude Code — Usage Monitor</h1>
135
+ <div class="meta">
136
+ <span><span class="dot"></span> live</span>
137
+ <span id="lastUpdate">cargando...</span>
138
+ <span id="refreshTimer" style="color:var(--muted)"></span>
139
+ </div>
140
+ </div>
141
+
142
+ <div class="container">
143
+ <div class="cards" id="cards"></div>
144
+
145
+ <div class="section">
146
+ <div class="section-header"><h3>Sesiones activas</h3><span id="activeCount" style="color:var(--muted);font-size:11px"></span></div>
147
+ <div class="active-grid" id="activeGrid"></div>
148
+ </div>
149
+
150
+ <div class="section">
151
+ <div class="charts-grid">
152
+ <div class="chart-box"><h3>Costo por día (30d)</h3><canvas id="cCost"></canvas></div>
153
+ <div class="chart-box"><h3>% límite 5h por sesión (últimas 30)</h3><canvas id="cFiveH"></canvas></div>
154
+ <div class="chart-box"><h3>Tokens por día — input + output (30d)</h3><canvas id="cTokens"></canvas></div>
155
+ <div class="chart-box"><h3>Sesiones por proyecto</h3><canvas id="cProjects"></canvas></div>
156
+ </div>
157
+ </div>
158
+
159
+ <div class="section">
160
+ <div class="section-header">
161
+ <h3>Historial de sesiones</h3>
162
+ <div class="filters">
163
+ <input type="text" id="filterText" placeholder="Filtrar..." oninput="renderTable()">
164
+ <select id="filterPeriod" onchange="renderTable()">
165
+ <option value="all">Todo</option>
166
+ <option value="today">Hoy</option>
167
+ <option value="week">Esta semana</option>
168
+ <option value="month">Este mes</option>
169
+ </select>
170
+ </div>
171
+ </div>
172
+ <table>
173
+ <thead><tr>
174
+ <th onclick="sort('date')">Fecha</th>
175
+ <th onclick="sort('time')">Hora</th>
176
+ <th>Session</th>
177
+ <th onclick="sort('project')">Proyecto</th>
178
+ <th onclick="sort('cost')">Costo</th>
179
+ <th onclick="sort('input_tokens')">In-tok</th>
180
+ <th onclick="sort('output_tokens')">Out-tok</th>
181
+ <th onclick="sort('ctx_pct')">Ctx%</th>
182
+ <th onclick="sort('five_h_end')">5h acum</th>
183
+ <th onclick="sort('five_h_delta')">5h sesión</th>
184
+ <th>Modelo</th>
185
+ </tr></thead>
186
+ <tbody id="tableBody"></tbody>
187
+ </table>
188
+ </div>
189
+ </div>
190
+
191
+ <script>
192
+ let sessions = [], active = [], charts = {}, sortKey = 'date', sortDir = -1;
193
+ const TODAY = () => new Date().toISOString().slice(0,10);
194
+ const WEEK_AGO = () => new Date(Date.now()-7*24*3600*1000).toISOString().slice(0,10);
195
+ const MONTH_AGO = () => new Date(Date.now()-30*24*3600*1000).toISOString().slice(0,10);
196
+
197
+ Chart.defaults.color = '#7d8590';
198
+ Chart.defaults.borderColor = '#30363d';
199
+ Chart.defaults.font.family = "'SF Mono','Fira Code',monospace";
200
+ Chart.defaults.font.size = 11;
201
+
202
+ async function fetchData() {
203
+ const r = await fetch('/api/data');
204
+ const d = await r.json();
205
+ sessions = d.sessions;
206
+ active = d.active;
207
+ document.getElementById('lastUpdate').textContent = 'actualizado: ' + new Date().toLocaleTimeString('es-CL');
208
+ renderAll();
209
+ }
210
+
211
+ function renderAll() {
212
+ renderCards();
213
+ renderActive();
214
+ renderCharts();
215
+ renderTable();
216
+ }
217
+
218
+ function renderCards() {
219
+ const d = sessions;
220
+ const today = TODAY();
221
+ const tc = d.reduce((s,r)=>s+r.cost,0);
222
+ const td = d.filter(r=>r.date===today).reduce((s,r)=>s+r.cost,0);
223
+ const tt = d.reduce((s,r)=>s+r.input_tokens+r.output_tokens,0);
224
+ const t5 = d.reduce((s,r)=>s+r.five_h_delta,0);
225
+ const tds = d.filter(r=>r.date===today).length;
226
+ const avg5 = d.length ? t5/d.length : 0;
227
+ const weekCost = d.filter(r=>r.date>=WEEK_AGO()).reduce((s,r)=>s+r.cost,0);
228
+
229
+ const c5color = t5>100?'var(--red)':t5>50?'var(--yellow)':'var(--green)';
230
+ document.getElementById('cards').innerHTML = \`
231
+ <div class="card"><div class="label">Costo total</div><div class="value" style="color:var(--yellow)">\$\${tc.toFixed(2)}</div><div class="sub">\${d.length} sesiones</div></div>
232
+ <div class="card"><div class="label">Hoy</div><div class="value" style="color:var(--green)">\$\${td.toFixed(2)}</div><div class="sub">\${tds} sesión(es)</div></div>
233
+ <div class="card"><div class="label">Esta semana</div><div class="value" style="color:var(--blue)">\$\${weekCost.toFixed(2)}</div><div class="sub">\${d.filter(r=>r.date>=WEEK_AGO()).length} sesiones</div></div>
234
+ <div class="card"><div class="label">Tokens totales</div><div class="value" style="color:var(--cyan)">\${(tt/1000).toFixed(0)}k</div><div class="sub">\${d.length?(tt/d.length/1000).toFixed(0):0}k prom/sesión</div></div>
235
+ <div class="card"><div class="label">5h consumido total</div><div class="value" style="color:\${c5color}">\${t5.toFixed(1)}%</div><div class="sub">\${avg5.toFixed(1)}% prom/sesión</div></div>
236
+ <div class="card"><div class="label">Activas ahora</div><div class="value" style="color:var(--purple)">\${active.length}</div><div class="sub">\${active.length?active.map(a=>a.cwd?a.cwd.split('/').pop():'?').join(', '):'ninguna'}</div></div>
237
+ \`;
238
+ }
239
+
240
+ function barHTML(pct, color) {
241
+ const w = Math.min(pct||0, 100);
242
+ return \`<div class="mini-bar"><div class="track"><div class="fill" style="width:\${w}%;background:\${color}"></div></div>\${(pct||0).toFixed(1)}%</div>\`;
243
+ }
244
+
245
+ function renderActive() {
246
+ const el = document.getElementById('activeGrid');
247
+ document.getElementById('activeCount').textContent = active.length ? \`\${active.length} activa(s)\` : '';
248
+ if (!active.length) {
249
+ el.innerHTML = '<div class="empty-state">Sin sesiones activas en las últimas 2 horas</div>';
250
+ return;
251
+ }
252
+ el.innerHTML = active.map(a => {
253
+ const proj = a.cwd ? a.cwd.split('/').pop() : '?';
254
+ const sid = (a.session_id||'').slice(0,8);
255
+ const cost = (a.cost_usd||0).toFixed(2);
256
+ const tok = Math.round(((a.total_input_tokens||0)+(a.total_output_tokens||0))/1000);
257
+ const ctx = a.ctx_used_pct||0;
258
+ const five_h = a.five_h_pct||0;
259
+ const delta = a.five_h_delta||0;
260
+ const ctxColor = ctx>=80?'var(--red)':ctx>=50?'var(--yellow)':'var(--blue)';
261
+ const fiveColor = five_h>=80?'var(--red)':five_h>=50?'var(--yellow)':'var(--green)';
262
+ return \`
263
+ <div class="active-card">
264
+ <div class="proj">\${proj}</div>
265
+ <div class="sess">\${sid} · \${a.model||''}</div>
266
+ <div class="bar-row"><span class="bar-label">Costo</span><span style="color:var(--yellow);font-weight:600">\$\${cost}</span></div>
267
+ <div class="bar-row"><span class="bar-label">Tokens</span><span style="color:var(--cyan)">\${tok}k</span></div>
268
+ <div class="bar-row">
269
+ <span class="bar-label">Ctx</span>
270
+ <div class="bar-track"><div class="bar-fill" style="width:\${Math.min(ctx,100)}%;background:\${ctxColor}"></div></div>
271
+ <span class="bar-val">\${ctx.toFixed(1)}%</span>
272
+ </div>
273
+ <div class="bar-row">
274
+ <span class="bar-label">5h acum</span>
275
+ <div class="bar-track"><div class="bar-fill" style="width:\${Math.min(five_h,100)}%;background:\${fiveColor}"></div></div>
276
+ <span class="bar-val">\${five_h.toFixed(1)}%</span>
277
+ </div>
278
+ <div class="bar-row"><span class="bar-label">5h sesión</span><span style="color:var(--cyan);font-weight:600">+\${delta.toFixed(1)}%</span></div>
279
+ </div>
280
+ \`;
281
+ }).join('');
282
+ }
283
+
284
+ function groupByDay(data, field) {
285
+ const m = {};
286
+ data.forEach(r => { m[r.date] = (m[r.date]||0) + r[field]; });
287
+ return m;
288
+ }
289
+
290
+ function renderCharts() {
291
+ const today30 = new Date(Date.now()-30*24*3600*1000).toISOString().slice(0,10);
292
+ const d30 = sessions.filter(r => r.date >= today30);
293
+ const days = [...new Set(d30.map(r=>r.date))].sort();
294
+
295
+ const costByDay = groupByDay(d30, 'cost');
296
+ const inByDay = groupByDay(d30, 'input_tokens');
297
+ const outByDay = groupByDay(d30, 'output_tokens');
298
+ const last30sess = sessions.slice(-30);
299
+
300
+ // Cost by day
301
+ rebuildChart('cCost', {
302
+ type: 'bar',
303
+ data: { labels: days, datasets: [{ label: 'USD', data: days.map(d=>costByDay[d]||0), backgroundColor: 'rgba(57,211,247,.3)', borderColor: '#39d3f7', borderWidth: 1, borderRadius: 3 }] },
304
+ options: { plugins:{legend:{display:false}}, scales:{x:{ticks:{maxRotation:45}}, y:{ticks:{callback:v=>'\$'+v.toFixed(2)}}} }
305
+ });
306
+
307
+ // 5h per session
308
+ rebuildChart('cFiveH', {
309
+ type: 'bar',
310
+ data: {
311
+ labels: last30sess.map(r=>r.date.slice(5)+' '+r.time),
312
+ datasets: [{ label: '% 5h', data: last30sess.map(r=>r.five_h_delta),
313
+ backgroundColor: last30sess.map(r=>r.five_h_delta>=20?'rgba(248,81,73,.4)':r.five_h_delta>=10?'rgba(210,153,34,.4)':'rgba(57,211,247,.3)'),
314
+ borderColor: last30sess.map(r=>r.five_h_delta>=20?'#f85149':r.five_h_delta>=10?'#d29922':'#39d3f7'),
315
+ borderWidth:1, borderRadius:3 }]
316
+ },
317
+ options: { plugins:{legend:{display:false}}, scales:{x:{ticks:{maxRotation:45,maxTicksLimit:15}},y:{title:{display:true,text:'%'}}} }
318
+ });
319
+
320
+ // Tokens stacked
321
+ rebuildChart('cTokens', {
322
+ type: 'bar',
323
+ data: { labels: days, datasets: [
324
+ { label:'Input', data: days.map(d=>(inByDay[d]||0)/1000), backgroundColor:'rgba(88,166,255,.4)', borderColor:'#58a6ff', borderWidth:1, borderRadius:3 },
325
+ { label:'Output', data: days.map(d=>(outByDay[d]||0)/1000), backgroundColor:'rgba(188,140,255,.4)', borderColor:'#bc8cff', borderWidth:1, borderRadius:3 },
326
+ ]},
327
+ options: { plugins:{legend:{position:'top'}}, scales:{x:{stacked:true,ticks:{maxRotation:45}},y:{stacked:true,title:{display:true,text:'k tokens'}}} }
328
+ });
329
+
330
+ // Projects donut
331
+ const byProj = {};
332
+ sessions.forEach(r=>{ byProj[r.project]=(byProj[r.project]||0)+1; });
333
+ const projKeys = Object.keys(byProj).sort((a,b)=>byProj[b]-byProj[a]).slice(0,10);
334
+ const palette = ['#39d3f7','#3fb950','#d29922','#f85149','#bc8cff','#f0883e','#58a6ff','#7d8590','#56d364','#e3b341'];
335
+ rebuildChart('cProjects', {
336
+ type: 'doughnut',
337
+ data: { labels: projKeys, datasets: [{ data: projKeys.map(p=>byProj[p]), backgroundColor: palette.map(c=>c+'99'), borderColor: palette, borderWidth: 2 }] },
338
+ options: { plugins:{legend:{position:'right',labels:{boxWidth:12}}}, cutout:'60%' }
339
+ });
340
+ }
341
+
342
+ function rebuildChart(id, config) {
343
+ if (charts[id]) { charts[id].destroy(); }
344
+ charts[id] = new Chart(document.getElementById(id), config);
345
+ }
346
+
347
+ function sort(key) {
348
+ sortKey === key ? sortDir *= -1 : (sortKey=key, sortDir=-1);
349
+ renderTable();
350
+ }
351
+
352
+ function renderTable() {
353
+ const today = TODAY();
354
+ const period = document.getElementById('filterPeriod').value;
355
+ const text = document.getElementById('filterText').value.toLowerCase();
356
+ let data = sessions.filter(r => {
357
+ if (period==='today' && r.date!==today) return false;
358
+ if (period==='week' && r.date<WEEK_AGO()) return false;
359
+ if (period==='month' && r.date<MONTH_AGO()) return false;
360
+ if (text && !JSON.stringify(r).toLowerCase().includes(text)) return false;
361
+ return true;
362
+ }).sort((a,b) => {
363
+ const [av,bv]=[a[sortKey],b[sortKey]];
364
+ return typeof av==='string' ? sortDir*av.localeCompare(bv) : sortDir*(av-bv);
365
+ });
366
+
367
+ const tb = document.getElementById('tableBody');
368
+ if (!data.length) { tb.innerHTML='<tr><td colspan="11" style="text-align:center;color:var(--muted);padding:24px">Sin datos</td></tr>'; return; }
369
+
370
+ tb.innerHTML = data.map(r => {
371
+ const cc = r.cost>1?'pr':r.cost>0.3?'py':'pg';
372
+ const ctxC = r.ctx_pct>=80?'#f85149':r.ctx_pct>=50?'#d29922':'#58a6ff';
373
+ const fendC = r.five_h_end>=80?'#f85149':r.five_h_end>=50?'#d29922':'#3fb950';
374
+ const fdC = r.five_h_delta>=20?'#f85149':r.five_h_delta>=10?'#d29922':'#39d3f7';
375
+ return \`<tr>
376
+ <td style="color:var(--muted)">\${r.date}</td>
377
+ <td style="color:var(--muted)">\${r.time}</td>
378
+ <td style="color:var(--cyan)">\${r.session_id}</td>
379
+ <td>\${r.project}</td>
380
+ <td><span class="pill \${cc}">\$\${r.cost.toFixed(2)}</span></td>
381
+ <td style="color:var(--muted)">\${(r.input_tokens/1000).toFixed(0)}k</td>
382
+ <td style="color:var(--muted)">\${(r.output_tokens/1000).toFixed(0)}k</td>
383
+ <td>\${barHTML(r.ctx_pct,ctxC)}</td>
384
+ <td>\${barHTML(r.five_h_end,fendC)}</td>
385
+ <td>\${barHTML(r.five_h_delta,fdC)}</td>
386
+ <td style="color:var(--muted)">\${r.model}</td>
387
+ </tr>\`;
388
+ }).join('');
389
+ }
390
+
391
+ // Auto-refresh cada 30s con countdown
392
+ let countdown = 30;
393
+ function tick() {
394
+ countdown--;
395
+ document.getElementById('refreshTimer').textContent = \`próx. actualización: \${countdown}s\`;
396
+ if (countdown <= 0) { countdown = 30; fetchData(); }
397
+ }
398
+
399
+ fetchData();
400
+ setInterval(tick, 1000);
401
+ </script>
402
+ </body>
403
+ </html>`;
404
+ }
405
+
406
+ // ── HTTP server ────────────────────────────────────────────────────────────────
407
+
408
+ const server = http.createServer((req, res) => {
409
+ const url = req.url.split('?')[0];
410
+
411
+ if (url === '/api/data') {
412
+ try {
413
+ const data = { sessions: readLog(), active: readActiveSessions() };
414
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
415
+ res.end(JSON.stringify(data));
416
+ } catch (e) {
417
+ res.writeHead(500); res.end(JSON.stringify({ error: e.message }));
418
+ }
419
+ return;
420
+ }
421
+
422
+ if (url === '/') {
423
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
424
+ res.end(dashboardHTML());
425
+ return;
426
+ }
427
+
428
+ res.writeHead(404); res.end('Not found');
429
+ });
430
+
431
+ server.listen(PORT, '127.0.0.1', () => {
432
+ console.log(`Claude Monitor corriendo en http://localhost:${PORT}`);
433
+ });
434
+
435
+ server.on('error', err => {
436
+ if (err.code === 'EADDRINUSE') {
437
+ console.error(`Puerto ${PORT} ocupado. ¿Ya está corriendo?`);
438
+ process.exit(1);
439
+ }
440
+ console.error(err);
441
+ });