agy-insights 0.8.0 → 0.8.1
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 +1 -1
- package/README.md +1 -1
- package/agy-insights.js +4 -3
- package/lib/findings.js +3 -2
- package/lib/history.js +21 -9
- package/lib/i18n.js +4 -4
- package/lib/report.js +7 -4
- 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.8.
|
|
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.1`.
|
|
40
40
|
|
|
41
41
|
**Instalándolo globalmente:**
|
|
42
42
|
|
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.8.
|
|
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.1`.
|
|
40
40
|
|
|
41
41
|
**Globally:**
|
|
42
42
|
|
package/agy-insights.js
CHANGED
|
@@ -86,14 +86,15 @@ function sinceTime({ since, days }) {
|
|
|
86
86
|
// Tab-separated table of where each conversation's call models came from, for bug reports.
|
|
87
87
|
function debugModels(r) {
|
|
88
88
|
const d = t.debugModels;
|
|
89
|
-
const sum = { rows: 0, rowsWithModel: 0, calls: 0, fromTranscript: 0, unknown: 0 };
|
|
89
|
+
const sum = { rows: 0, rowsWithModel: 0, calls: 0, fromTranscript: 0, unknown: 0, unmetered: 0 };
|
|
90
90
|
const lines = [d.header.join('\t')];
|
|
91
91
|
for (const s of r.sessions) {
|
|
92
92
|
const m = s.modelSources;
|
|
93
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(' | ') : ''
|
|
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(' | ') : '',
|
|
95
|
+
m.unmetered || '', m.unmetered ? m.unmeteredKeys.join(' ') : ''].join('\t'));
|
|
95
96
|
}
|
|
96
|
-
lines.push([d.total, sum.calls, sum.rows, sum.rowsWithModel, sum.fromTranscript, sum.unknown, '', ''].join('\t'), '', d.note, '');
|
|
97
|
+
lines.push([d.total, sum.calls, sum.rows, sum.rowsWithModel, sum.fromTranscript, sum.unknown, '', '', sum.unmetered || '', ''].join('\t'), '', d.note, '');
|
|
97
98
|
return lines.join('\n');
|
|
98
99
|
}
|
|
99
100
|
|
package/lib/findings.js
CHANGED
|
@@ -10,7 +10,7 @@ const DEFAULTS = { crossSessionMin: 3, heavyOutputTokens: 2000, longSessionToken
|
|
|
10
10
|
// Suggested .antigravityignore patterns for the built-in non-source types.
|
|
11
11
|
const KIND_PATTERNS = {
|
|
12
12
|
'node_modules': ['node_modules/'],
|
|
13
|
-
'build': ['bin/', 'obj/', 'dist/', 'build/', 'out/', 'target/', 'coverage/'],
|
|
13
|
+
'build': ['bin/', 'obj/', 'dist/', 'build/', 'out/', 'target/', 'coverage/', 'allure-results/', 'allure-report/'],
|
|
14
14
|
'lockfiles': ['*.lock', 'package-lock.json', 'pnpm-lock.yaml', 'packages.lock.json'],
|
|
15
15
|
'logs': ['*.log', '*.map', '*.min.js', '*.min.css'],
|
|
16
16
|
'generated': ['*.designer.cs', '*.g.cs', 'Migrations/'],
|
|
@@ -126,8 +126,9 @@ function findings(r, opts = {}) {
|
|
|
126
126
|
const size = (s) => (s.usage ? s.usage.input + s.usage.output : s.tokens);
|
|
127
127
|
const long = r.sessions.filter((s) => size(s) >= o.longSessionTokens);
|
|
128
128
|
if (long.length) {
|
|
129
|
+
// `exact`: every one has real usage, so the header shows the same figures as its items, as a share of real usage.
|
|
129
130
|
out.push({
|
|
130
|
-
id: 'long-sessions', level: 'info', title: f.longTitle, tokens: sum(long, (s) => s.
|
|
131
|
+
id: 'long-sessions', level: 'info', title: f.longTitle, tokens: sum(long, size), exact: long.every((s) => s.usage),
|
|
131
132
|
why: f.longWhy(long.length, num(o.longSessionTokens)), suggestion: f.longSuggestion,
|
|
132
133
|
items: long.slice(0, 5).map((s) => ({
|
|
133
134
|
// Real tokens when agy logged them (`exact`: shown without "~"), else the size estimate.
|
package/lib/history.js
CHANGED
|
@@ -154,7 +154,7 @@ function kind(p, { inRepo = false } = {}) {
|
|
|
154
154
|
// are never reported as generated or proposed for exclusion, whatever their names look like.
|
|
155
155
|
if (/(^|\/)\.gemini\//i.test(p)) return 'agent';
|
|
156
156
|
if (/(^|\/)node_modules\//.test(p)) return 'node_modules';
|
|
157
|
-
if (!inRepo && /(^|\/)(dist|build|out|target|coverage|\.next|bin|obj)\//i.test(p)) return 'build';
|
|
157
|
+
if (!inRepo && /(^|\/)(dist|build|out|target|coverage|allure-results|allure-report|\.next|bin|obj)\//i.test(p)) return 'build';
|
|
158
158
|
if (/(package-lock\.json|yarn\.lock|pnpm-lock\.yaml|packages\.lock\.json|\.lock)$/.test(p)) return 'lockfiles';
|
|
159
159
|
if (/\.(log|map)$|\.min\.(js|css)$/.test(p)) return 'logs';
|
|
160
160
|
if (/(^|\/)(tests?|__tests__|specs?)\//i.test(p) || /\.(test|spec|tests?)\.\w+$/i.test(p) || /Tests?\//.test(p)) return 'tests';
|
|
@@ -221,7 +221,10 @@ function realUsage(dir, id) {
|
|
|
221
221
|
try { text = fs.readFileSync(file, 'utf8'); } catch { return null; }
|
|
222
222
|
// `days`: the same figures per local day of each model call (calls without a time go under null).
|
|
223
223
|
// `perCall`: each call's figures, in order, and the model selected in the transcript when it was made.
|
|
224
|
-
|
|
224
|
+
// `calls` counts only the calls that carry token counts; some logs (seen with the VS Code plugin) list
|
|
225
|
+
// calls without them, and counting those as 0 tokens would drag every per-call figure down.
|
|
226
|
+
// `unmetered`: those other calls, and `unmeteredKeys` the field names they do have, for --debug-models.
|
|
227
|
+
const u = { calls: 0, input: 0, cache: 0, output: 0, errors: 0, start: null, end: null, days: new Map(), perCall: [], unmetered: 0, unmeteredKeys: new Set() };
|
|
225
228
|
let selected = null;
|
|
226
229
|
u.unrecognized = new Set();
|
|
227
230
|
for (const line of text.split('\n')) {
|
|
@@ -236,11 +239,17 @@ function realUsage(dir, id) {
|
|
|
236
239
|
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
240
|
}
|
|
238
241
|
if (e.type !== 'PLANNER_RESPONSE') continue;
|
|
242
|
+
if (!['input_tokens', 'cache_read_tokens', 'output_tokens'].some((k) => k in e)) {
|
|
243
|
+
u.unmetered++;
|
|
244
|
+
for (const k of Object.keys(e)) if (k.length < 40) u.unmeteredKeys.add(k);
|
|
245
|
+
u.perCall.push({ metered: false, model: selected });
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
239
248
|
u.calls++;
|
|
240
249
|
u.input += Number(e.input_tokens) || 0;
|
|
241
250
|
u.cache += Number(e.cache_read_tokens) || 0;
|
|
242
251
|
u.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 });
|
|
252
|
+
u.perCall.push({ metered: true, input: Number(e.input_tokens) || 0, cache: Number(e.cache_read_tokens) || 0, output: Number(e.output_tokens) || 0, model: selected });
|
|
244
253
|
const k = at ? dayKey(at) : null;
|
|
245
254
|
const d = u.days.get(k) || { calls: 0, input: 0, cache: 0, output: 0 };
|
|
246
255
|
d.calls++;
|
|
@@ -317,9 +326,11 @@ async function analyze({ dir = DEFAULT_DIR, workspaces = [], customKinds = {}, s
|
|
|
317
326
|
for (const f of dbs) {
|
|
318
327
|
// When it happened: the transcript's step times, else the database's last write.
|
|
319
328
|
const id = f.replace(/\.db$/, '');
|
|
320
|
-
const
|
|
321
|
-
const end =
|
|
322
|
-
const start =
|
|
329
|
+
const log = realUsage(dir, id);
|
|
330
|
+
const end = log?.end ?? fs.statSync(path.join(dir, f)).mtimeMs;
|
|
331
|
+
const start = log?.start ?? end;
|
|
332
|
+
// Real usage only when the log has token counts; otherwise the session is shown with the estimate.
|
|
333
|
+
const usage = log?.calls ? log : null;
|
|
323
334
|
if (since && end < since) { r.olderSkipped++; continue; }
|
|
324
335
|
let db;
|
|
325
336
|
try { db = await openDb(path.join(dir, f), tmp); } catch { r.unreadable++; continue; }
|
|
@@ -395,7 +406,7 @@ async function analyze({ dir = DEFAULT_DIR, workspaces = [], customKinds = {}, s
|
|
|
395
406
|
// calls, so the model selected in the transcript is used instead, or the conversation's only model.
|
|
396
407
|
const metaStrs = db.all('select data from gen_metadata order by idx').map(([blob]) => collect(blob));
|
|
397
408
|
const metaRows = metaStrs.map(modelOf);
|
|
398
|
-
const calls =
|
|
409
|
+
const calls = log ? log.perCall : metaRows.map(() => null);
|
|
399
410
|
const aligned = metaRows.length === calls.length;
|
|
400
411
|
const rowModels = new Set(metaRows.filter((m) => m.model).map(modelKey));
|
|
401
412
|
const onlyModel = rowModels.size === 1 ? metaRows.find((m) => m.model) : null;
|
|
@@ -412,7 +423,7 @@ async function analyze({ dir = DEFAULT_DIR, workspaces = [], customKinds = {}, s
|
|
|
412
423
|
e.calls++;
|
|
413
424
|
if (!seenModels.has(k)) { seenModels.add(k); e.convs++; }
|
|
414
425
|
const c = calls[i];
|
|
415
|
-
if (c) { e.known++; e.input += c.input; e.cache += c.cache; e.output += c.output; }
|
|
426
|
+
if (c?.metered) { e.known++; e.input += c.input; e.cache += c.cache; e.output += c.output; }
|
|
416
427
|
models.set(k, e);
|
|
417
428
|
if (m.model) counts.set(k, (counts.get(k) || 0) + 1);
|
|
418
429
|
});
|
|
@@ -426,7 +437,8 @@ async function analyze({ dir = DEFAULT_DIR, workspaces = [], customKinds = {}, s
|
|
|
426
437
|
// the short strings that look like a model id (to spot a new format). Never prompts, paths or code.
|
|
427
438
|
modelSources: { rows: metaRows.length, rowsWithModel: metaRows.filter((m) => m.model).length, calls: calls.length,
|
|
428
439
|
fromTranscript: calls.filter((c) => c?.model).length, unknown: callModels.filter((m) => !m.model).length,
|
|
429
|
-
|
|
440
|
+
unmetered: log?.unmetered || 0, unmeteredKeys: [...(log?.unmeteredKeys || [])].sort(),
|
|
441
|
+
candidates: [...new Set([...(log?.unrecognized || []),
|
|
430
442
|
...metaStrs.filter((_, i) => !metaRows[i].model).flat().filter(looksLikeModel)])].slice(0, 10) },
|
|
431
443
|
});
|
|
432
444
|
r.tokens += convBytes / BYTES_PER_TOKEN;
|
package/lib/i18n.js
CHANGED
|
@@ -35,7 +35,7 @@ Options:
|
|
|
35
35
|
mapDone: (tokens, file) => `\n~${tokens} tokens on every request. Paste it into ${file} if you like it; agy-insights does not write it.`,
|
|
36
36
|
reportName: 'report',
|
|
37
37
|
debugModels: {
|
|
38
|
-
header: ['conversation', 'calls', 'gen_metadata rows', 'rows with model', 'calls with model in transcript', 'unknown', 'main model', 'unrecognized model ids'],
|
|
38
|
+
header: ['conversation', 'calls', 'gen_metadata rows', 'rows with model', 'calls with model in transcript', 'unknown', 'main model', 'unrecognized model ids', 'calls without tokens', 'fields of those calls'],
|
|
39
39
|
total: 'total',
|
|
40
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
41
|
},
|
|
@@ -162,7 +162,7 @@ Options:
|
|
|
162
162
|
since: (date) => `active since ${date}`,
|
|
163
163
|
olderSkipped: (n) => `${plural(n, 'older conversation', 'older conversations')} left out by the date filter`,
|
|
164
164
|
periodLine: (period, since) => `Period: ${period}${since ? ` (filter: ${since})` : ''}`,
|
|
165
|
-
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` : ''}.`,
|
|
165
|
+
footerReal: (missing) => `Real tokens: the ones Antigravity records in each conversation's log (subagents included)${missing ? `; ${plural(missing, 'conversation has', 'conversations have')} no token counts in its log and ${missing === 1 ? 'is' : 'are'} not counted` : ''}.`,
|
|
166
166
|
footerCache: 'It is unknown whether the quota counts cache the same as new input, so both figures are shown.',
|
|
167
167
|
footerEstimate: '~tokens = stored size of each step / 4: useful to compare files and folders with each other.',
|
|
168
168
|
footerPrivacy: 'The history was read from a copy and was not modified. This page contains paths from your projects: review it before sharing.',
|
|
@@ -211,7 +211,7 @@ Opciones:
|
|
|
211
211
|
mapDone: (tokens, file) => `\n~${tokens} tokens en cada petición. Pégalo en ${file} si te convence; agy-insights no lo escribe.`,
|
|
212
212
|
reportName: 'informe',
|
|
213
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'],
|
|
214
|
+
header: ['conversación', 'llamadas', 'filas gen_metadata', 'filas con modelo', 'llamadas con modelo en transcript', 'desconocidas', 'modelo principal', 'ids de modelo no reconocidos', 'llamadas sin tokens', 'campos de esas llamadas'],
|
|
215
215
|
total: 'total',
|
|
216
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
217
|
},
|
|
@@ -338,7 +338,7 @@ Opciones:
|
|
|
338
338
|
since: (date) => `con actividad desde el ${date}`,
|
|
339
339
|
olderSkipped: (n) => `${plural(n, 'conversación anterior queda fuera', 'conversaciones anteriores quedan fuera')} por el filtro de fechas`,
|
|
340
340
|
periodLine: (period, since) => `Periodo: ${period}${since ? ` (filtro: ${since})` : ''}`,
|
|
341
|
-
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`}` : ''}.`,
|
|
341
|
+
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 tokens en su registro y no cuenta' : `${missing} conversaciones no tienen tokens en su registro y no cuentan`}` : ''}.`,
|
|
342
342
|
footerCache: 'No se sabe si la cuota cuenta la caché igual que la entrada nueva: por eso se muestran las dos cifras.',
|
|
343
343
|
footerEstimate: '~tokens = tamaño guardado de cada paso / 4: sirve para comparar ficheros y carpetas entre sí.',
|
|
344
344
|
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.',
|
package/lib/report.js
CHANGED
|
@@ -38,9 +38,12 @@ function item(it) {
|
|
|
38
38
|
<span class="num">${it.tokens ? `${it.exact ? '' : '~'}${fmt(it.tokens)}` : ''}</span></div>${extra}</li>`;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
// A finding's share: of real usage when its figure is real (`exact`), else of the estimated context.
|
|
42
|
+
const share = (f, r) => (f.exact ? billed(r.usage) : r.tokens);
|
|
43
|
+
|
|
44
|
+
function finding(f, r) {
|
|
42
45
|
return `<section class="finding ${f.level}">
|
|
43
|
-
<h3>${esc(f.title)}${f.tokens ? ` <span class="tok"
|
|
46
|
+
<h3>${esc(f.title)}${f.tokens ? ` <span class="tok">${f.exact ? '' : '~'}${fmt(f.tokens)} tokens · ${pct(f.tokens, share(f, r))}</span>` : ''}</h3>
|
|
44
47
|
<p>${esc(f.why)}</p><p class="hint">${esc(f.suggestion)}</p>
|
|
45
48
|
${f.items.length ? `<ul class="items">${f.items.map(item).join('')}</ul>` : ''}
|
|
46
49
|
</section>`;
|
|
@@ -188,7 +191,7 @@ function body(r, found, scope) {
|
|
|
188
191
|
${modelsTable(r)}
|
|
189
192
|
<h2>${t.report.happening}</h2>
|
|
190
193
|
<p class="hint">${t.report.happeningHint}</p>
|
|
191
|
-
${found.length ? found.map((f) => finding(f, r
|
|
194
|
+
${found.length ? found.map((f) => finding(f, r)).join('') : `<p>${t.report.nothing}</p>`}
|
|
192
195
|
<details class="detail-all"><summary><h2>${t.report.details}</h2></summary>
|
|
193
196
|
${sessionsTable(r)}
|
|
194
197
|
${table(t.report.byProject, r.projects, r.tokens, (x) => x.project || t.report.outsideGit)}
|
|
@@ -358,7 +361,7 @@ function text(r, found, { scope, paths }) {
|
|
|
358
361
|
}
|
|
359
362
|
}
|
|
360
363
|
for (const f of found) {
|
|
361
|
-
out.push('', `${f.level === 'warn' ? '⚠' : '•'} ${f.title}${f.tokens ? ` (
|
|
364
|
+
out.push('', `${f.level === 'warn' ? '⚠' : '•'} ${f.title}${f.tokens ? ` (${f.exact ? '' : '~'}${fmt(f.tokens)} tokens, ${pct(f.tokens, share(f, r))})` : ''}`,
|
|
362
365
|
` ${f.why}`, ` → ${f.suggestion}`);
|
|
363
366
|
// Only file and folder labels are paths; commands and session ids are always shown.
|
|
364
367
|
const hasPaths = ['rereads', 'non-source', 'cross-session'].includes(f.id);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agy-insights",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.1",
|
|
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": [
|