opencode-tokenwatch 0.4.0 → 0.6.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 +32 -38
- package/README.md +33 -39
- package/dist/host/command-actions.d.ts +20 -0
- package/dist/host/runtime.d.ts +7 -0
- package/dist/host/types.d.ts +148 -0
- package/dist/host/v1/adapter.d.ts +9 -0
- package/dist/host/v1/commands.d.ts +9 -0
- package/dist/{queries.d.ts → host/v1/data-source.d.ts} +1 -6
- package/dist/host/v2/adapter.d.ts +63 -0
- package/dist/host/v2/commands.d.ts +8 -0
- package/dist/host/v2/data-source.d.ts +26 -0
- package/dist/kernel/config.d.ts +33 -0
- package/dist/{formatter.d.ts → kernel/format.d.ts} +9 -10
- package/dist/kernel/model.d.ts +19 -0
- package/dist/kernel/perf-aggregate.d.ts +62 -0
- package/dist/{perf-tracker.d.ts → kernel/perf.d.ts} +13 -7
- package/dist/{generate-usage-html.d.ts → kernel/report-html.d.ts} +1 -1
- package/dist/kernel/report.d.ts +19 -0
- package/dist/{stats-store.d.ts → kernel/store.d.ts} +5 -5
- package/dist/server.d.ts +15 -0
- package/dist/server.js +15 -0
- package/dist/tui.d.ts +11 -12
- package/dist/tui.js +4802 -0
- package/dist/ui/sidebar.d.ts +14 -0
- package/index.ts +11 -0
- package/package.json +39 -12
- package/tui.ts +9 -0
- package/dist/commands.d.ts +0 -2
- package/dist/commands.js +0 -208
- package/dist/commands.jsx +0 -381
- package/dist/formatter.js +0 -289
- package/dist/generate-usage-html.js +0 -962
- package/dist/i18n.js +0 -173
- package/dist/index.d.ts +0 -5
- package/dist/index.js +0 -7
- package/dist/perf-tracker.js +0 -299
- package/dist/queries.js +0 -393
- package/dist/sidebar.d.ts +0 -22
- package/dist/sidebar.jsx +0 -604
- package/dist/stats-store.js +0 -258
- package/dist/tui.jsx +0 -178
- /package/dist/{i18n.d.ts → kernel/i18n.d.ts} +0 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { HostAdapter } from "../host/types.js";
|
|
2
|
+
import type { PerfTracker } from "../kernel/perf.js";
|
|
3
|
+
import type { TokenMessage } from "../kernel/model.js";
|
|
4
|
+
interface TokenWatchPanelProps {
|
|
5
|
+
host: HostAdapter;
|
|
6
|
+
perfTracker: PerfTracker;
|
|
7
|
+
perfRevision?: () => number;
|
|
8
|
+
messages: () => readonly any[];
|
|
9
|
+
/** 绑定了当前会话的 part 查询(v2 下避免跨会话扫描) */
|
|
10
|
+
messageParts: (messageID: string) => readonly any[];
|
|
11
|
+
allTokenMessages: () => TokenMessage[];
|
|
12
|
+
}
|
|
13
|
+
export declare function TokenWatchPanel(props: TokenWatchPanelProps): import("solid-js").JSX.Element;
|
|
14
|
+
export {};
|
package/index.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 目录标记 + 服务端插件入口。
|
|
3
|
+
*
|
|
4
|
+
* opencode2 有两套本地插件发现:
|
|
5
|
+
* - 服务端(role=server)以 `index.*` 为入口,要求 default 导出为合法插件模块
|
|
6
|
+
* - TUI 侧以 `tui.*` 为入口(见 ./tui.ts)
|
|
7
|
+
*
|
|
8
|
+
* 本文件同时满足两者:作为 v2 目录发现的标记文件,并导出 v1/v2 通用的
|
|
9
|
+
* server 插件对象(v1 不使用本文件,它通过 package.json 的 exports 加载)。
|
|
10
|
+
*/
|
|
11
|
+
export { default } from "./dist/server.js"
|
package/package.json
CHANGED
|
@@ -1,30 +1,39 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-tokenwatch",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Real-time token usage, cache analytics & performance dashboard plugin for OpenCode CLI",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"main": "./dist/
|
|
7
|
-
"types": "./dist/
|
|
6
|
+
"main": "./dist/server.js",
|
|
7
|
+
"types": "./dist/server.d.ts",
|
|
8
8
|
"exports": {
|
|
9
9
|
".": {
|
|
10
|
-
"types": "./dist/
|
|
11
|
-
"import": "./dist/
|
|
10
|
+
"types": "./dist/server.d.ts",
|
|
11
|
+
"import": "./dist/server.js"
|
|
12
12
|
},
|
|
13
13
|
"./tui": {
|
|
14
14
|
"types": "./dist/tui.d.ts",
|
|
15
|
-
"import": "./dist/tui.
|
|
15
|
+
"import": "./dist/tui.js",
|
|
16
|
+
"config": {
|
|
17
|
+
"enabled": true
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"./server": {
|
|
21
|
+
"types": "./dist/server.d.ts",
|
|
22
|
+
"import": "./dist/server.js"
|
|
16
23
|
},
|
|
17
24
|
"./package.json": "./package.json"
|
|
18
25
|
},
|
|
19
26
|
"files": [
|
|
20
|
-
"dist"
|
|
27
|
+
"dist",
|
|
28
|
+
"tui.ts",
|
|
29
|
+
"index.ts"
|
|
21
30
|
],
|
|
22
31
|
"sideEffects": false,
|
|
23
32
|
"engines": {
|
|
24
33
|
"node": ">=18"
|
|
25
34
|
},
|
|
26
35
|
"scripts": {
|
|
27
|
-
"build": "tsc",
|
|
36
|
+
"build": "tsc && node build.tui.mjs",
|
|
28
37
|
"release:check": "node ./scripts/publish-check.mjs",
|
|
29
38
|
"prepublishOnly": "npm run build"
|
|
30
39
|
},
|
|
@@ -50,14 +59,32 @@
|
|
|
50
59
|
},
|
|
51
60
|
"homepage": "https://github.com/TTWK/opencode-tokenwatch#readme",
|
|
52
61
|
"devDependencies": {
|
|
53
|
-
"@opencode-ai/plugin": "
|
|
54
|
-
"@opentui/core": "^0.
|
|
55
|
-
"@opentui/keymap": "^0.
|
|
56
|
-
"@opentui/solid": "^0.
|
|
62
|
+
"@opencode-ai/plugin": "1.18.30",
|
|
63
|
+
"@opentui/core": "^0.5.11",
|
|
64
|
+
"@opentui/keymap": "^0.5.11",
|
|
65
|
+
"@opentui/solid": "^0.5.11",
|
|
57
66
|
"@types/node": "^22.0.0",
|
|
67
|
+
"esbuild": "^0.25.0",
|
|
68
|
+
"esbuild-plugin-solid": "^0.5.0",
|
|
58
69
|
"typescript": "^5.7.0"
|
|
59
70
|
},
|
|
60
71
|
"publishConfig": {
|
|
61
72
|
"access": "public"
|
|
73
|
+
},
|
|
74
|
+
"peerDependencies": {
|
|
75
|
+
"solid-js": ">=1.9.0",
|
|
76
|
+
"@opentui/core": ">=0.5.0",
|
|
77
|
+
"@opentui/solid": ">=0.5.0"
|
|
78
|
+
},
|
|
79
|
+
"peerDependenciesMeta": {
|
|
80
|
+
"solid-js": {
|
|
81
|
+
"optional": true
|
|
82
|
+
},
|
|
83
|
+
"@opentui/core": {
|
|
84
|
+
"optional": true
|
|
85
|
+
},
|
|
86
|
+
"@opentui/solid": {
|
|
87
|
+
"optional": true
|
|
88
|
+
}
|
|
62
89
|
}
|
|
63
90
|
}
|
package/tui.ts
ADDED
package/dist/commands.d.ts
DELETED
package/dist/commands.js
DELETED
|
@@ -1,208 +0,0 @@
|
|
|
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
|
-
}
|