takomi 2.1.28 → 2.1.30
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/.pi/agents/coder.md +6 -0
- package/.pi/agents/orchestrator.md +16 -0
- package/.pi/extensions/oauth-router/README.md +4 -2
- package/.pi/extensions/oauth-router/commands.ts +38 -14
- package/.pi/extensions/oauth-router/config.ts +12 -3
- package/.pi/extensions/oauth-router/index.ts +1 -1
- package/.pi/extensions/oauth-router/oauth-flow.ts +105 -25
- package/.pi/extensions/oauth-router/provider.ts +112 -12
- package/.pi/extensions/oauth-router/state.ts +12 -1
- package/.pi/extensions/oauth-router/types.ts +17 -2
- package/.pi/extensions/takomi-runtime/context-panel.ts +153 -28
- package/.pi/extensions/takomi-runtime/index.ts +2 -0
- package/.pi/extensions/takomi-runtime/takomi-stats.js +679 -679
- package/.pi/prompts/build-prompt.md +15 -0
- package/.pi/prompts/genesis-prompt.md +8 -0
- package/.pi/prompts/takomi-prompt.md +16 -0
- package/package.json +7 -5
- package/src/pi-harness.js +53 -6
- package/src/takomi-stats.js +749 -679
|
@@ -1,679 +1,679 @@
|
|
|
1
|
-
import { promises as fs } from 'node:fs';
|
|
2
|
-
import os from 'os';
|
|
3
|
-
import path from 'path';
|
|
4
|
-
|
|
5
|
-
const colorEnabled = process.env.NO_COLOR !== '1' && process.env.NO_COLOR !== 'true';
|
|
6
|
-
const ansi = (open, close) => (value) => colorEnabled ? `\u001b[${open}m${value}\u001b[${close}m` : String(value);
|
|
7
|
-
const pc = {
|
|
8
|
-
bold: ansi(1, 22),
|
|
9
|
-
dim: ansi(2, 22),
|
|
10
|
-
white: ansi(37, 39),
|
|
11
|
-
gray: ansi(90, 39),
|
|
12
|
-
cyan: ansi(36, 39),
|
|
13
|
-
blue: ansi(34, 39),
|
|
14
|
-
magenta: ansi(35, 39),
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
const PRICES = {
|
|
18
|
-
'gpt-5.5': [5.00, 0.50, 30.00],
|
|
19
|
-
'gpt-5.4': [2.50, 0.25, 15.00],
|
|
20
|
-
'gpt-5.4-mini': [0.75, 0.075, 4.50],
|
|
21
|
-
'gpt-5.4-nano': [0.20, 0.02, 1.25],
|
|
22
|
-
'gpt-5.3-codex': [2.50, 0.25, 15.00],
|
|
23
|
-
'gpt-5.2-codex': [1.75, 0.175, 14.00],
|
|
24
|
-
'gpt-5-codex': [1.25, 0.125, 10.00],
|
|
25
|
-
'gpt-5.2': [1.75, 0.175, 14.00],
|
|
26
|
-
'gpt-5.1': [1.25, 0.125, 10.00],
|
|
27
|
-
'gpt-5': [1.25, 0.125, 10.00],
|
|
28
|
-
'gpt-5-mini': [0.25, 0.025, 2.00],
|
|
29
|
-
'gpt-4.1': [2.00, 0.50, 8.00],
|
|
30
|
-
'gpt-4o': [2.50, 1.25, 10.00],
|
|
31
|
-
'o4-mini': [1.10, 0.275, 4.40],
|
|
32
|
-
'claude-sonnet-4-6': [3.00, 0.30, 15.00],
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
async function exists(target) { try { await fs.access(target); return true; } catch { return false; } }
|
|
36
|
-
function safeJson(line) { try { return JSON.parse(line); } catch { return null; } }
|
|
37
|
-
function dayOf(ts) { return typeof ts === 'string' && ts.length >= 10 ? ts.slice(0, 10) : 'unknown'; }
|
|
38
|
-
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); }
|
|
39
|
-
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; }
|
|
40
|
-
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)); }
|
|
41
|
-
function fmtMoney(n) { return `$${(n || 0).toFixed(n > 100 ? 0 : 2)}`; }
|
|
42
|
-
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`; }
|
|
43
|
-
const ACTIVE_GAP_THRESHOLD_MS = 15 * 60 * 1000;
|
|
44
|
-
function timestampMs(value) {
|
|
45
|
-
if (typeof value === 'number') return Number.isFinite(value) ? value : null;
|
|
46
|
-
if (typeof value === 'string' && value) {
|
|
47
|
-
const parsed = Date.parse(value);
|
|
48
|
-
return Number.isFinite(parsed) ? parsed : null;
|
|
49
|
-
}
|
|
50
|
-
return null;
|
|
51
|
-
}
|
|
52
|
-
function addTimestamp(target, value) {
|
|
53
|
-
const parsed = timestampMs(value);
|
|
54
|
-
if (parsed !== null) target.push(parsed);
|
|
55
|
-
}
|
|
56
|
-
function activeDuration(timestamps, maxGapMs = ACTIVE_GAP_THRESHOLD_MS) {
|
|
57
|
-
const sorted = [...new Set((timestamps || []).filter(Number.isFinite))].sort((a, b) => a - b);
|
|
58
|
-
let total = 0;
|
|
59
|
-
for (let i = 1; i < sorted.length; i += 1) {
|
|
60
|
-
const delta = sorted[i] - sorted[i - 1];
|
|
61
|
-
if (delta > 0 && delta <= maxGapMs) total += delta;
|
|
62
|
-
}
|
|
63
|
-
return total;
|
|
64
|
-
}
|
|
65
|
-
function parseSince(value) {
|
|
66
|
-
if (!value) return null;
|
|
67
|
-
const raw = String(value).trim().toLowerCase();
|
|
68
|
-
const rel = raw.match(/^(\d+)(d|day|days|w|week|weeks|m|month|months)$/);
|
|
69
|
-
const d = new Date(); d.setHours(0,0,0,0);
|
|
70
|
-
if (rel) {
|
|
71
|
-
const n = Number(rel[1]);
|
|
72
|
-
const unit = rel[2][0];
|
|
73
|
-
d.setDate(d.getDate() - (unit === 'w' ? n * 7 : unit === 'm' ? n * 30 : n));
|
|
74
|
-
return d.toISOString().slice(0, 10);
|
|
75
|
-
}
|
|
76
|
-
if (/^\d{4}-\d{2}-\d{2}$/.test(raw)) return raw;
|
|
77
|
-
return null;
|
|
78
|
-
}
|
|
79
|
-
function projectKey(file) {
|
|
80
|
-
const normalized = String(file || '').replace(/\\/g, '/');
|
|
81
|
-
const marker = '/sessions/';
|
|
82
|
-
const idx = normalized.indexOf(marker);
|
|
83
|
-
if (idx >= 0) {
|
|
84
|
-
const encoded = normalized.slice(idx + marker.length).split('/')[0];
|
|
85
|
-
return encoded.replace(/^--/, '').replace(/--$/, '').replace(/--/g, '/').replace(/-/g, ' ').trim() || 'global';
|
|
86
|
-
}
|
|
87
|
-
const cwdMarker = '/.pi/';
|
|
88
|
-
const pidx = normalized.indexOf(cwdMarker);
|
|
89
|
-
if (pidx >= 0) return normalized.slice(0, pidx).split('/').slice(-2).join('/');
|
|
90
|
-
return 'unknown';
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// ── ANSI-aware string helpers ───────────────────────────────────────────────
|
|
94
|
-
// eslint-disable-next-line no-control-regex
|
|
95
|
-
const ANSI_RE = /\u001b\[[0-9;]*m/g;
|
|
96
|
-
function stripAnsi(s) { return String(s).replace(ANSI_RE, ''); }
|
|
97
|
-
function visLen(s) { return stripAnsi(s).length; }
|
|
98
|
-
function ansiPadEnd(s, w) { return s + ' '.repeat(Math.max(0, w - visLen(s))); }
|
|
99
|
-
function ansiPadStart(s, w) { return ' '.repeat(Math.max(0, w - visLen(s))) + s; }
|
|
100
|
-
|
|
101
|
-
async function files(root, suffix = '.jsonl') {
|
|
102
|
-
const out = [];
|
|
103
|
-
if (!root || !(await exists(root))) return out;
|
|
104
|
-
async function walk(dir) {
|
|
105
|
-
for (const ent of await fs.readdir(dir, { withFileTypes: true })) {
|
|
106
|
-
const p = path.join(dir, ent.name);
|
|
107
|
-
if (ent.isDirectory()) await walk(p); else if (ent.name.endsWith(suffix)) out.push(p);
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
await walk(root); return out;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
function pushTask(taskRows, task) {
|
|
114
|
-
if (!task?.end || task.end === task.start) return;
|
|
115
|
-
task.activeMs = activeDuration(task.activityTimestamps || []);
|
|
116
|
-
taskRows.push(task);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
async function scanPiSessions(root, source, events, sessionRows = [], taskRows = []) {
|
|
120
|
-
for (const file of await files(root)) {
|
|
121
|
-
let provider = 'unknown', model = 'unknown', session = path.basename(file, '.jsonl'), cwd = '', currentTask = null;
|
|
122
|
-
const row = { key: session, session, source, file, project: projectKey(file), cwd, start: '', end: '', turns: 0, messages: 0, toolCalls: 0, subagentCalls: 0, roles: new Map(), stages: new Map(), workflows: new Map(), activeMs: 0, activityTimestamps: [] };
|
|
123
|
-
const text = await fs.readFile(file, 'utf8').catch(() => '');
|
|
124
|
-
for (const line of text.split(/\r?\n/)) {
|
|
125
|
-
const obj = safeJson(line); if (!obj) continue;
|
|
126
|
-
if (obj.timestamp) { row.start ||= obj.timestamp; row.end = obj.timestamp; addTimestamp(row.activityTimestamps, obj.timestamp); }
|
|
127
|
-
if (obj.type === 'session') { session = obj.id || session; cwd = obj.cwd || cwd; row.key = session; row.session = session; row.cwd = cwd; }
|
|
128
|
-
if (obj.type === 'model_change') { provider = obj.provider || provider; model = obj.modelId || model; }
|
|
129
|
-
if (obj.type === 'custom' && obj.customType === 'takomi-runtime-state' && obj.data) {
|
|
130
|
-
const role = obj.data.role || 'unknown';
|
|
131
|
-
const stage = obj.data.stage || 'unknown';
|
|
132
|
-
const workflow = obj.data.workflow || 'unknown';
|
|
133
|
-
row.roles.set(role, (row.roles.get(role) || 0) + 1);
|
|
134
|
-
row.stages.set(stage, (row.stages.get(stage) || 0) + 1);
|
|
135
|
-
row.workflows.set(workflow, (row.workflows.get(workflow) || 0) + 1);
|
|
136
|
-
events.push({ source, file, timestamp: obj.timestamp, day: dayOf(obj.timestamp), session, provider, model, project: projectKey(file), kind: 'role', role, stage, workflow, input: 0, cache: 0, output: 0, total: 0, cost: 0 });
|
|
137
|
-
}
|
|
138
|
-
const msg = obj.type === 'message' && obj.message ? obj.message : null;
|
|
139
|
-
if (msg) {
|
|
140
|
-
row.messages += 1;
|
|
141
|
-
const ts = obj.timestamp || msg.timestamp || '';
|
|
142
|
-
if (msg.role === 'user') {
|
|
143
|
-
pushTask(taskRows, currentTask);
|
|
144
|
-
row.turns += 1;
|
|
145
|
-
const textPart = (msg.content || []).find(p => p?.type === 'text')?.text || '';
|
|
146
|
-
currentTask = { source, file, session, project: projectKey(file), cwd, start: ts, end: ts, provider, model, turns: 1, toolCalls: 0, title: String(textPart).replace(/\s+/g, ' ').trim(), activityTimestamps: [] };
|
|
147
|
-
addTimestamp(currentTask.activityTimestamps, ts);
|
|
148
|
-
} else if (currentTask && ts) {
|
|
149
|
-
currentTask.end = ts;
|
|
150
|
-
addTimestamp(currentTask.activityTimestamps, ts);
|
|
151
|
-
}
|
|
152
|
-
for (const part of msg.content || []) {
|
|
153
|
-
if (!part || part.type !== 'toolCall') continue;
|
|
154
|
-
const name = part.name || 'unknown';
|
|
155
|
-
row.toolCalls += 1;
|
|
156
|
-
if (currentTask) currentTask.toolCalls += 1;
|
|
157
|
-
if (name === 'takomi_subagent') {
|
|
158
|
-
const args = part.arguments || {};
|
|
159
|
-
const count = Array.isArray(args.tasks) ? args.tasks.length : Array.isArray(args.chain) ? args.chain.length : 1;
|
|
160
|
-
row.subagentCalls += count;
|
|
161
|
-
}
|
|
162
|
-
events.push({ source, file, timestamp: obj.timestamp, day: dayOf(obj.timestamp), session, provider, model, project: projectKey(file), kind: 'tool', tool: name, input: 0, cache: 0, output: 0, total: 0, cost: 0 });
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
const u = msg && msg.usage;
|
|
166
|
-
if (u) events.push({ source, file, timestamp: obj.timestamp, day: dayOf(obj.timestamp), session, provider, model, project: projectKey(file), kind: 'usage', 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) });
|
|
167
|
-
}
|
|
168
|
-
pushTask(taskRows, currentTask);
|
|
169
|
-
row.activeMs = activeDuration(row.activityTimestamps);
|
|
170
|
-
if (row.messages || row.toolCalls || row.turns) sessionRows.push(row);
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
async function scanRunHistory(file) {
|
|
175
|
-
const runs = [];
|
|
176
|
-
if (!(await exists(file))) return runs;
|
|
177
|
-
const text = await fs.readFile(file, 'utf8').catch(() => '');
|
|
178
|
-
for (const line of text.split(/\r?\n/)) { const o = safeJson(line); if (o) runs.push(o); }
|
|
179
|
-
return runs;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
export async function collectTakomiStats(opts = {}) {
|
|
183
|
-
const home = opts.home || os.homedir();
|
|
184
|
-
const cwd = opts.cwd || process.cwd();
|
|
185
|
-
const rawEvents = [], rawSessions = [], rawTasks = [];
|
|
186
|
-
const globalSessions = path.resolve(path.join(home, '.pi', 'agent', 'sessions'));
|
|
187
|
-
const projectSessions = path.resolve(path.join(cwd, '.pi', 'agent', 'sessions'));
|
|
188
|
-
await scanPiSessions(globalSessions, 'pi-global', rawEvents, rawSessions, rawTasks);
|
|
189
|
-
if (projectSessions !== globalSessions) await scanPiSessions(projectSessions, 'pi-project', rawEvents, rawSessions, rawTasks);
|
|
190
|
-
await scanPiSessions(path.join(cwd, '.pi', 'takomi'), 'takomi-project', rawEvents, rawSessions, rawTasks);
|
|
191
|
-
const sinceDay = parseSince(opts.since);
|
|
192
|
-
const events = rawEvents.filter(e => !sinceDay || e.day >= sinceDay);
|
|
193
|
-
const sessionRows = rawSessions.filter(s => !sinceDay || dayOf(s.end || s.start) >= sinceDay);
|
|
194
|
-
const taskRows = rawTasks.filter(t => !sinceDay || dayOf(t.end || t.start) >= sinceDay);
|
|
195
|
-
const runs = await scanRunHistory(path.join(home, '.pi', 'agent', 'run-history.jsonl'));
|
|
196
|
-
const byDay = new Map(), byModel = new Map(), bySource = new Map(), byProject = new Map(), byTool = new Map(), byRole = new Map(), byStage = new Map(), byWorkflow = new Map();
|
|
197
|
-
let totals = { input: 0, cache: 0, output: 0, total: 0, cost: 0, events: events.filter(e => e.kind === 'usage').length, toolCalls: 0, turns: 0 };
|
|
198
|
-
for (const s of sessionRows) { totals.toolCalls += s.toolCalls; totals.turns += s.turns; }
|
|
199
|
-
for (const e of events) {
|
|
200
|
-
if (e.kind === 'tool') { add(byTool, e.tool || 'unknown', { total: 0, events: 1 }); continue; }
|
|
201
|
-
if (e.kind === 'role') {
|
|
202
|
-
add(byRole, e.role || 'unknown', { total: 0, events: 1 });
|
|
203
|
-
add(byStage, e.stage || 'unknown', { total: 0, events: 1 });
|
|
204
|
-
add(byWorkflow, e.workflow || 'unknown', { total: 0, events: 1 });
|
|
205
|
-
continue;
|
|
206
|
-
}
|
|
207
|
-
totals.input += e.input; totals.cache += e.cache; totals.output += e.output; totals.total += e.total; totals.cost += e.cost;
|
|
208
|
-
add(byDay, e.day, e); add(byModel, e.model, e); add(bySource, e.source, e); add(byProject, e.project, e);
|
|
209
|
-
}
|
|
210
|
-
const byAgent = new Map(); let longestRun = null;
|
|
211
|
-
for (const r of runs) { add(byAgent, r.agent || 'unknown', { total: 0, events: 1 }); if (!longestRun || (+r.duration||0) > (+longestRun.duration||0)) longestRun = r; }
|
|
212
|
-
const topSessions = [...sessionRows].sort((a,b)=>(b.activeMs||0)-(a.activeMs||0) || b.turns-a.turns || b.toolCalls-a.toolCalls).slice(0, 20);
|
|
213
|
-
const topTasks = [...taskRows].sort((a,b)=>taskDuration(b)-taskDuration(a) || b.toolCalls-a.toolCalls).slice(0, 20);
|
|
214
|
-
const mostSubagentsSession = [...sessionRows].sort((a,b)=>b.subagentCalls-a.subagentCalls)[0] || null;
|
|
215
|
-
return { generatedAt: new Date().toISOString(), cwd, since: sinceDay, totals, sessions: new Set([...events.map(e => e.session), ...sessionRows.map(s => s.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), byTool: [...byTool.values()].sort((a,b)=>b.events-a.events), byRole: [...byRole.values()].sort((a,b)=>b.events-a.events), byStage: [...byStage.values()].sort((a,b)=>b.events-a.events), byWorkflow: [...byWorkflow.values()].sort((a,b)=>b.events-a.events), byAgent: [...byAgent.values()].sort((a,b)=>b.events-a.events), sessionRows, taskRows, topSessions, topTasks, mostSubagentsSession, runs, longestRun, recent: events.sort((a,b)=>(b.timestamp||'').localeCompare(a.timestamp||'')).slice(0, 10) };
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
// ── Streak Calculation ──────────────────────────────────────────────────────
|
|
219
|
-
function calcStreaks(byDay) {
|
|
220
|
-
if (!byDay.length) return { current: 0, longest: 0, quietDays: 0 };
|
|
221
|
-
const daySet = new Set(byDay.map(d => d.key));
|
|
222
|
-
const today = new Date(); today.setHours(0,0,0,0);
|
|
223
|
-
// current streak: walk back from today
|
|
224
|
-
let current = 0;
|
|
225
|
-
for (let d = new Date(today); ; d.setDate(d.getDate() - 1)) {
|
|
226
|
-
const key = d.toISOString().slice(0, 10);
|
|
227
|
-
if (daySet.has(key)) current++; else break;
|
|
228
|
-
}
|
|
229
|
-
// longest streak: walk all sorted days
|
|
230
|
-
const sorted = [...daySet].sort();
|
|
231
|
-
let longest = 0, run = 1;
|
|
232
|
-
for (let i = 1; i < sorted.length; i++) {
|
|
233
|
-
const prev = new Date(sorted[i - 1]); prev.setDate(prev.getDate() + 1);
|
|
234
|
-
if (prev.toISOString().slice(0, 10) === sorted[i]) { run++; } else { longest = Math.max(longest, run); run = 1; }
|
|
235
|
-
}
|
|
236
|
-
longest = Math.max(longest, run);
|
|
237
|
-
// quiet days
|
|
238
|
-
const first = new Date(sorted[0]);
|
|
239
|
-
const span = Math.round((today - first) / 86400000) + 1;
|
|
240
|
-
return { current, longest, quietDays: Math.max(0, span - sorted.length) };
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
// ── GitHub-style Heatmap Grid ───────────────────────────────────────────────
|
|
244
|
-
function heatmapGrid(byDay) {
|
|
245
|
-
const dayMap = new Map(byDay.map(d => [d.key, d.total]));
|
|
246
|
-
const max = Math.max(1, ...byDay.map(d => d.total));
|
|
247
|
-
|
|
248
|
-
// Determine range: last ~26 weeks (half year) ending at current week
|
|
249
|
-
const today = new Date(); today.setHours(0,0,0,0);
|
|
250
|
-
// End at end of current week (Sunday)
|
|
251
|
-
const endDate = new Date(today);
|
|
252
|
-
const todayDow = endDate.getDay(); // 0=Sun, 1=Mon...
|
|
253
|
-
if (todayDow !== 0) endDate.setDate(endDate.getDate() + (7 - todayDow));
|
|
254
|
-
// Start 26 weeks back on Monday
|
|
255
|
-
const startDate = new Date(endDate);
|
|
256
|
-
startDate.setDate(startDate.getDate() - (26 * 7) + 1);
|
|
257
|
-
while (startDate.getDay() !== 1) startDate.setDate(startDate.getDate() - 1);
|
|
258
|
-
|
|
259
|
-
// Build grid: 7 rows (Mon..Sun), N columns (weeks)
|
|
260
|
-
const weeks = [];
|
|
261
|
-
const monthPositions = []; // { col, label }
|
|
262
|
-
const cursor = new Date(startDate);
|
|
263
|
-
let col = 0;
|
|
264
|
-
let lastMonth = -1;
|
|
265
|
-
|
|
266
|
-
while (cursor <= endDate) {
|
|
267
|
-
const week = [];
|
|
268
|
-
for (let dow = 0; dow < 7; dow++) {
|
|
269
|
-
const key = cursor.toISOString().slice(0, 10);
|
|
270
|
-
const val = cursor <= today ? (dayMap.get(key) || 0) : -1; // -1 = future
|
|
271
|
-
week.push(val);
|
|
272
|
-
// Track month transitions on the Monday of each week
|
|
273
|
-
if (dow === 0) {
|
|
274
|
-
const m = cursor.getMonth();
|
|
275
|
-
if (m !== lastMonth) {
|
|
276
|
-
const monthNames = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
|
|
277
|
-
monthPositions.push({ col, label: monthNames[m] });
|
|
278
|
-
lastMonth = m;
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
cursor.setDate(cursor.getDate() + 1);
|
|
282
|
-
}
|
|
283
|
-
weeks.push(week);
|
|
284
|
-
col++;
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
// Intensity cell — use ■ for filled, · for empty
|
|
288
|
-
const SQ = '■';
|
|
289
|
-
const EMPTY = '·';
|
|
290
|
-
function cell(val) {
|
|
291
|
-
if (val < 0) return ' '; // future
|
|
292
|
-
if (val === 0) return pc.gray(EMPTY);
|
|
293
|
-
const x = val / max;
|
|
294
|
-
if (x < 0.12) return pc.dim(pc.cyan(SQ));
|
|
295
|
-
if (x < 0.30) return pc.cyan(SQ);
|
|
296
|
-
if (x < 0.55) return pc.blue(SQ);
|
|
297
|
-
if (x < 0.80) return pc.magenta(SQ);
|
|
298
|
-
return pc.bold(pc.magenta(SQ));
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
const dayLabels = ['Mon',' ','Wed',' ','Fri',' ','Sun'];
|
|
302
|
-
const rows = [];
|
|
303
|
-
|
|
304
|
-
// Each cell is 2 chars wide (char + space) in the grid
|
|
305
|
-
for (let dow = 0; dow < 7; dow++) {
|
|
306
|
-
const prefix = pc.dim(dayLabels[dow]);
|
|
307
|
-
const cells = weeks.map(w => cell(w[dow]));
|
|
308
|
-
rows.push(` ${prefix} ${cells.join(' ')}`);
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
// Month label row — positioned under the correct columns
|
|
312
|
-
// Each column is 2 chars wide (cell + space separator)
|
|
313
|
-
let labelStr = '';
|
|
314
|
-
let prevEnd = 0;
|
|
315
|
-
for (const ml of monthPositions) {
|
|
316
|
-
const targetPos = ml.col * 2; // 2 chars per column (char + space)
|
|
317
|
-
const gap = Math.max(0, targetPos - prevEnd);
|
|
318
|
-
labelStr += ' '.repeat(gap) + ml.label;
|
|
319
|
-
prevEnd = targetPos + ml.label.length;
|
|
320
|
-
}
|
|
321
|
-
rows.push(` ${pc.dim(labelStr)}`);
|
|
322
|
-
|
|
323
|
-
// Legend row
|
|
324
|
-
rows.push('');
|
|
325
|
-
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')}`);
|
|
326
|
-
|
|
327
|
-
return rows.join('\n');
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
// ── Box Drawing Helpers ─────────────────────────────────────────────────────
|
|
331
|
-
function hrule(w, ch = '─') { return ch.repeat(w); }
|
|
332
|
-
|
|
333
|
-
function center(text, width) {
|
|
334
|
-
const vl = visLen(text);
|
|
335
|
-
const pad = Math.max(0, Math.floor((width - vl) / 2));
|
|
336
|
-
return ' '.repeat(pad) + text;
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
function statCard(value, label, color = pc.white) {
|
|
340
|
-
return { value: String(value), label, color };
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
// ── Table Helper ────────────────────────────────────────────────────────────
|
|
344
|
-
function renderTable(title, rows, columns) {
|
|
345
|
-
const lines = [];
|
|
346
|
-
lines.push(' ' + pc.bold(pc.cyan(title)));
|
|
347
|
-
lines.push(' ' + pc.dim(hrule(columns.reduce((s, c) => s + c.width, 0) + columns.length * 2)));
|
|
348
|
-
for (const row of rows) {
|
|
349
|
-
let line = ' ';
|
|
350
|
-
for (const col of columns) {
|
|
351
|
-
const val = String(col.get(row));
|
|
352
|
-
line += col.align === 'right' ? ansiPadStart(val, col.width) : ansiPadEnd(val, col.width);
|
|
353
|
-
line += ' ';
|
|
354
|
-
}
|
|
355
|
-
lines.push(line);
|
|
356
|
-
}
|
|
357
|
-
return lines.join('\n');
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
function sessionLabel(row, width = 36) {
|
|
361
|
-
const project = row.project || row.cwd || row.key || 'unknown';
|
|
362
|
-
return project.length > width ? '…' + project.slice(-(width - 1)) : project;
|
|
363
|
-
}
|
|
364
|
-
function sessionDay(row) { return dayOf(row.start || row.end).slice(5) || '??-??'; }
|
|
365
|
-
function sessionDuration(row) {
|
|
366
|
-
return row?.activeMs || 0;
|
|
367
|
-
}
|
|
368
|
-
function taskDuration(row) {
|
|
369
|
-
return row?.activeMs ?? activeDuration(row?.activityTimestamps || []);
|
|
370
|
-
}
|
|
371
|
-
function taskLabel(row, width = 34) {
|
|
372
|
-
const label = row?.title || row?.project || row?.session || 'unknown';
|
|
373
|
-
return label.length > width ? label.slice(0, width - 1) + '…' : label;
|
|
374
|
-
}
|
|
375
|
-
function runLabel(run, width = 28) {
|
|
376
|
-
const label = run ? `${run.agent || 'unknown'}: ${run.task || ''}`.trim() : '-';
|
|
377
|
-
return label.length > width ? label.slice(0, width - 1) + '…' : label;
|
|
378
|
-
}
|
|
379
|
-
function indentWrap(text, width = 76, indent = ' ') {
|
|
380
|
-
const words = String(text || '').split(/\s+/).filter(Boolean);
|
|
381
|
-
const lines = [];
|
|
382
|
-
let line = '';
|
|
383
|
-
for (const word of words) {
|
|
384
|
-
if ((line + ' ' + word).trim().length > width && line) { lines.push(line); line = word; }
|
|
385
|
-
else line = (line + ' ' + word).trim();
|
|
386
|
-
}
|
|
387
|
-
if (line) lines.push(line);
|
|
388
|
-
return lines.map((l, i) => (i ? indent : '') + l).join('\n');
|
|
389
|
-
}
|
|
390
|
-
function renderFullList(title, rows, renderRow, limit = 20) {
|
|
391
|
-
const lines = ['\n' + pc.bold(pc.magenta('Takomi Stats')), renderTable(title, [], [{ width: 74 }])];
|
|
392
|
-
rows.slice(0, limit).forEach((row, i) => lines.push(renderRow(row, i)));
|
|
393
|
-
lines.push('\n' + pc.dim('Privacy: metadata only · no raw prompts or transcripts'));
|
|
394
|
-
return lines.join('\n');
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
function renderFocusedView(stats, opts = {}) {
|
|
398
|
-
const view = opts.view;
|
|
399
|
-
const limit = opts.limit || 20;
|
|
400
|
-
if (!view || view === 'overview') return null;
|
|
401
|
-
if (view === 'projects-full' || view === 'project-full') return renderFullList('Top Projects — Full Names', stats.byProject, (r, i) => [
|
|
402
|
-
` ${pc.dim(String(i + 1).padStart(2, '0') + '.')} ${pc.white(r.key)}`,
|
|
403
|
-
` ${pc.cyan(fmtTokens(r.total))} ${pc.dim(fmtMoney(r.cost))} ${pc.dim(r.events + ' calls')}`,
|
|
404
|
-
].join('\n'), limit);
|
|
405
|
-
if (view === 'sessions-full' || view === 'session-full') return renderFullList('Longest Active Sessions — Full Names', stats.topSessions, (r, i) => [
|
|
406
|
-
` ${pc.dim(String(i + 1).padStart(2, '0') + '.')} ${pc.white(r.project || r.cwd || r.key || 'unknown')}`,
|
|
407
|
-
` ${pc.cyan(ms(sessionDuration(r)))} ${pc.dim(r.turns + ' turns')} ${pc.dim(r.toolCalls + ' tools')}`,
|
|
408
|
-
` ${pc.dim(r.file || '')}`,
|
|
409
|
-
].join('\n'), limit);
|
|
410
|
-
if (view === 'tasks-full' || view === 'task-full') return renderFullList('Longest Active Turns — Full Prompts', stats.topTasks || [], (r, i) => [
|
|
411
|
-
` ${pc.dim(String(i + 1).padStart(2, '0') + '.')} ${pc.cyan(ms(taskDuration(r)))} ${pc.magenta(r.toolCalls + ' tools')} ${pc.dim(dayOf(r.start))}`,
|
|
412
|
-
` ${pc.white(indentWrap(r.title || r.project || r.session || 'unknown'))}`,
|
|
413
|
-
` ${pc.dim(r.project || '')}`,
|
|
414
|
-
].join('\n'), limit);
|
|
415
|
-
const tables = {
|
|
416
|
-
models: ['Top Models', stats.byModel, [
|
|
417
|
-
{ width: 26, align: 'left', get: r => pc.white(r.key) },
|
|
418
|
-
{ width: 10, align: 'right', get: r => pc.cyan(fmtTokens(r.total)) },
|
|
419
|
-
{ width: 10, align: 'right', get: r => pc.dim(fmtMoney(r.cost)) },
|
|
420
|
-
{ width: 12, align: 'right', get: r => pc.dim(r.events + ' calls') },
|
|
421
|
-
]],
|
|
422
|
-
sources: ['Sources', stats.bySource, [
|
|
423
|
-
{ width: 22, align: 'left', get: r => pc.white(r.key) },
|
|
424
|
-
{ width: 10, align: 'right', get: r => pc.cyan(fmtTokens(r.total)) },
|
|
425
|
-
{ width: 14, align: 'right', get: r => pc.dim(r.events + ' events') },
|
|
426
|
-
]],
|
|
427
|
-
projects: ['Top Projects', stats.byProject, [
|
|
428
|
-
{ width: 42, align: 'left', get: r => pc.white(r.key.length > 42 ? '…' + r.key.slice(-41) : r.key) },
|
|
429
|
-
{ width: 10, align: 'right', get: r => pc.cyan(fmtTokens(r.total)) },
|
|
430
|
-
{ width: 10, align: 'right', get: r => pc.dim(fmtMoney(r.cost)) },
|
|
431
|
-
{ width: 12, align: 'right', get: r => pc.dim(r.events + ' calls') },
|
|
432
|
-
]],
|
|
433
|
-
agents: ['Main Agent Roles', stats.byRole, [
|
|
434
|
-
{ width: 24, align: 'left', get: r => pc.white(r.key) },
|
|
435
|
-
{ width: 8, align: 'right', get: r => pc.cyan(String(r.events)) },
|
|
436
|
-
{ width: 12, align: 'left', get: () => pc.dim('state hits') },
|
|
437
|
-
]],
|
|
438
|
-
subagents: ['Top Subagents', stats.byAgent, [
|
|
439
|
-
{ width: 24, align: 'left', get: r => pc.white(r.key) },
|
|
440
|
-
{ width: 8, align: 'right', get: r => pc.cyan(String(r.events)) },
|
|
441
|
-
{ width: 8, align: 'left', get: () => pc.dim('runs') },
|
|
442
|
-
]],
|
|
443
|
-
tools: ['Top Tools', stats.byTool, [
|
|
444
|
-
{ width: 28, align: 'left', get: r => pc.white(r.key) },
|
|
445
|
-
{ width: 10, align: 'right', get: r => pc.cyan(String(r.events)) },
|
|
446
|
-
{ width: 8, align: 'left', get: () => pc.dim('calls') },
|
|
447
|
-
]],
|
|
448
|
-
sessions: ['Longest Active Sessions', stats.topSessions, [
|
|
449
|
-
{ width: 6, align: 'left', get: r => pc.dim(sessionDay(r)) },
|
|
450
|
-
{ width: 30, align: 'left', get: r => pc.white(sessionLabel(r, 30)) },
|
|
451
|
-
{ width: 9, align: 'right', get: r => pc.cyan(ms(sessionDuration(r))) },
|
|
452
|
-
{ width: 8, align: 'right', get: r => pc.cyan(String(r.turns)) },
|
|
453
|
-
{ width: 8, align: 'left', get: () => pc.dim('turns') },
|
|
454
|
-
{ width: 8, align: 'right', get: r => pc.cyan(String(r.toolCalls)) },
|
|
455
|
-
{ width: 8, align: 'left', get: () => pc.dim('tools') },
|
|
456
|
-
]],
|
|
457
|
-
tasks: ['Longest Active Turns', stats.topTasks || [], [
|
|
458
|
-
{ width: 6, align: 'left', get: r => pc.dim(dayOf(r.start).slice(5)) },
|
|
459
|
-
{ width: 9, align: 'right', get: r => pc.cyan(ms(taskDuration(r))) },
|
|
460
|
-
{ width: 11, align: 'right', get: r => pc.magenta(`${r.toolCalls} tools`) },
|
|
461
|
-
{ width: 36, align: 'left', get: r => pc.white(taskLabel(r, 36)) },
|
|
462
|
-
]],
|
|
463
|
-
daily: ['Daily Usage', [...stats.byDay].reverse(), [
|
|
464
|
-
{ width: 12, align: 'left', get: r => pc.white(r.key) },
|
|
465
|
-
{ width: 10, align: 'right', get: r => pc.cyan(fmtTokens(r.total)) },
|
|
466
|
-
{ width: 10, align: 'right', get: r => pc.dim(fmtMoney(r.cost)) },
|
|
467
|
-
{ width: 12, align: 'right', get: r => pc.dim(r.events + ' calls') },
|
|
468
|
-
]],
|
|
469
|
-
};
|
|
470
|
-
const spec = tables[view === 'project' ? 'projects' : view];
|
|
471
|
-
if (!spec) return null;
|
|
472
|
-
const [title, rows, cols] = spec;
|
|
473
|
-
const suffix = stats.since ? pc.dim(`\n Since: ${stats.since}`) : '';
|
|
474
|
-
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');
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
// ── Main Render ─────────────────────────────────────────────────────────────
|
|
478
|
-
export function renderTakomiStats(stats, opts = {}) {
|
|
479
|
-
const focused = renderFocusedView(stats, opts);
|
|
480
|
-
if (focused) return focused;
|
|
481
|
-
const W = Math.min(process.stdout.columns || 80, 86);
|
|
482
|
-
const topModel = stats.byModel[0]?.key || 'unknown';
|
|
483
|
-
const peak = stats.byDay.reduce((a,b) => b.total > (a?.total||0) ? b : a, null);
|
|
484
|
-
const streaks = calcStreaks(stats.byDay);
|
|
485
|
-
const longestSession = stats.topSessions[0] || null;
|
|
486
|
-
const longestTask = stats.topTasks?.[0] || null;
|
|
487
|
-
const lines = [];
|
|
488
|
-
|
|
489
|
-
// ── Header ────────────────────────────────────────────────────────────
|
|
490
|
-
lines.push('');
|
|
491
|
-
lines.push(pc.cyan(' ' + hrule(W - 4, '━')));
|
|
492
|
-
lines.push('');
|
|
493
|
-
lines.push(center(pc.bold(pc.white('T A K O M I S T A T S')), W));
|
|
494
|
-
const user = process.env.USERNAME || process.env.USER || 'local';
|
|
495
|
-
lines.push(center(pc.dim(`@${user} · Takomi`), W));
|
|
496
|
-
lines.push('');
|
|
497
|
-
lines.push(pc.cyan(' ' + hrule(W - 4)));
|
|
498
|
-
|
|
499
|
-
// ── Stat Cards Row 1 ─────────────────────────────────────────────────
|
|
500
|
-
const cards1 = [
|
|
501
|
-
statCard(fmtTokens(stats.totals.total), 'Lifetime Tokens'),
|
|
502
|
-
statCard(fmtTokens(stats.totals.cache), 'Cache Tokens'),
|
|
503
|
-
statCard(fmtMoney(stats.totals.cost), 'Est. Cost'),
|
|
504
|
-
statCard(String(stats.sessions), 'Sessions'),
|
|
505
|
-
statCard(String(stats.totals.turns), 'Main Turns'),
|
|
506
|
-
];
|
|
507
|
-
|
|
508
|
-
const cardW = Math.floor((W - 4) / cards1.length);
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
function buildCardLines(cards) {
|
|
512
|
-
let vStr = ' ';
|
|
513
|
-
let lStr = ' ';
|
|
514
|
-
for (const c of cards) {
|
|
515
|
-
const vPad = Math.max(0, Math.floor((cardW - c.value.length) / 2));
|
|
516
|
-
const lPad = Math.max(0, Math.floor((cardW - c.label.length) / 2));
|
|
517
|
-
const color = c.color || pc.white;
|
|
518
|
-
const vContent = ' '.repeat(vPad) + pc.bold(color(c.value));
|
|
519
|
-
const lContent = ' '.repeat(lPad) + pc.dim(color(c.label));
|
|
520
|
-
// Pad to cardW visible chars
|
|
521
|
-
vStr += ansiPadEnd(vContent, cardW);
|
|
522
|
-
lStr += ansiPadEnd(lContent, cardW);
|
|
523
|
-
}
|
|
524
|
-
return [vStr, lStr];
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
lines.push('');
|
|
528
|
-
const [v1, l1] = buildCardLines(cards1);
|
|
529
|
-
lines.push(v1);
|
|
530
|
-
lines.push(l1);
|
|
531
|
-
|
|
532
|
-
// ── Stat Cards Row 2 ─────────────────────────────────────────────────
|
|
533
|
-
lines.push('');
|
|
534
|
-
const cards2 = [
|
|
535
|
-
statCard(peak ? fmtTokens(peak.total) : '-', 'Peak Day'),
|
|
536
|
-
statCard(topModel, 'Top Model'),
|
|
537
|
-
statCard(String(stats.totals.toolCalls), 'Tool Calls'),
|
|
538
|
-
statCard(`${streaks.current} days`, 'Current Streak'),
|
|
539
|
-
statCard(`${streaks.longest} days`, 'Longest Streak'),
|
|
540
|
-
];
|
|
541
|
-
|
|
542
|
-
const [v2, l2] = buildCardLines(cards2);
|
|
543
|
-
lines.push(v2);
|
|
544
|
-
lines.push(l2);
|
|
545
|
-
|
|
546
|
-
// ── Duration Cards ────────────────────────────────────────────────────
|
|
547
|
-
lines.push('');
|
|
548
|
-
const cards3 = [
|
|
549
|
-
statCard(longestSession ? ms(sessionDuration(longestSession)) : '-', 'Top Active Session', pc.cyan),
|
|
550
|
-
statCard(longestSession ? String(longestSession.turns) : '-', 'Turns in Session', pc.cyan),
|
|
551
|
-
statCard(longestTask ? ms(taskDuration(longestTask)) : '-', 'Longest Active Turn', pc.magenta),
|
|
552
|
-
statCard(longestTask ? String(longestTask.toolCalls) : '-', 'Tools in Task', pc.magenta),
|
|
553
|
-
statCard(stats.mostSubagentsSession ? String(stats.mostSubagentsSession.subagentCalls) : '0', 'Max Subagents', pc.blue),
|
|
554
|
-
];
|
|
555
|
-
const [v3, l3] = buildCardLines(cards3);
|
|
556
|
-
lines.push(v3);
|
|
557
|
-
lines.push(l3);
|
|
558
|
-
|
|
559
|
-
// ── Info line ─────────────────────────────────────────────────────────
|
|
560
|
-
lines.push('');
|
|
561
|
-
const infoText = `Peak: ${peak?.key || '-'} · ${streaks.quietDays} quiet days · ${stats.totals.events.toLocaleString()} events · active gaps ≤15m${stats.since ? ` · since ${stats.since}` : ''}`;
|
|
562
|
-
lines.push(center(pc.dim(infoText), W));
|
|
563
|
-
|
|
564
|
-
lines.push('');
|
|
565
|
-
lines.push(pc.cyan(' ' + hrule(W - 4, '━')));
|
|
566
|
-
|
|
567
|
-
// ── Activity Heatmap ────────────────────────────────────────────────────
|
|
568
|
-
lines.push('');
|
|
569
|
-
lines.push(' ' + pc.bold(pc.cyan('Token Activity')));
|
|
570
|
-
lines.push(' ' + pc.dim(hrule(W - 4)));
|
|
571
|
-
lines.push(heatmapGrid(stats.byDay));
|
|
572
|
-
|
|
573
|
-
// ── Models Table ────────────────────────────────────────────────────────
|
|
574
|
-
lines.push('');
|
|
575
|
-
const modelLimit = opts.limit || 8;
|
|
576
|
-
lines.push(renderTable('Top Models', stats.byModel.slice(0, modelLimit), [
|
|
577
|
-
{ width: 24, align: 'left', get: r => pc.white(r.key) },
|
|
578
|
-
{ width: 10, align: 'right', get: r => pc.cyan(fmtTokens(r.total)) },
|
|
579
|
-
{ width: 10, align: 'right', get: r => pc.dim(fmtMoney(r.cost)) },
|
|
580
|
-
{ width: 12, align: 'right', get: r => pc.dim(r.events + ' calls') },
|
|
581
|
-
]));
|
|
582
|
-
|
|
583
|
-
// ── Projects Table ──────────────────────────────────────────────────────
|
|
584
|
-
if (stats.byProject.length) {
|
|
585
|
-
lines.push('');
|
|
586
|
-
lines.push(renderTable('Top Projects', stats.byProject.slice(0, 5), [
|
|
587
|
-
{ width: 34, align: 'left', get: r => pc.white(r.key.length > 34 ? '…' + r.key.slice(-33) : r.key) },
|
|
588
|
-
{ width: 10, align: 'right', get: r => pc.cyan(fmtTokens(r.total)) },
|
|
589
|
-
{ width: 10, align: 'right', get: r => pc.dim(fmtMoney(r.cost)) },
|
|
590
|
-
]));
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
// ── Main Agent Roles Table ──────────────────────────────────────────────
|
|
594
|
-
if (stats.byRole.length) {
|
|
595
|
-
lines.push('');
|
|
596
|
-
lines.push(renderTable('Main Agent Roles', stats.byRole.slice(0, modelLimit), [
|
|
597
|
-
{ width: 20, align: 'left', get: r => pc.white(r.key) },
|
|
598
|
-
{ width: 8, align: 'right', get: r => pc.cyan(String(r.events)) },
|
|
599
|
-
{ width: 10, align: 'left', get: r => pc.dim('state hits') },
|
|
600
|
-
]));
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
// ── Main Session Table ──────────────────────────────────────────────────
|
|
604
|
-
if (stats.topSessions.length) {
|
|
605
|
-
lines.push('');
|
|
606
|
-
lines.push(renderTable('Longest Active Sessions', stats.topSessions.slice(0, 5), [
|
|
607
|
-
{ width: 6, align: 'left', get: r => pc.dim(sessionDay(r)) },
|
|
608
|
-
{ width: 28, align: 'left', get: r => pc.white(sessionLabel(r, 28)) },
|
|
609
|
-
{ width: 9, align: 'right', get: r => pc.cyan(ms(sessionDuration(r))) },
|
|
610
|
-
{ width: 8, align: 'right', get: r => pc.cyan(String(r.turns)) },
|
|
611
|
-
{ width: 8, align: 'left', get: r => pc.dim('turns') },
|
|
612
|
-
{ width: 8, align: 'right', get: r => pc.cyan(String(r.toolCalls)) },
|
|
613
|
-
{ width: 6, align: 'left', get: r => pc.dim('tools') },
|
|
614
|
-
]));
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
// ── Longest Active Turns ───────────────────────────────────────────────
|
|
618
|
-
if (stats.topTasks?.length) {
|
|
619
|
-
lines.push('');
|
|
620
|
-
lines.push(renderTable('Longest Active Turns', stats.topTasks.slice(0, 5), [
|
|
621
|
-
{ width: 6, align: 'left', get: r => pc.dim(dayOf(r.start).slice(5)) },
|
|
622
|
-
{ width: 9, align: 'right', get: r => pc.cyan(ms(taskDuration(r))) },
|
|
623
|
-
{ width: 11, align: 'right', get: r => pc.magenta(`${r.toolCalls} tools`) },
|
|
624
|
-
{ width: 34, align: 'left', get: r => pc.white(taskLabel(r, 34)) },
|
|
625
|
-
]));
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
// ── Longest Subagent Run ───────────────────────────────────────────────
|
|
629
|
-
if (stats.longestRun) {
|
|
630
|
-
lines.push('');
|
|
631
|
-
lines.push(renderTable('Longest Subagent Run', [stats.longestRun], [
|
|
632
|
-
{ width: 22, align: 'left', get: r => pc.white(r.agent || 'unknown') },
|
|
633
|
-
{ width: 10, align: 'right', get: r => pc.cyan(ms(+r.duration || 0)) },
|
|
634
|
-
{ width: 30, align: 'left', get: r => pc.dim(runLabel(r, 30)) },
|
|
635
|
-
]));
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
// ── Tools Table ─────────────────────────────────────────────────────────
|
|
639
|
-
if (stats.byTool.length) {
|
|
640
|
-
lines.push('');
|
|
641
|
-
lines.push(renderTable('Top Tools', stats.byTool.slice(0, modelLimit), [
|
|
642
|
-
{ width: 24, align: 'left', get: r => pc.white(r.key) },
|
|
643
|
-
{ width: 8, align: 'right', get: r => pc.cyan(String(r.events)) },
|
|
644
|
-
{ width: 6, align: 'left', get: r => pc.dim('calls') },
|
|
645
|
-
]));
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
// ── Sources Table ───────────────────────────────────────────────────────
|
|
649
|
-
lines.push('');
|
|
650
|
-
lines.push(renderTable('Sources', stats.bySource, [
|
|
651
|
-
{ width: 20, align: 'left', get: r => pc.white(r.key) },
|
|
652
|
-
{ width: 10, align: 'right', get: r => pc.cyan(fmtTokens(r.total)) },
|
|
653
|
-
{ width: 14, align: 'right', get: r => pc.dim(r.events + ' events') },
|
|
654
|
-
]));
|
|
655
|
-
|
|
656
|
-
// ── Subagents Table ─────────────────────────────────────────────────────
|
|
657
|
-
if (stats.byAgent.length) {
|
|
658
|
-
lines.push('');
|
|
659
|
-
lines.push(renderTable('Top Subagents', stats.byAgent.slice(0, modelLimit), [
|
|
660
|
-
{ width: 20, align: 'left', get: r => pc.white(r.key) },
|
|
661
|
-
{ width: 8, align: 'right', get: r => pc.cyan(String(r.events)) },
|
|
662
|
-
{ width: 6, align: 'left', get: r => pc.dim('runs') },
|
|
663
|
-
]));
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
// ── Footer ──────────────────────────────────────────────────────────────
|
|
667
|
-
lines.push('');
|
|
668
|
-
lines.push(' ' + pc.dim(hrule(W - 4)));
|
|
669
|
-
lines.push(' ' + pc.dim('Privacy: metadata only · no raw prompts or transcripts'));
|
|
670
|
-
lines.push(' ' + pc.dim('Costs are estimates when provider prices are unknown.'));
|
|
671
|
-
lines.push('');
|
|
672
|
-
|
|
673
|
-
return lines.join('\n');
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
export async function printTakomiStats(options = {}) {
|
|
677
|
-
const stats = await collectTakomiStats(options);
|
|
678
|
-
if (options.json) console.log(JSON.stringify(stats, null, 2)); else console.log(renderTakomiStats(stats, options));
|
|
679
|
-
}
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
|
|
5
|
+
const colorEnabled = process.env.NO_COLOR !== '1' && process.env.NO_COLOR !== 'true';
|
|
6
|
+
const ansi = (open, close) => (value) => colorEnabled ? `\u001b[${open}m${value}\u001b[${close}m` : String(value);
|
|
7
|
+
const pc = {
|
|
8
|
+
bold: ansi(1, 22),
|
|
9
|
+
dim: ansi(2, 22),
|
|
10
|
+
white: ansi(37, 39),
|
|
11
|
+
gray: ansi(90, 39),
|
|
12
|
+
cyan: ansi(36, 39),
|
|
13
|
+
blue: ansi(34, 39),
|
|
14
|
+
magenta: ansi(35, 39),
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const PRICES = {
|
|
18
|
+
'gpt-5.5': [5.00, 0.50, 30.00],
|
|
19
|
+
'gpt-5.4': [2.50, 0.25, 15.00],
|
|
20
|
+
'gpt-5.4-mini': [0.75, 0.075, 4.50],
|
|
21
|
+
'gpt-5.4-nano': [0.20, 0.02, 1.25],
|
|
22
|
+
'gpt-5.3-codex': [2.50, 0.25, 15.00],
|
|
23
|
+
'gpt-5.2-codex': [1.75, 0.175, 14.00],
|
|
24
|
+
'gpt-5-codex': [1.25, 0.125, 10.00],
|
|
25
|
+
'gpt-5.2': [1.75, 0.175, 14.00],
|
|
26
|
+
'gpt-5.1': [1.25, 0.125, 10.00],
|
|
27
|
+
'gpt-5': [1.25, 0.125, 10.00],
|
|
28
|
+
'gpt-5-mini': [0.25, 0.025, 2.00],
|
|
29
|
+
'gpt-4.1': [2.00, 0.50, 8.00],
|
|
30
|
+
'gpt-4o': [2.50, 1.25, 10.00],
|
|
31
|
+
'o4-mini': [1.10, 0.275, 4.40],
|
|
32
|
+
'claude-sonnet-4-6': [3.00, 0.30, 15.00],
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
async function exists(target) { try { await fs.access(target); return true; } catch { return false; } }
|
|
36
|
+
function safeJson(line) { try { return JSON.parse(line); } catch { return null; } }
|
|
37
|
+
function dayOf(ts) { return typeof ts === 'string' && ts.length >= 10 ? ts.slice(0, 10) : 'unknown'; }
|
|
38
|
+
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); }
|
|
39
|
+
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; }
|
|
40
|
+
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)); }
|
|
41
|
+
function fmtMoney(n) { return `$${(n || 0).toFixed(n > 100 ? 0 : 2)}`; }
|
|
42
|
+
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`; }
|
|
43
|
+
const ACTIVE_GAP_THRESHOLD_MS = 15 * 60 * 1000;
|
|
44
|
+
function timestampMs(value) {
|
|
45
|
+
if (typeof value === 'number') return Number.isFinite(value) ? value : null;
|
|
46
|
+
if (typeof value === 'string' && value) {
|
|
47
|
+
const parsed = Date.parse(value);
|
|
48
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
49
|
+
}
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
function addTimestamp(target, value) {
|
|
53
|
+
const parsed = timestampMs(value);
|
|
54
|
+
if (parsed !== null) target.push(parsed);
|
|
55
|
+
}
|
|
56
|
+
function activeDuration(timestamps, maxGapMs = ACTIVE_GAP_THRESHOLD_MS) {
|
|
57
|
+
const sorted = [...new Set((timestamps || []).filter(Number.isFinite))].sort((a, b) => a - b);
|
|
58
|
+
let total = 0;
|
|
59
|
+
for (let i = 1; i < sorted.length; i += 1) {
|
|
60
|
+
const delta = sorted[i] - sorted[i - 1];
|
|
61
|
+
if (delta > 0 && delta <= maxGapMs) total += delta;
|
|
62
|
+
}
|
|
63
|
+
return total;
|
|
64
|
+
}
|
|
65
|
+
function parseSince(value) {
|
|
66
|
+
if (!value) return null;
|
|
67
|
+
const raw = String(value).trim().toLowerCase();
|
|
68
|
+
const rel = raw.match(/^(\d+)(d|day|days|w|week|weeks|m|month|months)$/);
|
|
69
|
+
const d = new Date(); d.setHours(0,0,0,0);
|
|
70
|
+
if (rel) {
|
|
71
|
+
const n = Number(rel[1]);
|
|
72
|
+
const unit = rel[2][0];
|
|
73
|
+
d.setDate(d.getDate() - (unit === 'w' ? n * 7 : unit === 'm' ? n * 30 : n));
|
|
74
|
+
return d.toISOString().slice(0, 10);
|
|
75
|
+
}
|
|
76
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(raw)) return raw;
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
function projectKey(file) {
|
|
80
|
+
const normalized = String(file || '').replace(/\\/g, '/');
|
|
81
|
+
const marker = '/sessions/';
|
|
82
|
+
const idx = normalized.indexOf(marker);
|
|
83
|
+
if (idx >= 0) {
|
|
84
|
+
const encoded = normalized.slice(idx + marker.length).split('/')[0];
|
|
85
|
+
return encoded.replace(/^--/, '').replace(/--$/, '').replace(/--/g, '/').replace(/-/g, ' ').trim() || 'global';
|
|
86
|
+
}
|
|
87
|
+
const cwdMarker = '/.pi/';
|
|
88
|
+
const pidx = normalized.indexOf(cwdMarker);
|
|
89
|
+
if (pidx >= 0) return normalized.slice(0, pidx).split('/').slice(-2).join('/');
|
|
90
|
+
return 'unknown';
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ── ANSI-aware string helpers ───────────────────────────────────────────────
|
|
94
|
+
// eslint-disable-next-line no-control-regex
|
|
95
|
+
const ANSI_RE = /\u001b\[[0-9;]*m/g;
|
|
96
|
+
function stripAnsi(s) { return String(s).replace(ANSI_RE, ''); }
|
|
97
|
+
function visLen(s) { return stripAnsi(s).length; }
|
|
98
|
+
function ansiPadEnd(s, w) { return s + ' '.repeat(Math.max(0, w - visLen(s))); }
|
|
99
|
+
function ansiPadStart(s, w) { return ' '.repeat(Math.max(0, w - visLen(s))) + s; }
|
|
100
|
+
|
|
101
|
+
async function files(root, suffix = '.jsonl') {
|
|
102
|
+
const out = [];
|
|
103
|
+
if (!root || !(await exists(root))) return out;
|
|
104
|
+
async function walk(dir) {
|
|
105
|
+
for (const ent of await fs.readdir(dir, { withFileTypes: true })) {
|
|
106
|
+
const p = path.join(dir, ent.name);
|
|
107
|
+
if (ent.isDirectory()) await walk(p); else if (ent.name.endsWith(suffix)) out.push(p);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
await walk(root); return out;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function pushTask(taskRows, task) {
|
|
114
|
+
if (!task?.end || task.end === task.start) return;
|
|
115
|
+
task.activeMs = activeDuration(task.activityTimestamps || []);
|
|
116
|
+
taskRows.push(task);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function scanPiSessions(root, source, events, sessionRows = [], taskRows = []) {
|
|
120
|
+
for (const file of await files(root)) {
|
|
121
|
+
let provider = 'unknown', model = 'unknown', session = path.basename(file, '.jsonl'), cwd = '', currentTask = null;
|
|
122
|
+
const row = { key: session, session, source, file, project: projectKey(file), cwd, start: '', end: '', turns: 0, messages: 0, toolCalls: 0, subagentCalls: 0, roles: new Map(), stages: new Map(), workflows: new Map(), activeMs: 0, activityTimestamps: [] };
|
|
123
|
+
const text = await fs.readFile(file, 'utf8').catch(() => '');
|
|
124
|
+
for (const line of text.split(/\r?\n/)) {
|
|
125
|
+
const obj = safeJson(line); if (!obj) continue;
|
|
126
|
+
if (obj.timestamp) { row.start ||= obj.timestamp; row.end = obj.timestamp; addTimestamp(row.activityTimestamps, obj.timestamp); }
|
|
127
|
+
if (obj.type === 'session') { session = obj.id || session; cwd = obj.cwd || cwd; row.key = session; row.session = session; row.cwd = cwd; }
|
|
128
|
+
if (obj.type === 'model_change') { provider = obj.provider || provider; model = obj.modelId || model; }
|
|
129
|
+
if (obj.type === 'custom' && obj.customType === 'takomi-runtime-state' && obj.data) {
|
|
130
|
+
const role = obj.data.role || 'unknown';
|
|
131
|
+
const stage = obj.data.stage || 'unknown';
|
|
132
|
+
const workflow = obj.data.workflow || 'unknown';
|
|
133
|
+
row.roles.set(role, (row.roles.get(role) || 0) + 1);
|
|
134
|
+
row.stages.set(stage, (row.stages.get(stage) || 0) + 1);
|
|
135
|
+
row.workflows.set(workflow, (row.workflows.get(workflow) || 0) + 1);
|
|
136
|
+
events.push({ source, file, timestamp: obj.timestamp, day: dayOf(obj.timestamp), session, provider, model, project: projectKey(file), kind: 'role', role, stage, workflow, input: 0, cache: 0, output: 0, total: 0, cost: 0 });
|
|
137
|
+
}
|
|
138
|
+
const msg = obj.type === 'message' && obj.message ? obj.message : null;
|
|
139
|
+
if (msg) {
|
|
140
|
+
row.messages += 1;
|
|
141
|
+
const ts = obj.timestamp || msg.timestamp || '';
|
|
142
|
+
if (msg.role === 'user') {
|
|
143
|
+
pushTask(taskRows, currentTask);
|
|
144
|
+
row.turns += 1;
|
|
145
|
+
const textPart = (msg.content || []).find(p => p?.type === 'text')?.text || '';
|
|
146
|
+
currentTask = { source, file, session, project: projectKey(file), cwd, start: ts, end: ts, provider, model, turns: 1, toolCalls: 0, title: String(textPart).replace(/\s+/g, ' ').trim(), activityTimestamps: [] };
|
|
147
|
+
addTimestamp(currentTask.activityTimestamps, ts);
|
|
148
|
+
} else if (currentTask && ts) {
|
|
149
|
+
currentTask.end = ts;
|
|
150
|
+
addTimestamp(currentTask.activityTimestamps, ts);
|
|
151
|
+
}
|
|
152
|
+
for (const part of msg.content || []) {
|
|
153
|
+
if (!part || part.type !== 'toolCall') continue;
|
|
154
|
+
const name = part.name || 'unknown';
|
|
155
|
+
row.toolCalls += 1;
|
|
156
|
+
if (currentTask) currentTask.toolCalls += 1;
|
|
157
|
+
if (name === 'takomi_subagent') {
|
|
158
|
+
const args = part.arguments || {};
|
|
159
|
+
const count = Array.isArray(args.tasks) ? args.tasks.length : Array.isArray(args.chain) ? args.chain.length : 1;
|
|
160
|
+
row.subagentCalls += count;
|
|
161
|
+
}
|
|
162
|
+
events.push({ source, file, timestamp: obj.timestamp, day: dayOf(obj.timestamp), session, provider, model, project: projectKey(file), kind: 'tool', tool: name, input: 0, cache: 0, output: 0, total: 0, cost: 0 });
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
const u = msg && msg.usage;
|
|
166
|
+
if (u) events.push({ source, file, timestamp: obj.timestamp, day: dayOf(obj.timestamp), session, provider, model, project: projectKey(file), kind: 'usage', 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) });
|
|
167
|
+
}
|
|
168
|
+
pushTask(taskRows, currentTask);
|
|
169
|
+
row.activeMs = activeDuration(row.activityTimestamps);
|
|
170
|
+
if (row.messages || row.toolCalls || row.turns) sessionRows.push(row);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async function scanRunHistory(file) {
|
|
175
|
+
const runs = [];
|
|
176
|
+
if (!(await exists(file))) return runs;
|
|
177
|
+
const text = await fs.readFile(file, 'utf8').catch(() => '');
|
|
178
|
+
for (const line of text.split(/\r?\n/)) { const o = safeJson(line); if (o) runs.push(o); }
|
|
179
|
+
return runs;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export async function collectTakomiStats(opts = {}) {
|
|
183
|
+
const home = opts.home || os.homedir();
|
|
184
|
+
const cwd = opts.cwd || process.cwd();
|
|
185
|
+
const rawEvents = [], rawSessions = [], rawTasks = [];
|
|
186
|
+
const globalSessions = path.resolve(path.join(home, '.pi', 'agent', 'sessions'));
|
|
187
|
+
const projectSessions = path.resolve(path.join(cwd, '.pi', 'agent', 'sessions'));
|
|
188
|
+
await scanPiSessions(globalSessions, 'pi-global', rawEvents, rawSessions, rawTasks);
|
|
189
|
+
if (projectSessions !== globalSessions) await scanPiSessions(projectSessions, 'pi-project', rawEvents, rawSessions, rawTasks);
|
|
190
|
+
await scanPiSessions(path.join(cwd, '.pi', 'takomi'), 'takomi-project', rawEvents, rawSessions, rawTasks);
|
|
191
|
+
const sinceDay = parseSince(opts.since);
|
|
192
|
+
const events = rawEvents.filter(e => !sinceDay || e.day >= sinceDay);
|
|
193
|
+
const sessionRows = rawSessions.filter(s => !sinceDay || dayOf(s.end || s.start) >= sinceDay);
|
|
194
|
+
const taskRows = rawTasks.filter(t => !sinceDay || dayOf(t.end || t.start) >= sinceDay);
|
|
195
|
+
const runs = await scanRunHistory(path.join(home, '.pi', 'agent', 'run-history.jsonl'));
|
|
196
|
+
const byDay = new Map(), byModel = new Map(), bySource = new Map(), byProject = new Map(), byTool = new Map(), byRole = new Map(), byStage = new Map(), byWorkflow = new Map();
|
|
197
|
+
let totals = { input: 0, cache: 0, output: 0, total: 0, cost: 0, events: events.filter(e => e.kind === 'usage').length, toolCalls: 0, turns: 0 };
|
|
198
|
+
for (const s of sessionRows) { totals.toolCalls += s.toolCalls; totals.turns += s.turns; }
|
|
199
|
+
for (const e of events) {
|
|
200
|
+
if (e.kind === 'tool') { add(byTool, e.tool || 'unknown', { total: 0, events: 1 }); continue; }
|
|
201
|
+
if (e.kind === 'role') {
|
|
202
|
+
add(byRole, e.role || 'unknown', { total: 0, events: 1 });
|
|
203
|
+
add(byStage, e.stage || 'unknown', { total: 0, events: 1 });
|
|
204
|
+
add(byWorkflow, e.workflow || 'unknown', { total: 0, events: 1 });
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
totals.input += e.input; totals.cache += e.cache; totals.output += e.output; totals.total += e.total; totals.cost += e.cost;
|
|
208
|
+
add(byDay, e.day, e); add(byModel, e.model, e); add(bySource, e.source, e); add(byProject, e.project, e);
|
|
209
|
+
}
|
|
210
|
+
const byAgent = new Map(); let longestRun = null;
|
|
211
|
+
for (const r of runs) { add(byAgent, r.agent || 'unknown', { total: 0, events: 1 }); if (!longestRun || (+r.duration||0) > (+longestRun.duration||0)) longestRun = r; }
|
|
212
|
+
const topSessions = [...sessionRows].sort((a,b)=>(b.activeMs||0)-(a.activeMs||0) || b.turns-a.turns || b.toolCalls-a.toolCalls).slice(0, 20);
|
|
213
|
+
const topTasks = [...taskRows].sort((a,b)=>taskDuration(b)-taskDuration(a) || b.toolCalls-a.toolCalls).slice(0, 20);
|
|
214
|
+
const mostSubagentsSession = [...sessionRows].sort((a,b)=>b.subagentCalls-a.subagentCalls)[0] || null;
|
|
215
|
+
return { generatedAt: new Date().toISOString(), cwd, since: sinceDay, totals, sessions: new Set([...events.map(e => e.session), ...sessionRows.map(s => s.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), byTool: [...byTool.values()].sort((a,b)=>b.events-a.events), byRole: [...byRole.values()].sort((a,b)=>b.events-a.events), byStage: [...byStage.values()].sort((a,b)=>b.events-a.events), byWorkflow: [...byWorkflow.values()].sort((a,b)=>b.events-a.events), byAgent: [...byAgent.values()].sort((a,b)=>b.events-a.events), sessionRows, taskRows, topSessions, topTasks, mostSubagentsSession, runs, longestRun, recent: events.sort((a,b)=>(b.timestamp||'').localeCompare(a.timestamp||'')).slice(0, 10) };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// ── Streak Calculation ──────────────────────────────────────────────────────
|
|
219
|
+
function calcStreaks(byDay) {
|
|
220
|
+
if (!byDay.length) return { current: 0, longest: 0, quietDays: 0 };
|
|
221
|
+
const daySet = new Set(byDay.map(d => d.key));
|
|
222
|
+
const today = new Date(); today.setHours(0,0,0,0);
|
|
223
|
+
// current streak: walk back from today
|
|
224
|
+
let current = 0;
|
|
225
|
+
for (let d = new Date(today); ; d.setDate(d.getDate() - 1)) {
|
|
226
|
+
const key = d.toISOString().slice(0, 10);
|
|
227
|
+
if (daySet.has(key)) current++; else break;
|
|
228
|
+
}
|
|
229
|
+
// longest streak: walk all sorted days
|
|
230
|
+
const sorted = [...daySet].sort();
|
|
231
|
+
let longest = 0, run = 1;
|
|
232
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
233
|
+
const prev = new Date(sorted[i - 1]); prev.setDate(prev.getDate() + 1);
|
|
234
|
+
if (prev.toISOString().slice(0, 10) === sorted[i]) { run++; } else { longest = Math.max(longest, run); run = 1; }
|
|
235
|
+
}
|
|
236
|
+
longest = Math.max(longest, run);
|
|
237
|
+
// quiet days
|
|
238
|
+
const first = new Date(sorted[0]);
|
|
239
|
+
const span = Math.round((today - first) / 86400000) + 1;
|
|
240
|
+
return { current, longest, quietDays: Math.max(0, span - sorted.length) };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// ── GitHub-style Heatmap Grid ───────────────────────────────────────────────
|
|
244
|
+
function heatmapGrid(byDay) {
|
|
245
|
+
const dayMap = new Map(byDay.map(d => [d.key, d.total]));
|
|
246
|
+
const max = Math.max(1, ...byDay.map(d => d.total));
|
|
247
|
+
|
|
248
|
+
// Determine range: last ~26 weeks (half year) ending at current week
|
|
249
|
+
const today = new Date(); today.setHours(0,0,0,0);
|
|
250
|
+
// End at end of current week (Sunday)
|
|
251
|
+
const endDate = new Date(today);
|
|
252
|
+
const todayDow = endDate.getDay(); // 0=Sun, 1=Mon...
|
|
253
|
+
if (todayDow !== 0) endDate.setDate(endDate.getDate() + (7 - todayDow));
|
|
254
|
+
// Start 26 weeks back on Monday
|
|
255
|
+
const startDate = new Date(endDate);
|
|
256
|
+
startDate.setDate(startDate.getDate() - (26 * 7) + 1);
|
|
257
|
+
while (startDate.getDay() !== 1) startDate.setDate(startDate.getDate() - 1);
|
|
258
|
+
|
|
259
|
+
// Build grid: 7 rows (Mon..Sun), N columns (weeks)
|
|
260
|
+
const weeks = [];
|
|
261
|
+
const monthPositions = []; // { col, label }
|
|
262
|
+
const cursor = new Date(startDate);
|
|
263
|
+
let col = 0;
|
|
264
|
+
let lastMonth = -1;
|
|
265
|
+
|
|
266
|
+
while (cursor <= endDate) {
|
|
267
|
+
const week = [];
|
|
268
|
+
for (let dow = 0; dow < 7; dow++) {
|
|
269
|
+
const key = cursor.toISOString().slice(0, 10);
|
|
270
|
+
const val = cursor <= today ? (dayMap.get(key) || 0) : -1; // -1 = future
|
|
271
|
+
week.push(val);
|
|
272
|
+
// Track month transitions on the Monday of each week
|
|
273
|
+
if (dow === 0) {
|
|
274
|
+
const m = cursor.getMonth();
|
|
275
|
+
if (m !== lastMonth) {
|
|
276
|
+
const monthNames = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
|
|
277
|
+
monthPositions.push({ col, label: monthNames[m] });
|
|
278
|
+
lastMonth = m;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
cursor.setDate(cursor.getDate() + 1);
|
|
282
|
+
}
|
|
283
|
+
weeks.push(week);
|
|
284
|
+
col++;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Intensity cell — use ■ for filled, · for empty
|
|
288
|
+
const SQ = '■';
|
|
289
|
+
const EMPTY = '·';
|
|
290
|
+
function cell(val) {
|
|
291
|
+
if (val < 0) return ' '; // future
|
|
292
|
+
if (val === 0) return pc.gray(EMPTY);
|
|
293
|
+
const x = val / max;
|
|
294
|
+
if (x < 0.12) return pc.dim(pc.cyan(SQ));
|
|
295
|
+
if (x < 0.30) return pc.cyan(SQ);
|
|
296
|
+
if (x < 0.55) return pc.blue(SQ);
|
|
297
|
+
if (x < 0.80) return pc.magenta(SQ);
|
|
298
|
+
return pc.bold(pc.magenta(SQ));
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const dayLabels = ['Mon',' ','Wed',' ','Fri',' ','Sun'];
|
|
302
|
+
const rows = [];
|
|
303
|
+
|
|
304
|
+
// Each cell is 2 chars wide (char + space) in the grid
|
|
305
|
+
for (let dow = 0; dow < 7; dow++) {
|
|
306
|
+
const prefix = pc.dim(dayLabels[dow]);
|
|
307
|
+
const cells = weeks.map(w => cell(w[dow]));
|
|
308
|
+
rows.push(` ${prefix} ${cells.join(' ')}`);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Month label row — positioned under the correct columns
|
|
312
|
+
// Each column is 2 chars wide (cell + space separator)
|
|
313
|
+
let labelStr = '';
|
|
314
|
+
let prevEnd = 0;
|
|
315
|
+
for (const ml of monthPositions) {
|
|
316
|
+
const targetPos = ml.col * 2; // 2 chars per column (char + space)
|
|
317
|
+
const gap = Math.max(0, targetPos - prevEnd);
|
|
318
|
+
labelStr += ' '.repeat(gap) + ml.label;
|
|
319
|
+
prevEnd = targetPos + ml.label.length;
|
|
320
|
+
}
|
|
321
|
+
rows.push(` ${pc.dim(labelStr)}`);
|
|
322
|
+
|
|
323
|
+
// Legend row
|
|
324
|
+
rows.push('');
|
|
325
|
+
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')}`);
|
|
326
|
+
|
|
327
|
+
return rows.join('\n');
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// ── Box Drawing Helpers ─────────────────────────────────────────────────────
|
|
331
|
+
function hrule(w, ch = '─') { return ch.repeat(w); }
|
|
332
|
+
|
|
333
|
+
function center(text, width) {
|
|
334
|
+
const vl = visLen(text);
|
|
335
|
+
const pad = Math.max(0, Math.floor((width - vl) / 2));
|
|
336
|
+
return ' '.repeat(pad) + text;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function statCard(value, label, color = pc.white) {
|
|
340
|
+
return { value: String(value), label, color };
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// ── Table Helper ────────────────────────────────────────────────────────────
|
|
344
|
+
function renderTable(title, rows, columns) {
|
|
345
|
+
const lines = [];
|
|
346
|
+
lines.push(' ' + pc.bold(pc.cyan(title)));
|
|
347
|
+
lines.push(' ' + pc.dim(hrule(columns.reduce((s, c) => s + c.width, 0) + columns.length * 2)));
|
|
348
|
+
for (const row of rows) {
|
|
349
|
+
let line = ' ';
|
|
350
|
+
for (const col of columns) {
|
|
351
|
+
const val = String(col.get(row));
|
|
352
|
+
line += col.align === 'right' ? ansiPadStart(val, col.width) : ansiPadEnd(val, col.width);
|
|
353
|
+
line += ' ';
|
|
354
|
+
}
|
|
355
|
+
lines.push(line);
|
|
356
|
+
}
|
|
357
|
+
return lines.join('\n');
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function sessionLabel(row, width = 36) {
|
|
361
|
+
const project = row.project || row.cwd || row.key || 'unknown';
|
|
362
|
+
return project.length > width ? '…' + project.slice(-(width - 1)) : project;
|
|
363
|
+
}
|
|
364
|
+
function sessionDay(row) { return dayOf(row.start || row.end).slice(5) || '??-??'; }
|
|
365
|
+
function sessionDuration(row) {
|
|
366
|
+
return row?.activeMs || 0;
|
|
367
|
+
}
|
|
368
|
+
function taskDuration(row) {
|
|
369
|
+
return row?.activeMs ?? activeDuration(row?.activityTimestamps || []);
|
|
370
|
+
}
|
|
371
|
+
function taskLabel(row, width = 34) {
|
|
372
|
+
const label = row?.title || row?.project || row?.session || 'unknown';
|
|
373
|
+
return label.length > width ? label.slice(0, width - 1) + '…' : label;
|
|
374
|
+
}
|
|
375
|
+
function runLabel(run, width = 28) {
|
|
376
|
+
const label = run ? `${run.agent || 'unknown'}: ${run.task || ''}`.trim() : '-';
|
|
377
|
+
return label.length > width ? label.slice(0, width - 1) + '…' : label;
|
|
378
|
+
}
|
|
379
|
+
function indentWrap(text, width = 76, indent = ' ') {
|
|
380
|
+
const words = String(text || '').split(/\s+/).filter(Boolean);
|
|
381
|
+
const lines = [];
|
|
382
|
+
let line = '';
|
|
383
|
+
for (const word of words) {
|
|
384
|
+
if ((line + ' ' + word).trim().length > width && line) { lines.push(line); line = word; }
|
|
385
|
+
else line = (line + ' ' + word).trim();
|
|
386
|
+
}
|
|
387
|
+
if (line) lines.push(line);
|
|
388
|
+
return lines.map((l, i) => (i ? indent : '') + l).join('\n');
|
|
389
|
+
}
|
|
390
|
+
function renderFullList(title, rows, renderRow, limit = 20) {
|
|
391
|
+
const lines = ['\n' + pc.bold(pc.magenta('Takomi Stats')), renderTable(title, [], [{ width: 74 }])];
|
|
392
|
+
rows.slice(0, limit).forEach((row, i) => lines.push(renderRow(row, i)));
|
|
393
|
+
lines.push('\n' + pc.dim('Privacy: metadata only · no raw prompts or transcripts'));
|
|
394
|
+
return lines.join('\n');
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function renderFocusedView(stats, opts = {}) {
|
|
398
|
+
const view = opts.view;
|
|
399
|
+
const limit = opts.limit || 20;
|
|
400
|
+
if (!view || view === 'overview') return null;
|
|
401
|
+
if (view === 'projects-full' || view === 'project-full') return renderFullList('Top Projects — Full Names', stats.byProject, (r, i) => [
|
|
402
|
+
` ${pc.dim(String(i + 1).padStart(2, '0') + '.')} ${pc.white(r.key)}`,
|
|
403
|
+
` ${pc.cyan(fmtTokens(r.total))} ${pc.dim(fmtMoney(r.cost))} ${pc.dim(r.events + ' calls')}`,
|
|
404
|
+
].join('\n'), limit);
|
|
405
|
+
if (view === 'sessions-full' || view === 'session-full') return renderFullList('Longest Active Sessions — Full Names', stats.topSessions, (r, i) => [
|
|
406
|
+
` ${pc.dim(String(i + 1).padStart(2, '0') + '.')} ${pc.white(r.project || r.cwd || r.key || 'unknown')}`,
|
|
407
|
+
` ${pc.cyan(ms(sessionDuration(r)))} ${pc.dim(r.turns + ' turns')} ${pc.dim(r.toolCalls + ' tools')}`,
|
|
408
|
+
` ${pc.dim(r.file || '')}`,
|
|
409
|
+
].join('\n'), limit);
|
|
410
|
+
if (view === 'tasks-full' || view === 'task-full') return renderFullList('Longest Active Turns — Full Prompts', stats.topTasks || [], (r, i) => [
|
|
411
|
+
` ${pc.dim(String(i + 1).padStart(2, '0') + '.')} ${pc.cyan(ms(taskDuration(r)))} ${pc.magenta(r.toolCalls + ' tools')} ${pc.dim(dayOf(r.start))}`,
|
|
412
|
+
` ${pc.white(indentWrap(r.title || r.project || r.session || 'unknown'))}`,
|
|
413
|
+
` ${pc.dim(r.project || '')}`,
|
|
414
|
+
].join('\n'), limit);
|
|
415
|
+
const tables = {
|
|
416
|
+
models: ['Top Models', stats.byModel, [
|
|
417
|
+
{ width: 26, align: 'left', get: r => pc.white(r.key) },
|
|
418
|
+
{ width: 10, align: 'right', get: r => pc.cyan(fmtTokens(r.total)) },
|
|
419
|
+
{ width: 10, align: 'right', get: r => pc.dim(fmtMoney(r.cost)) },
|
|
420
|
+
{ width: 12, align: 'right', get: r => pc.dim(r.events + ' calls') },
|
|
421
|
+
]],
|
|
422
|
+
sources: ['Sources', stats.bySource, [
|
|
423
|
+
{ width: 22, align: 'left', get: r => pc.white(r.key) },
|
|
424
|
+
{ width: 10, align: 'right', get: r => pc.cyan(fmtTokens(r.total)) },
|
|
425
|
+
{ width: 14, align: 'right', get: r => pc.dim(r.events + ' events') },
|
|
426
|
+
]],
|
|
427
|
+
projects: ['Top Projects', stats.byProject, [
|
|
428
|
+
{ width: 42, align: 'left', get: r => pc.white(r.key.length > 42 ? '…' + r.key.slice(-41) : r.key) },
|
|
429
|
+
{ width: 10, align: 'right', get: r => pc.cyan(fmtTokens(r.total)) },
|
|
430
|
+
{ width: 10, align: 'right', get: r => pc.dim(fmtMoney(r.cost)) },
|
|
431
|
+
{ width: 12, align: 'right', get: r => pc.dim(r.events + ' calls') },
|
|
432
|
+
]],
|
|
433
|
+
agents: ['Main Agent Roles', stats.byRole, [
|
|
434
|
+
{ width: 24, align: 'left', get: r => pc.white(r.key) },
|
|
435
|
+
{ width: 8, align: 'right', get: r => pc.cyan(String(r.events)) },
|
|
436
|
+
{ width: 12, align: 'left', get: () => pc.dim('state hits') },
|
|
437
|
+
]],
|
|
438
|
+
subagents: ['Top Subagents', stats.byAgent, [
|
|
439
|
+
{ width: 24, align: 'left', get: r => pc.white(r.key) },
|
|
440
|
+
{ width: 8, align: 'right', get: r => pc.cyan(String(r.events)) },
|
|
441
|
+
{ width: 8, align: 'left', get: () => pc.dim('runs') },
|
|
442
|
+
]],
|
|
443
|
+
tools: ['Top Tools', stats.byTool, [
|
|
444
|
+
{ width: 28, align: 'left', get: r => pc.white(r.key) },
|
|
445
|
+
{ width: 10, align: 'right', get: r => pc.cyan(String(r.events)) },
|
|
446
|
+
{ width: 8, align: 'left', get: () => pc.dim('calls') },
|
|
447
|
+
]],
|
|
448
|
+
sessions: ['Longest Active Sessions', stats.topSessions, [
|
|
449
|
+
{ width: 6, align: 'left', get: r => pc.dim(sessionDay(r)) },
|
|
450
|
+
{ width: 30, align: 'left', get: r => pc.white(sessionLabel(r, 30)) },
|
|
451
|
+
{ width: 9, align: 'right', get: r => pc.cyan(ms(sessionDuration(r))) },
|
|
452
|
+
{ width: 8, align: 'right', get: r => pc.cyan(String(r.turns)) },
|
|
453
|
+
{ width: 8, align: 'left', get: () => pc.dim('turns') },
|
|
454
|
+
{ width: 8, align: 'right', get: r => pc.cyan(String(r.toolCalls)) },
|
|
455
|
+
{ width: 8, align: 'left', get: () => pc.dim('tools') },
|
|
456
|
+
]],
|
|
457
|
+
tasks: ['Longest Active Turns', stats.topTasks || [], [
|
|
458
|
+
{ width: 6, align: 'left', get: r => pc.dim(dayOf(r.start).slice(5)) },
|
|
459
|
+
{ width: 9, align: 'right', get: r => pc.cyan(ms(taskDuration(r))) },
|
|
460
|
+
{ width: 11, align: 'right', get: r => pc.magenta(`${r.toolCalls} tools`) },
|
|
461
|
+
{ width: 36, align: 'left', get: r => pc.white(taskLabel(r, 36)) },
|
|
462
|
+
]],
|
|
463
|
+
daily: ['Daily Usage', [...stats.byDay].reverse(), [
|
|
464
|
+
{ width: 12, align: 'left', get: r => pc.white(r.key) },
|
|
465
|
+
{ width: 10, align: 'right', get: r => pc.cyan(fmtTokens(r.total)) },
|
|
466
|
+
{ width: 10, align: 'right', get: r => pc.dim(fmtMoney(r.cost)) },
|
|
467
|
+
{ width: 12, align: 'right', get: r => pc.dim(r.events + ' calls') },
|
|
468
|
+
]],
|
|
469
|
+
};
|
|
470
|
+
const spec = tables[view === 'project' ? 'projects' : view];
|
|
471
|
+
if (!spec) return null;
|
|
472
|
+
const [title, rows, cols] = spec;
|
|
473
|
+
const suffix = stats.since ? pc.dim(`\n Since: ${stats.since}`) : '';
|
|
474
|
+
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');
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// ── Main Render ─────────────────────────────────────────────────────────────
|
|
478
|
+
export function renderTakomiStats(stats, opts = {}) {
|
|
479
|
+
const focused = renderFocusedView(stats, opts);
|
|
480
|
+
if (focused) return focused;
|
|
481
|
+
const W = Math.min(process.stdout.columns || 80, 86);
|
|
482
|
+
const topModel = stats.byModel[0]?.key || 'unknown';
|
|
483
|
+
const peak = stats.byDay.reduce((a,b) => b.total > (a?.total||0) ? b : a, null);
|
|
484
|
+
const streaks = calcStreaks(stats.byDay);
|
|
485
|
+
const longestSession = stats.topSessions[0] || null;
|
|
486
|
+
const longestTask = stats.topTasks?.[0] || null;
|
|
487
|
+
const lines = [];
|
|
488
|
+
|
|
489
|
+
// ── Header ────────────────────────────────────────────────────────────
|
|
490
|
+
lines.push('');
|
|
491
|
+
lines.push(pc.cyan(' ' + hrule(W - 4, '━')));
|
|
492
|
+
lines.push('');
|
|
493
|
+
lines.push(center(pc.bold(pc.white('T A K O M I S T A T S')), W));
|
|
494
|
+
const user = process.env.USERNAME || process.env.USER || 'local';
|
|
495
|
+
lines.push(center(pc.dim(`@${user} · Takomi`), W));
|
|
496
|
+
lines.push('');
|
|
497
|
+
lines.push(pc.cyan(' ' + hrule(W - 4)));
|
|
498
|
+
|
|
499
|
+
// ── Stat Cards Row 1 ─────────────────────────────────────────────────
|
|
500
|
+
const cards1 = [
|
|
501
|
+
statCard(fmtTokens(stats.totals.total), 'Lifetime Tokens'),
|
|
502
|
+
statCard(fmtTokens(stats.totals.cache), 'Cache Tokens'),
|
|
503
|
+
statCard(fmtMoney(stats.totals.cost), 'Est. Cost'),
|
|
504
|
+
statCard(String(stats.sessions), 'Sessions'),
|
|
505
|
+
statCard(String(stats.totals.turns), 'Main Turns'),
|
|
506
|
+
];
|
|
507
|
+
|
|
508
|
+
const cardW = Math.floor((W - 4) / cards1.length);
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
function buildCardLines(cards) {
|
|
512
|
+
let vStr = ' ';
|
|
513
|
+
let lStr = ' ';
|
|
514
|
+
for (const c of cards) {
|
|
515
|
+
const vPad = Math.max(0, Math.floor((cardW - c.value.length) / 2));
|
|
516
|
+
const lPad = Math.max(0, Math.floor((cardW - c.label.length) / 2));
|
|
517
|
+
const color = c.color || pc.white;
|
|
518
|
+
const vContent = ' '.repeat(vPad) + pc.bold(color(c.value));
|
|
519
|
+
const lContent = ' '.repeat(lPad) + pc.dim(color(c.label));
|
|
520
|
+
// Pad to cardW visible chars
|
|
521
|
+
vStr += ansiPadEnd(vContent, cardW);
|
|
522
|
+
lStr += ansiPadEnd(lContent, cardW);
|
|
523
|
+
}
|
|
524
|
+
return [vStr, lStr];
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
lines.push('');
|
|
528
|
+
const [v1, l1] = buildCardLines(cards1);
|
|
529
|
+
lines.push(v1);
|
|
530
|
+
lines.push(l1);
|
|
531
|
+
|
|
532
|
+
// ── Stat Cards Row 2 ─────────────────────────────────────────────────
|
|
533
|
+
lines.push('');
|
|
534
|
+
const cards2 = [
|
|
535
|
+
statCard(peak ? fmtTokens(peak.total) : '-', 'Peak Day'),
|
|
536
|
+
statCard(topModel, 'Top Model'),
|
|
537
|
+
statCard(String(stats.totals.toolCalls), 'Tool Calls'),
|
|
538
|
+
statCard(`${streaks.current} days`, 'Current Streak'),
|
|
539
|
+
statCard(`${streaks.longest} days`, 'Longest Streak'),
|
|
540
|
+
];
|
|
541
|
+
|
|
542
|
+
const [v2, l2] = buildCardLines(cards2);
|
|
543
|
+
lines.push(v2);
|
|
544
|
+
lines.push(l2);
|
|
545
|
+
|
|
546
|
+
// ── Duration Cards ────────────────────────────────────────────────────
|
|
547
|
+
lines.push('');
|
|
548
|
+
const cards3 = [
|
|
549
|
+
statCard(longestSession ? ms(sessionDuration(longestSession)) : '-', 'Top Active Session', pc.cyan),
|
|
550
|
+
statCard(longestSession ? String(longestSession.turns) : '-', 'Turns in Session', pc.cyan),
|
|
551
|
+
statCard(longestTask ? ms(taskDuration(longestTask)) : '-', 'Longest Active Turn', pc.magenta),
|
|
552
|
+
statCard(longestTask ? String(longestTask.toolCalls) : '-', 'Tools in Task', pc.magenta),
|
|
553
|
+
statCard(stats.mostSubagentsSession ? String(stats.mostSubagentsSession.subagentCalls) : '0', 'Max Subagents', pc.blue),
|
|
554
|
+
];
|
|
555
|
+
const [v3, l3] = buildCardLines(cards3);
|
|
556
|
+
lines.push(v3);
|
|
557
|
+
lines.push(l3);
|
|
558
|
+
|
|
559
|
+
// ── Info line ─────────────────────────────────────────────────────────
|
|
560
|
+
lines.push('');
|
|
561
|
+
const infoText = `Peak: ${peak?.key || '-'} · ${streaks.quietDays} quiet days · ${stats.totals.events.toLocaleString()} events · active gaps ≤15m${stats.since ? ` · since ${stats.since}` : ''}`;
|
|
562
|
+
lines.push(center(pc.dim(infoText), W));
|
|
563
|
+
|
|
564
|
+
lines.push('');
|
|
565
|
+
lines.push(pc.cyan(' ' + hrule(W - 4, '━')));
|
|
566
|
+
|
|
567
|
+
// ── Activity Heatmap ────────────────────────────────────────────────────
|
|
568
|
+
lines.push('');
|
|
569
|
+
lines.push(' ' + pc.bold(pc.cyan('Token Activity')));
|
|
570
|
+
lines.push(' ' + pc.dim(hrule(W - 4)));
|
|
571
|
+
lines.push(heatmapGrid(stats.byDay));
|
|
572
|
+
|
|
573
|
+
// ── Models Table ────────────────────────────────────────────────────────
|
|
574
|
+
lines.push('');
|
|
575
|
+
const modelLimit = opts.limit || 8;
|
|
576
|
+
lines.push(renderTable('Top Models', stats.byModel.slice(0, modelLimit), [
|
|
577
|
+
{ width: 24, align: 'left', get: r => pc.white(r.key) },
|
|
578
|
+
{ width: 10, align: 'right', get: r => pc.cyan(fmtTokens(r.total)) },
|
|
579
|
+
{ width: 10, align: 'right', get: r => pc.dim(fmtMoney(r.cost)) },
|
|
580
|
+
{ width: 12, align: 'right', get: r => pc.dim(r.events + ' calls') },
|
|
581
|
+
]));
|
|
582
|
+
|
|
583
|
+
// ── Projects Table ──────────────────────────────────────────────────────
|
|
584
|
+
if (stats.byProject.length) {
|
|
585
|
+
lines.push('');
|
|
586
|
+
lines.push(renderTable('Top Projects', stats.byProject.slice(0, 5), [
|
|
587
|
+
{ width: 34, align: 'left', get: r => pc.white(r.key.length > 34 ? '…' + r.key.slice(-33) : r.key) },
|
|
588
|
+
{ width: 10, align: 'right', get: r => pc.cyan(fmtTokens(r.total)) },
|
|
589
|
+
{ width: 10, align: 'right', get: r => pc.dim(fmtMoney(r.cost)) },
|
|
590
|
+
]));
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// ── Main Agent Roles Table ──────────────────────────────────────────────
|
|
594
|
+
if (stats.byRole.length) {
|
|
595
|
+
lines.push('');
|
|
596
|
+
lines.push(renderTable('Main Agent Roles', stats.byRole.slice(0, modelLimit), [
|
|
597
|
+
{ width: 20, align: 'left', get: r => pc.white(r.key) },
|
|
598
|
+
{ width: 8, align: 'right', get: r => pc.cyan(String(r.events)) },
|
|
599
|
+
{ width: 10, align: 'left', get: r => pc.dim('state hits') },
|
|
600
|
+
]));
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// ── Main Session Table ──────────────────────────────────────────────────
|
|
604
|
+
if (stats.topSessions.length) {
|
|
605
|
+
lines.push('');
|
|
606
|
+
lines.push(renderTable('Longest Active Sessions', stats.topSessions.slice(0, 5), [
|
|
607
|
+
{ width: 6, align: 'left', get: r => pc.dim(sessionDay(r)) },
|
|
608
|
+
{ width: 28, align: 'left', get: r => pc.white(sessionLabel(r, 28)) },
|
|
609
|
+
{ width: 9, align: 'right', get: r => pc.cyan(ms(sessionDuration(r))) },
|
|
610
|
+
{ width: 8, align: 'right', get: r => pc.cyan(String(r.turns)) },
|
|
611
|
+
{ width: 8, align: 'left', get: r => pc.dim('turns') },
|
|
612
|
+
{ width: 8, align: 'right', get: r => pc.cyan(String(r.toolCalls)) },
|
|
613
|
+
{ width: 6, align: 'left', get: r => pc.dim('tools') },
|
|
614
|
+
]));
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// ── Longest Active Turns ───────────────────────────────────────────────
|
|
618
|
+
if (stats.topTasks?.length) {
|
|
619
|
+
lines.push('');
|
|
620
|
+
lines.push(renderTable('Longest Active Turns', stats.topTasks.slice(0, 5), [
|
|
621
|
+
{ width: 6, align: 'left', get: r => pc.dim(dayOf(r.start).slice(5)) },
|
|
622
|
+
{ width: 9, align: 'right', get: r => pc.cyan(ms(taskDuration(r))) },
|
|
623
|
+
{ width: 11, align: 'right', get: r => pc.magenta(`${r.toolCalls} tools`) },
|
|
624
|
+
{ width: 34, align: 'left', get: r => pc.white(taskLabel(r, 34)) },
|
|
625
|
+
]));
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// ── Longest Subagent Run ───────────────────────────────────────────────
|
|
629
|
+
if (stats.longestRun) {
|
|
630
|
+
lines.push('');
|
|
631
|
+
lines.push(renderTable('Longest Subagent Run', [stats.longestRun], [
|
|
632
|
+
{ width: 22, align: 'left', get: r => pc.white(r.agent || 'unknown') },
|
|
633
|
+
{ width: 10, align: 'right', get: r => pc.cyan(ms(+r.duration || 0)) },
|
|
634
|
+
{ width: 30, align: 'left', get: r => pc.dim(runLabel(r, 30)) },
|
|
635
|
+
]));
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// ── Tools Table ─────────────────────────────────────────────────────────
|
|
639
|
+
if (stats.byTool.length) {
|
|
640
|
+
lines.push('');
|
|
641
|
+
lines.push(renderTable('Top Tools', stats.byTool.slice(0, modelLimit), [
|
|
642
|
+
{ width: 24, align: 'left', get: r => pc.white(r.key) },
|
|
643
|
+
{ width: 8, align: 'right', get: r => pc.cyan(String(r.events)) },
|
|
644
|
+
{ width: 6, align: 'left', get: r => pc.dim('calls') },
|
|
645
|
+
]));
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// ── Sources Table ───────────────────────────────────────────────────────
|
|
649
|
+
lines.push('');
|
|
650
|
+
lines.push(renderTable('Sources', stats.bySource, [
|
|
651
|
+
{ width: 20, align: 'left', get: r => pc.white(r.key) },
|
|
652
|
+
{ width: 10, align: 'right', get: r => pc.cyan(fmtTokens(r.total)) },
|
|
653
|
+
{ width: 14, align: 'right', get: r => pc.dim(r.events + ' events') },
|
|
654
|
+
]));
|
|
655
|
+
|
|
656
|
+
// ── Subagents Table ─────────────────────────────────────────────────────
|
|
657
|
+
if (stats.byAgent.length) {
|
|
658
|
+
lines.push('');
|
|
659
|
+
lines.push(renderTable('Top Subagents', stats.byAgent.slice(0, modelLimit), [
|
|
660
|
+
{ width: 20, align: 'left', get: r => pc.white(r.key) },
|
|
661
|
+
{ width: 8, align: 'right', get: r => pc.cyan(String(r.events)) },
|
|
662
|
+
{ width: 6, align: 'left', get: r => pc.dim('runs') },
|
|
663
|
+
]));
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// ── Footer ──────────────────────────────────────────────────────────────
|
|
667
|
+
lines.push('');
|
|
668
|
+
lines.push(' ' + pc.dim(hrule(W - 4)));
|
|
669
|
+
lines.push(' ' + pc.dim('Privacy: metadata only · no raw prompts or transcripts'));
|
|
670
|
+
lines.push(' ' + pc.dim('Costs are estimates when provider prices are unknown.'));
|
|
671
|
+
lines.push('');
|
|
672
|
+
|
|
673
|
+
return lines.join('\n');
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
export async function printTakomiStats(options = {}) {
|
|
677
|
+
const stats = await collectTakomiStats(options);
|
|
678
|
+
if (options.json) console.log(JSON.stringify(stats, null, 2)); else console.log(renderTakomiStats(stats, options));
|
|
679
|
+
}
|