@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.
Files changed (133) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +189 -0
  3. package/bin/copilot-quota.ts +374 -0
  4. package/bin/glm-quota.ts +467 -0
  5. package/bin/install.js +439 -0
  6. package/bin/kimi-quota.ts +314 -0
  7. package/dist/bin/copilot-quota.d.ts +8 -0
  8. package/dist/bin/copilot-quota.d.ts.map +1 -0
  9. package/dist/bin/copilot-quota.js +298 -0
  10. package/dist/bin/copilot-quota.js.map +1 -0
  11. package/dist/bin/glm-quota.d.ts +8 -0
  12. package/dist/bin/glm-quota.d.ts.map +1 -0
  13. package/dist/bin/glm-quota.js +367 -0
  14. package/dist/bin/glm-quota.js.map +1 -0
  15. package/dist/bin/kimi-quota.d.ts +3 -0
  16. package/dist/bin/kimi-quota.d.ts.map +1 -0
  17. package/dist/bin/kimi-quota.js +241 -0
  18. package/dist/bin/kimi-quota.js.map +1 -0
  19. package/dist/src/api/client.d.ts +76 -0
  20. package/dist/src/api/client.d.ts.map +1 -0
  21. package/dist/src/api/client.js +203 -0
  22. package/dist/src/api/client.js.map +1 -0
  23. package/dist/src/api/endpoints.d.ts +22 -0
  24. package/dist/src/api/endpoints.d.ts.map +1 -0
  25. package/dist/src/api/endpoints.js +41 -0
  26. package/dist/src/api/endpoints.js.map +1 -0
  27. package/dist/src/api/platforms.d.ts +20 -0
  28. package/dist/src/api/platforms.d.ts.map +1 -0
  29. package/dist/src/api/platforms.js +38 -0
  30. package/dist/src/api/platforms.js.map +1 -0
  31. package/dist/src/index.d.ts +10 -0
  32. package/dist/src/index.d.ts.map +1 -0
  33. package/dist/src/index.js +723 -0
  34. package/dist/src/index.js.map +1 -0
  35. package/dist/src/shared/logging.d.ts +7 -0
  36. package/dist/src/shared/logging.d.ts.map +1 -0
  37. package/dist/src/shared/logging.js +29 -0
  38. package/dist/src/shared/logging.js.map +1 -0
  39. package/dist/src/utils/box-constants.d.ts +43 -0
  40. package/dist/src/utils/box-constants.d.ts.map +1 -0
  41. package/dist/src/utils/box-constants.js +43 -0
  42. package/dist/src/utils/box-constants.js.map +1 -0
  43. package/dist/src/utils/date-formatter.d.ts +17 -0
  44. package/dist/src/utils/date-formatter.d.ts.map +1 -0
  45. package/dist/src/utils/date-formatter.js +33 -0
  46. package/dist/src/utils/date-formatter.js.map +1 -0
  47. package/dist/src/utils/error-formatter.d.ts +17 -0
  48. package/dist/src/utils/error-formatter.d.ts.map +1 -0
  49. package/dist/src/utils/error-formatter.js +60 -0
  50. package/dist/src/utils/error-formatter.js.map +1 -0
  51. package/dist/src/utils/progress-bar.d.ts +35 -0
  52. package/dist/src/utils/progress-bar.d.ts.map +1 -0
  53. package/dist/src/utils/progress-bar.js +43 -0
  54. package/dist/src/utils/progress-bar.js.map +1 -0
  55. package/dist/src/utils/reset-timer.d.ts +11 -0
  56. package/dist/src/utils/reset-timer.d.ts.map +1 -0
  57. package/dist/src/utils/reset-timer.js +32 -0
  58. package/dist/src/utils/reset-timer.js.map +1 -0
  59. package/dist/src/utils/time-window.d.ts +30 -0
  60. package/dist/src/utils/time-window.d.ts.map +1 -0
  61. package/dist/src/utils/time-window.js +34 -0
  62. package/dist/src/utils/time-window.js.map +1 -0
  63. package/dist/tests/error-handling/api-errors.test.d.ts +7 -0
  64. package/dist/tests/error-handling/api-errors.test.d.ts.map +1 -0
  65. package/dist/tests/error-handling/api-errors.test.js +110 -0
  66. package/dist/tests/error-handling/api-errors.test.js.map +1 -0
  67. package/dist/tests/error-handling/auth-errors.test.d.ts +7 -0
  68. package/dist/tests/error-handling/auth-errors.test.d.ts.map +1 -0
  69. package/dist/tests/error-handling/auth-errors.test.js +110 -0
  70. package/dist/tests/error-handling/auth-errors.test.js.map +1 -0
  71. package/dist/tests/error-handling/network-errors.test.d.ts +7 -0
  72. package/dist/tests/error-handling/network-errors.test.d.ts.map +1 -0
  73. package/dist/tests/error-handling/network-errors.test.js +94 -0
  74. package/dist/tests/error-handling/network-errors.test.js.map +1 -0
  75. package/dist/tests/error-handling/parse-errors.test.d.ts +7 -0
  76. package/dist/tests/error-handling/parse-errors.test.d.ts.map +1 -0
  77. package/dist/tests/error-handling/parse-errors.test.js +87 -0
  78. package/dist/tests/error-handling/parse-errors.test.js.map +1 -0
  79. package/dist/tests/error-handling/token-sanitization.test.d.ts +2 -0
  80. package/dist/tests/error-handling/token-sanitization.test.d.ts.map +1 -0
  81. package/dist/tests/error-handling/token-sanitization.test.js +59 -0
  82. package/dist/tests/error-handling/token-sanitization.test.js.map +1 -0
  83. package/dist/tests/functional/date-formatter.test.d.ts +5 -0
  84. package/dist/tests/functional/date-formatter.test.d.ts.map +1 -0
  85. package/dist/tests/functional/date-formatter.test.js +46 -0
  86. package/dist/tests/functional/date-formatter.test.js.map +1 -0
  87. package/dist/tests/functional/progress-bar.test.d.ts +5 -0
  88. package/dist/tests/functional/progress-bar.test.d.ts.map +1 -0
  89. package/dist/tests/functional/progress-bar.test.js +82 -0
  90. package/dist/tests/functional/progress-bar.test.js.map +1 -0
  91. package/dist/tests/functional/reset-timer.test.d.ts +6 -0
  92. package/dist/tests/functional/reset-timer.test.d.ts.map +1 -0
  93. package/dist/tests/functional/reset-timer.test.js +67 -0
  94. package/dist/tests/functional/reset-timer.test.js.map +1 -0
  95. package/dist/tests/functional/time-window.test.d.ts +5 -0
  96. package/dist/tests/functional/time-window.test.d.ts.map +1 -0
  97. package/dist/tests/functional/time-window.test.js +46 -0
  98. package/dist/tests/functional/time-window.test.js.map +1 -0
  99. package/dist/tests/integration/box-alignment.test.d.ts +8 -0
  100. package/dist/tests/integration/box-alignment.test.d.ts.map +1 -0
  101. package/dist/tests/integration/box-alignment.test.js +238 -0
  102. package/dist/tests/integration/box-alignment.test.js.map +1 -0
  103. package/dist/tests/integration/error-handling.test.d.ts +2 -0
  104. package/dist/tests/integration/error-handling.test.d.ts.map +1 -0
  105. package/dist/tests/integration/error-handling.test.js +36 -0
  106. package/dist/tests/integration/error-handling.test.js.map +1 -0
  107. package/dist/tests/integration/installer-config.test.d.ts +2 -0
  108. package/dist/tests/integration/installer-config.test.d.ts.map +1 -0
  109. package/dist/tests/integration/installer-config.test.js +65 -0
  110. package/dist/tests/integration/installer-config.test.js.map +1 -0
  111. package/dist/tests/integration/plugin-catch-block.test.d.ts +2 -0
  112. package/dist/tests/integration/plugin-catch-block.test.d.ts.map +1 -0
  113. package/dist/tests/integration/plugin-catch-block.test.js +134 -0
  114. package/dist/tests/integration/plugin-catch-block.test.js.map +1 -0
  115. package/dist/tests/integration/reset-time-display.test.d.ts +6 -0
  116. package/dist/tests/integration/reset-time-display.test.d.ts.map +1 -0
  117. package/dist/tests/integration/reset-time-display.test.js +138 -0
  118. package/dist/tests/integration/reset-time-display.test.js.map +1 -0
  119. package/dist/tests/module/http-client.test.d.ts +2 -0
  120. package/dist/tests/module/http-client.test.d.ts.map +1 -0
  121. package/dist/tests/module/http-client.test.js +49 -0
  122. package/dist/tests/module/http-client.test.js.map +1 -0
  123. package/dist/tests/module/platform-detection.test.d.ts +5 -0
  124. package/dist/tests/module/platform-detection.test.d.ts.map +1 -0
  125. package/dist/tests/module/platform-detection.test.js +48 -0
  126. package/dist/tests/module/platform-detection.test.js.map +1 -0
  127. package/integration/agents/copilot-quota-exec.md +20 -0
  128. package/integration/agents/glm-quota-exec.md +20 -0
  129. package/integration/command/copilot_quota.md +6 -0
  130. package/integration/command/glm_quota.md +6 -0
  131. package/integration/skills/copilot-quota/SKILL.md +11 -0
  132. package/integration/skills/glm-quota/SKILL.md +11 -0
  133. 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
+ ## [![npm version](https://img.shields.io/npm/v/opencode-glm-quota.svg)](https://www.npmjs.com/package/opencode-glm-quota) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![Build Status](https://github.com/guyinwonder168/opencode-glm-quota/workflows/CI/badge.svg)](https://github.com/guyinwonder168/opencode-glm-quota/actions) [![SonarQube Cloud](https://sonarcloud.io/images/project_badges/sonarcloud-light.svg)](https://sonarcloud.io/summary/new_code?id=guyinwonder168_opencode-glm-quota)
4
+
5
+ [![Reliability Rating](https://sonarcloud.io/api/project_badges/measure?project=guyinwonder168_opencode-glm-quota&metric=reliability_rating)](https://sonarcloud.io/summary/new_code?id=guyinwonder168_opencode-glm-quota)
6
+ [![Security Rating](https://sonarcloud.io/api/project_badges/measure?project=guyinwonder168_opencode-glm-quota&metric=security_rating)](https://sonarcloud.io/summary/new_code?id=guyinwonder168_opencode-glm-quota)
7
+ [![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=guyinwonder168_opencode-glm-quota&metric=sqale_rating)](https://sonarcloud.io/summary/new_code?id=guyinwonder168_opencode-glm-quota)
8
+ [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=guyinwonder168_opencode-glm-quota&metric=alert_status)](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()