Add token-stats-hook Go module and update README

This commit is contained in:
Knowit
2026-04-18 10:05:00 +08:00
parent e04f79b1d1
commit e29e3ec91c
4 changed files with 143 additions and 0 deletions

View File

@@ -12,6 +12,15 @@ cp claudeplus/commands/* ~/.claude/commands/
重启 Claude Code 后即可使用。 重启 Claude Code 后即可使用。
## 模块列表
| 模块 | 路径 | 说明 |
|------|------|------|
| 命令集 | `commands/` | Claude Code 自定义斜杠命令 |
| Stop Hook | `hooks/token-stats-hook/` | 会话结束时自动备份 token 用量Go 二进制) |
---
## 命令列表 ## 命令列表
### /token-stats [username|all] ### /token-stats [username|all]

View File

@@ -0,0 +1,39 @@
# token-stats-hook
Claude Code Stop hook每次会话结束时自动将 token 用量快照追加到 `~/.claude/token-stats-backup.jsonl`
用 Go 编写,编译为二进制,启动时间 <5ms。
## 安装
```bash
cd hooks/token-stats-hook
go build -o ~/.claude/token-stats-hook-bin .
```
然后在 `~/.claude/settings.json` 中加入:
```json
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "~/.claude/token-stats-hook-bin 2>/dev/null || true"
}
]
}
]
}
}
```
## 备份格式
每行一条 JSON 记录:
```json
{"timestamp":"2026-04-18T02:03:11Z","date":"2026-04-18","user":"charles","input":12861,"output":176499,"cache_write":1352380,"cache_read":15525643,"actual":189360}
```

View File

@@ -0,0 +1,3 @@
module token-stats-hook
go 1.18

View File

@@ -0,0 +1,92 @@
package main
import (
"bufio"
"encoding/json"
"fmt"
"os"
"path/filepath"
"time"
)
type usage struct {
InputTokens int `json:"input_tokens"`
OutputTokens int `json:"output_tokens"`
CacheCreationInputTokens int `json:"cache_creation_input_tokens"`
CacheReadInputTokens int `json:"cache_read_input_tokens"`
}
type message struct {
Usage usage `json:"usage"`
}
type entry struct {
Message message `json:"message"`
}
type record struct {
Timestamp string `json:"timestamp"`
Date string `json:"date"`
User string `json:"user"`
Input int `json:"input"`
Output int `json:"output"`
CacheWrite int `json:"cache_write"`
CacheRead int `json:"cache_read"`
Actual int `json:"actual"`
}
func main() {
home, _ := os.UserHomeDir()
user := os.Getenv("USER")
if user == "" {
user = filepath.Base(home)
}
var total record
pattern := filepath.Join(home, ".claude", "projects", "**", "*.jsonl")
matches, _ := filepath.Glob(pattern)
// filepath.Glob doesn't support **, so walk manually
projectsDir := filepath.Join(home, ".claude", "projects")
filepath.Walk(projectsDir, func(path string, info os.FileInfo, err error) error {
if err != nil || info.IsDir() || filepath.Ext(path) != ".jsonl" {
return nil
}
_ = matches
f, err := os.Open(path)
if err != nil {
return nil
}
defer f.Close()
scanner := bufio.NewScanner(f)
scanner.Buffer(make([]byte, 1024*1024), 1024*1024)
for scanner.Scan() {
var e entry
if json.Unmarshal(scanner.Bytes(), &e) == nil {
u := e.Message.Usage
total.Input += u.InputTokens
total.Output += u.OutputTokens
total.CacheWrite += u.CacheCreationInputTokens
total.CacheRead += u.CacheReadInputTokens
}
}
return nil
})
now := time.Now().UTC()
total.Timestamp = now.Format(time.RFC3339)
total.Date = now.Format("2006-01-02")
total.User = user
total.Actual = total.Input + total.Output
backup := filepath.Join(home, ".claude", "token-stats-backup.jsonl")
f, err := os.OpenFile(backup, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
os.Exit(1)
}
defer f.Close()
line, _ := json.Marshal(total)
fmt.Fprintf(f, "%s\n", line)
}