@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
@@ -0,0 +1,723 @@
1
+ /**
2
+ * OpenCode GLM Quota Plugin
3
+ *
4
+ * Query Z.ai GLM Coding Plan usage statistics including quota limits,
5
+ * model usage, and MCP tool usage.
6
+ */
7
+ import { tool } from '@opencode-ai/plugin/tool';
8
+ import * as fs from 'fs';
9
+ import * as os from 'os';
10
+ import * as path from 'path';
11
+ import { makeRequest, queryEndpoint } from './api/client.js';
12
+ import { getEndpoints } from './api/endpoints.js';
13
+ import { detectPlatform, getPlatformName } from './api/platforms.js';
14
+ import { BOX_WIDTH, HEADER } from './utils/box-constants.js';
15
+ import { createBoxedError } from './utils/error-formatter.js';
16
+ import { formatProgressLine } from './utils/progress-bar.js';
17
+ import { formatTimeUntilReset } from './utils/reset-timer.js';
18
+ import { getTimeWindow, getTimeWindowQueryParams } from './utils/time-window.js';
19
+ // ============================================================================
20
+ // CONSTANTS
21
+ // ============================================================================
22
+ const CANDIDATE_PROVIDER_IDS = [
23
+ 'zai-coding-plan',
24
+ 'zai',
25
+ 'z-ai',
26
+ 'z.ai',
27
+ 'zhipu',
28
+ 'zhipuai',
29
+ 'github-copilot',
30
+ 'github',
31
+ 'copilot',
32
+ ];
33
+ const DEFAULT_TOKEN_LIMIT = 40000000;
34
+ const TOKEN_LIMIT_LABEL = 'Token usage(5 Hour)';
35
+ const MCP_LIMIT_LABEL = 'MCP usage(1 Month)';
36
+ const TOKEN_LIMIT_TYPE = 'TOKENS_LIMIT';
37
+ const TIME_LIMIT_TYPE = 'TIME_LIMIT';
38
+ // ============================================================================
39
+ // CREDENTIAL DISCOVERY
40
+ // ============================================================================
41
+ /**
42
+ * Get auth file path based on platform
43
+ * @returns Path to auth.json file
44
+ */
45
+ function getAuthFilePath() {
46
+ if (process.platform === 'win32') {
47
+ return path.join(process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local'), 'opencode', 'auth.json');
48
+ }
49
+ return path.join(os.homedir(), '.local', 'share', 'opencode', 'auth.json');
50
+ }
51
+ /**
52
+ * Format number with thousands separator
53
+ * @param num Number to format
54
+ * @returns Formatted number string
55
+ */
56
+ function formatNumber(num) {
57
+ return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
58
+ }
59
+ /**
60
+ * Extract API key from auth entry
61
+ * @param entry - Auth entry (string or object)
62
+ * @returns API key or null
63
+ */
64
+ function extractKeyFromEntry(entry) {
65
+ if (typeof entry === 'string')
66
+ return entry;
67
+ if (typeof entry === 'object' && entry !== null) {
68
+ const obj = entry;
69
+ for (const keyName of ['apiKey', 'api_key', 'token', 'key', 'accessToken', 'auth_token']) {
70
+ if (typeof obj[keyName] === 'string')
71
+ return obj[keyName];
72
+ }
73
+ }
74
+ return null;
75
+ }
76
+ /**
77
+ * Get credentials from OpenCode auth.json or environment variables
78
+ * @returns Credentials or null if not found
79
+ */
80
+ async function getCredentials() {
81
+ // Priority 1: OpenCode auth.json
82
+ const authPath = getAuthFilePath();
83
+ if (fs.existsSync(authPath)) {
84
+ try {
85
+ const content = fs.readFileSync(authPath, 'utf-8');
86
+ const authData = JSON.parse(content);
87
+ for (const providerId of CANDIDATE_PROVIDER_IDS) {
88
+ const entry = authData[providerId];
89
+ if (entry) {
90
+ const token = extractKeyFromEntry(entry);
91
+ if (token) {
92
+ const platform = detectPlatform(providerId);
93
+ if (platform) {
94
+ return { token, platform };
95
+ }
96
+ }
97
+ }
98
+ }
99
+ }
100
+ catch {
101
+ // Silent fail, try next method
102
+ }
103
+ }
104
+ // Priority 2: Environment variables (for development/testing)
105
+ if (process.env.ZAI_API_KEY) {
106
+ return { token: process.env.ZAI_API_KEY, platform: 'ZAI' };
107
+ }
108
+ if (process.env.ZHIPU_API_KEY || process.env.ZHIPUAI_API_KEY) {
109
+ return {
110
+ token: (process.env.ZHIPU_API_KEY || process.env.ZHIPUAI_API_KEY),
111
+ platform: 'ZHIPU',
112
+ };
113
+ }
114
+ return null;
115
+ }
116
+ /**
117
+ * Create error message for missing credentials
118
+ * @returns Error message with setup instructions (box formatted)
119
+ */
120
+ function createCredentialError() {
121
+ const header = '❌ Z.ai Credentials Not Found';
122
+ const instructions = [
123
+ '',
124
+ 'Please authenticate first:',
125
+ '',
126
+ '1. Run /connect command in OpenCode TUI',
127
+ '2. Select "Z.AI Coding Plan" or "Z.AI"',
128
+ '3. Or "Zhipu" (for China region)',
129
+ '',
130
+ 'For dev/testing, set environment:',
131
+ '- ZAI_API_KEY (global)',
132
+ '- ZHIPU_API_KEY (China)',
133
+ '',
134
+ ];
135
+ const lines = [];
136
+ lines.push('╔' + '═'.repeat(BOX_WIDTH.BORDER_CHARS) + '╗');
137
+ lines.push('║' + ' '.repeat(BOX_WIDTH.BORDER_CHARS) + '║');
138
+ lines.push(formatBoxLine(header, BOX_WIDTH.CONTENT));
139
+ lines.push('║' + ' '.repeat(BOX_WIDTH.BORDER_CHARS) + '║');
140
+ for (const instruction of instructions) {
141
+ lines.push(formatBoxLine(instruction, BOX_WIDTH.CONTENT));
142
+ }
143
+ lines.push('╚' + '═'.repeat(BOX_WIDTH.BORDER_CHARS) + '╝');
144
+ return lines.join('\n');
145
+ }
146
+ /**
147
+ * Create error message for missing GitHub credentials
148
+ * @returns Error message with setup instructions (box formatted)
149
+ */
150
+ function createGitHubCredentialError() {
151
+ const header = '❌ GitHub Copilot Credentials Not Found';
152
+ const instructions = [
153
+ '',
154
+ 'Please authenticate first:',
155
+ '',
156
+ '1. Run /connect command in OpenCode TUI',
157
+ '2. Select "GitHub Copilot"',
158
+ '',
159
+ 'For dev/testing, set environment:',
160
+ '- GITHUB_TOKEN (your GitHub personal access token)',
161
+ '- GITHUB_USERNAME (your GitHub username)',
162
+ '',
163
+ ];
164
+ const lines = [];
165
+ lines.push('╔' + '═'.repeat(BOX_WIDTH.BORDER_CHARS) + '╗');
166
+ lines.push('║' + ' '.repeat(BOX_WIDTH.BORDER_CHARS) + '║');
167
+ lines.push(formatBoxLine(header, BOX_WIDTH.CONTENT));
168
+ lines.push('║' + ' '.repeat(BOX_WIDTH.BORDER_CHARS) + '║');
169
+ for (const instruction of instructions) {
170
+ lines.push(formatBoxLine(instruction, BOX_WIDTH.CONTENT));
171
+ }
172
+ lines.push('╚' + '═'.repeat(BOX_WIDTH.BORDER_CHARS) + '╝');
173
+ return lines.join('\n');
174
+ }
175
+ // ============================================================================
176
+ // RESPONSE PROCESSING
177
+ // ============================================================================
178
+ /**
179
+ * Process quota limit response
180
+ * @param data - Raw API response
181
+ * @returns Processed response with human-readable types
182
+ */
183
+ function processQuotaLimit(data) {
184
+ const result = { ...data };
185
+ if (result.limits && Array.isArray(result.limits)) {
186
+ result.limits = result.limits.map((item) => {
187
+ if (typeof item === 'object' && item !== null) {
188
+ const limit = item;
189
+ if (limit.type === TOKEN_LIMIT_TYPE) {
190
+ return {
191
+ type: TOKEN_LIMIT_LABEL,
192
+ percentage: typeof limit.percentage === 'number' ? limit.percentage : 0,
193
+ nextResetTime: limit.nextResetTime,
194
+ };
195
+ }
196
+ if (limit.type === TIME_LIMIT_TYPE) {
197
+ return {
198
+ type: MCP_LIMIT_LABEL,
199
+ percentage: typeof limit.percentage === 'number' ? limit.percentage : 0,
200
+ currentValue: limit.currentValue,
201
+ total: limit.usage,
202
+ usageDetails: limit.usageDetails,
203
+ };
204
+ }
205
+ }
206
+ return item;
207
+ });
208
+ }
209
+ return result;
210
+ }
211
+ // ============================================================================
212
+ // OUTPUT FORMATTING
213
+ // ============================================================================
214
+ /**
215
+ * Query GitHub Copilot usage data
216
+ */
217
+ async function queryGitHubUsage(token) {
218
+ const url = 'https://api.github.com/copilot_internal/user';
219
+ const headers = {
220
+ 'Authorization': `token ${token}`,
221
+ 'Accept': 'application/json',
222
+ 'Editor-Version': 'vscode/1.96.2',
223
+ 'Editor-Plugin-Version': 'copilot-chat/0.26.7',
224
+ 'User-Agent': 'GitHubCopilotChat/0.26.7',
225
+ 'X-GitHub-Api-Version': '2025-04-01',
226
+ };
227
+ const data = await makeRequest({ url, authToken: token, customHeaders: headers });
228
+ return data;
229
+ }
230
+ /**
231
+ * Get token limit information from quota data
232
+ */
233
+ function getTokenLimitInfo(quotaData) {
234
+ let tokenLimit = DEFAULT_TOKEN_LIMIT;
235
+ let tokenPct = 0;
236
+ if (!quotaData?.limits)
237
+ return { tokenLimit, tokenPct };
238
+ for (const limit of quotaData.limits) {
239
+ if (limit.type === TOKEN_LIMIT_LABEL) {
240
+ tokenPct = typeof limit.percentage === 'number' ? limit.percentage : 0;
241
+ tokenLimit = limit.total || DEFAULT_TOKEN_LIMIT;
242
+ break;
243
+ }
244
+ }
245
+ return { tokenLimit, tokenPct };
246
+ }
247
+ /**
248
+ * Format MCP tool details as readable lines
249
+ */
250
+ function formatMcpToolLines(details) {
251
+ const lines = [];
252
+ const mcpTotal = details.reduce((sum, d) => sum + (d.usage || 0), 0);
253
+ for (const d of details) {
254
+ const pct = mcpTotal > 0 ? Math.round((d.usage / mcpTotal) * 100) : 0;
255
+ lines.push(` - ${d.modelCode}: ${d.usage} (${pct}%)`);
256
+ }
257
+ return lines;
258
+ }
259
+ function formatTokenUsageLines(tokens, tokenLimit, tokenPct) {
260
+ const pct24h = Math.round((tokens / tokenLimit) * 100);
261
+ return [
262
+ ` Total Tokens (24h): ${formatNumber(tokens)} (${pct24h}% of 5h limit)`,
263
+ ` 5h Window Usage: ${tokenPct}% of ${formatNumber(tokenLimit)}`,
264
+ ];
265
+ }
266
+ function formatModelUsageLines(totalUsage, quotaData) {
267
+ const lines = [];
268
+ const { tokenLimit, tokenPct } = getTokenLimitInfo(quotaData);
269
+ const calls = totalUsage.totalModelCallCount;
270
+ const tokens = totalUsage.totalTokensUsage;
271
+ if (tokens !== undefined) {
272
+ lines.push(...formatTokenUsageLines(tokens, tokenLimit, tokenPct));
273
+ }
274
+ if (calls !== undefined) {
275
+ lines.push(` Total Calls: ${formatNumber(calls)}`);
276
+ }
277
+ return lines;
278
+ }
279
+ /**
280
+ * Format model usage data as readable lines
281
+ */
282
+ function formatModelUsage(data, quotaData) {
283
+ const lines = [];
284
+ const totalUsage = data.totalUsage;
285
+ if (!totalUsage) {
286
+ lines.push(' No usage data');
287
+ return lines;
288
+ }
289
+ return formatModelUsageLines(totalUsage, quotaData);
290
+ }
291
+ /**
292
+ * Format tool usage data as readable lines
293
+ */
294
+ function formatToolUsageSummaryLines(totalUsage) {
295
+ if (!totalUsage)
296
+ return [];
297
+ const search = totalUsage.totalNetworkSearchCount;
298
+ const webRead = totalUsage.totalWebReadMcpCount;
299
+ const zread = totalUsage.totalZreadMcpCount;
300
+ return [
301
+ ...(search !== undefined ? [` Network Searches: ${formatNumber(search)}`] : []),
302
+ ...(webRead !== undefined ? [` Web Reads: ${formatNumber(webRead)}`] : []),
303
+ ...(zread !== undefined ? [` ZRead Calls: ${formatNumber(zread)}`] : []),
304
+ ];
305
+ }
306
+ function formatMcpUsageDetailLines(quotaData) {
307
+ if (!quotaData?.limits)
308
+ return [];
309
+ for (const limit of quotaData.limits) {
310
+ if (limit.type === MCP_LIMIT_LABEL && limit.usageDetails) {
311
+ const details = limit.usageDetails;
312
+ return [' MCP Tool Details:', ...formatMcpToolLines(details)];
313
+ }
314
+ }
315
+ return [];
316
+ }
317
+ function formatToolUsage(data, quotaData) {
318
+ const totalUsage = data.totalUsage;
319
+ const lines = [
320
+ ...formatToolUsageSummaryLines(totalUsage),
321
+ ...formatMcpUsageDetailLines(quotaData),
322
+ ];
323
+ if (lines.length === 0) {
324
+ return [' No usage data'];
325
+ }
326
+ return lines;
327
+ }
328
+ // ============================================================================
329
+ // HELPER FUNCTIONS FOR OUTPUT FORMATTING
330
+ // ============================================================================
331
+ /**
332
+ * Format a single line with box characters
333
+ * @param content - Content to display (without padding)
334
+ * @param lineIndent - Total line width after padding
335
+ * @returns Formatted line with box characters
336
+ */
337
+ function formatBoxLine(content, lineIndent) {
338
+ const trimmed = trimToDisplayWidth(content, lineIndent, 0);
339
+ const padding = Math.max(lineIndent - getDisplayWidth(trimmed), 0);
340
+ return '║ ' + trimmed + ' '.repeat(padding) + '║';
341
+ }
342
+ function formatProgressBoxLine(content, lineIndent) {
343
+ const gap = 2;
344
+ const contentWidth = Math.max(lineIndent - gap, 0);
345
+ const trimmed = trimToDisplayWidth(content, contentWidth, 0);
346
+ const padding = Math.max(contentWidth - getDisplayWidth(trimmed), 0);
347
+ return '║ ' + trimmed + ' '.repeat(padding + gap) + '║';
348
+ }
349
+ function getDisplayWidth(text) {
350
+ let width = 0;
351
+ for (let i = 0; i < text.length; i += 1) {
352
+ const codePoint = text.codePointAt(i);
353
+ if (codePoint === undefined) {
354
+ continue;
355
+ }
356
+ if (codePoint > 0xffff) {
357
+ i += 1;
358
+ }
359
+ if (isControlCodePoint(codePoint) || isZeroWidthCodePoint(codePoint)) {
360
+ continue;
361
+ }
362
+ width += isEmojiCodePoint(codePoint) || isFullWidthCodePoint(codePoint) ? 2 : 1;
363
+ }
364
+ return width;
365
+ }
366
+ function trimToDisplayWidth(text, maxWidth, reserveRightPadding) {
367
+ let width = 0;
368
+ let result = '';
369
+ const allowedWidth = Math.max(maxWidth - reserveRightPadding, 0);
370
+ for (let i = 0; i < text.length; i += 1) {
371
+ const codePoint = text.codePointAt(i);
372
+ if (codePoint === undefined) {
373
+ continue;
374
+ }
375
+ if (codePoint > 0xffff) {
376
+ i += 1;
377
+ }
378
+ if (isControlCodePoint(codePoint) || isZeroWidthCodePoint(codePoint)) {
379
+ continue;
380
+ }
381
+ const charWidth = isEmojiCodePoint(codePoint) || isFullWidthCodePoint(codePoint) ? 2 : 1;
382
+ if (width + charWidth > allowedWidth) {
383
+ break;
384
+ }
385
+ result += String.fromCodePoint(codePoint);
386
+ width += charWidth;
387
+ }
388
+ return result;
389
+ }
390
+ function isControlCodePoint(codePoint) {
391
+ return codePoint <= 0x1f || (codePoint >= 0x7f && codePoint <= 0x9f);
392
+ }
393
+ function isZeroWidthCodePoint(codePoint) {
394
+ return (codePoint === 0x200d
395
+ || codePoint === 0xfe0f
396
+ || (codePoint >= 0xfe00 && codePoint <= 0xfe0f));
397
+ }
398
+ function isEmojiCodePoint(codePoint) {
399
+ return ((codePoint >= 0x1f300 && codePoint <= 0x1f5ff)
400
+ || (codePoint >= 0x1f600 && codePoint <= 0x1f64f)
401
+ || (codePoint >= 0x1f680 && codePoint <= 0x1f6ff)
402
+ || (codePoint >= 0x1f700 && codePoint <= 0x1f77f)
403
+ || (codePoint >= 0x1f780 && codePoint <= 0x1f7ff)
404
+ || (codePoint >= 0x1f800 && codePoint <= 0x1f8ff)
405
+ || (codePoint >= 0x1f900 && codePoint <= 0x1f9ff)
406
+ || (codePoint >= 0x1fa00 && codePoint <= 0x1faff)
407
+ || (codePoint >= 0x2600 && codePoint <= 0x26ff)
408
+ || (codePoint >= 0x2700 && codePoint <= 0x27bf));
409
+ }
410
+ function isFullWidthCodePoint(codePoint) {
411
+ return (codePoint >= 0x1100 && (codePoint <= 0x115f
412
+ || codePoint === 0x2329
413
+ || codePoint === 0x232a
414
+ || (codePoint >= 0x2e80 && codePoint <= 0x3247 && codePoint !== 0x303f)
415
+ || (codePoint >= 0x3250 && codePoint <= 0x4dbf)
416
+ || (codePoint >= 0x4e00 && codePoint <= 0xa4c6)
417
+ || (codePoint >= 0xa960 && codePoint <= 0xa97c)
418
+ || (codePoint >= 0xac00 && codePoint <= 0xd7a3)
419
+ || (codePoint >= 0xf900 && codePoint <= 0xfaff)
420
+ || (codePoint >= 0xfe10 && codePoint <= 0xfe19)
421
+ || (codePoint >= 0xfe30 && codePoint <= 0xfe6b)
422
+ || (codePoint >= 0xff01 && codePoint <= 0xff60)
423
+ || (codePoint >= 0xffe0 && codePoint <= 0xffe6)
424
+ || (codePoint >= 0x1b000 && codePoint <= 0x1b001)
425
+ || (codePoint >= 0x1f200 && codePoint <= 0x1f251)
426
+ || (codePoint >= 0x20000 && codePoint <= 0x3fffd)));
427
+ }
428
+ /**
429
+ * Format header section
430
+ * @param platformName - Platform name
431
+ * @param startTime - Start time string
432
+ * @param endTime - End time string
433
+ * @returns Array of header lines
434
+ */
435
+ function formatHeader(platformName, startTime, endTime) {
436
+ const lines = [];
437
+ lines.push('╔' + '═'.repeat(BOX_WIDTH.BORDER_CHARS) + '╗');
438
+ lines.push('║' + ' '.repeat(BOX_WIDTH.BORDER_CHARS) + '║');
439
+ lines.push('║' + ' Z.ai GLM Coding Plan Usage Statistics '.padStart(HEADER.TITLE_PAD_START).padEnd(BOX_WIDTH.BORDER_CHARS)
440
+ + '║');
441
+ lines.push('║' + ' '.repeat(BOX_WIDTH.BORDER_CHARS) + '║');
442
+ lines.push('╠' + '═'.repeat(BOX_WIDTH.BORDER_CHARS) + '╣');
443
+ lines.push(formatBoxLine(`Platform: ${platformName}`, BOX_WIDTH.CONTENT));
444
+ lines.push(formatBoxLine(`Period: ${startTime} → ${endTime}`, BOX_WIDTH.CONTENT));
445
+ lines.push('╠' + '═'.repeat(BOX_WIDTH.BORDER_CHARS) + '╣');
446
+ return lines;
447
+ }
448
+ /**
449
+ * Format quota limits section
450
+ * @param quotaData - Quota limit data
451
+ * @returns Array of quota lines
452
+ */
453
+ function formatQuotaLimits(quotaData) {
454
+ const lines = [];
455
+ lines.push(formatBoxLine('QUOTA LIMITS', BOX_WIDTH.CONTENT));
456
+ lines.push('╟' + '─'.repeat(BOX_WIDTH.BORDER_CHARS) + '╢');
457
+ const limits = quotaData?.limits;
458
+ if (limits && Array.isArray(limits)) {
459
+ for (const limit of limits) {
460
+ const pct = typeof limit.percentage === 'number' ? limit.percentage : 0;
461
+ const line = formatProgressLine(limit.type || 'Unknown', pct);
462
+ lines.push(formatProgressBoxLine(line, BOX_WIDTH.CONTENT));
463
+ if (limit.nextResetTime !== undefined) {
464
+ const resetMsg = formatTimeUntilReset(limit.nextResetTime);
465
+ if (resetMsg) {
466
+ lines.push(formatBoxLine(resetMsg, BOX_WIDTH.CONTENT));
467
+ }
468
+ }
469
+ if (limit.currentValue !== undefined && limit.total !== undefined) {
470
+ const usageStr = ' Used: ' + limit.currentValue + '/' + limit.total;
471
+ lines.push(formatBoxLine(usageStr, BOX_WIDTH.CONTENT));
472
+ }
473
+ }
474
+ }
475
+ else {
476
+ lines.push(formatBoxLine('No quota data available', BOX_WIDTH.CONTENT));
477
+ }
478
+ lines.push('╠' + '═'.repeat(BOX_WIDTH.BORDER_CHARS) + '╣');
479
+ return lines;
480
+ }
481
+ /**
482
+ * Format data section with optional data
483
+ * @param title - Section title
484
+ * @param data - Data object or null
485
+ * @param formatter - Function to format data if present
486
+ * @param noDataMessage - Message to show if no data
487
+ * @param LINE_INDENT - Line indent width
488
+ * @returns Array of section lines
489
+ */
490
+ function formatDataSection(title, data, formatter, quotaData, noDataMessage, LINE_INDENT) {
491
+ const lines = [];
492
+ lines.push(formatBoxLine(title, LINE_INDENT));
493
+ lines.push('╟' + '─'.repeat(BOX_WIDTH.BORDER_CHARS) + '╢');
494
+ if (data) {
495
+ const formattedLines = formatter(data, quotaData);
496
+ for (const line of formattedLines) {
497
+ lines.push(formatBoxLine(line, LINE_INDENT));
498
+ }
499
+ }
500
+ else {
501
+ lines.push(formatBoxLine(noDataMessage, LINE_INDENT));
502
+ }
503
+ lines.push('╠' + '═'.repeat(BOX_WIDTH.BORDER_CHARS) + '╣');
504
+ return lines;
505
+ }
506
+ /**
507
+ * Format footer section
508
+ * @returns Footer lines
509
+ */
510
+ function formatFooter() {
511
+ return ['╚' + '═'.repeat(BOX_WIDTH.BORDER_CHARS) + '╝'];
512
+ }
513
+ /**
514
+ * Format usage statistics as ASCII table
515
+ * @param platform - Platform name
516
+ * @param startTime - Start time string
517
+ * @param endTime - End time string
518
+ * @param quotaData - Quota limit data
519
+ * @param modelData - Model usage data
520
+ * @param toolData - Tool usage data
521
+ * @returns Formatted output string
522
+ */
523
+ function formatOutput(platform, startTime, endTime, quotaData, modelData, toolData) {
524
+ const lines = [];
525
+ const platformName = getPlatformName(platform);
526
+ lines.push(...formatHeader(platformName, startTime, endTime));
527
+ lines.push(...formatQuotaLimits(quotaData));
528
+ lines.push(...formatDataSection('MODEL USAGE (24h)', modelData, formatModelUsage, quotaData, 'No model usage data available', BOX_WIDTH.CONTENT));
529
+ lines.push(...formatDataSection('TOOL/MCP USAGE (24h)', toolData, formatToolUsage, quotaData, 'No tool usage data available', BOX_WIDTH.CONTENT));
530
+ lines.push(...formatFooter());
531
+ return lines.join('\n');
532
+ }
533
+ // ============================================================================
534
+ // MAIN QUERY FUNCTION
535
+ // ============================================================================
536
+ /**
537
+ * Query all usage statistics
538
+ * @param credentials - API credentials
539
+ * @returns Formatted output string
540
+ */
541
+ async function queryAllUsage(credentials) {
542
+ const { token, platform } = credentials;
543
+ const endpoints = getEndpoints(platform);
544
+ const { startTime, endTime } = getTimeWindow();
545
+ const queryParams = getTimeWindowQueryParams();
546
+ // Query all endpoints
547
+ const [quotaResponse, modelResponse, toolResponse] = await Promise.all([
548
+ queryEndpoint(endpoints, token, 'quotaLimit').catch(() => null),
549
+ queryEndpoint(endpoints, token, 'modelUsage', queryParams).catch(() => null),
550
+ queryEndpoint(endpoints, token, 'toolUsage', queryParams).catch(() => null),
551
+ ]);
552
+ // Process responses
553
+ const quotaData = quotaResponse
554
+ ? processQuotaLimit(quotaResponse.data)
555
+ : null;
556
+ const modelData = modelResponse
557
+ ? (modelResponse.data || modelResponse)
558
+ : null;
559
+ const toolData = toolResponse
560
+ ? (toolResponse.data || toolResponse)
561
+ : null;
562
+ return formatOutput(platform, startTime, endTime, quotaData, modelData, toolData);
563
+ }
564
+ // ============================================================================
565
+ // PLUGIN EXPPORT
566
+ // ============================================================================
567
+ // ============================================================================
568
+ // GitHub Copilot Pro Support
569
+ // ============================================================================
570
+ /**
571
+ * Get GitHub credentials from environment variables
572
+ */
573
+ /**
574
+ * Get GitHub credentials from OpenCode auth or environment variables
575
+ * Automatically detects username from token if not provided
576
+ */
577
+ async function getGithubCredentials() {
578
+ // Try OpenCode auth.json first
579
+ try {
580
+ const authPath = path.join(os.homedir(), '.local', 'share', 'opencode', 'auth.json');
581
+ const authContent = fs.readFileSync(authPath, 'utf8');
582
+ const auth = JSON.parse(authContent);
583
+ if (auth['github-copilot']?.access) {
584
+ const token = auth['github-copilot'].access;
585
+ // Query GitHub API to get username from token
586
+ const username = await getGitHubUsername(token);
587
+ if (username) {
588
+ return { token, username };
589
+ }
590
+ }
591
+ }
592
+ catch {
593
+ // Fall through to env vars
594
+ }
595
+ // Fallback to environment variables
596
+ const token = process.env.GITHUB_TOKEN;
597
+ const username = process.env.GITHUB_USERNAME;
598
+ if (!token || !username) {
599
+ return null;
600
+ }
601
+ return { token, username };
602
+ }
603
+ /**
604
+ * Query GitHub API to get authenticated user's username
605
+ */
606
+ async function getGitHubUsername(token) {
607
+ try {
608
+ const response = await fetch('https://api.github.com/user', {
609
+ headers: {
610
+ 'Accept': 'application/vnd.github+json',
611
+ 'Authorization': `Bearer ${token}`,
612
+ 'X-GitHub-Api-Version': '2022-11-28',
613
+ },
614
+ });
615
+ if (!response.ok) {
616
+ return null;
617
+ }
618
+ const data = await response.json();
619
+ return data.login;
620
+ }
621
+ catch {
622
+ return null;
623
+ }
624
+ }
625
+ /**
626
+ * Format GitHub Copilot usage as ASCII boxed table
627
+ */
628
+ function formatGitHubOutput(data) {
629
+ const { copilot_plan, chat_enabled, quota_reset_date, quota_snapshots } = data;
630
+ const BOX_WIDTH = 58;
631
+ let output = '';
632
+ // Header
633
+ output += '╔' + '═'.repeat(BOX_WIDTH - 2) + '╗\n';
634
+ output += '║ ║\n';
635
+ output += '║ GitHub Copilot Usage Statistics ║\n';
636
+ output += '║ ║\n';
637
+ output += '╠' + '═'.repeat(BOX_WIDTH - 2) + '╣\n';
638
+ output += '║ Platform: GitHub ║\n';
639
+ output += `║ Plan: ${copilot_plan.padEnd(50)}║\n`;
640
+ output += `║ Chat: ${chat_enabled ? 'Enabled' : 'Disabled'.padEnd(49)}║\n`;
641
+ output += `║ Reset: ${quota_reset_date.substring(0, 10).padEnd(50)}║\n`;
642
+ output += '╠' + '═'.repeat(BOX_WIDTH - 2) + '╣\n';
643
+ output += '║ QUOTA USAGE ║\n';
644
+ output += '╟──────────────────────────────────────────────────────────╢\n';
645
+ // Format each quota type
646
+ const quotas = [
647
+ { name: 'Completions', data: quota_snapshots.completions },
648
+ { name: 'Chat', data: quota_snapshots.chat },
649
+ { name: 'Premium', data: quota_snapshots.premium_interactions },
650
+ ];
651
+ for (const quota of quotas) {
652
+ const { name, data } = quota;
653
+ const remaining = data.unlimited ? 'Unlimited' : data.quota_remaining.toLocaleString();
654
+ const percent = data.unlimited ? 100 : data.percent_remaining;
655
+ const entitlement = data.unlimited ? 'Unlimited' : data.entitlement.toLocaleString();
656
+ // Progress bar
657
+ const filled = Math.floor(percent / 5);
658
+ const empty = 20 - filled;
659
+ const progress = '#'.repeat(filled) + '-'.repeat(empty);
660
+ const percentStr = data.unlimited ? '∞' : `${percent}%`;
661
+ output += `║ ${name}:`.padEnd(12);
662
+ output += `${remaining.padEnd(15)}${progress} ${percentStr.padEnd(4)}║\n`;
663
+ output += `║ Entitlement: ${entitlement.padEnd(36)}║\n`;
664
+ if (data.overage_permitted && !data.unlimited) {
665
+ output += `║ Overage: ${data.overage_count.toLocaleString()} used${' '.repeat(41)}║\n`;
666
+ }
667
+ }
668
+ output += '╚' + '═'.repeat(BOX_WIDTH - 2) + '╝\n';
669
+ return output;
670
+ }
671
+ export const GlmQuotaPlugin = async () => {
672
+ return {
673
+ tool: {
674
+ glm_quota: tool({
675
+ description: 'Query Z.ai GLM Coding Plan and GitHub Copilot Pro usage statistics - automatically shows quota every 5 minutes during chat',
676
+ args: {},
677
+ async execute() {
678
+ try {
679
+ const credentials = await getCredentials();
680
+ if (!credentials) {
681
+ return createCredentialError();
682
+ }
683
+ return await queryAllUsage(credentials);
684
+ }
685
+ catch (error) {
686
+ const errorMessage = error instanceof Error ? error.message : String(error);
687
+ // If error message is already boxed (starts with top border), return it as is
688
+ if (errorMessage.trim().startsWith('╔') && errorMessage.includes('╚')) {
689
+ return errorMessage;
690
+ }
691
+ // Otherwise, wrap raw error in a box
692
+ return createBoxedError(errorMessage);
693
+ }
694
+ },
695
+ }),
696
+ copilot_quota: tool({
697
+ description: 'Query GitHub Copilot Pro usage statistics including monthly requests, costs, and model usage',
698
+ args: {},
699
+ async execute() {
700
+ try {
701
+ const credentials = await getGithubCredentials();
702
+ if (!credentials) {
703
+ return createGitHubCredentialError();
704
+ }
705
+ const usage = await queryGitHubUsage(credentials.token);
706
+ return formatGitHubOutput(usage);
707
+ }
708
+ catch (error) {
709
+ const errorMessage = error instanceof Error ? error.message : String(error);
710
+ // If error message is already boxed (starts with top border), return it as is
711
+ if (errorMessage.trim().startsWith('╔') && errorMessage.includes('╚')) {
712
+ return errorMessage;
713
+ }
714
+ // Otherwise, wrap raw error in a box
715
+ return createBoxedError(errorMessage);
716
+ }
717
+ },
718
+ }),
719
+ },
720
+ };
721
+ };
722
+ export default GlmQuotaPlugin;
723
+ //# sourceMappingURL=index.js.map