opencode-tokenwatch 0.1.0 → 0.2.0

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.
package/README.en.md ADDED
@@ -0,0 +1,96 @@
1
+ # opencode-tokenwatch
2
+
3
+ **English** · [简体中文](./README.md)
4
+
5
+ ![Sidebar](./assets/sidebar.png)
6
+
7
+ Real-time token usage, cache analytics & performance dashboard plugin for OpenCode CLI.
8
+
9
+ ## Features
10
+
11
+ - **Sidebar panel** — Session-level and per-model real-time stats (requests, tokens, cache, latency, cost)
12
+ - **Cache hit rate** — Inline progress bars with color thresholds and trend indicators (↑/↓)
13
+ - **Performance metrics** — Time to first token (TTFT), tokens per second (TPS), end-to-end latency
14
+ - **Token distribution** — Breakdown by role (system, user, Agent, Tool, etc.)
15
+ - **Model pricing** — Input/cache/output unit prices
16
+ - **`/usage` command** — HTML Report → JSON Export → Text Report → Settings
17
+ - **HTML report** — Interactive ECharts dashboard, auto-opened in browser
18
+ - **Multi-level collapse** — Panel, models, and sub-blocks collapsible with persisted state
19
+ - **Language switching** — Auto-detect or manually switch between Chinese and English
20
+ - **Per-request tracking** — TTFT/TPS/latency logged to JSONL for report analysis
21
+ - **Adaptive coloring** — Colors auto-derived from theme
22
+
23
+ ## Install
24
+
25
+ ```sh
26
+ npm install opencode-tokenwatch
27
+ ```
28
+
29
+ Add to `opencode.json` or `opencode.jsonc`:
30
+
31
+ ```json
32
+ {
33
+ "$schema": "https://opencode.ai/config.json",
34
+ "plugin": ["opencode-tokenwatch"]
35
+ }
36
+ ```
37
+
38
+ ## Configuration
39
+
40
+ ```jsonc
41
+ {
42
+ "plugin": ["opencode-tokenwatch"],
43
+ "pluginConfig": {
44
+ "opencode-tokenwatch": {
45
+ "sidebar": {
46
+ "showPerformance": true,
47
+ "showPricing": false,
48
+ "showTokenDistribution": false,
49
+ "showTrend": true
50
+ },
51
+ "language": "auto"
52
+ }
53
+ }
54
+ }
55
+ ```
56
+
57
+ | Option | Type | Default | Description |
58
+ |--------|------|---------|-------------|
59
+ | `sidebar.showPerformance` | boolean | `true` | Show TTFT/TPS/latency |
60
+ | `sidebar.showPricing` | boolean | `true` | Show model pricing |
61
+ | `sidebar.showTokenDistribution` | boolean | `true` | Show token distribution |
62
+ | `sidebar.showTrend` | boolean | `true` | Show trend indicator |
63
+ | `language` | `"auto"` / `"zh"` / `"en"` | `"auto"` | UI language |
64
+
65
+ Settings can also be toggled via `/usage` → Settings, taking precedence over `pluginConfig`.
66
+
67
+ ## Usage
68
+
69
+ In OpenCode TUI, run `/usage`:
70
+
71
+ - **HTML Report** — Pick a date range, generates a dashboard and opens it in browser
72
+ - **JSON Export** — Exports full usage data to `~/.opencode/reports/`
73
+ - **Text Report** — Exports Markdown report to `~/.opencode/reports/`
74
+ - **Settings** — Toggle sidebar blocks, switch language
75
+
76
+ ## Requirements
77
+
78
+ - OpenCode CLI (with `opencode db` command)
79
+ - Node.js 18+
80
+
81
+ ## Build
82
+
83
+ ```sh
84
+ npm install
85
+ npm run build
86
+ ```
87
+
88
+ ## Related
89
+
90
+ - [opencode-throughput](https://github.com/Howardzhangdqs/opencode-throughput) — Real-time LLM performance monitoring (TTFT/TPS/latency/cost)
91
+ - [opencode-visual-cache](https://github.com/Hotakus/opencode-visual-cache) — TUI sidebar cache hit rate visualization, token distribution analysis
92
+ - [magic-context](https://github.com/cortexkit/magic-context/) — Cache-aware infinite context + cross-session memory system
93
+
94
+ ## License
95
+
96
+ MIT
package/README.md CHANGED
@@ -1,31 +1,32 @@
1
1
  # opencode-tokenwatch
2
2
 
3
- Token usage analytics plugin for OpenCode.
3
+ [English](./README.en.md) · **简体中文**
4
4
 
5
- It adds a live sidebar panel for the current session and a `/usage` slash command
6
- that reads your local OpenCode history from SQLite, aggregates token usage by
7
- model, provider, date, and session, and supports export to JSON or CSV.
5
+ ![Sidebar](./assets/sidebar.png)
8
6
 
9
- ## Features
7
+ OpenCode CLI 的实时 Token 用量统计、缓存分析与性能指标插件。
10
8
 
11
- - Live sidebar updates while the assistant replies
12
- - Current-session aggregation across every model used in the conversation
13
- - `/usage` report for local history with model, provider, date, and session views
14
- - Export full reports to `JSON`
15
- - Export grouped tables to `CSV`
16
- - Uses `opencode db ... --format json` directly, so it works from local records
9
+ ## 功能
17
10
 
18
- ## Install
11
+ - **侧边栏面板** — 会话级与按模型的实时统计(请求数、Token、缓存、耗时、成本)
12
+ - **缓存命中率** — 模型行内彩色进度条,带趋势指示器(↑/↓)
13
+ - **性能指标** — 首 Token 延迟(TTFT)、每秒吞吐(TPS)、端到端延迟
14
+ - **Token 分布** — 按角色分解(系统、用户、Agent、Tool 等)
15
+ - **模型定价** — 输入/缓存/输出单价
16
+ - **`/usage` 命令** — HTML 报告 → JSON 导出 → 文本报告 → 设置
17
+ - **HTML 报告** — 交互式 ECharts 仪表盘,自动在浏览器打开
18
+ - **多级折叠** — 面板、模型、子区块均可折叠,状态持久化
19
+ - **语言切换** — 中英双语,可跟随系统或手动切换
20
+ - **性能追踪** — 每次请求的 TTFT/TPS/延迟写入 JSONL,用于报告分析
21
+ - **自适应配色** — 从主题色自动衍生
19
22
 
20
- Add the package to OpenCode's plugin list.
21
-
22
- Install the package first:
23
+ ## 安装
23
24
 
24
25
  ```sh
25
26
  npm install opencode-tokenwatch
26
27
  ```
27
28
 
28
- `opencode.json` or `opencode.jsonc`
29
+ `opencode.json` `opencode.jsonc` 中添加:
29
30
 
30
31
  ```json
31
32
  {
@@ -34,70 +35,62 @@ npm install opencode-tokenwatch
34
35
  }
35
36
  ```
36
37
 
37
- `tui.json` or `tui.jsonc`
38
+ ## 配置
38
39
 
39
- ```json
40
+ ```jsonc
40
41
  {
41
- "$schema": "https://opencode.ai/tui.json",
42
- "plugin": ["opencode-tokenwatch"]
42
+ "plugin": ["opencode-tokenwatch"],
43
+ "pluginConfig": {
44
+ "opencode-tokenwatch": {
45
+ "sidebar": {
46
+ "showPerformance": true,
47
+ "showPricing": false,
48
+ "showTokenDistribution": false,
49
+ "showTrend": true
50
+ },
51
+ "language": "auto"
52
+ }
53
+ }
43
54
  }
44
55
  ```
45
56
 
46
- If you are preparing for npm publishing, this package is already configured to
47
- publish public artifacts from `dist/`.
48
-
49
- ## Usage
50
-
51
- In OpenCode TUI:
57
+ | 配置项 | 类型 | 默认值 | 说明 |
58
+ |--------|------|--------|------|
59
+ | `sidebar.showPerformance` | boolean | `true` | 显示 TTFT/TPS/延迟 |
60
+ | `sidebar.showPricing` | boolean | `true` | 显示模型定价 |
61
+ | `sidebar.showTokenDistribution` | boolean | `true` | 显示 Token 分布 |
62
+ | `sidebar.showTrend` | boolean | `true` | 显示趋势指示器 |
63
+ | `language` | `"auto"` / `"zh"` / `"en"` | `"auto"` | 界面语言 |
52
64
 
53
- 1. Open the right sidebar to see live token totals for the current session.
54
- 2. Run `/usage`.
55
- 3. Choose:
56
- - `View report`
57
- - `Export JSON`
58
- - `Export CSV`
59
- 4. Pick a time range and optional provider/model filters.
65
+ 运行时也可通过 `/usage` 设置 调整,优先级高于 `pluginConfig`。
60
66
 
61
- Exports are written to the current working directory:
67
+ ## 用法
62
68
 
63
- - `tokenwatch-usage-report.json`
64
- - `tokenwatch-models.csv`
65
- - `tokenwatch-providers.csv`
66
- - `tokenwatch-daily.csv`
67
- - `tokenwatch-sessions.csv`
69
+ OpenCode TUI 中输入 `/usage`,选择:
68
70
 
69
- ## Report Dimensions
71
+ - **HTML 报告** — 选择日期范围,生成仪表盘并在浏览器打开
72
+ - **JSON 导出** — 导出完整用量数据至 `~/.opencode/reports/`
73
+ - **文本报告** — 导出 Markdown 格式至 `~/.opencode/reports/`
74
+ - **设置** — 开关侧边栏显示项、切换语言
70
75
 
71
- - Model
72
- - Provider
73
- - Day
74
- - Session
75
- - Current session summary
76
+ ## 系统要求
76
77
 
77
- ## Requirements
78
-
79
- - OpenCode CLI with `opencode db`
78
+ - OpenCode CLI(支持 `opencode db` 命令)
80
79
  - Node.js 18+
81
80
 
82
- ## Build
81
+ ## 构建
83
82
 
84
83
  ```sh
85
84
  npm install
86
85
  npm run build
87
86
  ```
88
87
 
89
- ## Publish Prep
90
-
91
- Before publishing to npm, run:
92
-
93
- ```sh
94
- npm run release:check
95
- ```
88
+ ## 相关项目
96
89
 
97
- This builds the package and runs `npm pack --dry-run` with an isolated temp npm
98
- cache, which is especially helpful on Windows when the default cache directory is
99
- locked by another process.
90
+ - [opencode-throughput](https://github.com/Howardzhangdqs/opencode-throughput) 实时 LLM 性能监控,采集 TTFT/TPS/延迟和成本
91
+ - [opencode-visual-cache](https://github.com/Hotakus/opencode-visual-cache) TUI 侧边栏缓存命中率可视化,Token 分布分析
92
+ - [magic-context](https://github.com/cortexkit/magic-context/) 缓存感知的无限上下文 + 跨会话记忆系统
100
93
 
101
- ## License
94
+ ## 许可
102
95
 
103
96
  MIT
@@ -0,0 +1,2 @@
1
+ import type { TuiPluginApi } from "@opencode-ai/plugin/tui";
2
+ export declare function registerCommands(api: TuiPluginApi): Promise<void>;
@@ -0,0 +1,208 @@
1
+ import { getUsageReport } from "./queries.js";
2
+ import { formatUsageReport } from "./formatter.js";
3
+ import { generateUsageHtml } from "./generate-usage-html.js";
4
+ import { t } from "./i18n.js";
5
+ import { readLogs } from "./perf-tracker.js";
6
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
7
+ import { join } from "node:path";
8
+ import { homedir } from "node:os";
9
+ import { execSync } from "node:child_process";
10
+ const DEFAULT_CONFIG = {
11
+ sidebar: { showPerformance: true, showPricing: true, showTokenDistribution: true, showTrend: true },
12
+ language: "auto",
13
+ };
14
+ export async function registerCommands(api) {
15
+ api.command?.register(() => [
16
+ {
17
+ value: "tokenwatch-html-report",
18
+ title: "Generate HTML report",
19
+ description: "Generate an HTML dashboard with token usage, cache efficiency, and performance charts",
20
+ category: "Stats",
21
+ slash: { name: "usage-html", aliases: ["usage"] },
22
+ onSelect: async () => {
23
+ await showHtmlReport(api);
24
+ },
25
+ },
26
+ {
27
+ value: "tokenwatch-json-export",
28
+ title: "Export as JSON",
29
+ description: "Export usage data as JSON file",
30
+ category: "Stats",
31
+ slash: { name: "usage-json" },
32
+ onSelect: async () => {
33
+ await showJsonExport(api);
34
+ },
35
+ },
36
+ {
37
+ value: "tokenwatch-text-report",
38
+ title: "Text report (legacy)",
39
+ description: "View plain text usage report in terminal",
40
+ category: "Stats",
41
+ slash: { name: "usage-text" },
42
+ onSelect: async () => {
43
+ await showTextReport(api);
44
+ },
45
+ },
46
+ {
47
+ value: "tokenwatch-settings",
48
+ title: "TokenWatch Settings",
49
+ description: "Configure sidebar display options",
50
+ category: "Stats",
51
+ slash: { name: "usage-settings", aliases: ["tokenwatch-settings"] },
52
+ onSelect: async () => {
53
+ await showSettingsDialog(api);
54
+ },
55
+ },
56
+ ]);
57
+ }
58
+ function ensureReportDir() {
59
+ const dir = join(homedir(), ".opencode", "reports");
60
+ if (!existsSync(dir))
61
+ mkdirSync(dir, { recursive: true });
62
+ return dir;
63
+ }
64
+ function openInBrowser(filePath) {
65
+ try {
66
+ const platform = process.platform;
67
+ if (platform === "win32")
68
+ execSync(`start "" "${filePath}"`, { windowsHide: true, timeout: 5000 });
69
+ else if (platform === "darwin")
70
+ execSync(`open "${filePath}"`, { timeout: 5000 });
71
+ else
72
+ execSync(`xdg-open "${filePath}"`, { timeout: 5000 });
73
+ }
74
+ catch { /* silently fail */ }
75
+ }
76
+ function aggregatePerfStats(logs) {
77
+ const map = new Map();
78
+ for (const entry of logs) {
79
+ const key = entry.model;
80
+ let s = map.get(key);
81
+ if (!s) {
82
+ s = {
83
+ model: key,
84
+ providerID: entry.providerID,
85
+ requestCount: 0,
86
+ totalInput: 0, totalOutput: 0, totalCacheRead: 0, totalCacheWrite: 0, totalCost: 0,
87
+ avgTTFT: null, maxTTFT: null, minTTFT: null,
88
+ avgTPS: null, maxTPS: null, minTPS: null,
89
+ avgLatency: null, maxLatency: null, minLatency: null,
90
+ };
91
+ map.set(key, s);
92
+ }
93
+ s.requestCount++;
94
+ s.totalInput += entry.inputTokens;
95
+ s.totalOutput += entry.outputTokens;
96
+ s.totalCacheRead += entry.cacheReadTokens;
97
+ s.totalCacheWrite += entry.cacheWriteTokens;
98
+ s.totalCost += entry.cost;
99
+ const c = s.requestCount;
100
+ if (entry.ttft_ms != null) {
101
+ s.avgTTFT = s.avgTTFT != null ? s.avgTTFT + (entry.ttft_ms - s.avgTTFT) / c : entry.ttft_ms;
102
+ s.maxTTFT = s.maxTTFT != null ? Math.max(s.maxTTFT, entry.ttft_ms) : entry.ttft_ms;
103
+ s.minTTFT = s.minTTFT != null ? Math.min(s.minTTFT, entry.ttft_ms) : entry.ttft_ms;
104
+ }
105
+ if (entry.tps != null) {
106
+ s.avgTPS = s.avgTPS != null ? s.avgTPS + (entry.tps - s.avgTPS) / c : entry.tps;
107
+ s.maxTPS = s.maxTPS != null ? Math.max(s.maxTPS, entry.tps) : entry.tps;
108
+ s.minTPS = s.minTPS != null ? Math.min(s.minTPS, entry.tps) : entry.tps;
109
+ }
110
+ if (entry.latency_ms != null) {
111
+ s.avgLatency = s.avgLatency != null ? s.avgLatency + (entry.latency_ms - s.avgLatency) / c : entry.latency_ms;
112
+ s.maxLatency = s.maxLatency != null ? Math.max(s.maxLatency, entry.latency_ms) : entry.latency_ms;
113
+ s.minLatency = s.minLatency != null ? Math.min(s.minLatency, entry.latency_ms) : entry.latency_ms;
114
+ }
115
+ }
116
+ return Array.from(map.values());
117
+ }
118
+ async function buildCombinedData(api) {
119
+ const report = await getUsageReport({});
120
+ const logs = readLogs(1000);
121
+ const now = new Date();
122
+ const pad = (n) => String(n).padStart(2, '0');
123
+ const meta = {
124
+ generatedAt: `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`,
125
+ dateRange: {
126
+ start: report.daily.length > 0 ? report.daily[report.daily.length - 1].day : "—",
127
+ end: report.daily.length > 0 ? report.daily[0].day : "—",
128
+ },
129
+ };
130
+ return {
131
+ ...report,
132
+ perfLogs: logs,
133
+ perfSummary: aggregatePerfStats(logs),
134
+ meta,
135
+ };
136
+ }
137
+ async function showHtmlReport(api) {
138
+ try {
139
+ const data = await buildCombinedData(api);
140
+ const html = generateUsageHtml(data);
141
+ const dir = ensureReportDir();
142
+ const dateStr = new Date().toISOString().slice(0, 10);
143
+ const filePath = join(dir, `tokenwatch-${dateStr}.html`);
144
+ writeFileSync(filePath, html, "utf-8");
145
+ api.ui.toast?.({ message: `Report: ${filePath}`, variant: "info" });
146
+ openInBrowser(filePath);
147
+ }
148
+ catch (err) {
149
+ const msg = err instanceof Error ? err.message : String(err);
150
+ api.ui.toast?.({ message: `Error: ${msg}`, variant: "error" });
151
+ }
152
+ }
153
+ async function showJsonExport(api) {
154
+ try {
155
+ const report = await getUsageReport({});
156
+ const dir = ensureReportDir();
157
+ const dateStr = new Date().toISOString().slice(0, 10);
158
+ const filePath = join(dir, `tokenwatch-${dateStr}.json`);
159
+ writeFileSync(filePath, JSON.stringify(report, null, 2), "utf-8");
160
+ api.ui.toast?.({ message: `JSON: ${filePath}`, variant: "info" });
161
+ }
162
+ catch (err) {
163
+ const msg = err instanceof Error ? err.message : String(err);
164
+ api.ui.toast?.({ message: `Error: ${msg}`, variant: "error" });
165
+ }
166
+ }
167
+ async function showTextReport(api) {
168
+ try {
169
+ const report = await getUsageReport({});
170
+ const formatted = formatUsageReport(report);
171
+ const dir = ensureReportDir();
172
+ const dateStr = new Date().toISOString().slice(0, 10);
173
+ const filePath = join(dir, `tokenwatch-${dateStr}.md`);
174
+ writeFileSync(filePath, formatted, "utf-8");
175
+ api.ui.toast?.({ message: `Report saved to ${filePath}`, variant: "info" });
176
+ }
177
+ catch (err) {
178
+ const msg = err instanceof Error ? err.message : String(err);
179
+ api.ui.toast?.({ message: `Error: ${msg}`, variant: "error" });
180
+ }
181
+ }
182
+ async function showSettingsDialog(api) {
183
+ const currentConfig = loadConfigFromStore(api);
184
+ const cfg = currentConfig.sidebar;
185
+ const options = [
186
+ `[${cfg.showPerformance ? "x" : " "}] ${t("showPerformance")}`,
187
+ `[${cfg.showPricing ? "x" : " "}] ${t("showPricing")}`,
188
+ `[${cfg.showTokenDistribution ? "x" : " "}] ${t("showTokenDistribution")}`,
189
+ `[${cfg.showTrend ? "x" : " "}] ${t("showTrend")}`,
190
+ `---`,
191
+ `${t("language")}: ${currentConfig.language}`,
192
+ ].join("\n");
193
+ api.ui.toast?.({ message: `TokenWatch settings:\n${options}`, variant: "info" });
194
+ }
195
+ function loadConfigFromStore(api) {
196
+ const base = { sidebar: { ...DEFAULT_CONFIG.sidebar }, language: DEFAULT_CONFIG.language };
197
+ try {
198
+ const stored = api.kv?.get?.("tokenwatch-config");
199
+ if (stored) {
200
+ if (stored.sidebar)
201
+ Object.assign(base.sidebar, stored.sidebar);
202
+ if (stored.language)
203
+ base.language = stored.language;
204
+ }
205
+ }
206
+ catch { /* defaults */ }
207
+ return base;
208
+ }