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 +8 -5
- package/README.md +8 -5
- package/docs/how-it-works.md +2 -0
- package/lib/findings.js +4 -3
- package/lib/fixes.js +29 -8
- package/lib/history.js +78 -13
- package/lib/i18n.js +16 -8
- package/package.json +1 -1
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.
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
package/docs/how-it-works.md
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
433
|
-
if (!fk || !/^(view_file|read_)/.test(c.name)) return;
|
|
497
|
+
if (!read) return;
|
|
434
498
|
|
|
435
|
-
const file =
|
|
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
|
-
|
|
455
|
-
|
|
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
|
|
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: '
|
|
129
|
+
origin: { cli: 'CLI', editor: 'IDE and extensions', other: 'Other folder' },
|
|
126
130
|
originsTitle: 'By origin',
|
|
127
|
-
originsHint: 'Where each conversation was held. The
|
|
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: '
|
|
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
|
|
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.
|
|
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": [
|