agy-insights 0.8.0 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.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.8.0`.
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.1`.
40
40
 
41
41
  **Instalándolo globalmente:**
42
42
 
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.8.0`.
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.1`.
40
40
 
41
41
  **Globally:**
42
42
 
package/agy-insights.js CHANGED
@@ -86,14 +86,15 @@ function sinceTime({ since, days }) {
86
86
  // Tab-separated table of where each conversation's call models came from, for bug reports.
87
87
  function debugModels(r) {
88
88
  const d = t.debugModels;
89
- const sum = { rows: 0, rowsWithModel: 0, calls: 0, fromTranscript: 0, unknown: 0 };
89
+ const sum = { rows: 0, rowsWithModel: 0, calls: 0, fromTranscript: 0, unknown: 0, unmetered: 0 };
90
90
  const lines = [d.header.join('\t')];
91
91
  for (const s of r.sessions) {
92
92
  const m = s.modelSources;
93
93
  for (const k of Object.keys(sum)) sum[k] += m[k];
94
- lines.push([s.id.slice(0, 8), m.calls, m.rows, m.rowsWithModel, m.fromTranscript, m.unknown, s.model ? `${s.model.model} (${s.model.effort})` : '', m.unknown ? m.candidates.join(' | ') : ''].join('\t'));
94
+ lines.push([s.id.slice(0, 8), m.calls, m.rows, m.rowsWithModel, m.fromTranscript, m.unknown, s.model ? `${s.model.model} (${s.model.effort})` : '', m.unknown ? m.candidates.join(' | ') : '',
95
+ m.unmetered || '', m.unmetered ? m.unmeteredKeys.join(' ') : ''].join('\t'));
95
96
  }
96
- lines.push([d.total, sum.calls, sum.rows, sum.rowsWithModel, sum.fromTranscript, sum.unknown, '', ''].join('\t'), '', d.note, '');
97
+ lines.push([d.total, sum.calls, sum.rows, sum.rowsWithModel, sum.fromTranscript, sum.unknown, '', '', sum.unmetered || '', ''].join('\t'), '', d.note, '');
97
98
  return lines.join('\n');
98
99
  }
99
100
 
package/lib/findings.js CHANGED
@@ -10,7 +10,7 @@ const DEFAULTS = { crossSessionMin: 3, heavyOutputTokens: 2000, longSessionToken
10
10
  // Suggested .antigravityignore patterns for the built-in non-source types.
11
11
  const KIND_PATTERNS = {
12
12
  'node_modules': ['node_modules/'],
13
- 'build': ['bin/', 'obj/', 'dist/', 'build/', 'out/', 'target/', 'coverage/'],
13
+ 'build': ['bin/', 'obj/', 'dist/', 'build/', 'out/', 'target/', 'coverage/', 'allure-results/', 'allure-report/'],
14
14
  'lockfiles': ['*.lock', 'package-lock.json', 'pnpm-lock.yaml', 'packages.lock.json'],
15
15
  'logs': ['*.log', '*.map', '*.min.js', '*.min.css'],
16
16
  'generated': ['*.designer.cs', '*.g.cs', 'Migrations/'],
@@ -126,8 +126,9 @@ function findings(r, opts = {}) {
126
126
  const size = (s) => (s.usage ? s.usage.input + s.usage.output : s.tokens);
127
127
  const long = r.sessions.filter((s) => size(s) >= o.longSessionTokens);
128
128
  if (long.length) {
129
+ // `exact`: every one has real usage, so the header shows the same figures as its items, as a share of real usage.
129
130
  out.push({
130
- id: 'long-sessions', level: 'info', title: f.longTitle, tokens: sum(long, (s) => s.tokens),
131
+ id: 'long-sessions', level: 'info', title: f.longTitle, tokens: sum(long, size), exact: long.every((s) => s.usage),
131
132
  why: f.longWhy(long.length, num(o.longSessionTokens)), suggestion: f.longSuggestion,
132
133
  items: long.slice(0, 5).map((s) => ({
133
134
  // Real tokens when agy logged them (`exact`: shown without "~"), else the size estimate.
package/lib/history.js CHANGED
@@ -154,7 +154,7 @@ function kind(p, { inRepo = false } = {}) {
154
154
  // are never reported as generated or proposed for exclusion, whatever their names look like.
155
155
  if (/(^|\/)\.gemini\//i.test(p)) return 'agent';
156
156
  if (/(^|\/)node_modules\//.test(p)) return 'node_modules';
157
- if (!inRepo && /(^|\/)(dist|build|out|target|coverage|\.next|bin|obj)\//i.test(p)) return 'build';
157
+ if (!inRepo && /(^|\/)(dist|build|out|target|coverage|allure-results|allure-report|\.next|bin|obj)\//i.test(p)) return 'build';
158
158
  if (/(package-lock\.json|yarn\.lock|pnpm-lock\.yaml|packages\.lock\.json|\.lock)$/.test(p)) return 'lockfiles';
159
159
  if (/\.(log|map)$|\.min\.(js|css)$/.test(p)) return 'logs';
160
160
  if (/(^|\/)(tests?|__tests__|specs?)\//i.test(p) || /\.(test|spec|tests?)\.\w+$/i.test(p) || /Tests?\//.test(p)) return 'tests';
@@ -221,7 +221,10 @@ function realUsage(dir, id) {
221
221
  try { text = fs.readFileSync(file, 'utf8'); } catch { return null; }
222
222
  // `days`: the same figures per local day of each model call (calls without a time go under null).
223
223
  // `perCall`: each call's figures, in order, and the model selected in the transcript when it was made.
224
- const u = { calls: 0, input: 0, cache: 0, output: 0, errors: 0, start: null, end: null, days: new Map(), perCall: [] };
224
+ // `calls` counts only the calls that carry token counts; some logs (seen with the VS Code plugin) list
225
+ // calls without them, and counting those as 0 tokens would drag every per-call figure down.
226
+ // `unmetered`: those other calls, and `unmeteredKeys` the field names they do have, for --debug-models.
227
+ const u = { calls: 0, input: 0, cache: 0, output: 0, errors: 0, start: null, end: null, days: new Map(), perCall: [], unmetered: 0, unmeteredKeys: new Set() };
225
228
  let selected = null;
226
229
  u.unrecognized = new Set();
227
230
  for (const line of text.split('\n')) {
@@ -236,11 +239,17 @@ function realUsage(dir, id) {
236
239
  for (const m of String(e.content).matchAll(MODEL_SETTING_RE)) if (!modelOf([m[1].trim()]).model) u.unrecognized.add(m[1].trim().slice(0, 60));
237
240
  }
238
241
  if (e.type !== 'PLANNER_RESPONSE') continue;
242
+ if (!['input_tokens', 'cache_read_tokens', 'output_tokens'].some((k) => k in e)) {
243
+ u.unmetered++;
244
+ for (const k of Object.keys(e)) if (k.length < 40) u.unmeteredKeys.add(k);
245
+ u.perCall.push({ metered: false, model: selected });
246
+ continue;
247
+ }
239
248
  u.calls++;
240
249
  u.input += Number(e.input_tokens) || 0;
241
250
  u.cache += Number(e.cache_read_tokens) || 0;
242
251
  u.output += Number(e.output_tokens) || 0;
243
- u.perCall.push({ input: Number(e.input_tokens) || 0, cache: Number(e.cache_read_tokens) || 0, output: Number(e.output_tokens) || 0, model: selected });
252
+ u.perCall.push({ metered: true, input: Number(e.input_tokens) || 0, cache: Number(e.cache_read_tokens) || 0, output: Number(e.output_tokens) || 0, model: selected });
244
253
  const k = at ? dayKey(at) : null;
245
254
  const d = u.days.get(k) || { calls: 0, input: 0, cache: 0, output: 0 };
246
255
  d.calls++;
@@ -317,9 +326,11 @@ async function analyze({ dir = DEFAULT_DIR, workspaces = [], customKinds = {}, s
317
326
  for (const f of dbs) {
318
327
  // When it happened: the transcript's step times, else the database's last write.
319
328
  const id = f.replace(/\.db$/, '');
320
- const usage = realUsage(dir, id);
321
- const end = usage?.end ?? fs.statSync(path.join(dir, f)).mtimeMs;
322
- const start = usage?.start ?? end;
329
+ const log = realUsage(dir, id);
330
+ const end = log?.end ?? fs.statSync(path.join(dir, f)).mtimeMs;
331
+ const start = log?.start ?? end;
332
+ // Real usage only when the log has token counts; otherwise the session is shown with the estimate.
333
+ const usage = log?.calls ? log : null;
323
334
  if (since && end < since) { r.olderSkipped++; continue; }
324
335
  let db;
325
336
  try { db = await openDb(path.join(dir, f), tmp); } catch { r.unreadable++; continue; }
@@ -395,7 +406,7 @@ async function analyze({ dir = DEFAULT_DIR, workspaces = [], customKinds = {}, s
395
406
  // calls, so the model selected in the transcript is used instead, or the conversation's only model.
396
407
  const metaStrs = db.all('select data from gen_metadata order by idx').map(([blob]) => collect(blob));
397
408
  const metaRows = metaStrs.map(modelOf);
398
- const calls = usage ? usage.perCall : metaRows.map(() => null);
409
+ const calls = log ? log.perCall : metaRows.map(() => null);
399
410
  const aligned = metaRows.length === calls.length;
400
411
  const rowModels = new Set(metaRows.filter((m) => m.model).map(modelKey));
401
412
  const onlyModel = rowModels.size === 1 ? metaRows.find((m) => m.model) : null;
@@ -412,7 +423,7 @@ async function analyze({ dir = DEFAULT_DIR, workspaces = [], customKinds = {}, s
412
423
  e.calls++;
413
424
  if (!seenModels.has(k)) { seenModels.add(k); e.convs++; }
414
425
  const c = calls[i];
415
- if (c) { e.known++; e.input += c.input; e.cache += c.cache; e.output += c.output; }
426
+ if (c?.metered) { e.known++; e.input += c.input; e.cache += c.cache; e.output += c.output; }
416
427
  models.set(k, e);
417
428
  if (m.model) counts.set(k, (counts.get(k) || 0) + 1);
418
429
  });
@@ -426,7 +437,8 @@ async function analyze({ dir = DEFAULT_DIR, workspaces = [], customKinds = {}, s
426
437
  // the short strings that look like a model id (to spot a new format). Never prompts, paths or code.
427
438
  modelSources: { rows: metaRows.length, rowsWithModel: metaRows.filter((m) => m.model).length, calls: calls.length,
428
439
  fromTranscript: calls.filter((c) => c?.model).length, unknown: callModels.filter((m) => !m.model).length,
429
- candidates: [...new Set([...(usage?.unrecognized || []),
440
+ unmetered: log?.unmetered || 0, unmeteredKeys: [...(log?.unmeteredKeys || [])].sort(),
441
+ candidates: [...new Set([...(log?.unrecognized || []),
430
442
  ...metaStrs.filter((_, i) => !metaRows[i].model).flat().filter(looksLikeModel)])].slice(0, 10) },
431
443
  });
432
444
  r.tokens += convBytes / BYTES_PER_TOKEN;
package/lib/i18n.js CHANGED
@@ -35,7 +35,7 @@ Options:
35
35
  mapDone: (tokens, file) => `\n~${tokens} tokens on every request. Paste it into ${file} if you like it; agy-insights does not write it.`,
36
36
  reportName: 'report',
37
37
  debugModels: {
38
- header: ['conversation', 'calls', 'gen_metadata rows', 'rows with model', 'calls with model in transcript', 'unknown', 'main model', 'unrecognized model ids'],
38
+ header: ['conversation', 'calls', 'gen_metadata rows', 'rows with model', 'calls with model in transcript', 'unknown', 'main model', 'unrecognized model ids', 'calls without tokens', 'fields of those calls'],
39
39
  total: 'total',
40
40
  note: 'Only counts and model names or ids: no paths, prompts or code. Paste it into an issue if models show up as unknown.',
41
41
  },
@@ -162,7 +162,7 @@ Options:
162
162
  since: (date) => `active since ${date}`,
163
163
  olderSkipped: (n) => `${plural(n, 'older conversation', 'older conversations')} left out by the date filter`,
164
164
  periodLine: (period, since) => `Period: ${period}${since ? ` (filter: ${since})` : ''}`,
165
- footerReal: (missing) => `Real tokens: the ones Antigravity records in each conversation's log (subagents included)${missing ? `; ${plural(missing, 'conversation has', 'conversations have')} no log and ${missing === 1 ? 'is' : 'are'} not counted` : ''}.`,
165
+ footerReal: (missing) => `Real tokens: the ones Antigravity records in each conversation's log (subagents included)${missing ? `; ${plural(missing, 'conversation has', 'conversations have')} no token counts in its log and ${missing === 1 ? 'is' : 'are'} not counted` : ''}.`,
166
166
  footerCache: 'It is unknown whether the quota counts cache the same as new input, so both figures are shown.',
167
167
  footerEstimate: '~tokens = stored size of each step / 4: useful to compare files and folders with each other.',
168
168
  footerPrivacy: 'The history was read from a copy and was not modified. This page contains paths from your projects: review it before sharing.',
@@ -211,7 +211,7 @@ Opciones:
211
211
  mapDone: (tokens, file) => `\n~${tokens} tokens en cada petición. Pégalo en ${file} si te convence; agy-insights no lo escribe.`,
212
212
  reportName: 'informe',
213
213
  debugModels: {
214
- header: ['conversación', 'llamadas', 'filas gen_metadata', 'filas con modelo', 'llamadas con modelo en transcript', 'desconocidas', 'modelo principal', 'ids de modelo no reconocidos'],
214
+ header: ['conversación', 'llamadas', 'filas gen_metadata', 'filas con modelo', 'llamadas con modelo en transcript', 'desconocidas', 'modelo principal', 'ids de modelo no reconocidos', 'llamadas sin tokens', 'campos de esas llamadas'],
215
215
  total: 'total',
216
216
  note: 'Solo recuentos y nombres o ids de modelo: ni rutas, ni prompts, ni código. Pégalo en una incidencia si salen modelos desconocidos.',
217
217
  },
@@ -338,7 +338,7 @@ Opciones:
338
338
  since: (date) => `con actividad desde el ${date}`,
339
339
  olderSkipped: (n) => `${plural(n, 'conversación anterior queda fuera', 'conversaciones anteriores quedan fuera')} por el filtro de fechas`,
340
340
  periodLine: (period, since) => `Periodo: ${period}${since ? ` (filtro: ${since})` : ''}`,
341
- footerReal: (missing) => `Tokens reales: los que Antigravity anota en el registro de cada conversación (incluidos subagentes)${missing ? `; ${missing === 1 ? '1 conversación no tiene registro y no cuenta' : `${missing} conversaciones no tienen registro y no cuentan`}` : ''}.`,
341
+ footerReal: (missing) => `Tokens reales: los que Antigravity anota en el registro de cada conversación (incluidos subagentes)${missing ? `; ${missing === 1 ? '1 conversación no tiene tokens en su registro y no cuenta' : `${missing} conversaciones no tienen tokens en su registro y no cuentan`}` : ''}.`,
342
342
  footerCache: 'No se sabe si la cuota cuenta la caché igual que la entrada nueva: por eso se muestran las dos cifras.',
343
343
  footerEstimate: '~tokens = tamaño guardado de cada paso / 4: sirve para comparar ficheros y carpetas entre sí.',
344
344
  footerPrivacy: 'El historial se ha leído desde una copia y no se ha modificado. Esta página contiene rutas de tus proyectos: no la compartas sin revisarla.',
package/lib/report.js CHANGED
@@ -38,9 +38,12 @@ function item(it) {
38
38
  <span class="num">${it.tokens ? `${it.exact ? '' : '~'}${fmt(it.tokens)}` : ''}</span></div>${extra}</li>`;
39
39
  }
40
40
 
41
- function finding(f, total) {
41
+ // A finding's share: of real usage when its figure is real (`exact`), else of the estimated context.
42
+ const share = (f, r) => (f.exact ? billed(r.usage) : r.tokens);
43
+
44
+ function finding(f, r) {
42
45
  return `<section class="finding ${f.level}">
43
- <h3>${esc(f.title)}${f.tokens ? ` <span class="tok">~${fmt(f.tokens)} tokens · ${pct(f.tokens, total)}</span>` : ''}</h3>
46
+ <h3>${esc(f.title)}${f.tokens ? ` <span class="tok">${f.exact ? '' : '~'}${fmt(f.tokens)} tokens · ${pct(f.tokens, share(f, r))}</span>` : ''}</h3>
44
47
  <p>${esc(f.why)}</p><p class="hint">${esc(f.suggestion)}</p>
45
48
  ${f.items.length ? `<ul class="items">${f.items.map(item).join('')}</ul>` : ''}
46
49
  </section>`;
@@ -188,7 +191,7 @@ function body(r, found, scope) {
188
191
  ${modelsTable(r)}
189
192
  <h2>${t.report.happening}</h2>
190
193
  <p class="hint">${t.report.happeningHint}</p>
191
- ${found.length ? found.map((f) => finding(f, r.tokens)).join('') : `<p>${t.report.nothing}</p>`}
194
+ ${found.length ? found.map((f) => finding(f, r)).join('') : `<p>${t.report.nothing}</p>`}
192
195
  <details class="detail-all"><summary><h2>${t.report.details}</h2></summary>
193
196
  ${sessionsTable(r)}
194
197
  ${table(t.report.byProject, r.projects, r.tokens, (x) => x.project || t.report.outsideGit)}
@@ -358,7 +361,7 @@ function text(r, found, { scope, paths }) {
358
361
  }
359
362
  }
360
363
  for (const f of found) {
361
- out.push('', `${f.level === 'warn' ? '⚠' : '•'} ${f.title}${f.tokens ? ` (~${fmt(f.tokens)} tokens, ${pct(f.tokens, r.tokens)})` : ''}`,
364
+ out.push('', `${f.level === 'warn' ? '⚠' : '•'} ${f.title}${f.tokens ? ` (${f.exact ? '' : '~'}${fmt(f.tokens)} tokens, ${pct(f.tokens, share(f, r))})` : ''}`,
362
365
  ` ${f.why}`, ` → ${f.suggestion}`);
363
366
  // Only file and folder labels are paths; commands and session ids are always shown.
364
367
  const hasPaths = ['rereads', 'non-source', 'cross-session'].includes(f.id);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agy-insights",
3
- "version": "0.8.0",
3
+ "version": "0.8.1",
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": [