@a-r-m-i-n/opencode-openai-usage 0.1.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/LICENSE +21 -0
- package/README.md +116 -0
- package/dist/chunk-SR64YZVV.js +396 -0
- package/dist/index.js +73 -0
- package/dist/tui.js +245 -0
- package/package.json +48 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Armin Vieweg
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# @a-r-m-i-n/opencode-openai-usage
|
|
2
|
+
|
|
3
|
+
OpenCode plugin that reads your ChatGPT account usage and shows it in the TUI sidebar and command palette.
|
|
4
|
+
|
|
5
|
+
The sidebar panel starts expanded and can be collapsed.
|
|
6
|
+
|
|
7
|
+

|
|
8
|
+
|
|
9
|
+
## What It Does
|
|
10
|
+
|
|
11
|
+
- fetches usage data from OpenAI Backend API
|
|
12
|
+
- reads the OpenCode OpenAI OAuth token from OpenCode's local state
|
|
13
|
+
- shows usage windows in the collapsible sidebar
|
|
14
|
+
- adds `OpenAI Usage`, `OpenAI Usage: Toggle Sidebar Display`, and `OpenAI Usage: Show/Hide Sidebar Section` commands to the TUI command list
|
|
15
|
+
|
|
16
|
+
## Screenshots
|
|
17
|
+
|
|
18
|
+
### Command Palette
|
|
19
|
+
|
|
20
|
+
Shows the OpenAI-specific commands added by the plugin.
|
|
21
|
+
|
|
22
|
+
- `OpenAI Usage`: opens the detailed usage dialog
|
|
23
|
+
- `OpenAI Usage: Toggle Sidebar Display`: switches the sidebar between used quota and remaining quota
|
|
24
|
+
- `OpenAI Usage: Show Sidebar Section` / `OpenAI Usage: Hide Sidebar Section`: toggles the sidebar section and persists the setting across restarts
|
|
25
|
+
|
|
26
|
+
<img src="docs/screenshots/command-palette-openai.png" alt="Command palette with OpenAI Usage commands" width="400" />
|
|
27
|
+
|
|
28
|
+
### Usage Dialog
|
|
29
|
+
|
|
30
|
+
Shows the detailed usage summary, including the current usage windows and account details returned by the upstream endpoint.
|
|
31
|
+
|
|
32
|
+
<img src="docs/screenshots/openai-usage-dialog.png" alt="OpenAI Usage dialog" width="400" />
|
|
33
|
+
|
|
34
|
+
### Sidebar Display Modes
|
|
35
|
+
|
|
36
|
+
The sidebar can show either used quota or remaining quota.
|
|
37
|
+
|
|
38
|
+
Used mode:
|
|
39
|
+
|
|
40
|
+
<img src="docs/screenshots/mode-used.png" alt="Sidebar showing used quota" width="350" />
|
|
41
|
+
|
|
42
|
+
Left mode:
|
|
43
|
+
|
|
44
|
+
<img src="docs/screenshots/mode-left.png" alt="Sidebar showing remaining quota" width="350" />
|
|
45
|
+
|
|
46
|
+
## Requirements
|
|
47
|
+
|
|
48
|
+
- OpenCode with an OpenAI account connected via OAuth
|
|
49
|
+
- Node.js and npm available for installation/build steps
|
|
50
|
+
|
|
51
|
+
## Install From npm
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
npm install @a-r-m-i-n/opencode-openai-usage
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Add the runtime plugin in `opencode.json`:
|
|
58
|
+
|
|
59
|
+
```json
|
|
60
|
+
{
|
|
61
|
+
"$schema": "https://opencode.ai/config.json",
|
|
62
|
+
"plugin": ["@a-r-m-i-n/opencode-openai-usage"]
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Add the TUI plugin in `tui.json`:
|
|
67
|
+
|
|
68
|
+
```json
|
|
69
|
+
{
|
|
70
|
+
"$schema": "https://opencode.ai/tui.json",
|
|
71
|
+
"plugin": [["@a-r-m-i-n/opencode-openai-usage/tui", { "invert": false }]]
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
After changing config, quit and restart OpenCode.
|
|
76
|
+
|
|
77
|
+
## TUI Options
|
|
78
|
+
|
|
79
|
+
The TUI plugin accepts options in `tui.json`:
|
|
80
|
+
|
|
81
|
+
```json
|
|
82
|
+
{
|
|
83
|
+
"$schema": "https://opencode.ai/tui.json",
|
|
84
|
+
"plugin": [["@a-r-m-i-n/opencode-openai-usage/tui", { "invert": true }]]
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
| Option | Default | Description |
|
|
89
|
+
|---|---|---|
|
|
90
|
+
| `invert` | `false` | Default sidebar mode. `false` shows used quota like `60% used`; `true` shows remaining quota like `40% left`. After the `OpenAI Usage: Toggle Sidebar Display` command is used, the last selected mode is persisted across restarts. |
|
|
91
|
+
|
|
92
|
+
## Project Structure
|
|
93
|
+
|
|
94
|
+
```text
|
|
95
|
+
src/
|
|
96
|
+
index.ts
|
|
97
|
+
tui.tsx
|
|
98
|
+
lib/openai-usage.ts
|
|
99
|
+
.opencode/
|
|
100
|
+
plugins/openai-usage.ts
|
|
101
|
+
tui/openai-usage.tsx
|
|
102
|
+
tui.json
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Notes And Limitations
|
|
106
|
+
|
|
107
|
+
- The plugin depends on OpenCode's locally stored `auth.json` format.
|
|
108
|
+
- The usage endpoint is an internal ChatGPT web endpoint and may change without notice.
|
|
109
|
+
- The plugin caches usage data locally in OpenCode's state directory.
|
|
110
|
+
- The plugin hides its sidebar panel when OpenCode has no OpenAI OAuth account configured.
|
|
111
|
+
- Usage-specific commands are hidden when OpenCode has no OpenAI OAuth account configured, but the sidebar visibility toggle remains available.
|
|
112
|
+
- The command summary currently includes the account email returned by the upstream endpoint.
|
|
113
|
+
|
|
114
|
+
## License
|
|
115
|
+
|
|
116
|
+
MIT
|
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
// src/lib/openai-usage.ts
|
|
2
|
+
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
3
|
+
import { homedir } from "os";
|
|
4
|
+
import { dirname, join } from "path";
|
|
5
|
+
var USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
|
|
6
|
+
var CACHE_FILE = "storage/openai-usage-cache.json";
|
|
7
|
+
var AUTH_FILE = "auth.json";
|
|
8
|
+
var FETCH_TIMEOUT_MS = 15e3;
|
|
9
|
+
var COMMAND_SUMMARY_DIVIDER = "-".repeat(56);
|
|
10
|
+
var DEFAULT_USAGE_STATE = {
|
|
11
|
+
primary: null,
|
|
12
|
+
secondary: null,
|
|
13
|
+
fetchedAt: null,
|
|
14
|
+
error: null,
|
|
15
|
+
loading: true,
|
|
16
|
+
configured: null,
|
|
17
|
+
rateLimitReachedType: null,
|
|
18
|
+
accountId: null,
|
|
19
|
+
userId: null,
|
|
20
|
+
email: null,
|
|
21
|
+
planType: null
|
|
22
|
+
};
|
|
23
|
+
function getOpenCodeStateDir() {
|
|
24
|
+
const xdgDataHome = process.env.XDG_DATA_HOME;
|
|
25
|
+
if (xdgDataHome) {
|
|
26
|
+
return join(xdgDataHome, "opencode");
|
|
27
|
+
}
|
|
28
|
+
if (process.platform === "darwin") {
|
|
29
|
+
return join(homedir(), "Library", "Application Support", "opencode");
|
|
30
|
+
}
|
|
31
|
+
if (process.platform === "win32") {
|
|
32
|
+
const appData = process.env.APPDATA;
|
|
33
|
+
if (appData) {
|
|
34
|
+
return join(appData, "opencode");
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return join(homedir(), ".local", "share", "opencode");
|
|
38
|
+
}
|
|
39
|
+
function getUsageCachePath(stateDir) {
|
|
40
|
+
return join(stateDir, CACHE_FILE);
|
|
41
|
+
}
|
|
42
|
+
async function readUsageState(stateDir) {
|
|
43
|
+
const cachePath = getUsageCachePath(stateDir);
|
|
44
|
+
try {
|
|
45
|
+
const raw = await readFile(cachePath, "utf8");
|
|
46
|
+
return normalizeStoredState(JSON.parse(raw));
|
|
47
|
+
} catch {
|
|
48
|
+
return { ...DEFAULT_USAGE_STATE };
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
async function writeUsageState(stateDir, state) {
|
|
52
|
+
const cachePath = getUsageCachePath(stateDir);
|
|
53
|
+
await mkdir(dirname(cachePath), { recursive: true });
|
|
54
|
+
await writeFile(cachePath, JSON.stringify(state, null, 2));
|
|
55
|
+
}
|
|
56
|
+
async function buildUsageState(stateDir) {
|
|
57
|
+
let accessToken;
|
|
58
|
+
try {
|
|
59
|
+
const auth = await readAuthFile(stateDir);
|
|
60
|
+
accessToken = extractAccessToken(auth);
|
|
61
|
+
} catch (error) {
|
|
62
|
+
if (isNotConfiguredError(error)) {
|
|
63
|
+
return buildUnconfiguredState();
|
|
64
|
+
}
|
|
65
|
+
throw error;
|
|
66
|
+
}
|
|
67
|
+
const data = await fetchUsagePayload(accessToken);
|
|
68
|
+
const rateLimits = extractRateLimitsPayload(data);
|
|
69
|
+
const secondaryPayload = rateLimits.secondary ?? rateLimits.secondary_window ?? null;
|
|
70
|
+
const rateLimitReachedType = normalizeOptionalString(
|
|
71
|
+
rateLimits.rateLimitReachedType ?? rateLimits.rate_limit_reached_type ?? null
|
|
72
|
+
);
|
|
73
|
+
return {
|
|
74
|
+
primary: normalizeWindow(rateLimits.primary ?? rateLimits.primary_window ?? null, "primary"),
|
|
75
|
+
secondary: secondaryPayload === null ? null : normalizeWindow(secondaryPayload, "secondary"),
|
|
76
|
+
fetchedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
77
|
+
error: null,
|
|
78
|
+
loading: false,
|
|
79
|
+
configured: true,
|
|
80
|
+
rateLimitReachedType,
|
|
81
|
+
email: normalizeOptionalString(data.email ?? rateLimits.email ?? null),
|
|
82
|
+
accountId: normalizeOptionalString(
|
|
83
|
+
data.account_id ?? data.accountId ?? rateLimits.account_id ?? rateLimits.accountId ?? null
|
|
84
|
+
),
|
|
85
|
+
userId: normalizeOptionalString(
|
|
86
|
+
data.user_id ?? data.userId ?? rateLimits.user_id ?? rateLimits.userId ?? null
|
|
87
|
+
),
|
|
88
|
+
planType: normalizeOptionalString(
|
|
89
|
+
data.plan_type ?? data.planType ?? rateLimits.plan_type ?? rateLimits.planType ?? null
|
|
90
|
+
)
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
function buildFailureState(previous, error) {
|
|
94
|
+
return {
|
|
95
|
+
...previous,
|
|
96
|
+
loading: false,
|
|
97
|
+
error: formatError(error)
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
function formatCommandSummary(state, packageName, pluginVersion, githubUrl) {
|
|
101
|
+
const lines = [];
|
|
102
|
+
let hasUsageDetails = false;
|
|
103
|
+
if (state.error) {
|
|
104
|
+
lines.push("Status: unavailable");
|
|
105
|
+
lines.push(`Error: ${state.error}`);
|
|
106
|
+
hasUsageDetails = true;
|
|
107
|
+
}
|
|
108
|
+
hasUsageDetails = appendWindowLines(lines, "Primary", state.primary) || hasUsageDetails;
|
|
109
|
+
if (state.primary && state.secondary) {
|
|
110
|
+
lines.push("");
|
|
111
|
+
}
|
|
112
|
+
hasUsageDetails = appendWindowLines(lines, "Secondary", state.secondary) || hasUsageDetails;
|
|
113
|
+
if (state.rateLimitReachedType) {
|
|
114
|
+
lines.push(`Rate limit reached type: ${state.rateLimitReachedType}`);
|
|
115
|
+
hasUsageDetails = true;
|
|
116
|
+
}
|
|
117
|
+
if (state.planType) {
|
|
118
|
+
lines.push("");
|
|
119
|
+
lines.push(`Plan: ${state.planType}`);
|
|
120
|
+
hasUsageDetails = true;
|
|
121
|
+
}
|
|
122
|
+
if (state.email) {
|
|
123
|
+
lines.push(`Account email: ${state.email}`);
|
|
124
|
+
hasUsageDetails = true;
|
|
125
|
+
}
|
|
126
|
+
if (state.fetchedAt) {
|
|
127
|
+
lines.push(`Fetched at: ${formatTimestamp(state.fetchedAt)}`);
|
|
128
|
+
hasUsageDetails = true;
|
|
129
|
+
}
|
|
130
|
+
if (!hasUsageDetails) {
|
|
131
|
+
lines.push("Status: unavailable");
|
|
132
|
+
lines.push("Error: Usage data has not been fetched yet.");
|
|
133
|
+
}
|
|
134
|
+
const pluginLines = [];
|
|
135
|
+
if (packageName) {
|
|
136
|
+
pluginLines.push(`Plugin: ${packageName}`);
|
|
137
|
+
}
|
|
138
|
+
if (pluginVersion) {
|
|
139
|
+
pluginLines.push(`Version: ${pluginVersion}`);
|
|
140
|
+
}
|
|
141
|
+
if (githubUrl) {
|
|
142
|
+
if (pluginLines.length > 0) {
|
|
143
|
+
pluginLines.push("");
|
|
144
|
+
}
|
|
145
|
+
pluginLines.push(githubUrl);
|
|
146
|
+
}
|
|
147
|
+
if (pluginLines.length > 0) {
|
|
148
|
+
lines.push("");
|
|
149
|
+
lines.push(COMMAND_SUMMARY_DIVIDER);
|
|
150
|
+
lines.push("");
|
|
151
|
+
lines.push(...pluginLines);
|
|
152
|
+
}
|
|
153
|
+
return lines.join("\n");
|
|
154
|
+
}
|
|
155
|
+
function getUsageDisplay(usedPercent, invert) {
|
|
156
|
+
const clampedUsedPercent = Math.max(0, Math.min(100, usedPercent));
|
|
157
|
+
if (invert) {
|
|
158
|
+
return {
|
|
159
|
+
percent: Math.max(0, 100 - clampedUsedPercent),
|
|
160
|
+
label: "left"
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
return {
|
|
164
|
+
percent: clampedUsedPercent,
|
|
165
|
+
label: "used"
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
function appendWindowLines(lines, label, window) {
|
|
169
|
+
if (!window) {
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
const leftPercent = Math.max(0, 100 - window.usedPercent);
|
|
173
|
+
lines.push(`${label} window`);
|
|
174
|
+
lines.push(
|
|
175
|
+
`${formatWindowLabel(window.windowDurationMins)}: ${formatPercent(window.usedPercent)} used, ${formatPercent(leftPercent)} left, resets ${formatTimestamp(window.resetsAt)}`
|
|
176
|
+
);
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
179
|
+
async function readAuthFile(stateDir) {
|
|
180
|
+
const authPath = join(stateDir, AUTH_FILE);
|
|
181
|
+
let raw;
|
|
182
|
+
try {
|
|
183
|
+
raw = await readFile(authPath, "utf8");
|
|
184
|
+
} catch (error) {
|
|
185
|
+
if (isMissingFileError(error)) {
|
|
186
|
+
throw new NotConfiguredError("OpenCode has no ChatGPT auth configured.");
|
|
187
|
+
}
|
|
188
|
+
throw error;
|
|
189
|
+
}
|
|
190
|
+
return JSON.parse(raw);
|
|
191
|
+
}
|
|
192
|
+
function extractAccessToken(auth) {
|
|
193
|
+
const provider = auth.openai;
|
|
194
|
+
if (!isRecord(provider)) {
|
|
195
|
+
throw new NotConfiguredError("OpenCode has no ChatGPT auth configured.");
|
|
196
|
+
}
|
|
197
|
+
const oauth = provider;
|
|
198
|
+
if (oauth.type !== "oauth") {
|
|
199
|
+
throw new NotConfiguredError("OpenCode has no ChatGPT auth configured.");
|
|
200
|
+
}
|
|
201
|
+
if (typeof oauth.access !== "string" || oauth.access.length === 0) {
|
|
202
|
+
throw new NotConfiguredError("OpenCode has no ChatGPT auth configured.");
|
|
203
|
+
}
|
|
204
|
+
return oauth.access;
|
|
205
|
+
}
|
|
206
|
+
function buildUnconfiguredState() {
|
|
207
|
+
return {
|
|
208
|
+
...DEFAULT_USAGE_STATE,
|
|
209
|
+
loading: false,
|
|
210
|
+
configured: false
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
async function fetchUsagePayload(accessToken) {
|
|
214
|
+
const controller = new AbortController();
|
|
215
|
+
const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
216
|
+
let response;
|
|
217
|
+
try {
|
|
218
|
+
response = await fetch(USAGE_URL, {
|
|
219
|
+
headers: {
|
|
220
|
+
Authorization: `Bearer ${accessToken}`,
|
|
221
|
+
Accept: "application/json"
|
|
222
|
+
},
|
|
223
|
+
signal: controller.signal
|
|
224
|
+
});
|
|
225
|
+
} catch (error) {
|
|
226
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
227
|
+
throw new Error(`Usage request timed out after ${Math.floor(FETCH_TIMEOUT_MS / 1e3)}s.`);
|
|
228
|
+
}
|
|
229
|
+
throw error;
|
|
230
|
+
} finally {
|
|
231
|
+
clearTimeout(timeout);
|
|
232
|
+
}
|
|
233
|
+
if (!response.ok) {
|
|
234
|
+
let details = "";
|
|
235
|
+
try {
|
|
236
|
+
details = await response.text();
|
|
237
|
+
} catch {
|
|
238
|
+
details = "";
|
|
239
|
+
}
|
|
240
|
+
const body = details.trim();
|
|
241
|
+
const suffix = body ? ` - ${body.slice(0, 300)}` : "";
|
|
242
|
+
throw new Error(`Usage request failed: HTTP ${response.status}${suffix}`);
|
|
243
|
+
}
|
|
244
|
+
const data = await response.json();
|
|
245
|
+
if (!isRecord(data)) {
|
|
246
|
+
throw new Error("Usage response is not a JSON object.");
|
|
247
|
+
}
|
|
248
|
+
return data;
|
|
249
|
+
}
|
|
250
|
+
function extractRateLimitsPayload(data) {
|
|
251
|
+
const rateLimits = data.rateLimits ?? data.rate_limits ?? data.rate_limit ?? null;
|
|
252
|
+
return isRecord(rateLimits) ? rateLimits : data;
|
|
253
|
+
}
|
|
254
|
+
function normalizeWindow(window, name) {
|
|
255
|
+
if (!isRecord(window)) {
|
|
256
|
+
throw new Error(`Usage response is missing ${name} window data.`);
|
|
257
|
+
}
|
|
258
|
+
let windowDurationMins = toNumber(window.windowDurationMins ?? window.window_duration_mins ?? null);
|
|
259
|
+
const windowDurationSeconds = toNumber(window.limit_window_seconds ?? null);
|
|
260
|
+
const usedPercent = toNumber(window.usedPercent ?? window.used_percent ?? null);
|
|
261
|
+
const resetsAt = window.resetsAt ?? window.resetAt ?? window.reset_at ?? null;
|
|
262
|
+
if (windowDurationMins === null && windowDurationSeconds !== null) {
|
|
263
|
+
windowDurationMins = Math.ceil(windowDurationSeconds / 60);
|
|
264
|
+
}
|
|
265
|
+
if (usedPercent === null || windowDurationMins === null || !isValidResetValue(resetsAt)) {
|
|
266
|
+
throw new Error(`Usage response contains incomplete ${name} window data.`);
|
|
267
|
+
}
|
|
268
|
+
return {
|
|
269
|
+
usedPercent,
|
|
270
|
+
windowDurationMins,
|
|
271
|
+
resetsAt: normalizeResetValue(resetsAt)
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
function normalizeStoredState(input) {
|
|
275
|
+
return {
|
|
276
|
+
primary: normalizeStoredWindow(input.primary),
|
|
277
|
+
secondary: normalizeStoredWindow(input.secondary),
|
|
278
|
+
fetchedAt: normalizeOptionalString(input.fetchedAt ?? null),
|
|
279
|
+
error: normalizeOptionalString(input.error ?? null),
|
|
280
|
+
loading: input.loading === true,
|
|
281
|
+
configured: typeof input.configured === "boolean" ? input.configured : null,
|
|
282
|
+
rateLimitReachedType: normalizeOptionalString(input.rateLimitReachedType ?? null),
|
|
283
|
+
accountId: normalizeOptionalString(input.accountId ?? null),
|
|
284
|
+
userId: normalizeOptionalString(input.userId ?? null),
|
|
285
|
+
email: normalizeOptionalString(input.email ?? null),
|
|
286
|
+
planType: normalizeOptionalString(input.planType ?? null)
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
function normalizeStoredWindow(window) {
|
|
290
|
+
if (!isRecord(window)) {
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
const usedPercent = toNumber(window.usedPercent ?? null);
|
|
294
|
+
const windowDurationMins = toNumber(window.windowDurationMins ?? null);
|
|
295
|
+
const resetsAt = normalizeOptionalString(window.resetsAt ?? null);
|
|
296
|
+
if (usedPercent === null || windowDurationMins === null || !resetsAt) {
|
|
297
|
+
return null;
|
|
298
|
+
}
|
|
299
|
+
return { usedPercent, windowDurationMins, resetsAt };
|
|
300
|
+
}
|
|
301
|
+
function normalizeOptionalString(value) {
|
|
302
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
303
|
+
}
|
|
304
|
+
function formatPercent(value) {
|
|
305
|
+
return Number.isInteger(value) ? `${value}%` : `${value.toFixed(1)}%`;
|
|
306
|
+
}
|
|
307
|
+
function formatWindowLabel(windowDurationMins) {
|
|
308
|
+
if (windowDurationMins % (60 * 24) === 0) {
|
|
309
|
+
return `${windowDurationMins / (60 * 24)}d`;
|
|
310
|
+
}
|
|
311
|
+
if (windowDurationMins % 60 === 0) {
|
|
312
|
+
return `${windowDurationMins / 60}h`;
|
|
313
|
+
}
|
|
314
|
+
return `${windowDurationMins}m`;
|
|
315
|
+
}
|
|
316
|
+
function formatTimestamp(value) {
|
|
317
|
+
const date = new Date(value);
|
|
318
|
+
if (Number.isNaN(date.getTime())) {
|
|
319
|
+
return value;
|
|
320
|
+
}
|
|
321
|
+
const yyyy = `${date.getFullYear()}`;
|
|
322
|
+
const mm = `${date.getMonth() + 1}`.padStart(2, "0");
|
|
323
|
+
const dd = `${date.getDate()}`.padStart(2, "0");
|
|
324
|
+
const hh = `${date.getHours()}`.padStart(2, "0");
|
|
325
|
+
const mi = `${date.getMinutes()}`.padStart(2, "0");
|
|
326
|
+
return `${yyyy}-${mm}-${dd} ${hh}:${mi}`;
|
|
327
|
+
}
|
|
328
|
+
function formatRelativeDuration(value) {
|
|
329
|
+
const timestamp = Date.parse(value);
|
|
330
|
+
if (Number.isNaN(timestamp)) {
|
|
331
|
+
return formatTimestamp(value);
|
|
332
|
+
}
|
|
333
|
+
return formatDurationParts(Math.max(0, timestamp - Date.now()));
|
|
334
|
+
}
|
|
335
|
+
function formatDurationParts(diffMs) {
|
|
336
|
+
const totalMinutes = Math.ceil(diffMs / 6e4);
|
|
337
|
+
const days = Math.floor(totalMinutes / (60 * 24));
|
|
338
|
+
const hours = Math.floor(totalMinutes % (60 * 24) / 60);
|
|
339
|
+
const minutes = totalMinutes % 60;
|
|
340
|
+
if (days > 0) {
|
|
341
|
+
return hours > 0 ? `${days}d ${hours}h` : `${days}d`;
|
|
342
|
+
}
|
|
343
|
+
if (hours > 0) {
|
|
344
|
+
return minutes > 0 ? `${hours}h ${minutes}m` : `${hours}h`;
|
|
345
|
+
}
|
|
346
|
+
return `${Math.max(1, minutes)}m`;
|
|
347
|
+
}
|
|
348
|
+
function isValidResetValue(value) {
|
|
349
|
+
return typeof value === "string" && value.length > 0 || typeof value === "number";
|
|
350
|
+
}
|
|
351
|
+
function normalizeResetValue(value) {
|
|
352
|
+
return typeof value === "number" ? new Date(value * 1e3).toISOString() : value;
|
|
353
|
+
}
|
|
354
|
+
function toNumber(value) {
|
|
355
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
356
|
+
return value;
|
|
357
|
+
}
|
|
358
|
+
if (typeof value === "string" && value.trim() !== "") {
|
|
359
|
+
const parsed = Number(value);
|
|
360
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
361
|
+
}
|
|
362
|
+
return null;
|
|
363
|
+
}
|
|
364
|
+
function isRecord(value) {
|
|
365
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
366
|
+
}
|
|
367
|
+
function isMissingFileError(error) {
|
|
368
|
+
return isRecord(error) && error.code === "ENOENT";
|
|
369
|
+
}
|
|
370
|
+
function isNotConfiguredError(error) {
|
|
371
|
+
return error instanceof NotConfiguredError;
|
|
372
|
+
}
|
|
373
|
+
var NotConfiguredError = class extends Error {
|
|
374
|
+
constructor(message) {
|
|
375
|
+
super(message);
|
|
376
|
+
this.name = "NotConfiguredError";
|
|
377
|
+
}
|
|
378
|
+
};
|
|
379
|
+
function formatError(error) {
|
|
380
|
+
if (error instanceof Error && error.message) {
|
|
381
|
+
return error.message;
|
|
382
|
+
}
|
|
383
|
+
return "Unknown usage error.";
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
export {
|
|
387
|
+
getOpenCodeStateDir,
|
|
388
|
+
readUsageState,
|
|
389
|
+
writeUsageState,
|
|
390
|
+
buildUsageState,
|
|
391
|
+
buildFailureState,
|
|
392
|
+
formatCommandSummary,
|
|
393
|
+
getUsageDisplay,
|
|
394
|
+
formatWindowLabel,
|
|
395
|
+
formatRelativeDuration
|
|
396
|
+
};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildFailureState,
|
|
3
|
+
buildUsageState,
|
|
4
|
+
getOpenCodeStateDir,
|
|
5
|
+
readUsageState,
|
|
6
|
+
writeUsageState
|
|
7
|
+
} from "./chunk-SR64YZVV.js";
|
|
8
|
+
|
|
9
|
+
// src/index.ts
|
|
10
|
+
var id = "openai-usage";
|
|
11
|
+
var REFRESH_MS = 6e4;
|
|
12
|
+
var MAX_BACKOFF_MS = 15 * 6e4;
|
|
13
|
+
function shouldRefreshOnEvent(type) {
|
|
14
|
+
return type === "account.added" || type === "account.removed" || type === "account.switched" || type === "session.next.prompted" || type === "command.executed";
|
|
15
|
+
}
|
|
16
|
+
var module = {
|
|
17
|
+
id,
|
|
18
|
+
server: async () => {
|
|
19
|
+
const stateDir = getOpenCodeStateDir();
|
|
20
|
+
let currentState = await readUsageState(stateDir);
|
|
21
|
+
let refreshInFlight = null;
|
|
22
|
+
let timer;
|
|
23
|
+
let nextDelay = REFRESH_MS;
|
|
24
|
+
const persistState = async (state = currentState) => {
|
|
25
|
+
currentState = state;
|
|
26
|
+
await writeUsageState(stateDir, currentState);
|
|
27
|
+
};
|
|
28
|
+
const refresh = async (_force = false) => {
|
|
29
|
+
if (refreshInFlight) {
|
|
30
|
+
return refreshInFlight;
|
|
31
|
+
}
|
|
32
|
+
refreshInFlight = (async () => {
|
|
33
|
+
try {
|
|
34
|
+
if (!currentState.fetchedAt) {
|
|
35
|
+
await persistState({ ...currentState, loading: true, error: null });
|
|
36
|
+
}
|
|
37
|
+
const nextState = await buildUsageState(stateDir);
|
|
38
|
+
nextDelay = REFRESH_MS;
|
|
39
|
+
await persistState(nextState);
|
|
40
|
+
} catch (error) {
|
|
41
|
+
nextDelay = Math.min(nextDelay * 2, MAX_BACKOFF_MS);
|
|
42
|
+
await persistState(buildFailureState(currentState, error));
|
|
43
|
+
} finally {
|
|
44
|
+
refreshInFlight = null;
|
|
45
|
+
schedule(nextDelay);
|
|
46
|
+
}
|
|
47
|
+
})();
|
|
48
|
+
return refreshInFlight;
|
|
49
|
+
};
|
|
50
|
+
const schedule = (delay) => {
|
|
51
|
+
if (timer) {
|
|
52
|
+
clearTimeout(timer);
|
|
53
|
+
}
|
|
54
|
+
timer = setTimeout(() => {
|
|
55
|
+
void refresh(true);
|
|
56
|
+
}, delay);
|
|
57
|
+
};
|
|
58
|
+
void refresh(true);
|
|
59
|
+
return {
|
|
60
|
+
event: async ({ event }) => {
|
|
61
|
+
if (!shouldRefreshOnEvent(event.type)) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
await refresh(true);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
var index_default = module;
|
|
70
|
+
export {
|
|
71
|
+
index_default as default,
|
|
72
|
+
id
|
|
73
|
+
};
|
package/dist/tui.js
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import {
|
|
2
|
+
formatCommandSummary,
|
|
3
|
+
formatRelativeDuration,
|
|
4
|
+
formatWindowLabel,
|
|
5
|
+
getOpenCodeStateDir,
|
|
6
|
+
getUsageDisplay,
|
|
7
|
+
readUsageState
|
|
8
|
+
} from "./chunk-SR64YZVV.js";
|
|
9
|
+
|
|
10
|
+
// src/tui.tsx
|
|
11
|
+
import { createRequire } from "module";
|
|
12
|
+
import { createTextAttributes } from "@opentui/core";
|
|
13
|
+
import { createSignal } from "solid-js";
|
|
14
|
+
import { jsx, jsxs } from "@opentui/solid/jsx-runtime";
|
|
15
|
+
var id = "openai-usage-tui";
|
|
16
|
+
var CACHE_SYNC_MS = 5e3;
|
|
17
|
+
var DIM_ATTRIBUTES = createTextAttributes({ dim: true });
|
|
18
|
+
var BAR_WIDTH = 20;
|
|
19
|
+
var BAR_EMPTY_COLOR = "#6b7280";
|
|
20
|
+
var BAR_LABEL_DARK_COLOR = "#111827";
|
|
21
|
+
var BAR_LABEL_LIGHT_COLOR = "#f9fafb";
|
|
22
|
+
var SIDEBAR_VERSION_COLOR = "#9ca3af";
|
|
23
|
+
var SIDEBAR_INVERT_KV_KEY = "openai-usage.sidebar.invert";
|
|
24
|
+
var SIDEBAR_VISIBLE_KV_KEY = "openai-usage.sidebar.visible";
|
|
25
|
+
var require2 = createRequire(import.meta.url);
|
|
26
|
+
var PLUGIN_MANIFEST = readPluginManifest();
|
|
27
|
+
var PACKAGE_NAME = PLUGIN_MANIFEST.name;
|
|
28
|
+
var PLUGIN_VERSION = PLUGIN_MANIFEST.version;
|
|
29
|
+
var PACKAGE_HOMEPAGE = PLUGIN_MANIFEST.homepage;
|
|
30
|
+
function readPluginManifest() {
|
|
31
|
+
try {
|
|
32
|
+
const manifest = require2("../package.json");
|
|
33
|
+
const homepage = typeof manifest.homepage === "string" && manifest.homepage.length > 0 ? manifest.homepage.replace(/#readme$/i, "") : null;
|
|
34
|
+
return {
|
|
35
|
+
name: typeof manifest.name === "string" && manifest.name.length > 0 ? manifest.name : null,
|
|
36
|
+
version: typeof manifest.version === "string" && manifest.version.length > 0 ? manifest.version : null,
|
|
37
|
+
homepage
|
|
38
|
+
};
|
|
39
|
+
} catch {
|
|
40
|
+
return { name: null, version: null, homepage: null };
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function formatPercent(percent) {
|
|
44
|
+
return Number.isInteger(percent) ? `${percent}%` : `${percent.toFixed(1)}%`;
|
|
45
|
+
}
|
|
46
|
+
function getBarFillColor(percent, labelSuffix) {
|
|
47
|
+
if (labelSuffix === "used") {
|
|
48
|
+
if (percent >= 50) {
|
|
49
|
+
return "#ef4444";
|
|
50
|
+
}
|
|
51
|
+
if (percent >= 20) {
|
|
52
|
+
return "#eab308";
|
|
53
|
+
}
|
|
54
|
+
return "#22c55e";
|
|
55
|
+
}
|
|
56
|
+
if (percent >= 50) {
|
|
57
|
+
return "#22c55e";
|
|
58
|
+
}
|
|
59
|
+
if (percent >= 20) {
|
|
60
|
+
return "#eab308";
|
|
61
|
+
}
|
|
62
|
+
return "#ef4444";
|
|
63
|
+
}
|
|
64
|
+
function getBarSegments(percent, width) {
|
|
65
|
+
const clampedPercent = Math.max(0, Math.min(100, percent));
|
|
66
|
+
const filled = Math.round(clampedPercent / 100 * width);
|
|
67
|
+
return {
|
|
68
|
+
filled: Math.max(0, Math.min(width, filled)),
|
|
69
|
+
empty: Math.max(0, width - filled)
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
function getBarLabelColor(fillColor) {
|
|
73
|
+
return fillColor === "#ef4444" ? BAR_LABEL_LIGHT_COLOR : BAR_LABEL_DARK_COLOR;
|
|
74
|
+
}
|
|
75
|
+
function renderProgressBar(percent, labelSuffix) {
|
|
76
|
+
const barSegments = getBarSegments(percent, BAR_WIDTH);
|
|
77
|
+
const barFillColor = getBarFillColor(percent, labelSuffix);
|
|
78
|
+
const label = `${formatPercent(percent)} ${labelSuffix}`;
|
|
79
|
+
const labelStart = Math.max(0, Math.floor((BAR_WIDTH - label.length) / 2));
|
|
80
|
+
const labelEnd = labelStart + label.length;
|
|
81
|
+
return Array.from({ length: BAR_WIDTH }, (_, index) => {
|
|
82
|
+
const isFilled = index < barSegments.filled;
|
|
83
|
+
const isLabelCell = index >= labelStart && index < labelEnd;
|
|
84
|
+
const underlyingColor = isFilled ? barFillColor : BAR_EMPTY_COLOR;
|
|
85
|
+
if (isLabelCell) {
|
|
86
|
+
return /* @__PURE__ */ jsx("text", { fg: isFilled ? getBarLabelColor(barFillColor) : BAR_LABEL_LIGHT_COLOR, bg: underlyingColor, children: label[index - labelStart] });
|
|
87
|
+
}
|
|
88
|
+
return isFilled ? /* @__PURE__ */ jsx("text", { fg: underlyingColor, children: "\u2588" }) : /* @__PURE__ */ jsx("text", { bg: BAR_EMPTY_COLOR, children: " " });
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
var module = {
|
|
92
|
+
id,
|
|
93
|
+
tui: async (api, rawOptions) => {
|
|
94
|
+
const stateDir = getOpenCodeStateDir();
|
|
95
|
+
const options = rawOptions ?? {};
|
|
96
|
+
const [sidebarVisible, setSidebarVisible] = createSignal(
|
|
97
|
+
api.kv.get(SIDEBAR_VISIBLE_KV_KEY, true) !== false
|
|
98
|
+
);
|
|
99
|
+
const [invert, setInvert] = createSignal(api.kv.get(SIDEBAR_INVERT_KV_KEY, options.invert === true) === true);
|
|
100
|
+
const [state, setState] = createSignal(await readUsageState(stateDir));
|
|
101
|
+
const [open, setOpen] = createSignal(true);
|
|
102
|
+
let syncInFlight = null;
|
|
103
|
+
const syncState = async () => {
|
|
104
|
+
if (syncInFlight) {
|
|
105
|
+
return syncInFlight;
|
|
106
|
+
}
|
|
107
|
+
syncInFlight = (async () => {
|
|
108
|
+
try {
|
|
109
|
+
const nextState = await readUsageState(stateDir);
|
|
110
|
+
setState(nextState);
|
|
111
|
+
} finally {
|
|
112
|
+
syncInFlight = null;
|
|
113
|
+
}
|
|
114
|
+
})();
|
|
115
|
+
return syncInFlight;
|
|
116
|
+
};
|
|
117
|
+
const showUsageDialog = async () => {
|
|
118
|
+
await syncState();
|
|
119
|
+
const latestState = state();
|
|
120
|
+
setState(latestState);
|
|
121
|
+
api.ui.dialog.replace(
|
|
122
|
+
() => api.ui.DialogAlert({
|
|
123
|
+
title: "OpenAI Usage",
|
|
124
|
+
message: formatCommandSummary(latestState, PACKAGE_NAME, PLUGIN_VERSION, PACKAGE_HOMEPAGE)
|
|
125
|
+
})
|
|
126
|
+
);
|
|
127
|
+
};
|
|
128
|
+
const toggleSidebarInvert = () => {
|
|
129
|
+
const nextInvert = !invert();
|
|
130
|
+
setInvert(nextInvert);
|
|
131
|
+
api.kv.set(SIDEBAR_INVERT_KV_KEY, nextInvert);
|
|
132
|
+
api.ui.toast({
|
|
133
|
+
message: nextInvert ? "Sidebar now shows usage left." : "Sidebar now shows usage used."
|
|
134
|
+
});
|
|
135
|
+
};
|
|
136
|
+
const toggleSidebarVisibility = () => {
|
|
137
|
+
const nextVisible = !sidebarVisible();
|
|
138
|
+
setSidebarVisible(nextVisible);
|
|
139
|
+
api.kv.set(SIDEBAR_VISIBLE_KV_KEY, nextVisible);
|
|
140
|
+
api.ui.toast({
|
|
141
|
+
message: nextVisible ? "Sidebar section is now visible." : "Sidebar section is now hidden."
|
|
142
|
+
});
|
|
143
|
+
};
|
|
144
|
+
void syncState();
|
|
145
|
+
const timer = setInterval(() => {
|
|
146
|
+
void syncState();
|
|
147
|
+
}, CACHE_SYNC_MS);
|
|
148
|
+
api.lifecycle.onDispose(() => {
|
|
149
|
+
clearInterval(timer);
|
|
150
|
+
});
|
|
151
|
+
api.event.on("account.added", () => {
|
|
152
|
+
void syncState();
|
|
153
|
+
});
|
|
154
|
+
api.event.on("account.removed", () => {
|
|
155
|
+
void syncState();
|
|
156
|
+
});
|
|
157
|
+
api.event.on("account.switched", () => {
|
|
158
|
+
void syncState();
|
|
159
|
+
});
|
|
160
|
+
api.event.on("session.next.prompted", () => {
|
|
161
|
+
void syncState();
|
|
162
|
+
});
|
|
163
|
+
api.event.on("command.executed", () => {
|
|
164
|
+
void syncState();
|
|
165
|
+
});
|
|
166
|
+
const renderSidebarWindow = (window) => {
|
|
167
|
+
if (!window) {
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
const usageDisplay = getUsageDisplay(window.usedPercent, invert());
|
|
171
|
+
return /* @__PURE__ */ jsx("box", { flexDirection: "column", gap: 0, padding: 0, margin: 0, children: /* @__PURE__ */ jsxs("box", { flexDirection: "row", gap: 0, padding: 0, margin: 0, children: [
|
|
172
|
+
/* @__PURE__ */ jsx("text", { children: `${formatWindowLabel(window.windowDurationMins)} ` }),
|
|
173
|
+
renderProgressBar(usageDisplay.percent, usageDisplay.label),
|
|
174
|
+
/* @__PURE__ */ jsx("text", { attributes: DIM_ATTRIBUTES, children: ` Reset: ${formatRelativeDuration(window.resetsAt)}` })
|
|
175
|
+
] }) });
|
|
176
|
+
};
|
|
177
|
+
const renderSidebarHeader = () => /* @__PURE__ */ jsxs("box", { flexDirection: "row", gap: 0, padding: 0, margin: 0, onMouseDown: () => setOpen(!open()), children: [
|
|
178
|
+
/* @__PURE__ */ jsx("text", { children: open() ? "\u25BC OpenAI Usage" : "\u25B6 OpenAI Usage" }),
|
|
179
|
+
PLUGIN_VERSION ? /* @__PURE__ */ jsx("text", { fg: SIDEBAR_VERSION_COLOR, attributes: DIM_ATTRIBUTES, children: ` ${PLUGIN_VERSION}` }) : null
|
|
180
|
+
] });
|
|
181
|
+
const renderSidebarBody = () => {
|
|
182
|
+
const currentState = state();
|
|
183
|
+
if (currentState.error && !currentState.primary && !currentState.secondary) {
|
|
184
|
+
return /* @__PURE__ */ jsx("text", { children: `Status: unavailable
|
|
185
|
+
Error: ${currentState.error}` });
|
|
186
|
+
}
|
|
187
|
+
if (!currentState.primary && !currentState.secondary) {
|
|
188
|
+
return /* @__PURE__ */ jsx("text", { children: "Status: waiting for usage data" });
|
|
189
|
+
}
|
|
190
|
+
return /* @__PURE__ */ jsxs("box", { flexDirection: "column", gap: 0, padding: 0, margin: 0, children: [
|
|
191
|
+
renderSidebarWindow(currentState.primary),
|
|
192
|
+
renderSidebarWindow(currentState.secondary)
|
|
193
|
+
] });
|
|
194
|
+
};
|
|
195
|
+
const renderSidebarContent = () => {
|
|
196
|
+
const currentState = state();
|
|
197
|
+
if (!sidebarVisible() || currentState.configured === false) {
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
return /* @__PURE__ */ jsxs("box", { flexDirection: "column", gap: 0, padding: 0, margin: 0, children: [
|
|
201
|
+
renderSidebarHeader(),
|
|
202
|
+
open() ? renderSidebarBody() : null
|
|
203
|
+
] });
|
|
204
|
+
};
|
|
205
|
+
api.slots.register({
|
|
206
|
+
order: -100,
|
|
207
|
+
slots: {
|
|
208
|
+
sidebar_content: renderSidebarContent
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
const unregisterCommand = api.command?.register(() => [
|
|
212
|
+
{
|
|
213
|
+
title: sidebarVisible() ? "Hide Sidebar Section" : "Show Sidebar Section",
|
|
214
|
+
value: "openai-usage.toggle-sidebar-visibility",
|
|
215
|
+
description: "show/hide sidebar entry",
|
|
216
|
+
category: "OpenAI Usage",
|
|
217
|
+
onSelect: toggleSidebarVisibility
|
|
218
|
+
},
|
|
219
|
+
...state().configured === false ? [] : [
|
|
220
|
+
{
|
|
221
|
+
title: "View status",
|
|
222
|
+
value: "openai-usage.show",
|
|
223
|
+
description: "from OpenAI's backend API",
|
|
224
|
+
category: "OpenAI Usage",
|
|
225
|
+
onSelect: showUsageDialog
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
title: "Toggle Display Mode",
|
|
229
|
+
value: "openai-usage.toggle-sidebar-invert",
|
|
230
|
+
description: "between used/left quota",
|
|
231
|
+
category: "OpenAI Usage",
|
|
232
|
+
onSelect: toggleSidebarInvert
|
|
233
|
+
}
|
|
234
|
+
]
|
|
235
|
+
]);
|
|
236
|
+
api.lifecycle.onDispose(() => {
|
|
237
|
+
unregisterCommand?.();
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
var tui_default = module;
|
|
242
|
+
export {
|
|
243
|
+
tui_default as default,
|
|
244
|
+
id
|
|
245
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@a-r-m-i-n/opencode-openai-usage",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "OpenCode plugin that shows ChatGPT/OpenAI usage in the sidebar and command palette.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/a-r-m-i-n/opencode-openai-usage.git"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://github.com/a-r-m-i-n/opencode-openai-usage#readme",
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/a-r-m-i-n/opencode-openai-usage/issues"
|
|
13
|
+
},
|
|
14
|
+
"type": "module",
|
|
15
|
+
"keywords": [
|
|
16
|
+
"opencode",
|
|
17
|
+
"opencode-plugin",
|
|
18
|
+
"openai",
|
|
19
|
+
"chatgpt",
|
|
20
|
+
"tui"
|
|
21
|
+
],
|
|
22
|
+
"files": [
|
|
23
|
+
"dist",
|
|
24
|
+
"README.md",
|
|
25
|
+
"LICENSE"
|
|
26
|
+
],
|
|
27
|
+
"exports": {
|
|
28
|
+
".": "./dist/index.js",
|
|
29
|
+
"./tui": "./dist/tui.js"
|
|
30
|
+
},
|
|
31
|
+
"scripts": {
|
|
32
|
+
"build": "tsup ./src/index.ts ./src/tui.tsx --format esm --out-dir dist --clean --external @opencode-ai/plugin --external @opencode-ai/plugin/tui --external @opentui/core --external @opentui/keymap --external @opentui/solid --external solid-js",
|
|
33
|
+
"test": "node --import tsx --test ./src/lib/openai-usage.test.ts",
|
|
34
|
+
"prepublishOnly": "npm run build"
|
|
35
|
+
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"@opencode-ai/plugin": "1.15.10",
|
|
38
|
+
"@opentui/core": "0.2.16",
|
|
39
|
+
"@opentui/keymap": "0.2.16",
|
|
40
|
+
"@opentui/solid": "0.2.16",
|
|
41
|
+
"solid-js": "1.9.12"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"tsup": "^8.3.6",
|
|
45
|
+
"tsx": "^4.19.2",
|
|
46
|
+
"typescript": "^5.8.3"
|
|
47
|
+
}
|
|
48
|
+
}
|