takomi 2.1.20 → 2.1.21
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.
|
@@ -88,10 +88,10 @@ async function files(root, suffix = '.jsonl') {
|
|
|
88
88
|
await walk(root); return out;
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
-
async function scanPiSessions(root, source, events, sessionRows = []) {
|
|
91
|
+
async function scanPiSessions(root, source, events, sessionRows = [], taskRows = []) {
|
|
92
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() };
|
|
93
|
+
let provider = 'unknown', model = 'unknown', session = path.basename(file, '.jsonl'), cwd = '', currentTask = null;
|
|
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(), activeMs: 0 };
|
|
95
95
|
const text = await fs.readFile(file, 'utf8').catch(() => '');
|
|
96
96
|
for (const line of text.split(/\r?\n/)) {
|
|
97
97
|
const obj = safeJson(line); if (!obj) continue;
|
|
@@ -110,11 +110,20 @@ async function scanPiSessions(root, source, events, sessionRows = []) {
|
|
|
110
110
|
const msg = obj.type === 'message' && obj.message ? obj.message : null;
|
|
111
111
|
if (msg) {
|
|
112
112
|
row.messages += 1;
|
|
113
|
-
|
|
113
|
+
const ts = obj.timestamp || msg.timestamp || '';
|
|
114
|
+
if (msg.role === 'user') {
|
|
115
|
+
if (currentTask?.end && currentTask.end !== currentTask.start) taskRows.push(currentTask);
|
|
116
|
+
row.turns += 1;
|
|
117
|
+
const textPart = (msg.content || []).find(p => p?.type === 'text')?.text || '';
|
|
118
|
+
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() };
|
|
119
|
+
} else if (currentTask && ts) {
|
|
120
|
+
currentTask.end = ts;
|
|
121
|
+
}
|
|
114
122
|
for (const part of msg.content || []) {
|
|
115
123
|
if (!part || part.type !== 'toolCall') continue;
|
|
116
124
|
const name = part.name || 'unknown';
|
|
117
125
|
row.toolCalls += 1;
|
|
126
|
+
if (currentTask) currentTask.toolCalls += 1;
|
|
118
127
|
if (name === 'takomi_subagent') {
|
|
119
128
|
const args = part.arguments || {};
|
|
120
129
|
const count = Array.isArray(args.tasks) ? args.tasks.length : Array.isArray(args.chain) ? args.chain.length : 1;
|
|
@@ -126,6 +135,8 @@ async function scanPiSessions(root, source, events, sessionRows = []) {
|
|
|
126
135
|
const u = msg && msg.usage;
|
|
127
136
|
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
137
|
}
|
|
138
|
+
if (currentTask?.end && currentTask.end !== currentTask.start) taskRows.push(currentTask);
|
|
139
|
+
row.activeMs = taskRows.filter(t => t.file === file).reduce((sum, t) => sum + taskDuration(t), 0);
|
|
129
140
|
if (row.messages || row.toolCalls || row.turns) sessionRows.push(row);
|
|
130
141
|
}
|
|
131
142
|
}
|
|
@@ -141,13 +152,16 @@ async function scanRunHistory(file) {
|
|
|
141
152
|
export async function collectTakomiStats(opts = {}) {
|
|
142
153
|
const home = opts.home || os.homedir();
|
|
143
154
|
const cwd = opts.cwd || process.cwd();
|
|
144
|
-
const rawEvents = [], rawSessions = [];
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
await scanPiSessions(
|
|
155
|
+
const rawEvents = [], rawSessions = [], rawTasks = [];
|
|
156
|
+
const globalSessions = path.resolve(path.join(home, '.pi', 'agent', 'sessions'));
|
|
157
|
+
const projectSessions = path.resolve(path.join(cwd, '.pi', 'agent', 'sessions'));
|
|
158
|
+
await scanPiSessions(globalSessions, 'pi-global', rawEvents, rawSessions, rawTasks);
|
|
159
|
+
if (projectSessions !== globalSessions) await scanPiSessions(projectSessions, 'pi-project', rawEvents, rawSessions, rawTasks);
|
|
160
|
+
await scanPiSessions(path.join(cwd, '.pi', 'takomi'), 'takomi-project', rawEvents, rawSessions, rawTasks);
|
|
148
161
|
const sinceDay = parseSince(opts.since);
|
|
149
162
|
const events = rawEvents.filter(e => !sinceDay || e.day >= sinceDay);
|
|
150
163
|
const sessionRows = rawSessions.filter(s => !sinceDay || dayOf(s.end || s.start) >= sinceDay);
|
|
164
|
+
const taskRows = rawTasks.filter(t => !sinceDay || dayOf(t.end || t.start) >= sinceDay);
|
|
151
165
|
const runs = await scanRunHistory(path.join(home, '.pi', 'agent', 'run-history.jsonl'));
|
|
152
166
|
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
167
|
let totals = { input: 0, cache: 0, output: 0, total: 0, cost: 0, events: events.filter(e => e.kind === 'usage').length, toolCalls: 0, turns: 0 };
|
|
@@ -165,9 +179,10 @@ export async function collectTakomiStats(opts = {}) {
|
|
|
165
179
|
}
|
|
166
180
|
const byAgent = new Map(); let longestRun = null;
|
|
167
181
|
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);
|
|
182
|
+
const topSessions = [...sessionRows].sort((a,b)=>(b.activeMs||0)-(a.activeMs||0) || b.turns-a.turns || b.toolCalls-a.toolCalls).slice(0, 20);
|
|
183
|
+
const topTasks = [...taskRows].sort((a,b)=>taskDuration(b)-taskDuration(a) || b.toolCalls-a.toolCalls).slice(0, 20);
|
|
169
184
|
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) };
|
|
185
|
+
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) };
|
|
171
186
|
}
|
|
172
187
|
|
|
173
188
|
// ── Streak Calculation ──────────────────────────────────────────────────────
|
|
@@ -317,6 +332,22 @@ function sessionLabel(row, width = 36) {
|
|
|
317
332
|
return project.length > width ? '…' + project.slice(-(width - 1)) : project;
|
|
318
333
|
}
|
|
319
334
|
function sessionDay(row) { return dayOf(row.start || row.end).slice(5) || '??-??'; }
|
|
335
|
+
function sessionDuration(row) {
|
|
336
|
+
return row?.activeMs || 0;
|
|
337
|
+
}
|
|
338
|
+
function taskDuration(row) {
|
|
339
|
+
const start = Date.parse(row?.start || '');
|
|
340
|
+
const end = Date.parse(row?.end || '');
|
|
341
|
+
return Number.isFinite(start) && Number.isFinite(end) && end >= start ? end - start : 0;
|
|
342
|
+
}
|
|
343
|
+
function taskLabel(row, width = 34) {
|
|
344
|
+
const label = row?.title || row?.project || row?.session || 'unknown';
|
|
345
|
+
return label.length > width ? label.slice(0, width - 1) + '…' : label;
|
|
346
|
+
}
|
|
347
|
+
function runLabel(run, width = 28) {
|
|
348
|
+
const label = run ? `${run.agent || 'unknown'}: ${run.task || ''}`.trim() : '-';
|
|
349
|
+
return label.length > width ? label.slice(0, width - 1) + '…' : label;
|
|
350
|
+
}
|
|
320
351
|
|
|
321
352
|
function renderFocusedView(stats, opts = {}) {
|
|
322
353
|
const view = opts.view;
|
|
@@ -355,14 +386,21 @@ function renderFocusedView(stats, opts = {}) {
|
|
|
355
386
|
{ width: 10, align: 'right', get: r => pc.cyan(String(r.events)) },
|
|
356
387
|
{ width: 8, align: 'left', get: () => pc.dim('calls') },
|
|
357
388
|
]],
|
|
358
|
-
sessions: ['Longest
|
|
389
|
+
sessions: ['Longest Active Sessions', stats.topSessions, [
|
|
359
390
|
{ width: 6, align: 'left', get: r => pc.dim(sessionDay(r)) },
|
|
360
|
-
{ width:
|
|
391
|
+
{ width: 30, align: 'left', get: r => pc.white(sessionLabel(r, 30)) },
|
|
392
|
+
{ width: 9, align: 'right', get: r => pc.cyan(ms(sessionDuration(r))) },
|
|
361
393
|
{ width: 8, align: 'right', get: r => pc.cyan(String(r.turns)) },
|
|
362
394
|
{ width: 8, align: 'left', get: () => pc.dim('turns') },
|
|
363
395
|
{ width: 8, align: 'right', get: r => pc.cyan(String(r.toolCalls)) },
|
|
364
396
|
{ width: 8, align: 'left', get: () => pc.dim('tools') },
|
|
365
397
|
]],
|
|
398
|
+
tasks: ['Longest Tasks', stats.topTasks || [], [
|
|
399
|
+
{ width: 6, align: 'left', get: r => pc.dim(dayOf(r.start).slice(5)) },
|
|
400
|
+
{ width: 9, align: 'right', get: r => pc.cyan(ms(taskDuration(r))) },
|
|
401
|
+
{ width: 8, align: 'right', get: r => pc.cyan(String(r.toolCalls)) },
|
|
402
|
+
{ width: 36, align: 'left', get: r => pc.white(taskLabel(r, 36)) },
|
|
403
|
+
]],
|
|
366
404
|
daily: ['Daily Usage', [...stats.byDay].reverse(), [
|
|
367
405
|
{ width: 12, align: 'left', get: r => pc.white(r.key) },
|
|
368
406
|
{ width: 10, align: 'right', get: r => pc.cyan(fmtTokens(r.total)) },
|
|
@@ -385,6 +423,8 @@ export function renderTakomiStats(stats, opts = {}) {
|
|
|
385
423
|
const topModel = stats.byModel[0]?.key || 'unknown';
|
|
386
424
|
const peak = stats.byDay.reduce((a,b) => b.total > (a?.total||0) ? b : a, null);
|
|
387
425
|
const streaks = calcStreaks(stats.byDay);
|
|
426
|
+
const longestSession = stats.topSessions[0] || null;
|
|
427
|
+
const longestTask = stats.topTasks?.[0] || null;
|
|
388
428
|
const lines = [];
|
|
389
429
|
|
|
390
430
|
// ── Header ────────────────────────────────────────────────────────────
|
|
@@ -443,6 +483,19 @@ export function renderTakomiStats(stats, opts = {}) {
|
|
|
443
483
|
lines.push(v2);
|
|
444
484
|
lines.push(l2);
|
|
445
485
|
|
|
486
|
+
// ── Duration Cards ────────────────────────────────────────────────────
|
|
487
|
+
lines.push('');
|
|
488
|
+
const cards3 = [
|
|
489
|
+
statCard(longestSession ? ms(sessionDuration(longestSession)) : '-', 'Longest Session'),
|
|
490
|
+
statCard(longestSession ? String(longestSession.turns) : '-', 'Session Turns'),
|
|
491
|
+
statCard(longestTask ? ms(taskDuration(longestTask)) : '-', 'Longest Task'),
|
|
492
|
+
statCard(longestTask ? String(longestTask.toolCalls) : '-', 'Task Tools'),
|
|
493
|
+
statCard(stats.mostSubagentsSession ? String(stats.mostSubagentsSession.subagentCalls) : '0', 'Most Subagents'),
|
|
494
|
+
];
|
|
495
|
+
const [v3, l3] = buildCardLines(cards3);
|
|
496
|
+
lines.push(v3);
|
|
497
|
+
lines.push(l3);
|
|
498
|
+
|
|
446
499
|
// ── Info line ─────────────────────────────────────────────────────────
|
|
447
500
|
lines.push('');
|
|
448
501
|
const infoText = `Peak: ${peak?.key || '-'} · ${streaks.quietDays} quiet days · ${stats.totals.events.toLocaleString()} events${stats.since ? ` · since ${stats.since}` : ''}`;
|
|
@@ -490,9 +543,10 @@ export function renderTakomiStats(stats, opts = {}) {
|
|
|
490
543
|
// ── Main Session Table ──────────────────────────────────────────────────
|
|
491
544
|
if (stats.topSessions.length) {
|
|
492
545
|
lines.push('');
|
|
493
|
-
lines.push(renderTable('Longest
|
|
546
|
+
lines.push(renderTable('Longest Active Sessions', stats.topSessions.slice(0, 5), [
|
|
494
547
|
{ width: 6, align: 'left', get: r => pc.dim(sessionDay(r)) },
|
|
495
|
-
{ width:
|
|
548
|
+
{ width: 28, align: 'left', get: r => pc.white(sessionLabel(r, 28)) },
|
|
549
|
+
{ width: 9, align: 'right', get: r => pc.cyan(ms(sessionDuration(r))) },
|
|
496
550
|
{ width: 8, align: 'right', get: r => pc.cyan(String(r.turns)) },
|
|
497
551
|
{ width: 8, align: 'left', get: r => pc.dim('turns') },
|
|
498
552
|
{ width: 8, align: 'right', get: r => pc.cyan(String(r.toolCalls)) },
|
|
@@ -500,6 +554,27 @@ export function renderTakomiStats(stats, opts = {}) {
|
|
|
500
554
|
]));
|
|
501
555
|
}
|
|
502
556
|
|
|
557
|
+
// ── Longest Tasks ──────────────────────────────────────────────────────
|
|
558
|
+
if (stats.topTasks?.length) {
|
|
559
|
+
lines.push('');
|
|
560
|
+
lines.push(renderTable('Longest Tasks', stats.topTasks.slice(0, 5), [
|
|
561
|
+
{ width: 6, align: 'left', get: r => pc.dim(dayOf(r.start).slice(5)) },
|
|
562
|
+
{ width: 9, align: 'right', get: r => pc.cyan(ms(taskDuration(r))) },
|
|
563
|
+
{ width: 8, align: 'right', get: r => pc.cyan(String(r.toolCalls)) },
|
|
564
|
+
{ width: 34, align: 'left', get: r => pc.white(taskLabel(r, 34)) },
|
|
565
|
+
]));
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// ── Longest Subagent Run ───────────────────────────────────────────────
|
|
569
|
+
if (stats.longestRun) {
|
|
570
|
+
lines.push('');
|
|
571
|
+
lines.push(renderTable('Longest Subagent Run', [stats.longestRun], [
|
|
572
|
+
{ width: 22, align: 'left', get: r => pc.white(r.agent || 'unknown') },
|
|
573
|
+
{ width: 10, align: 'right', get: r => pc.cyan(ms(+r.duration || 0)) },
|
|
574
|
+
{ width: 30, align: 'left', get: r => pc.dim(runLabel(r, 30)) },
|
|
575
|
+
]));
|
|
576
|
+
}
|
|
577
|
+
|
|
503
578
|
// ── Tools Table ─────────────────────────────────────────────────────────
|
|
504
579
|
if (stats.byTool.length) {
|
|
505
580
|
lines.push('');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "takomi",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.21",
|
|
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": {
|