agy-insights 0.0.0-stage → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +153 -2
- package/agy-insights.example.json +10 -0
- package/agy-insights.js +114 -0
- package/docs/como-funciona.md +88 -0
- package/lib/findings.js +147 -0
- package/lib/fixes.js +64 -0
- package/lib/history.js +300 -0
- package/lib/map.js +103 -0
- package/lib/report.js +183 -0
- package/package.json +44 -4
- package/vendor/sql.js/LICENSE +44 -0
- package/vendor/sql.js/VERSION +1 -0
- package/vendor/sql.js/sql-wasm.js +192 -0
- package/vendor/sql.js/sql-wasm.wasm +0 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Miguel González
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,154 @@
|
|
|
1
|
-
#
|
|
1
|
+
# agy-insights
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Te muestra qué contexto le estás mandando a **Antigravity** de forma repetida o innecesaria, a partir del historial que Antigravity CLI guarda en tu equipo. Tú decides qué hacer con ello.
|
|
4
|
+
|
|
5
|
+
- Se ejecuta solo en tu máquina y no envía nada a ningún sitio.
|
|
6
|
+
- No cambia nada: te dice qué pegar y dónde, y lo haces tú si quieres.
|
|
7
|
+
- Solo propone mecanismos oficiales de Antigravity.
|
|
8
|
+
- Funciona con cualquier editor, en Windows, macOS y Linux.
|
|
9
|
+
|
|
10
|
+
## Por qué
|
|
11
|
+
|
|
12
|
+
Cada fichero que lee el agente y cada salida de comando entra en el contexto y se reenvía en los turnos siguientes de la conversación. La cuota es una bolsa común, así que cada lectura innecesaria la gasta para todos. Esta herramienta te enseña dónde se va ese contexto: relecturas, carpetas de compilación, salidas de tests enormes y sesiones demasiado largas.
|
|
13
|
+
|
|
14
|
+
## Requisitos
|
|
15
|
+
|
|
16
|
+
- **Node.js 20 o superior** (`node --version`). No hace falta `npm install`.
|
|
17
|
+
- Haber usado **Antigravity CLI** (`agy`) en este equipo. El historial está en `~/.gemini/antigravity-cli/conversations` (en Windows, `%USERPROFILE%\.gemini\antigravity-cli\conversations`).
|
|
18
|
+
|
|
19
|
+
## Instalar
|
|
20
|
+
|
|
21
|
+
No tiene dependencias. Elige una forma:
|
|
22
|
+
|
|
23
|
+
**Con `npx`, sin instalar nada:**
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
npx agy-insights
|
|
27
|
+
npx agy-insights --text
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Para fijar una versión: `npx agy-insights@0.3.0`. En los ejemplos de abajo, cambia `node agy-insights.js` por `npx agy-insights`.
|
|
31
|
+
|
|
32
|
+
**Instalándolo globalmente:**
|
|
33
|
+
|
|
34
|
+
```sh
|
|
35
|
+
npm install -g agy-insights
|
|
36
|
+
agy-insights
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
**Clonando el repositorio:**
|
|
40
|
+
|
|
41
|
+
```sh
|
|
42
|
+
git clone https://github.com/migohe14/agy-insights.git
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Para actualizar: `git pull` dentro de la carpeta. Opcional, para tener el comando `agy-insights` en cualquier carpeta: `npm install -g .` dentro de ella.
|
|
46
|
+
|
|
47
|
+
## Usar
|
|
48
|
+
|
|
49
|
+
```sh
|
|
50
|
+
node agy-insights/agy-insights.js
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Analiza todo tu historial, guarda un informe HTML en la carpeta temporal y lo abre en el navegador.
|
|
54
|
+
|
|
55
|
+
| Para… | Ejecuta |
|
|
56
|
+
|---|---|
|
|
57
|
+
| Ver solo un proyecto | `node agy-insights.js --project C:\dev\mi-proyecto` |
|
|
58
|
+
| Ver el proyecto de la carpeta actual | `node <ruta>/agy-insights.js --project .` |
|
|
59
|
+
| Un resumen en la terminal | `node agy-insights.js --text` |
|
|
60
|
+
| El resumen con rutas y exclusiones | `node agy-insights.js --text --paths` |
|
|
61
|
+
| Un mapa del proyecto para pegar en `AGENTS.md` (carpetas, ficheros grandes, carpetas generadas; ~20 líneas aunque el repo sea grande) | `node agy-insights.js --map --project <carpeta>` |
|
|
62
|
+
| Guardar el informe en otro sitio | `node agy-insights.js --out informe.html --no-open` |
|
|
63
|
+
| Ver todas las opciones | `node agy-insights.js --help` |
|
|
64
|
+
|
|
65
|
+
El informe da dos cifras:
|
|
66
|
+
|
|
67
|
+
- **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.
|
|
68
|
+
- **«~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.
|
|
69
|
+
|
|
70
|
+
## Leer el informe
|
|
71
|
+
|
|
72
|
+
Arriba están los totales. 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`.
|
|
73
|
+
|
|
74
|
+
| Hallazgo | Qué significa | Qué hacer |
|
|
75
|
+
|---|---|---|
|
|
76
|
+
| El agente vuelve a leer lo que ya tenía | Lee otra vez un trozo de fichero que ya estaba en la conversación | Sesiones más cortas, una por tarea, o `/compact`. No excluyas el fichero: lo necesita. |
|
|
77
|
+
| Ficheros generados, compilados o de dependencias | Lecturas de `bin/`, `obj/`, `dist/`, `node_modules`, migraciones, lockfiles o logs | Si no hacían falta, exclúyelos (ver abajo) |
|
|
78
|
+
| Los mismos ficheros entran en muchas sesiones | Un fichero se lee en varias conversaciones distintas | Si es código central es normal; si es grande, dile al agente qué parte mirar |
|
|
79
|
+
| Comandos con salidas muy largas | La salida de tests o builds entra entera | Pide ejecutar solo el test o proyecto afectado |
|
|
80
|
+
| Sesiones muy largas | Todo lo acumulado se reenvía en cada turno | Una conversación nueva por tarea |
|
|
81
|
+
|
|
82
|
+
## Excluir algo
|
|
83
|
+
|
|
84
|
+
Despliega **Cómo excluirlo** en una propuesta. Verás el fichero exacto, dónde va el texto y un botón **Copiar**. Hay tres opciones, de más a menos fuerte:
|
|
85
|
+
|
|
86
|
+
### 1. Denegar la lectura (permiso de Antigravity)
|
|
87
|
+
|
|
88
|
+
Es lo único que impide de verdad que el agente lea una ruta. Usa los [permisos oficiales](https://antigravity.google/docs/permissions).
|
|
89
|
+
|
|
90
|
+
1. Abre `~/.gemini/antigravity-cli/settings.json` (en Windows, `%USERPROFILE%\.gemini\antigravity-cli\settings.json`).
|
|
91
|
+
2. Pega la regla dentro de `"permissions"` → `"deny"`. Si no existen, créalas; si existen, añade la regla sin borrar nada:
|
|
92
|
+
|
|
93
|
+
```json
|
|
94
|
+
{
|
|
95
|
+
"model": "Gemini 3.8 Flash (Medium)",
|
|
96
|
+
"permissions": {
|
|
97
|
+
"deny": [
|
|
98
|
+
"read_file(C:\\dev\\mi-proyecto\\src\\Infra\\obj)"
|
|
99
|
+
]
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
3. Guarda y reinicia `agy`.
|
|
105
|
+
|
|
106
|
+
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.
|
|
107
|
+
|
|
108
|
+
### 2. `.antigravityignore`
|
|
109
|
+
|
|
110
|
+
Es el fichero de exclusión de Antigravity, en la raíz del proyecto. Su efecto no está documentado: está confirmado que oculta los ficheros del autocompletado con `@`, pero no que impida al agente leerlos.
|
|
111
|
+
|
|
112
|
+
### 3. Regla en `AGENTS.md`
|
|
113
|
+
|
|
114
|
+
Una línea en el `AGENTS.md` del proyecto, pidiendo al modelo que no lea algo o que limite una salida. Es una indicación, no un bloqueo. Las reglas se envían en cada petición y también gastan tokens, así que mantenlas cortas. `AGENTS.md` suele estar en el repositorio del equipo: revisa el cambio antes de subirlo.
|
|
115
|
+
|
|
116
|
+
## Configurar
|
|
117
|
+
|
|
118
|
+
Copia `agy-insights.example.json` a `~/.agy-insights.json` (o pásalo con `--config`) y ajústalo. Es tu configuración personal:
|
|
119
|
+
|
|
120
|
+
| Clave | Para qué sirve | Por defecto |
|
|
121
|
+
|---|---|---|
|
|
122
|
+
| `conversationsDir` | Otra carpeta de historial | la de Antigravity CLI |
|
|
123
|
+
| `customKinds` | Tipos de fichero propios: `{ "regex": "etiqueta" }` | ninguno |
|
|
124
|
+
| `crossSessionMin` | A partir de cuántas conversaciones avisa de un fichero repetido | 3 |
|
|
125
|
+
| `heavyOutputTokens` | A partir de qué tamaño medio avisa de un comando | 2000 |
|
|
126
|
+
| `longSessionTokens` | A partir de qué tamaño avisa de una sesión larga | 100000 |
|
|
127
|
+
|
|
128
|
+
## Privacidad
|
|
129
|
+
|
|
130
|
+
- Todo se calcula en tu equipo; no se envía nada.
|
|
131
|
+
- El historial se lee desde una copia temporal y nunca se modifica.
|
|
132
|
+
- No escribe en `~/.gemini`, no instala hooks ni plugins y no toca tu `.gitignore` ni tus proyectos.
|
|
133
|
+
- Lo único que escribe es el informe HTML, en la carpeta temporal o donde digas con `--out`.
|
|
134
|
+
- El informe contiene rutas de tus proyectos. No lo compartas sin revisarlo.
|
|
135
|
+
- **No compartas los ficheros `.db` del historial**: contienen el código que leyó el agente, a menudo de cliente.
|
|
136
|
+
|
|
137
|
+
## Deshacer
|
|
138
|
+
|
|
139
|
+
- **Permiso**: borra la línea `read_file(…)` de `"deny"` en `settings.json` y reinicia `agy`.
|
|
140
|
+
- **.antigravityignore y AGENTS.md**: borra las líneas que añadiste.
|
|
141
|
+
|
|
142
|
+
## Problemas frecuentes
|
|
143
|
+
|
|
144
|
+
**«No hay conversaciones»**. Si usaste `--project`, puede que trabajaras con Antigravity desde otra carpeta: prueba sin `--project`. Si tampoco aparecen, comprueba que existe la carpeta del historial.
|
|
145
|
+
|
|
146
|
+
**«No se reconoce el formato del historial»**. Probablemente una versión nueva de Antigravity ha cambiado cómo guarda las conversaciones. Haz `git pull`; si sigue igual, abre una incidencia en el repo.
|
|
147
|
+
|
|
148
|
+
**Uso la app o el IDE de Antigravity, no el CLI**. Por ahora solo se lee el historial del CLI.
|
|
149
|
+
|
|
150
|
+
**El navegador no se abre**. La ruta del informe aparece en la terminal; ábrela a mano.
|
|
151
|
+
|
|
152
|
+
## Estado
|
|
153
|
+
|
|
154
|
+
Versión piloto. El formato del historial de Antigravity no está documentado, así que algunos datos pueden fallar con versiones nuevas. Si ves algo raro, abre una incidencia. Para contribuir, mira [docs/como-funciona.md](docs/como-funciona.md).
|
package/agy-insights.js
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// agy-insights: shows which context is sent to Antigravity repeatedly or needlessly, from the local
|
|
3
|
+
// Antigravity CLI history. Read-only: the only file it writes is the HTML report, in the temp folder
|
|
4
|
+
// (or wherever --out says).
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const os = require('os');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const { spawn } = require('child_process');
|
|
9
|
+
const { analyze, DEFAULT_DIR } = require('./lib/history');
|
|
10
|
+
const { findings, DEFAULTS } = require('./lib/findings');
|
|
11
|
+
const report = require('./lib/report');
|
|
12
|
+
const { buildMap } = require('./lib/map');
|
|
13
|
+
|
|
14
|
+
const HELP = `Uso: agy-insights [opciones] (o npx …, o node agy-insights.js)
|
|
15
|
+
|
|
16
|
+
Analiza tu historial de Antigravity CLI y abre un informe en el navegador.
|
|
17
|
+
|
|
18
|
+
Opciones:
|
|
19
|
+
--project <carpeta> Solo conversaciones que tocaron ese proyecto ("." = carpeta actual)
|
|
20
|
+
--text Resumen en la terminal en vez del informe HTML
|
|
21
|
+
--paths Con --text, muestra rutas y las exclusiones propuestas
|
|
22
|
+
--map Imprime un mapa del proyecto (--project, o la carpeta actual) para AGENTS.md
|
|
23
|
+
--out <fichero> Dónde guardar el informe HTML (por defecto, en la carpeta temporal)
|
|
24
|
+
--no-open No abrir el navegador
|
|
25
|
+
--dir <carpeta> Otra carpeta de historial (por defecto ${DEFAULT_DIR})
|
|
26
|
+
--config <fichero> Configuración (por defecto ~/.agy-insights.json si existe)
|
|
27
|
+
--help Esta ayuda
|
|
28
|
+
`;
|
|
29
|
+
|
|
30
|
+
function parseArgs(argv) {
|
|
31
|
+
const opts = { open: true };
|
|
32
|
+
for (let i = 0; i < argv.length; i++) {
|
|
33
|
+
const a = argv[i];
|
|
34
|
+
const value = () => {
|
|
35
|
+
if (i + 1 >= argv.length) fail(`Falta el valor de ${a}`);
|
|
36
|
+
return argv[++i];
|
|
37
|
+
};
|
|
38
|
+
if (a === '--help' || a === '-h') opts.help = true;
|
|
39
|
+
else if (a === '--project') opts.project = absolute(value());
|
|
40
|
+
else if (a === '--text') opts.text = true;
|
|
41
|
+
else if (a === '--paths') opts.paths = true;
|
|
42
|
+
else if (a === '--map') opts.map = true;
|
|
43
|
+
else if (a === '--out') opts.out = path.resolve(value());
|
|
44
|
+
else if (a === '--no-open') opts.open = false;
|
|
45
|
+
else if (a === '--dir') opts.dir = path.resolve(value());
|
|
46
|
+
else if (a === '--config') opts.config = path.resolve(value());
|
|
47
|
+
else fail(`Opción desconocida: ${a}. Usa --help.`);
|
|
48
|
+
}
|
|
49
|
+
return opts;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// History paths may come from Windows ("C:\..."): keep those as they are on any OS.
|
|
53
|
+
const absolute = (p) => (/^[A-Za-z]:[\\/]/.test(p) ? p : path.resolve(p));
|
|
54
|
+
|
|
55
|
+
function fail(msg) {
|
|
56
|
+
console.error(msg);
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function loadConfig(file) {
|
|
61
|
+
const target = file || path.join(os.homedir(), '.agy-insights.json');
|
|
62
|
+
if (!fs.existsSync(target)) {
|
|
63
|
+
if (file) fail(`No existe el fichero de configuración ${file}`);
|
|
64
|
+
return {};
|
|
65
|
+
}
|
|
66
|
+
try {
|
|
67
|
+
return JSON.parse(fs.readFileSync(target, 'utf8'));
|
|
68
|
+
} catch (e) {
|
|
69
|
+
return fail(`No se puede leer ${target}: ${e.message}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function openInBrowser(file) {
|
|
74
|
+
const [cmd, args] = process.platform === 'darwin' ? ['open', [file]]
|
|
75
|
+
: process.platform === 'win32' ? ['cmd', ['/c', 'start', '""', file]]
|
|
76
|
+
: ['xdg-open', [file]];
|
|
77
|
+
try {
|
|
78
|
+
spawn(cmd, args, { stdio: 'ignore', detached: true, windowsHide: true }).on('error', () => {}).unref();
|
|
79
|
+
} catch { /* the path is printed anyway */ }
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function main() {
|
|
83
|
+
const opts = parseArgs(process.argv.slice(2));
|
|
84
|
+
if (opts.help) return console.log(HELP);
|
|
85
|
+
const cfg = loadConfig(opts.config);
|
|
86
|
+
|
|
87
|
+
if (opts.map) opts.project = opts.project || process.cwd();
|
|
88
|
+
const r = await analyze({
|
|
89
|
+
dir: opts.dir || cfg.conversationsDir || DEFAULT_DIR,
|
|
90
|
+
workspaces: opts.project ? [opts.project] : [],
|
|
91
|
+
customKinds: cfg.customKinds || {},
|
|
92
|
+
});
|
|
93
|
+
if (opts.map) {
|
|
94
|
+
const m = buildMap(opts.project, { ...cfg.map, history: r.files });
|
|
95
|
+
console.log(m.block);
|
|
96
|
+
return console.error(`\n~${m.tokens} tokens en cada petición. Pégalo en ${path.join(opts.project, 'AGENTS.md')} si te convence; agy-insights no lo escribe.`);
|
|
97
|
+
}
|
|
98
|
+
const found = findings(r, {
|
|
99
|
+
crossSessionMin: cfg.crossSessionMin ?? DEFAULTS.crossSessionMin,
|
|
100
|
+
heavyOutputTokens: cfg.heavyOutputTokens ?? DEFAULTS.heavyOutputTokens,
|
|
101
|
+
longSessionTokens: cfg.longSessionTokens ?? DEFAULTS.longSessionTokens,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
if (opts.text) return process.stdout.write(report.text(r, found, { scope: opts.project, paths: opts.paths }));
|
|
105
|
+
|
|
106
|
+
const stamp = new Date().toISOString().slice(0, 16).replace(/[-:T]/g, '');
|
|
107
|
+
const out = opts.out || path.join(os.tmpdir(), 'agy-insights', `informe-${stamp}.html`);
|
|
108
|
+
fs.mkdirSync(path.dirname(out), { recursive: true });
|
|
109
|
+
fs.writeFileSync(out, report.html(r, found, { scope: opts.project }));
|
|
110
|
+
console.log(`${r.conversations} conversaciones analizadas, ${found.length} hallazgos.\nInforme: ${out}`);
|
|
111
|
+
if (opts.open) openInBrowser(out);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
main().catch((e) => fail(`Error: ${e.message}`));
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# Cómo funciona
|
|
2
|
+
|
|
3
|
+
Documento para quien mantenga o amplíe agy-insights.
|
|
4
|
+
|
|
5
|
+
## Principios
|
|
6
|
+
|
|
7
|
+
Cualquier cambio tiene que respetarlos:
|
|
8
|
+
|
|
9
|
+
1. **Solo lectura.** No se escribe en `~/.gemini` ni en los proyectos del dev. El historial se copia a una carpeta temporal antes de abrirlo, porque `agy` mantiene esas bases de datos abiertas. Lo único que se escribe es el informe.
|
|
10
|
+
2. **El dev decide.** La herramienta propone texto para pegar; nunca lo aplica.
|
|
11
|
+
3. **Solo mecanismos oficiales de Antigravity** para excluir: permisos (`read_file` en `permissions.deny`), `.antigravityignore` y reglas en `AGENTS.md`. Nada de hooks, plugins, `.gitignore` ni `.ignore`.
|
|
12
|
+
4. **Fallar de forma visible.** Si el formato del historial cambia, el informe debe avisar en vez de mostrar datos falsos.
|
|
13
|
+
5. **Sin dependencias que instalar.** Solo Node 20 o superior; SQLite va en `vendor/`.
|
|
14
|
+
|
|
15
|
+
## Estructura
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
agy-insights.js comando: opciones, configuración, escribe el informe y abre el navegador
|
|
19
|
+
lib/history.js lee el historial y agrega lecturas, comandos, proyectos y sesiones
|
|
20
|
+
lib/findings.js convierte el análisis en hallazgos (los 5 detectores)
|
|
21
|
+
lib/fixes.js construye el texto de cada exclusión; no escribe nada
|
|
22
|
+
lib/report.js informe HTML (estático, solo botones de copiar) y resumen de texto
|
|
23
|
+
vendor/sql.js/ SQLite en WebAssembly (versión en VERSION, licencia MIT en LICENSE)
|
|
24
|
+
test/fake-history.js genera historiales falsos con el mismo formato que los reales
|
|
25
|
+
test/*.test.js tests (node --test)
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## El historial de Antigravity CLI
|
|
29
|
+
|
|
30
|
+
No está documentado; esto es lo que se ha observado en `agy` 1.4.x.
|
|
31
|
+
|
|
32
|
+
- Una base de datos SQLite por conversación: `~/.gemini/antigravity-cli/conversations/<uuid>.db`, en modo WAL.
|
|
33
|
+
- Tabla `steps`: una fila por paso. `step_payload` es un mensaje protobuf. Las llamadas a herramientas guardan el nombre (`view_file`, `run_command`, `grep_search`…) seguido de sus argumentos en JSON, por ejemplo `{"AbsolutePath":"C:\\dev\\...\\X.cs","StartLine":301,"EndLine":380}`, precedido por un id `call_NNN`.
|
|
34
|
+
- Cada llamada ocupa dos pasos con el mismo id: el de la respuesta del modelo que la pide (`step_type` 15) y el del resultado (`step_type` 132), que repite nombre y argumentos y añade el contenido del fichero o la salida del comando. `lib/history.js` cuenta la llamada una vez por id y suma el tamaño de ambos pasos, que aproxima lo que entró en el contexto.
|
|
35
|
+
- Tabla `gen_metadata`: configuración de cada llamada al modelo, incluido el nombre del modelo.
|
|
36
|
+
- Junto al historial, `../brain/<id>/.system_generated/logs/transcript.jsonl`: una línea JSON por paso. Las de tipo `PLANNER_RESPONSE` son llamadas al modelo y traen `input_tokens`, `cache_read_tokens` y `output_tokens`; los pasos fallidos tienen `status: "ERROR"`. Los subagentes tienen su propia conversación y su propio transcript. `realUsage()` en `lib/history.js` suma esas cifras (solo lee el fichero). El total que imprime `agy -p --output-format json` no incluye los subagentes; este sí.
|
|
37
|
+
|
|
38
|
+
`lib/history.js` no depende de un esquema protobuf. Recorre el mensaje, recoge todas las cadenas, y considera llamada a herramienta cualquier JSON precedido (a menos de 3 cadenas) por un nombre en `snake_case`. Así aguanta cambios menores de formato. Si hay más de 50 pasos y ninguna llamada reconocida, marca `formatUnrecognized` y el informe lo avisa.
|
|
39
|
+
|
|
40
|
+
### SQLite
|
|
41
|
+
|
|
42
|
+
- Con Node 22.13 o superior se usa `node:sqlite`, que lee también el fichero `-wal`.
|
|
43
|
+
- Con versiones anteriores se usa `sql.js` desde `vendor/`. No lee el `-wal`, así que pueden faltar los últimos pasos de una conversación abierta.
|
|
44
|
+
|
|
45
|
+
Para actualizar `sql.js`: copia `dist/sql-wasm.js`, `dist/sql-wasm.wasm` y `LICENSE` de la nueva versión del paquete npm y actualiza `VERSION`.
|
|
46
|
+
|
|
47
|
+
### Proyectos
|
|
48
|
+
|
|
49
|
+
El proyecto de cada fichero se detecta subiendo por su ruta hasta encontrar una carpeta con `.git`, si la ruta existe en la máquina. Con `--project` se usa esa carpeta directamente. Las rutas de Windows (`C:\...`) se normalizan a `/` internamente y se devuelven con `\` en las exclusiones, porque `agy` compara las rutas de los permisos literalmente.
|
|
50
|
+
|
|
51
|
+
## Añadir un tipo de fichero
|
|
52
|
+
|
|
53
|
+
En `lib/history.js`, función `kind()`. Si es un tipo que casi nunca hace falta leer, añádelo también a `NON_SOURCE`, y sus patrones de `.antigravityignore` a `KIND_PATTERNS` en `lib/findings.js`. Cada dev puede añadir tipos propios sin tocar el código con `customKinds` en su configuración.
|
|
54
|
+
|
|
55
|
+
## Añadir un detector
|
|
56
|
+
|
|
57
|
+
En `lib/findings.js`. Un hallazgo es `{ id, level: 'info' | 'warn', title, why, suggestion, tokens, items }`, y cada `item` es `{ label, tokens, detail, fixes }`. Las `fixes` se crean con `lib/fixes.js`. No propongas excluir código fuente: el agente lo necesita.
|
|
58
|
+
|
|
59
|
+
Añade un caso en `test/fake-history.js` y un test.
|
|
60
|
+
|
|
61
|
+
## Tests
|
|
62
|
+
|
|
63
|
+
```sh
|
|
64
|
+
npm test
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Prueban el análisis con un historial falso, los cinco detectores, el formato de las reglas, que nunca se propone excluir código fuente, que el historial no cambia, la detección de proyectos por `.git`, el aviso de formato desconocido, el escapado del HTML y el comando completo. Se ejecutan en CI con Node 20 (sql.js) y Node 22 (node:sqlite).
|
|
68
|
+
|
|
69
|
+
Para generar un historial de ejemplo y probar el informe a mano:
|
|
70
|
+
|
|
71
|
+
```sh
|
|
72
|
+
node test/fake-history.js test/tmp/history
|
|
73
|
+
node agy-insights.js --dir test/tmp/history
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Paquete npm
|
|
77
|
+
|
|
78
|
+
`package.json` define el comando (`bin`) y qué ficheros entran en el paquete (`files`: el comando, `lib/`, `vendor/`, `docs/` y el ejemplo de configuración; los tests no). Se publica en el registro público de npm como `agy-insights`. El paquete solo contiene la herramienta; los historiales que analiza se quedan en la máquina de cada usuario.
|
|
79
|
+
|
|
80
|
+
- **Probar el paquete en local**: `npm pack` y después `npx --yes --package=./agy-insights-<versión>.tgz agy-insights --help`.
|
|
81
|
+
- **Desde el repositorio**, sin publicar: `npx --yes github:migohe14/agy-insights`. Al no tener dependencias ni paso de build, npm lo instala directamente desde git.
|
|
82
|
+
- **Publicar una versión**: `npm version <patch|minor|major>` (actualiza `package.json` y crea la etiqueta `v…`) y `git push --follow-tags`. El workflow `.github/workflows/publish.yml` pasa los tests y ejecuta `npm publish --provenance`. Necesita el secreto `NPM_TOKEN` en el repositorio de GitHub (un token de npm de tipo *Automation* o *Granular* con permiso de publicación).
|
|
83
|
+
|
|
84
|
+
## Pendiente
|
|
85
|
+
|
|
86
|
+
- Confirmar con `agy` que una regla `deny` de `read_file` impide la lectura.
|
|
87
|
+
- Probar con historiales reales de varios devs (Windows y macOS).
|
|
88
|
+
- Leer también el historial de la app y del IDE de Antigravity, si guardan el mismo formato.
|
package/lib/findings.js
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
// Turns an analysis report into findings: what context is being sent repeatedly or needlessly, why it
|
|
2
|
+
// matters, and which official Antigravity mechanism could address it. Findings only describe and propose
|
|
3
|
+
// (see fixes.js); applying anything is always the developer's decision.
|
|
4
|
+
const fixes = require('./fixes');
|
|
5
|
+
|
|
6
|
+
const DEFAULTS = { crossSessionMin: 3, heavyOutputTokens: 2000, longSessionTokens: 100000 };
|
|
7
|
+
|
|
8
|
+
// Suggested .antigravityignore patterns for the built-in non-source types.
|
|
9
|
+
const KIND_PATTERNS = {
|
|
10
|
+
'node_modules': ['node_modules/'],
|
|
11
|
+
'build output (dist, bin, obj…)': ['bin/', 'obj/', 'dist/', 'build/', 'out/', 'target/', 'coverage/'],
|
|
12
|
+
'lockfiles': ['*.lock', 'package-lock.json', 'pnpm-lock.yaml', 'packages.lock.json'],
|
|
13
|
+
'logs, maps, minificados': ['*.log', '*.map', '*.min.js', '*.min.css'],
|
|
14
|
+
'código generado / migraciones': ['*.designer.cs', '*.g.cs', 'Migrations/'],
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const sum = (xs, f) => xs.reduce((s, x) => s + f(x), 0);
|
|
18
|
+
const plural = (n, one, many) => `${n} ${n === 1 ? one : many}`;
|
|
19
|
+
const where = (x) => (x.project ? `${x.project} › ${x.rel}` : x.abs);
|
|
20
|
+
|
|
21
|
+
function findings(r, opts = {}) {
|
|
22
|
+
const t = { ...DEFAULTS, ...opts };
|
|
23
|
+
const nonSourceKinds = new Set(r.kinds.filter((k) => k.nonSource).map((k) => k.key));
|
|
24
|
+
const out = [];
|
|
25
|
+
|
|
26
|
+
if (r.formatUnrecognized) {
|
|
27
|
+
out.push({
|
|
28
|
+
id: 'format', level: 'warn', title: 'No se reconoce el formato del historial',
|
|
29
|
+
why: `Hay ${r.steps} pasos pero ninguna llamada a herramienta reconocible. Probablemente agy ha cambiado cómo guarda las conversaciones.`,
|
|
30
|
+
suggestion: 'Los resultados de abajo no son fiables. Actualiza agy-insights (git pull) o abre una incidencia en el repo.',
|
|
31
|
+
items: [],
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// 1. Same chunk read again within a session: the content is paid twice. The file itself is usually needed.
|
|
36
|
+
const reread = r.files.filter((f) => f.rereads > 0).sort((a, b) => b.rereadTokens - a.rereadTokens);
|
|
37
|
+
if (reread.length) {
|
|
38
|
+
out.push({
|
|
39
|
+
id: 'rereads', level: 'info', title: 'El agente vuelve a leer lo que ya tenía',
|
|
40
|
+
tokens: r.rereadTokens,
|
|
41
|
+
why: `${plural(r.rereads, 'lectura repite', 'lecturas repiten')} un trozo que ya se había leído en la misma sesión. Ese contenido entra dos veces en el contexto.`,
|
|
42
|
+
suggestion: 'Suele pasar en sesiones largas o después de editar. Sesiones más cortas por tarea, o /compact, lo reducen. No conviene excluir estos ficheros: el agente los necesita.',
|
|
43
|
+
items: reread.slice(0, 10).map((f) => ({ label: where(f), tokens: f.rereadTokens, detail: plural(f.rereads, 'relectura', 'relecturas'), fixes: [] })),
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// 2. Generated, build output, vendor, lockfiles and logs: rarely needed, and candidates for exclusion.
|
|
48
|
+
const nonSource = r.files.filter((f) => nonSourceKinds.has(f.kind));
|
|
49
|
+
if (nonSource.length) {
|
|
50
|
+
const byFolder = new Map();
|
|
51
|
+
for (const f of nonSource) {
|
|
52
|
+
const abs = f.abs.slice(0, f.abs.lastIndexOf('/'));
|
|
53
|
+
const e = byFolder.get(abs) || { abs, rel: f.rel.slice(0, f.rel.lastIndexOf('/')), root: f.root, project: f.project, kind: f.kind, tokens: 0, calls: 0 };
|
|
54
|
+
e.tokens += f.tokens;
|
|
55
|
+
e.calls += f.calls;
|
|
56
|
+
byFolder.set(abs, e);
|
|
57
|
+
}
|
|
58
|
+
const items = [...byFolder.values()].sort((a, b) => b.tokens - a.tokens).slice(0, 10).map((e) => ({
|
|
59
|
+
label: `${where(e)}/`, tokens: e.tokens, detail: `${e.kind} · ${plural(e.calls, 'lectura', 'lecturas')}`,
|
|
60
|
+
fixes: [
|
|
61
|
+
fixes.deny(e.abs),
|
|
62
|
+
e.root && fixes.ignore(e.root, [`${e.rel}/`]),
|
|
63
|
+
e.root && fixes.rule(e.root, `No leas \`${e.rel}/\`: es ${e.kind}; trabaja con el código fuente.`),
|
|
64
|
+
].filter(Boolean),
|
|
65
|
+
}));
|
|
66
|
+
// One pattern-based item per project and type, for folders that have not been read yet.
|
|
67
|
+
const perProject = new Map();
|
|
68
|
+
for (const f of nonSource) {
|
|
69
|
+
if (!f.root || !KIND_PATTERNS[f.kind]) continue;
|
|
70
|
+
const k = `${f.root}|${f.kind}`;
|
|
71
|
+
const e = perProject.get(k) || { root: f.root, project: f.project, kind: f.kind, tokens: 0 };
|
|
72
|
+
e.tokens += f.tokens;
|
|
73
|
+
perProject.set(k, e);
|
|
74
|
+
}
|
|
75
|
+
for (const e of perProject.values()) {
|
|
76
|
+
items.push({
|
|
77
|
+
label: `${e.project} › todo lo de tipo «${e.kind}»`, tokens: e.tokens, detail: 'patrón para cualquier carpeta del proyecto',
|
|
78
|
+
fixes: [
|
|
79
|
+
fixes.ignore(e.root, KIND_PATTERNS[e.kind]),
|
|
80
|
+
fixes.rule(e.root, `No leas ${e.kind} (${KIND_PATTERNS[e.kind].join(', ')}) salvo que la tarea lo pida.`),
|
|
81
|
+
],
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
out.push({
|
|
85
|
+
id: 'non-source', level: 'warn', title: 'Se leen ficheros generados, compilados o de dependencias',
|
|
86
|
+
tokens: sum(nonSource, (f) => f.tokens),
|
|
87
|
+
why: 'Salidas de build, código generado, dependencias, lockfiles y logs casi nunca hacen falta para entender o cambiar el código, y suelen ser grandes.',
|
|
88
|
+
suggestion: 'Revisa si alguna de estas lecturas era necesaria. Si no, puedes impedirlas con un permiso de Antigravity.',
|
|
89
|
+
items,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// 3. Same file entering many different sessions: repeated context across conversations.
|
|
94
|
+
const cross = r.files.filter((f) => f.convs >= t.crossSessionMin).sort((a, b) => b.tokens - a.tokens);
|
|
95
|
+
if (cross.length) {
|
|
96
|
+
out.push({
|
|
97
|
+
id: 'cross-session', level: 'info', title: 'Los mismos ficheros entran en muchas sesiones',
|
|
98
|
+
tokens: sum(cross, (f) => f.tokens),
|
|
99
|
+
why: `${plural(cross.length, 'fichero se ha leído', 'ficheros se han leído')} en ${t.crossSessionMin} o más conversaciones distintas.`,
|
|
100
|
+
suggestion: 'Si son el núcleo del proyecto es normal. Si son grandes y el agente solo necesita una parte, decirle qué trozo mirar ahorra lecturas.',
|
|
101
|
+
// Exclusion is only offered for non-source files: denying core source code would break the agent's work.
|
|
102
|
+
items: cross.slice(0, 10).map((f) => ({
|
|
103
|
+
label: where(f), tokens: f.tokens, detail: `${plural(f.convs, 'sesión', 'sesiones')} · ${plural(f.calls, 'lectura', 'lecturas')}`,
|
|
104
|
+
fixes: nonSourceKinds.has(f.kind) ? [fixes.deny(f.abs), f.root && fixes.ignore(f.root, [f.rel])].filter(Boolean) : [],
|
|
105
|
+
})),
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// 4. Commands whose output is large every time (test suites, builds).
|
|
110
|
+
const heavy = r.commands.filter((c) => c.tokens / c.calls >= t.heavyOutputTokens).sort((a, b) => b.tokens - a.tokens);
|
|
111
|
+
if (heavy.length) {
|
|
112
|
+
out.push({
|
|
113
|
+
id: 'heavy-commands', level: 'warn', title: 'Comandos con salidas muy largas',
|
|
114
|
+
tokens: sum(heavy, (c) => c.tokens),
|
|
115
|
+
why: 'La salida completa de cada comando entra en el contexto y se reenvía en los turnos siguientes.',
|
|
116
|
+
suggestion: 'Pide ejecutar solo lo necesario (un test concreto, un proyecto concreto) o filtrar la salida. Si pasa siempre, una regla en el AGENTS.md del proyecto lo recuerda en cada sesión.',
|
|
117
|
+
items: heavy.slice(0, 8).map((c) => ({
|
|
118
|
+
label: c.key, tokens: c.tokens, detail: `${plural(c.calls, 'vez', 'veces')} · ~${Math.round(c.tokens / c.calls).toLocaleString('es-ES')} tokens cada una`,
|
|
119
|
+
example: `- Al ejecutar \`${c.key}\`, limita la salida: solo el test o proyecto afectado, o filtra las líneas relevantes.`,
|
|
120
|
+
fixes: [],
|
|
121
|
+
})),
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// 5. Very long sessions: everything accumulated is resent on every turn.
|
|
126
|
+
// Real usage when agy's transcript has it (it includes everything resent on each call), else the estimate.
|
|
127
|
+
const size = (s) => (s.usage ? s.usage.input + s.usage.output : s.tokens);
|
|
128
|
+
const long = r.sessions.filter((s) => size(s) >= t.longSessionTokens);
|
|
129
|
+
if (long.length) {
|
|
130
|
+
out.push({
|
|
131
|
+
id: 'long-sessions', level: 'info', title: 'Sesiones muy largas',
|
|
132
|
+
tokens: sum(long, (s) => s.tokens),
|
|
133
|
+
why: `${plural(long.length, 'conversación supera', 'conversaciones superan')} ${t.longSessionTokens.toLocaleString('es-ES')} tokens. En una sesión larga, todo lo acumulado se vuelve a enviar en cada turno.`,
|
|
134
|
+
suggestion: 'Una conversación nueva por tarea, o /compact cuando cambias de tema.',
|
|
135
|
+
items: long.slice(0, 5).map((s) => ({
|
|
136
|
+
label: `conversación ${s.id.slice(0, 8)}`, tokens: s.tokens, fixes: [],
|
|
137
|
+
detail: plural(s.steps, 'paso', 'pasos') + (s.usage
|
|
138
|
+
? ` · ${s.usage.calls} llamadas al modelo, ${(s.usage.input + s.usage.output).toLocaleString('es-ES')} tokens reales (${(s.usage.input + s.usage.cache + s.usage.output).toLocaleString('es-ES')} con caché)`
|
|
139
|
+
: ''),
|
|
140
|
+
})),
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return out;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
module.exports = { findings, DEFAULTS };
|
package/lib/fixes.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// Ready-to-paste exclusions using only official Antigravity mechanisms. This module never writes
|
|
2
|
+
// anything: it builds the exact text and says which file it goes in. Applying it is the developer's call.
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
7
|
+
const AGY_SETTINGS = path.join(os.homedir(), '.gemini', 'antigravity-cli', 'settings.json');
|
|
8
|
+
|
|
9
|
+
// Paths as the developer's OS writes them (agy matches permission paths literally).
|
|
10
|
+
const native = (p) => (/^[A-Za-z]:\//.test(p) ? p.replace(/\//g, '\\') : p);
|
|
11
|
+
|
|
12
|
+
let denied = null;
|
|
13
|
+
function deniedRules() {
|
|
14
|
+
if (!denied) {
|
|
15
|
+
try { denied = JSON.parse(fs.readFileSync(AGY_SETTINGS, 'utf8')).permissions?.deny || []; } catch { denied = []; }
|
|
16
|
+
}
|
|
17
|
+
return denied;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Permission rule (https://antigravity.google/docs/permissions): deny > ask > allow, a denied read_file
|
|
21
|
+
// path covers everything below it, and denying a read also denies writes there.
|
|
22
|
+
function deny(absPath) {
|
|
23
|
+
const rule = `read_file(${native(absPath)})`;
|
|
24
|
+
return {
|
|
25
|
+
how: 'deny', title: 'Denegar la lectura (permiso de Antigravity)',
|
|
26
|
+
file: AGY_SETTINGS, where: 'dentro de "permissions" → "deny"',
|
|
27
|
+
text: JSON.stringify(rule),
|
|
28
|
+
note: 'Es lo único que impide la lectura. Afecta a todos tus proyectos, también impide escribir en esa ruta, y hay que reiniciar agy.',
|
|
29
|
+
applied: deniedRules().includes(rule),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// .antigravityignore: Antigravity's own exclusion file. Its scope is undocumented; it is confirmed to hide
|
|
34
|
+
// files from @ completion, not to stop the agent from reading them.
|
|
35
|
+
function ignore(root, patterns) {
|
|
36
|
+
const file = path.join(native(root), '.antigravityignore');
|
|
37
|
+
let current = [];
|
|
38
|
+
try { current = fs.readFileSync(file, 'utf8').split(/\r?\n/).map((l) => l.trim()); } catch { /* not there yet */ }
|
|
39
|
+
const missing = patterns.filter((p) => !current.includes(p));
|
|
40
|
+
return {
|
|
41
|
+
how: 'ignore', title: 'Añadir a .antigravityignore',
|
|
42
|
+
file, where: 'al final del fichero (créalo si no existe)',
|
|
43
|
+
text: missing.join('\n'),
|
|
44
|
+
note: 'Lo oculta del autocompletado con @; no está documentado que impida leerlo.',
|
|
45
|
+
applied: missing.length === 0,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// AGENTS.md rule: a request to the model, sent with every request (it costs tokens too).
|
|
50
|
+
function rule(root, text) {
|
|
51
|
+
const file = path.join(native(root), 'AGENTS.md');
|
|
52
|
+
const line = `- ${text}`;
|
|
53
|
+
let present = false;
|
|
54
|
+
try { present = fs.readFileSync(file, 'utf8').split(/\r?\n/).some((l) => l.trim() === line); } catch { /* none */ }
|
|
55
|
+
return {
|
|
56
|
+
how: 'rule', title: 'Añadir una regla a AGENTS.md',
|
|
57
|
+
file, where: 'al final del fichero',
|
|
58
|
+
text: line,
|
|
59
|
+
note: 'Es una indicación al modelo, no un bloqueo. Se envía en cada petición: mantenla corta. AGENTS.md suele estar en el repo del equipo.',
|
|
60
|
+
applied: present,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
module.exports = { AGY_SETTINGS, native, deny, ignore, rule };
|