agy-insights 0.7.3 → 0.8.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/README.es.md CHANGED
@@ -36,7 +36,7 @@ npx agy-insights
36
36
  npx agy-insights@latest --text
37
37
  ```
38
38
 
39
- `@latest` hace que npx compruebe cada vez si hay una versión nueva. Sin él, una copia instalada globalmente (`npm install -g`) o en el proyecto actual tiene prioridad y no se actualiza nunca. Para fijar una versión concreta: `npx agy-insights@0.7.3`.
39
+ `@latest` hace que npx compruebe cada vez si hay una versión nueva. Sin él, una copia instalada globalmente (`npm install -g`) o en el proyecto actual tiene prioridad y no se actualiza nunca. Para fijar una versión concreta: `npx agy-insights@0.8.0`.
40
40
 
41
41
  **Instalándolo globalmente:**
42
42
 
@@ -61,7 +61,7 @@ Y ejecútalo con `node agy-insights/agy-insights.js` en lugar de `npx agy-insigh
61
61
  npx agy-insights
62
62
  ```
63
63
 
64
- Analiza todo tu historial (todas las conversaciones que guarde agy, sin límite de fecha), guarda un informe HTML en la carpeta temporal y lo abre en el navegador. En el informe puedes cambiar entre todo el historial y los últimos 90, 30 o 7 días: los totales, la gráfica, los hallazgos y las tablas siguen el periodo elegido.
64
+ Analiza todo tu historial (todas las conversaciones que guarde agy, sin límite de fecha), guarda un informe HTML en la carpeta temporal y lo abre en el navegador. En el informe puedes cambiar entre todo el historial y los últimos 90 o 30 días, o esta semana (desde el lunes): los totales, la gráfica, los hallazgos y las tablas siguen el periodo elegido.
65
65
 
66
66
  | Para… | Ejecuta |
67
67
  |---|---|
@@ -82,7 +82,7 @@ El informe da dos cifras:
82
82
 
83
83
  ## Leer el informe
84
84
 
85
- Arriba están el selector de periodo y los totales. Después, una gráfica de **consumo de tokens por día** (por semana si el historial pasa de ~4 meses) para ver la evolución; pasa el ratón por una columna para ver sus cifras, o abre «Ver como tabla». Después, **Modelos y esfuerzo**: llamadas y tokens reales por modelo y nivel de esfuerzo (por ejemplo Gemini 3.8 Flash · Alto), con los tokens por llamada, para ver cuánto cuesta un esfuerzo mayor. Debajo, **Qué está pasando** reúne los hallazgos; al final, el detalle por proyecto, tipo de fichero, carpeta, fichero, herramienta y comando. El proyecto de cada fichero se detecta por su carpeta `.git`.
85
+ Arriba están el selector de periodo y los totales. Después, una gráfica de **consumo de tokens por día** (por semana si el historial pasa de ~4 meses) para ver la evolución. Cámbiala a **Por llamada** para ver los tokens medios de cada llamada al modelo: el total sube y baja según cuánto hayas trabajado, mientras que por llamada solo baja si cada llamada lleva menos contexto, así que es la que muestra si un cambio ha servido. Pasa el ratón por una columna para ver sus cifras, o abre «Ver como tabla». Después, **Modelos y esfuerzo**: llamadas y tokens reales por modelo y nivel de esfuerzo (por ejemplo Gemini 3.8 Flash · Alto), con los tokens por llamada, para ver cuánto cuesta un esfuerzo mayor. Debajo, **Qué está pasando** reúne los hallazgos; al final, el detalle por proyecto, tipo de fichero, carpeta, fichero, herramienta y comando. El proyecto de cada fichero se detecta por su carpeta `.git`.
86
86
 
87
87
  | Hallazgo | Qué significa | Qué hacer |
88
88
  |---|---|---|
package/README.md CHANGED
@@ -36,7 +36,7 @@ npx agy-insights
36
36
  npx agy-insights@latest --text
37
37
  ```
38
38
 
39
- `@latest` makes npx check for the newest version every time. Without it, a copy installed globally (`npm install -g`) or in the current project takes precedence and is never updated. To pin a version instead: `npx agy-insights@0.7.3`.
39
+ `@latest` makes npx check for the newest version every time. Without it, a copy installed globally (`npm install -g`) or in the current project takes precedence and is never updated. To pin a version instead: `npx agy-insights@0.8.0`.
40
40
 
41
41
  **Globally:**
42
42
 
@@ -61,7 +61,7 @@ Then run it with `node agy-insights/agy-insights.js` instead of `npx agy-insight
61
61
  npx agy-insights
62
62
  ```
63
63
 
64
- Analyzes your whole history (every conversation agy has kept, with no date limit), saves an HTML report in the temp folder and opens it in the browser. In the report you can switch between the whole history and the last 90, 30 or 7 days: totals, chart, findings and tables all follow the selected period.
64
+ Analyzes your whole history (every conversation agy has kept, with no date limit), saves an HTML report in the temp folder and opens it in the browser. In the report you can switch between the whole history and the last 90 or 30 days, or this week (since Monday): totals, chart, findings and tables all follow the selected period.
65
65
 
66
66
  | To… | Run |
67
67
  |---|---|
@@ -82,7 +82,7 @@ The report gives two figures:
82
82
 
83
83
  ## Reading the report
84
84
 
85
- At the top, the period selector and the totals. Then a chart of **token usage per day** (per week when the history spans more than ~4 months), to see how usage evolves; hover a column for its figures, or open "View as table". Next, **Models and effort**: calls and real tokens per model and effort level (for example Gemini 3.8 Flash · High), including tokens per call, so you can see what a higher effort costs. Below, **What's happening** lists the findings; at the end, the details by project, file type, folder, file, tool and command. The project of each file is detected through its `.git` folder.
85
+ At the top, the period selector and the totals. Then a chart of **token usage per day** (per week when the history spans more than ~4 months), to see how usage evolves. Switch it to **Per call** to see mean tokens per model call: the total follows how much you worked, while per call drops only when each call carries less context, so it is the one that shows whether a change paid off. Hover a column for its figures, or open "View as table". Next, **Models and effort**: calls and real tokens per model and effort level (for example Gemini 3.8 Flash · High), including tokens per call, so you can see what a higher effort costs. Below, **What's happening** lists the findings; at the end, the details by project, file type, folder, file, tool and command. The project of each file is detected through its `.git` folder.
86
86
 
87
87
  | Finding | What it means | What to do |
88
88
  |---|---|---|
package/agy-insights.js CHANGED
@@ -98,7 +98,15 @@ function debugModels(r) {
98
98
  }
99
99
 
100
100
  // Periods the HTML report can switch to, besides the whole history.
101
- const PRESET_DAYS = [90, 30, 7];
101
+ const PRESET_DAYS = [90, 30];
102
+
103
+ // This Monday at local midnight.
104
+ function weekStart() {
105
+ const d = new Date();
106
+ d.setHours(0, 0, 0, 0);
107
+ d.setDate(d.getDate() - ((d.getDay() + 6) % 7));
108
+ return d.getTime();
109
+ }
102
110
 
103
111
  async function main() {
104
112
  const argv = process.argv.slice(2);
@@ -144,6 +152,8 @@ async function main() {
144
152
  const x = await run(Date.now() - days * 24 * 60 * 60 * 1000);
145
153
  views.push({ key: `d${days}`, label: t.report.rangeDays(days), r: x, found: find(x) });
146
154
  }
155
+ const week = await run(weekStart());
156
+ views.push({ key: 'week', label: t.report.rangeWeek, r: week, found: find(week) });
147
157
  }
148
158
  fs.writeFileSync(out, report.htmlViews(views, { scope: opts.project }));
149
159
  console.log(t.done(r.conversations, found.length, out));
@@ -39,7 +39,7 @@ It is undocumented; this is what has been observed in `agy` 1.4.x.
39
39
 
40
40
  Each `transcript.jsonl` line also has `created_at` (ISO time). A conversation is dated by the first and last of those; without a transcript, by the `.db` file's last write. `--since` / `--days` keep a conversation when its last activity falls inside the period, and skip the rest before opening them. `../conversation_summaries.db` has one row per conversation: its short `title` (or `preview` in older ones) names each session in the report instead of its id; `last_modified_time` is not used. If the file is missing, sessions are shown by id. Each model call's `created_at` also puts its tokens on a local day (`r.daily`), which feeds the usage chart.
41
41
 
42
- The HTML report holds one view per period (whole history, last 90, 30 and 7 days): `agy-insights.js` runs `analyze()` and `findings()` once per period and `report.htmlViews()` renders them all, so switching in the browser needs no analysis code in the page and every number agrees with its period. The chart is inline SVG; its color (`--series`) is checked for contrast, chroma and lightness against both surfaces.
42
+ The HTML report holds one view per period (whole history, last 90 and 30 days, this week since Monday): `agy-insights.js` runs `analyze()` and `findings()` once per period and `report.htmlViews()` renders them all, so switching in the browser needs no analysis code in the page and every number agrees with its period. The chart is inline SVG; its color (`--series`) is checked for contrast, chroma and lightness against both surfaces.
43
43
 
44
44
  `lib/history.js` does not depend on a protobuf schema. It walks the message, collects every string, and treats as a tool call any JSON preceded (within 3 strings) by a `snake_case` name. That way it survives minor format changes. If there are more than 50 steps and no recognized call, it sets `formatUnrecognized` and the report warns about it.
45
45
 
package/lib/i18n.js CHANGED
@@ -142,12 +142,19 @@ Options:
142
142
  filterLabel: 'Period',
143
143
  rangeAll: 'All history',
144
144
  rangeDays: (n) => `Last ${n} days`,
145
+ rangeWeek: 'This week',
145
146
  chartTitleDay: 'Token usage per day',
146
147
  chartTitleWeek: 'Token usage per week',
147
148
  chartHint: 'Real tokens (input + output) of the model calls. Hover a column for details.',
148
149
  chartTable: 'View as table',
149
- chartCols: ['Day', 'Tokens', 'With cache', 'Calls'],
150
- chartColsWeek: ['Week of', 'Tokens', 'With cache', 'Calls'],
150
+ chartModeLabel: 'Show',
151
+ chartModeTotal: 'Total',
152
+ chartModeCall: 'Per call',
153
+ chartHintCall: 'Mean tokens (input + output) per model call. Unlike the total, it doesn\'t depend on how much you used the agent: it drops when each call carries less context (shorter sessions, filtered command output), so it shows whether a change worked.',
154
+ perCallTip: (n) => `${n} tokens per call`,
155
+ noActivity: 'No activity',
156
+ chartCols: ['Day', 'Tokens', 'With cache', 'Calls', 'Per call'],
157
+ chartColsWeek: ['Week of', 'Tokens', 'With cache', 'Calls', 'Per call'],
151
158
  chartCalls: (n) => plural(n, 'call', 'calls'),
152
159
  weekOf: (d) => `week of ${d}`,
153
160
  withCacheShort: 'with cache',
@@ -311,12 +318,19 @@ Opciones:
311
318
  filterLabel: 'Periodo',
312
319
  rangeAll: 'Todo el historial',
313
320
  rangeDays: (n) => `Últimos ${n} días`,
321
+ rangeWeek: 'Esta semana',
314
322
  chartTitleDay: 'Consumo de tokens por día',
315
323
  chartTitleWeek: 'Consumo de tokens por semana',
316
324
  chartHint: 'Tokens reales (entrada + salida) de las llamadas al modelo. Pasa el ratón por una columna para ver el detalle.',
317
325
  chartTable: 'Ver como tabla',
318
- chartCols: ['Día', 'Tokens', 'Con caché', 'Llamadas'],
319
- chartColsWeek: ['Semana del', 'Tokens', 'Con caché', 'Llamadas'],
326
+ chartModeLabel: 'Mostrar',
327
+ chartModeTotal: 'Total',
328
+ chartModeCall: 'Por llamada',
329
+ chartHintCall: 'Tokens medios (entrada + salida) por llamada al modelo. A diferencia del total, no depende de cuánto hayas usado el agente: baja cuando cada llamada lleva menos contexto (sesiones más cortas, salidas de comandos filtradas), así que muestra si un cambio ha funcionado.',
330
+ perCallTip: (n) => `${n} tokens por llamada`,
331
+ noActivity: 'Sin actividad',
332
+ chartCols: ['Día', 'Tokens', 'Con caché', 'Llamadas', 'Por llamada'],
333
+ chartColsWeek: ['Semana del', 'Tokens', 'Con caché', 'Llamadas', 'Por llamada'],
320
334
  chartCalls: (n) => plural(n, 'llamada', 'llamadas'),
321
335
  weekOf: (d) => `semana del ${d}`,
322
336
  withCacheShort: 'con caché',
package/lib/report.js CHANGED
@@ -115,12 +115,33 @@ function buckets(r) {
115
115
  return { weekly, rows: [...byKey.values()] };
116
116
  }
117
117
 
118
+ // Mean tokens per call: unlike the total, it doesn't follow how much the agent was used that day.
119
+ const perCall = (x) => (x.calls ? x.billed / x.calls : 0);
120
+
118
121
  function chart(r) {
119
122
  const b = buckets(r);
120
123
  if (!b) return '';
124
+ const title = b.weekly ? t.report.chartTitleWeek : t.report.chartTitleDay;
125
+ const rows = b.rows.filter((x) => x.calls);
126
+ return `<section class="chart">
127
+ <h2>${title}</h2>
128
+ <div class="mode" role="group" aria-label="${esc(t.report.chartModeLabel)}">
129
+ <button type="button" data-mode="total" aria-pressed="true">${t.report.chartModeTotal}</button><button type="button" data-mode="call" aria-pressed="false">${t.report.chartModeCall}</button></div>
130
+ <p class="hint" data-mode="total">${t.report.chartHint}</p>
131
+ <p class="hint" data-mode="call" hidden>${t.report.chartHintCall}</p>
132
+ <div class="plot" data-mode="total">${plot(b, (x) => x.billed, (x) => `${fmt(x.billed)} tokens`, (x) => `${fmt(x.billed + x.cache)} ${t.report.withCacheShort}`, title)}</div>
133
+ <div class="plot" data-mode="call" hidden>${plot(b, perCall, (x) => t.report.perCallTip(fmt(perCall(x))), (x) => `${fmt(perCall(x) + x.cache / x.calls)} ${t.report.withCacheShort}`, `${title} · ${t.report.chartModeCall}`)}</div>
134
+ <details><summary>${t.report.chartTable}</summary><div class="scroll"><table><thead><tr>${(b.weekly ? t.report.chartColsWeek : t.report.chartCols).map((c, i) => `<th${i ? ' class="num"' : ''}>${c}</th>`).join('')}</tr></thead><tbody>
135
+ ${rows.map((x) => `<tr><td>${esc(shortDate(x.date, true))}</td><td class="num">${fmt(x.billed)}</td><td class="num">${fmt(x.billed + x.cache)}</td><td class="num">${x.calls}</td><td class="num">${fmt(perCall(x))}</td></tr>`).join('')}
136
+ </tbody></table></div></details>
137
+ </section>`;
138
+ }
139
+
140
+ // One column chart of `value` per bucket; `tip` and `cache` are the tooltip's headline and cache line.
141
+ function plot(b, value, tip, cache, label) {
121
142
  const W = 900, H = 240, m = { l: 56, r: 8, t: 12, b: 28 };
122
143
  const pw = W - m.l - m.r, ph = H - m.t - m.b;
123
- const top = niceMax(Math.max(...b.rows.map((x) => x.billed)));
144
+ const top = niceMax(Math.max(...b.rows.map(value)));
124
145
  const band = pw / b.rows.length;
125
146
  const bw = Math.max(1, Math.min(24, band - 2));
126
147
  const y = (v) => m.t + ph - (v / top) * ph;
@@ -130,28 +151,20 @@ function chart(r) {
130
151
  <text x="${m.l - 8}" y="${y(v) + 4}" text-anchor="end" class="axis">${esc(compact(v))}</text>`).join('');
131
152
  // Rounded data end (4px), square at the baseline.
132
153
  const bars = b.rows.map((x, i) => {
133
- const h = (x.billed / top) * ph;
154
+ const h = (value(x) / top) * ph;
134
155
  if (h <= 0) return '';
135
156
  const x0 = m.l + i * band + (band - bw) / 2, y0 = m.t + ph - h, rr = Math.min(4, h, bw / 2);
136
157
  return `<path class="col" d="M${x0},${m.t + ph}V${y0 + rr}Q${x0},${y0} ${x0 + rr},${y0}H${x0 + bw - rr}Q${x0 + bw},${y0} ${x0 + bw},${y0 + rr}V${m.t + ph}Z"/>`;
137
158
  }).join('');
138
159
  // Hit targets: the whole band, so a 2px column is as easy to hover as a 24px one.
139
160
  const hits = b.rows.map((x, i) => `<rect class="hit" x="${m.l + i * band}" y="${m.t}" width="${band}" height="${ph}"
140
- data-when="${esc(when(x))}" data-v="${esc(fmt(x.billed))}" data-c="${esc(fmt(x.billed + x.cache))}" data-n="${esc(t.report.chartCalls(x.calls))}"/>`).join('');
161
+ data-when="${esc(when(x))}" data-v="${esc(x.calls ? tip(x) : t.report.noActivity)}" data-c="${esc(x.calls ? cache(x) : '')}" data-n="${esc(x.calls ? t.report.chartCalls(x.calls) : '')}"/>`).join('');
141
162
  const every = Math.max(1, Math.ceil(b.rows.length / 6));
142
163
  // A date every few columns, plus the last one when it isn't crowded by the previous label.
143
164
  const last = b.rows.length - 1;
144
165
  const ticks = b.rows.map((x, i) => (i % every === 0 || (i === last && last % every >= every / 2)) && `<text x="${i === 0 && band < 40 ? m.l : i === last && band < 40 ? W - m.r : m.l + i * band + band / 2}" y="${H - 8}" text-anchor="${i === 0 && band < 40 ? 'start' : i === last && band < 40 ? 'end' : 'middle'}" class="axis">${esc(shortDate(x.date))}</text>`).filter(Boolean).join('');
145
166
 
146
- const rows = b.rows.filter((x) => x.calls);
147
- return `<section class="chart">
148
- <h2>${b.weekly ? t.report.chartTitleWeek : t.report.chartTitleDay}</h2>
149
- <p class="hint">${t.report.chartHint}</p>
150
- <div class="plot"><svg viewBox="0 0 ${W} ${H}" role="img" aria-label="${esc(b.weekly ? t.report.chartTitleWeek : t.report.chartTitleDay)}">${grid}${bars}${ticks}${hits}</svg></div>
151
- <details><summary>${t.report.chartTable}</summary><div class="scroll"><table><thead><tr>${(b.weekly ? t.report.chartColsWeek : t.report.chartCols).map((c, i) => `<th${i ? ' class="num"' : ''}>${c}</th>`).join('')}</tr></thead><tbody>
152
- ${rows.map((x) => `<tr><td>${esc(shortDate(x.date, true))}</td><td class="num">${fmt(x.billed)}</td><td class="num">${fmt(x.billed + x.cache)}</td><td class="num">${x.calls}</td></tr>`).join('')}
153
- </tbody></table></div></details>
154
- </section>`;
167
+ return `<svg viewBox="0 0 ${W} ${H}" role="img" aria-label="${esc(label)}">${grid}${bars}${ticks}${hits}</svg>`;
155
168
  }
156
169
 
157
170
  // --- Page ---
@@ -219,6 +232,12 @@ function htmlViews(views, { scope }) {
219
232
  .filter button:hover { background: var(--accent-soft); }
220
233
  .filter button[aria-pressed="true"] { background: var(--accent); border-color: var(--accent); color: var(--surface); font-weight: 600; }
221
234
  .filter button:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
235
+ .mode { display: inline-flex; margin: 0 0 0 12px; vertical-align: 3px; border: 1px solid var(--line); border-radius: 6px; overflow: hidden; }
236
+ .mode button { font: 13px system-ui, sans-serif; color: var(--fg); background: var(--surface); border: 0; padding: 3px 10px; cursor: pointer; }
237
+ .mode button + button { border-left: 1px solid var(--line); }
238
+ .mode button:hover { background: var(--accent-soft); }
239
+ .mode button[aria-pressed="true"] { background: var(--accent-soft); color: var(--accent); font-weight: 600; }
240
+ .mode button:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
222
241
  .tiles { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 12px; margin: 16px 0 20px; }
223
242
  .tiles div { background: var(--surface); border: 1px solid var(--line); border-radius: 8px; padding: 12px 14px; }
224
243
  .tiles b { display: block; font-size: 24px; font-variant-numeric: tabular-nums; } .tiles span { color: var(--muted); font-size: 14px; }
@@ -271,6 +290,13 @@ function htmlViews(views, { scope }) {
271
290
  for (const s of document.querySelectorAll('.view')) s.hidden = s.dataset.view !== v.dataset.view;
272
291
  return;
273
292
  }
293
+ // Chart mode: the same in every period, so switching periods keeps it.
294
+ const m = e.target.closest('.mode button');
295
+ if (m) {
296
+ for (const b of document.querySelectorAll('.mode button')) b.setAttribute('aria-pressed', String(b.dataset.mode === m.dataset.mode));
297
+ for (const p of document.querySelectorAll('.chart [data-mode]:not(button)')) p.hidden = p.dataset.mode !== m.dataset.mode;
298
+ return;
299
+ }
274
300
  const b = e.target.closest('.copy'); if (!b) return;
275
301
  const pre = b.parentElement.querySelector('pre');
276
302
  const ok = () => { b.textContent = ${JSON.stringify(t.report.copied)}; setTimeout(() => (b.textContent = ${JSON.stringify(t.report.copy)}), 1500); };
@@ -285,9 +311,10 @@ function htmlViews(views, { scope }) {
285
311
  const h = e.target.closest && e.target.closest('.hit');
286
312
  if (!h) { tip.hidden = true; return; }
287
313
  const [v, when, rest] = tip.children;
288
- v.textContent = h.dataset.v + ' tokens';
314
+ v.textContent = h.dataset.v;
289
315
  when.textContent = h.dataset.when;
290
- rest.textContent = h.dataset.n + ' · ' + h.dataset.c + ' ' + ${JSON.stringify(t.report.withCacheShort)};
316
+ rest.textContent = h.dataset.c ? h.dataset.n + ' · ' + h.dataset.c : h.dataset.n;
317
+ rest.hidden = !rest.textContent;
291
318
  tip.hidden = false;
292
319
  const x = Math.min(e.clientX + 14, innerWidth - tip.offsetWidth - 8);
293
320
  tip.style.left = x + 'px';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agy-insights",
3
- "version": "0.7.3",
3
+ "version": "0.8.0",
4
4
  "description": "Measures Antigravity CLI token usage and shows which context is sent repeatedly or needlessly, in English or Spanish. Run: npx agy-insights@latest",
5
5
  "license": "MIT",
6
6
  "keywords": [