opencode-qwencode-auth 1.0.0 → 1.1.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/README.md CHANGED
@@ -1,8 +1,13 @@
1
1
  # 🤖 Qwen Code OAuth Plugin for OpenCode
2
2
 
3
+ ![npm version](https://img.shields.io/npm/v/opencode-qwencode-auth)
3
4
  ![License](https://img.shields.io/github/license/gustavodiasdev/opencode-qwencode-auth)
4
5
  ![GitHub stars](https://img.shields.io/github/stars/gustavodiasdev/opencode-qwencode-auth)
5
6
 
7
+ <p align="center">
8
+ <img src="assets/screenshot.png" alt="OpenCode with Qwen Code" width="800">
9
+ </p>
10
+
6
11
  **Authenticate OpenCode CLI with your qwen.ai account.** This plugin enables you to use Qwen3-Coder models with **2,000 free requests per day** - no API key or credit card required!
7
12
 
8
13
  [🇧🇷 Leia em Português](./README.pt-BR.md)
@@ -23,18 +28,14 @@
23
28
 
24
29
  ## 🚀 Installation
25
30
 
26
- ### 1. Add the plugin to OpenCode
27
-
28
- Edit `~/.opencode/package.json`:
31
+ ### 1. Install the plugin
29
32
 
30
- ```json
31
- {
32
- "dependencies": {
33
- "opencode-qwencode-auth": "github:gustavodiasdev/opencode-qwencode-auth"
34
- }
35
- }
33
+ ```bash
34
+ cd ~/.opencode && npm install opencode-qwencode-auth
36
35
  ```
37
36
 
37
+ ### 2. Enable the plugin
38
+
38
39
  Edit `~/.opencode/opencode.jsonc`:
39
40
 
40
41
  ```json
@@ -43,12 +44,6 @@ Edit `~/.opencode/opencode.jsonc`:
43
44
  }
44
45
  ```
45
46
 
46
- ### 2. Install dependencies
47
-
48
- ```bash
49
- cd ~/.opencode && npm install
50
- ```
51
-
52
47
  ## 🔑 Usage
53
48
 
54
49
  ### 1. Login
package/README.pt-BR.md CHANGED
@@ -1,8 +1,13 @@
1
1
  # 🤖 Qwen Code OAuth Plugin para OpenCode
2
2
 
3
+ ![npm version](https://img.shields.io/npm/v/opencode-qwencode-auth)
3
4
  ![License](https://img.shields.io/github/license/gustavodiasdev/opencode-qwencode-auth)
4
5
  ![GitHub stars](https://img.shields.io/github/stars/gustavodiasdev/opencode-qwencode-auth)
5
6
 
7
+ <p align="center">
8
+ <img src="assets/screenshot.png" alt="OpenCode com Qwen Code" width="800">
9
+ </p>
10
+
6
11
  **Autentique o OpenCode CLI com sua conta qwen.ai.** Este plugin permite usar modelos Qwen3-Coder com **2.000 requisições gratuitas por dia** - sem API key ou cartão de crédito!
7
12
 
8
13
  [🇺🇸 Read in English](./README.md)
@@ -23,18 +28,14 @@
23
28
 
24
29
  ## 🚀 Instalação
25
30
 
26
- ### 1. Adicione o plugin ao OpenCode
27
-
28
- Edite `~/.opencode/package.json`:
31
+ ### 1. Instale o plugin
29
32
 
30
- ```json
31
- {
32
- "dependencies": {
33
- "opencode-qwencode-auth": "github:gustavodiasdev/opencode-qwencode-auth"
34
- }
35
- }
33
+ ```bash
34
+ cd ~/.opencode && npm install opencode-qwencode-auth
36
35
  ```
37
36
 
37
+ ### 2. Habilite o plugin
38
+
38
39
  Edite `~/.opencode/opencode.jsonc`:
39
40
 
40
41
  ```json
@@ -43,12 +44,6 @@ Edite `~/.opencode/opencode.jsonc`:
43
44
  }
44
45
  ```
45
46
 
46
- ### 2. Instale as dependências
47
-
48
- ```bash
49
- cd ~/.opencode && npm install
50
- ```
51
-
52
47
  ## 🔑 Uso
53
48
 
54
49
  ### 1. Login
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-qwencode-auth",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Qwen OAuth authentication plugin for OpenCode - Access Qwen3-Coder models with your qwen.ai account",
5
5
  "module": "index.ts",
6
6
  "type": "module",
package/src/errors.ts ADDED
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Erros customizados do plugin Qwen Auth
3
+ *
4
+ * Fornece mensagens amigáveis para o usuário em vez de JSON bruto da API.
5
+ * Detalhes técnicos só aparecem com OPENCODE_QWEN_DEBUG=1.
6
+ */
7
+
8
+ const REAUTH_HINT =
9
+ 'Execute "npx opencode-qwencode-auth" ou "qwen-code auth login" para re-autenticar.';
10
+
11
+ // ============================================
12
+ // Erro de Autenticação
13
+ // ============================================
14
+
15
+ export type AuthErrorKind = 'token_expired' | 'refresh_failed' | 'auth_required';
16
+
17
+ const AUTH_MESSAGES: Record<AuthErrorKind, string> = {
18
+ token_expired: `[Qwen] Token expirado. ${REAUTH_HINT}`,
19
+ refresh_failed: `[Qwen] Falha ao renovar token. ${REAUTH_HINT}`,
20
+ auth_required: `[Qwen] Autenticacao necessaria. ${REAUTH_HINT}`,
21
+ };
22
+
23
+ export class QwenAuthError extends Error {
24
+ public readonly kind: AuthErrorKind;
25
+ public readonly technicalDetail?: string;
26
+
27
+ constructor(kind: AuthErrorKind, technicalDetail?: string) {
28
+ super(AUTH_MESSAGES[kind]);
29
+ this.name = 'QwenAuthError';
30
+ this.kind = kind;
31
+ this.technicalDetail = technicalDetail;
32
+ }
33
+ }
34
+
35
+ // ============================================
36
+ // Erro de API
37
+ // ============================================
38
+
39
+ function classifyApiStatus(statusCode: number): string {
40
+ if (statusCode === 401 || statusCode === 403) {
41
+ return `[Qwen] Token invalido ou expirado. ${REAUTH_HINT}`;
42
+ }
43
+ if (statusCode === 429) {
44
+ return '[Qwen] Limite de requisicoes atingido. Aguarde alguns minutos antes de tentar novamente.';
45
+ }
46
+ if (statusCode >= 500) {
47
+ return `[Qwen] Servidor Qwen indisponivel (erro ${statusCode}). Tente novamente em alguns minutos.`;
48
+ }
49
+ return `[Qwen] Erro na API Qwen (${statusCode}). Verifique sua conexao e tente novamente.`;
50
+ }
51
+
52
+ export class QwenApiError extends Error {
53
+ public readonly statusCode: number;
54
+ public readonly technicalDetail?: string;
55
+
56
+ constructor(statusCode: number, technicalDetail?: string) {
57
+ super(classifyApiStatus(statusCode));
58
+ this.name = 'QwenApiError';
59
+ this.statusCode = statusCode;
60
+ this.technicalDetail = technicalDetail;
61
+ }
62
+ }
63
+
64
+ // ============================================
65
+ // Helper de log condicional
66
+ // ============================================
67
+
68
+ /**
69
+ * Loga detalhes técnicos apenas quando debug está ativo.
70
+ */
71
+ export function logTechnicalDetail(detail: string): void {
72
+ if (process.env.OPENCODE_QWEN_DEBUG === '1') {
73
+ console.debug('[Qwen Debug]', detail);
74
+ }
75
+ }
package/src/index.ts CHANGED
@@ -23,6 +23,9 @@ import {
23
23
  tokenResponseToCredentials,
24
24
  refreshAccessToken,
25
25
  } from './qwen/oauth.js';
26
+ import { logTechnicalDetail } from './errors.js';
27
+ export { QwenAuthError, QwenApiError } from './errors.js';
28
+ export type { AuthErrorKind } from './errors.js';
26
29
 
27
30
  // ============================================
28
31
  // Helpers
@@ -102,14 +105,25 @@ export const QwenAuthPlugin = async (_input: unknown) => {
102
105
  accessToken = refreshed.accessToken;
103
106
  saveCredentials(refreshed);
104
107
  } catch (e) {
105
- console.error('[Qwen] Token refresh failed:', e);
108
+ const detail = e instanceof Error ? e.message : String(e);
109
+ logTechnicalDetail(`Token refresh falhou: ${detail}`);
110
+ // Não continuar com token expirado - tentar fallback
111
+ accessToken = undefined;
106
112
  }
107
113
  }
108
114
 
109
115
  // Fallback para credenciais do qwen-code
110
116
  if (!accessToken) {
111
117
  const creds = checkExistingCredentials();
112
- if (creds) accessToken = creds.accessToken;
118
+ if (creds) {
119
+ accessToken = creds.accessToken;
120
+ } else {
121
+ console.warn(
122
+ '[Qwen] Token expirado e sem credenciais alternativas. ' +
123
+ 'Execute "npx opencode-qwencode-auth" ou "qwen-code auth login" para re-autenticar.'
124
+ );
125
+ return null;
126
+ }
113
127
  }
114
128
 
115
129
  if (!accessToken) return null;
@@ -10,6 +10,7 @@ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
10
10
 
11
11
  import type { QwenCredentials } from '../types.js';
12
12
  import { refreshAccessToken, isCredentialsExpired } from '../qwen/oauth.js';
13
+ import { logTechnicalDetail } from '../errors.js';
13
14
 
14
15
  /**
15
16
  * Get the path to the credentials file
@@ -58,7 +59,7 @@ export function loadCredentials(): QwenCredentials | null {
58
59
 
59
60
  return null;
60
61
  } catch (error) {
61
- console.error('Error loading Qwen credentials:', error);
62
+ logTechnicalDetail(`Erro ao carregar credenciais: ${error}`);
62
63
  return null;
63
64
  }
64
65
  }
@@ -102,7 +103,7 @@ export async function getValidCredentials(): Promise<QwenCredentials | null> {
102
103
  credentials = await refreshAccessToken(credentials.refreshToken);
103
104
  saveCredentials(credentials);
104
105
  } catch (error) {
105
- console.error('Failed to refresh token:', error);
106
+ logTechnicalDetail(`Falha no refresh: ${error}`);
106
107
  return null;
107
108
  }
108
109
  }
@@ -12,6 +12,7 @@ import type {
12
12
  StreamChunk
13
13
  } from '../types.js';
14
14
  import { getValidCredentials, loadCredentials, isCredentialsExpired } from './auth.js';
15
+ import { QwenAuthError, QwenApiError } from '../errors.js';
15
16
 
16
17
  /**
17
18
  * QwenClient - Makes authenticated API calls to Qwen
@@ -66,7 +67,7 @@ export class QwenClient {
66
67
  */
67
68
  private getAuthHeader(): string {
68
69
  if (!this.credentials) {
69
- throw new Error('Not authenticated. Please run the OAuth flow first.');
70
+ throw new QwenAuthError('auth_required');
70
71
  }
71
72
  return `Bearer ${this.credentials.accessToken}`;
72
73
  }
@@ -87,7 +88,7 @@ export class QwenClient {
87
88
  if (!this.credentials) {
88
89
  const initialized = await this.initialize();
89
90
  if (!initialized) {
90
- throw new Error('No valid Qwen credentials found. Please authenticate first.');
91
+ throw new QwenAuthError('auth_required');
91
92
  }
92
93
  }
93
94
 
@@ -106,7 +107,7 @@ export class QwenClient {
106
107
  if (!response.ok) {
107
108
  const errorText = await response.text();
108
109
  this.log('API Error:', response.status, errorText);
109
- throw new Error(`Qwen API error: ${response.status} - ${errorText}`);
110
+ throw new QwenApiError(response.status, errorText);
110
111
  }
111
112
 
112
113
  const data = await response.json();
@@ -122,7 +123,7 @@ export class QwenClient {
122
123
  if (!this.credentials) {
123
124
  const initialized = await this.initialize();
124
125
  if (!initialized) {
125
- throw new Error('No valid Qwen credentials found. Please authenticate first.');
126
+ throw new QwenAuthError('auth_required');
126
127
  }
127
128
  }
128
129
 
@@ -142,7 +143,7 @@ export class QwenClient {
142
143
  if (!response.ok) {
143
144
  const errorText = await response.text();
144
145
  this.log('API Error:', response.status, errorText);
145
- throw new Error(`Qwen API error: ${response.status} - ${errorText}`);
146
+ throw new QwenApiError(response.status, errorText);
146
147
  }
147
148
 
148
149
  const reader = response.body?.getReader();
package/src/qwen/oauth.ts CHANGED
@@ -9,6 +9,7 @@ import { randomBytes, createHash, randomUUID } from 'node:crypto';
9
9
 
10
10
  import { QWEN_OAUTH_CONFIG } from '../constants.js';
11
11
  import type { QwenCredentials } from '../types.js';
12
+ import { QwenAuthError, logTechnicalDetail } from '../errors.js';
12
13
 
13
14
  /**
14
15
  * Device authorization response from Qwen OAuth
@@ -87,9 +88,8 @@ export async function requestDeviceAuthorization(
87
88
 
88
89
  if (!response.ok) {
89
90
  const errorData = await response.text();
90
- throw new Error(
91
- `Device authorization failed: ${response.status} ${response.statusText}. Response: ${errorData}`
92
- );
91
+ logTechnicalDetail(`Device auth HTTP ${response.status}: ${errorData}`);
92
+ throw new QwenAuthError('auth_required', `HTTP ${response.status}: ${errorData}`);
93
93
  }
94
94
 
95
95
  const result = await response.json() as DeviceAuthorizationResponse;
@@ -193,7 +193,8 @@ export async function refreshAccessToken(refreshToken: string): Promise<QwenCred
193
193
 
194
194
  if (!response.ok) {
195
195
  const errorText = await response.text();
196
- throw new Error(`Token refresh failed: ${response.status} - ${errorText}`);
196
+ logTechnicalDetail(`Token refresh HTTP ${response.status}: ${errorText}`);
197
+ throw new QwenAuthError('refresh_failed', `HTTP ${response.status}: ${errorText}`);
197
198
  }
198
199
 
199
200
  const data = await response.json() as TokenResponse;