Skip to content
This repository was archived by the owner on Apr 1, 2026. It is now read-only.

Commit 63862b1

Browse files
authored
feat: implement stats command (anomalyco#3832)
1 parent 1cf1e88 commit 63862b1

1 file changed

Lines changed: 196 additions & 9 deletions

File tree

packages/opencode/src/cli/cmd/stats.ts

Lines changed: 196 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1+
import type { Argv } from "yargs"
12
import { cmd } from "./cmd"
3+
import { Session } from "../../session"
4+
import { bootstrap } from "../bootstrap"
5+
import { Storage } from "../../storage/storage"
6+
import { Project } from "../../project/project"
7+
import { Instance } from "../../project/instance"
28

39
interface SessionStats {
410
totalSessions: number
@@ -24,10 +30,186 @@ interface SessionStats {
2430

2531
export const StatsCommand = cmd({
2632
command: "stats",
27-
handler: async () => {},
33+
describe: "show token usage and cost statistics",
34+
builder: (yargs: Argv) => {
35+
return yargs
36+
.option("days", {
37+
describe: "show stats for the last N days (default: all time)",
38+
type: "number",
39+
})
40+
.option("tools", {
41+
describe: "number of tools to show (default: all)",
42+
type: "number",
43+
})
44+
.option("project", {
45+
describe: "filter by project (default: all projects, empty string: current project)",
46+
type: "string",
47+
})
48+
},
49+
handler: async (args) => {
50+
await bootstrap(process.cwd(), async () => {
51+
const stats = await aggregateSessionStats(args.days, args.project)
52+
displayStats(stats, args.tools)
53+
})
54+
},
2855
})
2956

30-
export function displayStats(stats: SessionStats) {
57+
async function getCurrentProject(): Promise<Project.Info> {
58+
return Instance.project
59+
}
60+
61+
async function getAllSessions(): Promise<Session.Info[]> {
62+
const sessions: Session.Info[] = []
63+
64+
const projectKeys = await Storage.list(["project"])
65+
const projects = await Promise.all(projectKeys.map((key) => Storage.read<Project.Info>(key)))
66+
67+
for (const project of projects) {
68+
if (!project) continue
69+
70+
const sessionKeys = await Storage.list(["session", project.id])
71+
const projectSessions = await Promise.all(
72+
sessionKeys.map((key) => Storage.read<Session.Info>(key)),
73+
)
74+
75+
for (const session of projectSessions) {
76+
if (session) {
77+
sessions.push(session)
78+
}
79+
}
80+
}
81+
82+
return sessions
83+
}
84+
85+
async function aggregateSessionStats(days?: number, projectFilter?: string): Promise<SessionStats> {
86+
const sessions = await getAllSessions()
87+
const DAYS_IN_SECOND = 24 * 60 * 60 * 1000
88+
const cutoffTime = days ? Date.now() - days * DAYS_IN_SECOND : 0
89+
90+
let filteredSessions = days
91+
? sessions.filter((session) => session.time.updated >= cutoffTime)
92+
: sessions
93+
94+
if (projectFilter !== undefined) {
95+
if (projectFilter === "") {
96+
const currentProject = await getCurrentProject()
97+
filteredSessions = filteredSessions.filter(
98+
(session) => session.projectID === currentProject.id,
99+
)
100+
} else {
101+
filteredSessions = filteredSessions.filter((session) => session.projectID === projectFilter)
102+
}
103+
}
104+
105+
const stats: SessionStats = {
106+
totalSessions: filteredSessions.length,
107+
totalMessages: 0,
108+
totalCost: 0,
109+
totalTokens: {
110+
input: 0,
111+
output: 0,
112+
reasoning: 0,
113+
cache: {
114+
read: 0,
115+
write: 0,
116+
},
117+
},
118+
toolUsage: {},
119+
dateRange: {
120+
earliest: Date.now(),
121+
latest: Date.now(),
122+
},
123+
days: 0,
124+
costPerDay: 0,
125+
}
126+
127+
if (filteredSessions.length > 1000) {
128+
console.log(
129+
`Large dataset detected (${filteredSessions.length} sessions). This may take a while...`,
130+
)
131+
}
132+
133+
if (filteredSessions.length === 0) {
134+
return stats
135+
}
136+
137+
let earliestTime = Date.now()
138+
let latestTime = 0
139+
140+
const BATCH_SIZE = 20
141+
for (let i = 0; i < filteredSessions.length; i += BATCH_SIZE) {
142+
const batch = filteredSessions.slice(i, i + BATCH_SIZE)
143+
144+
const batchPromises = batch.map(async (session) => {
145+
const messages = await Session.messages(session.id)
146+
147+
let sessionCost = 0
148+
let sessionTokens = { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }
149+
let sessionToolUsage: Record<string, number> = {}
150+
151+
for (const message of messages) {
152+
if (message.info.role === "assistant") {
153+
sessionCost += message.info.cost || 0
154+
155+
if (message.info.tokens) {
156+
sessionTokens.input += message.info.tokens.input || 0
157+
sessionTokens.output += message.info.tokens.output || 0
158+
sessionTokens.reasoning += message.info.tokens.reasoning || 0
159+
sessionTokens.cache.read += message.info.tokens.cache?.read || 0
160+
sessionTokens.cache.write += message.info.tokens.cache?.write || 0
161+
}
162+
}
163+
164+
for (const part of message.parts) {
165+
if (part.type === "tool" && part.tool) {
166+
sessionToolUsage[part.tool] = (sessionToolUsage[part.tool] || 0) + 1
167+
}
168+
}
169+
}
170+
171+
return {
172+
messageCount: messages.length,
173+
sessionCost,
174+
sessionTokens,
175+
sessionToolUsage,
176+
earliestTime: session.time.created,
177+
latestTime: session.time.updated,
178+
}
179+
})
180+
181+
const batchResults = await Promise.all(batchPromises)
182+
183+
for (const result of batchResults) {
184+
earliestTime = Math.min(earliestTime, result.earliestTime)
185+
latestTime = Math.max(latestTime, result.latestTime)
186+
187+
stats.totalMessages += result.messageCount
188+
stats.totalCost += result.sessionCost
189+
stats.totalTokens.input += result.sessionTokens.input
190+
stats.totalTokens.output += result.sessionTokens.output
191+
stats.totalTokens.reasoning += result.sessionTokens.reasoning
192+
stats.totalTokens.cache.read += result.sessionTokens.cache.read
193+
stats.totalTokens.cache.write += result.sessionTokens.cache.write
194+
195+
for (const [tool, count] of Object.entries(result.sessionToolUsage)) {
196+
stats.toolUsage[tool] = (stats.toolUsage[tool] || 0) + count
197+
}
198+
}
199+
}
200+
201+
const actualDays = Math.max(1, Math.ceil((latestTime - earliestTime) / DAYS_IN_SECOND))
202+
stats.dateRange = {
203+
earliest: earliestTime,
204+
latest: latestTime,
205+
}
206+
stats.days = actualDays
207+
stats.costPerDay = stats.totalCost / actualDays
208+
209+
return stats
210+
}
211+
212+
export function displayStats(stats: SessionStats, toolLimit?: number) {
31213
const width = 56
32214

33215
function renderRow(label: string, value: string): string {
@@ -64,30 +246,35 @@ export function displayStats(stats: SessionStats) {
64246

65247
// Tool Usage section
66248
if (Object.keys(stats.toolUsage).length > 0) {
67-
const sortedTools = Object.entries(stats.toolUsage)
68-
.sort(([, a], [, b]) => b - a)
69-
.slice(0, 10)
249+
const sortedTools = Object.entries(stats.toolUsage).sort(([, a], [, b]) => b - a)
250+
const toolsToDisplay = toolLimit ? sortedTools.slice(0, toolLimit) : sortedTools
70251

71252
console.log("┌────────────────────────────────────────────────────────┐")
72253
console.log("│ TOOL USAGE │")
73254
console.log("├────────────────────────────────────────────────────────┤")
74255

75-
const maxCount = Math.max(...sortedTools.map(([, count]) => count))
256+
const maxCount = Math.max(...toolsToDisplay.map(([, count]) => count))
76257
const totalToolUsage = Object.values(stats.toolUsage).reduce((a, b) => a + b, 0)
77258

78-
for (const [tool, count] of sortedTools) {
259+
for (const [tool, count] of toolsToDisplay) {
79260
const barLength = Math.max(1, Math.floor((count / maxCount) * 20))
80261
const bar = "█".repeat(barLength)
81262
const percentage = ((count / totalToolUsage) * 100).toFixed(1)
82263

83-
const content = ` ${tool.padEnd(10)} ${bar.padEnd(20)} ${count.toString().padStart(3)} (${percentage.padStart(4)}%)`
84-
const padding = Math.max(0, width - content.length)
264+
const maxToolLength = 18
265+
const truncatedTool =
266+
tool.length > maxToolLength ? tool.substring(0, maxToolLength - 2) + ".." : tool
267+
const toolName = truncatedTool.padEnd(maxToolLength)
268+
269+
const content = ` ${toolName} ${bar.padEnd(20)} ${count.toString().padStart(3)} (${percentage.padStart(4)}%)`
270+
const padding = Math.max(0, width - content.length - 1)
85271
console.log(`│${content}${" ".repeat(padding)} │`)
86272
}
87273
console.log("└────────────────────────────────────────────────────────┘")
88274
}
89275
console.log()
90276
}
277+
91278
function formatNumber(num: number): string {
92279
if (num >= 1000000) {
93280
return (num / 1000000).toFixed(1) + "M"

0 commit comments

Comments
 (0)