takomi 2.1.18 → 2.1.19
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.
|
@@ -8,7 +8,7 @@ import type {
|
|
|
8
8
|
import { commandHelp, completions, statusText, workflowPrompt } from "./command-text";
|
|
9
9
|
import type { TakomiSubagentController } from "./subagent-types";
|
|
10
10
|
import { installTakomiRoutingPolicy, resolveTakomiRoutingPolicy, type RoutingPolicyInstallScope } from "./routing-policy";
|
|
11
|
-
import { collectTakomiStats, renderTakomiStats } from "
|
|
11
|
+
import { collectTakomiStats, renderTakomiStats } from "./takomi-stats.js";
|
|
12
12
|
|
|
13
13
|
export type TakomiRuntimeCommandState = {
|
|
14
14
|
enabled: boolean;
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export function collectTakomiStats(opts?: { home?: string; cwd?: string; json?: boolean; limit?: number; view?: string; since?: string }): Promise<any>;
|
|
2
|
+
export function renderTakomiStats(stats: any, opts?: { limit?: number; view?: string }): string;
|
|
3
|
+
export function printTakomiStats(options?: { home?: string; cwd?: string; json?: boolean; limit?: number; view?: string; since?: string }): Promise<void>;
|
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import pc from 'picocolors';
|
|
5
|
+
|
|
6
|
+
const PRICES = {
|
|
7
|
+
'gpt-5.5': [5.00, 0.50, 30.00],
|
|
8
|
+
'gpt-5.4': [2.50, 0.25, 15.00],
|
|
9
|
+
'gpt-5.4-mini': [0.75, 0.075, 4.50],
|
|
10
|
+
'gpt-5.4-nano': [0.20, 0.02, 1.25],
|
|
11
|
+
'gpt-5.3-codex': [2.50, 0.25, 15.00],
|
|
12
|
+
'gpt-5.2-codex': [1.75, 0.175, 14.00],
|
|
13
|
+
'gpt-5-codex': [1.25, 0.125, 10.00],
|
|
14
|
+
'gpt-5.2': [1.75, 0.175, 14.00],
|
|
15
|
+
'gpt-5.1': [1.25, 0.125, 10.00],
|
|
16
|
+
'gpt-5': [1.25, 0.125, 10.00],
|
|
17
|
+
'gpt-5-mini': [0.25, 0.025, 2.00],
|
|
18
|
+
'gpt-4.1': [2.00, 0.50, 8.00],
|
|
19
|
+
'gpt-4o': [2.50, 1.25, 10.00],
|
|
20
|
+
'o4-mini': [1.10, 0.275, 4.40],
|
|
21
|
+
'claude-sonnet-4-6': [3.00, 0.30, 15.00],
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
function safeJson(line) { try { return JSON.parse(line); } catch { return null; } }
|
|
25
|
+
function dayOf(ts) { return typeof ts === 'string' && ts.length >= 10 ? ts.slice(0, 10) : 'unknown'; }
|
|
26
|
+
function add(map, key, patch) { const row = map.get(key) || { key, input: 0, cache: 0, output: 0, total: 0, cost: 0, events: 0 }; for (const [k,v] of Object.entries(patch)) row[k] = (row[k] || 0) + (Number(v) || 0); if (!Object.prototype.hasOwnProperty.call(patch, 'events')) row.events += 1; map.set(key, row); }
|
|
27
|
+
function cost(model, input, cache, output, additiveCache = true) { const p = PRICES[model]; if (!p) return 0; const nonCached = additiveCache ? input : Math.max(input - cache, 0); return (nonCached*p[0] + cache*p[1] + output*p[2]) / 1_000_000; }
|
|
28
|
+
function fmtTokens(n) { if (n >= 1e9) return `${(n/1e9).toFixed(2)}B`; if (n >= 1e6) return `${(n/1e6).toFixed(1)}M`; if (n >= 1e3) return `${(n/1e3).toFixed(1)}K`; return String(Math.round(n || 0)); }
|
|
29
|
+
function fmtMoney(n) { return `$${(n || 0).toFixed(n > 100 ? 0 : 2)}`; }
|
|
30
|
+
function ms(n) { if (!n) return '-'; const s = Math.round(n/1000); if (s < 60) return `${s}s`; const m = Math.floor(s/60); if (m < 60) return `${m}m ${s%60}s`; const h = Math.floor(m/60); return `${h}h ${m%60}m`; }
|
|
31
|
+
function parseSince(value) {
|
|
32
|
+
if (!value) return null;
|
|
33
|
+
const raw = String(value).trim().toLowerCase();
|
|
34
|
+
const rel = raw.match(/^(\d+)(d|day|days|w|week|weeks|m|month|months)$/);
|
|
35
|
+
const d = new Date(); d.setHours(0,0,0,0);
|
|
36
|
+
if (rel) {
|
|
37
|
+
const n = Number(rel[1]);
|
|
38
|
+
const unit = rel[2][0];
|
|
39
|
+
d.setDate(d.getDate() - (unit === 'w' ? n * 7 : unit === 'm' ? n * 30 : n));
|
|
40
|
+
return d.toISOString().slice(0, 10);
|
|
41
|
+
}
|
|
42
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(raw)) return raw;
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
function projectKey(file) {
|
|
46
|
+
const normalized = String(file || '').replace(/\\/g, '/');
|
|
47
|
+
const marker = '/sessions/';
|
|
48
|
+
const idx = normalized.indexOf(marker);
|
|
49
|
+
if (idx >= 0) {
|
|
50
|
+
const encoded = normalized.slice(idx + marker.length).split('/')[0];
|
|
51
|
+
return encoded.replace(/^--/, '').replace(/--$/, '').replace(/--/g, '/').replace(/-/g, ' ').trim() || 'global';
|
|
52
|
+
}
|
|
53
|
+
const cwdMarker = '/.pi/';
|
|
54
|
+
const pidx = normalized.indexOf(cwdMarker);
|
|
55
|
+
if (pidx >= 0) return normalized.slice(0, pidx).split('/').slice(-2).join('/');
|
|
56
|
+
return 'unknown';
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ── ANSI-aware string helpers ───────────────────────────────────────────────
|
|
60
|
+
// eslint-disable-next-line no-control-regex
|
|
61
|
+
const ANSI_RE = /\u001b\[[0-9;]*m/g;
|
|
62
|
+
function stripAnsi(s) { return String(s).replace(ANSI_RE, ''); }
|
|
63
|
+
function visLen(s) { return stripAnsi(s).length; }
|
|
64
|
+
function ansiPadEnd(s, w) { return s + ' '.repeat(Math.max(0, w - visLen(s))); }
|
|
65
|
+
function ansiPadStart(s, w) { return ' '.repeat(Math.max(0, w - visLen(s))) + s; }
|
|
66
|
+
|
|
67
|
+
async function files(root, suffix = '.jsonl') {
|
|
68
|
+
const out = [];
|
|
69
|
+
if (!root || !(await fs.pathExists(root))) return out;
|
|
70
|
+
async function walk(dir) {
|
|
71
|
+
for (const ent of await fs.readdir(dir, { withFileTypes: true })) {
|
|
72
|
+
const p = path.join(dir, ent.name);
|
|
73
|
+
if (ent.isDirectory()) await walk(p); else if (ent.name.endsWith(suffix)) out.push(p);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
await walk(root); return out;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function scanPiSessions(root, source, events) {
|
|
80
|
+
for (const file of await files(root)) {
|
|
81
|
+
let provider = 'unknown', model = 'unknown', session = path.basename(file, '.jsonl');
|
|
82
|
+
const text = await fs.readFile(file, 'utf8').catch(() => '');
|
|
83
|
+
for (const line of text.split(/\r?\n/)) {
|
|
84
|
+
const obj = safeJson(line); if (!obj) continue;
|
|
85
|
+
if (obj.type === 'session') session = obj.id || session;
|
|
86
|
+
if (obj.type === 'model_change') { provider = obj.provider || provider; model = obj.modelId || model; }
|
|
87
|
+
const u = obj.type === 'message' && obj.message && obj.message.usage;
|
|
88
|
+
if (u) events.push({ source, file, timestamp: obj.timestamp, day: dayOf(obj.timestamp), session, provider, model, input: +u.input||0, cache: +u.cacheRead||0, output: +u.output||0, total: +u.totalTokens||0, cost: cost(model, +u.input||0, +u.cacheRead||0, +u.output||0, true) });
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function scanRunHistory(file) {
|
|
94
|
+
const runs = [];
|
|
95
|
+
if (!(await fs.pathExists(file))) return runs;
|
|
96
|
+
const text = await fs.readFile(file, 'utf8').catch(() => '');
|
|
97
|
+
for (const line of text.split(/\r?\n/)) { const o = safeJson(line); if (o) runs.push(o); }
|
|
98
|
+
return runs;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export async function collectTakomiStats(opts = {}) {
|
|
102
|
+
const home = opts.home || os.homedir();
|
|
103
|
+
const cwd = opts.cwd || process.cwd();
|
|
104
|
+
const rawEvents = [];
|
|
105
|
+
await scanPiSessions(path.join(home, '.pi', 'agent', 'sessions'), 'pi-global', rawEvents);
|
|
106
|
+
await scanPiSessions(path.join(cwd, '.pi', 'agent', 'sessions'), 'pi-project', rawEvents);
|
|
107
|
+
await scanPiSessions(path.join(cwd, '.pi', 'takomi'), 'takomi-project', rawEvents);
|
|
108
|
+
const sinceDay = parseSince(opts.since);
|
|
109
|
+
const events = rawEvents
|
|
110
|
+
.filter(e => !sinceDay || e.day >= sinceDay)
|
|
111
|
+
.map(e => ({ ...e, project: projectKey(e.file) }));
|
|
112
|
+
const runs = await scanRunHistory(path.join(home, '.pi', 'agent', 'run-history.jsonl'));
|
|
113
|
+
const byDay = new Map(), byModel = new Map(), bySource = new Map(), byProject = new Map();
|
|
114
|
+
let totals = { input: 0, cache: 0, output: 0, total: 0, cost: 0, events: events.length };
|
|
115
|
+
for (const e of events) {
|
|
116
|
+
totals.input += e.input; totals.cache += e.cache; totals.output += e.output; totals.total += e.total; totals.cost += e.cost;
|
|
117
|
+
add(byDay, e.day, e); add(byModel, e.model, e); add(bySource, e.source, e); add(byProject, e.project, e);
|
|
118
|
+
}
|
|
119
|
+
const byAgent = new Map(); let longestRun = null;
|
|
120
|
+
for (const r of runs) { add(byAgent, r.agent || 'unknown', { total: 0, events: 1 }); if (!longestRun || (+r.duration||0) > (+longestRun.duration||0)) longestRun = r; }
|
|
121
|
+
return { generatedAt: new Date().toISOString(), cwd, since: sinceDay, totals, sessions: new Set(events.map(e => e.session)).size, byDay: [...byDay.values()].sort((a,b)=>a.key.localeCompare(b.key)), byModel: [...byModel.values()].sort((a,b)=>b.total-a.total), bySource: [...bySource.values()].sort((a,b)=>b.total-a.total), byProject: [...byProject.values()].sort((a,b)=>b.total-a.total), byAgent: [...byAgent.values()].sort((a,b)=>b.events-a.events), runs, longestRun, recent: events.sort((a,b)=>(b.timestamp||'').localeCompare(a.timestamp||'')).slice(0, 10) };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ── Streak Calculation ──────────────────────────────────────────────────────
|
|
125
|
+
function calcStreaks(byDay) {
|
|
126
|
+
if (!byDay.length) return { current: 0, longest: 0, quietDays: 0 };
|
|
127
|
+
const daySet = new Set(byDay.map(d => d.key));
|
|
128
|
+
const today = new Date(); today.setHours(0,0,0,0);
|
|
129
|
+
// current streak: walk back from today
|
|
130
|
+
let current = 0;
|
|
131
|
+
for (let d = new Date(today); ; d.setDate(d.getDate() - 1)) {
|
|
132
|
+
const key = d.toISOString().slice(0, 10);
|
|
133
|
+
if (daySet.has(key)) current++; else break;
|
|
134
|
+
}
|
|
135
|
+
// longest streak: walk all sorted days
|
|
136
|
+
const sorted = [...daySet].sort();
|
|
137
|
+
let longest = 0, run = 1;
|
|
138
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
139
|
+
const prev = new Date(sorted[i - 1]); prev.setDate(prev.getDate() + 1);
|
|
140
|
+
if (prev.toISOString().slice(0, 10) === sorted[i]) { run++; } else { longest = Math.max(longest, run); run = 1; }
|
|
141
|
+
}
|
|
142
|
+
longest = Math.max(longest, run);
|
|
143
|
+
// quiet days
|
|
144
|
+
const first = new Date(sorted[0]);
|
|
145
|
+
const span = Math.round((today - first) / 86400000) + 1;
|
|
146
|
+
return { current, longest, quietDays: Math.max(0, span - sorted.length) };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ── GitHub-style Heatmap Grid ───────────────────────────────────────────────
|
|
150
|
+
function heatmapGrid(byDay) {
|
|
151
|
+
const dayMap = new Map(byDay.map(d => [d.key, d.total]));
|
|
152
|
+
const max = Math.max(1, ...byDay.map(d => d.total));
|
|
153
|
+
|
|
154
|
+
// Determine range: last ~26 weeks (half year) ending at current week
|
|
155
|
+
const today = new Date(); today.setHours(0,0,0,0);
|
|
156
|
+
// End at end of current week (Sunday)
|
|
157
|
+
const endDate = new Date(today);
|
|
158
|
+
const todayDow = endDate.getDay(); // 0=Sun, 1=Mon...
|
|
159
|
+
if (todayDow !== 0) endDate.setDate(endDate.getDate() + (7 - todayDow));
|
|
160
|
+
// Start 26 weeks back on Monday
|
|
161
|
+
const startDate = new Date(endDate);
|
|
162
|
+
startDate.setDate(startDate.getDate() - (26 * 7) + 1);
|
|
163
|
+
while (startDate.getDay() !== 1) startDate.setDate(startDate.getDate() - 1);
|
|
164
|
+
|
|
165
|
+
// Build grid: 7 rows (Mon..Sun), N columns (weeks)
|
|
166
|
+
const weeks = [];
|
|
167
|
+
const monthPositions = []; // { col, label }
|
|
168
|
+
const cursor = new Date(startDate);
|
|
169
|
+
let col = 0;
|
|
170
|
+
let lastMonth = -1;
|
|
171
|
+
|
|
172
|
+
while (cursor <= endDate) {
|
|
173
|
+
const week = [];
|
|
174
|
+
for (let dow = 0; dow < 7; dow++) {
|
|
175
|
+
const key = cursor.toISOString().slice(0, 10);
|
|
176
|
+
const val = cursor <= today ? (dayMap.get(key) || 0) : -1; // -1 = future
|
|
177
|
+
week.push(val);
|
|
178
|
+
// Track month transitions on the Monday of each week
|
|
179
|
+
if (dow === 0) {
|
|
180
|
+
const m = cursor.getMonth();
|
|
181
|
+
if (m !== lastMonth) {
|
|
182
|
+
const monthNames = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
|
|
183
|
+
monthPositions.push({ col, label: monthNames[m] });
|
|
184
|
+
lastMonth = m;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
cursor.setDate(cursor.getDate() + 1);
|
|
188
|
+
}
|
|
189
|
+
weeks.push(week);
|
|
190
|
+
col++;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Intensity cell — use ■ for filled, · for empty
|
|
194
|
+
const SQ = '■';
|
|
195
|
+
const EMPTY = '·';
|
|
196
|
+
function cell(val) {
|
|
197
|
+
if (val < 0) return ' '; // future
|
|
198
|
+
if (val === 0) return pc.gray(EMPTY);
|
|
199
|
+
const x = val / max;
|
|
200
|
+
if (x < 0.12) return pc.dim(pc.cyan(SQ));
|
|
201
|
+
if (x < 0.30) return pc.cyan(SQ);
|
|
202
|
+
if (x < 0.55) return pc.blue(SQ);
|
|
203
|
+
if (x < 0.80) return pc.magenta(SQ);
|
|
204
|
+
return pc.bold(pc.magenta(SQ));
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const dayLabels = ['Mon',' ','Wed',' ','Fri',' ','Sun'];
|
|
208
|
+
const rows = [];
|
|
209
|
+
|
|
210
|
+
// Each cell is 2 chars wide (char + space) in the grid
|
|
211
|
+
for (let dow = 0; dow < 7; dow++) {
|
|
212
|
+
const prefix = pc.dim(dayLabels[dow]);
|
|
213
|
+
const cells = weeks.map(w => cell(w[dow]));
|
|
214
|
+
rows.push(` ${prefix} ${cells.join(' ')}`);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Month label row — positioned under the correct columns
|
|
218
|
+
// Each column is 2 chars wide (cell + space separator)
|
|
219
|
+
let labelStr = '';
|
|
220
|
+
let prevEnd = 0;
|
|
221
|
+
for (const ml of monthPositions) {
|
|
222
|
+
const targetPos = ml.col * 2; // 2 chars per column (char + space)
|
|
223
|
+
const gap = Math.max(0, targetPos - prevEnd);
|
|
224
|
+
labelStr += ' '.repeat(gap) + ml.label;
|
|
225
|
+
prevEnd = targetPos + ml.label.length;
|
|
226
|
+
}
|
|
227
|
+
rows.push(` ${pc.dim(labelStr)}`);
|
|
228
|
+
|
|
229
|
+
// Legend row
|
|
230
|
+
rows.push('');
|
|
231
|
+
rows.push(` ${pc.dim('Less')} ${pc.gray(EMPTY)} ${pc.dim(pc.cyan(SQ))} ${pc.cyan(SQ)} ${pc.blue(SQ)} ${pc.magenta(SQ)} ${pc.bold(pc.magenta(SQ))} ${pc.dim('More')}`);
|
|
232
|
+
|
|
233
|
+
return rows.join('\n');
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// ── Box Drawing Helpers ─────────────────────────────────────────────────────
|
|
237
|
+
function hrule(w, ch = '─') { return ch.repeat(w); }
|
|
238
|
+
|
|
239
|
+
function center(text, width) {
|
|
240
|
+
const vl = visLen(text);
|
|
241
|
+
const pad = Math.max(0, Math.floor((width - vl) / 2));
|
|
242
|
+
return ' '.repeat(pad) + text;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function statCard(value, label) {
|
|
246
|
+
return { value: String(value), label };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// ── Table Helper ────────────────────────────────────────────────────────────
|
|
250
|
+
function renderTable(title, rows, columns) {
|
|
251
|
+
const lines = [];
|
|
252
|
+
lines.push(' ' + pc.bold(pc.cyan(title)));
|
|
253
|
+
lines.push(' ' + pc.dim(hrule(columns.reduce((s, c) => s + c.width, 0) + columns.length * 2)));
|
|
254
|
+
for (const row of rows) {
|
|
255
|
+
let line = ' ';
|
|
256
|
+
for (const col of columns) {
|
|
257
|
+
const val = String(col.get(row));
|
|
258
|
+
line += col.align === 'right' ? ansiPadStart(val, col.width) : ansiPadEnd(val, col.width);
|
|
259
|
+
line += ' ';
|
|
260
|
+
}
|
|
261
|
+
lines.push(line);
|
|
262
|
+
}
|
|
263
|
+
return lines.join('\n');
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function renderFocusedView(stats, opts = {}) {
|
|
267
|
+
const view = opts.view;
|
|
268
|
+
const limit = opts.limit || 20;
|
|
269
|
+
if (!view || view === 'overview') return null;
|
|
270
|
+
const tables = {
|
|
271
|
+
models: ['Top Models', stats.byModel, [
|
|
272
|
+
{ width: 26, align: 'left', get: r => pc.white(r.key) },
|
|
273
|
+
{ width: 10, align: 'right', get: r => pc.cyan(fmtTokens(r.total)) },
|
|
274
|
+
{ width: 10, align: 'right', get: r => pc.dim(fmtMoney(r.cost)) },
|
|
275
|
+
{ width: 12, align: 'right', get: r => pc.dim(r.events + ' calls') },
|
|
276
|
+
]],
|
|
277
|
+
sources: ['Sources', stats.bySource, [
|
|
278
|
+
{ width: 22, align: 'left', get: r => pc.white(r.key) },
|
|
279
|
+
{ width: 10, align: 'right', get: r => pc.cyan(fmtTokens(r.total)) },
|
|
280
|
+
{ width: 14, align: 'right', get: r => pc.dim(r.events + ' events') },
|
|
281
|
+
]],
|
|
282
|
+
projects: ['Top Projects', stats.byProject, [
|
|
283
|
+
{ width: 42, align: 'left', get: r => pc.white(r.key.length > 42 ? '…' + r.key.slice(-41) : r.key) },
|
|
284
|
+
{ width: 10, align: 'right', get: r => pc.cyan(fmtTokens(r.total)) },
|
|
285
|
+
{ width: 10, align: 'right', get: r => pc.dim(fmtMoney(r.cost)) },
|
|
286
|
+
{ width: 12, align: 'right', get: r => pc.dim(r.events + ' calls') },
|
|
287
|
+
]],
|
|
288
|
+
agents: ['Top Agents', stats.byAgent, [
|
|
289
|
+
{ width: 24, align: 'left', get: r => pc.white(r.key) },
|
|
290
|
+
{ width: 8, align: 'right', get: r => pc.cyan(String(r.events)) },
|
|
291
|
+
{ width: 8, align: 'left', get: () => pc.dim('runs') },
|
|
292
|
+
]],
|
|
293
|
+
daily: ['Daily Usage', [...stats.byDay].reverse(), [
|
|
294
|
+
{ width: 12, align: 'left', get: r => pc.white(r.key) },
|
|
295
|
+
{ width: 10, align: 'right', get: r => pc.cyan(fmtTokens(r.total)) },
|
|
296
|
+
{ width: 10, align: 'right', get: r => pc.dim(fmtMoney(r.cost)) },
|
|
297
|
+
{ width: 12, align: 'right', get: r => pc.dim(r.events + ' calls') },
|
|
298
|
+
]],
|
|
299
|
+
};
|
|
300
|
+
const spec = tables[view === 'project' ? 'projects' : view];
|
|
301
|
+
if (!spec) return null;
|
|
302
|
+
const [title, rows, cols] = spec;
|
|
303
|
+
const suffix = stats.since ? pc.dim(`\n Since: ${stats.since}`) : '';
|
|
304
|
+
return ['\n' + pc.bold(pc.magenta('Takomi Stats')), suffix, renderTable(title, rows.slice(0, limit), cols), '\n' + pc.dim('Privacy: metadata only · no raw prompts or transcripts')].filter(Boolean).join('\n');
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// ── Main Render ─────────────────────────────────────────────────────────────
|
|
308
|
+
export function renderTakomiStats(stats, opts = {}) {
|
|
309
|
+
const focused = renderFocusedView(stats, opts);
|
|
310
|
+
if (focused) return focused;
|
|
311
|
+
const W = Math.min(process.stdout.columns || 80, 86);
|
|
312
|
+
const topModel = stats.byModel[0]?.key || 'unknown';
|
|
313
|
+
const peak = stats.byDay.reduce((a,b) => b.total > (a?.total||0) ? b : a, null);
|
|
314
|
+
const streaks = calcStreaks(stats.byDay);
|
|
315
|
+
const lines = [];
|
|
316
|
+
|
|
317
|
+
// ── Header ────────────────────────────────────────────────────────────
|
|
318
|
+
lines.push('');
|
|
319
|
+
lines.push(pc.cyan(' ' + hrule(W - 4, '━')));
|
|
320
|
+
lines.push('');
|
|
321
|
+
lines.push(center(pc.bold(pc.white('T A K O M I S T A T S')), W));
|
|
322
|
+
const user = process.env.USERNAME || process.env.USER || 'local';
|
|
323
|
+
lines.push(center(pc.dim(`@${user} · Takomi`), W));
|
|
324
|
+
lines.push('');
|
|
325
|
+
lines.push(pc.cyan(' ' + hrule(W - 4)));
|
|
326
|
+
|
|
327
|
+
// ── Stat Cards Row 1 ─────────────────────────────────────────────────
|
|
328
|
+
const cards1 = [
|
|
329
|
+
statCard(fmtTokens(stats.totals.total), 'Lifetime Tokens'),
|
|
330
|
+
statCard(fmtTokens(stats.totals.cache), 'Cache Tokens'),
|
|
331
|
+
statCard(fmtMoney(stats.totals.cost), 'Est. Cost'),
|
|
332
|
+
statCard(String(stats.sessions), 'Sessions'),
|
|
333
|
+
statCard(String(stats.runs.length), 'Agent Runs'),
|
|
334
|
+
];
|
|
335
|
+
|
|
336
|
+
const cardW = Math.floor((W - 4) / cards1.length);
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
function buildCardLines(cards) {
|
|
340
|
+
let vStr = ' ';
|
|
341
|
+
let lStr = ' ';
|
|
342
|
+
for (const c of cards) {
|
|
343
|
+
const vPad = Math.max(0, Math.floor((cardW - c.value.length) / 2));
|
|
344
|
+
const lPad = Math.max(0, Math.floor((cardW - c.label.length) / 2));
|
|
345
|
+
const vContent = ' '.repeat(vPad) + pc.bold(pc.white(c.value));
|
|
346
|
+
const lContent = ' '.repeat(lPad) + pc.dim(c.label);
|
|
347
|
+
// Pad to cardW visible chars
|
|
348
|
+
vStr += ansiPadEnd(vContent, cardW);
|
|
349
|
+
lStr += ansiPadEnd(lContent, cardW);
|
|
350
|
+
}
|
|
351
|
+
return [vStr, lStr];
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
lines.push('');
|
|
355
|
+
const [v1, l1] = buildCardLines(cards1);
|
|
356
|
+
lines.push(v1);
|
|
357
|
+
lines.push(l1);
|
|
358
|
+
|
|
359
|
+
// ── Stat Cards Row 2 ─────────────────────────────────────────────────
|
|
360
|
+
lines.push('');
|
|
361
|
+
const cards2 = [
|
|
362
|
+
statCard(peak ? fmtTokens(peak.total) : '-', 'Peak Day'),
|
|
363
|
+
statCard(topModel, 'Top Model'),
|
|
364
|
+
statCard(ms(stats.longestRun?.duration), 'Longest Run'),
|
|
365
|
+
statCard(`${streaks.current} days`, 'Current Streak'),
|
|
366
|
+
statCard(`${streaks.longest} days`, 'Longest Streak'),
|
|
367
|
+
];
|
|
368
|
+
|
|
369
|
+
const [v2, l2] = buildCardLines(cards2);
|
|
370
|
+
lines.push(v2);
|
|
371
|
+
lines.push(l2);
|
|
372
|
+
|
|
373
|
+
// ── Info line ─────────────────────────────────────────────────────────
|
|
374
|
+
lines.push('');
|
|
375
|
+
const infoText = `Peak: ${peak?.key || '-'} · ${streaks.quietDays} quiet days · ${stats.totals.events.toLocaleString()} events${stats.since ? ` · since ${stats.since}` : ''}`;
|
|
376
|
+
lines.push(center(pc.dim(infoText), W));
|
|
377
|
+
|
|
378
|
+
lines.push('');
|
|
379
|
+
lines.push(pc.cyan(' ' + hrule(W - 4, '━')));
|
|
380
|
+
|
|
381
|
+
// ── Activity Heatmap ────────────────────────────────────────────────────
|
|
382
|
+
lines.push('');
|
|
383
|
+
lines.push(' ' + pc.bold(pc.cyan('Token Activity')));
|
|
384
|
+
lines.push(' ' + pc.dim(hrule(W - 4)));
|
|
385
|
+
lines.push(heatmapGrid(stats.byDay));
|
|
386
|
+
|
|
387
|
+
// ── Models Table ────────────────────────────────────────────────────────
|
|
388
|
+
lines.push('');
|
|
389
|
+
const modelLimit = opts.limit || 8;
|
|
390
|
+
lines.push(renderTable('Top Models', stats.byModel.slice(0, modelLimit), [
|
|
391
|
+
{ width: 24, align: 'left', get: r => pc.white(r.key) },
|
|
392
|
+
{ width: 10, align: 'right', get: r => pc.cyan(fmtTokens(r.total)) },
|
|
393
|
+
{ width: 10, align: 'right', get: r => pc.dim(fmtMoney(r.cost)) },
|
|
394
|
+
{ width: 12, align: 'right', get: r => pc.dim(r.events + ' calls') },
|
|
395
|
+
]));
|
|
396
|
+
|
|
397
|
+
// ── Projects Table ──────────────────────────────────────────────────────
|
|
398
|
+
if (stats.byProject.length) {
|
|
399
|
+
lines.push('');
|
|
400
|
+
lines.push(renderTable('Top Projects', stats.byProject.slice(0, 5), [
|
|
401
|
+
{ width: 34, align: 'left', get: r => pc.white(r.key.length > 34 ? '…' + r.key.slice(-33) : r.key) },
|
|
402
|
+
{ width: 10, align: 'right', get: r => pc.cyan(fmtTokens(r.total)) },
|
|
403
|
+
{ width: 10, align: 'right', get: r => pc.dim(fmtMoney(r.cost)) },
|
|
404
|
+
]));
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// ── Sources Table ───────────────────────────────────────────────────────
|
|
408
|
+
lines.push('');
|
|
409
|
+
lines.push(renderTable('Sources', stats.bySource, [
|
|
410
|
+
{ width: 20, align: 'left', get: r => pc.white(r.key) },
|
|
411
|
+
{ width: 10, align: 'right', get: r => pc.cyan(fmtTokens(r.total)) },
|
|
412
|
+
{ width: 14, align: 'right', get: r => pc.dim(r.events + ' events') },
|
|
413
|
+
]));
|
|
414
|
+
|
|
415
|
+
// ── Agents Table ────────────────────────────────────────────────────────
|
|
416
|
+
if (stats.byAgent.length) {
|
|
417
|
+
lines.push('');
|
|
418
|
+
lines.push(renderTable('Top Agents', stats.byAgent.slice(0, modelLimit), [
|
|
419
|
+
{ width: 20, align: 'left', get: r => pc.white(r.key) },
|
|
420
|
+
{ width: 8, align: 'right', get: r => pc.cyan(String(r.events)) },
|
|
421
|
+
{ width: 6, align: 'left', get: r => pc.dim('runs') },
|
|
422
|
+
]));
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// ── Footer ──────────────────────────────────────────────────────────────
|
|
426
|
+
lines.push('');
|
|
427
|
+
lines.push(' ' + pc.dim(hrule(W - 4)));
|
|
428
|
+
lines.push(' ' + pc.dim('Privacy: metadata only · no raw prompts or transcripts'));
|
|
429
|
+
lines.push(' ' + pc.dim('Costs are estimates when provider prices are unknown.'));
|
|
430
|
+
lines.push('');
|
|
431
|
+
|
|
432
|
+
return lines.join('\n');
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
export async function printTakomiStats(options = {}) {
|
|
436
|
+
const stats = await collectTakomiStats(options);
|
|
437
|
+
if (options.json) console.log(JSON.stringify(stats, null, 2)); else console.log(renderTakomiStats(stats, options));
|
|
438
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "takomi",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.19",
|
|
4
4
|
"description": "🎯 Stop wrestling with AI. Start building with purpose. The artisan's toolkit for agent workflows, Codex skills, and original Takomi capabilities like 21st.dev integration.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|