agy-insights 0.8.0 → 0.8.2

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.2`.
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.2`.
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
 
@@ -35,7 +35,7 @@ It is undocumented; this is what has been observed in `agy` 1.4.x.
35
35
  - Table `steps`: one row per step. `step_payload` is a protobuf message. Tool calls store the name (`view_file`, `run_command`, `grep_search`…) followed by their JSON arguments, for example `{"AbsolutePath":"C:\\dev\\...\\X.cs","StartLine":301,"EndLine":380}`, preceded by a `call_NNN` id.
36
36
  - Each call takes two steps with the same id: the model response that requests it (`step_type` 15) and the result (`step_type` 132), which repeats name and arguments and adds the file content or command output. `lib/history.js` counts the call once per id and adds the size of both steps, which approximates what entered the context.
37
37
  - Table `gen_metadata`: one row per model call, in the same order as the transcript's `PLANNER_RESPONSE` lines. Each row holds the model's display name with the effort level in brackets (`Gemini 3.8 Flash (High)`) and its id (`gemini-3.8-flash-high`). `modelOf()` takes the model and effort from whole strings only, so prose in the system prompt that mentions a model is ignored, and the n-th row is matched to the n-th call to get tokens per model and effort. That pairing is only trusted when the counts match. Otherwise each call takes the model selected in the transcript (`USER_INPUT` lines carry "changed setting `Model Selection` from X to Y"), or the conversation's only model; what is left is "unknown". `modelKey()` merges the two spellings (`Gemini 3.8 Flash` / `gemini-3.8-flash`, `High` / `high`). `--debug-models` prints, per conversation, how many calls got their model from each source: counts only, safe to paste into an issue.
38
- - Next to the history, `../brain/<id>/.system_generated/logs/transcript.jsonl`: one JSON line per step. `PLANNER_RESPONSE` lines are model calls and carry `input_tokens`, `cache_read_tokens` and `output_tokens`; failed steps have `status: "ERROR"`. Subagents have their own conversation and transcript. `realUsage()` in `lib/history.js` adds those figures up (it only reads the file). The total printed by `agy -p --output-format json` does not include subagents; this one does.
38
+ - Next to the history, `../brain/<id>/.system_generated/logs/transcript.jsonl`: one JSON line per step. `PLANNER_RESPONSE` lines are model calls and carry `input_tokens`, `cache_read_tokens` and `output_tokens`; failed steps have `status: "ERROR"`. Subagents have their own conversation and transcript. `realUsage()` in `lib/history.js` adds those figures up (it only reads the file). The total printed by `agy -p --output-format json` does not include subagents; this one does. Some logs (seen with the VS Code plugin) have `PLANNER_RESPONSE` lines without any token field; those are not counted as 0 tokens. The same figures are also in each `gen_metadata` row, at protobuf field 1 › 4 (2 = input, 3 = output, 5 = cache read; the model's display name is at 1 › 21). When the transcript has no counts, or there is no transcript, `usageFromMeta()` takes real usage from there, keeping the day and selected model of the transcript's n-th call when there is one.
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
 
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
@@ -114,6 +114,39 @@ function collect(blob) {
114
114
  return out;
115
115
  }
116
116
 
117
+ // The top-level fields of one protobuf message: tag -> varint value or bytes (last one wins). Null if malformed.
118
+ function fields(buf) {
119
+ const out = new Map();
120
+ let i = 0;
121
+ const varint = () => {
122
+ let v = 0, s = 0, b;
123
+ do { if (i >= buf.length) throw 0; b = buf[i++]; v += (b & 0x7f) * 2 ** s; s += 7; } while (b & 0x80);
124
+ return v;
125
+ };
126
+ try {
127
+ while (i < buf.length) {
128
+ const k = varint(), tag = Math.floor(k / 8), wt = k & 7;
129
+ if (wt === 0) out.set(tag, varint());
130
+ else if (wt === 1) i += 8;
131
+ else if (wt === 5) i += 4;
132
+ else if (wt === 2) { const len = varint(); if (i + len > buf.length) throw 0; out.set(tag, buf.subarray(i, i + len)); i += len; } else throw 0;
133
+ }
134
+ return out;
135
+ } catch { return null; }
136
+ }
137
+
138
+ // Token counts of one gen_metadata row (one model call): field 1 › 4 holds 2 = input, 3 = output,
139
+ // 5 = cache read, the same figures the transcript logs. Null when the row has none: some logs (seen
140
+ // with the VS Code plugin) carry no token counts, and this is then the only place that has them.
141
+ function metaUsage(blob) {
142
+ const top = blob && fields(Buffer.from(blob));
143
+ const call = Buffer.isBuffer(top?.get(1)) && fields(top.get(1));
144
+ const u = Buffer.isBuffer(call?.get(4)) && fields(call.get(4));
145
+ if (!u || !(u.has(2) || u.has(3))) return null;
146
+ const n = (k) => (typeof u.get(k) === 'number' ? u.get(k) : 0);
147
+ return { input: n(2), output: n(3), cache: n(5) };
148
+ }
149
+
117
150
  // A tool call is stored as the tool name followed (within a few strings) by its JSON arguments,
118
151
  // e.g. "view_file" + {"AbsolutePath":"C:\\dev\\...\\X.cs","StartLine":301,"EndLine":380}.
119
152
  // The call id ("call_5859111") just before the name identifies the call: agy stores each call twice,
@@ -154,7 +187,7 @@ function kind(p, { inRepo = false } = {}) {
154
187
  // are never reported as generated or proposed for exclusion, whatever their names look like.
155
188
  if (/(^|\/)\.gemini\//i.test(p)) return 'agent';
156
189
  if (/(^|\/)node_modules\//.test(p)) return 'node_modules';
157
- if (!inRepo && /(^|\/)(dist|build|out|target|coverage|\.next|bin|obj)\//i.test(p)) return 'build';
190
+ if (!inRepo && /(^|\/)(dist|build|out|target|coverage|allure-results|allure-report|\.next|bin|obj)\//i.test(p)) return 'build';
158
191
  if (/(package-lock\.json|yarn\.lock|pnpm-lock\.yaml|packages\.lock\.json|\.lock)$/.test(p)) return 'lockfiles';
159
192
  if (/\.(log|map)$|\.min\.(js|css)$/.test(p)) return 'logs';
160
193
  if (/(^|\/)(tests?|__tests__|specs?)\//i.test(p) || /\.(test|spec|tests?)\.\w+$/i.test(p) || /Tests?\//.test(p)) return 'tests';
@@ -221,7 +254,10 @@ function realUsage(dir, id) {
221
254
  try { text = fs.readFileSync(file, 'utf8'); } catch { return null; }
222
255
  // `days`: the same figures per local day of each model call (calls without a time go under null).
223
256
  // `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: [] };
257
+ // `calls` counts only the calls that carry token counts; some logs (seen with the VS Code plugin) list
258
+ // calls without them, and counting those as 0 tokens would drag every per-call figure down.
259
+ // `unmetered`: those other calls, and `unmeteredKeys` the field names they do have, for --debug-models.
260
+ 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
261
  let selected = null;
226
262
  u.unrecognized = new Set();
227
263
  for (const line of text.split('\n')) {
@@ -236,11 +272,17 @@ function realUsage(dir, id) {
236
272
  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
273
  }
238
274
  if (e.type !== 'PLANNER_RESPONSE') continue;
275
+ if (!['input_tokens', 'cache_read_tokens', 'output_tokens'].some((k) => k in e)) {
276
+ u.unmetered++;
277
+ for (const k of Object.keys(e)) if (k.length < 40) u.unmeteredKeys.add(k);
278
+ u.perCall.push({ metered: false, model: selected, at });
279
+ continue;
280
+ }
239
281
  u.calls++;
240
282
  u.input += Number(e.input_tokens) || 0;
241
283
  u.cache += Number(e.cache_read_tokens) || 0;
242
284
  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 });
285
+ 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
286
  const k = at ? dayKey(at) : null;
245
287
  const d = u.days.get(k) || { calls: 0, input: 0, cache: 0, output: 0 };
246
288
  d.calls++;
@@ -252,6 +294,27 @@ function realUsage(dir, id) {
252
294
  return u;
253
295
  }
254
296
 
297
+ // Real usage rebuilt from gen_metadata, one row per model call, for conversations whose log has no token
298
+ // counts (or no log). Each row's call keeps the model and day of the log's n-th call when there is one.
299
+ function usageFromMeta(blobs, log, end) {
300
+ const rows = blobs.map(metaUsage);
301
+ if (!rows.some(Boolean)) return null;
302
+ const u = { calls: 0, input: 0, cache: 0, output: 0, errors: log?.errors || 0, start: log?.start ?? null, end: log?.end ?? null,
303
+ days: new Map(), perCall: [], unmetered: 0, unmeteredKeys: new Set(), unrecognized: log?.unrecognized || new Set(), fromMeta: true };
304
+ rows.forEach((x, i) => {
305
+ const from = log?.perCall[i];
306
+ if (!x) { u.perCall.push({ metered: false, model: from?.model || null }); return; }
307
+ u.calls++;
308
+ u.input += x.input; u.cache += x.cache; u.output += x.output;
309
+ u.perCall.push({ metered: true, ...x, model: from?.model || null });
310
+ const k = dayKey(from?.at || log?.end || end);
311
+ const d = u.days.get(k) || { calls: 0, input: 0, cache: 0, output: 0 };
312
+ d.calls++; d.input += x.input; d.cache += x.cache; d.output += x.output;
313
+ u.days.set(k, d);
314
+ });
315
+ return u;
316
+ }
317
+
255
318
  // --- Titles ---
256
319
 
257
320
  // The short title agy gives each conversation ("Analyzing Package Lock Dependencies"), from
@@ -317,9 +380,11 @@ async function analyze({ dir = DEFAULT_DIR, workspaces = [], customKinds = {}, s
317
380
  for (const f of dbs) {
318
381
  // When it happened: the transcript's step times, else the database's last write.
319
382
  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;
383
+ const log = realUsage(dir, id);
384
+ const end = log?.end ?? fs.statSync(path.join(dir, f)).mtimeMs;
385
+ const start = log?.start ?? end;
386
+ // Real usage from the log when it has token counts; else from gen_metadata (below), else the estimate.
387
+ let usage = log?.calls ? log : null;
323
388
  if (since && end < since) { r.olderSkipped++; continue; }
324
389
  let db;
325
390
  try { db = await openDb(path.join(dir, f), tmp); } catch { r.unreadable++; continue; }
@@ -379,6 +444,8 @@ async function analyze({ dir = DEFAULT_DIR, workspaces = [], customKinds = {}, s
379
444
  files.add(key(file), { ...fields, convs: once(seenFiles, key(file)) }, { abs: file, rel, root, project, kind: k });
380
445
  });
381
446
  }
447
+ const metaBlobs = db.all('select data from gen_metadata order by idx').map(([blob]) => blob);
448
+ if (!usage) usage = usageFromMeta(metaBlobs, log, end);
382
449
  if (usage) {
383
450
  r.usage.conversations++;
384
451
  for (const k of ['calls', 'input', 'cache', 'output', 'errors']) r.usage[k] += usage[k];
@@ -393,9 +460,9 @@ async function analyze({ dir = DEFAULT_DIR, workspaces = [], customKinds = {}, s
393
460
  // Model of each call. gen_metadata usually has one row per call, in the transcript's order; when the
394
461
  // counts match, the n-th row is the n-th call. When they don't, pairing by position would mislabel
395
462
  // calls, so the model selected in the transcript is used instead, or the conversation's only model.
396
- const metaStrs = db.all('select data from gen_metadata order by idx').map(([blob]) => collect(blob));
463
+ const metaStrs = metaBlobs.map(collect);
397
464
  const metaRows = metaStrs.map(modelOf);
398
- const calls = usage ? usage.perCall : metaRows.map(() => null);
465
+ const calls = usage?.fromMeta ? usage.perCall : log ? log.perCall : metaRows.map(() => null);
399
466
  const aligned = metaRows.length === calls.length;
400
467
  const rowModels = new Set(metaRows.filter((m) => m.model).map(modelKey));
401
468
  const onlyModel = rowModels.size === 1 ? metaRows.find((m) => m.model) : null;
@@ -412,7 +479,7 @@ async function analyze({ dir = DEFAULT_DIR, workspaces = [], customKinds = {}, s
412
479
  e.calls++;
413
480
  if (!seenModels.has(k)) { seenModels.add(k); e.convs++; }
414
481
  const c = calls[i];
415
- if (c) { e.known++; e.input += c.input; e.cache += c.cache; e.output += c.output; }
482
+ if (c?.metered) { e.known++; e.input += c.input; e.cache += c.cache; e.output += c.output; }
416
483
  models.set(k, e);
417
484
  if (m.model) counts.set(k, (counts.get(k) || 0) + 1);
418
485
  });
@@ -426,7 +493,8 @@ async function analyze({ dir = DEFAULT_DIR, workspaces = [], customKinds = {}, s
426
493
  // the short strings that look like a model id (to spot a new format). Never prompts, paths or code.
427
494
  modelSources: { rows: metaRows.length, rowsWithModel: metaRows.filter((m) => m.model).length, calls: calls.length,
428
495
  fromTranscript: calls.filter((c) => c?.model).length, unknown: callModels.filter((m) => !m.model).length,
429
- candidates: [...new Set([...(usage?.unrecognized || []),
496
+ unmetered: log?.unmetered || 0, unmeteredKeys: [...(log?.unmeteredKeys || [])].sort(),
497
+ candidates: [...new Set([...(log?.unrecognized || []),
430
498
  ...metaStrs.filter((_, i) => !metaRows[i].model).flat().filter(looksLikeModel)])].slice(0, 10) },
431
499
  });
432
500
  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.2",
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": [