opencode-glm-quota 1.3.4 → 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -3,6 +3,12 @@
3
3
  [![npm version](https://img.shields.io/npm/v/opencode-glm-quota.svg)](https://www.npmjs.com/package/opencode-glm-quota)
4
4
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
5
  [![Build Status](https://github.com/guyinwonder168/opencode-glm-quota/workflows/CI/badge.svg)](https://github.com/guyinwonder168/opencode-glm-quota/actions)
6
+ [![SonarQube Cloud](https://sonarcloud.io/images/project_badges/sonarcloud-light.svg)](https://sonarcloud.io/summary/new_code?id=guyinwonder168_opencode-glm-quota)
7
+ ---
8
+ [![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)
9
+ [![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)
10
+ [![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)
11
+ [![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)
6
12
 
7
13
  OpenCode plugin to query Z.ai GLM Coding Plan usage statistics with real-time quota monitoring, model usage tracking, and MCP tool usage.
8
14
 
@@ -32,7 +38,7 @@ npx opencode-glm-quota install
32
38
  - Copies `/glm_quota` command to `~/.config/opencode/command/glm_quota.md`
33
39
  - Copies skill documentation to `~/.config/opencode/skills/glm-quota/SKILL.md`
34
40
  - Automatically adds plugin to your OpenCode config
35
- - Merges agent configuration into `~/.config/opencode/opencode.json`
41
+ - Copies agent to `~/.config/opencode/agents/`
36
42
  - Supports `--force` flag to overwrite existing files
37
43
 
38
44
  ### Uninstall
@@ -49,7 +55,7 @@ npx opencode-glm-quota uninstall --global
49
55
  - Removes `/glm_quota` command
50
56
  - Deletes `skills/glm-quota/SKILL.md`
51
57
  - Removes plugin entry from OpenCode config
52
- - Removes `glm-quota-exec` agent config
58
+ - Removes agent file and legacy config
53
59
  - Runs `npm remove opencode-glm-quota` (or `--global`)
54
60
 
55
61
  ### Option 2: From GitHub
@@ -252,9 +258,9 @@ src/
252
258
  progress-bar.ts # ASCII progress bar rendering
253
259
  time-window.ts # Rolling window calculation
254
260
  integration/
261
+ agents/glm-quota-exec.md # Minimal executor agent (Markdown)
255
262
  command/glm_quota.md # /glm_quota slash command
256
263
  skills/glm-quota/SKILL.md # Skill documentation
257
- opencode.jsonc # Agent configuration (JSONC)
258
264
  bin/
259
265
  install.js # Installation script
260
266
  dist/ # Compiled JavaScript (generated)
package/bin/install.js CHANGED
@@ -27,11 +27,12 @@ const __dirname = path.dirname(__filename)
27
27
  const SOURCE_DIR = path.join(__dirname, '..', 'integration')
28
28
  const COMMAND_FILE = path.join(SOURCE_DIR, 'command', 'glm_quota.md')
29
29
  const SKILL_FILE = path.join(SOURCE_DIR, 'skills', 'glm-quota', 'SKILL.md')
30
- const AGENT_CONFIG = path.join(SOURCE_DIR, 'opencode.jsonc')
30
+ const AGENT_FILE = path.join(SOURCE_DIR, 'agents', 'glm-quota-exec.md')
31
31
 
32
32
  const CONFIG_DIR = path.join(os.homedir(), '.config', 'opencode')
33
33
  const TARGET_COMMAND = path.join(CONFIG_DIR, 'command', 'glm_quota.md')
34
34
  const TARGET_SKILL = path.join(CONFIG_DIR, 'skills', 'glm-quota', 'SKILL.md')
35
+ const TARGET_AGENT = path.join(CONFIG_DIR, 'agents', 'glm-quota-exec.md')
35
36
 
36
37
  // Check which config file exists (opencode.json or opencode.jsonc)
37
38
  const TARGET_CONFIG_JSON = path.join(CONFIG_DIR, 'opencode.json')
@@ -181,25 +182,42 @@ function installSkill(force) {
181
182
  }
182
183
 
183
184
  /**
184
- * Merge agent configuration and add plugin to plugins array
185
+ * Install agent file
185
186
  */
186
- function mergeConfig() {
187
- // Parse existing config if it exists (same file type will be written)
187
+ function installAgent(force) {
188
+ if (fileExists(TARGET_AGENT) && !force) {
189
+ if (!promptConfirm(`Agent file exists: ${TARGET_AGENT}\nOverwrite?`)) {
190
+ console.log(` ⊘ Skipped ${TARGET_AGENT}`)
191
+ return
192
+ }
193
+ }
194
+
195
+ copyFile(AGENT_FILE, TARGET_AGENT)
196
+ console.log(` ✓ Created ${TARGET_AGENT}`)
197
+ }
198
+
199
+ /**
200
+ * Update plugin configuration and cleanup old JSON agent
201
+ */
202
+ function updatePluginConfig() {
203
+ // Parse existing config if it exists
188
204
  let existingConfig = {}
189
205
  if (fileExists(TARGET_CONFIG)) {
190
206
  existingConfig = parseConfig(TARGET_CONFIG)
191
- // REMOVE old 'options' field to prevent verbose agent output
192
- if (existingConfig.agent?.['glm-quota-exec']?.options) {
193
- delete existingConfig.agent['glm-quota-exec'].options
194
- }
195
207
  }
196
208
 
197
- // Parse new agent config from integration
198
- const newConfig = parseConfig(AGENT_CONFIG)
199
-
200
209
  const PLUGIN_NAME = 'opencode-glm-quota'
201
210
 
202
- // Handle both "plugin" array and "agent" section
211
+ // CLEANUP: Remove old JSON agent config if it exists (migration from v1.3.x)
212
+ if (existingConfig.agent && existingConfig.agent['glm-quota-exec']) {
213
+ delete existingConfig.agent['glm-quota-exec']
214
+ if (Object.keys(existingConfig.agent).length === 0) {
215
+ delete existingConfig.agent
216
+ }
217
+ console.log(' ✓ Removed old JSON agent config (migrated to Markdown)')
218
+ }
219
+
220
+ // Handle both "plugin" array and "plugins" array
203
221
  // Check for "plugin" array first (user's config uses this)
204
222
  const pluginArrayName = existingConfig.plugin ? 'plugin' : 'plugins'
205
223
 
@@ -210,16 +228,6 @@ function mergeConfig() {
210
228
 
211
229
  const plugins = Array.isArray(existingConfig[pluginArrayName]) ? existingConfig[pluginArrayName] : []
212
230
 
213
- // REPLACE entire glm-quota-exec agent (not merge, to remove old redundant fields)
214
- if (newConfig.agent && newConfig.agent['glm-quota-exec']) {
215
- if (!existingConfig.agent) {
216
- existingConfig.agent = {}
217
- }
218
- existingConfig.agent['glm-quota-exec'] = newConfig.agent['glm-quota-exec']
219
- } else if (!existingConfig.agent && newConfig.agent) {
220
- existingConfig.agent = newConfig.agent
221
- }
222
-
223
231
  // Only add if not already present
224
232
  if (!plugins.includes(PLUGIN_NAME)) {
225
233
  plugins.push(PLUGIN_NAME)
@@ -229,9 +237,9 @@ function mergeConfig() {
229
237
  console.log(` ⊙ Plugin ${PLUGIN_NAME} already in ${pluginArrayName} array`)
230
238
  }
231
239
 
232
- // Write merged config back to same file (opencode.json or opencode.jsonc)
240
+ // Write config back to same file (opencode.json or opencode.jsonc)
233
241
  writeConfig(TARGET_CONFIG, existingConfig)
234
- console.log(` ✓ Merged configuration into ${path.basename(TARGET_CONFIG)}`)
242
+ console.log(` ✓ Updated ${path.basename(TARGET_CONFIG)}`)
235
243
  }
236
244
 
237
245
  /**
@@ -303,6 +311,7 @@ function removePackage(globalFlag) {
303
311
  function uninstall(globalFlag) {
304
312
  removeFile(TARGET_COMMAND, TARGET_COMMAND)
305
313
  removeDirectory(path.dirname(TARGET_SKILL), path.dirname(TARGET_SKILL))
314
+ removeFile(TARGET_AGENT, TARGET_AGENT)
306
315
  removeConfig()
307
316
  removePackage(globalFlag)
308
317
  }
@@ -335,7 +344,8 @@ function main() {
335
344
  // Install integration files
336
345
  installCommand(forceFlag)
337
346
  installSkill(forceFlag)
338
- mergeConfig()
347
+ installAgent(forceFlag)
348
+ updatePluginConfig()
339
349
 
340
350
  console.log()
341
351
  console.log('✓ Installation complete!')
package/dist/index.js CHANGED
@@ -25,6 +25,17 @@ const CANDIDATE_PROVIDER_IDS = [
25
25
  'zhipu',
26
26
  'zhipuai'
27
27
  ];
28
+ /**
29
+ * Box drawing layout constants
30
+ * Total line width: LEFT_BORDER (1) + LEFT_PAD (2) + CONTENT (56) + RIGHT_PAD (0) + RIGHT_BORDER (1) = 60
31
+ * Border lines: ╔ + 58 chars + ╗ = 60 total
32
+ * Content lines: ║ + 2 spaces + 56 content + 2 spaces + ║ = 60 total (formatted by formatBoxLine)
33
+ */
34
+ const BOX_WIDTH = {
35
+ CONTENT: 56, // Available content width
36
+ BORDER_CHARS: 58, // Character count between borders (╔══...══╗)
37
+ TOTAL: 60 // Total line width including borders
38
+ };
28
39
  // ============================================================================
29
40
  // CREDENTIAL DISCOVERY
30
41
  // ============================================================================
@@ -97,20 +108,33 @@ async function getCredentials() {
97
108
  }
98
109
  /**
99
110
  * Create error message for missing credentials
100
- * @returns Error message with setup instructions
111
+ * @returns Error message with setup instructions (box formatted)
101
112
  */
102
113
  function createCredentialError() {
103
- return `❌ Z.ai Credentials Not Found
104
-
105
- Please authenticate first:
106
-
107
- 1. Run '/connect' command in OpenCode TUI
108
- 2. Select "Z.AI Coding Plan" or "Z.AI" (for global)
109
- 3. Or "Zhipu" (for China region)
110
-
111
- For development/testing, you can also set environment variables:
112
- - ZAI_API_KEY (global platform)
113
- - ZHIPU_API_KEY or ZHIPUAI_API_KEY (China platform)`;
114
+ const header = '❌ Z.ai Credentials Not Found';
115
+ const instructions = [
116
+ '',
117
+ 'Please authenticate first:',
118
+ '',
119
+ '1. Run /connect command in OpenCode TUI',
120
+ '2. Select "Z.AI Coding Plan" or "Z.AI"',
121
+ '3. Or "Zhipu" (for China region)',
122
+ '',
123
+ 'For dev/testing, set environment:',
124
+ '- ZAI_API_KEY (global)',
125
+ '- ZHIPU_API_KEY (China)',
126
+ ''
127
+ ];
128
+ const lines = [];
129
+ lines.push('╔' + '═'.repeat(BOX_WIDTH.BORDER_CHARS) + '╗');
130
+ lines.push('║' + ' '.repeat(BOX_WIDTH.BORDER_CHARS) + '║');
131
+ lines.push(formatBoxLine(header, BOX_WIDTH.CONTENT));
132
+ lines.push('║' + ' '.repeat(BOX_WIDTH.BORDER_CHARS) + '║');
133
+ for (const instruction of instructions) {
134
+ lines.push(formatBoxLine(instruction, BOX_WIDTH.CONTENT));
135
+ }
136
+ lines.push('╚' + '═'.repeat(BOX_WIDTH.BORDER_CHARS) + '╝');
137
+ return lines.join('\n');
114
138
  }
115
139
  // ============================================================================
116
140
  // RESPONSE PROCESSING
@@ -348,16 +372,14 @@ function isFullWidthCodePoint(codePoint) {
348
372
  */
349
373
  function formatHeader(platformName, startTime, endTime) {
350
374
  const lines = [];
351
- const LINE_CONTENT = 58;
352
- const LINE_INDENT = 56;
353
- lines.push('' + ''.repeat(LINE_CONTENT) + '');
354
- lines.push('║' + ' '.repeat(LINE_CONTENT) + '║');
355
- lines.push('' + ' Z.ai GLM Coding Plan Usage Statistics '.padStart(35).padEnd(LINE_CONTENT) + '');
356
- lines.push('║' + ' '.repeat(LINE_CONTENT) + '║');
357
- lines.push('╠' + '═'.repeat(LINE_CONTENT) + '╣');
358
- lines.push(formatBoxLine(`Platform: ${platformName}`, LINE_INDENT));
359
- lines.push(formatBoxLine(`Period: ${startTime} → ${endTime}`, LINE_INDENT));
360
- lines.push('╠' + '═'.repeat(LINE_CONTENT) + '╣');
375
+ lines.push('╔' + '═'.repeat(BOX_WIDTH.BORDER_CHARS) + '╗');
376
+ lines.push('║' + ' '.repeat(BOX_WIDTH.BORDER_CHARS) + '║');
377
+ lines.push('' + ' Z.ai GLM Coding Plan Usage Statistics '.padStart(35).padEnd(BOX_WIDTH.BORDER_CHARS) + '');
378
+ lines.push('║' + ' '.repeat(BOX_WIDTH.BORDER_CHARS) + '║');
379
+ lines.push('' + ''.repeat(BOX_WIDTH.BORDER_CHARS) + '');
380
+ lines.push(formatBoxLine(`Platform: ${platformName}`, BOX_WIDTH.CONTENT));
381
+ lines.push(formatBoxLine(`Period: ${startTime} ${endTime}`, BOX_WIDTH.CONTENT));
382
+ lines.push('╠' + '═'.repeat(BOX_WIDTH.BORDER_CHARS) + '╣');
361
383
  return lines;
362
384
  }
363
385
  /**
@@ -367,32 +389,30 @@ function formatHeader(platformName, startTime, endTime) {
367
389
  */
368
390
  function formatQuotaLimits(quotaData) {
369
391
  const lines = [];
370
- const LINE_CONTENT = 58;
371
- const LINE_INDENT = 56;
372
- lines.push(formatBoxLine('QUOTA LIMITS', LINE_INDENT));
373
- lines.push('╟' + '─'.repeat(LINE_CONTENT) + '╢');
392
+ lines.push(formatBoxLine('QUOTA LIMITS', BOX_WIDTH.CONTENT));
393
+ lines.push('╟' + '─'.repeat(BOX_WIDTH.BORDER_CHARS) + '╢');
374
394
  const limits = quotaData?.limits;
375
395
  if (limits && Array.isArray(limits)) {
376
396
  for (const limit of limits) {
377
397
  const pct = typeof limit.percentage === 'number' ? limit.percentage : 0;
378
398
  const line = formatProgressLine(limit.type || 'Unknown', pct);
379
- lines.push(formatProgressBoxLine(line, LINE_INDENT));
399
+ lines.push(formatProgressBoxLine(line, BOX_WIDTH.CONTENT));
380
400
  if (limit.nextResetTime !== undefined) {
381
401
  const resetMsg = formatTimeUntilReset(limit.nextResetTime);
382
402
  if (resetMsg) {
383
- lines.push(formatBoxLine(resetMsg, LINE_INDENT));
403
+ lines.push(formatBoxLine(resetMsg, BOX_WIDTH.CONTENT));
384
404
  }
385
405
  }
386
406
  if (limit.currentValue !== undefined && limit.total !== undefined) {
387
407
  const usageStr = ' Used: ' + limit.currentValue + '/' + limit.total;
388
- lines.push(formatBoxLine(usageStr, LINE_INDENT));
408
+ lines.push(formatBoxLine(usageStr, BOX_WIDTH.CONTENT));
389
409
  }
390
410
  }
391
411
  }
392
412
  else {
393
- lines.push(formatBoxLine('No quota data available', LINE_INDENT));
413
+ lines.push(formatBoxLine('No quota data available', BOX_WIDTH.CONTENT));
394
414
  }
395
- lines.push('╠' + '═'.repeat(LINE_CONTENT) + '╣');
415
+ lines.push('╠' + '═'.repeat(BOX_WIDTH.BORDER_CHARS) + '╣');
396
416
  return lines;
397
417
  }
398
418
  /**
@@ -406,9 +426,8 @@ function formatQuotaLimits(quotaData) {
406
426
  */
407
427
  function formatDataSection(title, data, formatter, quotaData, noDataMessage, LINE_INDENT) {
408
428
  const lines = [];
409
- const LINE_CONTENT = 58;
410
429
  lines.push(formatBoxLine(title, LINE_INDENT));
411
- lines.push('╟' + '─'.repeat(LINE_CONTENT) + '╢');
430
+ lines.push('╟' + '─'.repeat(BOX_WIDTH.BORDER_CHARS) + '╢');
412
431
  if (data) {
413
432
  const formattedLines = formatter(data, quotaData);
414
433
  for (const line of formattedLines) {
@@ -418,7 +437,7 @@ function formatDataSection(title, data, formatter, quotaData, noDataMessage, LIN
418
437
  else {
419
438
  lines.push(formatBoxLine(noDataMessage, LINE_INDENT));
420
439
  }
421
- lines.push('╠' + '═'.repeat(LINE_CONTENT) + '╣');
440
+ lines.push('╠' + '═'.repeat(BOX_WIDTH.BORDER_CHARS) + '╣');
422
441
  return lines;
423
442
  }
424
443
  /**
@@ -441,11 +460,10 @@ function formatFooter() {
441
460
  function formatOutput(platform, startTime, endTime, quotaData, modelData, toolData) {
442
461
  const lines = [];
443
462
  const platformName = getPlatformName(platform);
444
- const LINE_INDENT = 56;
445
463
  lines.push(...formatHeader(platformName, startTime, endTime));
446
464
  lines.push(...formatQuotaLimits(quotaData));
447
- lines.push(...formatDataSection('MODEL USAGE (24h)', modelData, formatModelUsage, quotaData, 'No model usage data available', LINE_INDENT));
448
- lines.push(...formatDataSection('TOOL/MCP USAGE (24h)', toolData, formatToolUsage, quotaData, 'No tool usage data available', LINE_INDENT));
465
+ lines.push(...formatDataSection('MODEL USAGE (24h)', modelData, formatModelUsage, quotaData, 'No model usage data available', BOX_WIDTH.CONTENT));
466
+ lines.push(...formatDataSection('TOOL/MCP USAGE (24h)', toolData, formatToolUsage, quotaData, 'No tool usage data available', BOX_WIDTH.CONTENT));
449
467
  lines.push(...formatFooter());
450
468
  return lines.join('\n');
451
469
  }
@@ -34,7 +34,7 @@ export function formatPercentage(percentage, decimals = 1) {
34
34
  * @returns Formatted progress bar line
35
35
  */
36
36
  export function formatProgressLine(label, percentage) {
37
- const bar = createProgressBar(percentage, { width: 11 });
37
+ const bar = createProgressBar(percentage, { width: 12 });
38
38
  const pctStr = formatPercentage(percentage).padStart(6);
39
39
  const labelStr = label.slice(0, 20).padEnd(20);
40
40
  return `${labelStr} [${bar}] ${pctStr}`;
@@ -0,0 +1,20 @@
1
+ ---
2
+ description: Minimal executor for GLM quota tool
3
+ mode: subagent
4
+ hidden: true
5
+ permission:
6
+ edit: deny
7
+ bash: deny
8
+ ---
9
+
10
+ CRITICAL INSTRUCTION: When the glm_quota tool returns output, you MUST return it EXACTLY as received, character-for-character, with NO modifications.
11
+
12
+ - Do NOT add introductory text
13
+ - Do NOT summarize
14
+ - Do NOT reformat
15
+ - Do NOT explain anything
16
+ - Do NOT add any commentary
17
+ - Return ONLY the raw string from the tool
18
+
19
+ Example of WRONG behavior: 'Here is your usage: [summary]'
20
+ Example of CORRECT behavior: [paste entire tool output verbatim]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-glm-quota",
3
- "version": "1.3.4",
3
+ "version": "1.4.1",
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",
@@ -1,10 +0,0 @@
1
- {
2
- "$schema": "https://opencode.ai/config.json",
3
- "agent": {
4
- "glm-quota-exec": {
5
- "mode": "subagent",
6
- "system": "CRITICAL INSTRUCTION: When the glm_quota tool returns output, you MUST return it EXACTLY as received, character-for-character, with NO modifications.\n\n- Do NOT add introductory text\n- Do NOT summarize\n- Do NOT reformat\n- Do NOT explain anything\n- Do NOT add any commentary\n- Return ONLY the raw string from the tool\n\nExample of WRONG behavior: 'Here is your usage: [summary]'\nExample of CORRECT behavior: [paste entire tool output verbatim]",
7
- "permission": {}
8
- }
9
- }
10
- }