opencode-usage-report 0.6.2 → 0.6.4
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.md +98 -282
- package/package.json +10 -4
- package/src/context/dialog.tsx +10 -1
- package/src/providers/kimi.ts +16 -7
- package/src/tui.tsx +79 -33
package/README.md
CHANGED
|
@@ -4,329 +4,145 @@
|
|
|
4
4
|
[](https://github.com/kaanchinar/opencode-usage-report/actions/workflows/ci.yml)
|
|
5
5
|
[](./LICENSE)
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
tool
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
7
|
+
See how much of your AI subscriptions you have left, right inside [opencode](https://opencode.ai).
|
|
8
|
+
|
|
9
|
+
Adds a `/usage` dialog, a sidebar panel and a `usage_report` tool that show your plan's quota windows and how full the current session's context is.
|
|
10
|
+
|
|
11
|
+
```text
|
|
12
|
+
Context
|
|
13
|
+
Kimi K2 (High) · 42,318 / 200,000 tokens (21.2%)
|
|
14
|
+
Auto-compacts at 180,000 · 137,682 headroom $0.42 spent
|
|
15
|
+
|
|
16
|
+
Kimi Code (kimi.ai)
|
|
17
|
+
5h limit █████████░░░░░ 62% resets in 2h 14m
|
|
18
|
+
Weekly limit ████░░░░░░░░░░ 31% resets in 4d 6h
|
|
19
|
+
|
|
20
|
+
GitHub Copilot
|
|
21
|
+
Monthly limit ██░░░░░░░░░░░░ 12% resets in 18d 3h
|
|
22
|
+
```
|
|
16
23
|
|
|
17
24
|
## Features
|
|
18
25
|
|
|
19
|
-
-
|
|
20
|
-
|
|
21
|
-
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
| Config | Entrypoint | Provides |
|
|
39
|
-
| --------------- | ------------- | ------------------------------------------------------- |
|
|
40
|
-
| `opencode.json` | server plugin | `usage_report` tool, low-quota warnings, prompt capture |
|
|
41
|
-
| `tui.json` | TUI plugin | `/usage` command, sidebar panel |
|
|
42
|
-
|
|
43
|
-
The quickest path is opencode's own installer, which detects both entrypoints
|
|
44
|
-
and writes both configs for you:
|
|
26
|
+
- Quota windows with progress bars and reset countdowns
|
|
27
|
+
- Context breakdown by category, with auto-compaction headroom and session cost
|
|
28
|
+
- `/usage` runs locally and costs no model tokens
|
|
29
|
+
- Low-quota toasts in the TUI
|
|
30
|
+
- Disk cache, with a local estimate when a provider API is unreachable
|
|
31
|
+
|
|
32
|
+
## Supported providers
|
|
33
|
+
|
|
34
|
+
| Provider | ID | Usage API |
|
|
35
|
+
| -------------------- | ----------------------- | ---------------- |
|
|
36
|
+
| Kimi Code (kimi.ai) | `kimi-code-plan-global` | `api.kimi.ai` |
|
|
37
|
+
| Kimi Code (kimi.com) | `kimi-code-plan-cn` | `api.kimi.com` |
|
|
38
|
+
| OpenCode Go | `opencode-go` | `opencode.ai` |
|
|
39
|
+
| GitHub Copilot | `github-copilot` | `api.github.com` |
|
|
40
|
+
| ChatGPT | `openai` | `chatgpt.com` |
|
|
41
|
+
|
|
42
|
+
## Installation
|
|
43
|
+
|
|
44
|
+
Requires opencode 1.18.29 or later, including 2.x.
|
|
45
45
|
|
|
46
46
|
```sh
|
|
47
|
-
opencode plugin opencode-usage-report
|
|
48
|
-
opencode plugin -g opencode-usage-report # global scope
|
|
47
|
+
opencode plugin -g opencode-usage-report
|
|
49
48
|
```
|
|
50
49
|
|
|
51
|
-
|
|
50
|
+
Omit `-g` to install for the current project only. Restart opencode afterwards.
|
|
51
|
+
|
|
52
|
+
<details>
|
|
53
|
+
<summary>Manual installation</summary>
|
|
54
|
+
|
|
55
|
+
The plugin has two entrypoints, so add it to both config files:
|
|
52
56
|
|
|
53
57
|
```jsonc
|
|
54
|
-
// opencode.json
|
|
55
|
-
{
|
|
56
|
-
"plugin": ["opencode-usage-report"],
|
|
57
|
-
}
|
|
58
|
+
// opencode.json: usage_report tool, low-quota warnings
|
|
59
|
+
{ "plugin": ["opencode-usage-report"] }
|
|
58
60
|
```
|
|
59
61
|
|
|
60
62
|
```jsonc
|
|
61
|
-
// tui.json
|
|
62
|
-
{
|
|
63
|
-
"plugin": ["opencode-usage-report"],
|
|
64
|
-
}
|
|
63
|
+
// tui.json: /usage dialog, sidebar panel
|
|
64
|
+
{ "plugin": ["opencode-usage-report"] }
|
|
65
65
|
```
|
|
66
66
|
|
|
67
|
-
|
|
68
|
-
> `exports` map itself and resolves `exports["./tui"]` for the TUI and
|
|
69
|
-
> `exports["./server"]` (or `main`) for the server. A subpath spec such as
|
|
70
|
-
> `opencode-usage-report/tui` is **not** valid and will silently load nothing.
|
|
71
|
-
> `opencode plugin opencode-usage-report` prints `Detected server + tui targets`
|
|
72
|
-
> when the resolution works.
|
|
67
|
+
Use the bare package name in both files. A subpath such as `opencode-usage-report/tui` silently loads nothing.
|
|
73
68
|
|
|
74
|
-
>
|
|
75
|
-
> definition. The server entrypoint exports `{ id, setup, server }` — V2 calls
|
|
76
|
-
> `setup`, V1 calls `server`. The TUI entrypoint exports `{ id, setup, tui }` —
|
|
77
|
-
> V2 calls `setup` (its `keymap.layer` runs from a headless `app`-slot
|
|
78
|
-
> component), V1 calls `tui`. OpenCode 2's TUI loader rejects a default export
|
|
79
|
-
> without a `setup` function (`Invalid V2 TUI plugin module`); before 0.6.0 the
|
|
80
|
-
> TUI module exported only the V1 `tui` shape and failed to load on 2.x.
|
|
81
|
-
> OpenCode 1.18.29+ and 2.x both load either entrypoint.
|
|
82
|
-
|
|
83
|
-
Options can be passed in the tuple form, independently per entrypoint:
|
|
84
|
-
|
|
85
|
-
```jsonc
|
|
86
|
-
{
|
|
87
|
-
"plugin": [["opencode-usage-report", { "thresholdPercent": 75 }]],
|
|
88
|
-
}
|
|
89
|
-
```
|
|
69
|
+
</details>
|
|
90
70
|
|
|
91
|
-
|
|
71
|
+
## Usage
|
|
92
72
|
|
|
93
|
-
|
|
94
|
-
once at startup and is not hot-reloaded, so restart opencode after every edit:
|
|
73
|
+
### `/usage`
|
|
95
74
|
|
|
96
|
-
|
|
97
|
-
// opencode.json
|
|
98
|
-
{ "plugin": ["file:///abs/path/to/opencode-usage-report/src/index.ts"] }
|
|
99
|
-
// tui.json
|
|
100
|
-
{ "plugin": ["file:///abs/path/to/opencode-usage-report/src/tui.tsx"] }
|
|
101
|
-
```
|
|
75
|
+
Opens the report dialog. Providers without a credential are skipped.
|
|
102
76
|
|
|
103
|
-
|
|
77
|
+
| Key | Action |
|
|
78
|
+
| ----- | ------------------------ |
|
|
79
|
+
| `r` | Refresh now |
|
|
80
|
+
| `tab` | Cycle provider scope |
|
|
81
|
+
| `j` | Toggle the raw JSON view |
|
|
82
|
+
| `esc` | Close |
|
|
104
83
|
|
|
105
|
-
|
|
106
|
-
on top, and every configured provider's quota windows below. This is a local
|
|
107
|
-
TUI command, so it costs no model tokens. Requires the TUI plugin. Providers
|
|
108
|
-
whose credential cannot be resolved are skipped in this view.
|
|
109
|
-
- `/usage` dialog keys — `esc` close, `r` refresh now, `tab` cycle provider
|
|
110
|
-
scope (all → each provider), `j` toggle the raw JSON view.
|
|
111
|
-
- `usage_report` tool — the same report for agents and headless runs. Takes
|
|
112
|
-
`provider` (exact id: `kimi-code-plan-global`, `kimi-code-plan-cn`,
|
|
113
|
-
`opencode-go`, `github-copilot` or `openai`; an unknown id returns an error
|
|
114
|
-
listing the known ids), `json` (raw `ProviderReport[]`) and `refresh` (bypass
|
|
115
|
-
the on-disk cache).
|
|
84
|
+
Press `ctrl+shift+u` to refresh the sidebar panel.
|
|
116
85
|
|
|
117
|
-
|
|
118
|
-
> routed through the model, and it accepted `--json` / `--refresh` / a provider
|
|
119
|
-
> id as slash arguments. The TUI command now owns the name, so `/usage` reaches
|
|
120
|
-
> no model and those capabilities moved to the dialog keys and the
|
|
121
|
-
> `usage_report` tool arguments. Running `/usage --json` is no longer supported.
|
|
86
|
+
### `usage_report` tool
|
|
122
87
|
|
|
123
|
-
|
|
88
|
+
The same report for agents and headless runs.
|
|
124
89
|
|
|
125
|
-
|
|
126
|
-
|
|
90
|
+
| Argument | Type | Description |
|
|
91
|
+
| ---------- | --------- | ---------------------------------- |
|
|
92
|
+
| `provider` | `string` | Report one provider, by exact ID |
|
|
93
|
+
| `json` | `boolean` | Return raw `ProviderReport[]` JSON |
|
|
94
|
+
| `refresh` | `boolean` | Bypass the disk cache |
|
|
127
95
|
|
|
128
|
-
|
|
129
|
-
Context
|
|
130
|
-
Kimi K2 (High) · 42,318 / 200,000 tokens (21.2%)
|
|
131
|
-
|
|
132
|
-
████████████████████████████ ████████████████████████████████ ████████████ ░░░░░░░░░░░░░░
|
|
133
|
-
████████████████████████████ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ ░░░░░░░░░░░░░░ ░░░░░░░░░░░░░░
|
|
134
|
-
░░░░░░░░░░░░░░░░░░░░░░░░░░░░ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ ░░░░░░░░░░░░░░ ░░░░░░░░░░░░░░
|
|
135
|
-
░░░░░░░░░░░░░░░░░░░░░░░░░░░░ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ ░░░░░░░░░░░░░░ ░░░░░░░░░░░░░░
|
|
136
|
-
░░░░░░░░░░░░░░░░░░░░░░░░░░░░ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ ░░░░░░░░░░░░░░ ░░░░░░░░░░░░░░
|
|
137
|
-
░░░░░░░░░░░░░░░░░░░░░░░░░░░░ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ ░░░░░░░░░░░░░░ ░░░░░░░░░░░░░░
|
|
138
|
-
|
|
139
|
-
● User messages 8,204 4.1% ● System & tools 8,144 4.1%
|
|
140
|
-
● Agent responses 11,650 5.8% ● Free space 157,682 78.8%
|
|
141
|
-
● Reasoning 2,140 1.1%
|
|
142
|
-
● Tool calls 12,180 6.1%
|
|
143
|
-
|
|
144
|
-
Auto-compacts at 180,000 · 137,682 headroom $0.42 spent
|
|
145
|
-
```
|
|
96
|
+
### Context accuracy
|
|
146
97
|
|
|
147
|
-
**
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
`input + cache.read + cache.write` — which is what actually occupies the context
|
|
151
|
-
window. opencode's own sidebar Context block sums five fields including the
|
|
152
|
-
completion, so it reads slightly higher than this panel by design.
|
|
153
|
-
|
|
154
|
-
**What is estimated.** The per-category rows. opencode records no token count
|
|
155
|
-
per message or part, so each category is estimated with the same `characters / 4`
|
|
156
|
-
heuristic opencode itself uses, then normalized so the rows agree with the exact
|
|
157
|
-
total.
|
|
158
|
-
|
|
159
|
-
**System & tools.** opencode assembles the system prompt and tool definitions on
|
|
160
|
-
every request and never persists them. The server plugin measures the real
|
|
161
|
-
system string through the `experimental.chat.system.transform` hook and stores
|
|
162
|
-
only its size, so the row can be split into `system prompt` and
|
|
163
|
-
`tools & framing`. That hook is undocumented, so it is treated as best-effort:
|
|
164
|
-
if it stops firing, the sidecar goes stale, or anything fails to validate, the
|
|
165
|
-
panel falls back to the derived residual and labels the row `(derived)`. The
|
|
166
|
-
plugin never breaks a request over it.
|
|
167
|
-
|
|
168
|
-
**Compaction headroom.** `Auto-compacts at N` mirrors opencode's own overflow
|
|
169
|
-
check, including the configured `compaction.reserved` buffer. This is the point
|
|
170
|
-
at which opencode automatically summarizes the session — a number opencode
|
|
171
|
-
exposes nowhere else.
|
|
172
|
-
|
|
173
|
-
## Options
|
|
174
|
-
|
|
175
|
-
Shared by both entrypoints:
|
|
176
|
-
|
|
177
|
-
| Option | Type | Default | Meaning |
|
|
178
|
-
| ------------------ | ------------------ | ------- | ------------------------------------------------------------ |
|
|
179
|
-
| `thresholdPercent` | `number` | `80` | Warn when a window is at/above this percent used. |
|
|
180
|
-
| `cacheTtlSeconds` | `number` | `120` | How long a cached API result is considered fresh. |
|
|
181
|
-
| `providers` | `string[] \| null` | `null` | Providers to report; `null` = every registered adapter. |
|
|
182
|
-
| `fallback` | `boolean` | `true` | Use a local estimate when the API fails and no cache exists. |
|
|
183
|
-
|
|
184
|
-
TUI only:
|
|
185
|
-
|
|
186
|
-
| Option | Type | Default | Meaning |
|
|
187
|
-
| ------------------------ | -------- | ------- | -------------------------------------------------- |
|
|
188
|
-
| `refreshIntervalSeconds` | `number` | `60` | Sidebar/dialog refresh cadence, clamped to 5–3600. |
|
|
189
|
-
| `barWidth` | `number` | `14` | Progress-bar width in cells, clamped to 4–40. |
|
|
190
|
-
|
|
191
|
-
`fallback` is fixed at `true` for the TUI entrypoint. Malformed option values
|
|
192
|
-
are ignored and the defaults are kept.
|
|
193
|
-
|
|
194
|
-
## Warnings
|
|
195
|
-
|
|
196
|
-
On startup and again on `session.idle` (both throttled to at most once every
|
|
197
|
-
10 minutes, sharing one timer), the plugin checks the cached reports. Any window
|
|
198
|
-
at or above `thresholdPercent`, or whose status is `rate-limited` / `frozen`,
|
|
199
|
-
produces a TUI toast. A warning fires once per window until that window resets;
|
|
200
|
-
it re-arms after usage drops below `thresholdPercent - 10`. Toast failures
|
|
201
|
-
(headless/server mode) are swallowed.
|
|
202
|
-
|
|
203
|
-
## Data sources & privacy
|
|
204
|
-
|
|
205
|
-
- **Credential resolution order.** First `OPENCODE_USAGE_<ID>_KEY` (optionally
|
|
206
|
-
paired with `OPENCODE_USAGE_<ID>_ACCOUNT_ID`, e.g.
|
|
207
|
-
`OPENCODE_USAGE_OPENAI_ACCOUNT_ID`, to supply the ChatGPT account id), then
|
|
208
|
-
`~/.local/share/opencode/auth.json`: a `type: "api"` entry's `.key`, or a
|
|
209
|
-
`type: "oauth"` entry's `.access` (falling back to `.refresh` for GitHub
|
|
210
|
-
Copilot, which stores its token there). For ChatGPT the oauth entry's
|
|
211
|
-
`.accountId` is read automatically. `$OPENCODE_DATA_HOME` overrides the data
|
|
212
|
-
directory.
|
|
213
|
-
- **APIs.** `https://api.kimi.ai/coding/v1/usages` (global plan),
|
|
214
|
-
`https://api.kimi.com/coding/v1/usages` (China plan),
|
|
215
|
-
`https://opencode.ai/zen/go/v1/usage` (requires a custom `User-Agent`),
|
|
216
|
-
`https://api.github.com/copilot_internal/user` (GitHub Copilot) and
|
|
217
|
-
`https://chatgpt.com/backend-api/wham/usage` (ChatGPT).
|
|
218
|
-
- **Auth.** Run `opencode auth login` and pick **GitHub Copilot** or **OpenAI
|
|
219
|
-
ChatGPT**. ChatGPT additionally needs the account id that login writes to
|
|
220
|
-
`auth.json`; the plugin reads it automatically.
|
|
221
|
-
- **Cache and state** live in `<data-home>/usage-report/`: the TTL cache, the
|
|
222
|
-
session id, the warn state, and `context/<sessionID>.json` — the system-prompt
|
|
223
|
-
size sidecar, which holds counts only and never prompt text.
|
|
224
|
-
- **Local fallback** reads `opencode.db` read-only.
|
|
225
|
-
- **API keys are never logged, cached, or rendered.** Adapter errors are
|
|
226
|
-
sanitized (key substrings replaced with `<redacted>`) before they reach any
|
|
227
|
-
output, and the live smoke script never prints credentials.
|
|
98
|
+
- **Exact:** the total, percentage, free space, compaction point and cost. The total is the prompt size (`input + cache.read + cache.write`), so it reads slightly lower than opencode's own sidebar, which also counts the completion.
|
|
99
|
+
- **Estimated:** the per-category rows, because opencode records no per-message token counts. They use `characters / 4`, then scale to match the exact total.
|
|
100
|
+
- **`(derived)`:** the system prompt size could not be measured, so the row is the remainder of the total.
|
|
228
101
|
|
|
229
|
-
##
|
|
102
|
+
## Configuration
|
|
230
103
|
|
|
231
|
-
|
|
232
|
-
Both configs are required — the command comes from the TUI plugin, not the
|
|
233
|
-
server plugin. Check that `tui.json` lists `"opencode-usage-report"` (bare name)
|
|
234
|
-
and restart opencode; config is not hot-reloaded. To confirm the package exposes
|
|
235
|
-
what opencode expects, run `opencode plugin opencode-usage-report` — it should
|
|
236
|
-
print `Detected server + tui targets`.
|
|
104
|
+
Pass options with the tuple form. Each entrypoint reads its own options, so set them in `opencode.json`, `tui.json`, or both.
|
|
237
105
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
106
|
+
```jsonc
|
|
107
|
+
{
|
|
108
|
+
"plugin": [["opencode-usage-report", { "thresholdPercent": 75 }]],
|
|
109
|
+
}
|
|
110
|
+
```
|
|
242
111
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
112
|
+
| Option | Type | Default | Entrypoint | Description |
|
|
113
|
+
| ------------------------ | ------------------ | ------- | ---------- | ----------------------------------------------------------- |
|
|
114
|
+
| `thresholdPercent` | `number` | `80` | both | Warn when a window reaches this percentage |
|
|
115
|
+
| `cacheTtlSeconds` | `number` | `120` | both | How long a cached API result stays fresh |
|
|
116
|
+
| `providers` | `string[] \| null` | `null` | both | Providers to report; `null` means all |
|
|
117
|
+
| `fallback` | `boolean` | `true` | server | Use a local estimate when the API fails and no cache exists |
|
|
118
|
+
| `refreshIntervalSeconds` | `number` | `60` | TUI | Refresh cadence, clamped to 5–3600 |
|
|
119
|
+
| `barWidth` | `number` | `14` | TUI | Progress bar width in cells, clamped to 4–40 |
|
|
249
120
|
|
|
250
|
-
|
|
251
|
-
`(stale)` means a cached result was served past its TTL without a successful
|
|
252
|
-
refresh; `(est)` means the API was unreachable and a local estimate was used.
|
|
253
|
-
Press `r` in the dialog (or `ctrl+shift+u` in the sidebar) to force a refetch.
|
|
121
|
+
Invalid values are ignored and the default is kept.
|
|
254
122
|
|
|
255
|
-
##
|
|
123
|
+
## Credentials and privacy
|
|
256
124
|
|
|
257
|
-
|
|
125
|
+
Run `opencode auth login` to sign in. The plugin reads credentials in this order:
|
|
258
126
|
|
|
259
|
-
1.
|
|
260
|
-
|
|
261
|
-
2. Register it in `src/providers/index.ts` (`adapters` array).
|
|
262
|
-
3. Add fixture-driven tests under `test/`.
|
|
127
|
+
1. `OPENCODE_USAGE_<ID>_KEY`, where `<ID>` is the provider ID in uppercase with non-alphanumeric characters replaced by `_`, for example `OPENCODE_USAGE_KIMI_CODE_PLAN_GLOBAL_KEY`. For ChatGPT, also set `OPENCODE_USAGE_OPENAI_ACCOUNT_ID`.
|
|
128
|
+
2. `~/.local/share/opencode/auth.json`. Set `$OPENCODE_DATA_HOME` to use another data directory.
|
|
263
129
|
|
|
264
|
-
|
|
130
|
+
Keys are never logged, cached or rendered, and adapter errors are redacted before output. The plugin contacts only the usage APIs listed above. Cached reports and plugin state live in `<data-home>/usage-report/`, and the local estimate reads `opencode.db` read-only. The plugin stores the size of your system prompt, never its text.
|
|
265
131
|
|
|
266
|
-
|
|
267
|
-
(requires Google OAuth).
|
|
268
|
-
- **Claude**: `api.anthropic.com/api/oauth/usage` (requires
|
|
269
|
-
`anthropic-beta: oauth-2025-04-20`, the claude-code User-Agent, and
|
|
270
|
-
`~/.claude/.credentials.json`).
|
|
132
|
+
## Troubleshooting
|
|
271
133
|
|
|
272
|
-
|
|
134
|
+
**`/usage` is missing.** The command comes from the TUI entrypoint. Check that `tui.json` lists the bare package name, then restart opencode. Running `opencode plugin opencode-usage-report` should print `Detected server + tui targets`.
|
|
273
135
|
|
|
274
|
-
|
|
275
|
-
npm run lint # oxlint
|
|
276
|
-
npm run lint:fix # oxlint --fix
|
|
277
|
-
npm run format # oxfmt (write in place)
|
|
278
|
-
npm run format:check # oxfmt --check
|
|
279
|
-
npm test # vitest, no network
|
|
280
|
-
npm run typecheck # tsc --noEmit
|
|
281
|
-
npm run check # lint + format:check + typecheck + test
|
|
282
|
-
npm run smoke -- --yes-live # manual live check (real keys; opt-in)
|
|
283
|
-
```
|
|
136
|
+
**No providers are reported.** No credential could be resolved. Run `opencode auth login` or set `OPENCODE_USAGE_<ID>_KEY`.
|
|
284
137
|
|
|
285
|
-
|
|
138
|
+
**An old version keeps loading.** opencode caches packages in `~/.cache/opencode/packages/<name>@latest/`. Re-run `opencode plugin opencode-usage-report` or delete that directory.
|
|
286
139
|
|
|
287
|
-
|
|
288
|
-
`.github/workflows/publish.yml`, which verifies that the tag matches
|
|
289
|
-
`package.json`, runs `npm ci`, and uploads the tarball with
|
|
290
|
-
`npm stage publish --provenance --access public` using npm Trusted Publishing
|
|
291
|
-
(OIDC — no `NPM_TOKEN` secret). Nothing goes live until a maintainer approves
|
|
292
|
-
the staged release with 2FA:
|
|
140
|
+
**Numbers show `(stale)` or `(est)`.** `(stale)` is a cached result past its TTL. `(est)` is a local estimate used because the API was unreachable. Press `r` in the dialog to refetch.
|
|
293
141
|
|
|
294
|
-
|
|
295
|
-
git tag v0.4.1 && git push origin main v0.4.1
|
|
296
|
-
npm stage list # find the stage id
|
|
297
|
-
npm stage approve <stage-id> # 2FA prompt; or approve in the npm UI
|
|
298
|
-
```
|
|
142
|
+
## Contributing
|
|
299
143
|
|
|
300
|
-
|
|
301
|
-
so a failing check blocks the stage.
|
|
302
|
-
|
|
303
|
-
## Changelog
|
|
304
|
-
|
|
305
|
-
- **0.6.2** — OpenCode 2 context panel fixes: the V2 adapter now reads the
|
|
306
|
-
model id from the store's `model.id`, so the model lookup succeeds and the
|
|
307
|
-
cell grid, per-category percentages and compaction-headroom line render
|
|
308
|
-
again instead of `—`; the translated messages also carry the session id, and
|
|
309
|
-
the system-prompt capture anchors its freshness check to the turn's user
|
|
310
|
-
message — OpenCode 2 fires the capture hook just before the assistant
|
|
311
|
-
message is stamped, which previously marked every capture `(derived)`.
|
|
312
|
-
- **0.6.1** — Kimi Code adapter tracks the 2026-09 quota-model API change:
|
|
313
|
-
usage windows now come from the `usages.limit_5h` / `limit_7d` /
|
|
314
|
-
`limit_month_total` ratios (with a `limit_month_code` breakdown), and Extra
|
|
315
|
-
Usage is read from the camelCase `boosterWallet` (fixed-point `BOOSTER`
|
|
316
|
-
balance). The pre-2026-09 `usage` / `limits[]` / `totalQuota` /
|
|
317
|
-
`booster_wallet` shapes remain as fallbacks.
|
|
318
|
-
- **0.4.1** — internal: shared provider HTTP plumbing (`src/providers/http.ts`)
|
|
319
|
-
replaces the copy-pasted fetch/retry/error code; context estimates simplified;
|
|
320
|
-
reset times render as `Wed, Sep 16`.
|
|
321
|
-
- **0.4.0** — context usage panel (`/usage` now shows context breakdown, grid,
|
|
322
|
-
compaction headroom and cost); `/usage` became a local TUI command that costs
|
|
323
|
-
no model tokens; sidebar rendering extracted to `src/quota-lines.ts`.
|
|
324
|
-
- **0.3.0** — GitHub Copilot and ChatGPT (OpenAI) adapters.
|
|
325
|
-
- **0.2.0** — Kimi Code split into the kimi.ai (`kimi-code-plan-global`) and
|
|
326
|
-
kimi.com (`kimi-code-plan-cn`) plans.
|
|
144
|
+
See [CONTRIBUTING.md](./CONTRIBUTING.md) for development setup and adding a provider, and [CHANGELOG.md](./CHANGELOG.md) for release history. Report vulnerabilities as described in [SECURITY.md](./SECURITY.md).
|
|
327
145
|
|
|
328
146
|
## License
|
|
329
147
|
|
|
330
|
-
MIT © [Kaan Chinar](https://github.com/kaanchinar)
|
|
331
|
-
|
|
332
|
-
Security policy: [SECURITY.md](./SECURITY.md)
|
|
148
|
+
[MIT](./LICENSE) © [Kaan Chinar](https://github.com/kaanchinar)
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-usage-report",
|
|
3
|
-
"version": "0.6.
|
|
4
|
-
"description": "opencode plugin: /usage
|
|
3
|
+
"version": "0.6.4",
|
|
4
|
+
"description": "opencode plugin: /usage dialog, sidebar panel and usage_report tool for subscription quota and context usage",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"kimi",
|
|
7
7
|
"opencode",
|
|
@@ -48,10 +48,10 @@
|
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
50
|
"@opencode-ai/plugin": "^1.0.0",
|
|
51
|
-
"@opentui/core": "^0.
|
|
51
|
+
"@opentui/core": "^0.6.2",
|
|
52
52
|
"@opentui/solid": "^0.5.11",
|
|
53
53
|
"@types/node": "^26.6.2",
|
|
54
|
-
"oxfmt": "^0.
|
|
54
|
+
"oxfmt": "^0.72.0",
|
|
55
55
|
"oxlint": "^1.85.0",
|
|
56
56
|
"solid-js": "1.9.12",
|
|
57
57
|
"tsx": "^4.19.0",
|
|
@@ -63,5 +63,11 @@
|
|
|
63
63
|
"@opentui/core": ">=0.5.11",
|
|
64
64
|
"@opentui/solid": ">=0.5.11",
|
|
65
65
|
"solid-js": ">=1.9.0"
|
|
66
|
+
},
|
|
67
|
+
"overrides": {
|
|
68
|
+
"seroval": "1.6.9",
|
|
69
|
+
"seroval-plugins": "1.6.9",
|
|
70
|
+
"toml": "5.0.1",
|
|
71
|
+
"zod": "4.6.7"
|
|
66
72
|
}
|
|
67
73
|
}
|
package/src/context/dialog.tsx
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** @jsxImportSource @opentui/solid */
|
|
2
2
|
import type { TuiPluginApi } from "@opencode-ai/plugin/tui";
|
|
3
3
|
import type { AssistantMessage, Model } from "@opencode-ai/sdk/v2";
|
|
4
|
-
import { For, Show, createMemo } from "solid-js";
|
|
4
|
+
import { For, Show, createMemo, onCleanup } from "solid-js";
|
|
5
5
|
import { buildContextLines, type ContextTone } from "./format";
|
|
6
6
|
import { collectContext } from "./collect";
|
|
7
7
|
import { captureIsFresh, captureSince } from "./system";
|
|
@@ -24,6 +24,10 @@ export interface UsageDialogProps {
|
|
|
24
24
|
system: () => SystemCapture | null;
|
|
25
25
|
scope: () => number;
|
|
26
26
|
showJson: () => boolean;
|
|
27
|
+
/** Fires on any panel change the dialog must re-read. */
|
|
28
|
+
subscribe: (listener: () => void) => () => void;
|
|
29
|
+
/** Re-renders the whole dialog; OpenCode loads this module on a separate Solid runtime. */
|
|
30
|
+
rerender: () => void;
|
|
27
31
|
barWidth: number;
|
|
28
32
|
}
|
|
29
33
|
|
|
@@ -73,6 +77,11 @@ function jsonLines(value: unknown): RenderedLine[] {
|
|
|
73
77
|
export function UsageDialog(props: UsageDialogProps) {
|
|
74
78
|
const theme = () => props.api.theme.current;
|
|
75
79
|
|
|
80
|
+
// OpenCode loads this module against a different Solid runtime than the
|
|
81
|
+
// plugin entry, so signals never track across the two. Ask the host to
|
|
82
|
+
// re-render the dialog instead.
|
|
83
|
+
onCleanup(props.subscribe(props.rerender));
|
|
84
|
+
|
|
76
85
|
const messages = createMemo(() =>
|
|
77
86
|
props.sessionID === null ? [] : props.api.state.session.messages(props.sessionID),
|
|
78
87
|
);
|
package/src/providers/kimi.ts
CHANGED
|
@@ -161,13 +161,18 @@ function parse(body: Record<string, unknown>, cred: Credential): AdapterResult {
|
|
|
161
161
|
|
|
162
162
|
// Count-based windows are authoritative when present; the `usages.limit_*`
|
|
163
163
|
// ratios are the primary wire on the post-2026-09 quota model, where the
|
|
164
|
-
// absolute `usage`/`limits[]` rows were removed.
|
|
164
|
+
// absolute `usage`/`limits[]` rows were removed. Some regional responses
|
|
165
|
+
// send a limits[] 5h row with no counts, so fall back to its ratio instead
|
|
166
|
+
// of rendering an empty bar.
|
|
165
167
|
const detail5h = findLimitDetail(limits, 300);
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
168
|
+
const limit5h = pick(usages, "limit_5h");
|
|
169
|
+
const fiveHour = detail5h === undefined ? undefined : detailWindow("5h", "5-hour", detail5h);
|
|
170
|
+
if (fiveHour !== undefined && fiveHour.usedPercent !== null) {
|
|
171
|
+
windows.push(fiveHour);
|
|
172
|
+
} else if (limit5h !== undefined) {
|
|
173
|
+
windows.push(ratioWindow("5h", "5-hour", limit5h));
|
|
174
|
+
} else if (fiveHour !== undefined) {
|
|
175
|
+
windows.push(fiveHour);
|
|
171
176
|
}
|
|
172
177
|
|
|
173
178
|
if (usage !== undefined) {
|
|
@@ -179,12 +184,16 @@ function parse(body: Record<string, unknown>, cred: Credential): AdapterResult {
|
|
|
179
184
|
|
|
180
185
|
// The monthly total joined the quota model; `totalQuota` is the legacy shape.
|
|
181
186
|
// `limit_month_code` is the code-typed share the official client shows as a
|
|
182
|
-
// breakdown under the monthly row.
|
|
187
|
+
// breakdown under the monthly row. Some regional responses carry the monthly
|
|
188
|
+
// window as a counted 30-day entry in `limits[]` instead.
|
|
183
189
|
const monthTotal = pick(usages, "limit_month_total");
|
|
190
|
+
const detailMonth = findLimitDetail(limits, 43200);
|
|
184
191
|
if (monthTotal !== undefined) {
|
|
185
192
|
windows.push(ratioWindow("monthly", "Monthly", monthTotal));
|
|
186
193
|
const codePercent = ratioPercent(pick(usages, "limit_month_code"));
|
|
187
194
|
if (codePercent !== null) extras["Monthly code"] = `${codePercent}%`;
|
|
195
|
+
} else if (detailMonth !== undefined) {
|
|
196
|
+
windows.push(detailWindow("monthly", "Monthly", detailMonth));
|
|
188
197
|
} else {
|
|
189
198
|
const totalQuota = pick(body, "totalQuota", "total_quota");
|
|
190
199
|
const quotaUsed = toNumber(pick(totalQuota, "used"));
|
package/src/tui.tsx
CHANGED
|
@@ -30,10 +30,33 @@ function createUsagePanel(getSessionID: () => string | null, options: ResolvedTu
|
|
|
30
30
|
const [error, setError] = createSignal<string | null>(null);
|
|
31
31
|
const [tick, setTick] = createSignal(Date.now());
|
|
32
32
|
const [capture, setCapture] = createSignal<SystemCapture | null>(null);
|
|
33
|
-
const [scope,
|
|
34
|
-
const [showJson,
|
|
33
|
+
const [scope, setScopeSignal] = createSignal(0);
|
|
34
|
+
const [showJson, setShowJsonSignal] = createSignal(false);
|
|
35
35
|
let disposed = false;
|
|
36
36
|
|
|
37
|
+
// The dialog lives in another module, which OpenCode can load against a
|
|
38
|
+
// second Solid runtime; its memos then never track these signals. Plain
|
|
39
|
+
// subscribers let it bump a signal it owns instead.
|
|
40
|
+
const listeners = new Set<() => void>();
|
|
41
|
+
const notify = (): void => {
|
|
42
|
+
// Snapshot: a listener may re-render the dialog, which resubscribes.
|
|
43
|
+
for (const listener of Array.from(listeners)) listener();
|
|
44
|
+
};
|
|
45
|
+
const subscribe = (listener: () => void): (() => void) => {
|
|
46
|
+
listeners.add(listener);
|
|
47
|
+
return () => {
|
|
48
|
+
listeners.delete(listener);
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
const setScope = (value: number | ((n: number) => number)): void => {
|
|
52
|
+
setScopeSignal(value);
|
|
53
|
+
notify();
|
|
54
|
+
};
|
|
55
|
+
const setShowJson = (value: boolean | ((v: boolean) => boolean)): void => {
|
|
56
|
+
setShowJsonSignal(value);
|
|
57
|
+
notify();
|
|
58
|
+
};
|
|
59
|
+
|
|
37
60
|
const load = async (refresh = false): Promise<void> => {
|
|
38
61
|
if (disposed) return;
|
|
39
62
|
setInFlight((n) => n + 1);
|
|
@@ -55,6 +78,7 @@ function createUsagePanel(getSessionID: () => string | null, options: ResolvedTu
|
|
|
55
78
|
setError(err instanceof Error ? err.message : String(err));
|
|
56
79
|
} finally {
|
|
57
80
|
setInFlight((n) => Math.max(0, n - 1));
|
|
81
|
+
notify();
|
|
58
82
|
}
|
|
59
83
|
};
|
|
60
84
|
|
|
@@ -63,13 +87,16 @@ function createUsagePanel(getSessionID: () => string | null, options: ResolvedTu
|
|
|
63
87
|
const sessionID = getSessionID();
|
|
64
88
|
if (sessionID === null) {
|
|
65
89
|
setCapture(null);
|
|
90
|
+
notify();
|
|
66
91
|
return;
|
|
67
92
|
}
|
|
68
93
|
setCapture(await readCapture(sessionID));
|
|
94
|
+
notify();
|
|
69
95
|
};
|
|
70
96
|
|
|
71
97
|
const refresh = (): void => {
|
|
72
98
|
setTick(Date.now());
|
|
99
|
+
notify();
|
|
73
100
|
void load();
|
|
74
101
|
void refreshCapture();
|
|
75
102
|
};
|
|
@@ -88,6 +115,7 @@ function createUsagePanel(getSessionID: () => string | null, options: ResolvedTu
|
|
|
88
115
|
showJson,
|
|
89
116
|
setScope,
|
|
90
117
|
setShowJson,
|
|
118
|
+
subscribe,
|
|
91
119
|
load,
|
|
92
120
|
refreshCapture,
|
|
93
121
|
refresh,
|
|
@@ -96,6 +124,7 @@ function createUsagePanel(getSessionID: () => string | null, options: ResolvedTu
|
|
|
96
124
|
dispose: (): void => {
|
|
97
125
|
disposed = true;
|
|
98
126
|
clearInterval(timer);
|
|
127
|
+
listeners.clear();
|
|
99
128
|
},
|
|
100
129
|
};
|
|
101
130
|
}
|
|
@@ -153,28 +182,37 @@ export const tui: TuiPlugin = async (api, rawOptions) => {
|
|
|
153
182
|
api.lifecycle.onDispose(panel.dispose);
|
|
154
183
|
api.lifecycle.onDispose(api.event.on("session.idle", panel.refresh));
|
|
155
184
|
|
|
185
|
+
// V1's replace re-invokes this, so the dialog reads the current panel state.
|
|
186
|
+
const showDialog = (sessionID: string | null): void => {
|
|
187
|
+
api.ui.dialog.replace(() => (
|
|
188
|
+
<UsageDialog
|
|
189
|
+
api={api}
|
|
190
|
+
sessionID={sessionID}
|
|
191
|
+
reports={panel.reports}
|
|
192
|
+
busy={panel.busy}
|
|
193
|
+
error={panel.error}
|
|
194
|
+
now={() => new Date(panel.tick())}
|
|
195
|
+
system={panel.capture}
|
|
196
|
+
scope={panel.scope}
|
|
197
|
+
showJson={panel.showJson}
|
|
198
|
+
subscribe={panel.subscribe}
|
|
199
|
+
rerender={() => {
|
|
200
|
+
if (!panel.isDisposed()) showDialog(sessionID);
|
|
201
|
+
}}
|
|
202
|
+
barWidth={options.barWidth}
|
|
203
|
+
/>
|
|
204
|
+
));
|
|
205
|
+
// Must follow replace(): the stack resets the size to "medium".
|
|
206
|
+
api.ui.dialog.setSize("xlarge");
|
|
207
|
+
};
|
|
208
|
+
|
|
156
209
|
const openDialog = (): void => {
|
|
157
210
|
const sessionID = activeSessionID(api);
|
|
158
211
|
void panel.refreshCapture().finally(() => {
|
|
159
212
|
if (panel.isDisposed()) return;
|
|
160
213
|
panel.setScope(0);
|
|
161
214
|
panel.setShowJson(false);
|
|
162
|
-
|
|
163
|
-
<UsageDialog
|
|
164
|
-
api={api}
|
|
165
|
-
sessionID={sessionID}
|
|
166
|
-
reports={panel.reports}
|
|
167
|
-
busy={panel.busy}
|
|
168
|
-
error={panel.error}
|
|
169
|
-
now={() => new Date(panel.tick())}
|
|
170
|
-
system={panel.capture}
|
|
171
|
-
scope={panel.scope}
|
|
172
|
-
showJson={panel.showJson}
|
|
173
|
-
barWidth={options.barWidth}
|
|
174
|
-
/>
|
|
175
|
-
));
|
|
176
|
-
// Must follow replace(): the stack resets the size to "medium".
|
|
177
|
-
api.ui.dialog.setSize("xlarge");
|
|
215
|
+
showDialog(sessionID);
|
|
178
216
|
});
|
|
179
217
|
};
|
|
180
218
|
|
|
@@ -316,27 +354,35 @@ function setup(ctx: V2Context): () => void {
|
|
|
316
354
|
|
|
317
355
|
const offIdle = ctx.data.on("session.idle", () => panel.refresh());
|
|
318
356
|
|
|
357
|
+
const showDialog = (sessionID: string | null): void => {
|
|
358
|
+
ctx.ui.dialog.show(() => (
|
|
359
|
+
<UsageDialog
|
|
360
|
+
api={api}
|
|
361
|
+
sessionID={sessionID}
|
|
362
|
+
reports={panel.reports}
|
|
363
|
+
busy={panel.busy}
|
|
364
|
+
error={panel.error}
|
|
365
|
+
now={() => new Date(panel.tick())}
|
|
366
|
+
system={panel.capture}
|
|
367
|
+
scope={panel.scope}
|
|
368
|
+
showJson={panel.showJson}
|
|
369
|
+
subscribe={panel.subscribe}
|
|
370
|
+
rerender={() => {
|
|
371
|
+
if (!panel.isDisposed()) showDialog(sessionID);
|
|
372
|
+
}}
|
|
373
|
+
barWidth={options.barWidth}
|
|
374
|
+
/>
|
|
375
|
+
));
|
|
376
|
+
ctx.ui.dialog.set({ size: "xlarge" });
|
|
377
|
+
};
|
|
378
|
+
|
|
319
379
|
const openDialog = (): void => {
|
|
320
380
|
const sessionID = activeSessionID(api);
|
|
321
381
|
void panel.refreshCapture().finally(() => {
|
|
322
382
|
if (panel.isDisposed()) return;
|
|
323
383
|
panel.setScope(0);
|
|
324
384
|
panel.setShowJson(false);
|
|
325
|
-
|
|
326
|
-
<UsageDialog
|
|
327
|
-
api={api}
|
|
328
|
-
sessionID={sessionID}
|
|
329
|
-
reports={panel.reports}
|
|
330
|
-
busy={panel.busy}
|
|
331
|
-
error={panel.error}
|
|
332
|
-
now={() => new Date(panel.tick())}
|
|
333
|
-
system={panel.capture}
|
|
334
|
-
scope={panel.scope}
|
|
335
|
-
showJson={panel.showJson}
|
|
336
|
-
barWidth={options.barWidth}
|
|
337
|
-
/>
|
|
338
|
-
));
|
|
339
|
-
ctx.ui.dialog.set({ size: "xlarge" });
|
|
385
|
+
showDialog(sessionID);
|
|
340
386
|
});
|
|
341
387
|
};
|
|
342
388
|
|