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
- if (msg.role === 'user') row.turns += 1;
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
- 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);
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 Main Sessions', stats.topSessions, [
389
+ sessions: ['Longest Active Sessions', stats.topSessions, [
359
390
  { width: 6, align: 'left', get: r => pc.dim(sessionDay(r)) },
360
- { width: 34, align: 'left', get: r => pc.white(sessionLabel(r, 34)) },
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 Main Sessions', stats.topSessions.slice(0, 5), [
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: 32, align: 'left', get: r => pc.white(sessionLabel(r, 32)) },
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.20",
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": {