agy-insights 0.3.0 → 0.4.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/README.es.md +169 -0
- package/README.md +91 -76
- package/agy-insights.example.json +2 -2
- package/agy-insights.js +16 -25
- package/docs/how-it-works.md +98 -0
- package/lib/findings.js +45 -61
- package/lib/fixes.js +10 -9
- package/lib/history.js +6 -6
- package/lib/i18n.js +293 -0
- package/lib/map.js +8 -7
- package/lib/report.js +47 -45
- package/package.json +8 -4
- package/docs/como-funciona.md +0 -88
package/lib/i18n.js
ADDED
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
// User-facing text in English and Spanish. The language comes from --lang, the "lang" config key, or the
|
|
2
|
+
// system locale (Spanish if it starts with "es", else English). Modules read strings through `t`, which
|
|
3
|
+
// always points at the current language, so setLang() must run before any report is built.
|
|
4
|
+
|
|
5
|
+
const plural = (n, one, many) => `${n} ${n === 1 ? one : many}`;
|
|
6
|
+
|
|
7
|
+
const en = {
|
|
8
|
+
locale: 'en-US',
|
|
9
|
+
help: (dir) => `Usage: agy-insights [options] (or npx agy-insights, or node agy-insights.js)
|
|
10
|
+
|
|
11
|
+
Analyzes your Antigravity CLI history and opens a report in the browser.
|
|
12
|
+
|
|
13
|
+
Options:
|
|
14
|
+
--project <folder> Only conversations that touched that project ("." = current folder)
|
|
15
|
+
--text Summary in the terminal instead of the HTML report
|
|
16
|
+
--paths With --text, show paths and the proposed exclusions
|
|
17
|
+
--map Print a project map (--project, or the current folder) for AGENTS.md
|
|
18
|
+
--out <file> Where to save the HTML report (default: the temp folder)
|
|
19
|
+
--no-open Don't open the browser
|
|
20
|
+
--lang <en|es> Language of the output (default: the system's)
|
|
21
|
+
--dir <folder> Another history folder (default ${dir})
|
|
22
|
+
--config <file> Configuration (default ~/.agy-insights.json if it exists)
|
|
23
|
+
--help This help
|
|
24
|
+
`,
|
|
25
|
+
missingValue: (opt) => `Missing value for ${opt}`,
|
|
26
|
+
unknownOption: (opt) => `Unknown option: ${opt}. See --help.`,
|
|
27
|
+
badLang: (v) => `Unsupported language: ${v}. Use en or es.`,
|
|
28
|
+
configNotFound: (f) => `Configuration file not found: ${f}`,
|
|
29
|
+
cannotRead: (f, msg) => `Cannot read ${f}: ${msg}`,
|
|
30
|
+
mapDone: (tokens, file) => `\n~${tokens} tokens on every request. Paste it into ${file} if you like it; agy-insights does not write it.`,
|
|
31
|
+
reportName: 'report',
|
|
32
|
+
done: (convs, found, out) => `${convs} conversations analyzed, ${found} findings.\nReport: ${out}`,
|
|
33
|
+
|
|
34
|
+
kinds: {
|
|
35
|
+
build: 'build output (dist, bin, obj…)', logs: 'logs, maps, minified', generated: 'generated code / migrations',
|
|
36
|
+
other: 'other', code: (ext) => `code ${ext}`,
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
fixes: {
|
|
40
|
+
denyTitle: 'Deny reading it (Antigravity permission)',
|
|
41
|
+
denyWhere: 'inside "permissions" → "deny"',
|
|
42
|
+
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.',
|
|
43
|
+
ignoreTitle: 'Add to .antigravityignore',
|
|
44
|
+
ignoreWhere: 'at the end of the file (create it if missing)',
|
|
45
|
+
ignoreNote: 'Hides it from @ completion; it is not documented to stop the agent from reading it.',
|
|
46
|
+
ruleTitle: 'Add a rule to AGENTS.md',
|
|
47
|
+
ruleWhere: 'at the end of the file',
|
|
48
|
+
ruleNote: 'It is an instruction to the model, not a block. It is sent with every request: keep it short. AGENTS.md is usually in the team repo.',
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
findings: {
|
|
52
|
+
formatTitle: 'The history format is not recognized',
|
|
53
|
+
formatWhy: (steps) => `There are ${steps} steps but no recognizable tool call. agy has probably changed how it stores conversations.`,
|
|
54
|
+
formatSuggestion: 'The results below are not reliable. Update agy-insights (npx agy-insights@latest) or open an issue on GitHub.',
|
|
55
|
+
rereadsTitle: 'The agent re-reads what it already had',
|
|
56
|
+
rereadsWhy: (n) => `${plural(n, 'read repeats', 'reads repeat')} a chunk already read in the same session. That content enters the context twice.`,
|
|
57
|
+
rereadsSuggestion: 'It usually happens in long sessions or after editing. Shorter sessions, one per task, or /compact reduce it. Do not exclude these files: the agent needs them.',
|
|
58
|
+
rereads: (n) => plural(n, 're-read', 're-reads'),
|
|
59
|
+
reads: (n) => plural(n, 'read', 'reads'),
|
|
60
|
+
ruleFolder: (rel, kind) => `Don't read \`${rel}/\`: it is ${kind}; work with the source code.`,
|
|
61
|
+
kindPattern: (project, kind) => `${project} › everything of type "${kind}"`,
|
|
62
|
+
kindPatternDetail: 'pattern for any folder in the project',
|
|
63
|
+
ruleKind: (kind, patterns) => `Don't read ${kind} (${patterns}) unless the task requires it.`,
|
|
64
|
+
nonSourceTitle: 'Generated, build or dependency files are being read',
|
|
65
|
+
nonSourceWhy: 'Build output, generated code, dependencies, lockfiles and logs are rarely needed to understand or change the code, and they tend to be large.',
|
|
66
|
+
nonSourceSuggestion: 'Check whether any of these reads was necessary. If not, you can block them with an Antigravity permission.',
|
|
67
|
+
crossTitle: 'The same files enter many sessions',
|
|
68
|
+
crossWhy: (n, min) => `${plural(n, 'file was', 'files were')} read in ${min} or more different conversations.`,
|
|
69
|
+
crossSuggestion: 'If they are the core of the project, that is normal. If they are large and the agent only needs part of them, telling it which part to look at saves reads.',
|
|
70
|
+
sessions: (n) => plural(n, 'session', 'sessions'),
|
|
71
|
+
heavyTitle: 'Commands with very long output',
|
|
72
|
+
heavyWhy: 'The full output of each command enters the context and is resent on the following turns.',
|
|
73
|
+
heavySuggestion: "Ask it to run only what is needed (one test, one project) or to filter the output. If it always happens, a rule in the project's AGENTS.md reminds it every session.",
|
|
74
|
+
heavyDetail: (calls, avg) => `${plural(calls, 'time', 'times')} · ~${avg} tokens each`,
|
|
75
|
+
heavyExample: (cmd) => `- When running \`${cmd}\`, limit the output: only the affected test or project, or filter the relevant lines.`,
|
|
76
|
+
longTitle: 'Very long sessions',
|
|
77
|
+
longWhy: (n, min) => `${plural(n, 'conversation exceeds', 'conversations exceed')} ${min} tokens. In a long session, everything accumulated is resent on every turn.`,
|
|
78
|
+
longSuggestion: 'A new conversation per task, or /compact when you change topic.',
|
|
79
|
+
conversation: (id) => `conversation ${id}`,
|
|
80
|
+
steps: (n) => plural(n, 'step', 'steps'),
|
|
81
|
+
longUsage: (calls, real, cached) => ` · ${calls} model calls, ${real} real tokens (${cached} with cache)`,
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
map: {
|
|
85
|
+
title: '## Project map',
|
|
86
|
+
intro: '(Generated by agy-insights. To find something, start from the matching folder and search with grep.)',
|
|
87
|
+
files: (n) => (n === 1 ? 'file' : 'files'),
|
|
88
|
+
large: 'Large files: search with grep and read only the range you need, not the whole file.',
|
|
89
|
+
lines: (n) => `~${n} lines`,
|
|
90
|
+
dontRead: (list) => `Don't read (generated or dependencies): ${list}`,
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
report: {
|
|
94
|
+
applied: 'already applied',
|
|
95
|
+
in: 'In',
|
|
96
|
+
copy: 'Copy',
|
|
97
|
+
copied: 'Copied',
|
|
98
|
+
howToExclude: 'How to exclude it',
|
|
99
|
+
exampleRule: 'Example rule for AGENTS.md',
|
|
100
|
+
exampleHint: "Add it to the project's AGENTS.md only if this happens often: rules are sent with every request.",
|
|
101
|
+
cols: ['What', '~tokens', '%', 'Times', 'Sessions'],
|
|
102
|
+
sessionsTitle: 'Sessions (real usage)',
|
|
103
|
+
sessionCols: ['Conversation', 'Tokens', '%', 'With cache', 'Calls', 'Errors'],
|
|
104
|
+
noConversations: (scope, dir) => `No conversations${scope ? ` for <code>${scope}</code>` : ''} in <code>${dir}</code>.`,
|
|
105
|
+
tileReal: 'real tokens (input + output)',
|
|
106
|
+
tileCache: (calls) => `including cache · ${calls} calls`,
|
|
107
|
+
tileContext: '~tokens in context',
|
|
108
|
+
tileTop5: 'in the 5 largest sessions',
|
|
109
|
+
tileReads: 'file reads',
|
|
110
|
+
tileRereads: 're-reads of the same chunk',
|
|
111
|
+
happening: "What's happening",
|
|
112
|
+
happeningHint: 'This page changes nothing. Each proposal tells you which file it goes in and what to paste; you decide.',
|
|
113
|
+
nothing: 'No repeated or unnecessary context detected.',
|
|
114
|
+
details: 'Details',
|
|
115
|
+
byProject: 'By project', outsideGit: 'outside a git repository',
|
|
116
|
+
byKind: 'By file type', byFolder: 'By folder', files: 'Files', tools: 'Tools', commands: 'Commands',
|
|
117
|
+
title: 'Context usage in Antigravity',
|
|
118
|
+
project: 'Project', wholeHistory: 'Whole history',
|
|
119
|
+
meta: (convs, date) => `${convs} conversations · generated ${date}`,
|
|
120
|
+
footerReal: (missing) => `Real tokens: the ones Antigravity records in each conversation's log (subagents included)${missing ? `; ${missing} conversations have no log and are not counted` : ''}.`,
|
|
121
|
+
footerCache: 'It is unknown whether the quota counts cache the same as new input, so both figures are shown.',
|
|
122
|
+
footerEstimate: '~tokens = stored size of each step / 4: useful to compare files and folders with each other.',
|
|
123
|
+
footerPrivacy: 'The history was read from a copy and was not modified. This page contains paths from your projects: review it before sharing.',
|
|
124
|
+
// Text summary
|
|
125
|
+
history: 'History', projectLower: 'project',
|
|
126
|
+
summary: (convs, steps, tokens) => `${convs} conversations, ${steps} steps, ~${tokens} tokens in context (estimated)`,
|
|
127
|
+
realUsage: (real, input, output, cached, calls) => `Real usage: ${real} tokens (input ${input} + output ${output}), ${cached} including cache, in ${calls} model calls`,
|
|
128
|
+
noLog: (n) => ` (${n} conversations without a log)`,
|
|
129
|
+
topSessions: (n) => ` The ${n} sessions that use the most:`,
|
|
130
|
+
sessionLine: (id, calls, errors, cached) => `conversation ${id} ${calls} calls${errors ? `, ${errors} errors` : ''}, ${cached} with cache`,
|
|
131
|
+
top5: (pct, reads, rereads, rpct) => `The 5 largest sessions account for ${pct}. Reads: ${reads}, re-reads ${rereads} (${rpct})`,
|
|
132
|
+
pathHidden: '(path hidden, use --paths)',
|
|
133
|
+
byKindText: 'By file type:',
|
|
134
|
+
textFooter: 'Nothing was modified. Real usage = agy log; ~tokens = step size / 4 (estimated).',
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const es = {
|
|
139
|
+
locale: 'es-ES',
|
|
140
|
+
help: (dir) => `Uso: agy-insights [opciones] (o npx agy-insights, o node agy-insights.js)
|
|
141
|
+
|
|
142
|
+
Analiza tu historial de Antigravity CLI y abre un informe en el navegador.
|
|
143
|
+
|
|
144
|
+
Opciones:
|
|
145
|
+
--project <carpeta> Solo conversaciones que tocaron ese proyecto ("." = carpeta actual)
|
|
146
|
+
--text Resumen en la terminal en vez del informe HTML
|
|
147
|
+
--paths Con --text, muestra rutas y las exclusiones propuestas
|
|
148
|
+
--map Imprime un mapa del proyecto (--project, o la carpeta actual) para AGENTS.md
|
|
149
|
+
--out <fichero> Dónde guardar el informe HTML (por defecto, en la carpeta temporal)
|
|
150
|
+
--no-open No abrir el navegador
|
|
151
|
+
--lang <en|es> Idioma de la salida (por defecto, el del sistema)
|
|
152
|
+
--dir <carpeta> Otra carpeta de historial (por defecto ${dir})
|
|
153
|
+
--config <fichero> Configuración (por defecto ~/.agy-insights.json si existe)
|
|
154
|
+
--help Esta ayuda
|
|
155
|
+
`,
|
|
156
|
+
missingValue: (opt) => `Falta el valor de ${opt}`,
|
|
157
|
+
unknownOption: (opt) => `Opción desconocida: ${opt}. Usa --help.`,
|
|
158
|
+
badLang: (v) => `Idioma no soportado: ${v}. Usa en o es.`,
|
|
159
|
+
configNotFound: (f) => `No existe el fichero de configuración ${f}`,
|
|
160
|
+
cannotRead: (f, msg) => `No se puede leer ${f}: ${msg}`,
|
|
161
|
+
mapDone: (tokens, file) => `\n~${tokens} tokens en cada petición. Pégalo en ${file} si te convence; agy-insights no lo escribe.`,
|
|
162
|
+
reportName: 'informe',
|
|
163
|
+
done: (convs, found, out) => `${convs} conversaciones analizadas, ${found} hallazgos.\nInforme: ${out}`,
|
|
164
|
+
|
|
165
|
+
kinds: {
|
|
166
|
+
build: 'build output (dist, bin, obj…)', logs: 'logs, maps, minificados', generated: 'código generado / migraciones',
|
|
167
|
+
other: 'otros', code: (ext) => `código ${ext}`,
|
|
168
|
+
},
|
|
169
|
+
|
|
170
|
+
fixes: {
|
|
171
|
+
denyTitle: 'Denegar la lectura (permiso de Antigravity)',
|
|
172
|
+
denyWhere: 'dentro de "permissions" → "deny"',
|
|
173
|
+
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.',
|
|
174
|
+
ignoreTitle: 'Añadir a .antigravityignore',
|
|
175
|
+
ignoreWhere: 'al final del fichero (créalo si no existe)',
|
|
176
|
+
ignoreNote: 'Lo oculta del autocompletado con @; no está documentado que impida leerlo.',
|
|
177
|
+
ruleTitle: 'Añadir una regla a AGENTS.md',
|
|
178
|
+
ruleWhere: 'al final del fichero',
|
|
179
|
+
ruleNote: '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.',
|
|
180
|
+
},
|
|
181
|
+
|
|
182
|
+
findings: {
|
|
183
|
+
formatTitle: 'No se reconoce el formato del historial',
|
|
184
|
+
formatWhy: (steps) => `Hay ${steps} pasos pero ninguna llamada a herramienta reconocible. Probablemente agy ha cambiado cómo guarda las conversaciones.`,
|
|
185
|
+
formatSuggestion: 'Los resultados de abajo no son fiables. Actualiza agy-insights (npx agy-insights@latest) o abre una incidencia en GitHub.',
|
|
186
|
+
rereadsTitle: 'El agente vuelve a leer lo que ya tenía',
|
|
187
|
+
rereadsWhy: (n) => `${plural(n, '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.`,
|
|
188
|
+
rereadsSuggestion: '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.',
|
|
189
|
+
rereads: (n) => plural(n, 'relectura', 'relecturas'),
|
|
190
|
+
reads: (n) => plural(n, 'lectura', 'lecturas'),
|
|
191
|
+
ruleFolder: (rel, kind) => `No leas \`${rel}/\`: es ${kind}; trabaja con el código fuente.`,
|
|
192
|
+
kindPattern: (project, kind) => `${project} › todo lo de tipo «${kind}»`,
|
|
193
|
+
kindPatternDetail: 'patrón para cualquier carpeta del proyecto',
|
|
194
|
+
ruleKind: (kind, patterns) => `No leas ${kind} (${patterns}) salvo que la tarea lo pida.`,
|
|
195
|
+
nonSourceTitle: 'Se leen ficheros generados, compilados o de dependencias',
|
|
196
|
+
nonSourceWhy: '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.',
|
|
197
|
+
nonSourceSuggestion: 'Revisa si alguna de estas lecturas era necesaria. Si no, puedes impedirlas con un permiso de Antigravity.',
|
|
198
|
+
crossTitle: 'Los mismos ficheros entran en muchas sesiones',
|
|
199
|
+
crossWhy: (n, min) => `${plural(n, 'fichero se ha leído', 'ficheros se han leído')} en ${min} o más conversaciones distintas.`,
|
|
200
|
+
crossSuggestion: '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.',
|
|
201
|
+
sessions: (n) => plural(n, 'sesión', 'sesiones'),
|
|
202
|
+
heavyTitle: 'Comandos con salidas muy largas',
|
|
203
|
+
heavyWhy: 'La salida completa de cada comando entra en el contexto y se reenvía en los turnos siguientes.',
|
|
204
|
+
heavySuggestion: '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.',
|
|
205
|
+
heavyDetail: (calls, avg) => `${plural(calls, 'vez', 'veces')} · ~${avg} tokens cada una`,
|
|
206
|
+
heavyExample: (cmd) => `- Al ejecutar \`${cmd}\`, limita la salida: solo el test o proyecto afectado, o filtra las líneas relevantes.`,
|
|
207
|
+
longTitle: 'Sesiones muy largas',
|
|
208
|
+
longWhy: (n, min) => `${plural(n, 'conversación supera', 'conversaciones superan')} ${min} tokens. En una sesión larga, todo lo acumulado se vuelve a enviar en cada turno.`,
|
|
209
|
+
longSuggestion: 'Una conversación nueva por tarea, o /compact cuando cambias de tema.',
|
|
210
|
+
conversation: (id) => `conversación ${id}`,
|
|
211
|
+
steps: (n) => plural(n, 'paso', 'pasos'),
|
|
212
|
+
longUsage: (calls, real, cached) => ` · ${calls} llamadas al modelo, ${real} tokens reales (${cached} con caché)`,
|
|
213
|
+
},
|
|
214
|
+
|
|
215
|
+
map: {
|
|
216
|
+
title: '## Mapa del proyecto',
|
|
217
|
+
intro: '(Generado por agy-insights. Para encontrar algo, empieza por la carpeta que corresponda y busca con grep.)',
|
|
218
|
+
files: (n) => (n === 1 ? 'fichero' : 'ficheros'),
|
|
219
|
+
large: 'Ficheros grandes: busca con grep y lee solo el rango que necesites, no el fichero entero.',
|
|
220
|
+
lines: (n) => `~${n} líneas`,
|
|
221
|
+
dontRead: (list) => `No leas (generado o dependencias): ${list}`,
|
|
222
|
+
},
|
|
223
|
+
|
|
224
|
+
report: {
|
|
225
|
+
applied: 'ya aplicado',
|
|
226
|
+
in: 'En',
|
|
227
|
+
copy: 'Copiar',
|
|
228
|
+
copied: 'Copiado',
|
|
229
|
+
howToExclude: 'Cómo excluirlo',
|
|
230
|
+
exampleRule: 'Regla de ejemplo para AGENTS.md',
|
|
231
|
+
exampleHint: 'Ponla en el AGENTS.md del proyecto solo si te pasa a menudo: las reglas se envían en cada petición.',
|
|
232
|
+
cols: ['Qué', '~tokens', '%', 'Veces', 'Sesiones'],
|
|
233
|
+
sessionsTitle: 'Sesiones (consumo real)',
|
|
234
|
+
sessionCols: ['Conversación', 'Tokens', '%', 'Con caché', 'Llamadas', 'Errores'],
|
|
235
|
+
noConversations: (scope, dir) => `No hay conversaciones${scope ? ` de <code>${scope}</code>` : ''} en <code>${dir}</code>.`,
|
|
236
|
+
tileReal: 'tokens reales (entrada + salida)',
|
|
237
|
+
tileCache: (calls) => `contando la caché · ${calls} llamadas`,
|
|
238
|
+
tileContext: '~tokens en contexto',
|
|
239
|
+
tileTop5: 'en las 5 sesiones más grandes',
|
|
240
|
+
tileReads: 'lecturas de ficheros',
|
|
241
|
+
tileRereads: 'relecturas del mismo trozo',
|
|
242
|
+
happening: 'Qué está pasando',
|
|
243
|
+
happeningHint: 'Esta página no cambia nada. Cada propuesta te dice en qué fichero va y qué pegar; decides tú.',
|
|
244
|
+
nothing: 'No se ha detectado contexto repetido ni innecesario.',
|
|
245
|
+
details: 'Detalle',
|
|
246
|
+
byProject: 'Por proyecto', outsideGit: 'fuera de un repositorio git',
|
|
247
|
+
byKind: 'Por tipo de fichero', byFolder: 'Por carpeta', files: 'Ficheros', tools: 'Herramientas', commands: 'Comandos',
|
|
248
|
+
title: 'Uso de contexto en Antigravity',
|
|
249
|
+
project: 'Proyecto', wholeHistory: 'Todo el historial',
|
|
250
|
+
meta: (convs, date) => `${convs} conversaciones · generado ${date}`,
|
|
251
|
+
footerReal: (missing) => `Tokens reales: los que Antigravity anota en el registro de cada conversación (incluidos subagentes)${missing ? `; ${missing} conversaciones no tienen registro y no cuentan` : ''}.`,
|
|
252
|
+
footerCache: 'No se sabe si la cuota cuenta la caché igual que la entrada nueva: por eso se muestran las dos cifras.',
|
|
253
|
+
footerEstimate: '~tokens = tamaño guardado de cada paso / 4: sirve para comparar ficheros y carpetas entre sí.',
|
|
254
|
+
footerPrivacy: 'El historial se ha leído desde una copia y no se ha modificado. Esta página contiene rutas de tus proyectos: no la compartas sin revisarla.',
|
|
255
|
+
history: 'Historial', projectLower: 'proyecto',
|
|
256
|
+
summary: (convs, steps, tokens) => `${convs} conversaciones, ${steps} pasos, ~${tokens} tokens en contexto (estimado)`,
|
|
257
|
+
realUsage: (real, input, output, cached, calls) => `Consumo real: ${real} tokens (entrada ${input} + salida ${output}), ${cached} contando la caché, en ${calls} llamadas al modelo`,
|
|
258
|
+
noLog: (n) => ` (${n} conversaciones sin registro)`,
|
|
259
|
+
topSessions: (n) => ` Las ${n} sesiones que más gastan:`,
|
|
260
|
+
sessionLine: (id, calls, errors, cached) => `conversación ${id} ${calls} llamadas${errors ? `, ${errors} errores` : ''}, ${cached} con caché`,
|
|
261
|
+
top5: (pct, reads, rereads, rpct) => `Las 5 sesiones más grandes suman el ${pct}. Lecturas: ${reads}, relecturas ${rereads} (${rpct})`,
|
|
262
|
+
pathHidden: '(ruta oculta, usa --paths)',
|
|
263
|
+
byKindText: 'Por tipo de fichero:',
|
|
264
|
+
textFooter: 'No se ha modificado nada. Consumo real = registro de agy; ~tokens = tamaño del paso / 4 (estimado).',
|
|
265
|
+
},
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
const LANGS = { en, es };
|
|
269
|
+
let current = 'en';
|
|
270
|
+
|
|
271
|
+
// "es", "es-ES", "es_ES.UTF-8"… → es; anything else → en.
|
|
272
|
+
function detect(env = process.env) {
|
|
273
|
+
const sys = env.LC_ALL || env.LC_MESSAGES || env.LANG || env.LANGUAGE || Intl.DateTimeFormat().resolvedOptions().locale || '';
|
|
274
|
+
return /^es\b|^es[-_]/i.test(sys) ? 'es' : 'en';
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function setLang(lang) {
|
|
278
|
+
if (!LANGS[lang]) return false;
|
|
279
|
+
current = lang;
|
|
280
|
+
return true;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const t = new Proxy({}, { get: (_, k) => LANGS[current][k] });
|
|
284
|
+
const lang = () => current;
|
|
285
|
+
const num = (n) => Math.round(n).toLocaleString(t.locale);
|
|
286
|
+
|
|
287
|
+
// Built-in kinds are stable ids (see history.js kind()); custom kinds from the config are shown as written.
|
|
288
|
+
function kindLabel(k) {
|
|
289
|
+
if (k.startsWith('code ')) return t.kinds.code(k.slice(5));
|
|
290
|
+
return t.kinds[k] || k;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
module.exports = { t, setLang, lang, detect, num, kindLabel, LANGS };
|
package/lib/map.js
CHANGED
|
@@ -5,9 +5,10 @@ const fs = require('fs');
|
|
|
5
5
|
const path = require('path');
|
|
6
6
|
const { spawnSync } = require('child_process');
|
|
7
7
|
const { kind, NON_SOURCE } = require('./history');
|
|
8
|
+
const { t, num } = require('./i18n');
|
|
8
9
|
|
|
9
|
-
const START = '<!-- agy-insights:
|
|
10
|
-
const END = '<!-- agy-insights:
|
|
10
|
+
const START = '<!-- agy-insights:map:start -->';
|
|
11
|
+
const END = '<!-- agy-insights:map:end -->';
|
|
11
12
|
const DEFAULTS = { largeLines: 500, maxLarge: 10, maxFolders: 15, maxNames: 6 };
|
|
12
13
|
// Never walked when git is not available; with git, ignored files are skipped anyway.
|
|
13
14
|
const SKIP_DIRS = new Set(['.git', 'node_modules', '.vs', '.vscode', '.idea', '.gemini', '.agents']);
|
|
@@ -83,18 +84,18 @@ function buildMap(root, opts = {}) {
|
|
|
83
84
|
.sort((a, b) => b.reads - a.reads || b.lines - a.lines)
|
|
84
85
|
.slice(0, o.maxLarge);
|
|
85
86
|
|
|
86
|
-
const lines = [
|
|
87
|
+
const lines = [t.map.title, '', t.map.intro, ''];
|
|
87
88
|
for (const { folder, files, subdirs } of mainFolders(source, o.maxFolders)) {
|
|
88
89
|
const names = subdirs.length ? `: ${subdirs.slice(0, o.maxNames).join(', ')}${subdirs.length > o.maxNames ? '…' : ''}` : '';
|
|
89
|
-
lines.push(`- ${folder}${names} (${files} ${files
|
|
90
|
+
lines.push(`- ${folder}${names} (${files} ${t.map.files(files)})`);
|
|
90
91
|
}
|
|
91
92
|
if (large.length) {
|
|
92
|
-
lines.push('',
|
|
93
|
-
for (const x of large) lines.push(`- ${x.file} (
|
|
93
|
+
lines.push('', t.map.large);
|
|
94
|
+
for (const x of large) lines.push(`- ${x.file} (${t.map.lines(num(x.lines))})`);
|
|
94
95
|
}
|
|
95
96
|
if (generated.size) {
|
|
96
97
|
const top = [...generated].filter((g) => ![...generated].some((h) => h !== g && g.startsWith(h))).sort();
|
|
97
|
-
lines.push('',
|
|
98
|
+
lines.push('', t.map.dontRead(top.join(', ')));
|
|
98
99
|
}
|
|
99
100
|
const text = lines.join('\n');
|
|
100
101
|
return { text, block: `${START}\n${text}\n${END}`, tokens: Math.round(text.length / BYTES_PER_TOKEN), files: all.length, large };
|
package/lib/report.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
// Renders the analysis as a self-contained HTML page (opened in the browser) or as plain text.
|
|
2
2
|
// The page is static: its only interaction is copying text, so it cannot change anything on disk.
|
|
3
3
|
|
|
4
|
+
const { t, lang, num, kindLabel } = require('./i18n');
|
|
5
|
+
|
|
4
6
|
const esc = (s) => String(s ?? '').replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[c]);
|
|
5
|
-
const fmt =
|
|
7
|
+
const fmt = num;
|
|
6
8
|
const pct = (a, b) => (b ? `${Math.round((100 * a) / b)}%` : '–');
|
|
7
9
|
const label = (x) => (x.project ? `${x.project} › ${x.rel}` : x.abs || x.key);
|
|
8
10
|
|
|
@@ -14,19 +16,19 @@ const withCache = (u) => u.input + u.cache + u.output;
|
|
|
14
16
|
|
|
15
17
|
function fixBlock(f) {
|
|
16
18
|
return `<div class="fix${f.applied ? ' done' : ''}">
|
|
17
|
-
<div class="fix-head"><b>${esc(f.title)}</b>${f.applied ?
|
|
18
|
-
<p class="where"
|
|
19
|
-
<div class="code"><button class="copy" type="button"
|
|
19
|
+
<div class="fix-head"><b>${esc(f.title)}</b>${f.applied ? `<span class="pill">${t.report.applied}</span>` : ''}</div>
|
|
20
|
+
<p class="where">${t.report.in} <code>${esc(f.file)}</code>, ${esc(f.where)}:</p>
|
|
21
|
+
<div class="code"><button class="copy" type="button">${t.report.copy}</button><pre>${esc(f.text)}</pre></div>
|
|
20
22
|
<p class="hint">${esc(f.note)}</p>
|
|
21
23
|
</div>`;
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
function item(it) {
|
|
25
27
|
const extra = it.fixes.length
|
|
26
|
-
? `<details><summary
|
|
28
|
+
? `<details><summary>${t.report.howToExclude}</summary>${it.fixes.map(fixBlock).join('')}</details>`
|
|
27
29
|
: it.example
|
|
28
|
-
? `<details><summary
|
|
29
|
-
<p class="hint"
|
|
30
|
+
? `<details><summary>${t.report.exampleRule}</summary><div class="fix"><div class="code"><button class="copy" type="button">${t.report.copy}</button><pre>${esc(it.example)}</pre></div>
|
|
31
|
+
<p class="hint">${t.report.exampleHint}</p></div></details>`
|
|
30
32
|
: '';
|
|
31
33
|
return `<li><div class="row"><div class="what"><span class="label">${esc(it.label)}</span><span class="detail">${esc(it.detail)}</span></div>
|
|
32
34
|
<span class="num">${it.tokens ? `~${fmt(it.tokens)}` : ''}</span></div>${extra}</li>`;
|
|
@@ -43,7 +45,7 @@ function finding(f, total) {
|
|
|
43
45
|
function table(title, rows, total, name = label) {
|
|
44
46
|
if (!rows.length) return '';
|
|
45
47
|
const max = Math.max(...rows.map((r) => r.tokens), 1);
|
|
46
|
-
return `<h3>${esc(title)}</h3><div class="scroll"><table><thead><tr
|
|
48
|
+
return `<h3>${esc(title)}</h3><div class="scroll"><table><thead><tr>${t.report.cols.map((c, i) => `<th${i ? ' class="num"' : ''}>${c}</th>`).join('')}</tr></thead><tbody>
|
|
47
49
|
${rows.slice(0, 15).map((r) => `<tr><td><div class="bar" style="--w:${((100 * r.tokens) / max).toFixed(1)}%"></div><span>${esc(name(r))}</span></td>
|
|
48
50
|
<td class="num">${fmt(r.tokens)}</td><td class="num">${pct(r.tokens, total)}</td><td class="num">${r.calls}</td><td class="num">${r.convs || ''}</td></tr>`).join('')}
|
|
49
51
|
</tbody></table></div>`;
|
|
@@ -53,7 +55,7 @@ function sessionsTable(r) {
|
|
|
53
55
|
const rows = r.sessions.filter((s) => s.usage).slice(0, 15);
|
|
54
56
|
if (!rows.length) return '';
|
|
55
57
|
const max = Math.max(...rows.map((s) => billed(s.usage)), 1);
|
|
56
|
-
return `<h3
|
|
58
|
+
return `<h3>${t.report.sessionsTitle}</h3><div class="scroll"><table><thead><tr>${t.report.sessionCols.map((c, i) => `<th${i ? ' class="num"' : ''}>${c}</th>`).join('')}</tr></thead><tbody>
|
|
57
59
|
${rows.map((s) => `<tr><td><div class="bar" style="--w:${((100 * billed(s.usage)) / max).toFixed(1)}%"></div><span>${esc(s.id.slice(0, 8))}</span></td>
|
|
58
60
|
<td class="num">${fmt(billed(s.usage))}</td><td class="num">${pct(billed(s.usage), billed(r.usage))}</td><td class="num">${fmt(withCache(s.usage))}</td>
|
|
59
61
|
<td class="num">${s.usage.calls}</td><td class="num">${s.usage.errors || ''}</td></tr>`).join('')}
|
|
@@ -64,29 +66,29 @@ function html(r, found, { scope }) {
|
|
|
64
66
|
const top5 = r.sessions.slice(0, 5).reduce((s, x) => s + x.tokens, 0);
|
|
65
67
|
const u = r.usage;
|
|
66
68
|
const body = !r.conversations
|
|
67
|
-
? `<p
|
|
69
|
+
? `<p>${t.report.noConversations(scope && esc(scope), esc(r.dir))}</p>`
|
|
68
70
|
: `<div class="tiles">
|
|
69
|
-
${u.conversations ? `<div><b>${fmt(billed(u))}</b><span
|
|
70
|
-
<div><b>${fmt(withCache(u))}</b><span
|
|
71
|
-
<div><b>${fmt(r.tokens)}</b><span
|
|
72
|
-
<div><b>${pct(top5, r.tokens)}</b><span
|
|
73
|
-
<div><b>${r.reads}</b><span
|
|
74
|
-
<div><b>${pct(r.rereads, r.reads)}</b><span
|
|
71
|
+
${u.conversations ? `<div><b>${fmt(billed(u))}</b><span>${t.report.tileReal}</span></div>
|
|
72
|
+
<div><b>${fmt(withCache(u))}</b><span>${t.report.tileCache(u.calls)}</span></div>` : ''}
|
|
73
|
+
<div><b>${fmt(r.tokens)}</b><span>${t.report.tileContext}</span></div>
|
|
74
|
+
<div><b>${pct(top5, r.tokens)}</b><span>${t.report.tileTop5}</span></div>
|
|
75
|
+
<div><b>${r.reads}</b><span>${t.report.tileReads}</span></div>
|
|
76
|
+
<div><b>${pct(r.rereads, r.reads)}</b><span>${t.report.tileRereads}</span></div>
|
|
75
77
|
</div>
|
|
76
|
-
<h2
|
|
77
|
-
<p class="hint"
|
|
78
|
-
${found.length ? found.map((f) => finding(f, r.tokens)).join('') :
|
|
79
|
-
<details class="detail-all"><summary><h2
|
|
78
|
+
<h2>${t.report.happening}</h2>
|
|
79
|
+
<p class="hint">${t.report.happeningHint}</p>
|
|
80
|
+
${found.length ? found.map((f) => finding(f, r.tokens)).join('') : `<p>${t.report.nothing}</p>`}
|
|
81
|
+
<details class="detail-all"><summary><h2>${t.report.details}</h2></summary>
|
|
80
82
|
${sessionsTable(r)}
|
|
81
|
-
${table(
|
|
82
|
-
${table(
|
|
83
|
-
${table(
|
|
84
|
-
${table(
|
|
85
|
-
${table(
|
|
86
|
-
${table(
|
|
83
|
+
${table(t.report.byProject, r.projects, r.tokens, (x) => x.project || t.report.outsideGit)}
|
|
84
|
+
${table(t.report.byKind, r.kinds, r.tokens, (x) => kindLabel(x.key))}
|
|
85
|
+
${table(t.report.byFolder, r.folders, r.tokens)}
|
|
86
|
+
${table(t.report.files, r.files, r.tokens)}
|
|
87
|
+
${table(t.report.tools, r.tools, r.tokens, (x) => x.key)}
|
|
88
|
+
${table(t.report.commands, r.commands, r.tokens, (x) => x.key)}
|
|
87
89
|
</details>`;
|
|
88
90
|
|
|
89
|
-
return `<!doctype html><html lang="
|
|
91
|
+
return `<!doctype html><html lang="${lang()}"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
|
|
90
92
|
<title>agy-insights</title>
|
|
91
93
|
<style>
|
|
92
94
|
:root { --bg: #f7f8f7; --surface: #fff; --fg: #1d2421; --muted: #5d6963; --line: #dde3df; --accent: #1f7a5a; --accent-soft: #e3f1ea;
|
|
@@ -125,19 +127,19 @@ function html(r, found, { scope }) {
|
|
|
125
127
|
.detail-all > summary { color: var(--fg); }
|
|
126
128
|
footer { margin-top: 32px; font-size: 13px; color: var(--muted); }
|
|
127
129
|
</style></head><body><main>
|
|
128
|
-
<h1
|
|
129
|
-
<p class="meta">${scope ?
|
|
130
|
+
<h1>${t.report.title}</h1>
|
|
131
|
+
<p class="meta">${scope ? `${t.report.project} <code>${esc(scope)}</code>` : t.report.wholeHistory} · ${t.report.meta(r.conversations, esc(new Date().toLocaleString(t.locale)))}</p>
|
|
130
132
|
${body}
|
|
131
|
-
<footer
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
133
|
+
<footer>${esc(t.report.footerReal(r.conversations - u.conversations))}
|
|
134
|
+
${esc(t.report.footerCache)}
|
|
135
|
+
${esc(t.report.footerEstimate)}
|
|
136
|
+
${esc(t.report.footerPrivacy)}</footer>
|
|
135
137
|
</main>
|
|
136
138
|
<script>
|
|
137
139
|
document.addEventListener('click', (e) => {
|
|
138
140
|
const b = e.target.closest('.copy'); if (!b) return;
|
|
139
141
|
const pre = b.parentElement.querySelector('pre');
|
|
140
|
-
const ok = () => { b.textContent =
|
|
142
|
+
const ok = () => { b.textContent = ${JSON.stringify(t.report.copied)}; setTimeout(() => (b.textContent = ${JSON.stringify(t.report.copy)}), 1500); };
|
|
141
143
|
(navigator.clipboard ? navigator.clipboard.writeText(pre.textContent) : Promise.reject()).then(ok).catch(() => {
|
|
142
144
|
const range = document.createRange(); range.selectNodeContents(pre);
|
|
143
145
|
const sel = getSelection(); sel.removeAllRanges(); sel.addRange(range);
|
|
@@ -150,33 +152,33 @@ function html(r, found, { scope }) {
|
|
|
150
152
|
|
|
151
153
|
function text(r, found, { scope, paths }) {
|
|
152
154
|
const top5 = r.sessions.slice(0, 5).reduce((s, x) => s + x.tokens, 0);
|
|
153
|
-
const out = [`\
|
|
154
|
-
|
|
155
|
+
const out = [`\n${t.report.history}: ${r.dir} (${r.backend})${scope ? ` · ${t.report.projectLower} ${scope}` : ''}`,
|
|
156
|
+
t.report.summary(r.conversations, r.steps, fmt(r.tokens))];
|
|
155
157
|
if (!r.conversations) return out.join('\n') + '\n';
|
|
156
158
|
const u = r.usage;
|
|
157
159
|
if (u.conversations) {
|
|
158
|
-
out.push(
|
|
159
|
-
+ (u.conversations < r.conversations ?
|
|
160
|
+
out.push(t.report.realUsage(fmt(billed(u)), fmt(u.input), fmt(u.output), fmt(withCache(u)), u.calls)
|
|
161
|
+
+ (u.conversations < r.conversations ? t.report.noLog(r.conversations - u.conversations) : ''));
|
|
160
162
|
const top = r.sessions.filter((s) => s.usage).slice(0, 5);
|
|
161
|
-
out.push(
|
|
163
|
+
out.push(t.report.topSessions(top.length));
|
|
162
164
|
for (const s of top) {
|
|
163
|
-
out.push(` ${fmt(billed(s.usage)).padStart(10)} ${pct(billed(s.usage), billed(u)).padStart(4)}
|
|
165
|
+
out.push(` ${fmt(billed(s.usage)).padStart(10)} ${pct(billed(s.usage), billed(u)).padStart(4)} ${t.report.sessionLine(s.id.slice(0, 8), s.usage.calls, s.usage.errors, fmt(withCache(s.usage)))}`);
|
|
164
166
|
}
|
|
165
167
|
}
|
|
166
|
-
out.push(
|
|
168
|
+
out.push(t.report.top5(pct(top5, r.tokens), r.reads, r.rereads, pct(r.rereads, r.reads)));
|
|
167
169
|
for (const f of found) {
|
|
168
170
|
out.push('', `${f.level === 'warn' ? '⚠' : '•'} ${f.title}${f.tokens ? ` (~${fmt(f.tokens)} tokens, ${pct(f.tokens, r.tokens)})` : ''}`,
|
|
169
171
|
` ${f.why}`, ` → ${f.suggestion}`);
|
|
170
172
|
// Only file and folder labels are paths; commands and session ids are always shown.
|
|
171
173
|
const hasPaths = ['rereads', 'non-source', 'cross-session'].includes(f.id);
|
|
172
174
|
for (const it of f.items) {
|
|
173
|
-
out.push(` ${(it.tokens ? `~${fmt(it.tokens)}` : '').padStart(10)} ${paths || !hasPaths ? it.label :
|
|
175
|
+
out.push(` ${(it.tokens ? `~${fmt(it.tokens)}` : '').padStart(10)} ${paths || !hasPaths ? it.label : t.report.pathHidden} ${it.detail || ''}`);
|
|
174
176
|
if (paths) for (const x of it.fixes.filter((x) => !x.applied)) out.push(`${' '.repeat(16)}${x.title}: ${x.text.split('\n').join(' ')} → ${x.file}`);
|
|
175
177
|
}
|
|
176
178
|
}
|
|
177
|
-
out.push('',
|
|
178
|
-
for (const k of r.kinds.slice(0, 10)) out.push(` ${`~${fmt(k.tokens)}`.padStart(10)} ${String(k.calls).padStart(5)}× ${k.key}`);
|
|
179
|
-
out.push('',
|
|
179
|
+
out.push('', t.report.byKindText);
|
|
180
|
+
for (const k of r.kinds.slice(0, 10)) out.push(` ${`~${fmt(k.tokens)}`.padStart(10)} ${String(k.calls).padStart(5)}× ${kindLabel(k.key)}`);
|
|
181
|
+
out.push('', t.report.textFooter, '');
|
|
180
182
|
return out.join('\n');
|
|
181
183
|
}
|
|
182
184
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agy-insights",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "Measures Antigravity CLI token usage and shows which context is sent repeatedly or needlessly, in English or Spanish. Run: npx agy-insights",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"antigravity",
|
|
@@ -9,7 +9,10 @@
|
|
|
9
9
|
"gemini",
|
|
10
10
|
"tokens",
|
|
11
11
|
"context",
|
|
12
|
-
"cli"
|
|
12
|
+
"cli",
|
|
13
|
+
"token-usage",
|
|
14
|
+
"optimization",
|
|
15
|
+
"i18n"
|
|
13
16
|
],
|
|
14
17
|
"bin": {
|
|
15
18
|
"agy-insights": "agy-insights.js"
|
|
@@ -19,7 +22,8 @@
|
|
|
19
22
|
"lib/",
|
|
20
23
|
"vendor/",
|
|
21
24
|
"agy-insights.example.json",
|
|
22
|
-
"docs/"
|
|
25
|
+
"docs/",
|
|
26
|
+
"README.es.md"
|
|
23
27
|
],
|
|
24
28
|
"engines": {
|
|
25
29
|
"node": ">=20"
|