# @cluski/pi-session-stats

本地 CLI：扫描 `~/.pi/agent/sessions/` 下的 session 日志（jsonl），启动一个本地 Web 看板，分析 Pi 的 token 用量。subagent / trellis_subagent 派发的子代理用量会自动从主会话的工具结果中提取，无需额外目录。

## 功能

- **时间范围**：近一天（按小时分桶）、近一周（按天）、近一个月（按天）
- **汇总卡片**：总 tokens、输入 / 输出、缓存读取 / 写入、缓存命中率、费用、请求数、会话数
- **趋势图**：堆叠柱状图（输入 / 输出 / 缓存读取 / 缓存写入），悬停查看明细
- **模型排行**：按 provider/model 统计 token 总量、缓存命中率、费用、请求数、会话数，支持点击表头排序
- **文件夹排行**：按工作目录（cwd）统计 token 总量、缓存命中率、费用、请求数、会话数，支持点击表头排序
- **增量扫描**：按文件 mtime/size 缓存解析结果，"重新扫描" 只读取变更过的文件
- **子代理自动纳入**：主会话中 `subagent` / `trellis_subagent` 工具结果（toolResult）携带的子代理 token 用量会被一并提取，与主会话用量统一汇总

## 使用

### CLI

```bash
# 在本 monorepo 中
node packages/pi-session-stats/src/cli.ts

# 或全局安装后
npm install -g @cluski/pi-session-stats
pi-session-stats
```

启动后访问终端中打印的地址（默认 <http://127.0.0.1:4789>）。

### 作为库引入

```js
// ESM
import {
  parseSessionFile,
  SessionStore,
  aggregate,
  isRangeKey,
  RANGES,
  createStatsServer,
} from "@cluski/pi-session-stats";

// CJS
const {
  parseSessionFile,
  SessionStore,
  aggregate,
  isRangeKey,
  createStatsServer,
} = require("@cluski/pi-session-stats");
```

主要导出：

| 导出 | 说明 |
| --- | --- |
| `parseSessionFile(content, fallbackSessionId)` | 解析单个 session jsonl 文本，返回 `UsageRecord[]` |
| `SessionStore` | 增量扫描器类，缓存文件 mtime/size，`scan()` 返回 `ScanResult` |
| `aggregate(records, range, now?)` | 聚合用量记录，返回 `Overview`（含 summary、buckets、models、folders） |
| `isRangeKey(value)` | 类型守卫，判断是否为 `"day" \| "week" \| "month"` |
| `RANGES` | 三个范围对应的 bucket 配置常量 |
| `createStatsServer(store)` | 创建 HTTP 服务（node:http），返回 `{ server, refresh }` |

### 选项

| 选项 | 说明 | 默认值 |
| --- | --- | --- |
| `-p, --port` | 监听端口 | `4789` |
| `--host` | 监听地址 | `127.0.0.1` |
| `-d, --dir` | session 目录 | `~/.pi/agent/sessions` |
| `--open` | 启动后自动打开浏览器（支持 WSL 的 wslview） | 关 |
| `-h, --help` | 帮助 | - |

## 统计口径

- 仅统计 session jsonl 中 `role === "assistant"` 且带 `usage` 的消息，逐请求累加，即账单口径（`input` 为每次请求的完整输入上下文，跨请求会重复计算上下文，属正常现象）。
- 子代理用量取自主会话中的工具结果：`subagent` 工具读取 `details.results[].usage`（无 results 时回退 `details.totalChildUsage`），`trellis_subagent` 工具读取 `details.runs[].usage`；两者均为子代理汇报的聚合值，按一次运行记为一条用量记录。
- subagent 运行记录的 `sessionId` 形如 `runId_agent_childIndex`（例如 `28aac0e3_researcher_0`），trellis 运行形如 `trellis_<agent>_<runId>`；cwd 沿用主会话，因此用量自然汇入对应项目文件夹。provider/model 从子代理汇报的模型字符串解析（剥掉 `:thinking` 后缀）。
- `subagent` 工具的子会话 session 文件嵌套存放在 `<会话名>/<runId>/run-N/session.jsonl`，与主会话格式相同；为避免与工具结果重复计数，扫描时会跳过这些与 `<会话名>.jsonl` 同名的嵌套目录。
- `totalTokens` 缺失时按 `input + output + cacheRead + cacheWrite` 兜底。
- 时间桶按**本地时区**对齐（近一天按整点、近一周/近一个月按自然日）。
- 费用直接取日志中的 `usage.cost.total`（美元）；子代理运行记录取工具结果中汇报的 `usage.cost` 数值。

## API

- `GET /api/overview?range=day|week|month` — 汇总、时间桶、模型排行、文件夹排行
- `POST /api/refresh` — 增量重扫 session 目录

## 要求

Node.js >= 22.19。发布前需执行 `npm run build`（tsup 打包为 CJS + ESM 双格式，含 `.d.ts` 类型声明）。
