agy-insights 0.4.0 → 0.7.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 +18 -15
- package/README.md +18 -15
- package/agy-insights.js +35 -3
- package/docs/how-it-works.md +5 -1
- package/lib/history.js +78 -12
- package/lib/i18n.js +87 -23
- package/lib/report.js +198 -35
- package/package.json +2 -2
package/README.es.md
CHANGED
|
@@ -10,8 +10,8 @@ Te muestra qué contexto le estás mandando a **Antigravity** de forma repetida
|
|
|
10
10
|
- Funciona con cualquier editor, en Windows, macOS y Linux.
|
|
11
11
|
|
|
12
12
|
```sh
|
|
13
|
-
npx agy-insights
|
|
14
|
-
npx agy-insights --text
|
|
13
|
+
npx agy-insights@latest # informe HTML en el navegador
|
|
14
|
+
npx agy-insights@latest --text # resumen en la terminal
|
|
15
15
|
```
|
|
16
16
|
|
|
17
17
|
Solo necesitas Node.js 20 o superior y haber usado Antigravity CLI (`agy`) en el equipo. La salida sale en español o en inglés según el idioma del sistema; puedes forzarlo con `--lang es` o `--lang en`.
|
|
@@ -33,10 +33,10 @@ No tiene dependencias. Elige una forma:
|
|
|
33
33
|
|
|
34
34
|
```sh
|
|
35
35
|
npx agy-insights
|
|
36
|
-
npx agy-insights --text
|
|
36
|
+
npx agy-insights@latest --text
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
-
Para fijar una versión: `npx agy-insights@0.
|
|
39
|
+
`@latest` hace que npx compruebe cada vez si hay una versión nueva. Sin él, una copia instalada globalmente (`npm install -g`) o en el proyecto actual tiene prioridad y no se actualiza nunca. Para fijar una versión concreta: `npx agy-insights@0.7.0`.
|
|
40
40
|
|
|
41
41
|
**Instalándolo globalmente:**
|
|
42
42
|
|
|
@@ -45,13 +45,15 @@ npm install -g agy-insights
|
|
|
45
45
|
agy-insights
|
|
46
46
|
```
|
|
47
47
|
|
|
48
|
+
Para actualizarlo: `npm install -g agy-insights@latest`.
|
|
49
|
+
|
|
48
50
|
**Clonando el repositorio:**
|
|
49
51
|
|
|
50
52
|
```sh
|
|
51
53
|
git clone https://github.com/migohe14/agy-insights.git
|
|
52
54
|
```
|
|
53
55
|
|
|
54
|
-
Y ejecútalo con `node agy-insights/agy-insights.js` en lugar de `npx agy-insights`. Para actualizar: `git pull` dentro de la carpeta.
|
|
56
|
+
Y ejecútalo con `node agy-insights/agy-insights.js` en lugar de `npx agy-insights@latest`. Para actualizar: `git pull` dentro de la carpeta.
|
|
55
57
|
|
|
56
58
|
## Usar
|
|
57
59
|
|
|
@@ -59,18 +61,19 @@ Y ejecútalo con `node agy-insights/agy-insights.js` en lugar de `npx agy-insigh
|
|
|
59
61
|
npx agy-insights
|
|
60
62
|
```
|
|
61
63
|
|
|
62
|
-
Analiza todo tu historial, guarda un informe HTML en la carpeta temporal y lo abre en el navegador.
|
|
64
|
+
Analiza todo tu historial (todas las conversaciones que guarde agy, sin límite de fecha), guarda un informe HTML en la carpeta temporal y lo abre en el navegador. En el informe puedes cambiar entre todo el historial y los últimos 90, 30 o 7 días: los totales, la gráfica, los hallazgos y las tablas siguen el periodo elegido.
|
|
63
65
|
|
|
64
66
|
| Para… | Ejecuta |
|
|
65
67
|
|---|---|
|
|
66
|
-
| Ver solo un proyecto | `npx agy-insights --project C:\dev\mi-proyecto` |
|
|
67
|
-
| Ver el proyecto de la carpeta actual | `npx agy-insights --project .` |
|
|
68
|
-
| Un resumen en la terminal | `npx agy-insights --text` |
|
|
69
|
-
| El resumen con rutas y exclusiones | `npx agy-insights --text --paths` |
|
|
70
|
-
| Un mapa del proyecto para pegar en `AGENTS.md` (carpetas, ficheros grandes, carpetas generadas; ~20 líneas aunque el repo sea grande) | `npx agy-insights --map --project <carpeta>` |
|
|
71
|
-
|
|
|
72
|
-
|
|
|
73
|
-
|
|
|
68
|
+
| Ver solo un proyecto | `npx agy-insights@latest --project C:\dev\mi-proyecto` |
|
|
69
|
+
| Ver el proyecto de la carpeta actual | `npx agy-insights@latest --project .` |
|
|
70
|
+
| Un resumen en la terminal | `npx agy-insights@latest --text` |
|
|
71
|
+
| El resumen con rutas y exclusiones | `npx agy-insights@latest --text --paths` |
|
|
72
|
+
| Un mapa del proyecto para pegar en `AGENTS.md` (carpetas, ficheros grandes, carpetas generadas; ~20 líneas aunque el repo sea grande) | `npx agy-insights@latest --map --project <carpeta>` |
|
|
73
|
+
| Solo un periodo a medida (el informe ya no tiene selector de periodo) | `npx agy-insights@latest --since 2026-10-01` o `--days 14` |
|
|
74
|
+
| Elegir el idioma (español o inglés) | `npx agy-insights@latest --lang es` |
|
|
75
|
+
| Guardar el informe en otro sitio | `npx agy-insights@latest --out informe.html --no-open` |
|
|
76
|
+
| Ver todas las opciones | `npx agy-insights@latest --help` |
|
|
74
77
|
|
|
75
78
|
El informe da dos cifras:
|
|
76
79
|
|
|
@@ -79,7 +82,7 @@ El informe da dos cifras:
|
|
|
79
82
|
|
|
80
83
|
## Leer el informe
|
|
81
84
|
|
|
82
|
-
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`.
|
|
85
|
+
Arriba están el selector de periodo y los totales. Después, una gráfica de **consumo de tokens por día** (por semana si el historial pasa de ~4 meses) para ver la evolución; pasa el ratón por una columna para ver sus cifras, o abre «Ver como tabla». Después, **Modelos y esfuerzo**: llamadas y tokens reales por modelo y nivel de esfuerzo (por ejemplo Gemini 3.8 Flash · Alto), con los tokens por llamada, para ver cuánto cuesta un esfuerzo mayor. Debajo, **Qué está pasando** reúne los hallazgos; al final, el detalle por proyecto, tipo de fichero, carpeta, fichero, herramienta y comando. El proyecto de cada fichero se detecta por su carpeta `.git`.
|
|
83
86
|
|
|
84
87
|
| Hallazgo | Qué significa | Qué hacer |
|
|
85
88
|
|---|---|---|
|
package/README.md
CHANGED
|
@@ -10,8 +10,8 @@ Shows which context you are sending to **Antigravity** repeatedly or needlessly,
|
|
|
10
10
|
- Works with any editor, on Windows, macOS and Linux.
|
|
11
11
|
|
|
12
12
|
```sh
|
|
13
|
-
npx agy-insights
|
|
14
|
-
npx agy-insights --text
|
|
13
|
+
npx agy-insights@latest # HTML report in the browser
|
|
14
|
+
npx agy-insights@latest --text # summary in the terminal
|
|
15
15
|
```
|
|
16
16
|
|
|
17
17
|
All you need is Node.js 20 or later and to have used Antigravity CLI (`agy`) on this machine. The output is in English or Spanish, following your system language; force it with `--lang en` or `--lang es`.
|
|
@@ -33,10 +33,10 @@ It has no dependencies. Pick one:
|
|
|
33
33
|
|
|
34
34
|
```sh
|
|
35
35
|
npx agy-insights
|
|
36
|
-
npx agy-insights --text
|
|
36
|
+
npx agy-insights@latest --text
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
-
To pin a version: `npx agy-insights@0.
|
|
39
|
+
`@latest` makes npx check for the newest version every time. Without it, a copy installed globally (`npm install -g`) or in the current project takes precedence and is never updated. To pin a version instead: `npx agy-insights@0.7.0`.
|
|
40
40
|
|
|
41
41
|
**Globally:**
|
|
42
42
|
|
|
@@ -45,13 +45,15 @@ npm install -g agy-insights
|
|
|
45
45
|
agy-insights
|
|
46
46
|
```
|
|
47
47
|
|
|
48
|
+
To update it: `npm install -g agy-insights@latest`.
|
|
49
|
+
|
|
48
50
|
**From the repository:**
|
|
49
51
|
|
|
50
52
|
```sh
|
|
51
53
|
git clone https://github.com/migohe14/agy-insights.git
|
|
52
54
|
```
|
|
53
55
|
|
|
54
|
-
Then run it with `node agy-insights/agy-insights.js` instead of `npx agy-insights`. To update: `git pull` inside the folder.
|
|
56
|
+
Then run it with `node agy-insights/agy-insights.js` instead of `npx agy-insights@latest`. To update: `git pull` inside the folder.
|
|
55
57
|
|
|
56
58
|
## Usage
|
|
57
59
|
|
|
@@ -59,18 +61,19 @@ Then run it with `node agy-insights/agy-insights.js` instead of `npx agy-insight
|
|
|
59
61
|
npx agy-insights
|
|
60
62
|
```
|
|
61
63
|
|
|
62
|
-
Analyzes your whole history, saves an HTML report in the temp folder and opens it in the browser.
|
|
64
|
+
Analyzes your whole history (every conversation agy has kept, with no date limit), saves an HTML report in the temp folder and opens it in the browser. In the report you can switch between the whole history and the last 90, 30 or 7 days: totals, chart, findings and tables all follow the selected period.
|
|
63
65
|
|
|
64
66
|
| To… | Run |
|
|
65
67
|
|---|---|
|
|
66
|
-
| See only one project | `npx agy-insights --project C:\dev\my-project` |
|
|
67
|
-
| See the project in the current folder | `npx agy-insights --project .` |
|
|
68
|
-
| Get a summary in the terminal | `npx agy-insights --text` |
|
|
69
|
-
| Get the summary with paths and exclusions | `npx agy-insights --text --paths` |
|
|
70
|
-
| Get a project map to paste into `AGENTS.md` (folders, large files, generated folders; ~20 lines even for a large repo) | `npx agy-insights --map --project <folder>` |
|
|
71
|
-
|
|
|
72
|
-
|
|
|
73
|
-
|
|
|
68
|
+
| See only one project | `npx agy-insights@latest --project C:\dev\my-project` |
|
|
69
|
+
| See the project in the current folder | `npx agy-insights@latest --project .` |
|
|
70
|
+
| Get a summary in the terminal | `npx agy-insights@latest --text` |
|
|
71
|
+
| Get the summary with paths and exclusions | `npx agy-insights@latest --text --paths` |
|
|
72
|
+
| Get a project map to paste into `AGENTS.md` (folders, large files, generated folders; ~20 lines even for a large repo) | `npx agy-insights@latest --map --project <folder>` |
|
|
73
|
+
| Only a custom period (the report then has no period selector) | `npx agy-insights@latest --since 2026-10-01` or `--days 14` |
|
|
74
|
+
| Choose the language (English or Spanish) | `npx agy-insights@latest --lang es` |
|
|
75
|
+
| Save the report somewhere else | `npx agy-insights@latest --out report.html --no-open` |
|
|
76
|
+
| See all options | `npx agy-insights@latest --help` |
|
|
74
77
|
|
|
75
78
|
The report gives two figures:
|
|
76
79
|
|
|
@@ -79,7 +82,7 @@ The report gives two figures:
|
|
|
79
82
|
|
|
80
83
|
## Reading the report
|
|
81
84
|
|
|
82
|
-
|
|
85
|
+
At the top, the period selector and the totals. Then a chart of **token usage per day** (per week when the history spans more than ~4 months), to see how usage evolves; hover a column for its figures, or open "View as table". Next, **Models and effort**: calls and real tokens per model and effort level (for example Gemini 3.8 Flash · High), including tokens per call, so you can see what a higher effort costs. Below, **What's happening** lists the findings; at the end, the details by project, file type, folder, file, tool and command. The project of each file is detected through its `.git` folder.
|
|
83
86
|
|
|
84
87
|
| Finding | What it means | What to do |
|
|
85
88
|
|---|---|---|
|
package/agy-insights.js
CHANGED
|
@@ -30,6 +30,8 @@ function parseArgs(argv) {
|
|
|
30
30
|
else if (a === '--dir') opts.dir = path.resolve(value());
|
|
31
31
|
else if (a === '--config') opts.config = path.resolve(value());
|
|
32
32
|
else if (a === '--lang') opts.lang = value();
|
|
33
|
+
else if (a === '--since') opts.since = value();
|
|
34
|
+
else if (a === '--days') opts.days = value();
|
|
33
35
|
else fail(t.unknownOption(a));
|
|
34
36
|
}
|
|
35
37
|
return opts;
|
|
@@ -65,6 +67,24 @@ function openInBrowser(file) {
|
|
|
65
67
|
} catch { /* the path is printed anyway */ }
|
|
66
68
|
}
|
|
67
69
|
|
|
70
|
+
// --since YYYY-MM-DD (local midnight) or --days n (n days back from now), as a time in ms; null for all.
|
|
71
|
+
function sinceTime({ since, days }) {
|
|
72
|
+
if (since) {
|
|
73
|
+
const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(since);
|
|
74
|
+
const d = m && new Date(+m[1], m[2] - 1, +m[3]);
|
|
75
|
+
if (!d || d.getMonth() !== m[2] - 1) fail(t.badDate(since));
|
|
76
|
+
return d.getTime();
|
|
77
|
+
}
|
|
78
|
+
if (days !== undefined) {
|
|
79
|
+
if (!/^\d+$/.test(days)) fail(t.badDays(days));
|
|
80
|
+
return Date.now() - Number(days) * 24 * 60 * 60 * 1000;
|
|
81
|
+
}
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Periods the HTML report can switch to, besides the whole history.
|
|
86
|
+
const PRESET_DAYS = [90, 30, 7];
|
|
87
|
+
|
|
68
88
|
async function main() {
|
|
69
89
|
const argv = process.argv.slice(2);
|
|
70
90
|
// Language first, so that option errors are already in it.
|
|
@@ -76,28 +96,40 @@ async function main() {
|
|
|
76
96
|
if (!opts.lang && cfg.lang && !setLang(cfg.lang)) fail(t.badLang(cfg.lang));
|
|
77
97
|
|
|
78
98
|
if (opts.map) opts.project = opts.project || process.cwd();
|
|
79
|
-
const
|
|
99
|
+
const since = sinceTime(opts);
|
|
100
|
+
const run = (from) => analyze({
|
|
101
|
+
since: from,
|
|
80
102
|
dir: opts.dir || cfg.conversationsDir || DEFAULT_DIR,
|
|
81
103
|
workspaces: opts.project ? [opts.project] : [],
|
|
82
104
|
customKinds: cfg.customKinds || {},
|
|
83
105
|
});
|
|
106
|
+
const r = await run(since);
|
|
84
107
|
if (opts.map) {
|
|
85
108
|
const m = buildMap(opts.project, { ...cfg.map, history: r.files });
|
|
86
109
|
console.log(m.block);
|
|
87
110
|
return console.error(t.mapDone(m.tokens, path.join(opts.project, 'AGENTS.md')));
|
|
88
111
|
}
|
|
89
|
-
const
|
|
112
|
+
const find = (x) => findings(x, {
|
|
90
113
|
crossSessionMin: cfg.crossSessionMin ?? DEFAULTS.crossSessionMin,
|
|
91
114
|
heavyOutputTokens: cfg.heavyOutputTokens ?? DEFAULTS.heavyOutputTokens,
|
|
92
115
|
longSessionTokens: cfg.longSessionTokens ?? DEFAULTS.longSessionTokens,
|
|
93
116
|
});
|
|
117
|
+
const found = find(r);
|
|
94
118
|
|
|
95
119
|
if (opts.text) return process.stdout.write(report.text(r, found, { scope: opts.project, paths: opts.paths }));
|
|
96
120
|
|
|
97
121
|
const stamp = new Date().toISOString().slice(0, 16).replace(/[-:T]/g, '');
|
|
98
122
|
const out = opts.out || path.join(os.tmpdir(), 'agy-insights', `${t.reportName}-${stamp}.html`);
|
|
99
123
|
fs.mkdirSync(path.dirname(out), { recursive: true });
|
|
100
|
-
|
|
124
|
+
// Whole history by default, plus the presets the page can switch to. With --since/--days, just that period.
|
|
125
|
+
const views = [{ key: 'all', label: since ? '' : t.report.rangeAll, r, found }];
|
|
126
|
+
if (!since && r.conversations) {
|
|
127
|
+
for (const days of PRESET_DAYS) {
|
|
128
|
+
const x = await run(Date.now() - days * 24 * 60 * 60 * 1000);
|
|
129
|
+
views.push({ key: `d${days}`, label: t.report.rangeDays(days), r: x, found: find(x) });
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
fs.writeFileSync(out, report.htmlViews(views, { scope: opts.project }));
|
|
101
133
|
console.log(t.done(r.conversations, found.length, out));
|
|
102
134
|
if (opts.open) openInBrowser(out);
|
|
103
135
|
}
|
package/docs/how-it-works.md
CHANGED
|
@@ -34,9 +34,13 @@ It is undocumented; this is what has been observed in `agy` 1.4.x.
|
|
|
34
34
|
- One SQLite database per conversation: `~/.gemini/antigravity-cli/conversations/<uuid>.db`, in WAL mode.
|
|
35
35
|
- Table `steps`: one row per step. `step_payload` is a protobuf message. Tool calls store the name (`view_file`, `run_command`, `grep_search`…) followed by their JSON arguments, for example `{"AbsolutePath":"C:\\dev\\...\\X.cs","StartLine":301,"EndLine":380}`, preceded by a `call_NNN` id.
|
|
36
36
|
- Each call takes two steps with the same id: the model response that requests it (`step_type` 15) and the result (`step_type` 132), which repeats name and arguments and adds the file content or command output. `lib/history.js` counts the call once per id and adds the size of both steps, which approximates what entered the context.
|
|
37
|
-
- Table `gen_metadata`:
|
|
37
|
+
- Table `gen_metadata`: one row per model call, in the same order as the transcript's `PLANNER_RESPONSE` lines. Each row holds the model's display name with the effort level in brackets (`Gemini 3.8 Flash (High)`) and its id (`gemini-3.8-flash-high`). `modelOf()` takes the model and effort from whole strings only, so prose in the system prompt that mentions a model is ignored, and the n-th row is matched to the n-th call to get tokens per model and effort.
|
|
38
38
|
- Next to the history, `../brain/<id>/.system_generated/logs/transcript.jsonl`: one JSON line per step. `PLANNER_RESPONSE` lines are model calls and carry `input_tokens`, `cache_read_tokens` and `output_tokens`; failed steps have `status: "ERROR"`. Subagents have their own conversation and transcript. `realUsage()` in `lib/history.js` adds those figures up (it only reads the file). The total printed by `agy -p --output-format json` does not include subagents; this one does.
|
|
39
39
|
|
|
40
|
+
Each `transcript.jsonl` line also has `created_at` (ISO time). A conversation is dated by the first and last of those; without a transcript, by the `.db` file's last write. `--since` / `--days` keep a conversation when its last activity falls inside the period, and skip the rest before opening them. `../conversation_summaries.db` also has `last_modified_time` per conversation, but it is not used. Each model call's `created_at` also puts its tokens on a local day (`r.daily`), which feeds the usage chart.
|
|
41
|
+
|
|
42
|
+
The HTML report holds one view per period (whole history, last 90, 30 and 7 days): `agy-insights.js` runs `analyze()` and `findings()` once per period and `report.htmlViews()` renders them all, so switching in the browser needs no analysis code in the page and every number agrees with its period. The chart is inline SVG; its color (`--series`) is checked for contrast, chroma and lightness against both surfaces.
|
|
43
|
+
|
|
40
44
|
`lib/history.js` does not depend on a protobuf schema. It walks the message, collects every string, and treats as a tool call any JSON preceded (within 3 strings) by a `snake_case` name. That way it survives minor format changes. If there are more than 50 steps and no recognized call, it sets `formatUnrecognized` and the report warns about it.
|
|
41
45
|
|
|
42
46
|
### SQLite
|
package/lib/history.js
CHANGED
|
@@ -10,7 +10,23 @@ const DEFAULT_DIR = path.join(os.homedir(), '.gemini', 'antigravity-cli', 'conve
|
|
|
10
10
|
const BYTES_PER_TOKEN = 4;
|
|
11
11
|
const TOOL_RE = /^[a-z][a-z0-9_]{2,40}$/;
|
|
12
12
|
const FILE_KEYS = ['AbsolutePath', 'TargetFile', 'FilePath', 'File', 'Path'];
|
|
13
|
-
|
|
13
|
+
// Model of a call, as agy names it in gen_metadata: "Gemini 3.8 Flash (High)" (name and effort level), or
|
|
14
|
+
// the id "gemini-3.8-flash-high" when the display name is missing.
|
|
15
|
+
// Whole strings only, so that prose in the system prompt that mentions a model is not taken for one.
|
|
16
|
+
const MODEL_NAME_RE = /^((?:Gemini|Claude|GPT)[\w .-]*?) \(([A-Za-z][A-Za-z ]{1,15})\)$/;
|
|
17
|
+
const MODEL_ID_RE = /^((?:gemini|claude|gpt)-[\w.-]*?)-(minimal|low|medium|high|xhigh|max)$/;
|
|
18
|
+
|
|
19
|
+
function modelOf(strs) {
|
|
20
|
+
for (const s of strs) {
|
|
21
|
+
const m = s.length < 200 && MODEL_NAME_RE.exec(s);
|
|
22
|
+
if (m) return { model: m[1].trim(), effort: m[2].trim() };
|
|
23
|
+
}
|
|
24
|
+
for (const s of strs) {
|
|
25
|
+
const m = s.length < 200 && MODEL_ID_RE.exec(s);
|
|
26
|
+
if (m) return { model: m[1], effort: m[2] };
|
|
27
|
+
}
|
|
28
|
+
return { model: null, effort: null };
|
|
29
|
+
}
|
|
14
30
|
|
|
15
31
|
// --- SQLite access: node:sqlite when the runtime has it (Node >= 22.13), sql.js (WASM) otherwise. ---
|
|
16
32
|
|
|
@@ -154,6 +170,9 @@ function locate(file, workspaces, depth = 3) {
|
|
|
154
170
|
|
|
155
171
|
// --- Real usage ---
|
|
156
172
|
|
|
173
|
+
// Local calendar day of a time, as YYYY-MM-DD.
|
|
174
|
+
const dayKey = (ms) => { const d = new Date(ms); return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`; };
|
|
175
|
+
|
|
157
176
|
// What the model was actually billed for in a conversation, from the transcript agy keeps next to the
|
|
158
177
|
// history (<conversations>/../brain/<id>/.system_generated/logs/transcript.jsonl): one line per step,
|
|
159
178
|
// and model calls carry input, cache-read and output tokens. Subagents have their own conversation and
|
|
@@ -162,16 +181,28 @@ function realUsage(dir, id) {
|
|
|
162
181
|
const file = path.join(dir, '..', 'brain', id, '.system_generated', 'logs', 'transcript.jsonl');
|
|
163
182
|
let text;
|
|
164
183
|
try { text = fs.readFileSync(file, 'utf8'); } catch { return null; }
|
|
165
|
-
|
|
184
|
+
// `days`: the same figures per local day of each model call (calls without a time go under null).
|
|
185
|
+
// `perCall`: each call's figures, in order (they match gen_metadata's rows one to one).
|
|
186
|
+
const u = { calls: 0, input: 0, cache: 0, output: 0, errors: 0, start: null, end: null, days: new Map(), perCall: [] };
|
|
166
187
|
for (const line of text.split('\n')) {
|
|
167
188
|
let e;
|
|
168
189
|
try { e = JSON.parse(line); } catch { continue; }
|
|
190
|
+
const at = Date.parse(e.created_at);
|
|
191
|
+
if (at) { u.start = Math.min(u.start ?? at, at); u.end = Math.max(u.end ?? at, at); }
|
|
169
192
|
if (e.status === 'ERROR') u.errors++;
|
|
170
193
|
if (e.type !== 'PLANNER_RESPONSE') continue;
|
|
171
194
|
u.calls++;
|
|
172
195
|
u.input += Number(e.input_tokens) || 0;
|
|
173
196
|
u.cache += Number(e.cache_read_tokens) || 0;
|
|
174
197
|
u.output += Number(e.output_tokens) || 0;
|
|
198
|
+
u.perCall.push({ input: Number(e.input_tokens) || 0, cache: Number(e.cache_read_tokens) || 0, output: Number(e.output_tokens) || 0 });
|
|
199
|
+
const k = at ? dayKey(at) : null;
|
|
200
|
+
const d = u.days.get(k) || { calls: 0, input: 0, cache: 0, output: 0 };
|
|
201
|
+
d.calls++;
|
|
202
|
+
d.input += Number(e.input_tokens) || 0;
|
|
203
|
+
d.cache += Number(e.cache_read_tokens) || 0;
|
|
204
|
+
d.output += Number(e.output_tokens) || 0;
|
|
205
|
+
u.days.set(k, d);
|
|
175
206
|
}
|
|
176
207
|
return u;
|
|
177
208
|
}
|
|
@@ -194,23 +225,34 @@ class Counter {
|
|
|
194
225
|
* @param {string} [opts.dir] conversations directory
|
|
195
226
|
* @param {string[]} [opts.workspaces] only count conversations that touched these folders (all if empty)
|
|
196
227
|
* @param {Object<string,string>} [opts.customKinds] regex -> label, checked before the built-in types
|
|
228
|
+
* @param {number} [opts.since] only conversations whose last activity is at or after this time (ms)
|
|
197
229
|
*/
|
|
198
|
-
async function analyze({ dir = DEFAULT_DIR, workspaces = [], customKinds = {} } = {}) {
|
|
230
|
+
async function analyze({ dir = DEFAULT_DIR, workspaces = [], customKinds = {}, since = null } = {}) {
|
|
199
231
|
const custom = Object.entries(customKinds).map(([re, label]) => [new RegExp(re, 'i'), label]);
|
|
200
232
|
const classify = (p) => custom.find(([re]) => re.test(p))?.[1] || kind(p);
|
|
201
233
|
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'agy-insights-'));
|
|
202
234
|
const r = {
|
|
203
235
|
dir, backend: nodeSqlite ? 'node:sqlite' : 'sql.js', conversations: 0, unreadable: 0, steps: 0, tokens: 0,
|
|
236
|
+
since, period: null, olderSkipped: 0,
|
|
204
237
|
toolCallsFound: 0, reads: 0, rangedReads: 0, linesRead: 0, rereads: 0, rereadTokens: 0, sessions: [],
|
|
205
238
|
usage: { conversations: 0, calls: 0, input: 0, cache: 0, output: 0, errors: 0 },
|
|
206
239
|
};
|
|
207
240
|
const tools = new Counter(), kinds = new Counter(), folders = new Counter(), files = new Counter();
|
|
208
|
-
const commands = new Counter(),
|
|
241
|
+
const commands = new Counter(), projects = new Counter();
|
|
242
|
+
const models = new Map(); // "model|effort" -> calls and real tokens
|
|
243
|
+
const daily = new Map(); // YYYY-MM-DD -> real usage that day
|
|
244
|
+
const firstDay = since ? dayKey(since) : '';
|
|
209
245
|
const wsKeys = workspaces.map(key);
|
|
210
246
|
|
|
211
247
|
try {
|
|
212
248
|
const dbs = fs.existsSync(dir) ? fs.readdirSync(dir).filter((f) => /^[0-9a-f-]{36}\.db$/i.test(f)) : [];
|
|
213
249
|
for (const f of dbs) {
|
|
250
|
+
// When it happened: the transcript's step times, else the database's last write.
|
|
251
|
+
const id = f.replace(/\.db$/, '');
|
|
252
|
+
const usage = realUsage(dir, id);
|
|
253
|
+
const end = usage?.end ?? fs.statSync(path.join(dir, f)).mtimeMs;
|
|
254
|
+
const start = usage?.start ?? end;
|
|
255
|
+
if (since && end < since) { r.olderSkipped++; continue; }
|
|
214
256
|
let db;
|
|
215
257
|
try { db = await openDb(path.join(dir, f), tmp); } catch { r.unreadable++; continue; }
|
|
216
258
|
try {
|
|
@@ -223,6 +265,7 @@ async function analyze({ dir = DEFAULT_DIR, workspaces = [], customKinds = {} }
|
|
|
223
265
|
}
|
|
224
266
|
|
|
225
267
|
r.conversations++;
|
|
268
|
+
r.period = { from: Math.min(r.period?.from ?? start, start), to: Math.max(r.period?.to ?? end, end) };
|
|
226
269
|
let convBytes = 0;
|
|
227
270
|
const seenRanges = new Set(), seenFiles = new Set(), seenFolders = new Set(), seenKinds = new Set(), seenProjects = new Set();
|
|
228
271
|
const seenCalls = new Map(); // call id -> 1 if that call re-read a range already read, else 0
|
|
@@ -268,17 +311,38 @@ async function analyze({ dir = DEFAULT_DIR, workspaces = [], customKinds = {} }
|
|
|
268
311
|
files.add(key(file), { ...fields, convs: once(seenFiles, key(file)) }, { abs: file, rel, root, project, kind: k });
|
|
269
312
|
});
|
|
270
313
|
}
|
|
271
|
-
const id = f.replace(/\.db$/, '');
|
|
272
|
-
const usage = realUsage(dir, id);
|
|
273
314
|
if (usage) {
|
|
274
315
|
r.usage.conversations++;
|
|
275
316
|
for (const k of ['calls', 'input', 'cache', 'output', 'errors']) r.usage[k] += usage[k];
|
|
317
|
+
for (const [k, v] of usage.days) {
|
|
318
|
+
const day = k || dayKey(end);
|
|
319
|
+
if (day < firstDay) continue;
|
|
320
|
+
const d = daily.get(day) || { day, calls: 0, input: 0, cache: 0, output: 0 };
|
|
321
|
+
for (const f of ['calls', 'input', 'cache', 'output']) d[f] += v[f];
|
|
322
|
+
daily.set(day, d);
|
|
323
|
+
}
|
|
276
324
|
}
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
325
|
+
// One gen_metadata row per model call, in the transcript's order: the n-th row's model made the n-th call.
|
|
326
|
+
const callModels = db.all('select data from gen_metadata order by idx').map(([blob]) => modelOf(collect(blob)));
|
|
327
|
+
const n = Math.max(callModels.length, usage ? usage.perCall.length : 0);
|
|
328
|
+
const seenModels = new Set();
|
|
329
|
+
for (let i = 0; i < n; i++) {
|
|
330
|
+
const { model, effort } = callModels[i] || { model: null, effort: null };
|
|
331
|
+
const k = `${model}|${effort}`;
|
|
332
|
+
const e = models.get(k) || { model, effort, calls: 0, convs: 0, input: 0, cache: 0, output: 0, known: 0 };
|
|
333
|
+
e.calls++;
|
|
334
|
+
if (!seenModels.has(k)) { seenModels.add(k); e.convs++; }
|
|
335
|
+
const c = usage?.perCall[i];
|
|
336
|
+
if (c) { e.known++; e.input += c.input; e.cache += c.cache; e.output += c.output; }
|
|
337
|
+
models.set(k, e);
|
|
281
338
|
}
|
|
339
|
+
// The session's model: the one that made most of its calls.
|
|
340
|
+
const counts = new Map();
|
|
341
|
+
for (const m of callModels) counts.set(`${m.model}|${m.effort}`, (counts.get(`${m.model}|${m.effort}`) || 0) + 1);
|
|
342
|
+
const main = [...counts].sort((a, b) => b[1] - a[1])[0];
|
|
343
|
+
const model = main ? callModels.find((m) => `${m.model}|${m.effort}` === main[0]) : null;
|
|
344
|
+
r.sessions.push({ id, start, end, steps: steps.length, tokens: convBytes / BYTES_PER_TOKEN, usage, model: model?.model ? model : null, models: counts.size });
|
|
345
|
+
r.tokens += convBytes / BYTES_PER_TOKEN;
|
|
282
346
|
} catch { r.unreadable++; } finally { db.close(); }
|
|
283
347
|
}
|
|
284
348
|
} finally {
|
|
@@ -293,8 +357,10 @@ async function analyze({ dir = DEFAULT_DIR, workspaces = [], customKinds = {} }
|
|
|
293
357
|
return {
|
|
294
358
|
...r,
|
|
295
359
|
tools: tools.all(), kinds: kinds.all(), folders: folders.all(), files: files.all(), projects: projects.all(),
|
|
296
|
-
commands: commands.all(),
|
|
360
|
+
commands: commands.all(),
|
|
361
|
+
models: [...models.values()].sort((a, b) => b.input + b.output - (a.input + a.output) || b.calls - a.calls),
|
|
362
|
+
daily: [...daily.values()].sort((a, b) => (a.day < b.day ? -1 : 1)),
|
|
297
363
|
};
|
|
298
364
|
}
|
|
299
365
|
|
|
300
|
-
module.exports = { analyze, DEFAULT_DIR, kind, NON_SOURCE, realUsage };
|
|
366
|
+
module.exports = { analyze, DEFAULT_DIR, kind, NON_SOURCE, realUsage, dayKey, modelOf };
|
package/lib/i18n.js
CHANGED
|
@@ -17,6 +17,8 @@ Options:
|
|
|
17
17
|
--map Print a project map (--project, or the current folder) for AGENTS.md
|
|
18
18
|
--out <file> Where to save the HTML report (default: the temp folder)
|
|
19
19
|
--no-open Don't open the browser
|
|
20
|
+
--since <YYYY-MM-DD> Only conversations active since that date
|
|
21
|
+
--days <n> Only conversations active in the last n days
|
|
20
22
|
--lang <en|es> Language of the output (default: the system's)
|
|
21
23
|
--dir <folder> Another history folder (default ${dir})
|
|
22
24
|
--config <file> Configuration (default ~/.agy-insights.json if it exists)
|
|
@@ -25,11 +27,13 @@ Options:
|
|
|
25
27
|
missingValue: (opt) => `Missing value for ${opt}`,
|
|
26
28
|
unknownOption: (opt) => `Unknown option: ${opt}. See --help.`,
|
|
27
29
|
badLang: (v) => `Unsupported language: ${v}. Use en or es.`,
|
|
30
|
+
badDate: (v) => `Invalid date: ${v}. Use YYYY-MM-DD.`,
|
|
31
|
+
badDays: (v) => `Invalid number of days: ${v}.`,
|
|
28
32
|
configNotFound: (f) => `Configuration file not found: ${f}`,
|
|
29
33
|
cannotRead: (f, msg) => `Cannot read ${f}: ${msg}`,
|
|
30
34
|
mapDone: (tokens, file) => `\n~${tokens} tokens on every request. Paste it into ${file} if you like it; agy-insights does not write it.`,
|
|
31
35
|
reportName: 'report',
|
|
32
|
-
done: (convs, found, out) => `${convs
|
|
36
|
+
done: (convs, found, out) => `${plural(convs, 'conversation', 'conversations')} analyzed, ${plural(found, 'finding', 'findings')}.\nReport: ${out}`,
|
|
33
37
|
|
|
34
38
|
kinds: {
|
|
35
39
|
build: 'build output (dist, bin, obj…)', logs: 'logs, maps, minified', generated: 'generated code / migrations',
|
|
@@ -78,7 +82,7 @@ Options:
|
|
|
78
82
|
longSuggestion: 'A new conversation per task, or /compact when you change topic.',
|
|
79
83
|
conversation: (id) => `conversation ${id}`,
|
|
80
84
|
steps: (n) => plural(n, 'step', 'steps'),
|
|
81
|
-
longUsage: (calls, real, cached) => ` · ${calls
|
|
85
|
+
longUsage: (calls, real, cached) => ` · ${plural(calls, 'model call', 'model calls')}, ${real} real tokens (${cached} with cache)`,
|
|
82
86
|
},
|
|
83
87
|
|
|
84
88
|
map: {
|
|
@@ -99,11 +103,20 @@ Options:
|
|
|
99
103
|
exampleRule: 'Example rule for AGENTS.md',
|
|
100
104
|
exampleHint: "Add it to the project's AGENTS.md only if this happens often: rules are sent with every request.",
|
|
101
105
|
cols: ['What', '~tokens', '%', 'Times', 'Sessions'],
|
|
106
|
+
modelsTitle: 'Models and effort',
|
|
107
|
+
modelsHint: 'Model and effort level (reasoning) of each model call, as agy records them. Higher effort usually means more tokens per call.',
|
|
108
|
+
modelCols: ['Model', 'Effort', 'Calls', 'Tokens', '%', 'Per call', 'With cache', 'Sessions'],
|
|
109
|
+
unknownModel: 'unknown',
|
|
110
|
+
modelsText: 'Models and effort:',
|
|
111
|
+
modelLine: (model, effort, calls, tokens, pct, perCall) => `${model} · ${effort} ${plural(calls, 'call', 'calls')}, ${tokens} tokens (${pct}), ~${perCall} per call`,
|
|
112
|
+
sessionModel: 'Model',
|
|
113
|
+
otherModels: (n) => ` +${n} more`,
|
|
114
|
+
efforts: { minimal: 'Minimal', low: 'Low', medium: 'Medium', high: 'High', xhigh: 'Extra high', 'extra high': 'Extra high', max: 'Max' },
|
|
102
115
|
sessionsTitle: 'Sessions (real usage)',
|
|
103
|
-
sessionCols: ['Conversation', 'Tokens', '%', 'With cache', 'Calls', 'Errors'],
|
|
116
|
+
sessionCols: ['Conversation', 'Model', 'Tokens', '%', 'With cache', 'Calls', 'Errors'],
|
|
104
117
|
noConversations: (scope, dir) => `No conversations${scope ? ` for <code>${scope}</code>` : ''} in <code>${dir}</code>.`,
|
|
105
118
|
tileReal: 'real tokens (input + output)',
|
|
106
|
-
tileCache: (calls) => `including cache · ${calls
|
|
119
|
+
tileCache: (calls) => `including cache · ${plural(calls, 'call', 'calls')}`,
|
|
107
120
|
tileContext: '~tokens in context',
|
|
108
121
|
tileTop5: 'in the 5 largest sessions',
|
|
109
122
|
tileReads: 'file reads',
|
|
@@ -116,18 +129,35 @@ Options:
|
|
|
116
129
|
byKind: 'By file type', byFolder: 'By folder', files: 'Files', tools: 'Tools', commands: 'Commands',
|
|
117
130
|
title: 'Context usage in Antigravity',
|
|
118
131
|
project: 'Project', wholeHistory: 'Whole history',
|
|
119
|
-
|
|
120
|
-
|
|
132
|
+
convs: (n) => plural(n, 'conversation', 'conversations'),
|
|
133
|
+
generated: (date) => `generated ${date}`,
|
|
134
|
+
filterLabel: 'Period',
|
|
135
|
+
rangeAll: 'All history',
|
|
136
|
+
rangeDays: (n) => `Last ${n} days`,
|
|
137
|
+
chartTitleDay: 'Token usage per day',
|
|
138
|
+
chartTitleWeek: 'Token usage per week',
|
|
139
|
+
chartHint: 'Real tokens (input + output) of the model calls. Hover a column for details.',
|
|
140
|
+
chartTable: 'View as table',
|
|
141
|
+
chartCols: ['Day', 'Tokens', 'With cache', 'Calls'],
|
|
142
|
+
chartColsWeek: ['Week of', 'Tokens', 'With cache', 'Calls'],
|
|
143
|
+
chartCalls: (n) => plural(n, 'call', 'calls'),
|
|
144
|
+
weekOf: (d) => `week of ${d}`,
|
|
145
|
+
withCacheShort: 'with cache',
|
|
146
|
+
period: (from, to) => (from === to ? `on ${from}` : `from ${from} to ${to}`),
|
|
147
|
+
since: (date) => `active since ${date}`,
|
|
148
|
+
olderSkipped: (n) => `${plural(n, 'older conversation', 'older conversations')} left out by the date filter`,
|
|
149
|
+
periodLine: (period, since) => `Period: ${period}${since ? ` (filter: ${since})` : ''}`,
|
|
150
|
+
footerReal: (missing) => `Real tokens: the ones Antigravity records in each conversation's log (subagents included)${missing ? `; ${plural(missing, 'conversation has', 'conversations have')} no log and ${missing === 1 ? 'is' : 'are'} not counted` : ''}.`,
|
|
121
151
|
footerCache: 'It is unknown whether the quota counts cache the same as new input, so both figures are shown.',
|
|
122
152
|
footerEstimate: '~tokens = stored size of each step / 4: useful to compare files and folders with each other.',
|
|
123
153
|
footerPrivacy: 'The history was read from a copy and was not modified. This page contains paths from your projects: review it before sharing.',
|
|
124
154
|
// Text summary
|
|
125
155
|
history: 'History', projectLower: 'project',
|
|
126
|
-
summary: (convs, steps, tokens) => `${convs
|
|
127
|
-
realUsage: (real, input, output, cached, calls) => `Real usage: ${real} tokens (input ${input} + output ${output}), ${cached} including cache, in ${calls
|
|
128
|
-
noLog: (n) => ` (${n
|
|
129
|
-
topSessions: (n) => ` The ${n} sessions that use the most
|
|
130
|
-
sessionLine: (id, calls, errors, cached) => `conversation ${id} ${calls
|
|
156
|
+
summary: (convs, steps, tokens) => `${plural(convs, 'conversation', 'conversations')}, ${plural(steps, 'step', 'steps')}, ~${tokens} tokens in context (estimated)`,
|
|
157
|
+
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
|
+
noLog: (n) => ` (${plural(n, 'conversation', 'conversations')} without a log)`,
|
|
159
|
+
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`,
|
|
131
161
|
top5: (pct, reads, rereads, rpct) => `The 5 largest sessions account for ${pct}. Reads: ${reads}, re-reads ${rereads} (${rpct})`,
|
|
132
162
|
pathHidden: '(path hidden, use --paths)',
|
|
133
163
|
byKindText: 'By file type:',
|
|
@@ -148,6 +178,8 @@ Opciones:
|
|
|
148
178
|
--map Imprime un mapa del proyecto (--project, o la carpeta actual) para AGENTS.md
|
|
149
179
|
--out <fichero> Dónde guardar el informe HTML (por defecto, en la carpeta temporal)
|
|
150
180
|
--no-open No abrir el navegador
|
|
181
|
+
--since <AAAA-MM-DD> Solo conversaciones con actividad desde esa fecha
|
|
182
|
+
--days <n> Solo conversaciones con actividad en los últimos n días
|
|
151
183
|
--lang <en|es> Idioma de la salida (por defecto, el del sistema)
|
|
152
184
|
--dir <carpeta> Otra carpeta de historial (por defecto ${dir})
|
|
153
185
|
--config <fichero> Configuración (por defecto ~/.agy-insights.json si existe)
|
|
@@ -156,11 +188,13 @@ Opciones:
|
|
|
156
188
|
missingValue: (opt) => `Falta el valor de ${opt}`,
|
|
157
189
|
unknownOption: (opt) => `Opción desconocida: ${opt}. Usa --help.`,
|
|
158
190
|
badLang: (v) => `Idioma no soportado: ${v}. Usa en o es.`,
|
|
191
|
+
badDate: (v) => `Fecha no válida: ${v}. Usa AAAA-MM-DD.`,
|
|
192
|
+
badDays: (v) => `Número de días no válido: ${v}.`,
|
|
159
193
|
configNotFound: (f) => `No existe el fichero de configuración ${f}`,
|
|
160
194
|
cannotRead: (f, msg) => `No se puede leer ${f}: ${msg}`,
|
|
161
195
|
mapDone: (tokens, file) => `\n~${tokens} tokens en cada petición. Pégalo en ${file} si te convence; agy-insights no lo escribe.`,
|
|
162
196
|
reportName: 'informe',
|
|
163
|
-
done: (convs, found, out) => `${convs
|
|
197
|
+
done: (convs, found, out) => `${plural(convs, 'conversación analizada', 'conversaciones analizadas')}, ${plural(found, 'hallazgo', 'hallazgos')}.\nInforme: ${out}`,
|
|
164
198
|
|
|
165
199
|
kinds: {
|
|
166
200
|
build: 'build output (dist, bin, obj…)', logs: 'logs, maps, minificados', generated: 'código generado / migraciones',
|
|
@@ -209,7 +243,7 @@ Opciones:
|
|
|
209
243
|
longSuggestion: 'Una conversación nueva por tarea, o /compact cuando cambias de tema.',
|
|
210
244
|
conversation: (id) => `conversación ${id}`,
|
|
211
245
|
steps: (n) => plural(n, 'paso', 'pasos'),
|
|
212
|
-
longUsage: (calls, real, cached) => ` · ${calls
|
|
246
|
+
longUsage: (calls, real, cached) => ` · ${plural(calls, 'llamada', 'llamadas')} al modelo, ${real} tokens reales (${cached} con caché)`,
|
|
213
247
|
},
|
|
214
248
|
|
|
215
249
|
map: {
|
|
@@ -230,11 +264,20 @@ Opciones:
|
|
|
230
264
|
exampleRule: 'Regla de ejemplo para AGENTS.md',
|
|
231
265
|
exampleHint: 'Ponla en el AGENTS.md del proyecto solo si te pasa a menudo: las reglas se envían en cada petición.',
|
|
232
266
|
cols: ['Qué', '~tokens', '%', 'Veces', 'Sesiones'],
|
|
267
|
+
modelsTitle: 'Modelos y esfuerzo',
|
|
268
|
+
modelsHint: 'Modelo y nivel de esfuerzo (razonamiento) de cada llamada al modelo, tal como los anota agy. Más esfuerzo suele suponer más tokens por llamada.',
|
|
269
|
+
modelCols: ['Modelo', 'Esfuerzo', 'Llamadas', 'Tokens', '%', 'Por llamada', 'Con caché', 'Sesiones'],
|
|
270
|
+
unknownModel: 'desconocido',
|
|
271
|
+
modelsText: 'Modelos y esfuerzo:',
|
|
272
|
+
modelLine: (model, effort, calls, tokens, pct, perCall) => `${model} · ${effort} ${plural(calls, 'llamada', 'llamadas')}, ${tokens} tokens (${pct}), ~${perCall} por llamada`,
|
|
273
|
+
sessionModel: 'Modelo',
|
|
274
|
+
otherModels: (n) => ` +${n} más`,
|
|
275
|
+
efforts: { minimal: 'Mínimo', low: 'Bajo', medium: 'Medio', high: 'Alto', xhigh: 'Muy alto', 'extra high': 'Muy alto', max: 'Máximo' },
|
|
233
276
|
sessionsTitle: 'Sesiones (consumo real)',
|
|
234
|
-
sessionCols: ['Conversación', 'Tokens', '%', 'Con caché', 'Llamadas', 'Errores'],
|
|
277
|
+
sessionCols: ['Conversación', 'Modelo', 'Tokens', '%', 'Con caché', 'Llamadas', 'Errores'],
|
|
235
278
|
noConversations: (scope, dir) => `No hay conversaciones${scope ? ` de <code>${scope}</code>` : ''} en <code>${dir}</code>.`,
|
|
236
279
|
tileReal: 'tokens reales (entrada + salida)',
|
|
237
|
-
tileCache: (calls) => `contando la caché · ${calls
|
|
280
|
+
tileCache: (calls) => `contando la caché · ${plural(calls, 'llamada', 'llamadas')}`,
|
|
238
281
|
tileContext: '~tokens en contexto',
|
|
239
282
|
tileTop5: 'en las 5 sesiones más grandes',
|
|
240
283
|
tileReads: 'lecturas de ficheros',
|
|
@@ -247,17 +290,34 @@ Opciones:
|
|
|
247
290
|
byKind: 'Por tipo de fichero', byFolder: 'Por carpeta', files: 'Ficheros', tools: 'Herramientas', commands: 'Comandos',
|
|
248
291
|
title: 'Uso de contexto en Antigravity',
|
|
249
292
|
project: 'Proyecto', wholeHistory: 'Todo el historial',
|
|
250
|
-
|
|
251
|
-
|
|
293
|
+
convs: (n) => plural(n, 'conversación', 'conversaciones'),
|
|
294
|
+
generated: (date) => `generado ${date}`,
|
|
295
|
+
filterLabel: 'Periodo',
|
|
296
|
+
rangeAll: 'Todo el historial',
|
|
297
|
+
rangeDays: (n) => `Últimos ${n} días`,
|
|
298
|
+
chartTitleDay: 'Consumo de tokens por día',
|
|
299
|
+
chartTitleWeek: 'Consumo de tokens por semana',
|
|
300
|
+
chartHint: 'Tokens reales (entrada + salida) de las llamadas al modelo. Pasa el ratón por una columna para ver el detalle.',
|
|
301
|
+
chartTable: 'Ver como tabla',
|
|
302
|
+
chartCols: ['Día', 'Tokens', 'Con caché', 'Llamadas'],
|
|
303
|
+
chartColsWeek: ['Semana del', 'Tokens', 'Con caché', 'Llamadas'],
|
|
304
|
+
chartCalls: (n) => plural(n, 'llamada', 'llamadas'),
|
|
305
|
+
weekOf: (d) => `semana del ${d}`,
|
|
306
|
+
withCacheShort: 'con caché',
|
|
307
|
+
period: (from, to) => (from === to ? `el ${from}` : `del ${from} al ${to}`),
|
|
308
|
+
since: (date) => `con actividad desde el ${date}`,
|
|
309
|
+
olderSkipped: (n) => `${plural(n, 'conversación anterior queda fuera', 'conversaciones anteriores quedan fuera')} por el filtro de fechas`,
|
|
310
|
+
periodLine: (period, since) => `Periodo: ${period}${since ? ` (filtro: ${since})` : ''}`,
|
|
311
|
+
footerReal: (missing) => `Tokens reales: los que Antigravity anota en el registro de cada conversación (incluidos subagentes)${missing ? `; ${missing === 1 ? '1 conversación no tiene registro y no cuenta' : `${missing} conversaciones no tienen registro y no cuentan`}` : ''}.`,
|
|
252
312
|
footerCache: 'No se sabe si la cuota cuenta la caché igual que la entrada nueva: por eso se muestran las dos cifras.',
|
|
253
313
|
footerEstimate: '~tokens = tamaño guardado de cada paso / 4: sirve para comparar ficheros y carpetas entre sí.',
|
|
254
314
|
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
315
|
history: 'Historial', projectLower: 'proyecto',
|
|
256
|
-
summary: (convs, steps, tokens) => `${convs
|
|
257
|
-
realUsage: (real, input, output, cached, calls) => `Consumo real: ${real} tokens (entrada ${input} + salida ${output}), ${cached} contando la caché, en ${calls
|
|
258
|
-
noLog: (n) => ` (${n
|
|
259
|
-
topSessions: (n) => ` Las ${n} sesiones que más gastan
|
|
260
|
-
sessionLine: (id, calls, errors, cached) => `conversación ${id} ${calls
|
|
316
|
+
summary: (convs, steps, tokens) => `${plural(convs, 'conversación', 'conversaciones')}, ${plural(steps, 'paso', 'pasos')}, ~${tokens} tokens en contexto (estimado)`,
|
|
317
|
+
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
|
+
noLog: (n) => ` (${plural(n, 'conversación', 'conversaciones')} sin registro)`,
|
|
319
|
+
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é`,
|
|
261
321
|
top5: (pct, reads, rereads, rpct) => `Las 5 sesiones más grandes suman el ${pct}. Lecturas: ${reads}, relecturas ${rereads} (${rpct})`,
|
|
262
322
|
pathHidden: '(ruta oculta, usa --paths)',
|
|
263
323
|
byKindText: 'Por tipo de fichero:',
|
|
@@ -283,6 +343,7 @@ function setLang(lang) {
|
|
|
283
343
|
const t = new Proxy({}, { get: (_, k) => LANGS[current][k] });
|
|
284
344
|
const lang = () => current;
|
|
285
345
|
const num = (n) => Math.round(n).toLocaleString(t.locale);
|
|
346
|
+
const date = (ms) => new Date(ms).toLocaleDateString(t.locale, { day: 'numeric', month: 'short', year: 'numeric' });
|
|
286
347
|
|
|
287
348
|
// Built-in kinds are stable ids (see history.js kind()); custom kinds from the config are shown as written.
|
|
288
349
|
function kindLabel(k) {
|
|
@@ -290,4 +351,7 @@ function kindLabel(k) {
|
|
|
290
351
|
return t.kinds[k] || k;
|
|
291
352
|
}
|
|
292
353
|
|
|
293
|
-
|
|
354
|
+
// "High" / "high" → "High" or "Alto"; levels agy may add later are shown as written.
|
|
355
|
+
const effortLabel = (e) => (e ? t.report.efforts[e.toLowerCase()] || e : t.report.unknownModel);
|
|
356
|
+
|
|
357
|
+
module.exports = { t, setLang, lang, detect, num, date, kindLabel, effortLabel, LANGS };
|
package/lib/report.js
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
// Renders the analysis as a self-contained HTML page (opened in the browser) or as plain text.
|
|
2
|
-
// The page is static: its only
|
|
2
|
+
// The page is static: its only interactions are switching the period, the chart tooltip and copying text,
|
|
3
|
+
// so it cannot change anything on disk.
|
|
3
4
|
|
|
4
|
-
const { t, lang, num, kindLabel } = require('./i18n');
|
|
5
|
+
const { t, lang, num, date, kindLabel, effortLabel } = require('./i18n');
|
|
6
|
+
const { dayKey } = require('./history');
|
|
5
7
|
|
|
6
8
|
const esc = (s) => String(s ?? '').replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[c]);
|
|
7
9
|
const fmt = num;
|
|
8
10
|
const pct = (a, b) => (b ? `${Math.round((100 * a) / b)}%` : '–');
|
|
11
|
+
// "Period: from 3 Oct 2026 to 29 Oct 2026 (filter: active since 1 Oct 2026)", or '' with no conversations.
|
|
12
|
+
const periodLine = (r) => (r.period ? t.report.periodLine(t.report.period(date(r.period.from), date(r.period.to)), r.since && t.report.since(date(r.since))) : '');
|
|
9
13
|
const label = (x) => (x.project ? `${x.project} › ${x.rel}` : x.abs || x.key);
|
|
10
14
|
|
|
11
15
|
// Real usage, from agy's transcripts: what counts against the quota, unlike the size estimate.
|
|
@@ -55,55 +59,176 @@ function sessionsTable(r) {
|
|
|
55
59
|
const rows = r.sessions.filter((s) => s.usage).slice(0, 15);
|
|
56
60
|
if (!rows.length) return '';
|
|
57
61
|
const max = Math.max(...rows.map((s) => billed(s.usage)), 1);
|
|
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>
|
|
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>
|
|
62
|
+
return `<h3>${t.report.sessionsTitle}</h3><div class="scroll"><table><thead><tr>${t.report.sessionCols.map((c, i) => `<th${i > 1 ? ' 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>${esc(sessionModel(s))}</td>
|
|
60
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>
|
|
61
65
|
<td class="num">${s.usage.calls}</td><td class="num">${s.usage.errors || ''}</td></tr>`).join('')}
|
|
62
66
|
</tbody></table></div>`;
|
|
63
67
|
}
|
|
64
68
|
|
|
65
|
-
|
|
69
|
+
// "Gemini 3.8 Flash · High", or "Gemini 3.8 Flash · High +1 more" when the session used several.
|
|
70
|
+
const modelName = (m) => (m.model || t.report.unknownModel);
|
|
71
|
+
const sessionModel = (s) => (s.model ? `${modelName(s.model)} · ${effortLabel(s.model.effort)}${s.models > 1 ? t.report.otherModels(s.models - 1) : ''}` : '');
|
|
72
|
+
|
|
73
|
+
// Calls and real tokens per model and effort level.
|
|
74
|
+
function modelsTable(r) {
|
|
75
|
+
const rows = r.models.filter((m) => m.model || m.calls);
|
|
76
|
+
if (!rows.length || (rows.length === 1 && !rows[0].model)) return '';
|
|
77
|
+
const total = rows.reduce((s, m) => s + m.input + m.output, 0);
|
|
78
|
+
const max = Math.max(...rows.map((m) => m.input + m.output), 1);
|
|
79
|
+
return `<section class="models"><h2>${t.report.modelsTitle}</h2><p class="hint">${t.report.modelsHint}</p>
|
|
80
|
+
<div class="scroll"><table><thead><tr>${t.report.modelCols.map((c, i) => `<th${i > 1 ? ' class="num"' : ''}>${c}</th>`).join('')}</tr></thead><tbody>
|
|
81
|
+
${rows.map((m) => { const tok = m.input + m.output; return `<tr><td><div class="bar" style="--w:${((100 * tok) / max).toFixed(1)}%"></div><span>${esc(modelName(m))}</span></td>
|
|
82
|
+
<td>${esc(effortLabel(m.effort))}</td><td class="num">${fmt(m.calls)}</td><td class="num">${m.known ? fmt(tok) : '–'}</td><td class="num">${m.known ? pct(tok, total) : '–'}</td>
|
|
83
|
+
<td class="num">${m.known ? fmt(tok / m.known) : '–'}</td><td class="num">${m.known ? fmt(tok + m.cache) : '–'}</td><td class="num">${m.convs}</td></tr>`; }).join('')}
|
|
84
|
+
</tbody></table></div></section>`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// --- Usage chart ---
|
|
88
|
+
|
|
89
|
+
const DAY = 24 * 60 * 60 * 1000;
|
|
90
|
+
const parseDay = (k) => { const [y, m, d] = k.split('-').map(Number); return new Date(y, m - 1, d); };
|
|
91
|
+
const shortDate = (d, year) => d.toLocaleDateString(t.locale, { day: 'numeric', month: 'short', ...(year ? { year: 'numeric' } : {}) });
|
|
92
|
+
const compact = (n) => new Intl.NumberFormat(t.locale, { notation: 'compact', maximumFractionDigits: 1 }).format(n);
|
|
93
|
+
// 1, 2, 2.5, 5 × 10^k: a round top for the y axis.
|
|
94
|
+
const niceMax = (v) => { const p = 10 ** Math.floor(Math.log10(v || 1)); return [1, 2, 2.5, 5, 10].map((m) => m * p).find((x) => x >= v); };
|
|
95
|
+
|
|
96
|
+
// Real usage per day (per week past ~4 months), continuous: days without calls are zero-height columns.
|
|
97
|
+
function buckets(r) {
|
|
98
|
+
if (!r.daily.length) return null;
|
|
99
|
+
const first = r.since ? parseDay(dayKey(r.since)) : parseDay(r.daily[0].day);
|
|
100
|
+
const last = r.since ? parseDay(dayKey(Date.now())) : parseDay(r.daily[r.daily.length - 1].day);
|
|
101
|
+
const weekly = (last - first) / DAY > 120;
|
|
102
|
+
const startOf = (d) => (weekly ? new Date(d.getFullYear(), d.getMonth(), d.getDate() - ((d.getDay() + 6) % 7)) : d);
|
|
103
|
+
const byKey = new Map();
|
|
104
|
+
for (let d = startOf(first); d <= last; d = new Date(d.getFullYear(), d.getMonth(), d.getDate() + (weekly ? 7 : 1))) {
|
|
105
|
+
byKey.set(dayKey(d.getTime()), { date: d, billed: 0, cache: 0, calls: 0 });
|
|
106
|
+
}
|
|
107
|
+
for (const x of r.daily) {
|
|
108
|
+
const b = byKey.get(dayKey(startOf(parseDay(x.day)).getTime()));
|
|
109
|
+
if (!b) continue;
|
|
110
|
+
b.billed += x.input + x.output;
|
|
111
|
+
b.cache += x.cache;
|
|
112
|
+
b.calls += x.calls;
|
|
113
|
+
}
|
|
114
|
+
return { weekly, rows: [...byKey.values()] };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function chart(r) {
|
|
118
|
+
const b = buckets(r);
|
|
119
|
+
if (!b) return '';
|
|
120
|
+
const W = 900, H = 240, m = { l: 56, r: 8, t: 12, b: 28 };
|
|
121
|
+
const pw = W - m.l - m.r, ph = H - m.t - m.b;
|
|
122
|
+
const top = niceMax(Math.max(...b.rows.map((x) => x.billed)));
|
|
123
|
+
const band = pw / b.rows.length;
|
|
124
|
+
const bw = Math.max(1, Math.min(24, band - 2));
|
|
125
|
+
const y = (v) => m.t + ph - (v / top) * ph;
|
|
126
|
+
const when = (x) => (b.weekly ? t.report.weekOf(shortDate(x.date, true)) : shortDate(x.date, true));
|
|
127
|
+
|
|
128
|
+
const grid = [0, top / 2, top].map((v) => `<line x1="${m.l}" x2="${W - m.r}" y1="${y(v)}" y2="${y(v)}" class="grid"/>
|
|
129
|
+
<text x="${m.l - 8}" y="${y(v) + 4}" text-anchor="end" class="axis">${esc(compact(v))}</text>`).join('');
|
|
130
|
+
// Rounded data end (4px), square at the baseline.
|
|
131
|
+
const bars = b.rows.map((x, i) => {
|
|
132
|
+
const h = (x.billed / top) * ph;
|
|
133
|
+
if (h <= 0) return '';
|
|
134
|
+
const x0 = m.l + i * band + (band - bw) / 2, y0 = m.t + ph - h, rr = Math.min(4, h, bw / 2);
|
|
135
|
+
return `<path class="col" d="M${x0},${m.t + ph}V${y0 + rr}Q${x0},${y0} ${x0 + rr},${y0}H${x0 + bw - rr}Q${x0 + bw},${y0} ${x0 + bw},${y0 + rr}V${m.t + ph}Z"/>`;
|
|
136
|
+
}).join('');
|
|
137
|
+
// Hit targets: the whole band, so a 2px column is as easy to hover as a 24px one.
|
|
138
|
+
const hits = b.rows.map((x, i) => `<rect class="hit" x="${m.l + i * band}" y="${m.t}" width="${band}" height="${ph}"
|
|
139
|
+
data-when="${esc(when(x))}" data-v="${esc(fmt(x.billed))}" data-c="${esc(fmt(x.billed + x.cache))}" data-n="${esc(t.report.chartCalls(x.calls))}"/>`).join('');
|
|
140
|
+
const every = Math.max(1, Math.ceil(b.rows.length / 6));
|
|
141
|
+
// A date every few columns, plus the last one when it isn't crowded by the previous label.
|
|
142
|
+
const last = b.rows.length - 1;
|
|
143
|
+
const ticks = b.rows.map((x, i) => (i % every === 0 || (i === last && last % every >= every / 2)) && `<text x="${i === 0 && band < 40 ? m.l : i === last && band < 40 ? W - m.r : m.l + i * band + band / 2}" y="${H - 8}" text-anchor="${i === 0 && band < 40 ? 'start' : i === last && band < 40 ? 'end' : 'middle'}" class="axis">${esc(shortDate(x.date))}</text>`).filter(Boolean).join('');
|
|
144
|
+
|
|
145
|
+
const rows = b.rows.filter((x) => x.calls);
|
|
146
|
+
return `<section class="chart">
|
|
147
|
+
<h2>${b.weekly ? t.report.chartTitleWeek : t.report.chartTitleDay}</h2>
|
|
148
|
+
<p class="hint">${t.report.chartHint}</p>
|
|
149
|
+
<div class="plot"><svg viewBox="0 0 ${W} ${H}" role="img" aria-label="${esc(b.weekly ? t.report.chartTitleWeek : t.report.chartTitleDay)}">${grid}${bars}${ticks}${hits}</svg></div>
|
|
150
|
+
<details><summary>${t.report.chartTable}</summary><div class="scroll"><table><thead><tr>${(b.weekly ? t.report.chartColsWeek : t.report.chartCols).map((c, i) => `<th${i ? ' class="num"' : ''}>${c}</th>`).join('')}</tr></thead><tbody>
|
|
151
|
+
${rows.map((x) => `<tr><td>${esc(shortDate(x.date, true))}</td><td class="num">${fmt(x.billed)}</td><td class="num">${fmt(x.billed + x.cache)}</td><td class="num">${x.calls}</td></tr>`).join('')}
|
|
152
|
+
</tbody></table></div></details>
|
|
153
|
+
</section>`;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// --- Page ---
|
|
157
|
+
|
|
158
|
+
function body(r, found, scope) {
|
|
66
159
|
const top5 = r.sessions.slice(0, 5).reduce((s, x) => s + x.tokens, 0);
|
|
67
160
|
const u = r.usage;
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
:
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
</div>
|
|
78
|
-
<
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
161
|
+
const meta = `<p class="meta">${t.report.convs(r.conversations)}${r.period ? ` · ${esc(periodLine(r))}` : ''}</p>`;
|
|
162
|
+
if (!r.conversations) {
|
|
163
|
+
return `${meta}<p>${t.report.noConversations(scope && esc(scope), esc(r.dir))}${r.olderSkipped ? ` ${esc(t.report.olderSkipped(r.olderSkipped))}.` : ''}</p>`;
|
|
164
|
+
}
|
|
165
|
+
return `${meta}<div class="tiles">
|
|
166
|
+
${u.conversations ? `<div><b>${fmt(billed(u))}</b><span>${t.report.tileReal}</span></div>
|
|
167
|
+
<div><b>${fmt(withCache(u))}</b><span>${t.report.tileCache(u.calls)}</span></div>` : ''}
|
|
168
|
+
<div><b>${fmt(r.tokens)}</b><span>${t.report.tileContext}</span></div>
|
|
169
|
+
<div><b>${pct(top5, r.tokens)}</b><span>${t.report.tileTop5}</span></div>
|
|
170
|
+
<div><b>${r.reads}</b><span>${t.report.tileReads}</span></div>
|
|
171
|
+
<div><b>${pct(r.rereads, r.reads)}</b><span>${t.report.tileRereads}</span></div>
|
|
172
|
+
</div>
|
|
173
|
+
${chart(r)}
|
|
174
|
+
${modelsTable(r)}
|
|
175
|
+
<h2>${t.report.happening}</h2>
|
|
176
|
+
<p class="hint">${t.report.happeningHint}</p>
|
|
177
|
+
${found.length ? found.map((f) => finding(f, r.tokens)).join('') : `<p>${t.report.nothing}</p>`}
|
|
178
|
+
<details class="detail-all"><summary><h2>${t.report.details}</h2></summary>
|
|
179
|
+
${sessionsTable(r)}
|
|
180
|
+
${table(t.report.byProject, r.projects, r.tokens, (x) => x.project || t.report.outsideGit)}
|
|
181
|
+
${table(t.report.byKind, r.kinds, r.tokens, (x) => kindLabel(x.key))}
|
|
182
|
+
${table(t.report.byFolder, r.folders, r.tokens)}
|
|
183
|
+
${table(t.report.files, r.files, r.tokens)}
|
|
184
|
+
${table(t.report.tools, r.tools, r.tokens, (x) => x.key)}
|
|
185
|
+
${table(t.report.commands, r.commands, r.tokens, (x) => x.key)}
|
|
186
|
+
</details>`;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* One report per period, switched in the browser by a row of presets. Each period is analyzed on its own,
|
|
191
|
+
* so every number, finding and table below the filter agrees with it.
|
|
192
|
+
* @param {{ key: string, label: string, r: object, found: object[] }[]} views the first one shows by default
|
|
193
|
+
*/
|
|
194
|
+
function htmlViews(views, { scope }) {
|
|
195
|
+
const u = views[0].r.usage;
|
|
196
|
+
const filter = views.length > 1
|
|
197
|
+
? `<div class="filter" role="group" aria-label="${esc(t.report.filterLabel)}"><span>${t.report.filterLabel}</span>
|
|
198
|
+
${views.map((v, i) => `<button type="button" data-view="${v.key}" aria-pressed="${i === 0}">${esc(v.label)}</button>`).join('')}</div>`
|
|
199
|
+
: '';
|
|
200
|
+
const sections = views.map((v, i) => `<div class="view" data-view="${v.key}"${i ? ' hidden' : ''}>${body(v.r, v.found, scope)}</div>`).join('');
|
|
90
201
|
|
|
91
202
|
return `<!doctype html><html lang="${lang()}"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
|
|
92
203
|
<title>agy-insights</title>
|
|
93
204
|
<style>
|
|
94
205
|
:root { --bg: #f7f8f7; --surface: #fff; --fg: #1d2421; --muted: #5d6963; --line: #dde3df; --accent: #1f7a5a; --accent-soft: #e3f1ea;
|
|
95
|
-
--warn: #b06a00; --code: #eef1ef; --bar: #1f7a5a; color-scheme: light; }
|
|
206
|
+
--warn: #b06a00; --code: #eef1ef; --bar: #1f7a5a; --series: #13805a; color-scheme: light; }
|
|
96
207
|
@media (prefers-color-scheme: dark) { :root { --bg: #141917; --surface: #1b211e; --fg: #e3e9e5; --muted: #9aa8a0; --line: #2c3530;
|
|
97
|
-
--accent: #5cc79c; --accent-soft: #1d3329; --warn: #e2a64a; --code: #222a26; --bar: #5cc79c; color-scheme: dark; } }
|
|
208
|
+
--accent: #5cc79c; --accent-soft: #1d3329; --warn: #e2a64a; --code: #222a26; --bar: #5cc79c; --series: #33a376; color-scheme: dark; } }
|
|
98
209
|
body { margin: 0; background: var(--bg); color: var(--fg); font: 15px/1.55 system-ui, -apple-system, "Segoe UI", sans-serif; }
|
|
99
210
|
main { max-width: 980px; margin: 0 auto; padding: 32px 16px 48px; }
|
|
100
211
|
h1 { font-size: 26px; margin: 0 0 4px; } h2 { font-size: 20px; margin: 32px 0 6px; display: inline-block; } h3 { font-size: 16px; margin: 0 0 6px; }
|
|
101
212
|
.meta, .hint, .detail, .tok, .where { color: var(--muted); } .tok { font-weight: normal; font-size: 14px; }
|
|
102
213
|
code, pre { font-family: ui-monospace, "Cascadia Mono", Consolas, monospace; font-size: 13px; }
|
|
103
214
|
code { background: var(--code); padding: 1px 5px; border-radius: 4px; overflow-wrap: anywhere; }
|
|
104
|
-
.
|
|
215
|
+
.filter { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; margin: 20px 0 4px; }
|
|
216
|
+
.filter span { color: var(--muted); font-size: 14px; margin-right: 4px; }
|
|
217
|
+
.filter button { font: 14px system-ui, sans-serif; color: var(--fg); background: var(--surface); border: 1px solid var(--line); border-radius: 999px; padding: 4px 12px; cursor: pointer; }
|
|
218
|
+
.filter button:hover { background: var(--accent-soft); }
|
|
219
|
+
.filter button[aria-pressed="true"] { background: var(--accent); border-color: var(--accent); color: var(--surface); font-weight: 600; }
|
|
220
|
+
.filter button:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
221
|
+
.tiles { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 12px; margin: 16px 0 20px; }
|
|
105
222
|
.tiles div { background: var(--surface); border: 1px solid var(--line); border-radius: 8px; padding: 12px 14px; }
|
|
106
223
|
.tiles b { display: block; font-size: 24px; font-variant-numeric: tabular-nums; } .tiles span { color: var(--muted); font-size: 14px; }
|
|
224
|
+
.chart h2 { margin-top: 8px; } .models .hint { margin-top: 0; } .chart .hint { margin-top: 0; }
|
|
225
|
+
.plot { background: var(--surface); border: 1px solid var(--line); border-radius: 8px; padding: 8px 8px 0; }
|
|
226
|
+
.plot svg { display: block; width: 100%; height: auto; }
|
|
227
|
+
.grid { stroke: var(--line); stroke-width: 1; } .axis { fill: var(--muted); font: 12px system-ui, sans-serif; font-variant-numeric: tabular-nums; }
|
|
228
|
+
.col { fill: var(--series); } .hit { fill: transparent; cursor: default; } .hit:hover { fill: var(--fg); fill-opacity: .05; }
|
|
229
|
+
.tip { position: fixed; pointer-events: none; background: var(--surface); color: var(--fg); border: 1px solid var(--line); border-radius: 6px;
|
|
230
|
+
padding: 6px 10px; font-size: 13px; box-shadow: 0 2px 8px rgb(0 0 0 / .12); z-index: 10; }
|
|
231
|
+
.tip b { display: block; font-size: 15px; font-variant-numeric: tabular-nums; } .tip span { display: block; color: var(--muted); }
|
|
107
232
|
.finding { background: var(--surface); border: 1px solid var(--line); border-left: 4px solid var(--accent); border-radius: 8px; padding: 14px 16px; margin: 12px 0; }
|
|
108
233
|
.finding.warn { border-left-color: var(--warn); } .finding p { margin: 4px 0; }
|
|
109
234
|
.items { list-style: none; margin: 10px 0 0; padding: 0; }
|
|
@@ -128,15 +253,23 @@ function html(r, found, { scope }) {
|
|
|
128
253
|
footer { margin-top: 32px; font-size: 13px; color: var(--muted); }
|
|
129
254
|
</style></head><body><main>
|
|
130
255
|
<h1>${t.report.title}</h1>
|
|
131
|
-
<p class="meta">${scope ? `${t.report.project} <code>${esc(scope)}</code
|
|
132
|
-
${
|
|
133
|
-
|
|
256
|
+
<p class="meta">${scope ? `${t.report.project} <code>${esc(scope)}</code> · ` : views[0].r.since ? '' : `${t.report.wholeHistory} · `}${esc(t.report.generated(new Date().toLocaleString(t.locale)))}</p>
|
|
257
|
+
${filter}
|
|
258
|
+
${sections}
|
|
259
|
+
<footer>${esc(t.report.footerReal(views[0].r.conversations - u.conversations))}
|
|
134
260
|
${esc(t.report.footerCache)}
|
|
135
261
|
${esc(t.report.footerEstimate)}
|
|
136
262
|
${esc(t.report.footerPrivacy)}</footer>
|
|
137
263
|
</main>
|
|
264
|
+
<div class="tip" hidden><b></b><span></span><span></span></div>
|
|
138
265
|
<script>
|
|
139
266
|
document.addEventListener('click', (e) => {
|
|
267
|
+
const v = e.target.closest('.filter button');
|
|
268
|
+
if (v) {
|
|
269
|
+
for (const b of document.querySelectorAll('.filter button')) b.setAttribute('aria-pressed', String(b === v));
|
|
270
|
+
for (const s of document.querySelectorAll('.view')) s.hidden = s.dataset.view !== v.dataset.view;
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
140
273
|
const b = e.target.closest('.copy'); if (!b) return;
|
|
141
274
|
const pre = b.parentElement.querySelector('pre');
|
|
142
275
|
const ok = () => { b.textContent = ${JSON.stringify(t.report.copied)}; setTimeout(() => (b.textContent = ${JSON.stringify(t.report.copy)}), 1500); };
|
|
@@ -145,15 +278,36 @@ function html(r, found, { scope }) {
|
|
|
145
278
|
const sel = getSelection(); sel.removeAllRanges(); sel.addRange(range);
|
|
146
279
|
});
|
|
147
280
|
});
|
|
281
|
+
// Chart tooltip: values are filled with textContent, never as HTML.
|
|
282
|
+
const tip = document.querySelector('.tip');
|
|
283
|
+
document.addEventListener('pointermove', (e) => {
|
|
284
|
+
const h = e.target.closest && e.target.closest('.hit');
|
|
285
|
+
if (!h) { tip.hidden = true; return; }
|
|
286
|
+
const [v, when, rest] = tip.children;
|
|
287
|
+
v.textContent = h.dataset.v + ' tokens';
|
|
288
|
+
when.textContent = h.dataset.when;
|
|
289
|
+
rest.textContent = h.dataset.n + ' · ' + h.dataset.c + ' ' + ${JSON.stringify(t.report.withCacheShort)};
|
|
290
|
+
tip.hidden = false;
|
|
291
|
+
const x = Math.min(e.clientX + 14, innerWidth - tip.offsetWidth - 8);
|
|
292
|
+
tip.style.left = x + 'px';
|
|
293
|
+
tip.style.top = (e.clientY - tip.offsetHeight - 10) + 'px';
|
|
294
|
+
});
|
|
148
295
|
</script></body></html>`;
|
|
149
296
|
}
|
|
150
297
|
|
|
298
|
+
function html(r, found, opts) {
|
|
299
|
+
return htmlViews([{ key: 'all', label: '', r, found }], opts);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
|
|
151
303
|
// --- Text ---
|
|
152
304
|
|
|
153
305
|
function text(r, found, { scope, paths }) {
|
|
154
306
|
const top5 = r.sessions.slice(0, 5).reduce((s, x) => s + x.tokens, 0);
|
|
155
307
|
const out = [`\n${t.report.history}: ${r.dir} (${r.backend})${scope ? ` · ${t.report.projectLower} ${scope}` : ''}`,
|
|
156
308
|
t.report.summary(r.conversations, r.steps, fmt(r.tokens))];
|
|
309
|
+
if (r.period) out.push(periodLine(r));
|
|
310
|
+
if (r.olderSkipped) out.push(`(${t.report.olderSkipped(r.olderSkipped)})`);
|
|
157
311
|
if (!r.conversations) return out.join('\n') + '\n';
|
|
158
312
|
const u = r.usage;
|
|
159
313
|
if (u.conversations) {
|
|
@@ -162,10 +316,19 @@ function text(r, found, { scope, paths }) {
|
|
|
162
316
|
const top = r.sessions.filter((s) => s.usage).slice(0, 5);
|
|
163
317
|
out.push(t.report.topSessions(top.length));
|
|
164
318
|
for (const s of top) {
|
|
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)))}`);
|
|
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))}`);
|
|
166
320
|
}
|
|
167
321
|
}
|
|
168
322
|
out.push(t.report.top5(pct(top5, r.tokens), r.reads, r.rereads, pct(r.rereads, r.reads)));
|
|
323
|
+
const models = r.models.filter((m) => m.model);
|
|
324
|
+
if (models.length) {
|
|
325
|
+
const total = r.models.reduce((s, m) => s + m.input + m.output, 0);
|
|
326
|
+
out.push('', t.report.modelsText);
|
|
327
|
+
for (const m of models) {
|
|
328
|
+
const tok = m.input + m.output;
|
|
329
|
+
out.push(` ${t.report.modelLine(m.model, effortLabel(m.effort), m.calls, fmt(tok), pct(tok, total), m.known ? fmt(tok / m.known) : '–')}`);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
169
332
|
for (const f of found) {
|
|
170
333
|
out.push('', `${f.level === 'warn' ? '⚠' : '•'} ${f.title}${f.tokens ? ` (~${fmt(f.tokens)} tokens, ${pct(f.tokens, r.tokens)})` : ''}`,
|
|
171
334
|
` ${f.why}`, ` → ${f.suggestion}`);
|
|
@@ -182,4 +345,4 @@ function text(r, found, { scope, paths }) {
|
|
|
182
345
|
return out.join('\n');
|
|
183
346
|
}
|
|
184
347
|
|
|
185
|
-
module.exports = { html, text };
|
|
348
|
+
module.exports = { html, htmlViews, text };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agy-insights",
|
|
3
|
-
"version": "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",
|
|
3
|
+
"version": "0.7.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@latest",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"antigravity",
|