agy-insights 0.7.0 → 0.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.es.md +2 -2
- package/README.md +2 -2
- package/docs/how-it-works.md +4 -0
- package/lib/findings.js +20 -5
- package/lib/history.js +28 -5
- package/lib/i18n.js +20 -9
- package/lib/map.js +7 -6
- package/lib/report.js +7 -7
- 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.7.
|
|
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.7.2`.
|
|
40
40
|
|
|
41
41
|
**Instalándolo globalmente:**
|
|
42
42
|
|
|
@@ -87,7 +87,7 @@ Arriba están el selector de periodo y los totales. Después, una gráfica de **
|
|
|
87
87
|
| Hallazgo | Qué significa | Qué hacer |
|
|
88
88
|
|---|---|---|
|
|
89
89
|
| 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. |
|
|
90
|
-
| 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) |
|
|
90
|
+
| Ficheros generados, compilados o de dependencias | Lecturas de `bin/`, `obj/`, `dist/`, `node_modules`, migraciones, lockfiles o logs (nunca cuentan los ficheros propios de Antigravity en `~/.gemini`, como su brain, ni una carpeta `build/` o `bin/` que git mantiene en el repo, como una configuración de webpack) | Si no hacían falta, exclúyelos (ver abajo) |
|
|
91
91
|
| 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 |
|
|
92
92
|
| Comandos con salidas muy largas | La salida de tests o builds entra entera | Pide ejecutar solo el test o proyecto afectado |
|
|
93
93
|
| Sesiones muy largas | Todo lo acumulado se reenvía en cada turno | Una conversación nueva por tarea |
|
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.7.
|
|
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.7.2`.
|
|
40
40
|
|
|
41
41
|
**Globally:**
|
|
42
42
|
|
|
@@ -87,7 +87,7 @@ At the top, the period selector and the totals. Then a chart of **token usage pe
|
|
|
87
87
|
| Finding | What it means | What to do |
|
|
88
88
|
|---|---|---|
|
|
89
89
|
| The agent re-reads what it already had | It reads again a chunk of a file that was already in the conversation | Shorter sessions, one per task, or `/compact`. Don't exclude the file: the agent needs it. |
|
|
90
|
-
| Generated, build or dependency files are being read | Reads from `bin/`, `obj/`, `dist/`, `node_modules`, migrations, lockfiles or logs | If they weren't needed, exclude them (see below) |
|
|
90
|
+
| Generated, build or dependency files are being read | Reads from `bin/`, `obj/`, `dist/`, `node_modules`, migrations, lockfiles or logs (Antigravity's own files in `~/.gemini`, such as its brain, never count; nor does a `build/` or `bin/` folder that git keeps in the repo, like a webpack config) | If they weren't needed, exclude them (see below) |
|
|
91
91
|
| The same files enter many sessions | A file is read in several different conversations | Normal for core code; if it is large, tell the agent which part to look at |
|
|
92
92
|
| Commands with very long output | The full output of tests or builds enters the context | Ask it to run only the affected test or project |
|
|
93
93
|
| Very long sessions | Everything accumulated is resent on every turn | A new conversation per task |
|
package/docs/how-it-works.md
CHANGED
|
@@ -58,6 +58,10 @@ The project of each file is detected by walking up its path until a folder with
|
|
|
58
58
|
|
|
59
59
|
All user-facing text lives in `lib/i18n.js`, in an `en` and an `es` table with the same keys (a test checks it). Modules read it through `t`, which always points at the current language. The language comes from `--lang`, then the `lang` config key, then the system locale (`LC_ALL`, `LC_MESSAGES`, `LANG`, `LANGUAGE`, or `Intl`): Spanish if it starts with `es`, English otherwise. Numbers follow it too (`en-US` / `es-ES`).
|
|
60
60
|
|
|
61
|
+
Folder names alone are not enough for build output: `build/webpack/` or `bin/` often hold sources. When the file's repo is on this machine, `repoFiles()` asks git for the files it does not ignore (`git ls-files --cached --others --exclude-standard`, once per repo), and a file in that list is never kind `build`. `--map` does the same, since it lists files through git. Without git, or for a repo that is not on this machine, the folder name decides.
|
|
62
|
+
|
|
63
|
+
Anything under `~/.gemini/` is kind `agent`: the agent's own brain (plans, artifacts, its logs) and skills. It is checked first and is not in `NON_SOURCE`, so those reads are never reported as generated files or proposed for exclusion, even when a path contains `out/` or ends in `.log`.
|
|
64
|
+
|
|
61
65
|
Built-in file kinds are stable ids (`build`, `logs`, `generated`, `code .cs`…) and are only turned into text when shown, with `kindLabel()`. Kinds from `customKinds` are shown as the user wrote them.
|
|
62
66
|
|
|
63
67
|
To add a message, add it to both tables. To add a language, add a table and teach `detect()` to pick it.
|
package/lib/findings.js
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
// matters, and which official Antigravity mechanism could address it. Findings only describe and propose
|
|
3
3
|
// (see fixes.js); applying anything is always the developer's decision.
|
|
4
4
|
const fixes = require('./fixes');
|
|
5
|
-
const {
|
|
5
|
+
const { repoFiles } = require('./history');
|
|
6
|
+
const { t, num, when, kindLabel } = require('./i18n');
|
|
6
7
|
|
|
7
8
|
const DEFAULTS = { crossSessionMin: 3, heavyOutputTokens: 2000, longSessionTokens: 100000 };
|
|
8
9
|
|
|
@@ -15,6 +16,16 @@ const KIND_PATTERNS = {
|
|
|
15
16
|
'generated': ['*.designer.cs', '*.g.cs', 'Migrations/'],
|
|
16
17
|
};
|
|
17
18
|
|
|
19
|
+
// A type's patterns for one project, without the build folder names that git keeps in that repo: if
|
|
20
|
+
// build/ holds sources there (a webpack config), "build/" must not be proposed for exclusion.
|
|
21
|
+
function projectPatterns(root, kind) {
|
|
22
|
+
const files = kind === 'build' ? repoFiles(root) : null;
|
|
23
|
+
if (!files) return KIND_PATTERNS[kind];
|
|
24
|
+
const folders = new Set();
|
|
25
|
+
for (const file of files) for (const part of file.split('/').slice(0, -1)) folders.add(part);
|
|
26
|
+
return KIND_PATTERNS[kind].filter((p) => !(p.endsWith('/') && folders.has(p.slice(0, -1).toLowerCase())));
|
|
27
|
+
}
|
|
28
|
+
|
|
18
29
|
const sum = (xs, f) => xs.reduce((s, x) => s + f(x), 0);
|
|
19
30
|
const where = (x) => (x.project ? `${x.project} › ${x.rel}` : x.abs);
|
|
20
31
|
|
|
@@ -67,11 +78,13 @@ function findings(r, opts = {}) {
|
|
|
67
78
|
perProject.set(k, e);
|
|
68
79
|
}
|
|
69
80
|
for (const e of perProject.values()) {
|
|
81
|
+
const patterns = projectPatterns(e.root, e.kind);
|
|
82
|
+
if (!patterns.length) continue;
|
|
70
83
|
items.push({
|
|
71
84
|
label: f.kindPattern(e.project, kindLabel(e.kind)), tokens: e.tokens, detail: f.kindPatternDetail,
|
|
72
85
|
fixes: [
|
|
73
|
-
fixes.ignore(e.root,
|
|
74
|
-
fixes.rule(e.root, f.ruleKind(kindLabel(e.kind),
|
|
86
|
+
fixes.ignore(e.root, patterns),
|
|
87
|
+
fixes.rule(e.root, f.ruleKind(kindLabel(e.kind), patterns.join(', '))),
|
|
75
88
|
],
|
|
76
89
|
});
|
|
77
90
|
}
|
|
@@ -117,9 +130,11 @@ function findings(r, opts = {}) {
|
|
|
117
130
|
id: 'long-sessions', level: 'info', title: f.longTitle, tokens: sum(long, (s) => s.tokens),
|
|
118
131
|
why: f.longWhy(long.length, num(o.longSessionTokens)), suggestion: f.longSuggestion,
|
|
119
132
|
items: long.slice(0, 5).map((s) => ({
|
|
120
|
-
|
|
133
|
+
// Real tokens when agy logged them (`exact`: shown without "~"), else the size estimate.
|
|
134
|
+
label: f.conversation(s.id.slice(0, 8), when(s.start, s.end)), fixes: [],
|
|
135
|
+
tokens: s.usage ? s.usage.input + s.usage.output : s.tokens, exact: Boolean(s.usage),
|
|
121
136
|
detail: f.steps(s.steps) + (s.usage
|
|
122
|
-
? f.longUsage(s.usage.calls, num(s.usage.input + s.usage.
|
|
137
|
+
? f.longUsage(s.usage.calls, num(s.usage.input + s.usage.cache + s.usage.output))
|
|
123
138
|
: ''),
|
|
124
139
|
})),
|
|
125
140
|
});
|
package/lib/history.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
const fs = require('fs');
|
|
6
6
|
const os = require('os');
|
|
7
7
|
const path = require('path');
|
|
8
|
+
const { spawnSync } = require('child_process');
|
|
8
9
|
|
|
9
10
|
const DEFAULT_DIR = path.join(os.homedir(), '.gemini', 'antigravity-cli', 'conversations');
|
|
10
11
|
const BYTES_PER_TOKEN = 4;
|
|
@@ -125,9 +126,18 @@ const key = (p) => norm(p).toLowerCase().replace(/\/+$/, '');
|
|
|
125
126
|
// Kinds are stable ids; i18n.kindLabel() turns them into text.
|
|
126
127
|
const NON_SOURCE = new Set(['node_modules', 'build', 'lockfiles', 'logs', 'generated']);
|
|
127
128
|
|
|
128
|
-
|
|
129
|
+
/**
|
|
130
|
+
* @param {string} p path with "/" separators
|
|
131
|
+
* @param {object} [opts]
|
|
132
|
+
* @param {boolean} [opts.inRepo] git says the file is part of the repo (not ignored): a build/ or bin/
|
|
133
|
+
* folder then holds sources (webpack config, CLI scripts), not build output.
|
|
134
|
+
*/
|
|
135
|
+
function kind(p, { inRepo = false } = {}) {
|
|
136
|
+
// Antigravity's own data (the agent's brain: plans, artifacts, logs; and skills): it needs them, so they
|
|
137
|
+
// are never reported as generated or proposed for exclusion, whatever their names look like.
|
|
138
|
+
if (/(^|\/)\.gemini\//i.test(p)) return 'agent';
|
|
129
139
|
if (/(^|\/)node_modules\//.test(p)) return 'node_modules';
|
|
130
|
-
if (/(^|\/)(dist|build|out|target|coverage|\.next|bin|obj)\//i.test(p)) return 'build';
|
|
140
|
+
if (!inRepo && /(^|\/)(dist|build|out|target|coverage|\.next|bin|obj)\//i.test(p)) return 'build';
|
|
131
141
|
if (/(package-lock\.json|yarn\.lock|pnpm-lock\.yaml|packages\.lock\.json|\.lock)$/.test(p)) return 'lockfiles';
|
|
132
142
|
if (/\.(log|map)$|\.min\.(js|css)$/.test(p)) return 'logs';
|
|
133
143
|
if (/(^|\/)(tests?|__tests__|specs?)\//i.test(p) || /\.(test|spec|tests?)\.\w+$/i.test(p) || /Tests?\//.test(p)) return 'tests';
|
|
@@ -136,6 +146,17 @@ function kind(p) {
|
|
|
136
146
|
return ext ? `code ${ext}` : 'other';
|
|
137
147
|
}
|
|
138
148
|
|
|
149
|
+
// Files of a git repo that git does not ignore (tracked, or untracked and not ignored), lowercased and
|
|
150
|
+
// relative to the root; null when git cannot tell (no git, or the repo is not on this machine).
|
|
151
|
+
const repoFilesCache = new Map();
|
|
152
|
+
function repoFiles(root) {
|
|
153
|
+
if (!repoFilesCache.has(root)) {
|
|
154
|
+
const git = spawnSync('git', ['ls-files', '--cached', '--others', '--exclude-standard', '-z'], { cwd: root, encoding: 'utf8', maxBuffer: 1 << 28, windowsHide: true });
|
|
155
|
+
repoFilesCache.set(root, git.status === 0 ? new Set(git.stdout.split('\0').filter(Boolean).map((f) => f.toLowerCase())) : null);
|
|
156
|
+
}
|
|
157
|
+
return repoFilesCache.get(root);
|
|
158
|
+
}
|
|
159
|
+
|
|
139
160
|
// Project root of a file: an explicit workspace containing it, or else the nearest folder with a .git
|
|
140
161
|
// entry, when the path exists on this machine. Null when unknown (e.g. a repo that was deleted).
|
|
141
162
|
const rootCache = new Map();
|
|
@@ -229,7 +250,9 @@ class Counter {
|
|
|
229
250
|
*/
|
|
230
251
|
async function analyze({ dir = DEFAULT_DIR, workspaces = [], customKinds = {}, since = null } = {}) {
|
|
231
252
|
const custom = Object.entries(customKinds).map(([re, label]) => [new RegExp(re, 'i'), label]);
|
|
232
|
-
|
|
253
|
+
// A file git keeps in the repo is never build output, whatever its folder is called.
|
|
254
|
+
const classify = (p, root, rel) => custom.find(([re]) => re.test(p))?.[1]
|
|
255
|
+
|| kind(p, { inRepo: Boolean(root && repoFiles(root)?.has(rel.toLowerCase())) });
|
|
233
256
|
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'agy-insights-'));
|
|
234
257
|
const r = {
|
|
235
258
|
dir, backend: nodeSqlite ? 'node:sqlite' : 'sql.js', conversations: 0, unreadable: 0, steps: 0, tokens: 0,
|
|
@@ -303,7 +326,7 @@ async function analyze({ dir = DEFAULT_DIR, workspaces = [], customKinds = {}, s
|
|
|
303
326
|
|
|
304
327
|
// Folders and files are keyed by absolute path: several projects can share relative paths.
|
|
305
328
|
const { root, project, rel, folderRel, folderAbs } = locate(file, workspaces);
|
|
306
|
-
const k = classify(file);
|
|
329
|
+
const k = classify(file, root, rel);
|
|
307
330
|
const fields = { calls, tokens, rereads: isNew ? reread : 0, rereadTokens: reread * tokens };
|
|
308
331
|
kinds.add(k, { ...fields, convs: once(seenKinds, k) }, { nonSource: NON_SOURCE.has(k) });
|
|
309
332
|
projects.add(root || '', { ...fields, convs: once(seenProjects, root || '') }, { project });
|
|
@@ -363,4 +386,4 @@ async function analyze({ dir = DEFAULT_DIR, workspaces = [], customKinds = {}, s
|
|
|
363
386
|
};
|
|
364
387
|
}
|
|
365
388
|
|
|
366
|
-
module.exports = { analyze, DEFAULT_DIR, kind, NON_SOURCE, realUsage, dayKey, modelOf };
|
|
389
|
+
module.exports = { analyze, DEFAULT_DIR, kind, NON_SOURCE, realUsage, dayKey, modelOf, repoFiles };
|
package/lib/i18n.js
CHANGED
|
@@ -36,6 +36,7 @@ Options:
|
|
|
36
36
|
done: (convs, found, out) => `${plural(convs, 'conversation', 'conversations')} analyzed, ${plural(found, 'finding', 'findings')}.\nReport: ${out}`,
|
|
37
37
|
|
|
38
38
|
kinds: {
|
|
39
|
+
agent: "Antigravity's own files (brain, skills)",
|
|
39
40
|
build: 'build output (dist, bin, obj…)', logs: 'logs, maps, minified', generated: 'generated code / migrations',
|
|
40
41
|
other: 'other', code: (ext) => `code ${ext}`,
|
|
41
42
|
},
|
|
@@ -80,9 +81,9 @@ Options:
|
|
|
80
81
|
longTitle: 'Very long sessions',
|
|
81
82
|
longWhy: (n, min) => `${plural(n, 'conversation exceeds', 'conversations exceed')} ${min} tokens. In a long session, everything accumulated is resent on every turn.`,
|
|
82
83
|
longSuggestion: 'A new conversation per task, or /compact when you change topic.',
|
|
83
|
-
conversation: (id) => `conversation ${id}`,
|
|
84
|
+
conversation: (id, at) => `conversation ${id}${at ? ` · ${at}` : ''}`,
|
|
84
85
|
steps: (n) => plural(n, 'step', 'steps'),
|
|
85
|
-
longUsage: (calls,
|
|
86
|
+
longUsage: (calls, cached) => ` · ${plural(calls, 'model call', 'model calls')} · real tokens, ${cached} with cache`,
|
|
86
87
|
},
|
|
87
88
|
|
|
88
89
|
map: {
|
|
@@ -113,7 +114,7 @@ Options:
|
|
|
113
114
|
otherModels: (n) => ` +${n} more`,
|
|
114
115
|
efforts: { minimal: 'Minimal', low: 'Low', medium: 'Medium', high: 'High', xhigh: 'Extra high', 'extra high': 'Extra high', max: 'Max' },
|
|
115
116
|
sessionsTitle: 'Sessions (real usage)',
|
|
116
|
-
sessionCols: ['Conversation', 'Model', 'Tokens', '%', 'With cache', 'Calls', 'Errors'],
|
|
117
|
+
sessionCols: ['Conversation', 'When', 'Model', 'Tokens', '%', 'With cache', 'Calls', 'Errors'],
|
|
117
118
|
noConversations: (scope, dir) => `No conversations${scope ? ` for <code>${scope}</code>` : ''} in <code>${dir}</code>.`,
|
|
118
119
|
tileReal: 'real tokens (input + output)',
|
|
119
120
|
tileCache: (calls) => `including cache · ${plural(calls, 'call', 'calls')}`,
|
|
@@ -157,7 +158,7 @@ Options:
|
|
|
157
158
|
realUsage: (real, input, output, cached, calls) => `Real usage: ${real} tokens (input ${input} + output ${output}), ${cached} including cache, in ${plural(calls, 'model call', 'model calls')}`,
|
|
158
159
|
noLog: (n) => ` (${plural(n, 'conversation', 'conversations')} without a log)`,
|
|
159
160
|
topSessions: (n) => (n === 1 ? ' The session that uses the most:' : ` The ${n} sessions that use the most:`),
|
|
160
|
-
sessionLine: (id, calls, errors, cached, model) => `conversation ${id}${model ? ` (${model})` : ''} ${plural(calls, 'call', 'calls')}${errors ? `, ${plural(errors, 'error', 'errors')}` : ''}, ${cached} with cache`,
|
|
161
|
+
sessionLine: (id, calls, errors, cached, model, at) => `conversation ${id}${at ? ` · ${at}` : ''}${model ? ` (${model})` : ''} ${plural(calls, 'call', 'calls')}${errors ? `, ${plural(errors, 'error', 'errors')}` : ''}, ${cached} with cache`,
|
|
161
162
|
top5: (pct, reads, rereads, rpct) => `The 5 largest sessions account for ${pct}. Reads: ${reads}, re-reads ${rereads} (${rpct})`,
|
|
162
163
|
pathHidden: '(path hidden, use --paths)',
|
|
163
164
|
byKindText: 'By file type:',
|
|
@@ -197,6 +198,7 @@ Opciones:
|
|
|
197
198
|
done: (convs, found, out) => `${plural(convs, 'conversación analizada', 'conversaciones analizadas')}, ${plural(found, 'hallazgo', 'hallazgos')}.\nInforme: ${out}`,
|
|
198
199
|
|
|
199
200
|
kinds: {
|
|
201
|
+
agent: 'ficheros propios de Antigravity (brain, skills)',
|
|
200
202
|
build: 'build output (dist, bin, obj…)', logs: 'logs, maps, minificados', generated: 'código generado / migraciones',
|
|
201
203
|
other: 'otros', code: (ext) => `código ${ext}`,
|
|
202
204
|
},
|
|
@@ -241,9 +243,9 @@ Opciones:
|
|
|
241
243
|
longTitle: 'Sesiones muy largas',
|
|
242
244
|
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.`,
|
|
243
245
|
longSuggestion: 'Una conversación nueva por tarea, o /compact cuando cambias de tema.',
|
|
244
|
-
conversation: (id) => `conversación ${id}`,
|
|
246
|
+
conversation: (id, at) => `conversación ${id}${at ? ` · ${at}` : ''}`,
|
|
245
247
|
steps: (n) => plural(n, 'paso', 'pasos'),
|
|
246
|
-
longUsage: (calls,
|
|
248
|
+
longUsage: (calls, cached) => ` · ${plural(calls, 'llamada', 'llamadas')} al modelo · tokens reales, ${cached} con caché`,
|
|
247
249
|
},
|
|
248
250
|
|
|
249
251
|
map: {
|
|
@@ -274,7 +276,7 @@ Opciones:
|
|
|
274
276
|
otherModels: (n) => ` +${n} más`,
|
|
275
277
|
efforts: { minimal: 'Mínimo', low: 'Bajo', medium: 'Medio', high: 'Alto', xhigh: 'Muy alto', 'extra high': 'Muy alto', max: 'Máximo' },
|
|
276
278
|
sessionsTitle: 'Sesiones (consumo real)',
|
|
277
|
-
sessionCols: ['Conversación', 'Modelo', 'Tokens', '%', 'Con caché', 'Llamadas', 'Errores'],
|
|
279
|
+
sessionCols: ['Conversación', 'Cuándo', 'Modelo', 'Tokens', '%', 'Con caché', 'Llamadas', 'Errores'],
|
|
278
280
|
noConversations: (scope, dir) => `No hay conversaciones${scope ? ` de <code>${scope}</code>` : ''} en <code>${dir}</code>.`,
|
|
279
281
|
tileReal: 'tokens reales (entrada + salida)',
|
|
280
282
|
tileCache: (calls) => `contando la caché · ${plural(calls, 'llamada', 'llamadas')}`,
|
|
@@ -317,7 +319,7 @@ Opciones:
|
|
|
317
319
|
realUsage: (real, input, output, cached, calls) => `Consumo real: ${real} tokens (entrada ${input} + salida ${output}), ${cached} contando la caché, en ${plural(calls, 'llamada', 'llamadas')} al modelo`,
|
|
318
320
|
noLog: (n) => ` (${plural(n, 'conversación', 'conversaciones')} sin registro)`,
|
|
319
321
|
topSessions: (n) => (n === 1 ? ' La sesión que más gasta:' : ` Las ${n} sesiones que más gastan:`),
|
|
320
|
-
sessionLine: (id, calls, errors, cached, model) => `conversación ${id}${model ? ` (${model})` : ''} ${plural(calls, 'llamada', 'llamadas')}${errors ? `, ${plural(errors, 'error', 'errores')}` : ''}, ${cached} con caché`,
|
|
322
|
+
sessionLine: (id, calls, errors, cached, model, at) => `conversación ${id}${at ? ` · ${at}` : ''}${model ? ` (${model})` : ''} ${plural(calls, 'llamada', 'llamadas')}${errors ? `, ${plural(errors, 'error', 'errores')}` : ''}, ${cached} con caché`,
|
|
321
323
|
top5: (pct, reads, rereads, rpct) => `Las 5 sesiones más grandes suman el ${pct}. Lecturas: ${reads}, relecturas ${rereads} (${rpct})`,
|
|
322
324
|
pathHidden: '(ruta oculta, usa --paths)',
|
|
323
325
|
byKindText: 'Por tipo de fichero:',
|
|
@@ -344,6 +346,15 @@ const t = new Proxy({}, { get: (_, k) => LANGS[current][k] });
|
|
|
344
346
|
const lang = () => current;
|
|
345
347
|
const num = (n) => Math.round(n).toLocaleString(t.locale);
|
|
346
348
|
const date = (ms) => new Date(ms).toLocaleDateString(t.locale, { day: 'numeric', month: 'short', year: 'numeric' });
|
|
349
|
+
const time = (ms) => new Date(ms).toLocaleTimeString(t.locale, { hour: '2-digit', minute: '2-digit' });
|
|
350
|
+
|
|
351
|
+
// When a session happened: "29 Oct 2026, 11:16–11:40", or "27 Oct 2026, 09:02 – 29 Oct 2026, 11:40" when it
|
|
352
|
+
// spans several days; '' when unknown.
|
|
353
|
+
function when(start, end) {
|
|
354
|
+
if (!start || !end) return '';
|
|
355
|
+
if (date(start) !== date(end)) return `${date(start)}, ${time(start)} – ${date(end)}, ${time(end)}`;
|
|
356
|
+
return time(start) === time(end) ? `${date(start)}, ${time(start)}` : `${date(start)}, ${time(start)}–${time(end)}`;
|
|
357
|
+
}
|
|
347
358
|
|
|
348
359
|
// Built-in kinds are stable ids (see history.js kind()); custom kinds from the config are shown as written.
|
|
349
360
|
function kindLabel(k) {
|
|
@@ -354,4 +365,4 @@ function kindLabel(k) {
|
|
|
354
365
|
// "High" / "high" → "High" or "Alto"; levels agy may add later are shown as written.
|
|
355
366
|
const effortLabel = (e) => (e ? t.report.efforts[e.toLowerCase()] || e : t.report.unknownModel);
|
|
356
367
|
|
|
357
|
-
module.exports = { t, setLang, lang, detect, num, date, kindLabel, effortLabel, LANGS };
|
|
368
|
+
module.exports = { t, setLang, lang, detect, num, date, when, kindLabel, effortLabel, LANGS };
|
package/lib/map.js
CHANGED
|
@@ -15,10 +15,10 @@ const SKIP_DIRS = new Set(['.git', 'node_modules', '.vs', '.vscode', '.idea', '.
|
|
|
15
15
|
const BYTES_PER_TOKEN = 4;
|
|
16
16
|
|
|
17
17
|
// Project files relative to `root`, with "/" separators: tracked plus untracked-but-not-ignored when
|
|
18
|
-
// it is a git repo, else a plain walk.
|
|
18
|
+
// it is a git repo (`inRepo`: none of them is ignored build output), else a plain walk.
|
|
19
19
|
function listFiles(root) {
|
|
20
20
|
const git = spawnSync('git', ['ls-files', '--cached', '--others', '--exclude-standard', '-z'], { cwd: root, encoding: 'utf8', maxBuffer: 1 << 28 });
|
|
21
|
-
if (git.status === 0) return [...new Set(git.stdout.split('\0').filter(Boolean))];
|
|
21
|
+
if (git.status === 0) return { files: [...new Set(git.stdout.split('\0').filter(Boolean))], inRepo: true };
|
|
22
22
|
const out = [];
|
|
23
23
|
const walk = (rel) => {
|
|
24
24
|
for (const e of fs.readdirSync(path.join(root, rel), { withFileTypes: true })) {
|
|
@@ -27,7 +27,7 @@ function listFiles(root) {
|
|
|
27
27
|
}
|
|
28
28
|
};
|
|
29
29
|
walk('');
|
|
30
|
-
return out;
|
|
30
|
+
return { files: out, inRepo: false };
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
// Line count of a text file; null for binaries or unreadable files.
|
|
@@ -71,11 +71,12 @@ function mainFolders(files, maxFolders) {
|
|
|
71
71
|
*/
|
|
72
72
|
function buildMap(root, opts = {}) {
|
|
73
73
|
const o = { ...DEFAULTS, ...opts };
|
|
74
|
-
const all = listFiles(root);
|
|
74
|
+
const { files: all, inRepo } = listFiles(root);
|
|
75
75
|
const generated = new Set(), source = [];
|
|
76
76
|
for (const f of all) {
|
|
77
|
-
const
|
|
78
|
-
|
|
77
|
+
const nonSource = NON_SOURCE.has(kind(f, { inRepo }));
|
|
78
|
+
const g = nonSource ? generatedFolder(f) : null;
|
|
79
|
+
if (g) generated.add(g); else if (!nonSource) source.push(f);
|
|
79
80
|
}
|
|
80
81
|
const reads = new Map((o.history || []).filter((h) => h.root).map((h) => [`${h.root}/${h.rel}`.toLowerCase(), h.calls]));
|
|
81
82
|
const rootNorm = root.replace(/[\\/]+/g, '/').replace(/\/$/, '');
|
package/lib/report.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// The page is static: its only interactions are switching the period, the chart tooltip and copying text,
|
|
3
3
|
// so it cannot change anything on disk.
|
|
4
4
|
|
|
5
|
-
const { t, lang, num, date, kindLabel, effortLabel } = require('./i18n');
|
|
5
|
+
const { t, lang, num, date, when, kindLabel, effortLabel } = require('./i18n');
|
|
6
6
|
const { dayKey } = require('./history');
|
|
7
7
|
|
|
8
8
|
const esc = (s) => String(s ?? '').replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[c]);
|
|
@@ -35,7 +35,7 @@ function item(it) {
|
|
|
35
35
|
<p class="hint">${t.report.exampleHint}</p></div></details>`
|
|
36
36
|
: '';
|
|
37
37
|
return `<li><div class="row"><div class="what"><span class="label">${esc(it.label)}</span><span class="detail">${esc(it.detail)}</span></div>
|
|
38
|
-
<span class="num">${it.tokens ?
|
|
38
|
+
<span class="num">${it.tokens ? `${it.exact ? '' : '~'}${fmt(it.tokens)}` : ''}</span></div>${extra}</li>`;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
function finding(f, total) {
|
|
@@ -59,8 +59,8 @@ function sessionsTable(r) {
|
|
|
59
59
|
const rows = r.sessions.filter((s) => s.usage).slice(0, 15);
|
|
60
60
|
if (!rows.length) return '';
|
|
61
61
|
const max = Math.max(...rows.map((s) => billed(s.usage)), 1);
|
|
62
|
-
return `<h3>${t.report.sessionsTitle}</h3><div class="scroll"><table><thead><tr>${t.report.sessionCols.map((c, i) => `<th${i >
|
|
63
|
-
${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><td>${esc(sessionModel(s))}</td>
|
|
62
|
+
return `<h3>${t.report.sessionsTitle}</h3><div class="scroll"><table><thead><tr>${t.report.sessionCols.map((c, i) => `<th${i > 2 ? ' class="num"' : ''}>${c}</th>`).join('')}</tr></thead><tbody>
|
|
63
|
+
${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><td class="when">${esc(when(s.start, s.end))}</td><td>${esc(sessionModel(s))}</td>
|
|
64
64
|
<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>
|
|
65
65
|
<td class="num">${s.usage.calls}</td><td class="num">${s.usage.errors || ''}</td></tr>`).join('')}
|
|
66
66
|
</tbody></table></div>`;
|
|
@@ -247,7 +247,7 @@ function htmlViews(views, { scope }) {
|
|
|
247
247
|
.copy:focus-visible { outline: 2px solid var(--accent); }
|
|
248
248
|
.scroll { overflow-x: auto; } table { width: 100%; border-collapse: collapse; font-size: 14px; margin: 6px 0 18px; }
|
|
249
249
|
th { text-align: left; color: var(--muted); font-weight: 600; } th, td { padding: 5px 8px; border-bottom: 1px solid var(--line); }
|
|
250
|
-
td:first-child { position: relative; word-break: break-all; }
|
|
250
|
+
td:first-child { position: relative; word-break: break-all; } td.when { white-space: nowrap; }
|
|
251
251
|
.bar { position: absolute; inset: 3px auto 3px 0; width: var(--w); background: var(--bar); opacity: .15; border-radius: 3px; } td span { position: relative; }
|
|
252
252
|
.detail-all > summary { color: var(--fg); }
|
|
253
253
|
footer { margin-top: 32px; font-size: 13px; color: var(--muted); }
|
|
@@ -316,7 +316,7 @@ function text(r, found, { scope, paths }) {
|
|
|
316
316
|
const top = r.sessions.filter((s) => s.usage).slice(0, 5);
|
|
317
317
|
out.push(t.report.topSessions(top.length));
|
|
318
318
|
for (const s of top) {
|
|
319
|
-
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)), sessionModel(s))}`);
|
|
319
|
+
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)), sessionModel(s), when(s.start, s.end))}`);
|
|
320
320
|
}
|
|
321
321
|
}
|
|
322
322
|
out.push(t.report.top5(pct(top5, r.tokens), r.reads, r.rereads, pct(r.rereads, r.reads)));
|
|
@@ -335,7 +335,7 @@ function text(r, found, { scope, paths }) {
|
|
|
335
335
|
// Only file and folder labels are paths; commands and session ids are always shown.
|
|
336
336
|
const hasPaths = ['rereads', 'non-source', 'cross-session'].includes(f.id);
|
|
337
337
|
for (const it of f.items) {
|
|
338
|
-
out.push(` ${(it.tokens ?
|
|
338
|
+
out.push(` ${(it.tokens ? `${it.exact ? '' : '~'}${fmt(it.tokens)}` : '').padStart(10)} ${paths || !hasPaths ? it.label : t.report.pathHidden} ${it.detail || ''}`);
|
|
339
339
|
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}`);
|
|
340
340
|
}
|
|
341
341
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agy-insights",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.2",
|
|
4
4
|
"description": "Measures Antigravity CLI token usage and shows which context is sent repeatedly or needlessly, in English or Spanish. Run: npx agy-insights@latest",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"keywords": [
|