agy-insights 0.7.2 → 0.8.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 +5 -3
- package/README.md +5 -3
- package/agy-insights.js +27 -1
- package/docs/how-it-works.md +3 -3
- package/lib/findings.js +1 -1
- package/lib/history.js +81 -17
- package/lib/i18n.js +36 -8
- package/lib/report.js +45 -17
- 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.
|
|
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.8.0`.
|
|
40
40
|
|
|
41
41
|
**Instalándolo globalmente:**
|
|
42
42
|
|
|
@@ -61,7 +61,7 @@ Y ejecútalo con `node agy-insights/agy-insights.js` en lugar de `npx agy-insigh
|
|
|
61
61
|
npx agy-insights
|
|
62
62
|
```
|
|
63
63
|
|
|
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
|
|
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 o 30 días, o esta semana (desde el lunes): los totales, la gráfica, los hallazgos y las tablas siguen el periodo elegido.
|
|
65
65
|
|
|
66
66
|
| Para… | Ejecuta |
|
|
67
67
|
|---|---|
|
|
@@ -82,7 +82,7 @@ El informe da dos cifras:
|
|
|
82
82
|
|
|
83
83
|
## Leer el informe
|
|
84
84
|
|
|
85
|
-
Arriba están el selector de periodo y los totales. Después, una gráfica de **consumo de tokens por día** (por semana si el historial pasa de ~4 meses) para ver la evolución
|
|
85
|
+
Arriba están el selector de periodo y los totales. Después, una gráfica de **consumo de tokens por día** (por semana si el historial pasa de ~4 meses) para ver la evolución. Cámbiala a **Por llamada** para ver los tokens medios de cada llamada al modelo: el total sube y baja según cuánto hayas trabajado, mientras que por llamada solo baja si cada llamada lleva menos contexto, así que es la que muestra si un cambio ha servido. Pasa el ratón por una columna para ver sus cifras, o abre «Ver como tabla». Después, **Modelos y esfuerzo**: llamadas y tokens reales por modelo y nivel de esfuerzo (por ejemplo Gemini 3.8 Flash · Alto), con los tokens por llamada, para ver cuánto cuesta un esfuerzo mayor. Debajo, **Qué está pasando** reúne los hallazgos; al final, el detalle por proyecto, tipo de fichero, carpeta, fichero, herramienta y comando. El proyecto de cada fichero se detecta por su carpeta `.git`.
|
|
86
86
|
|
|
87
87
|
| Hallazgo | Qué significa | Qué hacer |
|
|
88
88
|
|---|---|---|
|
|
@@ -161,6 +161,8 @@ Copia `agy-insights.example.json` a `~/.agy-insights.json` (o pásalo con `--con
|
|
|
161
161
|
|
|
162
162
|
**Uso la app o el IDE de Antigravity, no el CLI**. Por ahora solo se lee el historial del CLI.
|
|
163
163
|
|
|
164
|
+
**Muchas llamadas con modelo «desconocido»**. agy no anotó su modelo. Ejecuta `npx agy-insights@latest --debug-models` y pega el resultado en una [incidencia](https://github.com/migohe14/agy-insights/issues): solo contiene recuentos y nombres de modelo.
|
|
165
|
+
|
|
164
166
|
**El navegador no se abre**. La ruta del informe aparece en la terminal; ábrela a mano.
|
|
165
167
|
|
|
166
168
|
## Estado
|
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.
|
|
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.8.0`.
|
|
40
40
|
|
|
41
41
|
**Globally:**
|
|
42
42
|
|
|
@@ -61,7 +61,7 @@ Then run it with `node agy-insights/agy-insights.js` instead of `npx agy-insight
|
|
|
61
61
|
npx agy-insights
|
|
62
62
|
```
|
|
63
63
|
|
|
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
|
|
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 or 30 days, or this week (since Monday): totals, chart, findings and tables all follow the selected period.
|
|
65
65
|
|
|
66
66
|
| To… | Run |
|
|
67
67
|
|---|---|
|
|
@@ -82,7 +82,7 @@ The report gives two figures:
|
|
|
82
82
|
|
|
83
83
|
## Reading the report
|
|
84
84
|
|
|
85
|
-
At the top, the period selector and the totals. Then a chart of **token usage per day** (per week when the history spans more than ~4 months), to see how usage evolves
|
|
85
|
+
At the top, the period selector and the totals. Then a chart of **token usage per day** (per week when the history spans more than ~4 months), to see how usage evolves. Switch it to **Per call** to see mean tokens per model call: the total follows how much you worked, while per call drops only when each call carries less context, so it is the one that shows whether a change paid off. Hover a column for its figures, or open "View as table". Next, **Models and effort**: calls and real tokens per model and effort level (for example Gemini 3.8 Flash · High), including tokens per call, so you can see what a higher effort costs. Below, **What's happening** lists the findings; at the end, the details by project, file type, folder, file, tool and command. The project of each file is detected through its `.git` folder.
|
|
86
86
|
|
|
87
87
|
| Finding | What it means | What to do |
|
|
88
88
|
|---|---|---|
|
|
@@ -161,6 +161,8 @@ Copy `agy-insights.example.json` to `~/.agy-insights.json` (or pass it with `--c
|
|
|
161
161
|
|
|
162
162
|
**I use the Antigravity app or IDE, not the CLI**. For now only the CLI history is read.
|
|
163
163
|
|
|
164
|
+
**Many calls under model "unknown"**. agy recorded no model for them. Run `npx agy-insights@latest --debug-models` and paste the output into an [issue](https://github.com/migohe14/agy-insights/issues): it only has counts and model names.
|
|
165
|
+
|
|
164
166
|
**The browser doesn't open**. The report path is printed in the terminal; open it manually.
|
|
165
167
|
|
|
166
168
|
## Status
|
package/agy-insights.js
CHANGED
|
@@ -32,6 +32,7 @@ function parseArgs(argv) {
|
|
|
32
32
|
else if (a === '--lang') opts.lang = value();
|
|
33
33
|
else if (a === '--since') opts.since = value();
|
|
34
34
|
else if (a === '--days') opts.days = value();
|
|
35
|
+
else if (a === '--debug-models') opts.debugModels = true;
|
|
35
36
|
else fail(t.unknownOption(a));
|
|
36
37
|
}
|
|
37
38
|
return opts;
|
|
@@ -82,8 +83,30 @@ function sinceTime({ since, days }) {
|
|
|
82
83
|
return null;
|
|
83
84
|
}
|
|
84
85
|
|
|
86
|
+
// Tab-separated table of where each conversation's call models came from, for bug reports.
|
|
87
|
+
function debugModels(r) {
|
|
88
|
+
const d = t.debugModels;
|
|
89
|
+
const sum = { rows: 0, rowsWithModel: 0, calls: 0, fromTranscript: 0, unknown: 0 };
|
|
90
|
+
const lines = [d.header.join('\t')];
|
|
91
|
+
for (const s of r.sessions) {
|
|
92
|
+
const m = s.modelSources;
|
|
93
|
+
for (const k of Object.keys(sum)) sum[k] += m[k];
|
|
94
|
+
lines.push([s.id.slice(0, 8), m.calls, m.rows, m.rowsWithModel, m.fromTranscript, m.unknown, s.model ? `${s.model.model} (${s.model.effort})` : '', m.unknown ? m.candidates.join(' | ') : ''].join('\t'));
|
|
95
|
+
}
|
|
96
|
+
lines.push([d.total, sum.calls, sum.rows, sum.rowsWithModel, sum.fromTranscript, sum.unknown, '', ''].join('\t'), '', d.note, '');
|
|
97
|
+
return lines.join('\n');
|
|
98
|
+
}
|
|
99
|
+
|
|
85
100
|
// Periods the HTML report can switch to, besides the whole history.
|
|
86
|
-
const PRESET_DAYS = [90, 30
|
|
101
|
+
const PRESET_DAYS = [90, 30];
|
|
102
|
+
|
|
103
|
+
// This Monday at local midnight.
|
|
104
|
+
function weekStart() {
|
|
105
|
+
const d = new Date();
|
|
106
|
+
d.setHours(0, 0, 0, 0);
|
|
107
|
+
d.setDate(d.getDate() - ((d.getDay() + 6) % 7));
|
|
108
|
+
return d.getTime();
|
|
109
|
+
}
|
|
87
110
|
|
|
88
111
|
async function main() {
|
|
89
112
|
const argv = process.argv.slice(2);
|
|
@@ -104,6 +127,7 @@ async function main() {
|
|
|
104
127
|
customKinds: cfg.customKinds || {},
|
|
105
128
|
});
|
|
106
129
|
const r = await run(since);
|
|
130
|
+
if (opts.debugModels) return process.stdout.write(debugModels(r));
|
|
107
131
|
if (opts.map) {
|
|
108
132
|
const m = buildMap(opts.project, { ...cfg.map, history: r.files });
|
|
109
133
|
console.log(m.block);
|
|
@@ -128,6 +152,8 @@ async function main() {
|
|
|
128
152
|
const x = await run(Date.now() - days * 24 * 60 * 60 * 1000);
|
|
129
153
|
views.push({ key: `d${days}`, label: t.report.rangeDays(days), r: x, found: find(x) });
|
|
130
154
|
}
|
|
155
|
+
const week = await run(weekStart());
|
|
156
|
+
views.push({ key: 'week', label: t.report.rangeWeek, r: week, found: find(week) });
|
|
131
157
|
}
|
|
132
158
|
fs.writeFileSync(out, report.htmlViews(views, { scope: opts.project }));
|
|
133
159
|
console.log(t.done(r.conversations, found.length, out));
|
package/docs/how-it-works.md
CHANGED
|
@@ -34,12 +34,12 @@ 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`: 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.
|
|
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. That pairing is only trusted when the counts match. Otherwise each call takes the model selected in the transcript (`USER_INPUT` lines carry "changed setting `Model Selection` from X to Y"), or the conversation's only model; what is left is "unknown". `modelKey()` merges the two spellings (`Gemini 3.8 Flash` / `gemini-3.8-flash`, `High` / `high`). `--debug-models` prints, per conversation, how many calls got their model from each source: counts only, safe to paste into an issue.
|
|
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`
|
|
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` has one row per conversation: its short `title` (or `preview` in older ones) names each session in the report instead of its id; `last_modified_time` is not used. If the file is missing, sessions are shown by id. Each model call's `created_at` also puts its tokens on a local day (`r.daily`), which feeds the usage chart.
|
|
41
41
|
|
|
42
|
-
The HTML report holds one view per period (whole history, last 90
|
|
42
|
+
The HTML report holds one view per period (whole history, last 90 and 30 days, this week since Monday): `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
43
|
|
|
44
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.
|
|
45
45
|
|
package/lib/findings.js
CHANGED
|
@@ -131,7 +131,7 @@ function findings(r, opts = {}) {
|
|
|
131
131
|
why: f.longWhy(long.length, num(o.longSessionTokens)), suggestion: f.longSuggestion,
|
|
132
132
|
items: long.slice(0, 5).map((s) => ({
|
|
133
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: [],
|
|
134
|
+
label: f.conversation(s.id.slice(0, 8), when(s.start, s.end), s.title), fixes: [],
|
|
135
135
|
tokens: s.usage ? s.usage.input + s.usage.output : s.tokens, exact: Boolean(s.usage),
|
|
136
136
|
detail: f.steps(s.steps) + (s.usage
|
|
137
137
|
? f.longUsage(s.usage.calls, num(s.usage.input + s.usage.cache + s.usage.output))
|
package/lib/history.js
CHANGED
|
@@ -29,6 +29,23 @@ function modelOf(strs) {
|
|
|
29
29
|
return { model: null, effort: null };
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
// agy notes model switches in the user's message: "changed setting `Model Selection` from None to
|
|
33
|
+
// Gemini 3.8 Flash (High)." The model in force for every call that follows.
|
|
34
|
+
const MODEL_SETTING_RE = /changed setting `Model Selection` from .+? to (.+?)\.(?:\s|$)/g;
|
|
35
|
+
function selectedModel(text) {
|
|
36
|
+
let last = null;
|
|
37
|
+
for (const m of String(text).matchAll(MODEL_SETTING_RE)) last = m[1].trim();
|
|
38
|
+
return last && modelOf([last]).model ? modelOf([last]) : null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// A short string that may name a model in a format modelOf() does not know yet: mentions a model family,
|
|
42
|
+
// or is an enum-like id ("MODEL_PLACEHOLDER_M26"). Only used for --debug-models.
|
|
43
|
+
const looksLikeModel = (s) => s.length <= 60 && !/[\\/:]/.test(s)
|
|
44
|
+
&& (/\b(gemini|claude|gpt|model|flash|pro|argon)\b/i.test(s) || /^[A-Z][A-Z0-9]*(_[A-Z0-9]+)+$/.test(s));
|
|
45
|
+
|
|
46
|
+
// "Gemini 3.8 Flash" and "gemini-3.8-flash", "High" and "high": the same model and effort.
|
|
47
|
+
const modelKey = (m) => `${(m.model || '').toLowerCase().replace(/[\s_]+/g, '-')}|${(m.effort || '').toLowerCase()}`;
|
|
48
|
+
|
|
32
49
|
// --- SQLite access: node:sqlite when the runtime has it (Node >= 22.13), sql.js (WASM) otherwise. ---
|
|
33
50
|
|
|
34
51
|
let nodeSqlite = null;
|
|
@@ -203,20 +220,27 @@ function realUsage(dir, id) {
|
|
|
203
220
|
let text;
|
|
204
221
|
try { text = fs.readFileSync(file, 'utf8'); } catch { return null; }
|
|
205
222
|
// `days`: the same figures per local day of each model call (calls without a time go under null).
|
|
206
|
-
// `perCall`: each call's figures, in order
|
|
223
|
+
// `perCall`: each call's figures, in order, and the model selected in the transcript when it was made.
|
|
207
224
|
const u = { calls: 0, input: 0, cache: 0, output: 0, errors: 0, start: null, end: null, days: new Map(), perCall: [] };
|
|
225
|
+
let selected = null;
|
|
226
|
+
u.unrecognized = new Set();
|
|
208
227
|
for (const line of text.split('\n')) {
|
|
209
228
|
let e;
|
|
210
229
|
try { e = JSON.parse(line); } catch { continue; }
|
|
211
230
|
const at = Date.parse(e.created_at);
|
|
212
231
|
if (at) { u.start = Math.min(u.start ?? at, at); u.end = Math.max(u.end ?? at, at); }
|
|
213
232
|
if (e.status === 'ERROR') u.errors++;
|
|
233
|
+
if (e.type === 'USER_INPUT') {
|
|
234
|
+
selected = selectedModel(e.content) || selected;
|
|
235
|
+
// Model names agy selected that modelOf() does not understand, for --debug-models.
|
|
236
|
+
for (const m of String(e.content).matchAll(MODEL_SETTING_RE)) if (!modelOf([m[1].trim()]).model) u.unrecognized.add(m[1].trim().slice(0, 60));
|
|
237
|
+
}
|
|
214
238
|
if (e.type !== 'PLANNER_RESPONSE') continue;
|
|
215
239
|
u.calls++;
|
|
216
240
|
u.input += Number(e.input_tokens) || 0;
|
|
217
241
|
u.cache += Number(e.cache_read_tokens) || 0;
|
|
218
242
|
u.output += Number(e.output_tokens) || 0;
|
|
219
|
-
u.perCall.push({ input: Number(e.input_tokens) || 0, cache: Number(e.cache_read_tokens) || 0, output: Number(e.output_tokens) || 0 });
|
|
243
|
+
u.perCall.push({ input: Number(e.input_tokens) || 0, cache: Number(e.cache_read_tokens) || 0, output: Number(e.output_tokens) || 0, model: selected });
|
|
220
244
|
const k = at ? dayKey(at) : null;
|
|
221
245
|
const d = u.days.get(k) || { calls: 0, input: 0, cache: 0, output: 0 };
|
|
222
246
|
d.calls++;
|
|
@@ -228,6 +252,26 @@ function realUsage(dir, id) {
|
|
|
228
252
|
return u;
|
|
229
253
|
}
|
|
230
254
|
|
|
255
|
+
// --- Titles ---
|
|
256
|
+
|
|
257
|
+
// The short title agy gives each conversation ("Analyzing Package Lock Dependencies"), from
|
|
258
|
+
// <conversations>/../conversation_summaries.db; older ones only have it as `preview`. Empty map when
|
|
259
|
+
// the file is missing or unreadable: the report then falls back to the conversation id.
|
|
260
|
+
async function conversationTitles(dir, tmp) {
|
|
261
|
+
const titles = new Map();
|
|
262
|
+
const file = path.join(dir, '..', 'conversation_summaries.db');
|
|
263
|
+
if (!fs.existsSync(file)) return titles;
|
|
264
|
+
let db;
|
|
265
|
+
try {
|
|
266
|
+
db = await openDb(file, tmp);
|
|
267
|
+
for (const [id, title, preview] of db.all('select conversation_id, title, preview from conversation_summaries')) {
|
|
268
|
+
const name = String(title || preview || '').trim().slice(0, 80);
|
|
269
|
+
if (name) titles.set(String(id), name);
|
|
270
|
+
}
|
|
271
|
+
} catch { /* no titles */ } finally { db?.close(); }
|
|
272
|
+
return titles;
|
|
273
|
+
}
|
|
274
|
+
|
|
231
275
|
// --- Aggregation ---
|
|
232
276
|
|
|
233
277
|
class Counter {
|
|
@@ -269,6 +313,7 @@ async function analyze({ dir = DEFAULT_DIR, workspaces = [], customKinds = {}, s
|
|
|
269
313
|
|
|
270
314
|
try {
|
|
271
315
|
const dbs = fs.existsSync(dir) ? fs.readdirSync(dir).filter((f) => /^[0-9a-f-]{36}\.db$/i.test(f)) : [];
|
|
316
|
+
const titles = await conversationTitles(dir, tmp);
|
|
272
317
|
for (const f of dbs) {
|
|
273
318
|
// When it happened: the transcript's step times, else the database's last write.
|
|
274
319
|
const id = f.replace(/\.db$/, '');
|
|
@@ -345,26 +390,45 @@ async function analyze({ dir = DEFAULT_DIR, workspaces = [], customKinds = {}, s
|
|
|
345
390
|
daily.set(day, d);
|
|
346
391
|
}
|
|
347
392
|
}
|
|
348
|
-
//
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
const
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
393
|
+
// Model of each call. gen_metadata usually has one row per call, in the transcript's order; when the
|
|
394
|
+
// counts match, the n-th row is the n-th call. When they don't, pairing by position would mislabel
|
|
395
|
+
// calls, so the model selected in the transcript is used instead, or the conversation's only model.
|
|
396
|
+
const metaStrs = db.all('select data from gen_metadata order by idx').map(([blob]) => collect(blob));
|
|
397
|
+
const metaRows = metaStrs.map(modelOf);
|
|
398
|
+
const calls = usage ? usage.perCall : metaRows.map(() => null);
|
|
399
|
+
const aligned = metaRows.length === calls.length;
|
|
400
|
+
const rowModels = new Set(metaRows.filter((m) => m.model).map(modelKey));
|
|
401
|
+
const onlyModel = rowModels.size === 1 ? metaRows.find((m) => m.model) : null;
|
|
402
|
+
const callModels = calls.map((c, i) => {
|
|
403
|
+
if (aligned && metaRows[i].model) return metaRows[i];
|
|
404
|
+
return c?.model || onlyModel || { model: null, effort: null };
|
|
405
|
+
});
|
|
406
|
+
const seenModels = new Set(), counts = new Map();
|
|
407
|
+
callModels.forEach((m, i) => {
|
|
408
|
+
const k = modelKey(m);
|
|
409
|
+
const e = models.get(k) || { model: m.model, effort: m.effort, calls: 0, convs: 0, input: 0, cache: 0, output: 0, known: 0 };
|
|
410
|
+
// Prefer the display name ("Gemini 3.8 Flash", "High") over the id spelling.
|
|
411
|
+
if (m.model && /[A-Z ]/.test(m.model) && !/[A-Z ]/.test(e.model || '')) { e.model = m.model; e.effort = m.effort; }
|
|
356
412
|
e.calls++;
|
|
357
413
|
if (!seenModels.has(k)) { seenModels.add(k); e.convs++; }
|
|
358
|
-
const c =
|
|
414
|
+
const c = calls[i];
|
|
359
415
|
if (c) { e.known++; e.input += c.input; e.cache += c.cache; e.output += c.output; }
|
|
360
416
|
models.set(k, e);
|
|
361
|
-
|
|
417
|
+
if (m.model) counts.set(k, (counts.get(k) || 0) + 1);
|
|
418
|
+
});
|
|
362
419
|
// The session's model: the one that made most of its calls.
|
|
363
|
-
const counts = new Map();
|
|
364
|
-
for (const m of callModels) counts.set(`${m.model}|${m.effort}`, (counts.get(`${m.model}|${m.effort}`) || 0) + 1);
|
|
365
420
|
const main = [...counts].sort((a, b) => b[1] - a[1])[0];
|
|
366
|
-
const model = main ?
|
|
367
|
-
r.sessions.push({
|
|
421
|
+
const model = main ? models.get(main[0]) : null;
|
|
422
|
+
r.sessions.push({
|
|
423
|
+
id, title: titles.get(id) || null, start, end, steps: steps.length, tokens: convBytes / BYTES_PER_TOKEN, usage,
|
|
424
|
+
model: model ? { model: model.model, effort: model.effort } : null, models: counts.size,
|
|
425
|
+
// For --debug-models: where each call's model came from, and for rows where no model was recognized,
|
|
426
|
+
// the short strings that look like a model id (to spot a new format). Never prompts, paths or code.
|
|
427
|
+
modelSources: { rows: metaRows.length, rowsWithModel: metaRows.filter((m) => m.model).length, calls: calls.length,
|
|
428
|
+
fromTranscript: calls.filter((c) => c?.model).length, unknown: callModels.filter((m) => !m.model).length,
|
|
429
|
+
candidates: [...new Set([...(usage?.unrecognized || []),
|
|
430
|
+
...metaStrs.filter((_, i) => !metaRows[i].model).flat().filter(looksLikeModel)])].slice(0, 10) },
|
|
431
|
+
});
|
|
368
432
|
r.tokens += convBytes / BYTES_PER_TOKEN;
|
|
369
433
|
} catch { r.unreadable++; } finally { db.close(); }
|
|
370
434
|
}
|
|
@@ -386,4 +450,4 @@ async function analyze({ dir = DEFAULT_DIR, workspaces = [], customKinds = {}, s
|
|
|
386
450
|
};
|
|
387
451
|
}
|
|
388
452
|
|
|
389
|
-
module.exports = { analyze, DEFAULT_DIR, kind, NON_SOURCE, realUsage, dayKey, modelOf, repoFiles };
|
|
453
|
+
module.exports = { analyze, DEFAULT_DIR, kind, NON_SOURCE, realUsage, dayKey, modelOf, selectedModel, repoFiles };
|
package/lib/i18n.js
CHANGED
|
@@ -20,6 +20,7 @@ Options:
|
|
|
20
20
|
--since <YYYY-MM-DD> Only conversations active since that date
|
|
21
21
|
--days <n> Only conversations active in the last n days
|
|
22
22
|
--lang <en|es> Language of the output (default: the system's)
|
|
23
|
+
--debug-models Per conversation, where each call's model came from (counts only, to report issues)
|
|
23
24
|
--dir <folder> Another history folder (default ${dir})
|
|
24
25
|
--config <file> Configuration (default ~/.agy-insights.json if it exists)
|
|
25
26
|
--help This help
|
|
@@ -33,6 +34,11 @@ Options:
|
|
|
33
34
|
cannotRead: (f, msg) => `Cannot read ${f}: ${msg}`,
|
|
34
35
|
mapDone: (tokens, file) => `\n~${tokens} tokens on every request. Paste it into ${file} if you like it; agy-insights does not write it.`,
|
|
35
36
|
reportName: 'report',
|
|
37
|
+
debugModels: {
|
|
38
|
+
header: ['conversation', 'calls', 'gen_metadata rows', 'rows with model', 'calls with model in transcript', 'unknown', 'main model', 'unrecognized model ids'],
|
|
39
|
+
total: 'total',
|
|
40
|
+
note: 'Only counts and model names or ids: no paths, prompts or code. Paste it into an issue if models show up as unknown.',
|
|
41
|
+
},
|
|
36
42
|
done: (convs, found, out) => `${plural(convs, 'conversation', 'conversations')} analyzed, ${plural(found, 'finding', 'findings')}.\nReport: ${out}`,
|
|
37
43
|
|
|
38
44
|
kinds: {
|
|
@@ -81,7 +87,7 @@ Options:
|
|
|
81
87
|
longTitle: 'Very long sessions',
|
|
82
88
|
longWhy: (n, min) => `${plural(n, 'conversation exceeds', 'conversations exceed')} ${min} tokens. In a long session, everything accumulated is resent on every turn.`,
|
|
83
89
|
longSuggestion: 'A new conversation per task, or /compact when you change topic.',
|
|
84
|
-
conversation: (id, at) => `conversation ${id}${at ? ` · ${at}` : ''}`,
|
|
90
|
+
conversation: (id, at, title) => `${title ? `"${title}" (${id})` : `conversation ${id}`}${at ? ` · ${at}` : ''}`,
|
|
85
91
|
steps: (n) => plural(n, 'step', 'steps'),
|
|
86
92
|
longUsage: (calls, cached) => ` · ${plural(calls, 'model call', 'model calls')} · real tokens, ${cached} with cache`,
|
|
87
93
|
},
|
|
@@ -112,6 +118,7 @@ Options:
|
|
|
112
118
|
modelLine: (model, effort, calls, tokens, pct, perCall) => `${model} · ${effort} ${plural(calls, 'call', 'calls')}, ${tokens} tokens (${pct}), ~${perCall} per call`,
|
|
113
119
|
sessionModel: 'Model',
|
|
114
120
|
otherModels: (n) => ` +${n} more`,
|
|
121
|
+
modelsUnknown: (n) => `"unknown": ${n} calls for which agy recorded no model, neither in gen_metadata nor as a model change in the transcript. If there are many, run npx agy-insights@latest --debug-models and paste the result into an issue on GitHub (it only contains counts).`,
|
|
115
122
|
efforts: { minimal: 'Minimal', low: 'Low', medium: 'Medium', high: 'High', xhigh: 'Extra high', 'extra high': 'Extra high', max: 'Max' },
|
|
116
123
|
sessionsTitle: 'Sessions (real usage)',
|
|
117
124
|
sessionCols: ['Conversation', 'When', 'Model', 'Tokens', '%', 'With cache', 'Calls', 'Errors'],
|
|
@@ -135,12 +142,19 @@ Options:
|
|
|
135
142
|
filterLabel: 'Period',
|
|
136
143
|
rangeAll: 'All history',
|
|
137
144
|
rangeDays: (n) => `Last ${n} days`,
|
|
145
|
+
rangeWeek: 'This week',
|
|
138
146
|
chartTitleDay: 'Token usage per day',
|
|
139
147
|
chartTitleWeek: 'Token usage per week',
|
|
140
148
|
chartHint: 'Real tokens (input + output) of the model calls. Hover a column for details.',
|
|
141
149
|
chartTable: 'View as table',
|
|
142
|
-
|
|
143
|
-
|
|
150
|
+
chartModeLabel: 'Show',
|
|
151
|
+
chartModeTotal: 'Total',
|
|
152
|
+
chartModeCall: 'Per call',
|
|
153
|
+
chartHintCall: 'Mean tokens (input + output) per model call. Unlike the total, it doesn\'t depend on how much you used the agent: it drops when each call carries less context (shorter sessions, filtered command output), so it shows whether a change worked.',
|
|
154
|
+
perCallTip: (n) => `${n} tokens per call`,
|
|
155
|
+
noActivity: 'No activity',
|
|
156
|
+
chartCols: ['Day', 'Tokens', 'With cache', 'Calls', 'Per call'],
|
|
157
|
+
chartColsWeek: ['Week of', 'Tokens', 'With cache', 'Calls', 'Per call'],
|
|
144
158
|
chartCalls: (n) => plural(n, 'call', 'calls'),
|
|
145
159
|
weekOf: (d) => `week of ${d}`,
|
|
146
160
|
withCacheShort: 'with cache',
|
|
@@ -158,7 +172,7 @@ Options:
|
|
|
158
172
|
realUsage: (real, input, output, cached, calls) => `Real usage: ${real} tokens (input ${input} + output ${output}), ${cached} including cache, in ${plural(calls, 'model call', 'model calls')}`,
|
|
159
173
|
noLog: (n) => ` (${plural(n, 'conversation', 'conversations')} without a log)`,
|
|
160
174
|
topSessions: (n) => (n === 1 ? ' The session that uses the most:' : ` The ${n} sessions that use the most:`),
|
|
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`,
|
|
175
|
+
sessionLine: (id, calls, errors, cached, model, at, title) => `${title ? `"${title}" (${id})` : `conversation ${id}`}${at ? ` · ${at}` : ''}${model ? ` (${model})` : ''} ${plural(calls, 'call', 'calls')}${errors ? `, ${plural(errors, 'error', 'errors')}` : ''}, ${cached} with cache`,
|
|
162
176
|
top5: (pct, reads, rereads, rpct) => `The 5 largest sessions account for ${pct}. Reads: ${reads}, re-reads ${rereads} (${rpct})`,
|
|
163
177
|
pathHidden: '(path hidden, use --paths)',
|
|
164
178
|
byKindText: 'By file type:',
|
|
@@ -182,6 +196,7 @@ Opciones:
|
|
|
182
196
|
--since <AAAA-MM-DD> Solo conversaciones con actividad desde esa fecha
|
|
183
197
|
--days <n> Solo conversaciones con actividad en los últimos n días
|
|
184
198
|
--lang <en|es> Idioma de la salida (por defecto, el del sistema)
|
|
199
|
+
--debug-models Por conversación, de dónde sale el modelo de cada llamada (solo recuentos, para reportar fallos)
|
|
185
200
|
--dir <carpeta> Otra carpeta de historial (por defecto ${dir})
|
|
186
201
|
--config <fichero> Configuración (por defecto ~/.agy-insights.json si existe)
|
|
187
202
|
--help Esta ayuda
|
|
@@ -195,6 +210,11 @@ Opciones:
|
|
|
195
210
|
cannotRead: (f, msg) => `No se puede leer ${f}: ${msg}`,
|
|
196
211
|
mapDone: (tokens, file) => `\n~${tokens} tokens en cada petición. Pégalo en ${file} si te convence; agy-insights no lo escribe.`,
|
|
197
212
|
reportName: 'informe',
|
|
213
|
+
debugModels: {
|
|
214
|
+
header: ['conversación', 'llamadas', 'filas gen_metadata', 'filas con modelo', 'llamadas con modelo en transcript', 'desconocidas', 'modelo principal', 'ids de modelo no reconocidos'],
|
|
215
|
+
total: 'total',
|
|
216
|
+
note: 'Solo recuentos y nombres o ids de modelo: ni rutas, ni prompts, ni código. Pégalo en una incidencia si salen modelos desconocidos.',
|
|
217
|
+
},
|
|
198
218
|
done: (convs, found, out) => `${plural(convs, 'conversación analizada', 'conversaciones analizadas')}, ${plural(found, 'hallazgo', 'hallazgos')}.\nInforme: ${out}`,
|
|
199
219
|
|
|
200
220
|
kinds: {
|
|
@@ -243,7 +263,7 @@ Opciones:
|
|
|
243
263
|
longTitle: 'Sesiones muy largas',
|
|
244
264
|
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.`,
|
|
245
265
|
longSuggestion: 'Una conversación nueva por tarea, o /compact cuando cambias de tema.',
|
|
246
|
-
conversation: (id, at) => `conversación ${id}${at ? ` · ${at}` : ''}`,
|
|
266
|
+
conversation: (id, at, title) => `${title ? `«${title}» (${id})` : `conversación ${id}`}${at ? ` · ${at}` : ''}`,
|
|
247
267
|
steps: (n) => plural(n, 'paso', 'pasos'),
|
|
248
268
|
longUsage: (calls, cached) => ` · ${plural(calls, 'llamada', 'llamadas')} al modelo · tokens reales, ${cached} con caché`,
|
|
249
269
|
},
|
|
@@ -274,6 +294,7 @@ Opciones:
|
|
|
274
294
|
modelLine: (model, effort, calls, tokens, pct, perCall) => `${model} · ${effort} ${plural(calls, 'llamada', 'llamadas')}, ${tokens} tokens (${pct}), ~${perCall} por llamada`,
|
|
275
295
|
sessionModel: 'Modelo',
|
|
276
296
|
otherModels: (n) => ` +${n} más`,
|
|
297
|
+
modelsUnknown: (n) => `«desconocido»: ${n} llamadas de las que agy no anotó el modelo, ni en gen_metadata ni como cambio de modelo en el transcript. Si son muchas, ejecuta npx agy-insights@latest --debug-models y pega el resultado en una incidencia de GitHub (solo contiene recuentos).`,
|
|
277
298
|
efforts: { minimal: 'Mínimo', low: 'Bajo', medium: 'Medio', high: 'Alto', xhigh: 'Muy alto', 'extra high': 'Muy alto', max: 'Máximo' },
|
|
278
299
|
sessionsTitle: 'Sesiones (consumo real)',
|
|
279
300
|
sessionCols: ['Conversación', 'Cuándo', 'Modelo', 'Tokens', '%', 'Con caché', 'Llamadas', 'Errores'],
|
|
@@ -297,12 +318,19 @@ Opciones:
|
|
|
297
318
|
filterLabel: 'Periodo',
|
|
298
319
|
rangeAll: 'Todo el historial',
|
|
299
320
|
rangeDays: (n) => `Últimos ${n} días`,
|
|
321
|
+
rangeWeek: 'Esta semana',
|
|
300
322
|
chartTitleDay: 'Consumo de tokens por día',
|
|
301
323
|
chartTitleWeek: 'Consumo de tokens por semana',
|
|
302
324
|
chartHint: 'Tokens reales (entrada + salida) de las llamadas al modelo. Pasa el ratón por una columna para ver el detalle.',
|
|
303
325
|
chartTable: 'Ver como tabla',
|
|
304
|
-
|
|
305
|
-
|
|
326
|
+
chartModeLabel: 'Mostrar',
|
|
327
|
+
chartModeTotal: 'Total',
|
|
328
|
+
chartModeCall: 'Por llamada',
|
|
329
|
+
chartHintCall: 'Tokens medios (entrada + salida) por llamada al modelo. A diferencia del total, no depende de cuánto hayas usado el agente: baja cuando cada llamada lleva menos contexto (sesiones más cortas, salidas de comandos filtradas), así que muestra si un cambio ha funcionado.',
|
|
330
|
+
perCallTip: (n) => `${n} tokens por llamada`,
|
|
331
|
+
noActivity: 'Sin actividad',
|
|
332
|
+
chartCols: ['Día', 'Tokens', 'Con caché', 'Llamadas', 'Por llamada'],
|
|
333
|
+
chartColsWeek: ['Semana del', 'Tokens', 'Con caché', 'Llamadas', 'Por llamada'],
|
|
306
334
|
chartCalls: (n) => plural(n, 'llamada', 'llamadas'),
|
|
307
335
|
weekOf: (d) => `semana del ${d}`,
|
|
308
336
|
withCacheShort: 'con caché',
|
|
@@ -319,7 +347,7 @@ Opciones:
|
|
|
319
347
|
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`,
|
|
320
348
|
noLog: (n) => ` (${plural(n, 'conversación', 'conversaciones')} sin registro)`,
|
|
321
349
|
topSessions: (n) => (n === 1 ? ' La sesión que más gasta:' : ` Las ${n} sesiones que más gastan:`),
|
|
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é`,
|
|
350
|
+
sessionLine: (id, calls, errors, cached, model, at, title) => `${title ? `«${title}» (${id})` : `conversación ${id}`}${at ? ` · ${at}` : ''}${model ? ` (${model})` : ''} ${plural(calls, 'llamada', 'llamadas')}${errors ? `, ${plural(errors, 'error', 'errores')}` : ''}, ${cached} con caché`,
|
|
323
351
|
top5: (pct, reads, rereads, rpct) => `Las 5 sesiones más grandes suman el ${pct}. Lecturas: ${reads}, relecturas ${rereads} (${rpct})`,
|
|
324
352
|
pathHidden: '(ruta oculta, usa --paths)',
|
|
325
353
|
byKindText: 'Por tipo de fichero:',
|
package/lib/report.js
CHANGED
|
@@ -60,7 +60,7 @@ function sessionsTable(r) {
|
|
|
60
60
|
if (!rows.length) return '';
|
|
61
61
|
const max = Math.max(...rows.map((s) => billed(s.usage)), 1);
|
|
62
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
|
|
63
|
+
${rows.map((s) => `<tr><td><div class="bar" style="--w:${((100 * billed(s.usage)) / max).toFixed(1)}%"></div><span>${esc(s.title || s.id.slice(0, 8))}</span>${s.title ? `<span class="detail">${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>`;
|
|
@@ -76,12 +76,13 @@ function modelsTable(r) {
|
|
|
76
76
|
if (!rows.length || (rows.length === 1 && !rows[0].model)) return '';
|
|
77
77
|
const total = rows.reduce((s, m) => s + m.input + m.output, 0);
|
|
78
78
|
const max = Math.max(...rows.map((m) => m.input + m.output), 1);
|
|
79
|
+
const unknown = rows.find((m) => !m.model);
|
|
79
80
|
return `<section class="models"><h2>${t.report.modelsTitle}</h2><p class="hint">${t.report.modelsHint}</p>
|
|
80
81
|
<div class="scroll"><table><thead><tr>${t.report.modelCols.map((c, i) => `<th${i > 1 ? ' class="num"' : ''}>${c}</th>`).join('')}</tr></thead><tbody>
|
|
81
82
|
${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
83
|
<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
84
|
<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
|
|
85
|
+
</tbody></table></div>${unknown ? `<p class="hint">${esc(t.report.modelsUnknown(fmt(unknown.calls)))}</p>` : ''}</section>`;
|
|
85
86
|
}
|
|
86
87
|
|
|
87
88
|
// --- Usage chart ---
|
|
@@ -114,12 +115,33 @@ function buckets(r) {
|
|
|
114
115
|
return { weekly, rows: [...byKey.values()] };
|
|
115
116
|
}
|
|
116
117
|
|
|
118
|
+
// Mean tokens per call: unlike the total, it doesn't follow how much the agent was used that day.
|
|
119
|
+
const perCall = (x) => (x.calls ? x.billed / x.calls : 0);
|
|
120
|
+
|
|
117
121
|
function chart(r) {
|
|
118
122
|
const b = buckets(r);
|
|
119
123
|
if (!b) return '';
|
|
124
|
+
const title = b.weekly ? t.report.chartTitleWeek : t.report.chartTitleDay;
|
|
125
|
+
const rows = b.rows.filter((x) => x.calls);
|
|
126
|
+
return `<section class="chart">
|
|
127
|
+
<h2>${title}</h2>
|
|
128
|
+
<div class="mode" role="group" aria-label="${esc(t.report.chartModeLabel)}">
|
|
129
|
+
<button type="button" data-mode="total" aria-pressed="true">${t.report.chartModeTotal}</button><button type="button" data-mode="call" aria-pressed="false">${t.report.chartModeCall}</button></div>
|
|
130
|
+
<p class="hint" data-mode="total">${t.report.chartHint}</p>
|
|
131
|
+
<p class="hint" data-mode="call" hidden>${t.report.chartHintCall}</p>
|
|
132
|
+
<div class="plot" data-mode="total">${plot(b, (x) => x.billed, (x) => `${fmt(x.billed)} tokens`, (x) => `${fmt(x.billed + x.cache)} ${t.report.withCacheShort}`, title)}</div>
|
|
133
|
+
<div class="plot" data-mode="call" hidden>${plot(b, perCall, (x) => t.report.perCallTip(fmt(perCall(x))), (x) => `${fmt(perCall(x) + x.cache / x.calls)} ${t.report.withCacheShort}`, `${title} · ${t.report.chartModeCall}`)}</div>
|
|
134
|
+
<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>
|
|
135
|
+
${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><td class="num">${fmt(perCall(x))}</td></tr>`).join('')}
|
|
136
|
+
</tbody></table></div></details>
|
|
137
|
+
</section>`;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// One column chart of `value` per bucket; `tip` and `cache` are the tooltip's headline and cache line.
|
|
141
|
+
function plot(b, value, tip, cache, label) {
|
|
120
142
|
const W = 900, H = 240, m = { l: 56, r: 8, t: 12, b: 28 };
|
|
121
143
|
const pw = W - m.l - m.r, ph = H - m.t - m.b;
|
|
122
|
-
const top = niceMax(Math.max(...b.rows.map(
|
|
144
|
+
const top = niceMax(Math.max(...b.rows.map(value)));
|
|
123
145
|
const band = pw / b.rows.length;
|
|
124
146
|
const bw = Math.max(1, Math.min(24, band - 2));
|
|
125
147
|
const y = (v) => m.t + ph - (v / top) * ph;
|
|
@@ -129,28 +151,20 @@ function chart(r) {
|
|
|
129
151
|
<text x="${m.l - 8}" y="${y(v) + 4}" text-anchor="end" class="axis">${esc(compact(v))}</text>`).join('');
|
|
130
152
|
// Rounded data end (4px), square at the baseline.
|
|
131
153
|
const bars = b.rows.map((x, i) => {
|
|
132
|
-
const h = (x
|
|
154
|
+
const h = (value(x) / top) * ph;
|
|
133
155
|
if (h <= 0) return '';
|
|
134
156
|
const x0 = m.l + i * band + (band - bw) / 2, y0 = m.t + ph - h, rr = Math.min(4, h, bw / 2);
|
|
135
157
|
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
158
|
}).join('');
|
|
137
159
|
// Hit targets: the whole band, so a 2px column is as easy to hover as a 24px one.
|
|
138
160
|
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(
|
|
161
|
+
data-when="${esc(when(x))}" data-v="${esc(x.calls ? tip(x) : t.report.noActivity)}" data-c="${esc(x.calls ? cache(x) : '')}" data-n="${esc(x.calls ? t.report.chartCalls(x.calls) : '')}"/>`).join('');
|
|
140
162
|
const every = Math.max(1, Math.ceil(b.rows.length / 6));
|
|
141
163
|
// A date every few columns, plus the last one when it isn't crowded by the previous label.
|
|
142
164
|
const last = b.rows.length - 1;
|
|
143
165
|
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
166
|
|
|
145
|
-
|
|
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>`;
|
|
167
|
+
return `<svg viewBox="0 0 ${W} ${H}" role="img" aria-label="${esc(label)}">${grid}${bars}${ticks}${hits}</svg>`;
|
|
154
168
|
}
|
|
155
169
|
|
|
156
170
|
// --- Page ---
|
|
@@ -218,6 +232,12 @@ function htmlViews(views, { scope }) {
|
|
|
218
232
|
.filter button:hover { background: var(--accent-soft); }
|
|
219
233
|
.filter button[aria-pressed="true"] { background: var(--accent); border-color: var(--accent); color: var(--surface); font-weight: 600; }
|
|
220
234
|
.filter button:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
235
|
+
.mode { display: inline-flex; margin: 0 0 0 12px; vertical-align: 3px; border: 1px solid var(--line); border-radius: 6px; overflow: hidden; }
|
|
236
|
+
.mode button { font: 13px system-ui, sans-serif; color: var(--fg); background: var(--surface); border: 0; padding: 3px 10px; cursor: pointer; }
|
|
237
|
+
.mode button + button { border-left: 1px solid var(--line); }
|
|
238
|
+
.mode button:hover { background: var(--accent-soft); }
|
|
239
|
+
.mode button[aria-pressed="true"] { background: var(--accent-soft); color: var(--accent); font-weight: 600; }
|
|
240
|
+
.mode button:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
|
|
221
241
|
.tiles { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 12px; margin: 16px 0 20px; }
|
|
222
242
|
.tiles div { background: var(--surface); border: 1px solid var(--line); border-radius: 8px; padding: 12px 14px; }
|
|
223
243
|
.tiles b { display: block; font-size: 24px; font-variant-numeric: tabular-nums; } .tiles span { color: var(--muted); font-size: 14px; }
|
|
@@ -270,6 +290,13 @@ function htmlViews(views, { scope }) {
|
|
|
270
290
|
for (const s of document.querySelectorAll('.view')) s.hidden = s.dataset.view !== v.dataset.view;
|
|
271
291
|
return;
|
|
272
292
|
}
|
|
293
|
+
// Chart mode: the same in every period, so switching periods keeps it.
|
|
294
|
+
const m = e.target.closest('.mode button');
|
|
295
|
+
if (m) {
|
|
296
|
+
for (const b of document.querySelectorAll('.mode button')) b.setAttribute('aria-pressed', String(b.dataset.mode === m.dataset.mode));
|
|
297
|
+
for (const p of document.querySelectorAll('.chart [data-mode]:not(button)')) p.hidden = p.dataset.mode !== m.dataset.mode;
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
273
300
|
const b = e.target.closest('.copy'); if (!b) return;
|
|
274
301
|
const pre = b.parentElement.querySelector('pre');
|
|
275
302
|
const ok = () => { b.textContent = ${JSON.stringify(t.report.copied)}; setTimeout(() => (b.textContent = ${JSON.stringify(t.report.copy)}), 1500); };
|
|
@@ -284,9 +311,10 @@ function htmlViews(views, { scope }) {
|
|
|
284
311
|
const h = e.target.closest && e.target.closest('.hit');
|
|
285
312
|
if (!h) { tip.hidden = true; return; }
|
|
286
313
|
const [v, when, rest] = tip.children;
|
|
287
|
-
v.textContent = h.dataset.v
|
|
314
|
+
v.textContent = h.dataset.v;
|
|
288
315
|
when.textContent = h.dataset.when;
|
|
289
|
-
rest.textContent = h.dataset.n + ' · ' + h.dataset.c
|
|
316
|
+
rest.textContent = h.dataset.c ? h.dataset.n + ' · ' + h.dataset.c : h.dataset.n;
|
|
317
|
+
rest.hidden = !rest.textContent;
|
|
290
318
|
tip.hidden = false;
|
|
291
319
|
const x = Math.min(e.clientX + 14, innerWidth - tip.offsetWidth - 8);
|
|
292
320
|
tip.style.left = x + 'px';
|
|
@@ -316,7 +344,7 @@ function text(r, found, { scope, paths }) {
|
|
|
316
344
|
const top = r.sessions.filter((s) => s.usage).slice(0, 5);
|
|
317
345
|
out.push(t.report.topSessions(top.length));
|
|
318
346
|
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), when(s.start, s.end))}`);
|
|
347
|
+
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), s.title)}`);
|
|
320
348
|
}
|
|
321
349
|
}
|
|
322
350
|
out.push(t.report.top5(pct(top5, r.tokens), r.reads, r.rereads, pct(r.rereads, r.reads)));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agy-insights",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
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": [
|