takomi 2.1.18 → 2.1.20
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/extensions/notify-sound/index.ts +115 -25
- package/.pi/extensions/takomi-runtime/command-text.ts +6 -3
- package/.pi/extensions/takomi-runtime/commands.ts +2 -2
- package/.pi/extensions/takomi-runtime/takomi-stats.d.ts +3 -0
- package/.pi/extensions/takomi-runtime/takomi-stats.js +544 -0
- package/package.json +1 -1
- package/src/cli.js +1 -1
- package/src/pi-installer.js +2 -1
- package/src/takomi-stats.js +130 -24
|
@@ -8,7 +8,12 @@ type NotifySoundConfig = {
|
|
|
8
8
|
enabled: boolean;
|
|
9
9
|
};
|
|
10
10
|
|
|
11
|
+
type NotifyMethod = "auto" | "wav";
|
|
12
|
+
|
|
11
13
|
const CONFIG_PATH = join(process.cwd(), ".pi", "notify-sound.json");
|
|
14
|
+
const WAV_PATH = join(process.cwd(), ".pi", "notify-sound.wav");
|
|
15
|
+
const PS1_PATH = join(process.cwd(), ".pi", "notify-sound.ps1");
|
|
16
|
+
const VBS_PATH = join(process.cwd(), ".pi", "notify-sound.vbs");
|
|
12
17
|
const STALE_AGENT_START_MS = 24 * 60 * 60 * 1000;
|
|
13
18
|
|
|
14
19
|
let config: NotifySoundConfig = { enabled: true };
|
|
@@ -16,6 +21,7 @@ let lastAgentStartedAt = 0;
|
|
|
16
21
|
|
|
17
22
|
export default async function notifySoundExtension(pi: ExtensionAPI) {
|
|
18
23
|
await loadConfig();
|
|
24
|
+
await ensureTuneWav();
|
|
19
25
|
|
|
20
26
|
pi.on("session_start", async (_event, ctx) => {
|
|
21
27
|
updateStatus(ctx);
|
|
@@ -37,12 +43,34 @@ export default async function notifySoundExtension(pi: ExtensionAPI) {
|
|
|
37
43
|
|
|
38
44
|
const command = {
|
|
39
45
|
description: "Toggle or test the agent completion tune notification",
|
|
46
|
+
getArgumentCompletions: (argumentPrefix: string) => {
|
|
47
|
+
const token = argumentPrefix.trim().toLowerCase();
|
|
48
|
+
return [
|
|
49
|
+
{ value: "status", label: "status", description: "Show whether the completion tune is on" },
|
|
50
|
+
{ value: "test", label: "test", description: "Play the completion tune" },
|
|
51
|
+
{ value: "test wav", label: "test wav", description: "Play the generated WAV completion tune" },
|
|
52
|
+
{ value: "on", label: "on", description: "Enable the completion tune" },
|
|
53
|
+
{ value: "off", label: "off", description: "Disable the completion tune" },
|
|
54
|
+
{ value: "toggle", label: "toggle", description: "Toggle the completion tune" },
|
|
55
|
+
].filter((completion) => !token || completion.value.startsWith(token));
|
|
56
|
+
},
|
|
40
57
|
handler: async (args: string, ctx: ExtensionCommandContext) => {
|
|
41
58
|
const action = args.trim().toLowerCase() || "toggle";
|
|
42
59
|
|
|
43
60
|
if (action === "test") {
|
|
44
|
-
playCompletionTune();
|
|
45
|
-
ctx.ui.notify("Played completion tune.", "info");
|
|
61
|
+
playCompletionTune("wav");
|
|
62
|
+
ctx.ui.notify("Played completion tune using WAV mode. This is the default reliable Windows path.", "info");
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (action === "test wav") {
|
|
67
|
+
playCompletionTune("wav");
|
|
68
|
+
ctx.ui.notify("Played completion tune using WAV mode.", "info");
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (action.startsWith("test ")) {
|
|
73
|
+
ctx.ui.notify("Usage: /notify test", "warning");
|
|
46
74
|
return;
|
|
47
75
|
}
|
|
48
76
|
|
|
@@ -101,16 +129,89 @@ async function setEnabled(enabled: boolean, ctx: ExtensionContext): Promise<void
|
|
|
101
129
|
ctx.ui.notify(`Completion tune ${enabled ? "enabled" : "disabled"}.`, "info");
|
|
102
130
|
}
|
|
103
131
|
|
|
132
|
+
async function ensureTuneWav(): Promise<void> {
|
|
133
|
+
await mkdir(dirname(WAV_PATH), { recursive: true });
|
|
134
|
+
await writeFile(WAV_PATH, createTuneWav(), "binary");
|
|
135
|
+
const wav = WAV_PATH.replace(/'/g, "''");
|
|
136
|
+
await writeFile(PS1_PATH, [
|
|
137
|
+
"$ErrorActionPreference = 'SilentlyContinue'",
|
|
138
|
+
`if (Test-Path '${wav}') {`,
|
|
139
|
+
` $p = New-Object System.Media.SoundPlayer '${wav}'`,
|
|
140
|
+
" $p.Load()",
|
|
141
|
+
" $p.PlaySync()",
|
|
142
|
+
"}",
|
|
143
|
+
"[System.Media.SystemSounds]::Asterisk.Play()",
|
|
144
|
+
"Start-Sleep -Milliseconds 250",
|
|
145
|
+
"",
|
|
146
|
+
].join("\n"), "utf8");
|
|
147
|
+
const ps1 = PS1_PATH.replace(/"/g, "\"\"");
|
|
148
|
+
await writeFile(VBS_PATH, [
|
|
149
|
+
"Set shell = CreateObject(\"WScript.Shell\")",
|
|
150
|
+
`shell.Run "powershell.exe -NoProfile -ExecutionPolicy Bypass -File ""${ps1}""", 0, False`,
|
|
151
|
+
"",
|
|
152
|
+
].join("\r\n"), "utf8");
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function createTuneWav(): Buffer {
|
|
156
|
+
const sampleRate = 44100;
|
|
157
|
+
const notes = [
|
|
158
|
+
[659, 0.11],
|
|
159
|
+
[784, 0.11],
|
|
160
|
+
[988, 0.15],
|
|
161
|
+
[1319, 0.21],
|
|
162
|
+
[1175, 0.12],
|
|
163
|
+
[1319, 0.26],
|
|
164
|
+
];
|
|
165
|
+
const gapSeconds = 0.035;
|
|
166
|
+
const samples: number[] = [];
|
|
167
|
+
|
|
168
|
+
for (const [freq, seconds] of notes) {
|
|
169
|
+
const count = Math.floor(sampleRate * seconds);
|
|
170
|
+
for (let i = 0; i < count; i++) {
|
|
171
|
+
const t = i / sampleRate;
|
|
172
|
+
const fadeIn = Math.min(1, i / 120);
|
|
173
|
+
const fadeOut = Math.min(1, (count - i) / 300);
|
|
174
|
+
const env = Math.min(fadeIn, fadeOut) * 0.28;
|
|
175
|
+
samples.push(Math.sin(2 * Math.PI * freq * t) * env);
|
|
176
|
+
}
|
|
177
|
+
for (let i = 0; i < Math.floor(sampleRate * gapSeconds); i++) samples.push(0);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const dataSize = samples.length * 2;
|
|
181
|
+
const buffer = Buffer.alloc(44 + dataSize);
|
|
182
|
+
buffer.write("RIFF", 0);
|
|
183
|
+
buffer.writeUInt32LE(36 + dataSize, 4);
|
|
184
|
+
buffer.write("WAVE", 8);
|
|
185
|
+
buffer.write("fmt ", 12);
|
|
186
|
+
buffer.writeUInt32LE(16, 16);
|
|
187
|
+
buffer.writeUInt16LE(1, 20);
|
|
188
|
+
buffer.writeUInt16LE(1, 22);
|
|
189
|
+
buffer.writeUInt32LE(sampleRate, 24);
|
|
190
|
+
buffer.writeUInt32LE(sampleRate * 2, 28);
|
|
191
|
+
buffer.writeUInt16LE(2, 32);
|
|
192
|
+
buffer.writeUInt16LE(16, 34);
|
|
193
|
+
buffer.write("data", 36);
|
|
194
|
+
buffer.writeUInt32LE(dataSize, 40);
|
|
195
|
+
|
|
196
|
+
let offset = 44;
|
|
197
|
+
for (const sample of samples) {
|
|
198
|
+
const value = Math.max(-1, Math.min(1, sample));
|
|
199
|
+
buffer.writeInt16LE(Math.round(value * 32767), offset);
|
|
200
|
+
offset += 2;
|
|
201
|
+
}
|
|
202
|
+
return buffer;
|
|
203
|
+
}
|
|
204
|
+
|
|
104
205
|
function updateStatus(ctx: ExtensionContext): void {
|
|
105
206
|
if (!ctx.hasUI) return;
|
|
106
207
|
ctx.ui.setStatus("notify-sound", ctx.ui.theme.fg("dim", `tune:${config.enabled ? "on" : "off"}`));
|
|
107
208
|
}
|
|
108
209
|
|
|
109
|
-
function playCompletionTune(): void {
|
|
210
|
+
function playCompletionTune(method: NotifyMethod = "auto"): void {
|
|
110
211
|
const os = platform();
|
|
111
212
|
|
|
112
213
|
if (os === "win32") {
|
|
113
|
-
playWindowsTune();
|
|
214
|
+
playWindowsTune(method);
|
|
114
215
|
return;
|
|
115
216
|
}
|
|
116
217
|
|
|
@@ -122,28 +223,16 @@ function playCompletionTune(): void {
|
|
|
122
223
|
playLinuxTune();
|
|
123
224
|
}
|
|
124
225
|
|
|
125
|
-
function playWindowsTune(): void {
|
|
126
|
-
//
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
[988, 150], // B5
|
|
131
|
-
[1319, 210], // E6
|
|
132
|
-
[1175, 120], // D6
|
|
133
|
-
[1319, 260], // E6
|
|
134
|
-
];
|
|
135
|
-
|
|
136
|
-
const commands = melody
|
|
137
|
-
.map(([frequency, duration]) => `[Console]::Beep(${frequency}, ${duration})`)
|
|
138
|
-
.join("; Start-Sleep -Milliseconds 35; ");
|
|
226
|
+
function playWindowsTune(method: NotifyMethod): void {
|
|
227
|
+
// Windows uses the generated WAV path because Console.Beep, system sounds,
|
|
228
|
+
// and terminal bells are silent on many modern Windows machines.
|
|
229
|
+
playWindowsWav();
|
|
230
|
+
}
|
|
139
231
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
"-Command",
|
|
145
|
-
commands,
|
|
146
|
-
]);
|
|
232
|
+
function playWindowsWav(): void {
|
|
233
|
+
// WScript launches the PowerShell player hidden without stealing focus. This
|
|
234
|
+
// is more reliable than spawning hidden PowerShell directly from Pi's TUI.
|
|
235
|
+
runDetached("wscript.exe", [VBS_PATH]);
|
|
147
236
|
}
|
|
148
237
|
|
|
149
238
|
function playLinuxTune(): void {
|
|
@@ -163,6 +252,7 @@ function runDetached(command: string, args: string[]): void {
|
|
|
163
252
|
detached: true,
|
|
164
253
|
stdio: "ignore",
|
|
165
254
|
windowsHide: true,
|
|
255
|
+
shell: false,
|
|
166
256
|
});
|
|
167
257
|
child.unref();
|
|
168
258
|
} catch {
|
|
@@ -17,7 +17,7 @@ const ROOT_COMPLETIONS: TakomiCompletion[] = [
|
|
|
17
17
|
{ value: "mode", label: "mode", description: "Set direct, orchestrate, or review mode" },
|
|
18
18
|
{ value: "gate", label: "gate", description: "Set auto or review-gated execution" },
|
|
19
19
|
{ value: "subagents", label: "subagents", description: "Control subagent usage and view" },
|
|
20
|
-
{ value: "stats", label: "stats", description: "Show token, model, project, and subagent usage stats" },
|
|
20
|
+
{ value: "stats", label: "stats", description: "Show token, model, project, session, tool, and subagent usage stats" },
|
|
21
21
|
{ value: "routing", label: "routing", description: "Show or update Takomi model routing policy" },
|
|
22
22
|
];
|
|
23
23
|
|
|
@@ -44,7 +44,10 @@ const SUBCOMMAND_COMPLETIONS: Record<string, TakomiCompletion[]> = {
|
|
|
44
44
|
{ value: "daily", label: "daily", description: "Show daily usage rows" },
|
|
45
45
|
{ value: "models", label: "models", description: "Show model usage leaderboard" },
|
|
46
46
|
{ value: "projects", label: "projects", description: "Show project usage leaderboard" },
|
|
47
|
-
{ value: "
|
|
47
|
+
{ value: "sessions", label: "sessions", description: "Show longest/busiest main sessions" },
|
|
48
|
+
{ value: "tools", label: "tools", description: "Show most used tools" },
|
|
49
|
+
{ value: "agents", label: "agents", description: "Show main agent role leaderboard" },
|
|
50
|
+
{ value: "subagents", label: "subagents", description: "Show subagent run leaderboard" },
|
|
48
51
|
{ value: "sources", label: "sources", description: "Show global/project source split" },
|
|
49
52
|
{ value: "since 7d", label: "since 7d", description: "Filter stats to the last 7 days" },
|
|
50
53
|
{ value: "since 14d", label: "since 14d", description: "Filter stats to the last 14 days" },
|
|
@@ -92,7 +95,7 @@ export function commandHelp(): string {
|
|
|
92
95
|
"/takomi mode <direct|orchestrate|review>",
|
|
93
96
|
"/takomi gate <auto|review>",
|
|
94
97
|
"/takomi subagents <on|off|status|expand|collapse|toggle>",
|
|
95
|
-
"/takomi stats [overview|daily|models|projects|
|
|
98
|
+
"/takomi stats [overview|daily|models|projects|sessions|tools|subagents|sources] [since 7d]",
|
|
96
99
|
"/takomi routing [show|where]",
|
|
97
100
|
"/takomi routing <policy text> # updates global policy",
|
|
98
101
|
"/takomi routing local <policy text> # project override",
|
|
@@ -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;
|
|
@@ -257,7 +257,7 @@ export function registerTakomiCommands(pi: ExtensionAPI, options: RegisterTakomi
|
|
|
257
257
|
});
|
|
258
258
|
|
|
259
259
|
pi.registerCommand("takomi-stats", {
|
|
260
|
-
description: "Show
|
|
260
|
+
description: "Show Takomi token, model, project, session, tool, and subagent usage stats",
|
|
261
261
|
getArgumentCompletions: (argumentPrefix: string) => completions(`stats ${argumentPrefix}`).map((completion) => ({
|
|
262
262
|
...completion,
|
|
263
263
|
value: completion.value.replace(/^stats\s+/, ""),
|
|
@@ -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,544 @@
|
|
|
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
|
+
function parseSince(value) {
|
|
44
|
+
if (!value) return null;
|
|
45
|
+
const raw = String(value).trim().toLowerCase();
|
|
46
|
+
const rel = raw.match(/^(\d+)(d|day|days|w|week|weeks|m|month|months)$/);
|
|
47
|
+
const d = new Date(); d.setHours(0,0,0,0);
|
|
48
|
+
if (rel) {
|
|
49
|
+
const n = Number(rel[1]);
|
|
50
|
+
const unit = rel[2][0];
|
|
51
|
+
d.setDate(d.getDate() - (unit === 'w' ? n * 7 : unit === 'm' ? n * 30 : n));
|
|
52
|
+
return d.toISOString().slice(0, 10);
|
|
53
|
+
}
|
|
54
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(raw)) return raw;
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
function projectKey(file) {
|
|
58
|
+
const normalized = String(file || '').replace(/\\/g, '/');
|
|
59
|
+
const marker = '/sessions/';
|
|
60
|
+
const idx = normalized.indexOf(marker);
|
|
61
|
+
if (idx >= 0) {
|
|
62
|
+
const encoded = normalized.slice(idx + marker.length).split('/')[0];
|
|
63
|
+
return encoded.replace(/^--/, '').replace(/--$/, '').replace(/--/g, '/').replace(/-/g, ' ').trim() || 'global';
|
|
64
|
+
}
|
|
65
|
+
const cwdMarker = '/.pi/';
|
|
66
|
+
const pidx = normalized.indexOf(cwdMarker);
|
|
67
|
+
if (pidx >= 0) return normalized.slice(0, pidx).split('/').slice(-2).join('/');
|
|
68
|
+
return 'unknown';
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ── ANSI-aware string helpers ───────────────────────────────────────────────
|
|
72
|
+
// eslint-disable-next-line no-control-regex
|
|
73
|
+
const ANSI_RE = /\u001b\[[0-9;]*m/g;
|
|
74
|
+
function stripAnsi(s) { return String(s).replace(ANSI_RE, ''); }
|
|
75
|
+
function visLen(s) { return stripAnsi(s).length; }
|
|
76
|
+
function ansiPadEnd(s, w) { return s + ' '.repeat(Math.max(0, w - visLen(s))); }
|
|
77
|
+
function ansiPadStart(s, w) { return ' '.repeat(Math.max(0, w - visLen(s))) + s; }
|
|
78
|
+
|
|
79
|
+
async function files(root, suffix = '.jsonl') {
|
|
80
|
+
const out = [];
|
|
81
|
+
if (!root || !(await exists(root))) return out;
|
|
82
|
+
async function walk(dir) {
|
|
83
|
+
for (const ent of await fs.readdir(dir, { withFileTypes: true })) {
|
|
84
|
+
const p = path.join(dir, ent.name);
|
|
85
|
+
if (ent.isDirectory()) await walk(p); else if (ent.name.endsWith(suffix)) out.push(p);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
await walk(root); return out;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function scanPiSessions(root, source, events, sessionRows = []) {
|
|
92
|
+
for (const file of await files(root)) {
|
|
93
|
+
let provider = 'unknown', model = 'unknown', session = path.basename(file, '.jsonl'), cwd = '';
|
|
94
|
+
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() };
|
|
95
|
+
const text = await fs.readFile(file, 'utf8').catch(() => '');
|
|
96
|
+
for (const line of text.split(/\r?\n/)) {
|
|
97
|
+
const obj = safeJson(line); if (!obj) continue;
|
|
98
|
+
if (obj.timestamp) { row.start ||= obj.timestamp; row.end = obj.timestamp; }
|
|
99
|
+
if (obj.type === 'session') { session = obj.id || session; cwd = obj.cwd || cwd; row.key = session; row.session = session; row.cwd = cwd; }
|
|
100
|
+
if (obj.type === 'model_change') { provider = obj.provider || provider; model = obj.modelId || model; }
|
|
101
|
+
if (obj.type === 'custom' && obj.customType === 'takomi-runtime-state' && obj.data) {
|
|
102
|
+
const role = obj.data.role || 'unknown';
|
|
103
|
+
const stage = obj.data.stage || 'unknown';
|
|
104
|
+
const workflow = obj.data.workflow || 'unknown';
|
|
105
|
+
row.roles.set(role, (row.roles.get(role) || 0) + 1);
|
|
106
|
+
row.stages.set(stage, (row.stages.get(stage) || 0) + 1);
|
|
107
|
+
row.workflows.set(workflow, (row.workflows.get(workflow) || 0) + 1);
|
|
108
|
+
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 });
|
|
109
|
+
}
|
|
110
|
+
const msg = obj.type === 'message' && obj.message ? obj.message : null;
|
|
111
|
+
if (msg) {
|
|
112
|
+
row.messages += 1;
|
|
113
|
+
if (msg.role === 'user') row.turns += 1;
|
|
114
|
+
for (const part of msg.content || []) {
|
|
115
|
+
if (!part || part.type !== 'toolCall') continue;
|
|
116
|
+
const name = part.name || 'unknown';
|
|
117
|
+
row.toolCalls += 1;
|
|
118
|
+
if (name === 'takomi_subagent') {
|
|
119
|
+
const args = part.arguments || {};
|
|
120
|
+
const count = Array.isArray(args.tasks) ? args.tasks.length : Array.isArray(args.chain) ? args.chain.length : 1;
|
|
121
|
+
row.subagentCalls += count;
|
|
122
|
+
}
|
|
123
|
+
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 });
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
const u = msg && msg.usage;
|
|
127
|
+
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) });
|
|
128
|
+
}
|
|
129
|
+
if (row.messages || row.toolCalls || row.turns) sessionRows.push(row);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function scanRunHistory(file) {
|
|
134
|
+
const runs = [];
|
|
135
|
+
if (!(await exists(file))) return runs;
|
|
136
|
+
const text = await fs.readFile(file, 'utf8').catch(() => '');
|
|
137
|
+
for (const line of text.split(/\r?\n/)) { const o = safeJson(line); if (o) runs.push(o); }
|
|
138
|
+
return runs;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export async function collectTakomiStats(opts = {}) {
|
|
142
|
+
const home = opts.home || os.homedir();
|
|
143
|
+
const cwd = opts.cwd || process.cwd();
|
|
144
|
+
const rawEvents = [], rawSessions = [];
|
|
145
|
+
await scanPiSessions(path.join(home, '.pi', 'agent', 'sessions'), 'pi-global', rawEvents, rawSessions);
|
|
146
|
+
await scanPiSessions(path.join(cwd, '.pi', 'agent', 'sessions'), 'pi-project', rawEvents, rawSessions);
|
|
147
|
+
await scanPiSessions(path.join(cwd, '.pi', 'takomi'), 'takomi-project', rawEvents, rawSessions);
|
|
148
|
+
const sinceDay = parseSince(opts.since);
|
|
149
|
+
const events = rawEvents.filter(e => !sinceDay || e.day >= sinceDay);
|
|
150
|
+
const sessionRows = rawSessions.filter(s => !sinceDay || dayOf(s.end || s.start) >= sinceDay);
|
|
151
|
+
const runs = await scanRunHistory(path.join(home, '.pi', 'agent', 'run-history.jsonl'));
|
|
152
|
+
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();
|
|
153
|
+
let totals = { input: 0, cache: 0, output: 0, total: 0, cost: 0, events: events.filter(e => e.kind === 'usage').length, toolCalls: 0, turns: 0 };
|
|
154
|
+
for (const s of sessionRows) { totals.toolCalls += s.toolCalls; totals.turns += s.turns; }
|
|
155
|
+
for (const e of events) {
|
|
156
|
+
if (e.kind === 'tool') { add(byTool, e.tool || 'unknown', { total: 0, events: 1 }); continue; }
|
|
157
|
+
if (e.kind === 'role') {
|
|
158
|
+
add(byRole, e.role || 'unknown', { total: 0, events: 1 });
|
|
159
|
+
add(byStage, e.stage || 'unknown', { total: 0, events: 1 });
|
|
160
|
+
add(byWorkflow, e.workflow || 'unknown', { total: 0, events: 1 });
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
totals.input += e.input; totals.cache += e.cache; totals.output += e.output; totals.total += e.total; totals.cost += e.cost;
|
|
164
|
+
add(byDay, e.day, e); add(byModel, e.model, e); add(bySource, e.source, e); add(byProject, e.project, e);
|
|
165
|
+
}
|
|
166
|
+
const byAgent = new Map(); let longestRun = null;
|
|
167
|
+
for (const r of runs) { add(byAgent, r.agent || 'unknown', { total: 0, events: 1 }); if (!longestRun || (+r.duration||0) > (+longestRun.duration||0)) longestRun = r; }
|
|
168
|
+
const topSessions = [...sessionRows].sort((a,b)=>b.turns-a.turns || b.toolCalls-a.toolCalls).slice(0, 20);
|
|
169
|
+
const mostSubagentsSession = [...sessionRows].sort((a,b)=>b.subagentCalls-a.subagentCalls)[0] || null;
|
|
170
|
+
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, topSessions, mostSubagentsSession, runs, longestRun, recent: events.sort((a,b)=>(b.timestamp||'').localeCompare(a.timestamp||'')).slice(0, 10) };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ── Streak Calculation ──────────────────────────────────────────────────────
|
|
174
|
+
function calcStreaks(byDay) {
|
|
175
|
+
if (!byDay.length) return { current: 0, longest: 0, quietDays: 0 };
|
|
176
|
+
const daySet = new Set(byDay.map(d => d.key));
|
|
177
|
+
const today = new Date(); today.setHours(0,0,0,0);
|
|
178
|
+
// current streak: walk back from today
|
|
179
|
+
let current = 0;
|
|
180
|
+
for (let d = new Date(today); ; d.setDate(d.getDate() - 1)) {
|
|
181
|
+
const key = d.toISOString().slice(0, 10);
|
|
182
|
+
if (daySet.has(key)) current++; else break;
|
|
183
|
+
}
|
|
184
|
+
// longest streak: walk all sorted days
|
|
185
|
+
const sorted = [...daySet].sort();
|
|
186
|
+
let longest = 0, run = 1;
|
|
187
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
188
|
+
const prev = new Date(sorted[i - 1]); prev.setDate(prev.getDate() + 1);
|
|
189
|
+
if (prev.toISOString().slice(0, 10) === sorted[i]) { run++; } else { longest = Math.max(longest, run); run = 1; }
|
|
190
|
+
}
|
|
191
|
+
longest = Math.max(longest, run);
|
|
192
|
+
// quiet days
|
|
193
|
+
const first = new Date(sorted[0]);
|
|
194
|
+
const span = Math.round((today - first) / 86400000) + 1;
|
|
195
|
+
return { current, longest, quietDays: Math.max(0, span - sorted.length) };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// ── GitHub-style Heatmap Grid ───────────────────────────────────────────────
|
|
199
|
+
function heatmapGrid(byDay) {
|
|
200
|
+
const dayMap = new Map(byDay.map(d => [d.key, d.total]));
|
|
201
|
+
const max = Math.max(1, ...byDay.map(d => d.total));
|
|
202
|
+
|
|
203
|
+
// Determine range: last ~26 weeks (half year) ending at current week
|
|
204
|
+
const today = new Date(); today.setHours(0,0,0,0);
|
|
205
|
+
// End at end of current week (Sunday)
|
|
206
|
+
const endDate = new Date(today);
|
|
207
|
+
const todayDow = endDate.getDay(); // 0=Sun, 1=Mon...
|
|
208
|
+
if (todayDow !== 0) endDate.setDate(endDate.getDate() + (7 - todayDow));
|
|
209
|
+
// Start 26 weeks back on Monday
|
|
210
|
+
const startDate = new Date(endDate);
|
|
211
|
+
startDate.setDate(startDate.getDate() - (26 * 7) + 1);
|
|
212
|
+
while (startDate.getDay() !== 1) startDate.setDate(startDate.getDate() - 1);
|
|
213
|
+
|
|
214
|
+
// Build grid: 7 rows (Mon..Sun), N columns (weeks)
|
|
215
|
+
const weeks = [];
|
|
216
|
+
const monthPositions = []; // { col, label }
|
|
217
|
+
const cursor = new Date(startDate);
|
|
218
|
+
let col = 0;
|
|
219
|
+
let lastMonth = -1;
|
|
220
|
+
|
|
221
|
+
while (cursor <= endDate) {
|
|
222
|
+
const week = [];
|
|
223
|
+
for (let dow = 0; dow < 7; dow++) {
|
|
224
|
+
const key = cursor.toISOString().slice(0, 10);
|
|
225
|
+
const val = cursor <= today ? (dayMap.get(key) || 0) : -1; // -1 = future
|
|
226
|
+
week.push(val);
|
|
227
|
+
// Track month transitions on the Monday of each week
|
|
228
|
+
if (dow === 0) {
|
|
229
|
+
const m = cursor.getMonth();
|
|
230
|
+
if (m !== lastMonth) {
|
|
231
|
+
const monthNames = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
|
|
232
|
+
monthPositions.push({ col, label: monthNames[m] });
|
|
233
|
+
lastMonth = m;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
cursor.setDate(cursor.getDate() + 1);
|
|
237
|
+
}
|
|
238
|
+
weeks.push(week);
|
|
239
|
+
col++;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Intensity cell — use ■ for filled, · for empty
|
|
243
|
+
const SQ = '■';
|
|
244
|
+
const EMPTY = '·';
|
|
245
|
+
function cell(val) {
|
|
246
|
+
if (val < 0) return ' '; // future
|
|
247
|
+
if (val === 0) return pc.gray(EMPTY);
|
|
248
|
+
const x = val / max;
|
|
249
|
+
if (x < 0.12) return pc.dim(pc.cyan(SQ));
|
|
250
|
+
if (x < 0.30) return pc.cyan(SQ);
|
|
251
|
+
if (x < 0.55) return pc.blue(SQ);
|
|
252
|
+
if (x < 0.80) return pc.magenta(SQ);
|
|
253
|
+
return pc.bold(pc.magenta(SQ));
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const dayLabels = ['Mon',' ','Wed',' ','Fri',' ','Sun'];
|
|
257
|
+
const rows = [];
|
|
258
|
+
|
|
259
|
+
// Each cell is 2 chars wide (char + space) in the grid
|
|
260
|
+
for (let dow = 0; dow < 7; dow++) {
|
|
261
|
+
const prefix = pc.dim(dayLabels[dow]);
|
|
262
|
+
const cells = weeks.map(w => cell(w[dow]));
|
|
263
|
+
rows.push(` ${prefix} ${cells.join(' ')}`);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Month label row — positioned under the correct columns
|
|
267
|
+
// Each column is 2 chars wide (cell + space separator)
|
|
268
|
+
let labelStr = '';
|
|
269
|
+
let prevEnd = 0;
|
|
270
|
+
for (const ml of monthPositions) {
|
|
271
|
+
const targetPos = ml.col * 2; // 2 chars per column (char + space)
|
|
272
|
+
const gap = Math.max(0, targetPos - prevEnd);
|
|
273
|
+
labelStr += ' '.repeat(gap) + ml.label;
|
|
274
|
+
prevEnd = targetPos + ml.label.length;
|
|
275
|
+
}
|
|
276
|
+
rows.push(` ${pc.dim(labelStr)}`);
|
|
277
|
+
|
|
278
|
+
// Legend row
|
|
279
|
+
rows.push('');
|
|
280
|
+
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')}`);
|
|
281
|
+
|
|
282
|
+
return rows.join('\n');
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// ── Box Drawing Helpers ─────────────────────────────────────────────────────
|
|
286
|
+
function hrule(w, ch = '─') { return ch.repeat(w); }
|
|
287
|
+
|
|
288
|
+
function center(text, width) {
|
|
289
|
+
const vl = visLen(text);
|
|
290
|
+
const pad = Math.max(0, Math.floor((width - vl) / 2));
|
|
291
|
+
return ' '.repeat(pad) + text;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function statCard(value, label) {
|
|
295
|
+
return { value: String(value), label };
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// ── Table Helper ────────────────────────────────────────────────────────────
|
|
299
|
+
function renderTable(title, rows, columns) {
|
|
300
|
+
const lines = [];
|
|
301
|
+
lines.push(' ' + pc.bold(pc.cyan(title)));
|
|
302
|
+
lines.push(' ' + pc.dim(hrule(columns.reduce((s, c) => s + c.width, 0) + columns.length * 2)));
|
|
303
|
+
for (const row of rows) {
|
|
304
|
+
let line = ' ';
|
|
305
|
+
for (const col of columns) {
|
|
306
|
+
const val = String(col.get(row));
|
|
307
|
+
line += col.align === 'right' ? ansiPadStart(val, col.width) : ansiPadEnd(val, col.width);
|
|
308
|
+
line += ' ';
|
|
309
|
+
}
|
|
310
|
+
lines.push(line);
|
|
311
|
+
}
|
|
312
|
+
return lines.join('\n');
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function sessionLabel(row, width = 36) {
|
|
316
|
+
const project = row.project || row.cwd || row.key || 'unknown';
|
|
317
|
+
return project.length > width ? '…' + project.slice(-(width - 1)) : project;
|
|
318
|
+
}
|
|
319
|
+
function sessionDay(row) { return dayOf(row.start || row.end).slice(5) || '??-??'; }
|
|
320
|
+
|
|
321
|
+
function renderFocusedView(stats, opts = {}) {
|
|
322
|
+
const view = opts.view;
|
|
323
|
+
const limit = opts.limit || 20;
|
|
324
|
+
if (!view || view === 'overview') return null;
|
|
325
|
+
const tables = {
|
|
326
|
+
models: ['Top Models', stats.byModel, [
|
|
327
|
+
{ width: 26, align: 'left', get: r => pc.white(r.key) },
|
|
328
|
+
{ width: 10, align: 'right', get: r => pc.cyan(fmtTokens(r.total)) },
|
|
329
|
+
{ width: 10, align: 'right', get: r => pc.dim(fmtMoney(r.cost)) },
|
|
330
|
+
{ width: 12, align: 'right', get: r => pc.dim(r.events + ' calls') },
|
|
331
|
+
]],
|
|
332
|
+
sources: ['Sources', stats.bySource, [
|
|
333
|
+
{ width: 22, align: 'left', get: r => pc.white(r.key) },
|
|
334
|
+
{ width: 10, align: 'right', get: r => pc.cyan(fmtTokens(r.total)) },
|
|
335
|
+
{ width: 14, align: 'right', get: r => pc.dim(r.events + ' events') },
|
|
336
|
+
]],
|
|
337
|
+
projects: ['Top Projects', stats.byProject, [
|
|
338
|
+
{ width: 42, align: 'left', get: r => pc.white(r.key.length > 42 ? '…' + r.key.slice(-41) : r.key) },
|
|
339
|
+
{ width: 10, align: 'right', get: r => pc.cyan(fmtTokens(r.total)) },
|
|
340
|
+
{ width: 10, align: 'right', get: r => pc.dim(fmtMoney(r.cost)) },
|
|
341
|
+
{ width: 12, align: 'right', get: r => pc.dim(r.events + ' calls') },
|
|
342
|
+
]],
|
|
343
|
+
agents: ['Main Agent Roles', stats.byRole, [
|
|
344
|
+
{ width: 24, align: 'left', get: r => pc.white(r.key) },
|
|
345
|
+
{ width: 8, align: 'right', get: r => pc.cyan(String(r.events)) },
|
|
346
|
+
{ width: 12, align: 'left', get: () => pc.dim('state hits') },
|
|
347
|
+
]],
|
|
348
|
+
subagents: ['Top Subagents', stats.byAgent, [
|
|
349
|
+
{ width: 24, align: 'left', get: r => pc.white(r.key) },
|
|
350
|
+
{ width: 8, align: 'right', get: r => pc.cyan(String(r.events)) },
|
|
351
|
+
{ width: 8, align: 'left', get: () => pc.dim('runs') },
|
|
352
|
+
]],
|
|
353
|
+
tools: ['Top Tools', stats.byTool, [
|
|
354
|
+
{ width: 28, align: 'left', get: r => pc.white(r.key) },
|
|
355
|
+
{ width: 10, align: 'right', get: r => pc.cyan(String(r.events)) },
|
|
356
|
+
{ width: 8, align: 'left', get: () => pc.dim('calls') },
|
|
357
|
+
]],
|
|
358
|
+
sessions: ['Longest Main Sessions', stats.topSessions, [
|
|
359
|
+
{ width: 6, align: 'left', get: r => pc.dim(sessionDay(r)) },
|
|
360
|
+
{ width: 34, align: 'left', get: r => pc.white(sessionLabel(r, 34)) },
|
|
361
|
+
{ width: 8, align: 'right', get: r => pc.cyan(String(r.turns)) },
|
|
362
|
+
{ width: 8, align: 'left', get: () => pc.dim('turns') },
|
|
363
|
+
{ width: 8, align: 'right', get: r => pc.cyan(String(r.toolCalls)) },
|
|
364
|
+
{ width: 8, align: 'left', get: () => pc.dim('tools') },
|
|
365
|
+
]],
|
|
366
|
+
daily: ['Daily Usage', [...stats.byDay].reverse(), [
|
|
367
|
+
{ width: 12, align: 'left', get: r => pc.white(r.key) },
|
|
368
|
+
{ width: 10, align: 'right', get: r => pc.cyan(fmtTokens(r.total)) },
|
|
369
|
+
{ width: 10, align: 'right', get: r => pc.dim(fmtMoney(r.cost)) },
|
|
370
|
+
{ width: 12, align: 'right', get: r => pc.dim(r.events + ' calls') },
|
|
371
|
+
]],
|
|
372
|
+
};
|
|
373
|
+
const spec = tables[view === 'project' ? 'projects' : view];
|
|
374
|
+
if (!spec) return null;
|
|
375
|
+
const [title, rows, cols] = spec;
|
|
376
|
+
const suffix = stats.since ? pc.dim(`\n Since: ${stats.since}`) : '';
|
|
377
|
+
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');
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// ── Main Render ─────────────────────────────────────────────────────────────
|
|
381
|
+
export function renderTakomiStats(stats, opts = {}) {
|
|
382
|
+
const focused = renderFocusedView(stats, opts);
|
|
383
|
+
if (focused) return focused;
|
|
384
|
+
const W = Math.min(process.stdout.columns || 80, 86);
|
|
385
|
+
const topModel = stats.byModel[0]?.key || 'unknown';
|
|
386
|
+
const peak = stats.byDay.reduce((a,b) => b.total > (a?.total||0) ? b : a, null);
|
|
387
|
+
const streaks = calcStreaks(stats.byDay);
|
|
388
|
+
const lines = [];
|
|
389
|
+
|
|
390
|
+
// ── Header ────────────────────────────────────────────────────────────
|
|
391
|
+
lines.push('');
|
|
392
|
+
lines.push(pc.cyan(' ' + hrule(W - 4, '━')));
|
|
393
|
+
lines.push('');
|
|
394
|
+
lines.push(center(pc.bold(pc.white('T A K O M I S T A T S')), W));
|
|
395
|
+
const user = process.env.USERNAME || process.env.USER || 'local';
|
|
396
|
+
lines.push(center(pc.dim(`@${user} · Takomi`), W));
|
|
397
|
+
lines.push('');
|
|
398
|
+
lines.push(pc.cyan(' ' + hrule(W - 4)));
|
|
399
|
+
|
|
400
|
+
// ── Stat Cards Row 1 ─────────────────────────────────────────────────
|
|
401
|
+
const cards1 = [
|
|
402
|
+
statCard(fmtTokens(stats.totals.total), 'Lifetime Tokens'),
|
|
403
|
+
statCard(fmtTokens(stats.totals.cache), 'Cache Tokens'),
|
|
404
|
+
statCard(fmtMoney(stats.totals.cost), 'Est. Cost'),
|
|
405
|
+
statCard(String(stats.sessions), 'Sessions'),
|
|
406
|
+
statCard(String(stats.totals.turns), 'Main Turns'),
|
|
407
|
+
];
|
|
408
|
+
|
|
409
|
+
const cardW = Math.floor((W - 4) / cards1.length);
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
function buildCardLines(cards) {
|
|
413
|
+
let vStr = ' ';
|
|
414
|
+
let lStr = ' ';
|
|
415
|
+
for (const c of cards) {
|
|
416
|
+
const vPad = Math.max(0, Math.floor((cardW - c.value.length) / 2));
|
|
417
|
+
const lPad = Math.max(0, Math.floor((cardW - c.label.length) / 2));
|
|
418
|
+
const vContent = ' '.repeat(vPad) + pc.bold(pc.white(c.value));
|
|
419
|
+
const lContent = ' '.repeat(lPad) + pc.dim(c.label);
|
|
420
|
+
// Pad to cardW visible chars
|
|
421
|
+
vStr += ansiPadEnd(vContent, cardW);
|
|
422
|
+
lStr += ansiPadEnd(lContent, cardW);
|
|
423
|
+
}
|
|
424
|
+
return [vStr, lStr];
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
lines.push('');
|
|
428
|
+
const [v1, l1] = buildCardLines(cards1);
|
|
429
|
+
lines.push(v1);
|
|
430
|
+
lines.push(l1);
|
|
431
|
+
|
|
432
|
+
// ── Stat Cards Row 2 ─────────────────────────────────────────────────
|
|
433
|
+
lines.push('');
|
|
434
|
+
const cards2 = [
|
|
435
|
+
statCard(peak ? fmtTokens(peak.total) : '-', 'Peak Day'),
|
|
436
|
+
statCard(topModel, 'Top Model'),
|
|
437
|
+
statCard(String(stats.totals.toolCalls), 'Tool Calls'),
|
|
438
|
+
statCard(`${streaks.current} days`, 'Current Streak'),
|
|
439
|
+
statCard(`${streaks.longest} days`, 'Longest Streak'),
|
|
440
|
+
];
|
|
441
|
+
|
|
442
|
+
const [v2, l2] = buildCardLines(cards2);
|
|
443
|
+
lines.push(v2);
|
|
444
|
+
lines.push(l2);
|
|
445
|
+
|
|
446
|
+
// ── Info line ─────────────────────────────────────────────────────────
|
|
447
|
+
lines.push('');
|
|
448
|
+
const infoText = `Peak: ${peak?.key || '-'} · ${streaks.quietDays} quiet days · ${stats.totals.events.toLocaleString()} events${stats.since ? ` · since ${stats.since}` : ''}`;
|
|
449
|
+
lines.push(center(pc.dim(infoText), W));
|
|
450
|
+
|
|
451
|
+
lines.push('');
|
|
452
|
+
lines.push(pc.cyan(' ' + hrule(W - 4, '━')));
|
|
453
|
+
|
|
454
|
+
// ── Activity Heatmap ────────────────────────────────────────────────────
|
|
455
|
+
lines.push('');
|
|
456
|
+
lines.push(' ' + pc.bold(pc.cyan('Token Activity')));
|
|
457
|
+
lines.push(' ' + pc.dim(hrule(W - 4)));
|
|
458
|
+
lines.push(heatmapGrid(stats.byDay));
|
|
459
|
+
|
|
460
|
+
// ── Models Table ────────────────────────────────────────────────────────
|
|
461
|
+
lines.push('');
|
|
462
|
+
const modelLimit = opts.limit || 8;
|
|
463
|
+
lines.push(renderTable('Top Models', stats.byModel.slice(0, modelLimit), [
|
|
464
|
+
{ width: 24, 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
|
+
// ── Projects Table ──────────────────────────────────────────────────────
|
|
471
|
+
if (stats.byProject.length) {
|
|
472
|
+
lines.push('');
|
|
473
|
+
lines.push(renderTable('Top Projects', stats.byProject.slice(0, 5), [
|
|
474
|
+
{ width: 34, align: 'left', get: r => pc.white(r.key.length > 34 ? '…' + r.key.slice(-33) : r.key) },
|
|
475
|
+
{ width: 10, align: 'right', get: r => pc.cyan(fmtTokens(r.total)) },
|
|
476
|
+
{ width: 10, align: 'right', get: r => pc.dim(fmtMoney(r.cost)) },
|
|
477
|
+
]));
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// ── Main Agent Roles Table ──────────────────────────────────────────────
|
|
481
|
+
if (stats.byRole.length) {
|
|
482
|
+
lines.push('');
|
|
483
|
+
lines.push(renderTable('Main Agent Roles', stats.byRole.slice(0, modelLimit), [
|
|
484
|
+
{ width: 20, align: 'left', get: r => pc.white(r.key) },
|
|
485
|
+
{ width: 8, align: 'right', get: r => pc.cyan(String(r.events)) },
|
|
486
|
+
{ width: 10, align: 'left', get: r => pc.dim('state hits') },
|
|
487
|
+
]));
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// ── Main Session Table ──────────────────────────────────────────────────
|
|
491
|
+
if (stats.topSessions.length) {
|
|
492
|
+
lines.push('');
|
|
493
|
+
lines.push(renderTable('Longest Main Sessions', stats.topSessions.slice(0, 5), [
|
|
494
|
+
{ width: 6, align: 'left', get: r => pc.dim(sessionDay(r)) },
|
|
495
|
+
{ width: 32, align: 'left', get: r => pc.white(sessionLabel(r, 32)) },
|
|
496
|
+
{ width: 8, align: 'right', get: r => pc.cyan(String(r.turns)) },
|
|
497
|
+
{ width: 8, align: 'left', get: r => pc.dim('turns') },
|
|
498
|
+
{ width: 8, align: 'right', get: r => pc.cyan(String(r.toolCalls)) },
|
|
499
|
+
{ width: 6, align: 'left', get: r => pc.dim('tools') },
|
|
500
|
+
]));
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// ── Tools Table ─────────────────────────────────────────────────────────
|
|
504
|
+
if (stats.byTool.length) {
|
|
505
|
+
lines.push('');
|
|
506
|
+
lines.push(renderTable('Top Tools', stats.byTool.slice(0, modelLimit), [
|
|
507
|
+
{ width: 24, align: 'left', get: r => pc.white(r.key) },
|
|
508
|
+
{ width: 8, align: 'right', get: r => pc.cyan(String(r.events)) },
|
|
509
|
+
{ width: 6, align: 'left', get: r => pc.dim('calls') },
|
|
510
|
+
]));
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// ── Sources Table ───────────────────────────────────────────────────────
|
|
514
|
+
lines.push('');
|
|
515
|
+
lines.push(renderTable('Sources', stats.bySource, [
|
|
516
|
+
{ width: 20, align: 'left', get: r => pc.white(r.key) },
|
|
517
|
+
{ width: 10, align: 'right', get: r => pc.cyan(fmtTokens(r.total)) },
|
|
518
|
+
{ width: 14, align: 'right', get: r => pc.dim(r.events + ' events') },
|
|
519
|
+
]));
|
|
520
|
+
|
|
521
|
+
// ── Subagents Table ─────────────────────────────────────────────────────
|
|
522
|
+
if (stats.byAgent.length) {
|
|
523
|
+
lines.push('');
|
|
524
|
+
lines.push(renderTable('Top Subagents', stats.byAgent.slice(0, modelLimit), [
|
|
525
|
+
{ width: 20, align: 'left', get: r => pc.white(r.key) },
|
|
526
|
+
{ width: 8, align: 'right', get: r => pc.cyan(String(r.events)) },
|
|
527
|
+
{ width: 6, align: 'left', get: r => pc.dim('runs') },
|
|
528
|
+
]));
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// ── Footer ──────────────────────────────────────────────────────────────
|
|
532
|
+
lines.push('');
|
|
533
|
+
lines.push(' ' + pc.dim(hrule(W - 4)));
|
|
534
|
+
lines.push(' ' + pc.dim('Privacy: metadata only · no raw prompts or transcripts'));
|
|
535
|
+
lines.push(' ' + pc.dim('Costs are estimates when provider prices are unknown.'));
|
|
536
|
+
lines.push('');
|
|
537
|
+
|
|
538
|
+
return lines.join('\n');
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
export async function printTakomiStats(options = {}) {
|
|
542
|
+
const stats = await collectTakomiStats(options);
|
|
543
|
+
if (options.json) console.log(JSON.stringify(stats, null, 2)); else console.log(renderTakomiStats(stats, options));
|
|
544
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "takomi",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.20",
|
|
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": {
|
package/src/cli.js
CHANGED
|
@@ -846,7 +846,7 @@ program
|
|
|
846
846
|
|
|
847
847
|
program
|
|
848
848
|
.command('stats [view]')
|
|
849
|
-
.description('Show
|
|
849
|
+
.description('Show Takomi token, model, project, session, tool, and subagent usage stats')
|
|
850
850
|
.option('--json', 'Print machine-readable JSON')
|
|
851
851
|
.option('--home <path>', 'Override home directory for Pi history scanning')
|
|
852
852
|
.option('--cwd <path>', 'Override project directory for project-local stats')
|
package/src/pi-installer.js
CHANGED
|
@@ -173,6 +173,7 @@ export async function validatePiHarnessInstall() {
|
|
|
173
173
|
subagents: await fs.pathExists(path.join(targets.extensions, 'takomi-subagents')),
|
|
174
174
|
contextManager: await fs.pathExists(path.join(targets.extensions, 'takomi-context-manager')),
|
|
175
175
|
oauthRouter: await fs.pathExists(path.join(targets.extensions, 'oauth-router')),
|
|
176
|
+
notifySound: await fs.pathExists(path.join(targets.extensions, 'notify-sound')),
|
|
176
177
|
prompts: await fs.pathExists(targets.prompts),
|
|
177
178
|
agents: await fs.pathExists(targets.agents),
|
|
178
179
|
themes: await fs.pathExists(targets.themes),
|
|
@@ -187,7 +188,7 @@ export function printPiInstallSummary(result, validation) {
|
|
|
187
188
|
console.log(pc.green('\n✔ Installed Takomi Pi harness assets'));
|
|
188
189
|
console.log(pc.white(` Root: ${result.targets.root}`));
|
|
189
190
|
console.log(pc.white(` Manifest: ${PI_MANIFEST_PATH}`));
|
|
190
|
-
console.log(pc.white(` Extensions: ${validation.runtime && validation.subagents && validation.contextManager && validation.oauthRouter ? 'ok' : 'check needed'}`));
|
|
191
|
+
console.log(pc.white(` Extensions: ${validation.runtime && validation.subagents && validation.contextManager && validation.oauthRouter && validation.notifySound ? 'ok' : 'check needed'}`));
|
|
191
192
|
console.log(pc.white(` Prompts: ${validation.prompts ? 'ok' : 'missing'}`));
|
|
192
193
|
console.log(pc.white(` Agents: ${validation.agents ? 'ok' : 'missing'}`));
|
|
193
194
|
console.log(pc.white(` Themes: ${validation.themes ? 'ok' : 'missing'}`));
|
package/src/takomi-stats.js
CHANGED
|
@@ -1,7 +1,18 @@
|
|
|
1
|
-
import fs from 'fs
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
2
|
import os from 'os';
|
|
3
3
|
import path from 'path';
|
|
4
|
-
|
|
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
|
+
};
|
|
5
16
|
|
|
6
17
|
const PRICES = {
|
|
7
18
|
'gpt-5.5': [5.00, 0.50, 30.00],
|
|
@@ -21,6 +32,7 @@ const PRICES = {
|
|
|
21
32
|
'claude-sonnet-4-6': [3.00, 0.30, 15.00],
|
|
22
33
|
};
|
|
23
34
|
|
|
35
|
+
async function exists(target) { try { await fs.access(target); return true; } catch { return false; } }
|
|
24
36
|
function safeJson(line) { try { return JSON.parse(line); } catch { return null; } }
|
|
25
37
|
function dayOf(ts) { return typeof ts === 'string' && ts.length >= 10 ? ts.slice(0, 10) : 'unknown'; }
|
|
26
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); }
|
|
@@ -66,7 +78,7 @@ function ansiPadStart(s, w) { return ' '.repeat(Math.max(0, w - visLen(s))) + s;
|
|
|
66
78
|
|
|
67
79
|
async function files(root, suffix = '.jsonl') {
|
|
68
80
|
const out = [];
|
|
69
|
-
if (!root || !(await
|
|
81
|
+
if (!root || !(await exists(root))) return out;
|
|
70
82
|
async function walk(dir) {
|
|
71
83
|
for (const ent of await fs.readdir(dir, { withFileTypes: true })) {
|
|
72
84
|
const p = path.join(dir, ent.name);
|
|
@@ -76,23 +88,51 @@ async function files(root, suffix = '.jsonl') {
|
|
|
76
88
|
await walk(root); return out;
|
|
77
89
|
}
|
|
78
90
|
|
|
79
|
-
async function scanPiSessions(root, source, events) {
|
|
91
|
+
async function scanPiSessions(root, source, events, sessionRows = []) {
|
|
80
92
|
for (const file of await files(root)) {
|
|
81
|
-
let provider = 'unknown', model = 'unknown', session = path.basename(file, '.jsonl');
|
|
93
|
+
let provider = 'unknown', model = 'unknown', session = path.basename(file, '.jsonl'), cwd = '';
|
|
94
|
+
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() };
|
|
82
95
|
const text = await fs.readFile(file, 'utf8').catch(() => '');
|
|
83
96
|
for (const line of text.split(/\r?\n/)) {
|
|
84
97
|
const obj = safeJson(line); if (!obj) continue;
|
|
85
|
-
if (obj.
|
|
98
|
+
if (obj.timestamp) { row.start ||= obj.timestamp; row.end = obj.timestamp; }
|
|
99
|
+
if (obj.type === 'session') { session = obj.id || session; cwd = obj.cwd || cwd; row.key = session; row.session = session; row.cwd = cwd; }
|
|
86
100
|
if (obj.type === 'model_change') { provider = obj.provider || provider; model = obj.modelId || model; }
|
|
87
|
-
|
|
88
|
-
|
|
101
|
+
if (obj.type === 'custom' && obj.customType === 'takomi-runtime-state' && obj.data) {
|
|
102
|
+
const role = obj.data.role || 'unknown';
|
|
103
|
+
const stage = obj.data.stage || 'unknown';
|
|
104
|
+
const workflow = obj.data.workflow || 'unknown';
|
|
105
|
+
row.roles.set(role, (row.roles.get(role) || 0) + 1);
|
|
106
|
+
row.stages.set(stage, (row.stages.get(stage) || 0) + 1);
|
|
107
|
+
row.workflows.set(workflow, (row.workflows.get(workflow) || 0) + 1);
|
|
108
|
+
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 });
|
|
109
|
+
}
|
|
110
|
+
const msg = obj.type === 'message' && obj.message ? obj.message : null;
|
|
111
|
+
if (msg) {
|
|
112
|
+
row.messages += 1;
|
|
113
|
+
if (msg.role === 'user') row.turns += 1;
|
|
114
|
+
for (const part of msg.content || []) {
|
|
115
|
+
if (!part || part.type !== 'toolCall') continue;
|
|
116
|
+
const name = part.name || 'unknown';
|
|
117
|
+
row.toolCalls += 1;
|
|
118
|
+
if (name === 'takomi_subagent') {
|
|
119
|
+
const args = part.arguments || {};
|
|
120
|
+
const count = Array.isArray(args.tasks) ? args.tasks.length : Array.isArray(args.chain) ? args.chain.length : 1;
|
|
121
|
+
row.subagentCalls += count;
|
|
122
|
+
}
|
|
123
|
+
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 });
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
const u = msg && msg.usage;
|
|
127
|
+
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) });
|
|
89
128
|
}
|
|
129
|
+
if (row.messages || row.toolCalls || row.turns) sessionRows.push(row);
|
|
90
130
|
}
|
|
91
131
|
}
|
|
92
132
|
|
|
93
133
|
async function scanRunHistory(file) {
|
|
94
134
|
const runs = [];
|
|
95
|
-
if (!(await
|
|
135
|
+
if (!(await exists(file))) return runs;
|
|
96
136
|
const text = await fs.readFile(file, 'utf8').catch(() => '');
|
|
97
137
|
for (const line of text.split(/\r?\n/)) { const o = safeJson(line); if (o) runs.push(o); }
|
|
98
138
|
return runs;
|
|
@@ -101,24 +141,33 @@ async function scanRunHistory(file) {
|
|
|
101
141
|
export async function collectTakomiStats(opts = {}) {
|
|
102
142
|
const home = opts.home || os.homedir();
|
|
103
143
|
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);
|
|
144
|
+
const rawEvents = [], rawSessions = [];
|
|
145
|
+
await scanPiSessions(path.join(home, '.pi', 'agent', 'sessions'), 'pi-global', rawEvents, rawSessions);
|
|
146
|
+
await scanPiSessions(path.join(cwd, '.pi', 'agent', 'sessions'), 'pi-project', rawEvents, rawSessions);
|
|
147
|
+
await scanPiSessions(path.join(cwd, '.pi', 'takomi'), 'takomi-project', rawEvents, rawSessions);
|
|
108
148
|
const sinceDay = parseSince(opts.since);
|
|
109
|
-
const events = rawEvents
|
|
110
|
-
|
|
111
|
-
.map(e => ({ ...e, project: projectKey(e.file) }));
|
|
149
|
+
const events = rawEvents.filter(e => !sinceDay || e.day >= sinceDay);
|
|
150
|
+
const sessionRows = rawSessions.filter(s => !sinceDay || dayOf(s.end || s.start) >= sinceDay);
|
|
112
151
|
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 };
|
|
152
|
+
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();
|
|
153
|
+
let totals = { input: 0, cache: 0, output: 0, total: 0, cost: 0, events: events.filter(e => e.kind === 'usage').length, toolCalls: 0, turns: 0 };
|
|
154
|
+
for (const s of sessionRows) { totals.toolCalls += s.toolCalls; totals.turns += s.turns; }
|
|
115
155
|
for (const e of events) {
|
|
156
|
+
if (e.kind === 'tool') { add(byTool, e.tool || 'unknown', { total: 0, events: 1 }); continue; }
|
|
157
|
+
if (e.kind === 'role') {
|
|
158
|
+
add(byRole, e.role || 'unknown', { total: 0, events: 1 });
|
|
159
|
+
add(byStage, e.stage || 'unknown', { total: 0, events: 1 });
|
|
160
|
+
add(byWorkflow, e.workflow || 'unknown', { total: 0, events: 1 });
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
116
163
|
totals.input += e.input; totals.cache += e.cache; totals.output += e.output; totals.total += e.total; totals.cost += e.cost;
|
|
117
164
|
add(byDay, e.day, e); add(byModel, e.model, e); add(bySource, e.source, e); add(byProject, e.project, e);
|
|
118
165
|
}
|
|
119
166
|
const byAgent = new Map(); let longestRun = null;
|
|
120
167
|
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
|
-
|
|
168
|
+
const topSessions = [...sessionRows].sort((a,b)=>b.turns-a.turns || b.toolCalls-a.toolCalls).slice(0, 20);
|
|
169
|
+
const mostSubagentsSession = [...sessionRows].sort((a,b)=>b.subagentCalls-a.subagentCalls)[0] || null;
|
|
170
|
+
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, topSessions, mostSubagentsSession, runs, longestRun, recent: events.sort((a,b)=>(b.timestamp||'').localeCompare(a.timestamp||'')).slice(0, 10) };
|
|
122
171
|
}
|
|
123
172
|
|
|
124
173
|
// ── Streak Calculation ──────────────────────────────────────────────────────
|
|
@@ -263,6 +312,12 @@ function renderTable(title, rows, columns) {
|
|
|
263
312
|
return lines.join('\n');
|
|
264
313
|
}
|
|
265
314
|
|
|
315
|
+
function sessionLabel(row, width = 36) {
|
|
316
|
+
const project = row.project || row.cwd || row.key || 'unknown';
|
|
317
|
+
return project.length > width ? '…' + project.slice(-(width - 1)) : project;
|
|
318
|
+
}
|
|
319
|
+
function sessionDay(row) { return dayOf(row.start || row.end).slice(5) || '??-??'; }
|
|
320
|
+
|
|
266
321
|
function renderFocusedView(stats, opts = {}) {
|
|
267
322
|
const view = opts.view;
|
|
268
323
|
const limit = opts.limit || 20;
|
|
@@ -285,11 +340,29 @@ function renderFocusedView(stats, opts = {}) {
|
|
|
285
340
|
{ width: 10, align: 'right', get: r => pc.dim(fmtMoney(r.cost)) },
|
|
286
341
|
{ width: 12, align: 'right', get: r => pc.dim(r.events + ' calls') },
|
|
287
342
|
]],
|
|
288
|
-
agents: ['
|
|
343
|
+
agents: ['Main Agent Roles', stats.byRole, [
|
|
344
|
+
{ width: 24, align: 'left', get: r => pc.white(r.key) },
|
|
345
|
+
{ width: 8, align: 'right', get: r => pc.cyan(String(r.events)) },
|
|
346
|
+
{ width: 12, align: 'left', get: () => pc.dim('state hits') },
|
|
347
|
+
]],
|
|
348
|
+
subagents: ['Top Subagents', stats.byAgent, [
|
|
289
349
|
{ width: 24, align: 'left', get: r => pc.white(r.key) },
|
|
290
350
|
{ width: 8, align: 'right', get: r => pc.cyan(String(r.events)) },
|
|
291
351
|
{ width: 8, align: 'left', get: () => pc.dim('runs') },
|
|
292
352
|
]],
|
|
353
|
+
tools: ['Top Tools', stats.byTool, [
|
|
354
|
+
{ width: 28, align: 'left', get: r => pc.white(r.key) },
|
|
355
|
+
{ width: 10, align: 'right', get: r => pc.cyan(String(r.events)) },
|
|
356
|
+
{ width: 8, align: 'left', get: () => pc.dim('calls') },
|
|
357
|
+
]],
|
|
358
|
+
sessions: ['Longest Main Sessions', stats.topSessions, [
|
|
359
|
+
{ width: 6, align: 'left', get: r => pc.dim(sessionDay(r)) },
|
|
360
|
+
{ width: 34, align: 'left', get: r => pc.white(sessionLabel(r, 34)) },
|
|
361
|
+
{ width: 8, align: 'right', get: r => pc.cyan(String(r.turns)) },
|
|
362
|
+
{ width: 8, align: 'left', get: () => pc.dim('turns') },
|
|
363
|
+
{ width: 8, align: 'right', get: r => pc.cyan(String(r.toolCalls)) },
|
|
364
|
+
{ width: 8, align: 'left', get: () => pc.dim('tools') },
|
|
365
|
+
]],
|
|
293
366
|
daily: ['Daily Usage', [...stats.byDay].reverse(), [
|
|
294
367
|
{ width: 12, align: 'left', get: r => pc.white(r.key) },
|
|
295
368
|
{ width: 10, align: 'right', get: r => pc.cyan(fmtTokens(r.total)) },
|
|
@@ -330,7 +403,7 @@ export function renderTakomiStats(stats, opts = {}) {
|
|
|
330
403
|
statCard(fmtTokens(stats.totals.cache), 'Cache Tokens'),
|
|
331
404
|
statCard(fmtMoney(stats.totals.cost), 'Est. Cost'),
|
|
332
405
|
statCard(String(stats.sessions), 'Sessions'),
|
|
333
|
-
statCard(String(stats.
|
|
406
|
+
statCard(String(stats.totals.turns), 'Main Turns'),
|
|
334
407
|
];
|
|
335
408
|
|
|
336
409
|
const cardW = Math.floor((W - 4) / cards1.length);
|
|
@@ -361,7 +434,7 @@ export function renderTakomiStats(stats, opts = {}) {
|
|
|
361
434
|
const cards2 = [
|
|
362
435
|
statCard(peak ? fmtTokens(peak.total) : '-', 'Peak Day'),
|
|
363
436
|
statCard(topModel, 'Top Model'),
|
|
364
|
-
statCard(
|
|
437
|
+
statCard(String(stats.totals.toolCalls), 'Tool Calls'),
|
|
365
438
|
statCard(`${streaks.current} days`, 'Current Streak'),
|
|
366
439
|
statCard(`${streaks.longest} days`, 'Longest Streak'),
|
|
367
440
|
];
|
|
@@ -404,6 +477,39 @@ export function renderTakomiStats(stats, opts = {}) {
|
|
|
404
477
|
]));
|
|
405
478
|
}
|
|
406
479
|
|
|
480
|
+
// ── Main Agent Roles Table ──────────────────────────────────────────────
|
|
481
|
+
if (stats.byRole.length) {
|
|
482
|
+
lines.push('');
|
|
483
|
+
lines.push(renderTable('Main Agent Roles', stats.byRole.slice(0, modelLimit), [
|
|
484
|
+
{ width: 20, align: 'left', get: r => pc.white(r.key) },
|
|
485
|
+
{ width: 8, align: 'right', get: r => pc.cyan(String(r.events)) },
|
|
486
|
+
{ width: 10, align: 'left', get: r => pc.dim('state hits') },
|
|
487
|
+
]));
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// ── Main Session Table ──────────────────────────────────────────────────
|
|
491
|
+
if (stats.topSessions.length) {
|
|
492
|
+
lines.push('');
|
|
493
|
+
lines.push(renderTable('Longest Main Sessions', stats.topSessions.slice(0, 5), [
|
|
494
|
+
{ width: 6, align: 'left', get: r => pc.dim(sessionDay(r)) },
|
|
495
|
+
{ width: 32, align: 'left', get: r => pc.white(sessionLabel(r, 32)) },
|
|
496
|
+
{ width: 8, align: 'right', get: r => pc.cyan(String(r.turns)) },
|
|
497
|
+
{ width: 8, align: 'left', get: r => pc.dim('turns') },
|
|
498
|
+
{ width: 8, align: 'right', get: r => pc.cyan(String(r.toolCalls)) },
|
|
499
|
+
{ width: 6, align: 'left', get: r => pc.dim('tools') },
|
|
500
|
+
]));
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// ── Tools Table ─────────────────────────────────────────────────────────
|
|
504
|
+
if (stats.byTool.length) {
|
|
505
|
+
lines.push('');
|
|
506
|
+
lines.push(renderTable('Top Tools', stats.byTool.slice(0, modelLimit), [
|
|
507
|
+
{ width: 24, align: 'left', get: r => pc.white(r.key) },
|
|
508
|
+
{ width: 8, align: 'right', get: r => pc.cyan(String(r.events)) },
|
|
509
|
+
{ width: 6, align: 'left', get: r => pc.dim('calls') },
|
|
510
|
+
]));
|
|
511
|
+
}
|
|
512
|
+
|
|
407
513
|
// ── Sources Table ───────────────────────────────────────────────────────
|
|
408
514
|
lines.push('');
|
|
409
515
|
lines.push(renderTable('Sources', stats.bySource, [
|
|
@@ -412,10 +518,10 @@ export function renderTakomiStats(stats, opts = {}) {
|
|
|
412
518
|
{ width: 14, align: 'right', get: r => pc.dim(r.events + ' events') },
|
|
413
519
|
]));
|
|
414
520
|
|
|
415
|
-
// ──
|
|
521
|
+
// ── Subagents Table ─────────────────────────────────────────────────────
|
|
416
522
|
if (stats.byAgent.length) {
|
|
417
523
|
lines.push('');
|
|
418
|
-
lines.push(renderTable('Top
|
|
524
|
+
lines.push(renderTable('Top Subagents', stats.byAgent.slice(0, modelLimit), [
|
|
419
525
|
{ width: 20, align: 'left', get: r => pc.white(r.key) },
|
|
420
526
|
{ width: 8, align: 'right', get: r => pc.cyan(String(r.events)) },
|
|
421
527
|
{ width: 6, align: 'left', get: r => pc.dim('runs') },
|