opencode-glm-quota 1.1.0 → 1.2.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/dist/api/client.js +1 -1
- package/dist/index.js +134 -78
- package/dist/utils/date-formatter.js +3 -2
- package/dist/utils/reset-timer.js +31 -0
- package/package.json +9 -6
package/dist/api/client.js
CHANGED
package/dist/index.js
CHANGED
|
@@ -13,6 +13,7 @@ import { getEndpoints } from "./api/endpoints.js";
|
|
|
13
13
|
import { queryEndpoint } from "./api/client.js";
|
|
14
14
|
import { getTimeWindow, getTimeWindowQueryParams } from "./utils/time-window.js";
|
|
15
15
|
import { formatProgressLine } from "./utils/progress-bar.js";
|
|
16
|
+
import { formatTimeUntilReset } from "./utils/reset-timer.js";
|
|
16
17
|
// ============================================================================
|
|
17
18
|
// CONSTANTS
|
|
18
19
|
// ============================================================================
|
|
@@ -128,7 +129,8 @@ function processQuotaLimit(data) {
|
|
|
128
129
|
if (limit.type === 'TOKENS_LIMIT') {
|
|
129
130
|
return {
|
|
130
131
|
type: 'Token usage(5 Hour)',
|
|
131
|
-
percentage: typeof limit.percentage === 'number' ? limit.percentage : 0
|
|
132
|
+
percentage: typeof limit.percentage === 'number' ? limit.percentage : 0,
|
|
133
|
+
nextResetTime: limit.nextResetTime
|
|
132
134
|
};
|
|
133
135
|
}
|
|
134
136
|
if (limit.type === 'TIME_LIMIT') {
|
|
@@ -156,38 +158,55 @@ function formatNumber(num) {
|
|
|
156
158
|
return num.toLocaleString('en-US');
|
|
157
159
|
}
|
|
158
160
|
/**
|
|
159
|
-
*
|
|
161
|
+
* Get token limit information from quota data
|
|
160
162
|
*/
|
|
161
|
-
function
|
|
162
|
-
const lines = [];
|
|
163
|
-
const totalUsage = data.totalUsage;
|
|
164
|
-
// Find token limit info from quota
|
|
163
|
+
function getTokenLimitInfo(quotaData) {
|
|
165
164
|
let tokenLimit = 40000000; // Default 40M
|
|
166
165
|
let tokenPct = 0;
|
|
167
|
-
if (quotaData?.limits)
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
166
|
+
if (!quotaData?.limits)
|
|
167
|
+
return { tokenLimit, tokenPct };
|
|
168
|
+
for (const limit of quotaData.limits) {
|
|
169
|
+
if (limit.type === 'Token usage(5 Hour)') {
|
|
170
|
+
tokenPct = typeof limit.percentage === 'number' ? limit.percentage : 0;
|
|
171
|
+
tokenLimit = limit.total || 40000000;
|
|
172
|
+
break;
|
|
174
173
|
}
|
|
175
174
|
}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
}
|
|
175
|
+
return { tokenLimit, tokenPct };
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Format MCP tool details as readable lines
|
|
179
|
+
*/
|
|
180
|
+
function formatMcpToolLines(details) {
|
|
181
|
+
const lines = [];
|
|
182
|
+
const mcpTotal = details.reduce((sum, d) => sum + (d.usage || 0), 0);
|
|
183
|
+
for (const d of details) {
|
|
184
|
+
const pct = mcpTotal > 0 ? Math.round((d.usage / mcpTotal) * 100) : 0;
|
|
185
|
+
lines.push(` - ${d.modelCode}: ${d.usage} (${pct}%)`);
|
|
188
186
|
}
|
|
189
|
-
|
|
187
|
+
return lines;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Format model usage data as readable lines
|
|
191
|
+
*/
|
|
192
|
+
function formatModelUsage(data, quotaData) {
|
|
193
|
+
const lines = [];
|
|
194
|
+
const totalUsage = data.totalUsage;
|
|
195
|
+
// Get token limit info from quota
|
|
196
|
+
const { tokenLimit, tokenPct } = getTokenLimitInfo(quotaData);
|
|
197
|
+
if (!totalUsage) {
|
|
190
198
|
lines.push(' No usage data');
|
|
199
|
+
return lines;
|
|
200
|
+
}
|
|
201
|
+
const calls = totalUsage.totalModelCallCount;
|
|
202
|
+
const tokens = totalUsage.totalTokensUsage;
|
|
203
|
+
if (tokens !== undefined) {
|
|
204
|
+
// Show 24h tokens and percentage relative to 5h limit
|
|
205
|
+
const pct24h = Math.round((tokens / tokenLimit) * 100);
|
|
206
|
+
lines.push(` Total Tokens (24h): ${formatNumber(tokens)} (${pct24h}% of 5h limit)`, ` 5h Window Usage: ${tokenPct}% of ${formatNumber(tokenLimit)}`);
|
|
207
|
+
}
|
|
208
|
+
if (calls !== undefined) {
|
|
209
|
+
lines.push(` Total Calls: ${formatNumber(calls)}`);
|
|
191
210
|
}
|
|
192
211
|
return lines;
|
|
193
212
|
}
|
|
@@ -202,24 +221,15 @@ function formatToolUsage(data, quotaData) {
|
|
|
202
221
|
const search = totalUsage.totalNetworkSearchCount;
|
|
203
222
|
const webRead = totalUsage.totalWebReadMcpCount;
|
|
204
223
|
const zread = totalUsage.totalZreadMcpCount;
|
|
205
|
-
|
|
206
|
-
lines.push(` Network Searches: ${formatNumber(search)}`);
|
|
207
|
-
if (webRead !== undefined)
|
|
208
|
-
lines.push(` Web Reads: ${formatNumber(webRead)}`);
|
|
209
|
-
if (zread !== undefined)
|
|
210
|
-
lines.push(` ZRead Calls: ${formatNumber(zread)}`);
|
|
224
|
+
lines.push(...(search !== undefined ? [` Network Searches: ${formatNumber(search)}`] : []), ...(webRead !== undefined ? [` Web Reads: ${formatNumber(webRead)}`] : []), ...(zread !== undefined ? [` ZRead Calls: ${formatNumber(zread)}`] : []));
|
|
211
225
|
}
|
|
212
226
|
// Show MCP usage details from quota if available
|
|
213
227
|
if (quotaData?.limits) {
|
|
214
228
|
for (const limit of quotaData.limits) {
|
|
215
229
|
if (limit.type === 'MCP usage(1 Month)' && limit.usageDetails) {
|
|
216
|
-
const details = limit.usageDetails;
|
|
217
230
|
lines.push(' MCP Tool Details:');
|
|
218
|
-
const
|
|
219
|
-
|
|
220
|
-
const pct = mcpTotal > 0 ? Math.round((d.usage / mcpTotal) * 100) : 0;
|
|
221
|
-
lines.push(` - ${d.modelCode}: ${d.usage} (${pct}%)`);
|
|
222
|
-
}
|
|
231
|
+
const details = limit.usageDetails;
|
|
232
|
+
lines.push(...formatMcpToolLines(details));
|
|
223
233
|
break;
|
|
224
234
|
}
|
|
225
235
|
}
|
|
@@ -229,46 +239,65 @@ function formatToolUsage(data, quotaData) {
|
|
|
229
239
|
}
|
|
230
240
|
return lines;
|
|
231
241
|
}
|
|
242
|
+
// ============================================================================
|
|
243
|
+
// HELPER FUNCTIONS FOR OUTPUT FORMATTING
|
|
244
|
+
// ============================================================================
|
|
232
245
|
/**
|
|
233
|
-
* Format
|
|
234
|
-
* @param
|
|
246
|
+
* Format a single line with box characters
|
|
247
|
+
* @param content - Content to display (without padding)
|
|
248
|
+
* @param lineIndent - Total line width after padding
|
|
249
|
+
* @returns Formatted line with box characters
|
|
250
|
+
*/
|
|
251
|
+
function formatBoxLine(content, lineIndent) {
|
|
252
|
+
return '║ ' + content.padEnd(lineIndent) + '║';
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Format header section
|
|
256
|
+
* @param platformName - Platform name
|
|
235
257
|
* @param startTime - Start time string
|
|
236
258
|
* @param endTime - End time string
|
|
237
|
-
* @
|
|
238
|
-
* @param modelData - Model usage data
|
|
239
|
-
* @param toolData - Tool usage data
|
|
240
|
-
* @returns Formatted output string
|
|
259
|
+
* @returns Array of header lines
|
|
241
260
|
*/
|
|
242
|
-
function
|
|
261
|
+
function formatHeader(platformName, startTime, endTime) {
|
|
243
262
|
const lines = [];
|
|
244
|
-
const platformName = getPlatformName(platform);
|
|
245
|
-
// Constants for line width (total 60 chars)
|
|
246
263
|
const LINE_WIDTH = 60;
|
|
247
|
-
const LINE_CONTENT = 58; // Between ║ and ║
|
|
248
|
-
const LINE_INDENT = 56; // After "║ "
|
|
249
|
-
// Header
|
|
250
264
|
lines.push('╔' + '═'.repeat(58) + '╗');
|
|
251
265
|
lines.push('║' + ' '.repeat(58) + '║');
|
|
252
266
|
lines.push('║' + ' Z.ai GLM Coding Plan Usage Statistics '.padStart(35).padEnd(58) + '║');
|
|
253
267
|
lines.push('║' + ' '.repeat(58) + '║');
|
|
254
268
|
lines.push('╠' + '═'.repeat(58) + '╣');
|
|
255
|
-
// Platform line: "║ Platform: " (13 chars) + name + padding + "║"
|
|
256
269
|
lines.push('║ Platform: ' + platformName.padEnd(LINE_WIDTH - 13 - 1) + '║');
|
|
257
|
-
// Period line: "║ Period: " (14 chars) + start + " → " + end + "║"
|
|
258
270
|
const periodLine = '║ Period: ' + startTime + ' → ' + endTime;
|
|
259
271
|
lines.push(periodLine.padEnd(LINE_WIDTH) + '║');
|
|
260
272
|
lines.push('╠' + '═'.repeat(58) + '╣');
|
|
261
|
-
|
|
273
|
+
return lines;
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Format quota limits section
|
|
277
|
+
* @param quotaData - Quota limit data
|
|
278
|
+
* @returns Array of quota lines
|
|
279
|
+
*/
|
|
280
|
+
function formatQuotaLimits(quotaData) {
|
|
281
|
+
const lines = [];
|
|
282
|
+
const LINE_CONTENT = 58;
|
|
283
|
+
const LINE_INDENT = 56;
|
|
262
284
|
lines.push('║ 📊 QUOTA LIMITS' + ' '.repeat(LINE_CONTENT - 14) + '║');
|
|
263
285
|
lines.push('╟' + '─'.repeat(58) + '╢');
|
|
264
|
-
|
|
265
|
-
|
|
286
|
+
const limits = quotaData?.limits;
|
|
287
|
+
if (limits && Array.isArray(limits)) {
|
|
288
|
+
for (const limit of limits) {
|
|
266
289
|
const pct = typeof limit.percentage === 'number' ? limit.percentage : 0;
|
|
267
290
|
const line = formatProgressLine(limit.type || 'Unknown', pct);
|
|
268
|
-
lines.push(
|
|
291
|
+
lines.push(formatBoxLine(line, LINE_INDENT));
|
|
292
|
+
if (limit.nextResetTime !== undefined) {
|
|
293
|
+
const resetMsg = formatTimeUntilReset(limit.nextResetTime);
|
|
294
|
+
if (resetMsg) {
|
|
295
|
+
lines.push(formatBoxLine(resetMsg, LINE_INDENT));
|
|
296
|
+
}
|
|
297
|
+
}
|
|
269
298
|
if (limit.currentValue !== undefined && limit.total !== undefined) {
|
|
270
299
|
const usageStr = ' Used: ' + limit.currentValue + '/' + limit.total;
|
|
271
|
-
lines.push(
|
|
300
|
+
lines.push(formatBoxLine(usageStr, LINE_INDENT));
|
|
272
301
|
}
|
|
273
302
|
}
|
|
274
303
|
}
|
|
@@ -276,33 +305,60 @@ function formatOutput(platform, startTime, endTime, quotaData, modelData, toolDa
|
|
|
276
305
|
lines.push('║ No quota data available' + ' '.repeat(LINE_INDENT - 21) + '║');
|
|
277
306
|
}
|
|
278
307
|
lines.push('╠' + '═'.repeat(58) + '╣');
|
|
279
|
-
|
|
280
|
-
|
|
308
|
+
return lines;
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Format data section with optional data
|
|
312
|
+
* @param title - Section title
|
|
313
|
+
* @param data - Data object or null
|
|
314
|
+
* @param formatter - Function to format data if present
|
|
315
|
+
* @param noDataMessage - Message to show if no data
|
|
316
|
+
* @param LINE_INDENT - Line indent width
|
|
317
|
+
* @returns Array of section lines
|
|
318
|
+
*/
|
|
319
|
+
function formatDataSection(title, data, formatter, quotaData, noDataMessage, LINE_INDENT) {
|
|
320
|
+
const lines = [];
|
|
321
|
+
const LINE_CONTENT = 58;
|
|
322
|
+
lines.push('║ ' + title + ' '.repeat(LINE_CONTENT - title.length) + '║');
|
|
281
323
|
lines.push('╟' + '─'.repeat(58) + '╢');
|
|
282
|
-
if (
|
|
283
|
-
const
|
|
284
|
-
for (const line of
|
|
285
|
-
lines.push(
|
|
324
|
+
if (data) {
|
|
325
|
+
const formattedLines = formatter(data, quotaData);
|
|
326
|
+
for (const line of formattedLines) {
|
|
327
|
+
lines.push(formatBoxLine(line, LINE_INDENT));
|
|
286
328
|
}
|
|
287
329
|
}
|
|
288
330
|
else {
|
|
289
|
-
lines.push('║
|
|
331
|
+
lines.push('║ ' + noDataMessage + ' '.repeat(LINE_INDENT - noDataMessage.length) + '║');
|
|
290
332
|
}
|
|
291
333
|
lines.push('╠' + '═'.repeat(58) + '╣');
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
334
|
+
return lines;
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* Format footer section
|
|
338
|
+
* @returns Footer lines
|
|
339
|
+
*/
|
|
340
|
+
function formatFooter() {
|
|
341
|
+
return ['╚' + '═'.repeat(58) + '╝'];
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Format usage statistics as ASCII table
|
|
345
|
+
* @param platform - Platform name
|
|
346
|
+
* @param startTime - Start time string
|
|
347
|
+
* @param endTime - End time string
|
|
348
|
+
* @param quotaData - Quota limit data
|
|
349
|
+
* @param modelData - Model usage data
|
|
350
|
+
* @param toolData - Tool usage data
|
|
351
|
+
* @returns Formatted output string
|
|
352
|
+
*/
|
|
353
|
+
function formatOutput(platform, startTime, endTime, quotaData, modelData, toolData) {
|
|
354
|
+
const lines = [];
|
|
355
|
+
const platformName = getPlatformName(platform);
|
|
356
|
+
const LINE_INDENT = 56;
|
|
357
|
+
lines.push(...formatHeader(platformName, startTime, endTime));
|
|
358
|
+
lines.push(...formatQuotaLimits(quotaData));
|
|
359
|
+
lines.push(...formatDataSection('🤖 MODEL USAGE (24h)', modelData, formatModelUsage, quotaData, 'No model usage data available', LINE_INDENT));
|
|
360
|
+
lines.push(...formatDataSection('🔧 TOOL/MCP USAGE (24h)', toolData, formatToolUsage, quotaData, 'No tool usage data available', LINE_INDENT));
|
|
361
|
+
lines.push(...formatFooter());
|
|
306
362
|
return lines.join('\n');
|
|
307
363
|
}
|
|
308
364
|
// ============================================================================
|
|
@@ -22,10 +22,11 @@ export function formatDateTime(date) {
|
|
|
22
22
|
* @returns Date object
|
|
23
23
|
*/
|
|
24
24
|
export function parseDateTime(dateTime) {
|
|
25
|
-
const
|
|
25
|
+
const dateTimeRegex = /^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})$/;
|
|
26
|
+
const match = dateTimeRegex.exec(dateTime);
|
|
26
27
|
if (!match) {
|
|
27
28
|
throw new Error(`Invalid datetime format: ${dateTime}`);
|
|
28
29
|
}
|
|
29
30
|
const [, year, month, day, hours, minutes, seconds] = match;
|
|
30
|
-
return new Date(parseInt(year), parseInt(month) - 1, parseInt(day), parseInt(hours), parseInt(minutes), parseInt(seconds));
|
|
31
|
+
return new Date(Number.parseInt(year), Number.parseInt(month) - 1, Number.parseInt(day), Number.parseInt(hours), Number.parseInt(minutes), Number.parseInt(seconds));
|
|
31
32
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reset timer utility module
|
|
3
|
+
* Formats Unix timestamps to human-readable countdown strings
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Format time until reset as "X hours Y minutes"
|
|
7
|
+
* @param resetTime - Unix timestamp in milliseconds (from API's nextResetTime field)
|
|
8
|
+
* @returns Human-readable countdown string or empty string if invalid/past
|
|
9
|
+
*/
|
|
10
|
+
export function formatTimeUntilReset(resetTime) {
|
|
11
|
+
// Validate input
|
|
12
|
+
if (resetTime === null || resetTime === undefined) {
|
|
13
|
+
return '';
|
|
14
|
+
}
|
|
15
|
+
// Check if valid number
|
|
16
|
+
if (typeof resetTime !== 'number' || Number.isNaN(resetTime) || resetTime < 0) {
|
|
17
|
+
return '';
|
|
18
|
+
}
|
|
19
|
+
// Calculate time difference
|
|
20
|
+
const now = Date.now();
|
|
21
|
+
const diffMs = resetTime - now;
|
|
22
|
+
// Return empty for past timestamps
|
|
23
|
+
if (diffMs <= 0) {
|
|
24
|
+
return '';
|
|
25
|
+
}
|
|
26
|
+
// Convert to hours and minutes
|
|
27
|
+
const totalMinutes = Math.floor(diffMs / (1000 * 60));
|
|
28
|
+
const hours = Math.floor(totalMinutes / 60);
|
|
29
|
+
const minutes = totalMinutes % 60;
|
|
30
|
+
return `Resets in ${hours} hours ${minutes} minutes`;
|
|
31
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-glm-quota",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "OpenCode plugin to query Z.ai GLM Coding Plan usage statistics including quota limits, model usage, and MCP tool usage",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
"build": "tsc",
|
|
15
15
|
"clean": "rm -rf dist",
|
|
16
16
|
"prepublishOnly": "npm run clean && npm run build",
|
|
17
|
-
"test": "tsx --test",
|
|
17
|
+
"test": "tsx --test $(find tests -name '*.test.ts' -type f)",
|
|
18
|
+
"test:coverage": "c8 --reporter=lcov --reporter=text -- npm test",
|
|
18
19
|
"lint": "eslint src/"
|
|
19
20
|
},
|
|
20
21
|
"keywords": [
|
|
@@ -49,10 +50,12 @@
|
|
|
49
50
|
"devDependencies": {
|
|
50
51
|
"@opencode-ai/plugin": "latest",
|
|
51
52
|
"@types/node": "^20.0.0",
|
|
52
|
-
"tsx": "^4.0.0",
|
|
53
|
-
"typescript": "^5.0.0",
|
|
54
|
-
"eslint": "^8.0.0",
|
|
55
53
|
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
|
56
|
-
"@typescript-eslint/parser": "^6.0.0"
|
|
54
|
+
"@typescript-eslint/parser": "^6.0.0",
|
|
55
|
+
"baseline-browser-mapping": "^2.9.17",
|
|
56
|
+
"c8": "^10.1.3",
|
|
57
|
+
"eslint": "^8.0.0",
|
|
58
|
+
"tsx": "^4.0.0",
|
|
59
|
+
"typescript": "^5.0.0"
|
|
57
60
|
}
|
|
58
61
|
}
|