@krtclcdy/tersio 2.9.0 → 2.11.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/cli/usage.js CHANGED
@@ -1,5 +1,6 @@
1
1
  // cli/usage.ts — ledger + session-token usage report, tokscale-style.
2
- import { co2Grams, importSessionTokens, priceFor, readUsage, usdCost, } from '../extensions/shared/usage-ledger.js';
2
+ import { co2GramsFor, energyWhFor, importSessionTokens, ledgerPath, priceFor, readUsage, sessionsDir, usdCost, } from '../extensions/shared/usage-ledger.js';
3
+ import { readRtkGain, rtkDbPath } from '../extensions/shared/rtk-gain.js';
3
4
  import { withInteractiveSpinner } from './interactive.js';
4
5
  import { PACKAGE_VERSION } from './common.js';
5
6
  export function summarizeUsage(rows) {
@@ -14,9 +15,15 @@ export function summarizeUsage(rows) {
14
15
  let usd = 0;
15
16
  let priced = true;
16
17
  let savedUsd = 0;
18
+ const byModelUsd = {};
19
+ let co2g = 0;
20
+ let energyWh = 0;
17
21
  for (const [model, t] of Object.entries(session.byModel)) {
18
22
  const c = usdCost(t, model);
19
23
  usd += c.usd;
24
+ byModelUsd[model] = c.usd;
25
+ co2g += co2GramsFor(model, t.output);
26
+ energyWh += energyWhFor(model, t.output);
20
27
  if (!c.priced)
21
28
  priced = false;
22
29
  savedUsd += (t.cacheRead / 1e6) * priceFor(model).price.input;
@@ -31,50 +38,108 @@ export function summarizeUsage(rows) {
31
38
  messages: session.messages,
32
39
  tokens: session.totals,
33
40
  byModel: session.byModel,
41
+ byModelUsd,
42
+ byModelMessages: session.byModelMessages,
34
43
  byDay: session.byDay,
35
44
  byDayModel: session.byDayModel,
36
45
  byTool,
46
+ recent: session.recent,
47
+ rtkGain: readRtkGain(),
37
48
  usd,
38
49
  priced,
39
50
  savedUsd,
40
51
  costMeasured: session.costMeasured,
41
- co2g: co2Grams(session.totals.output),
52
+ co2g,
53
+ energyWh,
42
54
  version: PACKAGE_VERSION,
55
+ paths: { ledger: ledgerPath(), sessions: sessionsDir(), rtk: rtkDbPath() },
43
56
  };
44
57
  }
45
58
  function fmt(n) {
46
59
  return n.toLocaleString('en-US');
47
60
  }
61
+ function fmtShort(n) {
62
+ if (n >= 1e12)
63
+ return (n / 1e12).toFixed(1) + 'T';
64
+ if (n >= 1e9)
65
+ return (n / 1e9).toFixed(1) + 'B';
66
+ if (n >= 1e6)
67
+ return (n / 1e6).toFixed(1) + 'M';
68
+ if (n >= 1e3)
69
+ return (n / 1e3).toFixed(1) + 'K';
70
+ return String(Math.round(n));
71
+ }
72
+ function fmtMs(ms) {
73
+ if (ms < 1000)
74
+ return `${Math.round(ms)}ms`;
75
+ return `${(ms / 1000).toFixed(1)}s`;
76
+ }
77
+ function bar(frac, width = 12) {
78
+ const filled = Math.max(0, Math.min(width, Math.round(frac * width)));
79
+ return '█'.repeat(filled) + '░'.repeat(width - filled);
80
+ }
81
+ function pad(s, n) {
82
+ return s.length >= n ? s : s + ' '.repeat(n - s.length);
83
+ }
84
+ // Box-drawing table, plain text (piped-safe, byte-stable). Numeric columns
85
+ // right-align; long cells truncate with an ellipsis.
86
+ function table(headers, rows, right = [], maxW = 32) {
87
+ const cells = [headers, ...rows].map((r) => r.map((c) => (c.length > maxW ? c.slice(0, maxW - 1) + '…' : c)));
88
+ const widths = headers.map((_, i) => Math.max(...cells.map((r) => r[i].length)));
89
+ const line = (l, m, r) => l + widths.map((w) => '─'.repeat(w + 2)).join(m) + r;
90
+ const row = (cs) => '│ ' + cs.map((c, i) => (right[i] ? c.padStart(widths[i]) : pad(c, widths[i]))).join(' │ ') + ' │';
91
+ const out = [line('┌', '┬', '┐'), row(cells[0]), line('├', '┼', '┤')];
92
+ for (const r of cells.slice(1))
93
+ out.push(row(r));
94
+ out.push(line('└', '┴', '┘'));
95
+ return out.map((l) => ' ' + l);
96
+ }
48
97
  function printReport(report) {
49
- console.log('\n=== Tersio Usage ===');
98
+ console.log(`\n=== Tersio Usage v${report.version} · ${fmt(report.messages)} msgs ===`);
50
99
  if (report.empty) {
51
100
  console.log(' No ledger rows or session tokens yet — run session commands first.');
52
101
  return;
53
102
  }
54
103
  const t = report.tokens;
55
- console.log(` Tokens input ${fmt(t.input)} · output ${fmt(t.output)} · cache read ${fmt(t.cacheRead)} · cache write ${fmt(t.cacheWrite)} (${report.messages} assistant messages)`);
56
- console.log(` Cost $${report.usd.toFixed(2)}${report.priced ? '' : ' (includes default pricing for unknown models)'} · ~$${report.savedUsd.toFixed(2)} saved by cache (est.) · CO2 ~${report.co2g.toFixed(1)}g (est.)`);
57
- const models = Object.entries(report.byModel).sort((a, b) => (b[1].input + b[1].output) - (a[1].input + a[1].output)).slice(0, 5);
104
+ console.log(` TOKENS ${fmt(t.input)} in · ${fmt(t.output)} out · ${fmt(t.cacheRead)} cache read${t.cacheWrite > 0 ? ` · ${fmt(t.cacheWrite)} written` : ''}`);
105
+ console.log(` COST $${report.usd.toFixed(2)}${report.priced ? '' : ' (includes default pricing)'} · ~$${report.savedUsd.toFixed(2)} cache-saved (est.) · ~${report.co2g.toFixed(1)}g CO2 (est.)`);
106
+ const models = Object.entries(report.byModel).filter(([, b]) => b.input + b.output + b.cacheRead + b.cacheWrite > 0).sort((a, b) => (b[1].input + b[1].output) - (a[1].input + a[1].output)).slice(0, 8);
58
107
  if (models.length) {
59
- console.log(' By model');
60
- for (const [model, b] of models) {
61
- console.log(` ${model}: in ${fmt(b.input)} · out ${fmt(b.output)} · cache r/w ${fmt(b.cacheRead)}/${fmt(b.cacheWrite)}`);
108
+ console.log(' BY MODEL');
109
+ const top = models.reduce((m, [, b]) => Math.max(m, b.input + b.output + b.cacheRead + b.cacheWrite), 1);
110
+ const mrows = models.map(([model, b]) => {
111
+ const mt = b.input + b.output + b.cacheRead + b.cacheWrite;
112
+ const hit = b.input + b.cacheRead ? (b.cacheRead / (b.input + b.cacheRead)) * 100 : 0;
113
+ const usd = report.byModelUsd[model] ?? 0;
114
+ return [model, `in ${fmt(b.input)}`, `out ${fmt(b.output)}`, `${fmt(b.cacheRead)}/${fmt(b.cacheWrite)}`, `$${usd.toFixed(2)}`, `${hit.toFixed(1)}%`, `${bar(mt / top, 8)} ${fmtShort(mt)}`];
115
+ });
116
+ for (const l of table(['Model', 'Input', 'Output', 'Cache r/w', 'USD', 'Hit', 'Share'], mrows, [false, true, true, true, true, true, false])) {
117
+ console.log(l);
62
118
  }
63
119
  }
64
- if (report.byTool.length) {
65
- console.log(' Top tools');
66
- for (const [tool, n] of report.byTool) {
67
- console.log(` ${n}× ${tool}`);
68
- }
120
+ const cmdRows = report.byTool.map(([tool, n]) => ({ name: tool, count: n, saved: null, avgPct: null, avgMs: null }));
121
+ for (const r of report.rtkGain.byCommand) {
122
+ cmdRows.push({ name: r.command, count: r.count, saved: r.saved, avgPct: r.avgPct, avgMs: r.avgMs });
69
123
  }
70
- if (report.total) {
71
- console.log(' Activity');
72
- for (const [kind, n] of Object.entries(report.byKind)) {
73
- console.log(` ${kind}: ${n}`);
74
- }
75
- console.log(' Top commands');
76
- for (const [detail, n] of report.byDetail) {
77
- console.log(` ${n}× ${detail}`);
124
+ cmdRows.sort((a, b) => b.count - a.count);
125
+ if (cmdRows.length) {
126
+ const g = report.rtkGain;
127
+ const scope = g.commands
128
+ ? ` COMMAND TOOLS ${fmt(report.byTool.length)} tools · ${fmt(g.commands)} commands · ${fmtShort(g.saved)} saved`
129
+ : ' COMMAND TOOLS';
130
+ console.log(scope);
131
+ const top = cmdRows.reduce((m, r) => Math.max(m, r.count), 1);
132
+ const crows = cmdRows.slice(0, 15).map((r, idx) => [
133
+ String(idx + 1),
134
+ r.name,
135
+ fmt(r.count),
136
+ r.saved === null ? '–' : fmtShort(r.saved),
137
+ r.avgPct === null ? '–' : `${r.avgPct.toFixed(1)}%`,
138
+ r.avgMs === null ? '–' : fmtMs(r.avgMs),
139
+ bar(r.count / top, 8),
140
+ ]);
141
+ for (const l of table(['#', 'Tool/Command', 'Count', 'Saved', 'Avg%', 'Time', 'Impact'], crows, [true, false, true, true, true, true, false], 30)) {
142
+ console.log(l);
78
143
  }
79
144
  }
80
145
  }