@slkiser/opencode-quota 1.3.0 → 1.4.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.md +171 -74
- package/dist/lib/firmware.d.ts +22 -0
- package/dist/lib/firmware.d.ts.map +1 -1
- package/dist/lib/firmware.js +96 -7
- package/dist/lib/firmware.js.map +1 -1
- package/dist/lib/markdown-table.d.ts +7 -0
- package/dist/lib/markdown-table.d.ts.map +1 -1
- package/dist/lib/markdown-table.js +76 -9
- package/dist/lib/markdown-table.js.map +1 -1
- package/dist/lib/quota-stats-format.d.ts.map +1 -1
- package/dist/lib/quota-stats-format.js +9 -3
- package/dist/lib/quota-stats-format.js.map +1 -1
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +76 -157
- package/dist/plugin.js.map +1 -1
- package/dist/providers/firmware.d.ts.map +1 -1
- package/dist/providers/firmware.js +40 -7
- package/dist/providers/firmware.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Opencode Quota
|
|
2
2
|
|
|
3
3
|
Quota and token tracking for OpenCode providers via Toasts and Commands with no LLM calls.
|
|
4
4
|
|
|
@@ -10,7 +10,7 @@ Quota and token tracking for OpenCode providers via Toasts and Commands with no
|
|
|
10
10
|
|
|
11
11
|
**Token Report Commands** - Track token usage and estimated costs across sessions.
|
|
12
12
|
|
|
13
|
-

|
|
14
14
|
|
|
15
15
|
## Installation
|
|
16
16
|
|
|
@@ -22,9 +22,7 @@ Add to your `opencode.json`:
|
|
|
22
22
|
}
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
Enable the providers you use:
|
|
25
|
+
Then enable the providers you use:
|
|
28
26
|
|
|
29
27
|
```jsonc
|
|
30
28
|
{
|
|
@@ -36,54 +34,72 @@ Enable the providers you use:
|
|
|
36
34
|
}
|
|
37
35
|
```
|
|
38
36
|
|
|
39
|
-
That's it. Toasts appear automatically after
|
|
37
|
+
That's it. Toasts appear automatically after assistant responses.
|
|
38
|
+
|
|
39
|
+
> **LLM Agents:** Ask your agent to install `https://github.com/slkiser/opencode-quota` with the providers you have enabled. See [LLM Agent Installation Instructions](#llm-agent-installation-instructions) for details.
|
|
40
40
|
|
|
41
41
|
## Commands
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
| Command | Description |
|
|
44
|
+
| ------------------------ | ------------------------------------------------ |
|
|
45
|
+
| `/quota` | Show quota toast (verbose) |
|
|
46
|
+
| `/quota_status` | Show diagnostics (config, providers, accounts) |
|
|
47
|
+
| | |
|
|
48
|
+
| `/tokens_today` | Tokens used today (calendar day) |
|
|
49
|
+
| `/tokens_daily` | Tokens used in last 24 hours |
|
|
50
|
+
| `/tokens_weekly` | Tokens used in last 7 days |
|
|
51
|
+
| `/tokens_monthly` | Tokens used in last 30 days |
|
|
52
|
+
| `/tokens_all` | Tokens used all time |
|
|
53
|
+
| `/tokens_session` | Tokens used in current session |
|
|
54
|
+
| `/tokens_between` | Tokens between two dates (YYYY-MM-DD) |
|
|
55
|
+
| | |
|
|
56
|
+
| `/firmware_reset_window` | Resets Firmware 5-hour window (requires confirm) |
|
|
44
57
|
|
|
45
|
-
|
|
46
|
-
| ----------------- | -------------------------------------------------------- |
|
|
47
|
-
| `/tokens_today` | Tokens used (Today) (/tokens_today) |
|
|
48
|
-
| `/tokens_daily` | Tokens used (Last 24 Hours) (/tokens_daily) |
|
|
49
|
-
| `/tokens_weekly` | Tokens used (Last 7 Days) (/tokens_weekly) |
|
|
50
|
-
| `/tokens_monthly` | Tokens used (Last 30 Days) (/tokens_monthly) |
|
|
51
|
-
| `/tokens_all` | Tokens used (All Time) (/tokens_all) |
|
|
52
|
-
| `/tokens_session` | Tokens used (Current Session) (/tokens_session) |
|
|
53
|
-
| `/tokens_between` | Tokens used (YYYY-MM-DD .. YYYY-MM-DD) (/tokens_between) |
|
|
58
|
+
## Supported Providers
|
|
54
59
|
|
|
55
|
-
|
|
60
|
+
| Provider | Config ID | Auth Source |
|
|
61
|
+
| ------------------ | -------------------- | --------------------------------------------- |
|
|
62
|
+
| GitHub Copilot | `copilot` | OpenCode auth (automatic) |
|
|
63
|
+
| OpenAI (Plus/Pro) | `openai` | OpenCode auth (automatic) |
|
|
64
|
+
| Firmware AI | `firmware` | OpenCode auth or API key |
|
|
65
|
+
| Chutes AI | `chutes` | OpenCode auth or API key |
|
|
66
|
+
| Google Antigravity | `google-antigravity` | Multi-account via `opencode-antigravity-auth` |
|
|
56
67
|
|
|
57
|
-
|
|
58
|
-
| --------------- | --------------------------------- |
|
|
59
|
-
| `/quota` | Quota Toast (Verbose) (/quota) |
|
|
60
|
-
| `/quota_status` | Quota Diagnostics (/quota_status) |
|
|
68
|
+
### Provider-Specific Setup
|
|
61
69
|
|
|
62
|
-
|
|
70
|
+
<details>
|
|
71
|
+
<summary><strong>GitHub Copilot</strong> (usually no setup needed)</summary>
|
|
63
72
|
|
|
64
|
-
|
|
73
|
+
Copilot works automatically if OpenCode has Copilot configured and logged in.
|
|
65
74
|
|
|
66
|
-
|
|
67
|
-
- `/quota_daily` -> `/tokens_daily`
|
|
68
|
-
- `/quota_weekly` -> `/tokens_weekly`
|
|
69
|
-
- `/quota_monthly` -> `/tokens_monthly`
|
|
70
|
-
- `/quota_all` -> `/tokens_all`
|
|
71
|
-
- `/quota_session` -> `/tokens_session`
|
|
72
|
-
- `/quota_between` -> `/tokens_between`
|
|
75
|
+
**Optional:** For more reliable quota reporting, provide a fine-grained PAT:
|
|
73
76
|
|
|
74
|
-
|
|
77
|
+
1. Create a fine-grained PAT at GitHub with **Account permissions > Plan > Read**
|
|
78
|
+
2. Create `~/.config/opencode/copilot-quota-token.json`:
|
|
75
79
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
80
|
+
```json
|
|
81
|
+
{
|
|
82
|
+
"token": "github_pat_...",
|
|
83
|
+
"username": "your-username",
|
|
84
|
+
"tier": "pro"
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Tier options: `free`, `pro`, `pro+`, `business`, `enterprise`
|
|
89
|
+
|
|
90
|
+
</details>
|
|
91
|
+
|
|
92
|
+
<details>
|
|
93
|
+
<summary><strong>OpenAI</strong> (no setup needed)</summary>
|
|
94
|
+
|
|
95
|
+
OpenAI works automatically if OpenCode has OpenAI/ChatGPT configured.
|
|
96
|
+
|
|
97
|
+
</details>
|
|
83
98
|
|
|
84
|
-
|
|
99
|
+
<details>
|
|
100
|
+
<summary><strong>Firmware AI</strong></summary>
|
|
85
101
|
|
|
86
|
-
|
|
102
|
+
Works automatically if OpenCode has Firmware configured. Alternatively, provide an API key:
|
|
87
103
|
|
|
88
104
|
```jsonc
|
|
89
105
|
{
|
|
@@ -102,11 +118,16 @@ Firmware works automatically if OpenCode has Firmware configured. Alternatively,
|
|
|
102
118
|
}
|
|
103
119
|
```
|
|
104
120
|
|
|
105
|
-
The `apiKey` field supports
|
|
121
|
+
The `apiKey` field supports `{env:VAR_NAME}` syntax or a direct key.
|
|
106
122
|
|
|
107
|
-
|
|
123
|
+
**Firmware-specific command:** Use `/firmware_reset_window confirm` to reset your 5-hour spending window (consumes 1 of 2 weekly resets). Running without `confirm` shows a warning first.
|
|
108
124
|
|
|
109
|
-
|
|
125
|
+
</details>
|
|
126
|
+
|
|
127
|
+
<details>
|
|
128
|
+
<summary><strong>Chutes AI</strong></summary>
|
|
129
|
+
|
|
130
|
+
Works automatically if OpenCode has Chutes configured. Alternatively, provide an API key:
|
|
110
131
|
|
|
111
132
|
```jsonc
|
|
112
133
|
{
|
|
@@ -125,64 +146,140 @@ Chutes works automatically if OpenCode has Chutes configured. Alternatively, you
|
|
|
125
146
|
}
|
|
126
147
|
```
|
|
127
148
|
|
|
128
|
-
|
|
149
|
+
</details>
|
|
129
150
|
|
|
130
|
-
|
|
151
|
+
<details>
|
|
152
|
+
<summary><strong>Google Antigravity</strong></summary>
|
|
131
153
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
1. Create a fine-grained PAT at GitHub with **Account permissions > Plan > Read**
|
|
135
|
-
2. Create `~/.config/opencode/copilot-quota-token.json`:
|
|
154
|
+
Requires the `opencode-antigravity-auth` plugin for multi-account support:
|
|
136
155
|
|
|
137
156
|
```json
|
|
138
157
|
{
|
|
139
|
-
"
|
|
140
|
-
"username": "your-username",
|
|
141
|
-
"tier": "pro"
|
|
158
|
+
"plugin": ["opencode-antigravity-auth", "@slkiser/opencode-quota"]
|
|
142
159
|
}
|
|
143
160
|
```
|
|
144
161
|
|
|
145
|
-
|
|
162
|
+
Account credentials are stored in `~/.config/opencode/antigravity-accounts.json`.
|
|
146
163
|
|
|
147
|
-
|
|
164
|
+
</details>
|
|
148
165
|
|
|
149
166
|
## Configuration Reference
|
|
150
167
|
|
|
151
168
|
All options go under `experimental.quotaToast` in `opencode.json`:
|
|
152
169
|
|
|
153
|
-
| Option | Default | Description
|
|
154
|
-
| ------------------- | ------------ |
|
|
155
|
-
| `enabled` | `true` | Enable/disable plugin
|
|
156
|
-
| `enableToast` | `true` | Show popup toasts
|
|
157
|
-
| `enabledProviders` | `[]` | Provider
|
|
158
|
-
| `minIntervalMs` | `300000` |
|
|
159
|
-
| `toastDurationMs` | `9000` |
|
|
160
|
-
| `onlyCurrentModel` | `false` | Only show current model
|
|
161
|
-
| `showSessionTokens` | `true` | Show per-model input/output tokens in toast
|
|
162
|
-
| `googleModels` | `["CLAUDE"]` | Google models: `CLAUDE`, `G3PRO`, `G3FLASH`
|
|
163
|
-
| `debug` | `false` | Show debug info in toasts
|
|
170
|
+
| Option | Default | Description |
|
|
171
|
+
| ------------------- | ------------ | ----------------------------------------------- |
|
|
172
|
+
| `enabled` | `true` | Enable/disable plugin |
|
|
173
|
+
| `enableToast` | `true` | Show popup toasts |
|
|
174
|
+
| `enabledProviders` | `[]` | Provider IDs to query (see table above) |
|
|
175
|
+
| `minIntervalMs` | `300000` | Minimum ms between API fetches (default: 5 min) |
|
|
176
|
+
| `toastDurationMs` | `9000` | How long toasts display (ms) |
|
|
177
|
+
| `onlyCurrentModel` | `false` | Only show quota for the current model |
|
|
178
|
+
| `showSessionTokens` | `true` | Show per-model input/output tokens in toast |
|
|
179
|
+
| `googleModels` | `["CLAUDE"]` | Google models: `CLAUDE`, `G3PRO`, `G3FLASH` |
|
|
180
|
+
| `debug` | `false` | Show debug info in toasts |
|
|
181
|
+
|
|
182
|
+
## Troubleshooting
|
|
183
|
+
|
|
184
|
+
Toast not appearing? Run `/quota_status` to check config and provider availability.
|
|
164
185
|
|
|
165
|
-
|
|
186
|
+
---
|
|
166
187
|
|
|
167
|
-
|
|
188
|
+
<details id="llm-agent-installation-instructions">
|
|
189
|
+
<summary><strong>LLM Agent Installation Instructions</strong></summary>
|
|
190
|
+
|
|
191
|
+
### For LLM Agents: How to Install and Configure
|
|
192
|
+
|
|
193
|
+
This section provides instructions for LLM agents to install and configure `opencode-quota` based on the user's current OpenCode setup.
|
|
194
|
+
|
|
195
|
+
#### Step 1: Check Current Configuration
|
|
196
|
+
|
|
197
|
+
First, determine what providers the user has connected. You can:
|
|
198
|
+
|
|
199
|
+
1. Read the user's `opencode.json` (typically at `~/.config/opencode/opencode.json`)
|
|
200
|
+
2. Run `/connected` in OpenCode to see active providers
|
|
201
|
+
3. Ask the user which providers they use
|
|
202
|
+
|
|
203
|
+
#### Step 2: Install the Plugin
|
|
204
|
+
|
|
205
|
+
Add the plugin to the user's `opencode.json`:
|
|
206
|
+
|
|
207
|
+
```json
|
|
208
|
+
{
|
|
209
|
+
"plugin": ["@slkiser/opencode-quota"]
|
|
210
|
+
}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
If the user already has plugins, append to the existing array.
|
|
214
|
+
|
|
215
|
+
#### Step 3: Configure Providers
|
|
216
|
+
|
|
217
|
+
Based on the user's connected providers, add the appropriate `enabledProviders`:
|
|
168
218
|
|
|
169
219
|
```jsonc
|
|
170
220
|
{
|
|
171
|
-
"plugin": ["@slkiser/opencode-quota"],
|
|
172
221
|
"experimental": {
|
|
173
222
|
"quotaToast": {
|
|
174
|
-
"enabledProviders": [
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
223
|
+
"enabledProviders": [
|
|
224
|
+
// Add only the providers the user has configured:
|
|
225
|
+
// "copilot" - if using GitHub Copilot
|
|
226
|
+
// "openai" - if using OpenAI/ChatGPT
|
|
227
|
+
// "firmware" - if using Firmware AI
|
|
228
|
+
// "chutes" - if using Chutes AI
|
|
229
|
+
// "google-antigravity" - if using Google Antigravity (requires opencode-antigravity-auth)
|
|
230
|
+
],
|
|
178
231
|
},
|
|
179
232
|
},
|
|
180
233
|
}
|
|
181
234
|
```
|
|
182
235
|
|
|
183
|
-
|
|
236
|
+
#### Provider Detection Guide
|
|
184
237
|
|
|
185
|
-
|
|
238
|
+
| If user's config has... | Add to enabledProviders |
|
|
239
|
+
| ------------------------------------------- | ----------------------- |
|
|
240
|
+
| `github-copilot` provider or Copilot models | `"copilot"` |
|
|
241
|
+
| `openai` / `chatgpt` provider | `"openai"` |
|
|
242
|
+
| `firmware` / `firmware-ai` provider | `"firmware"` |
|
|
243
|
+
| `chutes` provider | `"chutes"` |
|
|
244
|
+
| `google` provider with antigravity models | `"google-antigravity"` |
|
|
245
|
+
| `opencode-antigravity-auth` in plugins | `"google-antigravity"` |
|
|
246
|
+
|
|
247
|
+
#### Example: Full Configuration
|
|
248
|
+
|
|
249
|
+
For a user with Copilot and Google Antigravity:
|
|
250
|
+
|
|
251
|
+
```jsonc
|
|
252
|
+
{
|
|
253
|
+
"plugin": [
|
|
254
|
+
"opencode-antigravity-auth", // Required for google-antigravity
|
|
255
|
+
"@slkiser/opencode-quota",
|
|
256
|
+
],
|
|
257
|
+
"experimental": {
|
|
258
|
+
"quotaToast": {
|
|
259
|
+
"enabledProviders": ["copilot", "google-antigravity"],
|
|
260
|
+
"googleModels": ["CLAUDE"], // Options: CLAUDE, G3PRO, G3FLASH
|
|
261
|
+
"minIntervalMs": 300000, // 5 minutes between fetches
|
|
262
|
+
"toastDurationMs": 9000, // Toast shows for 9 seconds
|
|
263
|
+
},
|
|
264
|
+
},
|
|
265
|
+
}
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
#### Step 4: Verify Installation
|
|
269
|
+
|
|
270
|
+
After configuration, instruct the user to:
|
|
271
|
+
|
|
272
|
+
1. Restart OpenCode (or reload the window)
|
|
273
|
+
2. Run `/quota_status` to verify providers are detected
|
|
274
|
+
3. Run `/quota` to manually trigger a toast
|
|
275
|
+
|
|
276
|
+
#### Common Issues
|
|
277
|
+
|
|
278
|
+
- **Toast not showing**: Run `/quota_status` to diagnose
|
|
279
|
+
- **Google Antigravity not working**: Ensure `opencode-antigravity-auth` plugin is installed and accounts are configured
|
|
280
|
+
- **Copilot quota unreliable**: Consider setting up a fine-grained PAT (see Provider-Specific Setup above)
|
|
281
|
+
|
|
282
|
+
</details>
|
|
186
283
|
|
|
187
284
|
## License
|
|
188
285
|
|
package/dist/lib/firmware.d.ts
CHANGED
|
@@ -5,12 +5,34 @@
|
|
|
5
5
|
* and queries: https://app.firmware.ai/api/v1/quota
|
|
6
6
|
*/
|
|
7
7
|
import type { QuotaError } from "./types.js";
|
|
8
|
+
/** Single window quota info */
|
|
9
|
+
export interface FirmwareWindowQuota {
|
|
10
|
+
percentRemaining: number;
|
|
11
|
+
resetTimeIso?: string;
|
|
12
|
+
}
|
|
8
13
|
export type FirmwareResult = {
|
|
9
14
|
success: true;
|
|
15
|
+
/** Back-compat: worst of 5h window and weekly (for classic toast) */
|
|
10
16
|
percentRemaining: number;
|
|
11
17
|
resetTimeIso?: string;
|
|
18
|
+
/** Individual windows for grouped display */
|
|
19
|
+
windows: {
|
|
20
|
+
window: FirmwareWindowQuota;
|
|
21
|
+
weekly: FirmwareWindowQuota;
|
|
22
|
+
};
|
|
23
|
+
/** Manual resets available this week (0-2) */
|
|
24
|
+
windowResetsRemaining: number;
|
|
25
|
+
} | QuotaError | null;
|
|
26
|
+
export type FirmwareResetWindowResult = {
|
|
27
|
+
success: true;
|
|
28
|
+
windowResetsRemaining?: number;
|
|
12
29
|
} | QuotaError | null;
|
|
13
30
|
export declare function hasFirmwareApiKeyConfigured(): Promise<boolean>;
|
|
14
31
|
export { getFirmwareKeyDiagnostics, type FirmwareKeySource } from "./firmware-config.js";
|
|
15
32
|
export declare function queryFirmwareQuota(): Promise<FirmwareResult>;
|
|
33
|
+
/**
|
|
34
|
+
* Manually reset the 5-hour spending window.
|
|
35
|
+
* Consumes one of the 2 weekly resets available.
|
|
36
|
+
*/
|
|
37
|
+
export declare function resetFirmwareQuotaWindow(): Promise<FirmwareResetWindowResult>;
|
|
16
38
|
//# sourceMappingURL=firmware.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"firmware.d.ts","sourceRoot":"","sources":["../../src/lib/firmware.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"firmware.d.ts","sourceRoot":"","sources":["../../src/lib/firmware.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAkB7C,+BAA+B;AAC/B,MAAM,WAAW,mBAAmB;IAClC,gBAAgB,EAAE,MAAM,CAAC;IACzB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AA6BD,MAAM,MAAM,cAAc,GACtB;IACE,OAAO,EAAE,IAAI,CAAC;IACd,qEAAqE;IACrE,gBAAgB,EAAE,MAAM,CAAC;IACzB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,6CAA6C;IAC7C,OAAO,EAAE;QACP,MAAM,EAAE,mBAAmB,CAAC;QAC5B,MAAM,EAAE,mBAAmB,CAAC;KAC7B,CAAC;IACF,8CAA8C;IAC9C,qBAAqB,EAAE,MAAM,CAAC;CAC/B,GACD,UAAU,GACV,IAAI,CAAC;AAET,MAAM,MAAM,yBAAyB,GACjC;IAAE,OAAO,EAAE,IAAI,CAAC;IAAC,qBAAqB,CAAC,EAAE,MAAM,CAAA;CAAE,GACjD,UAAU,GACV,IAAI,CAAC;AAKT,wBAAsB,2BAA2B,IAAI,OAAO,CAAC,OAAO,CAAC,CAEpE;AAED,OAAO,EAAE,yBAAyB,EAAE,KAAK,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AAEzF,wBAAsB,kBAAkB,IAAI,OAAO,CAAC,cAAc,CAAC,CA2ElE;AAED;;;GAGG;AACH,wBAAsB,wBAAwB,IAAI,OAAO,CAAC,yBAAyB,CAAC,CAsDnF"}
|
package/dist/lib/firmware.js
CHANGED
|
@@ -11,6 +11,15 @@ function clampPercent(n) {
|
|
|
11
11
|
return 0;
|
|
12
12
|
return Math.max(0, Math.min(100, Math.round(n)));
|
|
13
13
|
}
|
|
14
|
+
/**
|
|
15
|
+
* Strip control characters (ANSI escapes, etc.) from error text
|
|
16
|
+
* to prevent terminal injection when displayed in TUI output.
|
|
17
|
+
*/
|
|
18
|
+
function sanitizeErrorText(text) {
|
|
19
|
+
// Remove ANSI escape sequences and other control characters (except newline/tab)
|
|
20
|
+
// eslint-disable-next-line no-control-regex
|
|
21
|
+
return text.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]|\x1B\[[0-9;]*[A-Za-z]/g, "");
|
|
22
|
+
}
|
|
14
23
|
async function readFirmwareAuth() {
|
|
15
24
|
const result = await resolveFirmwareApiKey();
|
|
16
25
|
if (!result)
|
|
@@ -18,6 +27,7 @@ async function readFirmwareAuth() {
|
|
|
18
27
|
return { type: "api", key: result.key, source: result.source };
|
|
19
28
|
}
|
|
20
29
|
const FIRMWARE_QUOTA_URL = "https://app.firmware.ai/api/v1/quota";
|
|
30
|
+
const FIRMWARE_RESET_WINDOW_URL = "https://app.firmware.ai/api/v1/quota/reset-window";
|
|
21
31
|
export async function hasFirmwareApiKeyConfigured() {
|
|
22
32
|
return await hasFirmwareApiKey();
|
|
23
33
|
}
|
|
@@ -38,18 +48,97 @@ export async function queryFirmwareQuota() {
|
|
|
38
48
|
const text = await resp.text();
|
|
39
49
|
return {
|
|
40
50
|
success: false,
|
|
41
|
-
error: `Firmware API error ${resp.status}: ${text.slice(0, 120)}`,
|
|
51
|
+
error: `Firmware API error ${resp.status}: ${sanitizeErrorText(text.slice(0, 120))}`,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
const data = (await resp.json());
|
|
55
|
+
// Parse 5-hour window
|
|
56
|
+
const windowUsed = typeof data.windowUsed === "number" ? data.windowUsed : NaN;
|
|
57
|
+
const windowPercentRemaining = clampPercent(100 - windowUsed * 100);
|
|
58
|
+
const windowResetIso = typeof data.windowReset === "string" && data.windowReset.length > 0
|
|
59
|
+
? data.windowReset
|
|
60
|
+
: undefined;
|
|
61
|
+
// Parse weekly
|
|
62
|
+
const weeklyUsed = typeof data.weeklyUsed === "number" ? data.weeklyUsed : NaN;
|
|
63
|
+
const weeklyPercentRemaining = clampPercent(100 - weeklyUsed * 100);
|
|
64
|
+
const weeklyResetIso = typeof data.weeklyReset === "string" && data.weeklyReset.length > 0
|
|
65
|
+
? data.weeklyReset
|
|
66
|
+
: undefined;
|
|
67
|
+
// Parse resets remaining (use trunc to avoid surprising rounding)
|
|
68
|
+
const windowResetsRemaining = typeof data.windowResetsRemaining === "number"
|
|
69
|
+
? Math.max(0, Math.min(2, Math.trunc(data.windowResetsRemaining)))
|
|
70
|
+
: 0;
|
|
71
|
+
// Back-compat: use worst window for classic display
|
|
72
|
+
const windowQuota = {
|
|
73
|
+
percentRemaining: windowPercentRemaining,
|
|
74
|
+
resetTimeIso: windowResetIso,
|
|
75
|
+
};
|
|
76
|
+
const weeklyQuota = {
|
|
77
|
+
percentRemaining: weeklyPercentRemaining,
|
|
78
|
+
resetTimeIso: weeklyResetIso,
|
|
79
|
+
};
|
|
80
|
+
// Worst window for classic mode
|
|
81
|
+
const isWindowWorse = windowPercentRemaining <= weeklyPercentRemaining;
|
|
82
|
+
const worst = isWindowWorse ? windowQuota : weeklyQuota;
|
|
83
|
+
return {
|
|
84
|
+
success: true,
|
|
85
|
+
percentRemaining: worst.percentRemaining,
|
|
86
|
+
resetTimeIso: worst.resetTimeIso,
|
|
87
|
+
windows: {
|
|
88
|
+
window: windowQuota,
|
|
89
|
+
weekly: weeklyQuota,
|
|
90
|
+
},
|
|
91
|
+
windowResetsRemaining,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
catch (err) {
|
|
95
|
+
return {
|
|
96
|
+
success: false,
|
|
97
|
+
error: err instanceof Error ? err.message : String(err),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Manually reset the 5-hour spending window.
|
|
103
|
+
* Consumes one of the 2 weekly resets available.
|
|
104
|
+
*/
|
|
105
|
+
export async function resetFirmwareQuotaWindow() {
|
|
106
|
+
const auth = await readFirmwareAuth();
|
|
107
|
+
if (!auth)
|
|
108
|
+
return null;
|
|
109
|
+
try {
|
|
110
|
+
const resp = await fetchWithTimeout(FIRMWARE_RESET_WINDOW_URL, {
|
|
111
|
+
method: "POST",
|
|
112
|
+
headers: {
|
|
113
|
+
Authorization: `Bearer ${auth.key}`,
|
|
114
|
+
"User-Agent": "OpenCode-Quota-Toast/1.0",
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
if (!resp.ok) {
|
|
118
|
+
const text = await resp.text();
|
|
119
|
+
// Try to parse error JSON for better message
|
|
120
|
+
try {
|
|
121
|
+
const errorData = JSON.parse(text);
|
|
122
|
+
if (errorData.message) {
|
|
123
|
+
return {
|
|
124
|
+
success: false,
|
|
125
|
+
error: sanitizeErrorText(errorData.message),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
// Not JSON, use raw text
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
success: false,
|
|
134
|
+
error: `Firmware API error ${resp.status}: ${sanitizeErrorText(text.slice(0, 120))}`,
|
|
42
135
|
};
|
|
43
136
|
}
|
|
137
|
+
// Parse success response
|
|
44
138
|
const data = (await resp.json());
|
|
45
|
-
// Firmware returns used ratio [0..1]. We convert to remaining %.
|
|
46
|
-
const used = typeof data.used === "number" ? data.used : NaN;
|
|
47
|
-
const percentRemaining = clampPercent(100 - used * 100);
|
|
48
|
-
const resetIso = typeof data.reset === "string" && data.reset.length > 0 ? data.reset : undefined;
|
|
49
139
|
return {
|
|
50
140
|
success: true,
|
|
51
|
-
|
|
52
|
-
resetTimeIso: resetIso,
|
|
141
|
+
windowResetsRemaining: typeof data.windowResetsRemaining === "number" ? data.windowResetsRemaining : undefined,
|
|
53
142
|
};
|
|
54
143
|
}
|
|
55
144
|
catch (err) {
|
package/dist/lib/firmware.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"firmware.js","sourceRoot":"","sources":["../../src/lib/firmware.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,EACL,qBAAqB,EACrB,iBAAiB,GAGlB,MAAM,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"firmware.js","sourceRoot":"","sources":["../../src/lib/firmware.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
type Align = "left" | "right" | "center";
|
|
2
|
+
/**
|
|
3
|
+
* Width measurement mode:
|
|
4
|
+
* - "raw": Use string length (grapheme count)
|
|
5
|
+
* - "markdown-conceal": Strip markdown syntax for width calculation (for TUI concealment mode)
|
|
6
|
+
*/
|
|
7
|
+
export type WidthMode = "raw" | "markdown-conceal";
|
|
2
8
|
export declare function renderMarkdownTable(params: {
|
|
3
9
|
headers: string[];
|
|
4
10
|
rows: string[][];
|
|
5
11
|
aligns?: Align[];
|
|
12
|
+
widthMode?: WidthMode;
|
|
6
13
|
}): string;
|
|
7
14
|
export {};
|
|
8
15
|
//# sourceMappingURL=markdown-table.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"markdown-table.d.ts","sourceRoot":"","sources":["../../src/lib/markdown-table.ts"],"names":[],"mappings":"AAAA,KAAK,KAAK,GAAG,MAAM,GAAG,OAAO,GAAG,QAAQ,CAAC;
|
|
1
|
+
{"version":3,"file":"markdown-table.d.ts","sourceRoot":"","sources":["../../src/lib/markdown-table.ts"],"names":[],"mappings":"AAAA,KAAK,KAAK,GAAG,MAAM,GAAG,OAAO,GAAG,QAAQ,CAAC;AAEzC;;;;GAIG;AACH,MAAM,MAAM,SAAS,GAAG,KAAK,GAAG,kBAAkB,CAAC;AAgGnD,wBAAgB,mBAAmB,CAAC,MAAM,EAAE;IAC1C,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,IAAI,EAAE,MAAM,EAAE,EAAE,CAAC;IACjB,MAAM,CAAC,EAAE,KAAK,EAAE,CAAC;IACjB,SAAS,CAAC,EAAE,SAAS,CAAC;CACvB,GAAG,MAAM,CAqCT"}
|
|
@@ -1,9 +1,77 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Use Intl.Segmenter for grapheme-aware width measurement when available.
|
|
3
|
+
* Falls back to Array.from for code point count.
|
|
4
|
+
*/
|
|
5
|
+
const GRAPHEME_SEGMENTER = typeof Intl !== "undefined" && typeof Intl.Segmenter === "function"
|
|
6
|
+
? new Intl.Segmenter(undefined, { granularity: "grapheme" })
|
|
7
|
+
: null;
|
|
8
|
+
/**
|
|
9
|
+
* Measure width using grapheme clusters (preferred) or code points (fallback).
|
|
10
|
+
*/
|
|
11
|
+
function measureWidth(text) {
|
|
2
12
|
const s = text ?? "";
|
|
3
|
-
|
|
4
|
-
|
|
13
|
+
if (!s)
|
|
14
|
+
return 0;
|
|
15
|
+
if (GRAPHEME_SEGMENTER) {
|
|
16
|
+
let n = 0;
|
|
17
|
+
for (const _ of GRAPHEME_SEGMENTER.segment(s))
|
|
18
|
+
n++;
|
|
19
|
+
return n;
|
|
20
|
+
}
|
|
21
|
+
return Array.from(s).length;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Convert markdown text to visual representation for width calculation.
|
|
25
|
+
* Strips markdown syntax that is hidden in concealment mode.
|
|
26
|
+
*/
|
|
27
|
+
function toVisualTextForWidth(text) {
|
|
28
|
+
// Treat escaped markdown pipes as a single visual char.
|
|
29
|
+
let t = (text ?? "").replace(/\\\|/g, "|");
|
|
30
|
+
// Protect inline code so markdown inside `code` remains literal.
|
|
31
|
+
const codeSpans = [];
|
|
32
|
+
t = t.replace(/`([^`]+?)`/g, (_m, content) => {
|
|
33
|
+
codeSpans.push(content);
|
|
34
|
+
return `\x00CODE${codeSpans.length - 1}\x00`;
|
|
35
|
+
});
|
|
36
|
+
let prev = "";
|
|
37
|
+
while (t !== prev) {
|
|
38
|
+
prev = t;
|
|
39
|
+
t = t
|
|
40
|
+
// ***bold+italic*** -> text
|
|
41
|
+
.replace(/\*\*\*(.+?)\*\*\*/g, "$1")
|
|
42
|
+
// **bold** -> text
|
|
43
|
+
.replace(/\*\*(.+?)\*\*/g, "$1")
|
|
44
|
+
// *italic* -> text
|
|
45
|
+
.replace(/\*(.+?)\*/g, "$1")
|
|
46
|
+
// ~~strikethrough~~ -> text
|
|
47
|
+
.replace(/~~(.+?)~~/g, "$1")
|
|
48
|
+
//  -> alt
|
|
49
|
+
.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, "$1")
|
|
50
|
+
// [text](url) -> text (url)
|
|
51
|
+
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, "$1 ($2)");
|
|
52
|
+
}
|
|
53
|
+
// Restore inline code contents (without backticks)
|
|
54
|
+
t = t.replace(/\x00CODE(\d+)\x00/g, (_m, idx) => {
|
|
55
|
+
const i = Number(idx);
|
|
56
|
+
return Number.isFinite(i) ? (codeSpans[i] ?? "") : "";
|
|
57
|
+
});
|
|
58
|
+
return t;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Calculate cell width based on the specified mode.
|
|
62
|
+
*/
|
|
63
|
+
function cellWidth(text, widthMode) {
|
|
64
|
+
if (widthMode === "markdown-conceal") {
|
|
65
|
+
return measureWidth(toVisualTextForWidth(text));
|
|
66
|
+
}
|
|
67
|
+
return measureWidth(text);
|
|
68
|
+
}
|
|
69
|
+
function padCell(text, width, align, widthMode) {
|
|
70
|
+
const s = text ?? "";
|
|
71
|
+
const w = cellWidth(s, widthMode);
|
|
72
|
+
if (w >= width)
|
|
5
73
|
return s;
|
|
6
|
-
const pad = width -
|
|
74
|
+
const pad = width - w;
|
|
7
75
|
if (align === "right")
|
|
8
76
|
return " ".repeat(pad) + s;
|
|
9
77
|
if (align === "center") {
|
|
@@ -19,6 +87,7 @@ function escapeCell(text) {
|
|
|
19
87
|
}
|
|
20
88
|
export function renderMarkdownTable(params) {
|
|
21
89
|
const aligns = params.aligns ?? params.headers.map(() => "left");
|
|
90
|
+
const widthMode = params.widthMode ?? "raw";
|
|
22
91
|
const colCount = params.headers.length;
|
|
23
92
|
const safeRows = params.rows.map((r) => {
|
|
24
93
|
const out = [];
|
|
@@ -27,15 +96,13 @@ export function renderMarkdownTable(params) {
|
|
|
27
96
|
return out;
|
|
28
97
|
});
|
|
29
98
|
const headerCells = params.headers.map((h) => escapeCell(h));
|
|
30
|
-
const widths = headerCells.map((h) => Math.max(3, h
|
|
99
|
+
const widths = headerCells.map((h) => Math.max(3, cellWidth(h, widthMode)));
|
|
31
100
|
for (const row of safeRows) {
|
|
32
101
|
for (let i = 0; i < colCount; i++) {
|
|
33
|
-
widths[i] = Math.max(widths[i], (row[i] ?? "")
|
|
102
|
+
widths[i] = Math.max(widths[i], cellWidth(row[i] ?? "", widthMode));
|
|
34
103
|
}
|
|
35
104
|
}
|
|
36
|
-
const fmtRow = (cells) => `| ${cells
|
|
37
|
-
.map((c, i) => padCell(c, widths[i], aligns[i] ?? "left"))
|
|
38
|
-
.join(" | ")} |`;
|
|
105
|
+
const fmtRow = (cells) => `| ${cells.map((c, i) => padCell(c, widths[i], aligns[i] ?? "left", widthMode)).join(" | ")} |`;
|
|
39
106
|
const sep = `| ${widths
|
|
40
107
|
.map((w, i) => {
|
|
41
108
|
const a = aligns[i] ?? "left";
|