agy-insights 0.9.0 → 0.9.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.9.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.9.1`.
40
40
 
41
41
  **Instalándolo globalmente:**
42
42
 
@@ -78,11 +78,11 @@ Analiza todo tu historial (todas las conversaciones que guarde agy, sin límite
78
78
  El informe da dos cifras:
79
79
 
80
80
  - **Consumo real**: los tokens que Antigravity anota en el registro de cada conversación (`~/.gemini/antigravity-cli/brain/<id>/.system_generated/logs/transcript.jsonl`), subagentes incluidos. Se muestra sin caché y con caché, porque no sabemos si la cuota cuenta igual la caché. Es la cifra que sirve para comparar entre compañeros.
81
- - **«~tokens»**: estimados a partir del tamaño guardado de cada paso. Sirven para comparar qué ficheros, carpetas o comandos pesan más, pero se quedan cortos frente al consumo real, porque cada llamada al modelo vuelve a enviar el contexto acumulado.
81
+ - **«~tokens»**: estimados a partir del tamaño guardado de cada paso. Sirven para comparar qué ficheros, carpetas o comandos pesan más, pero se quedan cortos frente al consumo real, porque cada llamada al modelo vuelve a enviar el contexto acumulado. Las imágenes (capturas de pantalla, por ejemplo) se estiman por su tamaño en píxeles, como las cuenta Gemini (258 tokens por bloque de 768 px), y no por sus bytes.
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. 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». Si usas el CLI y también el editor o la extensión de VS Code, **Por origen** reparte las conversaciones y los tokens reales entre ellos (el editor y la extensión comparten historial, así que cuentan juntos), y cada sesión indica desde dónde se tuvo. 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». Si usas el CLI y también el IDE de Antigravity, su app o una extensión de editor como la de VS Code, **Por origen** reparte las conversaciones y los tokens reales entre «CLI» e «IDE y extensiones» (estos comparten historial, así que cuentan juntos), y cada sesión indica desde dónde se tuvo. 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
  |---|---|---|
@@ -98,7 +98,10 @@ Despliega **Cómo excluirlo** en una propuesta. Verás el fichero exacto, dónde
98
98
 
99
99
  ### 1. Denegar la lectura (permiso de Antigravity)
100
100
 
101
- Es lo único que impide de verdad que el agente lea una ruta. Usa los [permisos oficiales](https://antigravity.google/docs/permissions).
101
+ Es lo único que impide de verdad que el agente lea una ruta. Usa los [permisos oficiales](https://antigravity.google/docs/permissions). Dónde va la regla depende de qué Antigravity leyó el fichero, y el informe propone el sitio correcto:
102
+
103
+ - **IDE, app o extensión de editor** (como la de VS Code): no tienen un fichero de ajustes que editar. Añade la regla (`read_file(<ruta>)`) como regla de denegación en sus **Settings → Permission Grants**, para todos los proyectos o para uno.
104
+ - **CLI (`agy`)**:
102
105
 
103
106
  1. Abre `~/.gemini/antigravity-cli/settings.json` (en Windows, `%USERPROFILE%\.gemini\antigravity-cli\settings.json`).
104
107
  2. Pega la regla dentro de `"permissions"` → `"deny"`. Si no existen, créalas; si existen, añade la regla sin borrar nada:
@@ -116,7 +119,7 @@ Es lo único que impide de verdad que el agente lea una ruta. Usa los [permisos
116
119
 
117
120
  3. Guarda y reinicia `agy`.
118
121
 
119
- Denegar la lectura de una ruta también impide escribir en ella, y la regla vale para todos tus proyectos. Úsala para carpetas que el agente no deba tocar nunca, como salidas de compilación.
122
+ Denegar la lectura de una ruta también impide escribir en ella, y la regla vale para todos tus proyectos. Úsala para carpetas que el agente no deba tocar nunca, como salidas de compilación. Nunca se propone nada de `~/.gemini` ni de las extensiones de editor de Antigravity: Antigravity necesita esos ficheros para funcionar.
120
123
 
121
124
  ### 2. `.antigravityignore`
122
125
 
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.9.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.9.1`.
40
40
 
41
41
  **Globally:**
42
42
 
@@ -78,11 +78,11 @@ Analyzes your whole history (every conversation agy has kept, with no date limit
78
78
  The report gives two figures:
79
79
 
80
80
  - **Real usage**: the tokens Antigravity records in each conversation's log (`~/.gemini/antigravity-cli/brain/<id>/.system_generated/logs/transcript.jsonl`), subagents included. It is shown with and without cache, because it is unknown whether the quota counts cache the same way. This is the figure to compare between teammates.
81
- - **"~tokens"**: estimated from the stored size of each step. Useful to compare which files, folders or commands weigh the most, but they fall short of real usage, because every model call resends the accumulated context.
81
+ - **"~tokens"**: estimated from the stored size of each step. Useful to compare which files, folders or commands weigh the most, but they fall short of real usage, because every model call resends the accumulated context. Images (screenshots, for example) are estimated by their size in pixels, as Gemini counts them (258 tokens per 768 px tile), not by their bytes.
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. 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". If you use both the CLI and the editor or the VS Code extension, **By origin** splits conversations and real tokens between them (the editor and the extension share their history, so they count together), and each session shows where it was held. 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". If you use both the CLI and the Antigravity IDE, app or an editor extension such as VS Code's, **By origin** splits conversations and real tokens between "CLI" and "IDE and extensions" (those share their history, so they count together), and each session shows where it was held. 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
  |---|---|---|
@@ -98,7 +98,10 @@ Expand **How to exclude it** on a proposal. You'll see the exact file, where the
98
98
 
99
99
  ### 1. Deny reading it (Antigravity permission)
100
100
 
101
- This is the only thing that actually stops the agent from reading a path. It uses the [official permissions](https://antigravity.google/docs/permissions).
101
+ This is the only thing that actually stops the agent from reading a path. It uses the [official permissions](https://antigravity.google/docs/permissions). Where the rule goes depends on which Antigravity read the file, and the report proposes the right place:
102
+
103
+ - **IDE, app or editor extension** (such as VS Code's): they have no settings file to edit. Add the rule (`read_file(<path>)`) as a deny rule in their **Settings → Permission Grants**, for all projects or for one.
104
+ - **CLI (`agy`)**:
102
105
 
103
106
  1. Open `~/.gemini/antigravity-cli/settings.json` (on Windows, `%USERPROFILE%\.gemini\antigravity-cli\settings.json`).
104
107
  2. Paste the rule inside `"permissions"` → `"deny"`. Create them if they don't exist; if they do, add the rule without removing anything:
@@ -116,7 +119,7 @@ This is the only thing that actually stops the agent from reading a path. It use
116
119
 
117
120
  3. Save and restart `agy`.
118
121
 
119
- Denying reads on a path also blocks writing to it, and the rule applies to all your projects. Use it for folders the agent should never touch, such as build output.
122
+ Denying reads on a path also blocks writing to it, and the rule applies to all your projects. Use it for folders the agent should never touch, such as build output. Nothing under `~/.gemini` or Antigravity's editor extensions is ever proposed: Antigravity needs those files to work.
120
123
 
121
124
  ### 2. `.antigravityignore`
122
125
 
@@ -102,5 +102,7 @@ node agy-insights.js --dir test/tmp/history
102
102
  ## To do
103
103
 
104
104
  - Confirm with `agy` that a `read_file` `deny` rule blocks the read.
105
+ - Confirm that a deny rule added under Settings → Permission Grants blocks the read in the VS Code extension. Antigravity's own guide (`~/.gemini/antigravity/builtin/skills/antigravity_guide/references/app.md`) documents Permission Grants for the IDE and the app; `~/.gemini/antigravity/` has no `settings.json`, so `fixes.deny()` proposes the CLI's file only for paths the CLI read, and the Settings rule for paths the editor read (from each file's `origins`).
106
+ - Image reads are estimated from the image's pixel size (`imageTokens()`: 258 tokens up to 384×384 px, else 258 per 768 px tile, as Gemini documents), read from the file's header; when the file can't be read, as 4 tiles.
105
107
  - Test with real histories from several developers (Windows and macOS).
106
108
  - Also read the history of the Antigravity app and IDE, if they use the same format.
package/lib/findings.js CHANGED
@@ -55,15 +55,16 @@ function findings(r, opts = {}) {
55
55
  const byFolder = new Map();
56
56
  for (const x of nonSource) {
57
57
  const abs = x.abs.slice(0, x.abs.lastIndexOf('/'));
58
- const e = byFolder.get(abs) || { abs, rel: x.rel.slice(0, x.rel.lastIndexOf('/')), root: x.root, project: x.project, kind: x.kind, tokens: 0, calls: 0 };
58
+ const e = byFolder.get(abs) || { abs, rel: x.rel.slice(0, x.rel.lastIndexOf('/')), root: x.root, project: x.project, kind: x.kind, tokens: 0, calls: 0, origins: [] };
59
59
  e.tokens += x.tokens;
60
60
  e.calls += x.calls;
61
+ for (const o of x.origins || []) if (!e.origins.includes(o)) e.origins.push(o);
61
62
  byFolder.set(abs, e);
62
63
  }
63
64
  const items = [...byFolder.values()].sort((a, b) => b.tokens - a.tokens).slice(0, 10).map((e) => ({
64
65
  label: `${where(e)}/`, tokens: e.tokens, detail: `${kindLabel(e.kind)} · ${f.reads(e.calls)}`,
65
66
  fixes: [
66
- fixes.deny(e.abs),
67
+ ...fixes.deny(e.abs, e.origins),
67
68
  e.root && fixes.ignore(e.root, [`${e.rel}/`]),
68
69
  e.root && fixes.rule(e.root, f.ruleFolder(e.rel, kindLabel(e.kind))),
69
70
  ].filter(Boolean),
@@ -103,7 +104,7 @@ function findings(r, opts = {}) {
103
104
  // Exclusion is only offered for non-source files: denying core source code would break the agent's work.
104
105
  items: cross.slice(0, 10).map((x) => ({
105
106
  label: where(x), tokens: x.tokens, detail: `${f.sessions(x.convs)} · ${f.reads(x.calls)}`,
106
- fixes: nonSourceKinds.has(x.kind) ? [fixes.deny(x.abs), x.root && fixes.ignore(x.root, [x.rel])].filter(Boolean) : [],
107
+ fixes: nonSourceKinds.has(x.kind) ? [...fixes.deny(x.abs, x.origins), x.root && fixes.ignore(x.root, [x.rel])].filter(Boolean) : [],
107
108
  })),
108
109
  });
109
110
  }
package/lib/fixes.js CHANGED
@@ -4,6 +4,9 @@ const fs = require('fs');
4
4
  const os = require('os');
5
5
  const path = require('path');
6
6
  const { t } = require('./i18n');
7
+ const { isAgentFile } = require('./history');
8
+
9
+ const norm = (p) => p.replace(/[\\/]+/g, '/');
7
10
 
8
11
  const AGY_SETTINGS = path.join(os.homedir(), '.gemini', 'antigravity-cli', 'settings.json');
9
12
 
@@ -20,15 +23,33 @@ function deniedRules() {
20
23
 
21
24
  // Permission rule (https://antigravity.google/docs/permissions): deny > ask > allow, a denied read_file
22
25
  // path covers everything below it, and denying a read also denies writes there.
23
- function deny(absPath) {
26
+ // The CLI reads its rules from ~/.gemini/antigravity-cli/settings.json. The IDE, the app and the editor
27
+ // extensions have no such file: their rules are set in their Settings, under Permission Grants. So the
28
+ // proposal follows `origins`, the Antigravity that read the path ('cli', 'editor'; null or none: the CLI's).
29
+ // Antigravity's own files (~/.gemini, its extensions) are never proposed: it needs them to work.
30
+ function deny(absPath, origins = ['cli']) {
31
+ if (isAgentFile(norm(absPath))) return [];
24
32
  const rule = `read_file(${native(absPath)})`;
25
- return {
26
- how: 'deny', title: t.fixes.denyTitle,
27
- file: AGY_SETTINGS, where: t.fixes.denyWhere,
28
- text: JSON.stringify(rule),
29
- note: t.fixes.denyNote,
30
- applied: deniedRules().includes(rule),
31
- };
33
+ const out = [];
34
+ if (!origins.length || origins.some((o) => o !== 'editor')) {
35
+ out.push({
36
+ how: 'deny', title: t.fixes.denyTitle,
37
+ file: AGY_SETTINGS, where: t.fixes.denyWhere,
38
+ text: JSON.stringify(rule),
39
+ note: t.fixes.denyNote,
40
+ applied: deniedRules().includes(rule),
41
+ });
42
+ }
43
+ if (origins.includes('editor')) {
44
+ out.push({
45
+ how: 'deny-editor', title: t.fixes.denyEditorTitle,
46
+ file: t.fixes.denyEditorPlace, where: t.fixes.denyEditorWhere,
47
+ text: rule,
48
+ note: t.fixes.denyEditorNote,
49
+ applied: false,
50
+ });
51
+ }
52
+ return out;
32
53
  }
33
54
 
34
55
  // .antigravityignore: Antigravity's own exclusion file. Its scope is undocumented; it is confirmed to hide
package/lib/history.js CHANGED
@@ -182,6 +182,57 @@ const key = (p) => norm(p).toLowerCase().replace(/\/+$/, '');
182
182
  // Kinds are stable ids; i18n.kindLabel() turns them into text.
183
183
  const NON_SOURCE = new Set(['node_modules', 'build', 'lockfiles', 'logs', 'generated']);
184
184
 
185
+ // Antigravity's own files: ~/.gemini (CLI, editor, brain, skills) and its editor extensions.
186
+ const isAgentFile = (p) => /(^|\/)\.gemini\//i.test(p) || /(^|\/)\.[\w-]+\/extensions\/[^/]*antigravity[^/]*\//i.test(p);
187
+
188
+ // --- Images ---
189
+
190
+ // The model gets images as pixels, not bytes: Gemini counts 258 tokens for an image up to 384×384 px, and
191
+ // 258 per 768×768 tile above that. Their stored size / 4 would count a screenshot as ~100,000 tokens.
192
+ const IMAGE_RE = /\.(png|jpe?g|gif|webp|bmp)$/i;
193
+ const TILE_TOKENS = 258;
194
+ const UNKNOWN_IMAGE_TOKENS = 4 * TILE_TOKENS; // a typical screenshot, when the file can't be read
195
+
196
+ // Width and height from the file's header (PNG, GIF, BMP, WebP, JPEG), or null.
197
+ function imageSize(file) {
198
+ let b;
199
+ try {
200
+ const fd = fs.openSync(file, 'r');
201
+ b = Buffer.alloc(64 * 1024);
202
+ b = b.subarray(0, fs.readSync(fd, b, 0, b.length, 0));
203
+ fs.closeSync(fd);
204
+ } catch { return null; }
205
+ if (b.length >= 24 && b.readUInt32BE(0) === 0x89504e47) return [b.readUInt32BE(16), b.readUInt32BE(20)];
206
+ if (b.length >= 10 && b.toString('latin1', 0, 3) === 'GIF') return [b.readUInt16LE(6), b.readUInt16LE(8)];
207
+ if (b.length >= 26 && b.toString('latin1', 0, 2) === 'BM') return [b.readInt32LE(18), Math.abs(b.readInt32LE(22))];
208
+ if (b.length >= 30 && b.toString('latin1', 0, 4) === 'RIFF' && b.toString('latin1', 8, 12) === 'WEBP') {
209
+ const f = b.toString('latin1', 12, 16);
210
+ if (f === 'VP8X') return [1 + b.readUIntLE(24, 3), 1 + b.readUIntLE(27, 3)];
211
+ if (f === 'VP8 ') return [b.readUInt16LE(26) & 0x3fff, b.readUInt16LE(28) & 0x3fff];
212
+ if (f === 'VP8L') { const v = b.readUInt32LE(21); return [1 + (v & 0x3fff), 1 + ((v >> 14) & 0x3fff)]; }
213
+ }
214
+ if (b[0] === 0xff && b[1] === 0xd8) {
215
+ for (let i = 2; i + 9 < b.length;) {
216
+ if (b[i] !== 0xff) { i++; continue; }
217
+ const m = b[i + 1];
218
+ if (m >= 0xc0 && m <= 0xcf && ![0xc4, 0xc8, 0xcc].includes(m)) return [b.readUInt16BE(i + 7), b.readUInt16BE(i + 5)];
219
+ i += 2 + b.readUInt16BE(i + 2);
220
+ }
221
+ }
222
+ return null;
223
+ }
224
+
225
+ const imageCache = new Map();
226
+ function imageTokens(file) {
227
+ if (!imageCache.has(file)) {
228
+ const size = imageSize(file);
229
+ const [w, h] = size && size[0] > 0 && size[1] > 0 ? size : [0, 0];
230
+ imageCache.set(file, !w ? UNKNOWN_IMAGE_TOKENS
231
+ : w <= 384 && h <= 384 ? TILE_TOKENS : Math.ceil(w / 768) * Math.ceil(h / 768) * TILE_TOKENS);
232
+ }
233
+ return imageCache.get(file);
234
+ }
235
+
185
236
  /**
186
237
  * @param {string} p path with "/" separators
187
238
  * @param {object} [opts]
@@ -190,12 +241,14 @@ const NON_SOURCE = new Set(['node_modules', 'build', 'lockfiles', 'logs', 'gener
190
241
  */
191
242
  function kind(p, { inRepo = false } = {}) {
192
243
  // Antigravity's own data (the agent's brain: plans, artifacts, logs; and skills): it needs them, so they
193
- // are never reported as generated or proposed for exclusion, whatever their names look like.
194
- if (/(^|\/)\.gemini\//i.test(p)) return 'agent';
244
+ // are never reported as generated or proposed for exclusion, whatever their names look like. The same for
245
+ // the Antigravity extension installed in an editor (~/.vscode/extensions/google.google-antigravity-…).
246
+ if (isAgentFile(p)) return 'agent';
195
247
  if (/(^|\/)node_modules\//.test(p)) return 'node_modules';
196
248
  if (!inRepo && /(^|\/)(dist|build|out|target|coverage|allure-results|allure-report|\.next|bin|obj)\//i.test(p)) return 'build';
197
249
  if (/(package-lock\.json|yarn\.lock|pnpm-lock\.yaml|packages\.lock\.json|\.lock)$/.test(p)) return 'lockfiles';
198
250
  if (/\.(log|map)$|\.min\.(js|css)$/.test(p)) return 'logs';
251
+ if (IMAGE_RE.test(p)) return 'images';
199
252
  if (/(^|\/)(tests?|__tests__|specs?)\//i.test(p) || /\.(test|spec|tests?)\.\w+$/i.test(p) || /Tests?\//.test(p)) return 'tests';
200
253
  if (/\.designer\.cs$|\.g\.cs$|\.generated\.\w+$|(^|\/)(generated|Migrations)\//i.test(p)) return 'generated';
201
254
  const ext = path.extname(p).toLowerCase();
@@ -345,11 +398,13 @@ async function conversationTitles(dir, tmp) {
345
398
 
346
399
  class Counter {
347
400
  constructor() { this.m = new Map(); }
348
- // Numeric fields are summed; `meta` is stored the first time a key is seen.
349
- add(k, fields, meta = {}) {
401
+ // Numeric fields are summed; `meta` is stored the first time a key is seen, except `origin`, which is
402
+ // collected into `origins` (every origin that touched the key).
403
+ add(k, fields, { origin, ...meta } = {}) {
350
404
  let e = this.m.get(k);
351
405
  if (!e) this.m.set(k, (e = { key: k, calls: 0, tokens: 0, rereads: 0, rereadTokens: 0, convs: 0, ...meta }));
352
406
  for (const [f, v] of Object.entries(fields)) e[f] = (e[f] || 0) + v;
407
+ if (origin !== undefined && !(e.origins ||= []).includes(origin)) e.origins.push(origin);
353
408
  }
354
409
  all() { return [...this.m.values()].sort((a, b) => b.tokens - a.tokens || b.calls - a.calls); }
355
410
  }
@@ -367,7 +422,8 @@ async function analyze({ dir: dirs = DEFAULT_DIRS, workspaces = [], customKinds
367
422
  const dir = (dirs.filter((d) => fs.existsSync(d)).length ? dirs.filter((d) => fs.existsSync(d)) : dirs).join(' + ');
368
423
  const custom = Object.entries(customKinds).map(([re, label]) => [new RegExp(re, 'i'), label]);
369
424
  // A file git keeps in the repo is never build output, whatever its folder is called.
370
- const classify = (p, root, rel) => custom.find(([re]) => re.test(p))?.[1]
425
+ // Antigravity's own files stay 'agent' even when a custom type matches them: they must never be excluded.
426
+ const classify = (p, root, rel) => (isAgentFile(p) ? 'agent' : custom.find(([re]) => re.test(p))?.[1])
371
427
  || kind(p, { inRepo: Boolean(root && repoFiles(root)?.has(rel.toLowerCase())) });
372
428
  const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'agy-insights-'));
373
429
  const r = {
@@ -392,6 +448,7 @@ async function analyze({ dir: dirs = DEFAULT_DIRS, workspaces = [], customKinds
392
448
  // When it happened: the transcript's step times, else the database's last write.
393
449
  const id = f.replace(/\.db$/, '');
394
450
  const log = realUsage(dir, id);
451
+ const origin = originOf(dir);
395
452
  const end = log?.end ?? fs.statSync(path.join(dir, f)).mtimeMs;
396
453
  const start = log?.start ?? end;
397
454
  // Real usage from the log when it has token counts; else from gen_metadata (below), else the estimate.
@@ -419,8 +476,16 @@ async function analyze({ dir: dirs = DEFAULT_DIRS, workspaces = [], customKinds
419
476
  convBytes += bytes;
420
477
  // The step payload also carries the tool result: its size approximates what entered the context.
421
478
  toolCalls(strs).forEach((c, i) => {
479
+ const fk = FILE_KEYS.find((k) => typeof c.args[k] === 'string');
480
+ const read = fk && /^(view_file|read_)/.test(c.name) ? norm(c.args[fk]) : null;
422
481
  // A call is counted once (first time its id shows up); every step that carries it adds its size.
423
- const tokens = i === 0 ? bytes / BYTES_PER_TOKEN : 0;
482
+ // An image read counts what the model gets from its pixels, not the bytes stored in the step.
483
+ let tokens = i === 0 ? bytes / BYTES_PER_TOKEN : 0;
484
+ if (tokens && read && IMAGE_RE.test(read)) {
485
+ const image = Math.min(tokens, imageTokens(read));
486
+ convBytes -= (tokens - image) * BYTES_PER_TOKEN;
487
+ tokens = image;
488
+ }
424
489
  const isNew = !seenCalls.has(c.id);
425
490
  if (isNew) seenCalls.set(c.id, 0);
426
491
  const calls = isNew ? 1 : 0;
@@ -429,10 +494,9 @@ async function analyze({ dir: dirs = DEFAULT_DIRS, workspaces = [], customKinds
429
494
  if (c.name === 'run_command' && typeof c.args.CommandLine === 'string') {
430
495
  commands.add(c.args.CommandLine.trim().split(/\s+/).slice(0, 2).join(' '), { calls, tokens });
431
496
  }
432
- const fk = FILE_KEYS.find((k) => typeof c.args[k] === 'string');
433
- if (!fk || !/^(view_file|read_)/.test(c.name)) return;
497
+ if (!read) return;
434
498
 
435
- const file = norm(c.args[fk]);
499
+ const file = read;
436
500
  const { StartLine: s, EndLine: e } = c.args;
437
501
  if (isNew) {
438
502
  const range = `${key(file)}|${s}-${e}`;
@@ -451,8 +515,9 @@ async function analyze({ dir: dirs = DEFAULT_DIRS, workspaces = [], customKinds
451
515
  const fields = { calls, tokens, rereads: isNew ? reread : 0, rereadTokens: reread * tokens };
452
516
  kinds.add(k, { ...fields, convs: once(seenKinds, k) }, { nonSource: NON_SOURCE.has(k) });
453
517
  projects.add(root || '', { ...fields, convs: once(seenProjects, root || '') }, { project });
454
- folders.add(key(folderAbs), { ...fields, convs: once(seenFolders, key(folderAbs)) }, { abs: folderAbs, rel: folderRel, root, project });
455
- files.add(key(file), { ...fields, convs: once(seenFiles, key(file)) }, { abs: file, rel, root, project, kind: k });
518
+ // `origin`: which Antigravity read it (CLI or editor), so exclusions point to the right settings.
519
+ folders.add(key(folderAbs), { ...fields, convs: once(seenFolders, key(folderAbs)) }, { abs: folderAbs, rel: folderRel, root, project, origin });
520
+ files.add(key(file), { ...fields, convs: once(seenFiles, key(file)) }, { abs: file, rel, root, project, kind: k, origin });
456
521
  });
457
522
  }
458
523
  const metaBlobs = db.all('select data from gen_metadata order by idx').map(([blob]) => blob);
@@ -498,7 +563,7 @@ async function analyze({ dir: dirs = DEFAULT_DIRS, workspaces = [], customKinds
498
563
  const main = [...counts].sort((a, b) => b[1] - a[1])[0];
499
564
  const model = main ? models.get(main[0]) : null;
500
565
  r.sessions.push({
501
- id, title: titles.get(id) || null, origin: originOf(dir), start, end, steps: steps.length, tokens: convBytes / BYTES_PER_TOKEN, usage,
566
+ id, title: titles.get(id) || null, origin, start, end, steps: steps.length, tokens: convBytes / BYTES_PER_TOKEN, usage,
502
567
  model: model ? { model: model.model, effort: model.effort } : null, models: counts.size,
503
568
  // For --debug-models: where each call's model came from, and for rows where no model was recognized,
504
569
  // the short strings that look like a model id (to spot a new format). Never prompts, paths or code.
@@ -538,4 +603,4 @@ async function analyze({ dir: dirs = DEFAULT_DIRS, workspaces = [], customKinds
538
603
  };
539
604
  }
540
605
 
541
- module.exports = { analyze, DEFAULT_DIR, DEFAULT_DIRS, kind, NON_SOURCE, realUsage, dayKey, modelOf, selectedModel, repoFiles };
606
+ module.exports = { analyze, DEFAULT_DIR, DEFAULT_DIRS, kind, isAgentFile, imageTokens, NON_SOURCE, realUsage, dayKey, modelOf, selectedModel, repoFiles };
package/lib/i18n.js CHANGED
@@ -43,14 +43,18 @@ Options:
43
43
 
44
44
  kinds: {
45
45
  agent: "Antigravity's own files (brain, skills)",
46
- build: 'build output (dist, bin, obj…)', logs: 'logs, maps, minified', generated: 'generated code / migrations',
46
+ build: 'build output (dist, bin, obj…)', logs: 'logs, maps, minified', generated: 'generated code / migrations', images: 'images',
47
47
  other: 'other', code: (ext) => `code ${ext}`,
48
48
  },
49
49
 
50
50
  fixes: {
51
- denyTitle: 'Deny reading it (Antigravity permission)',
51
+ denyTitle: 'Deny reading it in the CLI (Antigravity permission)',
52
52
  denyWhere: 'inside "permissions" → "deny"',
53
53
  denyNote: 'This is the only thing that actually blocks the read. It applies to all your projects, also blocks writing to that path, and agy must be restarted.',
54
+ denyEditorTitle: 'Deny reading it in the IDE and extensions (Antigravity permission)',
55
+ denyEditorPlace: 'Settings → Permission Grants',
56
+ denyEditorWhere: 'of the Antigravity IDE, app or editor extension, as a deny rule',
57
+ denyEditorNote: 'These have no settings file to edit: rules are added from their Settings. It blocks the read in all your projects (or only in one, as a project-level grant) and also blocks writing to that path.',
54
58
  ignoreTitle: 'Add to .antigravityignore',
55
59
  ignoreWhere: 'at the end of the file (create it if missing)',
56
60
  ignoreNote: 'Hides it from @ completion; it is not documented to stop the agent from reading it.',
@@ -122,9 +126,9 @@ Options:
122
126
  efforts: { minimal: 'Minimal', low: 'Low', medium: 'Medium', high: 'High', xhigh: 'Extra high', 'extra high': 'Extra high', max: 'Max' },
123
127
  sessionsTitle: 'Sessions (real usage)',
124
128
  sessionCols: ['Conversation', 'When', 'Origin', 'Model', 'Tokens', '%', 'With cache', 'Calls', 'Errors'],
125
- origin: { cli: 'CLI', editor: 'Editor / VS Code', other: 'Other folder' },
129
+ origin: { cli: 'CLI', editor: 'IDE and extensions', other: 'Other folder' },
126
130
  originsTitle: 'By origin',
127
- originsHint: 'Where each conversation was held. The editor and the VS Code extension keep the same history, so they are counted together.',
131
+ originsHint: 'Where each conversation was held. The Antigravity IDE, the Antigravity app and the editor extensions (such as the VS Code one) keep the same history, so they are counted together.',
128
132
  originCols: ['Origin', 'Conversations', 'Calls', 'Tokens', '%', 'Per call', 'With cache'],
129
133
  originsText: ' By origin:',
130
134
  noConversations: (scope, dir) => `No conversations${scope ? ` for <code>${scope}</code>` : ''} in <code>${dir}</code>.`,
@@ -224,14 +228,18 @@ Opciones:
224
228
 
225
229
  kinds: {
226
230
  agent: 'ficheros propios de Antigravity (brain, skills)',
227
- build: 'build output (dist, bin, obj…)', logs: 'logs, maps, minificados', generated: 'código generado / migraciones',
231
+ build: 'build output (dist, bin, obj…)', logs: 'logs, maps, minificados', generated: 'código generado / migraciones', images: 'imágenes',
228
232
  other: 'otros', code: (ext) => `código ${ext}`,
229
233
  },
230
234
 
231
235
  fixes: {
232
- denyTitle: 'Denegar la lectura (permiso de Antigravity)',
236
+ denyTitle: 'Denegar la lectura en el CLI (permiso de Antigravity)',
233
237
  denyWhere: 'dentro de "permissions" → "deny"',
234
238
  denyNote: 'Es lo único que impide la lectura. Afecta a todos tus proyectos, también impide escribir en esa ruta, y hay que reiniciar agy.',
239
+ denyEditorTitle: 'Denegar la lectura en el IDE y extensiones (permiso de Antigravity)',
240
+ denyEditorPlace: 'Settings → Permission Grants',
241
+ denyEditorWhere: 'del IDE, la app o la extensión de Antigravity, como regla de denegación',
242
+ denyEditorNote: 'No tienen un fichero de ajustes que editar: la regla se añade desde sus Settings. Impide la lectura en todos tus proyectos (o solo en uno, como permiso del proyecto) y también impide escribir en esa ruta.',
235
243
  ignoreTitle: 'Añadir a .antigravityignore',
236
244
  ignoreWhere: 'al final del fichero (créalo si no existe)',
237
245
  ignoreNote: 'Lo oculta del autocompletado con @; no está documentado que impida leerlo.',
@@ -303,9 +311,9 @@ Opciones:
303
311
  efforts: { minimal: 'Mínimo', low: 'Bajo', medium: 'Medio', high: 'Alto', xhigh: 'Muy alto', 'extra high': 'Muy alto', max: 'Máximo' },
304
312
  sessionsTitle: 'Sesiones (consumo real)',
305
313
  sessionCols: ['Conversación', 'Cuándo', 'Origen', 'Modelo', 'Tokens', '%', 'Con caché', 'Llamadas', 'Errores'],
306
- origin: { cli: 'CLI', editor: 'Editor / VS Code', other: 'Otra carpeta' },
314
+ origin: { cli: 'CLI', editor: 'IDE y extensiones', other: 'Otra carpeta' },
307
315
  originsTitle: 'Por origen',
308
- originsHint: 'Desde dónde se tuvo cada conversación. El editor y la extensión de VS Code guardan el mismo historial, así que se cuentan juntos.',
316
+ originsHint: 'Desde dónde se tuvo cada conversación. El IDE de Antigravity, la app de Antigravity y las extensiones de editor (como la de VS Code) guardan el mismo historial, así que se cuentan juntos.',
309
317
  originCols: ['Origen', 'Conversaciones', 'Llamadas', 'Tokens', '%', 'Por llamada', 'Con caché'],
310
318
  originsText: ' Por origen:',
311
319
  noConversations: (scope, dir) => `No hay conversaciones${scope ? ` de <code>${scope}</code>` : ''} en <code>${dir}</code>.`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agy-insights",
3
- "version": "0.9.0",
3
+ "version": "0.9.1",
4
4
  "description": "Measures Antigravity token usage (CLI, editor and VS Code extension) 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": [