@mdrv/opencode-quota 262.0.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 +189 -0
- package/bin/copilot-quota.ts +374 -0
- package/bin/glm-quota.ts +467 -0
- package/bin/install.js +439 -0
- package/bin/kimi-quota.ts +314 -0
- package/dist/bin/copilot-quota.d.ts +8 -0
- package/dist/bin/copilot-quota.d.ts.map +1 -0
- package/dist/bin/copilot-quota.js +298 -0
- package/dist/bin/copilot-quota.js.map +1 -0
- package/dist/bin/glm-quota.d.ts +8 -0
- package/dist/bin/glm-quota.d.ts.map +1 -0
- package/dist/bin/glm-quota.js +367 -0
- package/dist/bin/glm-quota.js.map +1 -0
- package/dist/bin/kimi-quota.d.ts +3 -0
- package/dist/bin/kimi-quota.d.ts.map +1 -0
- package/dist/bin/kimi-quota.js +241 -0
- package/dist/bin/kimi-quota.js.map +1 -0
- package/dist/src/api/client.d.ts +76 -0
- package/dist/src/api/client.d.ts.map +1 -0
- package/dist/src/api/client.js +203 -0
- package/dist/src/api/client.js.map +1 -0
- package/dist/src/api/endpoints.d.ts +22 -0
- package/dist/src/api/endpoints.d.ts.map +1 -0
- package/dist/src/api/endpoints.js +41 -0
- package/dist/src/api/endpoints.js.map +1 -0
- package/dist/src/api/platforms.d.ts +20 -0
- package/dist/src/api/platforms.d.ts.map +1 -0
- package/dist/src/api/platforms.js +38 -0
- package/dist/src/api/platforms.js.map +1 -0
- package/dist/src/index.d.ts +10 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +723 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/shared/logging.d.ts +7 -0
- package/dist/src/shared/logging.d.ts.map +1 -0
- package/dist/src/shared/logging.js +29 -0
- package/dist/src/shared/logging.js.map +1 -0
- package/dist/src/utils/box-constants.d.ts +43 -0
- package/dist/src/utils/box-constants.d.ts.map +1 -0
- package/dist/src/utils/box-constants.js +43 -0
- package/dist/src/utils/box-constants.js.map +1 -0
- package/dist/src/utils/date-formatter.d.ts +17 -0
- package/dist/src/utils/date-formatter.d.ts.map +1 -0
- package/dist/src/utils/date-formatter.js +33 -0
- package/dist/src/utils/date-formatter.js.map +1 -0
- package/dist/src/utils/error-formatter.d.ts +17 -0
- package/dist/src/utils/error-formatter.d.ts.map +1 -0
- package/dist/src/utils/error-formatter.js +60 -0
- package/dist/src/utils/error-formatter.js.map +1 -0
- package/dist/src/utils/progress-bar.d.ts +35 -0
- package/dist/src/utils/progress-bar.d.ts.map +1 -0
- package/dist/src/utils/progress-bar.js +43 -0
- package/dist/src/utils/progress-bar.js.map +1 -0
- package/dist/src/utils/reset-timer.d.ts +11 -0
- package/dist/src/utils/reset-timer.d.ts.map +1 -0
- package/dist/src/utils/reset-timer.js +32 -0
- package/dist/src/utils/reset-timer.js.map +1 -0
- package/dist/src/utils/time-window.d.ts +30 -0
- package/dist/src/utils/time-window.d.ts.map +1 -0
- package/dist/src/utils/time-window.js +34 -0
- package/dist/src/utils/time-window.js.map +1 -0
- package/dist/tests/error-handling/api-errors.test.d.ts +7 -0
- package/dist/tests/error-handling/api-errors.test.d.ts.map +1 -0
- package/dist/tests/error-handling/api-errors.test.js +110 -0
- package/dist/tests/error-handling/api-errors.test.js.map +1 -0
- package/dist/tests/error-handling/auth-errors.test.d.ts +7 -0
- package/dist/tests/error-handling/auth-errors.test.d.ts.map +1 -0
- package/dist/tests/error-handling/auth-errors.test.js +110 -0
- package/dist/tests/error-handling/auth-errors.test.js.map +1 -0
- package/dist/tests/error-handling/network-errors.test.d.ts +7 -0
- package/dist/tests/error-handling/network-errors.test.d.ts.map +1 -0
- package/dist/tests/error-handling/network-errors.test.js +94 -0
- package/dist/tests/error-handling/network-errors.test.js.map +1 -0
- package/dist/tests/error-handling/parse-errors.test.d.ts +7 -0
- package/dist/tests/error-handling/parse-errors.test.d.ts.map +1 -0
- package/dist/tests/error-handling/parse-errors.test.js +87 -0
- package/dist/tests/error-handling/parse-errors.test.js.map +1 -0
- package/dist/tests/error-handling/token-sanitization.test.d.ts +2 -0
- package/dist/tests/error-handling/token-sanitization.test.d.ts.map +1 -0
- package/dist/tests/error-handling/token-sanitization.test.js +59 -0
- package/dist/tests/error-handling/token-sanitization.test.js.map +1 -0
- package/dist/tests/functional/date-formatter.test.d.ts +5 -0
- package/dist/tests/functional/date-formatter.test.d.ts.map +1 -0
- package/dist/tests/functional/date-formatter.test.js +46 -0
- package/dist/tests/functional/date-formatter.test.js.map +1 -0
- package/dist/tests/functional/progress-bar.test.d.ts +5 -0
- package/dist/tests/functional/progress-bar.test.d.ts.map +1 -0
- package/dist/tests/functional/progress-bar.test.js +82 -0
- package/dist/tests/functional/progress-bar.test.js.map +1 -0
- package/dist/tests/functional/reset-timer.test.d.ts +6 -0
- package/dist/tests/functional/reset-timer.test.d.ts.map +1 -0
- package/dist/tests/functional/reset-timer.test.js +67 -0
- package/dist/tests/functional/reset-timer.test.js.map +1 -0
- package/dist/tests/functional/time-window.test.d.ts +5 -0
- package/dist/tests/functional/time-window.test.d.ts.map +1 -0
- package/dist/tests/functional/time-window.test.js +46 -0
- package/dist/tests/functional/time-window.test.js.map +1 -0
- package/dist/tests/integration/box-alignment.test.d.ts +8 -0
- package/dist/tests/integration/box-alignment.test.d.ts.map +1 -0
- package/dist/tests/integration/box-alignment.test.js +238 -0
- package/dist/tests/integration/box-alignment.test.js.map +1 -0
- package/dist/tests/integration/error-handling.test.d.ts +2 -0
- package/dist/tests/integration/error-handling.test.d.ts.map +1 -0
- package/dist/tests/integration/error-handling.test.js +36 -0
- package/dist/tests/integration/error-handling.test.js.map +1 -0
- package/dist/tests/integration/installer-config.test.d.ts +2 -0
- package/dist/tests/integration/installer-config.test.d.ts.map +1 -0
- package/dist/tests/integration/installer-config.test.js +65 -0
- package/dist/tests/integration/installer-config.test.js.map +1 -0
- package/dist/tests/integration/plugin-catch-block.test.d.ts +2 -0
- package/dist/tests/integration/plugin-catch-block.test.d.ts.map +1 -0
- package/dist/tests/integration/plugin-catch-block.test.js +134 -0
- package/dist/tests/integration/plugin-catch-block.test.js.map +1 -0
- package/dist/tests/integration/reset-time-display.test.d.ts +6 -0
- package/dist/tests/integration/reset-time-display.test.d.ts.map +1 -0
- package/dist/tests/integration/reset-time-display.test.js +138 -0
- package/dist/tests/integration/reset-time-display.test.js.map +1 -0
- package/dist/tests/module/http-client.test.d.ts +2 -0
- package/dist/tests/module/http-client.test.d.ts.map +1 -0
- package/dist/tests/module/http-client.test.js +49 -0
- package/dist/tests/module/http-client.test.js.map +1 -0
- package/dist/tests/module/platform-detection.test.d.ts +5 -0
- package/dist/tests/module/platform-detection.test.d.ts.map +1 -0
- package/dist/tests/module/platform-detection.test.js +48 -0
- package/dist/tests/module/platform-detection.test.js.map +1 -0
- package/integration/agents/copilot-quota-exec.md +20 -0
- package/integration/agents/glm-quota-exec.md +20 -0
- package/integration/command/copilot_quota.md +6 -0
- package/integration/command/glm_quota.md +6 -0
- package/integration/skills/copilot-quota/SKILL.md +11 -0
- package/integration/skills/glm-quota/SKILL.md +11 -0
- package/package.json +69 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026
|
|
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,189 @@
|
|
|
1
|
+
# opencode-quota
|
|
2
|
+
|
|
3
|
+
## [](https://www.npmjs.com/package/opencode-glm-quota) [](https://opensource.org/licenses/MIT) [](https://github.com/guyinwonder168/opencode-glm-quota/actions) [](https://sonarcloud.io/summary/new_code?id=guyinwonder168_opencode-glm-quota)
|
|
4
|
+
|
|
5
|
+
[](https://sonarcloud.io/summary/new_code?id=guyinwonder168_opencode-glm-quota)
|
|
6
|
+
[](https://sonarcloud.io/summary/new_code?id=guyinwonder168_opencode-glm-quota)
|
|
7
|
+
[](https://sonarcloud.io/summary/new_code?id=guyinwonder168_opencode-glm-quota)
|
|
8
|
+
[](https://sonarcloud.io/summary/new_code?id=guyinwonder168_opencode-glm-quota)
|
|
9
|
+
|
|
10
|
+
OpenCode plugin for querying Z.ai GLM, GitHub Copilot, and Kimi usage statistics. Check quota limits, model usage, and MCP tool usage with manual commands.
|
|
11
|
+
|
|
12
|
+
## Features
|
|
13
|
+
|
|
14
|
+
- 📊 **Multi-Platform Support**: Query GLM, GitHub Copilot, and Kimi Coding Plans.
|
|
15
|
+
- 🖱️ **Manual-Only**: Plugin only runs when you ask (no periodic background checks).
|
|
16
|
+
- ⏱️ **Human Durations**: Clear reset timers (e.g., `24d 13h 6m 12s`), hiding zero values.
|
|
17
|
+
- 🔐 **Secure Credential Discovery**: Automatically fetches tokens from OpenCode auth context.
|
|
18
|
+
- 📈 **Visual Progress**: Plain text progress bars and usage percentages.
|
|
19
|
+
- ⚡ **Fail-Fast**: Direct reporting without hidden retry loops.
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
### Option 1: npm (Recommended)
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
# Install package
|
|
27
|
+
npm install opencode-glm-quota
|
|
28
|
+
|
|
29
|
+
# Run installer to configure OpenCode
|
|
30
|
+
npx opencode-glm-quota install
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
**What the installer does:**
|
|
34
|
+
|
|
35
|
+
- Copies `/glm_quota`, `/copilot_quota`, and `/kimi_quota` commands to your OpenCode config.
|
|
36
|
+
- Configures skill documentation in `~/.config/opencode/skills/`.
|
|
37
|
+
- Automatically adds the plugin to your OpenCode configuration.
|
|
38
|
+
|
|
39
|
+
### Uninstall
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
npx opencode-glm-quota uninstall
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Quick Start
|
|
46
|
+
|
|
47
|
+
Run any of the following commands in OpenCode:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
/glm_quota # Z.AI GLM Coding Plan statistics
|
|
51
|
+
/copilot_quota # GitHub Copilot usage statistics
|
|
52
|
+
/kimi_quota # Kimi Coding Plan usage statistics
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## CLI Usage
|
|
56
|
+
|
|
57
|
+
Check your quota directly from the terminal using the provided binaries:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
# Check GLM quota
|
|
61
|
+
bun run bin/glm-quota.ts
|
|
62
|
+
|
|
63
|
+
# Check GitHub Copilot quota
|
|
64
|
+
bun run bin/copilot-quota.ts
|
|
65
|
+
|
|
66
|
+
# Check Kimi quota
|
|
67
|
+
bun run bin/kimi-quota.ts
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### CLI Flags
|
|
71
|
+
|
|
72
|
+
| Flag | Shorthand | Description |
|
|
73
|
+
| --------- | --------- | ---------------------------------------- |
|
|
74
|
+
| `--debug` | `-d` | Enable debug logging (hidden by default) |
|
|
75
|
+
| `--raw` | | Output raw JSON (useful for scripting) |
|
|
76
|
+
| `--help` | `-h` | Show help message |
|
|
77
|
+
|
|
78
|
+
## Platform Features
|
|
79
|
+
|
|
80
|
+
### Z.AI GLM
|
|
81
|
+
|
|
82
|
+
- Shows **TIME_LIMIT** (1-month) and **TOKENS_LIMIT** (5-hour rolling window).
|
|
83
|
+
- Detailed model usage counts and MCP tool usage statistics.
|
|
84
|
+
|
|
85
|
+
### GitHub Copilot
|
|
86
|
+
|
|
87
|
+
- Displays user login, subscription plan, and reset dates.
|
|
88
|
+
- Shows usage for Chat, Completions, and Premium features.
|
|
89
|
+
- Indicates if usage is "Unlimited".
|
|
90
|
+
|
|
91
|
+
### Kimi
|
|
92
|
+
|
|
93
|
+
- Shows total usage and 5-hour rolling window usage.
|
|
94
|
+
- Displays specific "Time to Reset" for each quota window.
|
|
95
|
+
|
|
96
|
+
## Output Example
|
|
97
|
+
|
|
98
|
+
```text
|
|
99
|
+
--------------------------------------------------------
|
|
100
|
+
Z.ai GLM Coding Plan Usage Statistics
|
|
101
|
+
--------------------------------------------------------
|
|
102
|
+
Platform: Z.AI
|
|
103
|
+
Period: 2026-02-22 09:00:00 -> 2026-02-23 08:59:59
|
|
104
|
+
--------------------------------------------------------
|
|
105
|
+
QUOTA LIMITS
|
|
106
|
+
- Token usage(5 Hour) [###################--------] 40.5%
|
|
107
|
+
- MCP usage(1 Month) [####-----------------------] 12.3%
|
|
108
|
+
Used: 123/1,000
|
|
109
|
+
Reset in: 12d 4h 15m
|
|
110
|
+
--------------------------------------------------------
|
|
111
|
+
MODEL USAGE (24h)
|
|
112
|
+
- Total Tokens (24h): 12,500,000 (31% of 5h limit)
|
|
113
|
+
- 5h Window Usage: 40.5% of 40,000,000
|
|
114
|
+
- Total Calls: 1,234
|
|
115
|
+
--------------------------------------------------------
|
|
116
|
+
TOOL/MCP USAGE (24h)
|
|
117
|
+
- Network Searches: 5,678
|
|
118
|
+
- Web Reads: 2,345
|
|
119
|
+
- ZRead Calls: 890
|
|
120
|
+
--------------------------------------------------------
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Error Handling
|
|
124
|
+
|
|
125
|
+
The plugin uses fail-fast error handling. If an API request fails or credentials are missing, it displays a plain text error message immediately.
|
|
126
|
+
|
|
127
|
+
```text
|
|
128
|
+
--------------------------------------------------------
|
|
129
|
+
ERROR: Z.ai Credentials Not Found
|
|
130
|
+
--------------------------------------------------------
|
|
131
|
+
Please authenticate first:
|
|
132
|
+
1. Run /connect in OpenCode TUI
|
|
133
|
+
2. Select "Z.AI Coding Plan" or "Z.AI"
|
|
134
|
+
--------------------------------------------------------
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## API Reference
|
|
138
|
+
|
|
139
|
+
| Platform | Endpoint |
|
|
140
|
+
| ------------------ | ---------------------------------------------- |
|
|
141
|
+
| **Z.AI GLM** | `https://api.z.ai/api/monitor/usage/*` |
|
|
142
|
+
| **GitHub Copilot** | `https://api.github.com/copilot_internal/user` |
|
|
143
|
+
| **Kimi** | `https://api.kimi.com/coding/v1/usages` |
|
|
144
|
+
|
|
145
|
+
## Development
|
|
146
|
+
|
|
147
|
+
### Build & Test
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
# Build TypeScript
|
|
151
|
+
bun run build
|
|
152
|
+
|
|
153
|
+
# Run all tests
|
|
154
|
+
bun run test
|
|
155
|
+
|
|
156
|
+
# Format code
|
|
157
|
+
bun run fmt
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### Project Structure
|
|
161
|
+
|
|
162
|
+
```text
|
|
163
|
+
src/
|
|
164
|
+
index.ts # Main plugin entry point
|
|
165
|
+
api/
|
|
166
|
+
client.ts # Shared HTTPS client
|
|
167
|
+
endpoints.ts # Endpoint definitions
|
|
168
|
+
utils/
|
|
169
|
+
duration.ts # d h m s formatting logic
|
|
170
|
+
progress-bar.ts # Plain text bar rendering
|
|
171
|
+
bin/
|
|
172
|
+
glm-quota.ts # CLI binary for GLM
|
|
173
|
+
copilot-quota.ts # CLI binary for Copilot
|
|
174
|
+
kimi-quota.ts # CLI binary for Kimi
|
|
175
|
+
install.js # Installation script
|
|
176
|
+
integration/
|
|
177
|
+
command/ # /command slash commands
|
|
178
|
+
skills/ # OpenCode skill docs
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## License
|
|
182
|
+
|
|
183
|
+
MIT © 2026
|
|
184
|
+
|
|
185
|
+
## Changelog
|
|
186
|
+
|
|
187
|
+
See [CHANGELOG.md](CHANGELOG.md) for version history.
|
|
188
|
+
|
|
189
|
+
- **v2.62.0**: Removed auto quota features; Added Copilot & Kimi support; Plain text output; CLI binaries.
|
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
#!/usr/bin/env -S bun
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* GitHub Copilot quota CLI tool
|
|
5
|
+
* Fetches and displays GitHub Copilot usage statistics from command line
|
|
6
|
+
* Credentials are fetched from OpenCode's auth.json
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import * as https from 'https'
|
|
10
|
+
import * as os from 'os'
|
|
11
|
+
import * as path from 'path'
|
|
12
|
+
import { configureLogging, getLogger } from '../src/shared/logging.js'
|
|
13
|
+
|
|
14
|
+
const COMMAND_NAME = 'opencode-copilot-quota'
|
|
15
|
+
const AUTH_JSON_PATH = path.join(os.homedir(), '.local', 'share', 'opencode', 'auth.json')
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Type definitions
|
|
19
|
+
*/
|
|
20
|
+
interface Credentials {
|
|
21
|
+
token: string
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
type AuthJson = Record<string, { access?: string; refresh?: string; token?: string }>
|
|
25
|
+
|
|
26
|
+
// Copilot API response types
|
|
27
|
+
interface CopilotQuotaSnapshot {
|
|
28
|
+
unlimited: boolean
|
|
29
|
+
percent_remaining?: number
|
|
30
|
+
entitlement?: number
|
|
31
|
+
quota_remaining?: number
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface CopilotResponse {
|
|
35
|
+
login: string
|
|
36
|
+
copilot_plan: string
|
|
37
|
+
quota_reset_date_utc: string
|
|
38
|
+
quota_snapshots: {
|
|
39
|
+
chat: CopilotQuotaSnapshot
|
|
40
|
+
completions: CopilotQuotaSnapshot
|
|
41
|
+
premium_interactions: CopilotQuotaSnapshot
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Global logger, assigned after configureLogging()
|
|
46
|
+
let logger: ReturnType<typeof getLogger>
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Calculate duration until reset date
|
|
50
|
+
* @param resetDate - Reset date string (ISO 8601)
|
|
51
|
+
* @returns Human-readable duration (e.g., "7d 4h 32m 15s", "2h 57m 31s", "5m")
|
|
52
|
+
*/
|
|
53
|
+
function calculateDurationToReset(resetDate: string): string {
|
|
54
|
+
const now = new Date()
|
|
55
|
+
const reset = new Date(resetDate)
|
|
56
|
+
const diffMs = reset.getTime() - now.getTime()
|
|
57
|
+
|
|
58
|
+
// If reset date is in the past, show "0d 0h 0m 0s"
|
|
59
|
+
if (diffMs <= 0) {
|
|
60
|
+
return '0d 0h 0m 0s'
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24))
|
|
64
|
+
const diffHours = Math.floor((diffMs % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60))
|
|
65
|
+
const diffMinutes = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60))
|
|
66
|
+
const diffSeconds = Math.floor((diffMs % (1000 * 60)) / 1000)
|
|
67
|
+
|
|
68
|
+
// Build duration string, hiding zero values
|
|
69
|
+
const parts: string[] = []
|
|
70
|
+
if (diffDays > 0) parts.push(`${diffDays}d`)
|
|
71
|
+
if (diffHours > 0) parts.push(`${diffHours}h`)
|
|
72
|
+
if (diffMinutes > 0) parts.push(`${diffMinutes}m`)
|
|
73
|
+
if (diffSeconds > 0) parts.push(`${diffSeconds}s`)
|
|
74
|
+
|
|
75
|
+
return parts.length > 0 ? parts.join(' ') : '0s'
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Parse Copilot response and format output
|
|
80
|
+
* @param response - Raw JSON response string
|
|
81
|
+
* @returns Formatted output string
|
|
82
|
+
*/
|
|
83
|
+
function parseCopilotResponse(response: string): string {
|
|
84
|
+
const data = JSON.parse(response) as CopilotResponse
|
|
85
|
+
|
|
86
|
+
const { login, copilot_plan, quota_reset_date_utc, quota_snapshots } = data
|
|
87
|
+
|
|
88
|
+
// Format reset date and duration
|
|
89
|
+
const resetDate = new Date(quota_reset_date_utc)
|
|
90
|
+
.toLocaleDateString('en-US', {
|
|
91
|
+
year: 'numeric',
|
|
92
|
+
month: '2-digit',
|
|
93
|
+
day: '2-digit',
|
|
94
|
+
})
|
|
95
|
+
const duration = calculateDurationToReset(quota_reset_date_utc)
|
|
96
|
+
|
|
97
|
+
// Build formatted output (plain text, no boxes)
|
|
98
|
+
let output = ''
|
|
99
|
+
output += `GitHub Copilot Usage Statistics\n`
|
|
100
|
+
output += `\n`
|
|
101
|
+
output += `User: ${login}\n`
|
|
102
|
+
output += `Plan: ${copilot_plan}\n`
|
|
103
|
+
output += `Reset: ${resetDate} (${duration})\n`
|
|
104
|
+
output += `\n`
|
|
105
|
+
output += `QUOTA USAGE\n`
|
|
106
|
+
output += `\n`
|
|
107
|
+
|
|
108
|
+
// Chat
|
|
109
|
+
if (quota_snapshots.chat.unlimited) {
|
|
110
|
+
output += `Chat: Unlimited\n`
|
|
111
|
+
} else {
|
|
112
|
+
const remaining = quota_snapshots.chat.percent_remaining ?? 0
|
|
113
|
+
const used = 100 - remaining
|
|
114
|
+
output += `Chat: ${used}% used\n`
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Completions
|
|
118
|
+
if (quota_snapshots.completions.unlimited) {
|
|
119
|
+
output += `Completions: Unlimited\n`
|
|
120
|
+
} else {
|
|
121
|
+
const remaining = quota_snapshots.completions.percent_remaining ?? 0
|
|
122
|
+
const used = 100 - remaining
|
|
123
|
+
output += `Completions: ${used}% used\n`
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Premium
|
|
127
|
+
const premium = quota_snapshots.premium_interactions
|
|
128
|
+
if (premium.unlimited) {
|
|
129
|
+
output += `Premium: Unlimited\n`
|
|
130
|
+
} else {
|
|
131
|
+
const remaining = premium.quota_remaining ?? 0
|
|
132
|
+
const entitlement = premium.entitlement ?? 0
|
|
133
|
+
const used = entitlement - remaining
|
|
134
|
+
const percentUsed = entitlement > 0 ? ((used / entitlement) * 100).toFixed(1) : '0.0'
|
|
135
|
+
output += `Premium: ${used.toFixed(2)} / ${entitlement} (${percentUsed}% used)\n`
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return output
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Make HTTPS request with timeout
|
|
143
|
+
*/
|
|
144
|
+
function makeRequest(
|
|
145
|
+
url: string,
|
|
146
|
+
token: string,
|
|
147
|
+
headers: Record<string, string> = {},
|
|
148
|
+
): Promise<string> {
|
|
149
|
+
logger.debug(`Making request to ${url}`)
|
|
150
|
+
logger.debug(`Using token: ${token.slice(0, 8)}...${token.slice(-4)}`)
|
|
151
|
+
|
|
152
|
+
return new Promise((resolve, reject) => {
|
|
153
|
+
const timeout = 10000 // 10 seconds
|
|
154
|
+
const timer = setTimeout(() => {
|
|
155
|
+
req.destroy()
|
|
156
|
+
logger.error(`Request timeout for ${url}`)
|
|
157
|
+
reject(new Error('Request timeout'))
|
|
158
|
+
}, timeout)
|
|
159
|
+
|
|
160
|
+
const urlObj = new URL(url)
|
|
161
|
+
const options = {
|
|
162
|
+
hostname: urlObj.hostname,
|
|
163
|
+
port: urlObj.port || 443,
|
|
164
|
+
path: urlObj.pathname + urlObj.search,
|
|
165
|
+
method: 'GET',
|
|
166
|
+
headers: {
|
|
167
|
+
'Authorization': `token ${token}`,
|
|
168
|
+
'Accept': 'application/json',
|
|
169
|
+
'Content-Type': 'application/json',
|
|
170
|
+
...headers,
|
|
171
|
+
},
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
logger.debug(`Request options: ${
|
|
175
|
+
JSON.stringify({
|
|
176
|
+
hostname: options.hostname,
|
|
177
|
+
path: options.path,
|
|
178
|
+
method: options.method,
|
|
179
|
+
headers: Object.fromEntries(
|
|
180
|
+
Object.entries(options.headers).map(([k, v]) => [
|
|
181
|
+
k,
|
|
182
|
+
k === 'Authorization' ? `${v.slice(0, 13)}...${v.slice(-4)}` : v,
|
|
183
|
+
]),
|
|
184
|
+
),
|
|
185
|
+
})
|
|
186
|
+
}`)
|
|
187
|
+
|
|
188
|
+
const req = https.request(options, (res) => {
|
|
189
|
+
let data = ''
|
|
190
|
+
|
|
191
|
+
logger.debug(`Response status: ${res.statusCode} ${res.statusMessage}`)
|
|
192
|
+
|
|
193
|
+
res.on('data', chunk => {
|
|
194
|
+
data += chunk
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
res.on('end', () => {
|
|
198
|
+
clearTimeout(timer)
|
|
199
|
+
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
|
|
200
|
+
logger.debug(`Response received: ${data.slice(0, 100)}... (${data.length} bytes)`)
|
|
201
|
+
resolve(data)
|
|
202
|
+
} else {
|
|
203
|
+
logger.error(`HTTP error: ${res.statusCode} ${res.statusMessage}`)
|
|
204
|
+
logger.debug(`Error response: ${data}`)
|
|
205
|
+
reject(new Error(`HTTP ${res.statusCode}: ${res.statusMessage}`))
|
|
206
|
+
}
|
|
207
|
+
})
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
req.on('error', (err) => {
|
|
211
|
+
clearTimeout(timer)
|
|
212
|
+
logger.error(`Request error: ${err.message}`)
|
|
213
|
+
reject(err)
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
req.end()
|
|
217
|
+
})
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Get credentials from OpenCode auth.json
|
|
222
|
+
*/
|
|
223
|
+
function getCredentials(): Credentials | null {
|
|
224
|
+
logger.debug(`Looking for credentials at ${AUTH_JSON_PATH}`)
|
|
225
|
+
|
|
226
|
+
try {
|
|
227
|
+
const fs = require('fs')
|
|
228
|
+
if (!fs.existsSync(AUTH_JSON_PATH)) {
|
|
229
|
+
logger.debug('auth.json does not exist')
|
|
230
|
+
return null
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const content = fs.readFileSync(AUTH_JSON_PATH, 'utf-8')
|
|
234
|
+
const auth = JSON.parse(content) as AuthJson
|
|
235
|
+
|
|
236
|
+
logger.debug(`auth.json loaded with ${Object.keys(auth).length} providers`)
|
|
237
|
+
|
|
238
|
+
const copilotToken = auth['github-copilot']?.access ?? auth['github-copilot']?.refresh
|
|
239
|
+
if (copilotToken) {
|
|
240
|
+
logger.info('Found credentials for provider: github-copilot')
|
|
241
|
+
return { token: copilotToken }
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
logger.debug('No credentials found for github-copilot provider')
|
|
245
|
+
return null
|
|
246
|
+
} catch (error) {
|
|
247
|
+
logger.error(`Error reading auth.json: ${error}`)
|
|
248
|
+
return null
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Display help message
|
|
254
|
+
*/
|
|
255
|
+
function showHelp() {
|
|
256
|
+
console.log(`
|
|
257
|
+
${COMMAND_NAME} - Check GitHub Copilot quota from terminal
|
|
258
|
+
|
|
259
|
+
USAGE:
|
|
260
|
+
bun run bin/copilot-quota.ts [--raw] [--debug]
|
|
261
|
+
|
|
262
|
+
OPTIONS:
|
|
263
|
+
--raw Output raw JSON (default: formatted output)
|
|
264
|
+
--debug, -d Enable debug logging (logs hidden by default)
|
|
265
|
+
--help, -h Display this help message
|
|
266
|
+
|
|
267
|
+
AUTHENTICATION:
|
|
268
|
+
Reads credentials from ~/.local/share/opencode/auth.json
|
|
269
|
+
Provider: github-copilot
|
|
270
|
+
|
|
271
|
+
EXAMPLES:
|
|
272
|
+
bun run bin/copilot-quota.ts
|
|
273
|
+
bun run bin/copilot-quota.ts --debug
|
|
274
|
+
bun run bin/copilot-quota.ts --raw
|
|
275
|
+
`)
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Main CLI entry point
|
|
280
|
+
*/
|
|
281
|
+
async function main() {
|
|
282
|
+
const args = process.argv.slice(2)
|
|
283
|
+
|
|
284
|
+
// Check for help flag first (before logging config)
|
|
285
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
286
|
+
showHelp()
|
|
287
|
+
process.exit(0)
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Check for debug flag before configuring logging
|
|
291
|
+
const debugMode = args.includes('--debug') || args.includes('-d')
|
|
292
|
+
|
|
293
|
+
// Only configure logging when --debug is present
|
|
294
|
+
if (debugMode) {
|
|
295
|
+
await configureLogging('debug')
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Create logger after configureLogging (or with no-op if not configured)
|
|
299
|
+
if (debugMode) {
|
|
300
|
+
logger = getLogger(['opencode-quota', 'cli', 'copilot'])
|
|
301
|
+
} else {
|
|
302
|
+
// No-op logger when debug mode is off
|
|
303
|
+
logger = {
|
|
304
|
+
debug: () => {},
|
|
305
|
+
info: () => {},
|
|
306
|
+
warn: () => {},
|
|
307
|
+
error: () => {},
|
|
308
|
+
} as unknown as ReturnType<typeof getLogger>
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
logger.info(`${COMMAND_NAME} starting...`)
|
|
312
|
+
|
|
313
|
+
const rawOutput = args.includes('--raw')
|
|
314
|
+
|
|
315
|
+
logger.debug(`Arguments: ${args.join(' ')}`)
|
|
316
|
+
logger.debug(`Raw output: ${rawOutput}`)
|
|
317
|
+
|
|
318
|
+
try {
|
|
319
|
+
// Get credentials from OpenCode auth.json
|
|
320
|
+
const credentials = getCredentials()
|
|
321
|
+
|
|
322
|
+
if (!credentials) {
|
|
323
|
+
logger.error('No credentials found')
|
|
324
|
+
console.error(`\n${COMMAND_NAME}: Not configured.\nPlease connect to GitHub Copilot in OpenCode first.\n`)
|
|
325
|
+
process.exit(1)
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Query GitHub Copilot internal API
|
|
329
|
+
const customHeaders = {
|
|
330
|
+
'X-GitHub-Api-Version': '2025-04-01',
|
|
331
|
+
'User-Agent': 'GitHubCopilotChat/0.26.7',
|
|
332
|
+
'Editor-Version': 'vscode/1.96.2',
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
logger.info('Querying GitHub Copilot internal API...')
|
|
336
|
+
|
|
337
|
+
const response = await makeRequest(
|
|
338
|
+
'https://api.github.com/copilot_internal/user',
|
|
339
|
+
credentials.token,
|
|
340
|
+
customHeaders,
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
logger.info('Received response from GitHub API')
|
|
344
|
+
|
|
345
|
+
if (!rawOutput) {
|
|
346
|
+
// Parse and display formatted output
|
|
347
|
+
try {
|
|
348
|
+
const formatted = parseCopilotResponse(response)
|
|
349
|
+
console.log(formatted)
|
|
350
|
+
} catch (error) {
|
|
351
|
+
logger.error(`Failed to parse Copilot response: ${error}`)
|
|
352
|
+
logger.debug(`Invalid response: ${response}`)
|
|
353
|
+
throw new Error(`Parse error: ${error instanceof Error ? error.message : String(error)}`)
|
|
354
|
+
}
|
|
355
|
+
} else {
|
|
356
|
+
// Print raw response
|
|
357
|
+
console.log(response)
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
logger.info('Completed successfully')
|
|
361
|
+
} catch (error) {
|
|
362
|
+
if (error instanceof Error) {
|
|
363
|
+
logger.error(`${error.name}: ${error.message}`)
|
|
364
|
+
console.error(`\n${COMMAND_NAME}: ${error.message}\n`)
|
|
365
|
+
} else {
|
|
366
|
+
logger.error(`Unknown error: ${String(error)}`)
|
|
367
|
+
console.error(`\n${COMMAND_NAME}: Unknown error occurred\n`)
|
|
368
|
+
}
|
|
369
|
+
process.exit(1)
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Run CLI
|
|
374
|
+
main()
|