agy-insights 0.7.1 → 0.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.es.md +1 -1
- package/README.md +1 -1
- package/lib/findings.js +20 -5
- package/lib/history.js +1 -1
- package/lib/i18n.js +18 -9
- package/lib/report.js +7 -7
- package/package.json +1 -1
package/README.es.md
CHANGED
|
@@ -36,7 +36,7 @@ npx agy-insights
|
|
|
36
36
|
npx agy-insights@latest --text
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
-
`@latest` hace que npx compruebe cada vez si hay una versión nueva. Sin él, una copia instalada globalmente (`npm install -g`) o en el proyecto actual tiene prioridad y no se actualiza nunca. Para fijar una versión concreta: `npx agy-insights@0.7.
|
|
39
|
+
`@latest` hace que npx compruebe cada vez si hay una versión nueva. Sin él, una copia instalada globalmente (`npm install -g`) o en el proyecto actual tiene prioridad y no se actualiza nunca. Para fijar una versión concreta: `npx agy-insights@0.7.2`.
|
|
40
40
|
|
|
41
41
|
**Instalándolo globalmente:**
|
|
42
42
|
|
package/README.md
CHANGED
|
@@ -36,7 +36,7 @@ npx agy-insights
|
|
|
36
36
|
npx agy-insights@latest --text
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
-
`@latest` makes npx check for the newest version every time. Without it, a copy installed globally (`npm install -g`) or in the current project takes precedence and is never updated. To pin a version instead: `npx agy-insights@0.7.
|
|
39
|
+
`@latest` makes npx check for the newest version every time. Without it, a copy installed globally (`npm install -g`) or in the current project takes precedence and is never updated. To pin a version instead: `npx agy-insights@0.7.2`.
|
|
40
40
|
|
|
41
41
|
**Globally:**
|
|
42
42
|
|
package/lib/findings.js
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
// matters, and which official Antigravity mechanism could address it. Findings only describe and propose
|
|
3
3
|
// (see fixes.js); applying anything is always the developer's decision.
|
|
4
4
|
const fixes = require('./fixes');
|
|
5
|
-
const {
|
|
5
|
+
const { repoFiles } = require('./history');
|
|
6
|
+
const { t, num, when, kindLabel } = require('./i18n');
|
|
6
7
|
|
|
7
8
|
const DEFAULTS = { crossSessionMin: 3, heavyOutputTokens: 2000, longSessionTokens: 100000 };
|
|
8
9
|
|
|
@@ -15,6 +16,16 @@ const KIND_PATTERNS = {
|
|
|
15
16
|
'generated': ['*.designer.cs', '*.g.cs', 'Migrations/'],
|
|
16
17
|
};
|
|
17
18
|
|
|
19
|
+
// A type's patterns for one project, without the build folder names that git keeps in that repo: if
|
|
20
|
+
// build/ holds sources there (a webpack config), "build/" must not be proposed for exclusion.
|
|
21
|
+
function projectPatterns(root, kind) {
|
|
22
|
+
const files = kind === 'build' ? repoFiles(root) : null;
|
|
23
|
+
if (!files) return KIND_PATTERNS[kind];
|
|
24
|
+
const folders = new Set();
|
|
25
|
+
for (const file of files) for (const part of file.split('/').slice(0, -1)) folders.add(part);
|
|
26
|
+
return KIND_PATTERNS[kind].filter((p) => !(p.endsWith('/') && folders.has(p.slice(0, -1).toLowerCase())));
|
|
27
|
+
}
|
|
28
|
+
|
|
18
29
|
const sum = (xs, f) => xs.reduce((s, x) => s + f(x), 0);
|
|
19
30
|
const where = (x) => (x.project ? `${x.project} › ${x.rel}` : x.abs);
|
|
20
31
|
|
|
@@ -67,11 +78,13 @@ function findings(r, opts = {}) {
|
|
|
67
78
|
perProject.set(k, e);
|
|
68
79
|
}
|
|
69
80
|
for (const e of perProject.values()) {
|
|
81
|
+
const patterns = projectPatterns(e.root, e.kind);
|
|
82
|
+
if (!patterns.length) continue;
|
|
70
83
|
items.push({
|
|
71
84
|
label: f.kindPattern(e.project, kindLabel(e.kind)), tokens: e.tokens, detail: f.kindPatternDetail,
|
|
72
85
|
fixes: [
|
|
73
|
-
fixes.ignore(e.root,
|
|
74
|
-
fixes.rule(e.root, f.ruleKind(kindLabel(e.kind),
|
|
86
|
+
fixes.ignore(e.root, patterns),
|
|
87
|
+
fixes.rule(e.root, f.ruleKind(kindLabel(e.kind), patterns.join(', '))),
|
|
75
88
|
],
|
|
76
89
|
});
|
|
77
90
|
}
|
|
@@ -117,9 +130,11 @@ function findings(r, opts = {}) {
|
|
|
117
130
|
id: 'long-sessions', level: 'info', title: f.longTitle, tokens: sum(long, (s) => s.tokens),
|
|
118
131
|
why: f.longWhy(long.length, num(o.longSessionTokens)), suggestion: f.longSuggestion,
|
|
119
132
|
items: long.slice(0, 5).map((s) => ({
|
|
120
|
-
|
|
133
|
+
// Real tokens when agy logged them (`exact`: shown without "~"), else the size estimate.
|
|
134
|
+
label: f.conversation(s.id.slice(0, 8), when(s.start, s.end)), fixes: [],
|
|
135
|
+
tokens: s.usage ? s.usage.input + s.usage.output : s.tokens, exact: Boolean(s.usage),
|
|
121
136
|
detail: f.steps(s.steps) + (s.usage
|
|
122
|
-
? f.longUsage(s.usage.calls, num(s.usage.input + s.usage.
|
|
137
|
+
? f.longUsage(s.usage.calls, num(s.usage.input + s.usage.cache + s.usage.output))
|
|
123
138
|
: ''),
|
|
124
139
|
})),
|
|
125
140
|
});
|
package/lib/history.js
CHANGED
|
@@ -386,4 +386,4 @@ async function analyze({ dir = DEFAULT_DIR, workspaces = [], customKinds = {}, s
|
|
|
386
386
|
};
|
|
387
387
|
}
|
|
388
388
|
|
|
389
|
-
module.exports = { analyze, DEFAULT_DIR, kind, NON_SOURCE, realUsage, dayKey, modelOf };
|
|
389
|
+
module.exports = { analyze, DEFAULT_DIR, kind, NON_SOURCE, realUsage, dayKey, modelOf, repoFiles };
|
package/lib/i18n.js
CHANGED
|
@@ -81,9 +81,9 @@ Options:
|
|
|
81
81
|
longTitle: 'Very long sessions',
|
|
82
82
|
longWhy: (n, min) => `${plural(n, 'conversation exceeds', 'conversations exceed')} ${min} tokens. In a long session, everything accumulated is resent on every turn.`,
|
|
83
83
|
longSuggestion: 'A new conversation per task, or /compact when you change topic.',
|
|
84
|
-
conversation: (id) => `conversation ${id}`,
|
|
84
|
+
conversation: (id, at) => `conversation ${id}${at ? ` · ${at}` : ''}`,
|
|
85
85
|
steps: (n) => plural(n, 'step', 'steps'),
|
|
86
|
-
longUsage: (calls,
|
|
86
|
+
longUsage: (calls, cached) => ` · ${plural(calls, 'model call', 'model calls')} · real tokens, ${cached} with cache`,
|
|
87
87
|
},
|
|
88
88
|
|
|
89
89
|
map: {
|
|
@@ -114,7 +114,7 @@ Options:
|
|
|
114
114
|
otherModels: (n) => ` +${n} more`,
|
|
115
115
|
efforts: { minimal: 'Minimal', low: 'Low', medium: 'Medium', high: 'High', xhigh: 'Extra high', 'extra high': 'Extra high', max: 'Max' },
|
|
116
116
|
sessionsTitle: 'Sessions (real usage)',
|
|
117
|
-
sessionCols: ['Conversation', 'Model', 'Tokens', '%', 'With cache', 'Calls', 'Errors'],
|
|
117
|
+
sessionCols: ['Conversation', 'When', 'Model', 'Tokens', '%', 'With cache', 'Calls', 'Errors'],
|
|
118
118
|
noConversations: (scope, dir) => `No conversations${scope ? ` for <code>${scope}</code>` : ''} in <code>${dir}</code>.`,
|
|
119
119
|
tileReal: 'real tokens (input + output)',
|
|
120
120
|
tileCache: (calls) => `including cache · ${plural(calls, 'call', 'calls')}`,
|
|
@@ -158,7 +158,7 @@ Options:
|
|
|
158
158
|
realUsage: (real, input, output, cached, calls) => `Real usage: ${real} tokens (input ${input} + output ${output}), ${cached} including cache, in ${plural(calls, 'model call', 'model calls')}`,
|
|
159
159
|
noLog: (n) => ` (${plural(n, 'conversation', 'conversations')} without a log)`,
|
|
160
160
|
topSessions: (n) => (n === 1 ? ' The session that uses the most:' : ` The ${n} sessions that use the most:`),
|
|
161
|
-
sessionLine: (id, calls, errors, cached, model) => `conversation ${id}${model ? ` (${model})` : ''} ${plural(calls, 'call', 'calls')}${errors ? `, ${plural(errors, 'error', 'errors')}` : ''}, ${cached} with cache`,
|
|
161
|
+
sessionLine: (id, calls, errors, cached, model, at) => `conversation ${id}${at ? ` · ${at}` : ''}${model ? ` (${model})` : ''} ${plural(calls, 'call', 'calls')}${errors ? `, ${plural(errors, 'error', 'errors')}` : ''}, ${cached} with cache`,
|
|
162
162
|
top5: (pct, reads, rereads, rpct) => `The 5 largest sessions account for ${pct}. Reads: ${reads}, re-reads ${rereads} (${rpct})`,
|
|
163
163
|
pathHidden: '(path hidden, use --paths)',
|
|
164
164
|
byKindText: 'By file type:',
|
|
@@ -243,9 +243,9 @@ Opciones:
|
|
|
243
243
|
longTitle: 'Sesiones muy largas',
|
|
244
244
|
longWhy: (n, min) => `${plural(n, 'conversación supera', 'conversaciones superan')} ${min} tokens. En una sesión larga, todo lo acumulado se vuelve a enviar en cada turno.`,
|
|
245
245
|
longSuggestion: 'Una conversación nueva por tarea, o /compact cuando cambias de tema.',
|
|
246
|
-
conversation: (id) => `conversación ${id}`,
|
|
246
|
+
conversation: (id, at) => `conversación ${id}${at ? ` · ${at}` : ''}`,
|
|
247
247
|
steps: (n) => plural(n, 'paso', 'pasos'),
|
|
248
|
-
longUsage: (calls,
|
|
248
|
+
longUsage: (calls, cached) => ` · ${plural(calls, 'llamada', 'llamadas')} al modelo · tokens reales, ${cached} con caché`,
|
|
249
249
|
},
|
|
250
250
|
|
|
251
251
|
map: {
|
|
@@ -276,7 +276,7 @@ Opciones:
|
|
|
276
276
|
otherModels: (n) => ` +${n} más`,
|
|
277
277
|
efforts: { minimal: 'Mínimo', low: 'Bajo', medium: 'Medio', high: 'Alto', xhigh: 'Muy alto', 'extra high': 'Muy alto', max: 'Máximo' },
|
|
278
278
|
sessionsTitle: 'Sesiones (consumo real)',
|
|
279
|
-
sessionCols: ['Conversación', 'Modelo', 'Tokens', '%', 'Con caché', 'Llamadas', 'Errores'],
|
|
279
|
+
sessionCols: ['Conversación', 'Cuándo', 'Modelo', 'Tokens', '%', 'Con caché', 'Llamadas', 'Errores'],
|
|
280
280
|
noConversations: (scope, dir) => `No hay conversaciones${scope ? ` de <code>${scope}</code>` : ''} en <code>${dir}</code>.`,
|
|
281
281
|
tileReal: 'tokens reales (entrada + salida)',
|
|
282
282
|
tileCache: (calls) => `contando la caché · ${plural(calls, 'llamada', 'llamadas')}`,
|
|
@@ -319,7 +319,7 @@ Opciones:
|
|
|
319
319
|
realUsage: (real, input, output, cached, calls) => `Consumo real: ${real} tokens (entrada ${input} + salida ${output}), ${cached} contando la caché, en ${plural(calls, 'llamada', 'llamadas')} al modelo`,
|
|
320
320
|
noLog: (n) => ` (${plural(n, 'conversación', 'conversaciones')} sin registro)`,
|
|
321
321
|
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) => `conversación ${id}${model ? ` (${model})` : ''} ${plural(calls, 'llamada', 'llamadas')}${errors ? `, ${plural(errors, 'error', 'errores')}` : ''}, ${cached} con caché`,
|
|
322
|
+
sessionLine: (id, calls, errors, cached, model, at) => `conversación ${id}${at ? ` · ${at}` : ''}${model ? ` (${model})` : ''} ${plural(calls, 'llamada', 'llamadas')}${errors ? `, ${plural(errors, 'error', 'errores')}` : ''}, ${cached} con caché`,
|
|
323
323
|
top5: (pct, reads, rereads, rpct) => `Las 5 sesiones más grandes suman el ${pct}. Lecturas: ${reads}, relecturas ${rereads} (${rpct})`,
|
|
324
324
|
pathHidden: '(ruta oculta, usa --paths)',
|
|
325
325
|
byKindText: 'Por tipo de fichero:',
|
|
@@ -346,6 +346,15 @@ const t = new Proxy({}, { get: (_, k) => LANGS[current][k] });
|
|
|
346
346
|
const lang = () => current;
|
|
347
347
|
const num = (n) => Math.round(n).toLocaleString(t.locale);
|
|
348
348
|
const date = (ms) => new Date(ms).toLocaleDateString(t.locale, { day: 'numeric', month: 'short', year: 'numeric' });
|
|
349
|
+
const time = (ms) => new Date(ms).toLocaleTimeString(t.locale, { hour: '2-digit', minute: '2-digit' });
|
|
350
|
+
|
|
351
|
+
// When a session happened: "29 Oct 2026, 11:16–11:40", or "27 Oct 2026, 09:02 – 29 Oct 2026, 11:40" when it
|
|
352
|
+
// spans several days; '' when unknown.
|
|
353
|
+
function when(start, end) {
|
|
354
|
+
if (!start || !end) return '';
|
|
355
|
+
if (date(start) !== date(end)) return `${date(start)}, ${time(start)} – ${date(end)}, ${time(end)}`;
|
|
356
|
+
return time(start) === time(end) ? `${date(start)}, ${time(start)}` : `${date(start)}, ${time(start)}–${time(end)}`;
|
|
357
|
+
}
|
|
349
358
|
|
|
350
359
|
// Built-in kinds are stable ids (see history.js kind()); custom kinds from the config are shown as written.
|
|
351
360
|
function kindLabel(k) {
|
|
@@ -356,4 +365,4 @@ function kindLabel(k) {
|
|
|
356
365
|
// "High" / "high" → "High" or "Alto"; levels agy may add later are shown as written.
|
|
357
366
|
const effortLabel = (e) => (e ? t.report.efforts[e.toLowerCase()] || e : t.report.unknownModel);
|
|
358
367
|
|
|
359
|
-
module.exports = { t, setLang, lang, detect, num, date, kindLabel, effortLabel, LANGS };
|
|
368
|
+
module.exports = { t, setLang, lang, detect, num, date, when, kindLabel, effortLabel, LANGS };
|
package/lib/report.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// The page is static: its only interactions are switching the period, the chart tooltip and copying text,
|
|
3
3
|
// so it cannot change anything on disk.
|
|
4
4
|
|
|
5
|
-
const { t, lang, num, date, kindLabel, effortLabel } = require('./i18n');
|
|
5
|
+
const { t, lang, num, date, when, kindLabel, effortLabel } = require('./i18n');
|
|
6
6
|
const { dayKey } = require('./history');
|
|
7
7
|
|
|
8
8
|
const esc = (s) => String(s ?? '').replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[c]);
|
|
@@ -35,7 +35,7 @@ function item(it) {
|
|
|
35
35
|
<p class="hint">${t.report.exampleHint}</p></div></details>`
|
|
36
36
|
: '';
|
|
37
37
|
return `<li><div class="row"><div class="what"><span class="label">${esc(it.label)}</span><span class="detail">${esc(it.detail)}</span></div>
|
|
38
|
-
<span class="num">${it.tokens ?
|
|
38
|
+
<span class="num">${it.tokens ? `${it.exact ? '' : '~'}${fmt(it.tokens)}` : ''}</span></div>${extra}</li>`;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
function finding(f, total) {
|
|
@@ -59,8 +59,8 @@ function sessionsTable(r) {
|
|
|
59
59
|
const rows = r.sessions.filter((s) => s.usage).slice(0, 15);
|
|
60
60
|
if (!rows.length) return '';
|
|
61
61
|
const max = Math.max(...rows.map((s) => billed(s.usage)), 1);
|
|
62
|
-
return `<h3>${t.report.sessionsTitle}</h3><div class="scroll"><table><thead><tr>${t.report.sessionCols.map((c, i) => `<th${i >
|
|
63
|
-
${rows.map((s) => `<tr><td><div class="bar" style="--w:${((100 * billed(s.usage)) / max).toFixed(1)}%"></div><span>${esc(s.id.slice(0, 8))}</span></td><td>${esc(sessionModel(s))}</td>
|
|
62
|
+
return `<h3>${t.report.sessionsTitle}</h3><div class="scroll"><table><thead><tr>${t.report.sessionCols.map((c, i) => `<th${i > 2 ? ' class="num"' : ''}>${c}</th>`).join('')}</tr></thead><tbody>
|
|
63
|
+
${rows.map((s) => `<tr><td><div class="bar" style="--w:${((100 * billed(s.usage)) / max).toFixed(1)}%"></div><span>${esc(s.id.slice(0, 8))}</span></td><td class="when">${esc(when(s.start, s.end))}</td><td>${esc(sessionModel(s))}</td>
|
|
64
64
|
<td class="num">${fmt(billed(s.usage))}</td><td class="num">${pct(billed(s.usage), billed(r.usage))}</td><td class="num">${fmt(withCache(s.usage))}</td>
|
|
65
65
|
<td class="num">${s.usage.calls}</td><td class="num">${s.usage.errors || ''}</td></tr>`).join('')}
|
|
66
66
|
</tbody></table></div>`;
|
|
@@ -247,7 +247,7 @@ function htmlViews(views, { scope }) {
|
|
|
247
247
|
.copy:focus-visible { outline: 2px solid var(--accent); }
|
|
248
248
|
.scroll { overflow-x: auto; } table { width: 100%; border-collapse: collapse; font-size: 14px; margin: 6px 0 18px; }
|
|
249
249
|
th { text-align: left; color: var(--muted); font-weight: 600; } th, td { padding: 5px 8px; border-bottom: 1px solid var(--line); }
|
|
250
|
-
td:first-child { position: relative; word-break: break-all; }
|
|
250
|
+
td:first-child { position: relative; word-break: break-all; } td.when { white-space: nowrap; }
|
|
251
251
|
.bar { position: absolute; inset: 3px auto 3px 0; width: var(--w); background: var(--bar); opacity: .15; border-radius: 3px; } td span { position: relative; }
|
|
252
252
|
.detail-all > summary { color: var(--fg); }
|
|
253
253
|
footer { margin-top: 32px; font-size: 13px; color: var(--muted); }
|
|
@@ -316,7 +316,7 @@ function text(r, found, { scope, paths }) {
|
|
|
316
316
|
const top = r.sessions.filter((s) => s.usage).slice(0, 5);
|
|
317
317
|
out.push(t.report.topSessions(top.length));
|
|
318
318
|
for (const s of top) {
|
|
319
|
-
out.push(` ${fmt(billed(s.usage)).padStart(10)} ${pct(billed(s.usage), billed(u)).padStart(4)} ${t.report.sessionLine(s.id.slice(0, 8), s.usage.calls, s.usage.errors, fmt(withCache(s.usage)), sessionModel(s))}`);
|
|
319
|
+
out.push(` ${fmt(billed(s.usage)).padStart(10)} ${pct(billed(s.usage), billed(u)).padStart(4)} ${t.report.sessionLine(s.id.slice(0, 8), s.usage.calls, s.usage.errors, fmt(withCache(s.usage)), sessionModel(s), when(s.start, s.end))}`);
|
|
320
320
|
}
|
|
321
321
|
}
|
|
322
322
|
out.push(t.report.top5(pct(top5, r.tokens), r.reads, r.rereads, pct(r.rereads, r.reads)));
|
|
@@ -335,7 +335,7 @@ function text(r, found, { scope, paths }) {
|
|
|
335
335
|
// Only file and folder labels are paths; commands and session ids are always shown.
|
|
336
336
|
const hasPaths = ['rereads', 'non-source', 'cross-session'].includes(f.id);
|
|
337
337
|
for (const it of f.items) {
|
|
338
|
-
out.push(` ${(it.tokens ?
|
|
338
|
+
out.push(` ${(it.tokens ? `${it.exact ? '' : '~'}${fmt(it.tokens)}` : '').padStart(10)} ${paths || !hasPaths ? it.label : t.report.pathHidden} ${it.detail || ''}`);
|
|
339
339
|
if (paths) for (const x of it.fixes.filter((x) => !x.applied)) out.push(`${' '.repeat(16)}${x.title}: ${x.text.split('\n').join(' ')} → ${x.file}`);
|
|
340
340
|
}
|
|
341
341
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agy-insights",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.2",
|
|
4
4
|
"description": "Measures Antigravity CLI token usage and shows which context is sent repeatedly or needlessly, in English or Spanish. Run: npx agy-insights@latest",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"keywords": [
|