opencode-qwen-oauth 1.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Your Name
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,213 @@
1
+ # opencode-qwen-oauth
2
+
3
+ [![npm version](https://img.shields.io/npm/v/opencode-qwen-oauth.svg)](https://www.npmjs.com/package/opencode-qwen-oauth)
4
+ [![npm downloads](https://img.shields.io/npm/dm/opencode-qwen-oauth.svg)](https://www.npmjs.com/package/opencode-qwen-oauth)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
6
+
7
+ Qwen OAuth authentication plugin for [OpenCode](https://opencode.ai) - authenticate with Qwen.ai using OAuth device flow (PKCE).
8
+
9
+ ## Features
10
+
11
+ - šŸ” **OAuth Device Flow** - PKCE-secured authentication, works in headless/CI environments
12
+ - šŸ”„ **Automatic Token Refresh** - Tokens are refreshed before expiry
13
+ - 🌐 **Auto Browser Open** - Automatically opens browser for authentication
14
+ - šŸ“ **File Logging** - All OAuth activity logged to `~/.config/opencode/logs/qwen-oauth.log`
15
+ - šŸ› **Debug Mode** - Enable verbose output with `QWEN_OAUTH_DEBUG=true`
16
+ - šŸš€ **Easy Install** - One-command installation with CLI tool
17
+
18
+ ## Quick Start
19
+
20
+ ### Install
21
+
22
+ ```bash
23
+ # Using npx (recommended)
24
+ npx opencode-qwen-oauth install
25
+
26
+ # Or using bunx
27
+ bunx opencode-qwen-oauth install
28
+
29
+ # Or install manually
30
+ npm install opencode-qwen-oauth
31
+ ```
32
+
33
+ The installer will:
34
+ - Add `opencode-qwen-oauth` to your `.opencode/opencode.json` plugins
35
+ - Configure the Qwen provider with models
36
+
37
+ ### Authenticate
38
+
39
+ ```bash
40
+ # Start OpenCode
41
+ opencode
42
+
43
+ # Connect to Qwen
44
+ /connect
45
+ ```
46
+
47
+ Select **"Qwen Code (qwen.ai OAuth)"** and follow the device flow instructions.
48
+
49
+ ### Use Qwen Models
50
+
51
+ ```
52
+ /model qwen/qwen3-coder-plus
53
+ ```
54
+
55
+ ## Models
56
+
57
+ | Model | Context | Features |
58
+ |-------|---------|----------|
59
+ | `qwen3-coder-plus` | 1M tokens | Optimized for coding |
60
+ | `qwen3-vl-plus` | 256K tokens | Vision + language |
61
+
62
+ ## Configuration
63
+
64
+ ### Debug Mode
65
+
66
+ Enable verbose logging to console:
67
+
68
+ ```bash
69
+ QWEN_OAUTH_DEBUG=true opencode
70
+ ```
71
+
72
+ ### Log Files
73
+
74
+ All OAuth activity is logged to:
75
+ ```
76
+ ~/.config/opencode/logs/qwen-oauth.log
77
+ ```
78
+
79
+ View logs in real-time:
80
+ ```bash
81
+ tail -f ~/.config/opencode/logs/qwen-oauth.log
82
+ ```
83
+
84
+ ### Manual Configuration
85
+
86
+ If you prefer manual setup, add to `.opencode/opencode.json`:
87
+
88
+ ```json
89
+ {
90
+ "$schema": "https://opencode.ai/config.json",
91
+ "plugin": ["opencode-qwen-oauth"],
92
+ "provider": {
93
+ "qwen": {
94
+ "npm": "@ai-sdk/openai-compatible",
95
+ "name": "Qwen Code",
96
+ "options": {
97
+ "baseURL": "https://portal.qwen.ai/v1"
98
+ },
99
+ "models": {
100
+ "qwen3-coder-plus": {
101
+ "id": "qwen3-coder-plus",
102
+ "name": "Qwen3 Coder Plus"
103
+ },
104
+ "qwen3-vl-plus": {
105
+ "id": "qwen3-vl-plus",
106
+ "name": "Qwen3 VL Plus",
107
+ "attachment": true
108
+ }
109
+ }
110
+ }
111
+ }
112
+ }
113
+ ```
114
+
115
+ ## CLI Commands
116
+
117
+ ```bash
118
+ # Install (default)
119
+ npx opencode-qwen-oauth install
120
+
121
+ # Uninstall
122
+ npx opencode-qwen-oauth uninstall
123
+
124
+ # Help
125
+ npx opencode-qwen-oauth --help
126
+ ```
127
+
128
+ ## Troubleshooting
129
+
130
+ ### "Device code expired"
131
+ Complete the browser login within 5 minutes of starting `/connect`.
132
+
133
+ ### "invalid_grant" error
134
+ Your refresh token has expired. Run `/connect` to re-authenticate.
135
+
136
+ ### Provider not showing in /connect
137
+ Use the CLI directly:
138
+ ```bash
139
+ opencode auth login qwen
140
+ ```
141
+
142
+ ### Check logs
143
+ ```bash
144
+ cat ~/.config/opencode/logs/qwen-oauth.log
145
+ ```
146
+
147
+ ## How It Works
148
+
149
+ This plugin implements OAuth 2.0 Device Flow (RFC 8628) with PKCE:
150
+
151
+ 1. **Device Code Request** - Plugin requests a device code from Qwen OAuth server
152
+ 2. **User Authorization** - User visits the verification URL and enters the user code
153
+ 3. **Token Polling** - Plugin polls for the access token until user authorizes
154
+ 4. **Token Storage** - Tokens are stored in OpenCode's auth system
155
+ 5. **Auto Refresh** - Access tokens are refreshed before expiry
156
+
157
+ ## Security
158
+
159
+ - Uses PKCE (RFC 7636) for enhanced security
160
+ - No client secret required
161
+ - Tokens stored in OpenCode's secure auth storage
162
+ - All OAuth activity logged for auditing
163
+
164
+ ## Development
165
+
166
+ ```bash
167
+ # Clone and install
168
+ git clone https://github.com/yourusername/opencode-qwen-oauth.git
169
+ cd opencode-qwen-oauth
170
+ npm install
171
+
172
+ # Build
173
+ npm run build
174
+
175
+ # Watch mode
176
+ npm run dev
177
+
178
+ # Test locally
179
+ npm link
180
+ cd /path/to/project
181
+ opencode-qwen-oauth install
182
+ ```
183
+
184
+ ## Project Structure
185
+
186
+ ```
187
+ opencode-qwen-oauth/
188
+ ā”œā”€ā”€ src/ # TypeScript source files
189
+ │ └── index.ts # Main plugin implementation
190
+ ā”œā”€ā”€ bin/ # CLI scripts
191
+ │ └── install.js # Installer script
192
+ ā”œā”€ā”€ dist/ # Compiled JavaScript (generated)
193
+ ā”œā”€ā”€ package.json # Package manifest
194
+ ā”œā”€ā”€ tsconfig.json # TypeScript config
195
+ ā”œā”€ā”€ LICENSE # MIT license
196
+ └── README.md # This file
197
+ ```
198
+
199
+ **Note:** `.opencode/` directory is for local testing only and is not included in the npm package.
200
+
201
+ ## License
202
+
203
+ MIT
204
+
205
+ ## Contributing
206
+
207
+ Contributions welcome! Please open an issue or submit a PR.
208
+
209
+ ## Related
210
+
211
+ - [OpenCode Documentation](https://opencode.ai/docs)
212
+ - [OpenCode Plugin API](https://opencode.ai/docs/plugins)
213
+ - [Qwen.ai](https://chat.qwen.ai)
package/bin/install.js ADDED
@@ -0,0 +1,236 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Qwen OAuth Plugin Installer
5
+ *
6
+ * Usage:
7
+ * npx opencode-qwen-oauth install
8
+ * bunx opencode-qwen-oauth install
9
+ */
10
+
11
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, appendFileSync } from "node:fs";
12
+ import { join } from "node:path";
13
+ import { homedir } from "node:os";
14
+
15
+ // ============================================
16
+ // Helpers
17
+ // ============================================
18
+
19
+ function getProjectRoot() {
20
+ let current = process.cwd();
21
+ while (current !== "/") {
22
+ if (existsSync(join(current, ".opencode"))) {
23
+ return current;
24
+ }
25
+ const parent = join(current, "..");
26
+ if (parent === current) break;
27
+ current = parent;
28
+ }
29
+ return process.cwd();
30
+ }
31
+
32
+ function getOpencodeDir() {
33
+ return join(getProjectRoot(), ".opencode");
34
+ }
35
+
36
+ function getOpencodeConfigPath() {
37
+ return join(getOpencodeDir(), "opencode.json");
38
+ }
39
+
40
+ function ensureDir(dir) {
41
+ if (!existsSync(dir)) {
42
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
43
+ }
44
+ }
45
+
46
+ function log(message) {
47
+ console.log(`[opencode-qwen-oauth] ${message}`);
48
+ }
49
+
50
+ function error(message) {
51
+ console.error(`[opencode-qwen-oauth] ERROR: ${message}`);
52
+ }
53
+
54
+ // ============================================
55
+ // Install
56
+ // ============================================
57
+
58
+ function install() {
59
+ log("Installing Qwen OAuth plugin...");
60
+
61
+ const opencodeDir = getOpencodeDir();
62
+ const configPath = getOpencodeConfigPath();
63
+
64
+ ensureDir(opencodeDir);
65
+
66
+ // Read or create config
67
+ let config = { "$schema": "https://opencode.ai/config.json", plugin: [], provider: {} };
68
+
69
+ if (existsSync(configPath)) {
70
+ try {
71
+ const content = readFileSync(configPath, "utf-8");
72
+ config = JSON.parse(content);
73
+ } catch (e) {
74
+ error(`Failed to parse ${configPath}: ${e.message}`);
75
+ process.exit(1);
76
+ }
77
+ }
78
+
79
+ // Add plugin
80
+ config.plugin = config.plugin || [];
81
+ if (!config.plugin.includes("opencode-qwen-oauth")) {
82
+ config.plugin.push("opencode-qwen-oauth");
83
+ log("Added 'opencode-qwen-oauth' to plugins");
84
+ } else {
85
+ log("Plugin 'opencode-qwen-oauth' already in config");
86
+ }
87
+
88
+ // Add provider config
89
+ config.provider = config.provider || {};
90
+ if (!config.provider.qwen) {
91
+ config.provider.qwen = {
92
+ npm: "@ai-sdk/openai-compatible",
93
+ name: "Qwen Code",
94
+ options: {
95
+ baseURL: "https://portal.qwen.ai/v1",
96
+ },
97
+ models: {
98
+ "qwen3-coder-plus": {
99
+ id: "qwen3-coder-plus",
100
+ name: "Qwen3 Coder Plus",
101
+ },
102
+ "qwen3-vl-plus": {
103
+ id: "qwen3-vl-plus",
104
+ name: "Qwen3 VL Plus",
105
+ attachment: true,
106
+ },
107
+ },
108
+ };
109
+ log("Added 'qwen' provider configuration");
110
+ } else {
111
+ log("Provider 'qwen' already configured");
112
+ }
113
+
114
+ // Write config
115
+ try {
116
+ writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
117
+ log(`Configuration written to ${configPath}`);
118
+ } catch (e) {
119
+ error(`Failed to write config: ${e.message}`);
120
+ process.exit(1);
121
+ }
122
+
123
+ // Install npm package in .opencode
124
+ const opencodePackagePath = join(opencodeDir, "package.json");
125
+ let opencodePackage = { dependencies: {} };
126
+
127
+ if (existsSync(opencodePackagePath)) {
128
+ try {
129
+ opencodePackage = JSON.parse(readFileSync(opencodePackagePath, "utf-8"));
130
+ } catch {
131
+ // Ignore
132
+ }
133
+ }
134
+
135
+ opencodePackage.dependencies = opencodePackage.dependencies || {};
136
+ if (!opencodePackage.dependencies["opencode-qwen-oauth"]) {
137
+ opencodePackage.dependencies["opencode-qwen-oauth"] = "^1.0.0";
138
+ log("Added 'opencode-qwen-oauth' to .opencode/package.json dependencies");
139
+ }
140
+
141
+ try {
142
+ writeFileSync(opencodePackagePath, JSON.stringify(opencodePackage, null, 2) + "\n", "utf-8");
143
+ } catch (e) {
144
+ error(`Failed to write package.json: ${e.message}`);
145
+ process.exit(1);
146
+ }
147
+
148
+ log("\nāœ… Installation complete!");
149
+ log("\nNext steps:");
150
+ log(" 1. Run: opencode");
151
+ log(" 2. Connect: /connect (select 'Qwen Code (qwen.ai OAuth)')");
152
+ log(" 3. Use model: /model qwen/qwen3-coder-plus");
153
+ log("\nDebug mode: QWEN_OAUTH_DEBUG=true opencode");
154
+ }
155
+
156
+ // ============================================
157
+ // Uninstall
158
+ // ============================================
159
+
160
+ function uninstall() {
161
+ log("Uninstalling Qwen OAuth plugin...");
162
+
163
+ const configPath = getOpencodeConfigPath();
164
+
165
+ if (!existsSync(configPath)) {
166
+ log("No config file found, nothing to uninstall");
167
+ return;
168
+ }
169
+
170
+ let config;
171
+ try {
172
+ config = JSON.parse(readFileSync(configPath, "utf-8"));
173
+ } catch (e) {
174
+ error(`Failed to parse config: ${e.message}`);
175
+ process.exit(1);
176
+ }
177
+
178
+ // Remove plugin
179
+ if (config.plugin) {
180
+ const index = config.plugin.indexOf("opencode-qwen-oauth");
181
+ if (index !== -1) {
182
+ config.plugin.splice(index, 1);
183
+ log("Removed 'opencode-qwen-oauth' from plugins");
184
+ }
185
+ }
186
+
187
+ // Remove provider
188
+ if (config.provider && config.provider.qwen) {
189
+ delete config.provider.qwen;
190
+ log("Removed 'qwen' provider configuration");
191
+ }
192
+
193
+ try {
194
+ writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
195
+ log(`Configuration updated: ${configPath}`);
196
+ } catch (e) {
197
+ error(`Failed to write config: ${e.message}`);
198
+ process.exit(1);
199
+ }
200
+
201
+ log("\nāœ… Uninstallation complete!");
202
+ log("Note: You may want to manually remove the npm package:");
203
+ log(" npm uninstall opencode-qwen-auth");
204
+ }
205
+
206
+ // ============================================
207
+ // Main
208
+ // ============================================
209
+
210
+ const command = process.argv[2];
211
+
212
+ if (command === "install" || !command) {
213
+ install();
214
+ } else if (command === "uninstall") {
215
+ uninstall();
216
+ } else if (command === "--help" || command === "-h") {
217
+ console.log(`
218
+ opencode-qwen-oauth - Qwen OAuth Plugin for OpenCode
219
+
220
+ Usage:
221
+ npx opencode-qwen-oauth install Install the plugin (default)
222
+ npx opencode-qwen-oauth uninstall Remove the plugin
223
+ npx opencode-qwen-oauth --help Show this help
224
+
225
+ After installation:
226
+ 1. Run: opencode
227
+ 2. Connect: /connect (select 'Qwen Code (qwen.ai OAuth)')
228
+ 3. Use model: /model qwen/qwen3-coder-plus
229
+
230
+ Debug mode: QWEN_OAUTH_DEBUG=true opencode
231
+ `);
232
+ } else {
233
+ error(`Unknown command: ${command}`);
234
+ console.log("Run 'npx opencode-qwen-auth --help' for usage");
235
+ process.exit(1);
236
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Qwen OAuth Plugin for OpenCode
3
+ * Provides OAuth device flow authentication for Qwen.ai
4
+ *
5
+ * @packageDocumentation
6
+ */
7
+ import type { Plugin } from "@opencode-ai/plugin";
8
+ export declare const QwenOAuthPlugin: Plugin;
9
+ export default QwenOAuthPlugin;
10
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAe,MAAM,qBAAqB,CAAC;AAkP/D,eAAO,MAAM,eAAe,EAAE,MAuF7B,CAAC;AAEF,eAAe,eAAe,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,250 @@
1
+ /**
2
+ * Qwen OAuth Plugin for OpenCode
3
+ * Provides OAuth device flow authentication for Qwen.ai
4
+ *
5
+ * @packageDocumentation
6
+ */
7
+ import { createHash, randomBytes } from "node:crypto";
8
+ import { spawn } from "node:child_process";
9
+ import { appendFileSync, mkdirSync, existsSync } from "node:fs";
10
+ import { join } from "node:path";
11
+ import { homedir } from "node:os";
12
+ // ============================================
13
+ // Constants
14
+ // ============================================
15
+ const QWEN_OAUTH_BASE_URL = "https://chat.qwen.ai";
16
+ const QWEN_DEVICE_CODE_ENDPOINT = "/api/v1/oauth2/device/code";
17
+ const QWEN_TOKEN_ENDPOINT = "/api/v1/oauth2/token";
18
+ const QWEN_CLIENT_ID = "f0304373b74a44d2b584a3fb70ca9e56";
19
+ const QWEN_SCOPES = ["openid", "profile", "email", "model.completion"];
20
+ const QWEN_API_BASE_URL = "https://portal.qwen.ai/v1";
21
+ // ============================================
22
+ // Logging
23
+ // ============================================
24
+ function getLogDir() {
25
+ const xdgConfig = process.env.XDG_CONFIG_HOME || join(homedir(), ".config");
26
+ return join(xdgConfig, "opencode", "logs");
27
+ }
28
+ function getLogFilePath() {
29
+ return join(getLogDir(), "qwen-oauth.log");
30
+ }
31
+ function ensureLogDir() {
32
+ const logDir = getLogDir();
33
+ if (!existsSync(logDir)) {
34
+ mkdirSync(logDir, { recursive: true, mode: 0o700 });
35
+ }
36
+ }
37
+ function writeLog(message) {
38
+ try {
39
+ ensureLogDir();
40
+ const timestamp = new Date().toISOString();
41
+ const logLine = `[${timestamp}] ${message}\n`;
42
+ appendFileSync(getLogFilePath(), logLine, { encoding: "utf-8" });
43
+ }
44
+ catch {
45
+ // Silently ignore log write errors
46
+ }
47
+ }
48
+ const DEBUG = process.env.QWEN_OAUTH_DEBUG === "true" || process.env.QWEN_OAUTH_DEBUG === "1";
49
+ function debugLog(message, data) {
50
+ const logMessage = data ? `${message} ${JSON.stringify(data)}` : message;
51
+ writeLog(logMessage);
52
+ if (DEBUG) {
53
+ console.log(`[Qwen OAuth] ${message}`, data ? JSON.stringify(data, null, 2) : "");
54
+ }
55
+ }
56
+ // ============================================
57
+ // PKCE
58
+ // ============================================
59
+ function base64UrlEncode(buffer) {
60
+ return buffer
61
+ .toString("base64")
62
+ .replace(/\+/g, "-")
63
+ .replace(/\//g, "_")
64
+ .replace(/=+$/, "");
65
+ }
66
+ function createPkcePair() {
67
+ const verifier = base64UrlEncode(randomBytes(32));
68
+ const challenge = base64UrlEncode(createHash("sha256").update(verifier).digest());
69
+ return { verifier, challenge };
70
+ }
71
+ // ============================================
72
+ // Browser
73
+ // ============================================
74
+ function openBrowser(url) {
75
+ try {
76
+ const platform = process.platform;
77
+ const command = platform === "darwin"
78
+ ? "open"
79
+ : platform === "win32"
80
+ ? "rundll32"
81
+ : "xdg-open";
82
+ const args = platform === "win32"
83
+ ? ["url.dll,FileProtocolHandler", url]
84
+ : [url];
85
+ const child = spawn(command, args, {
86
+ stdio: "ignore",
87
+ detached: true,
88
+ });
89
+ child.unref?.();
90
+ }
91
+ catch {
92
+ // Ignore errors
93
+ }
94
+ }
95
+ async function authorizeDevice() {
96
+ const { verifier, challenge } = createPkcePair();
97
+ const params = new URLSearchParams({
98
+ client_id: QWEN_CLIENT_ID,
99
+ scope: QWEN_SCOPES.join(" "),
100
+ code_challenge: challenge,
101
+ code_challenge_method: "S256",
102
+ });
103
+ const response = await fetch(`${QWEN_OAUTH_BASE_URL}${QWEN_DEVICE_CODE_ENDPOINT}`, {
104
+ method: "POST",
105
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
106
+ body: params.toString(),
107
+ });
108
+ if (!response.ok) {
109
+ throw new Error(`Failed to start device flow: ${response.statusText}`);
110
+ }
111
+ const data = (await response.json());
112
+ return {
113
+ device_code: data.device_code,
114
+ user_code: data.user_code,
115
+ verification_uri: data.verification_uri,
116
+ verification_uri_complete: data.verification_uri_complete,
117
+ expires_in: data.expires_in,
118
+ interval: data.interval || 5,
119
+ verifier,
120
+ };
121
+ }
122
+ async function pollForToken(deviceCode, codeVerifier, intervalSeconds, expiresIn) {
123
+ const timeoutMs = expiresIn * 1000;
124
+ const startTime = Date.now();
125
+ let currentInterval = intervalSeconds * 1000;
126
+ debugLog("Starting token polling", { timeoutMs, interval: currentInterval });
127
+ while (Date.now() - startTime < timeoutMs) {
128
+ await new Promise((resolve) => setTimeout(resolve, currentInterval));
129
+ const params = new URLSearchParams({
130
+ client_id: QWEN_CLIENT_ID,
131
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code",
132
+ device_code: deviceCode,
133
+ code_verifier: codeVerifier,
134
+ });
135
+ debugLog("Polling for token...");
136
+ const response = await fetch(`${QWEN_OAUTH_BASE_URL}${QWEN_TOKEN_ENDPOINT}`, {
137
+ method: "POST",
138
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
139
+ body: params.toString(),
140
+ });
141
+ if (response.ok) {
142
+ const data = (await response.json());
143
+ debugLog("Token received successfully");
144
+ return {
145
+ success: true,
146
+ access_token: data.access_token,
147
+ refresh_token: data.refresh_token,
148
+ expires_in: data.expires_in,
149
+ };
150
+ }
151
+ const error = (await response.json().catch(() => ({})));
152
+ if (error.error === "authorization_pending") {
153
+ debugLog("Authorization pending, retrying...");
154
+ continue;
155
+ }
156
+ if (error.error === "slow_down") {
157
+ currentInterval += 5000;
158
+ debugLog("Server requested slow down, new interval:", { interval: currentInterval });
159
+ continue;
160
+ }
161
+ if (error.error === "expired_token") {
162
+ debugLog("Device code expired");
163
+ return { success: false, error: "Device code expired. Please try again." };
164
+ }
165
+ debugLog(`Token polling failed: ${error.error_description || "unknown error"}`);
166
+ return { success: false, error: error.error_description || "Authentication failed" };
167
+ }
168
+ debugLog("Polling timeout exceeded");
169
+ return { success: false, error: "Polling timeout - device code expired" };
170
+ }
171
+ // ============================================
172
+ // Plugin
173
+ // ============================================
174
+ export const QwenOAuthPlugin = async ({ project, client, $, directory, worktree }) => {
175
+ debugLog("Plugin initialized", { directory, worktree, project: project?.name || "N/A" });
176
+ return {
177
+ auth: {
178
+ provider: "qwen",
179
+ methods: [
180
+ {
181
+ type: "oauth",
182
+ label: "Qwen Code (qwen.ai OAuth)",
183
+ authorize: async () => {
184
+ debugLog("Starting Qwen OAuth device flow...");
185
+ const device = await authorizeDevice();
186
+ const url = device.verification_uri_complete || device.verification_uri;
187
+ // Try to open browser automatically
188
+ openBrowser(url);
189
+ debugLog("Device authorization received", {
190
+ user_code: device.user_code,
191
+ verification_uri: device.verification_uri,
192
+ expires_in: device.expires_in,
193
+ interval: device.interval,
194
+ });
195
+ // Show essential info to user
196
+ console.log(`\nšŸ“± Qwen OAuth: Visit ${url}`);
197
+ console.log(`šŸ”‘ Enter code: ${device.user_code}\n`);
198
+ return {
199
+ url,
200
+ instructions: `Enter code: ${device.user_code}`,
201
+ method: "auto",
202
+ callback: async () => {
203
+ debugLog("Polling for OAuth token...");
204
+ const result = await pollForToken(device.device_code, device.verifier, device.interval, device.expires_in);
205
+ if (result.success) {
206
+ debugLog("Qwen authentication successful!", {
207
+ expires_in: result.expires_in,
208
+ has_refresh: !!result.refresh_token,
209
+ });
210
+ return {
211
+ type: "success",
212
+ access: result.access_token,
213
+ refresh: result.refresh_token,
214
+ expires: Date.now() + result.expires_in * 1000,
215
+ };
216
+ }
217
+ debugLog(`Authentication failed: ${result.error}`);
218
+ return { type: "failed", error: result.error };
219
+ },
220
+ };
221
+ },
222
+ },
223
+ ],
224
+ },
225
+ config: async (config) => {
226
+ const providers = config.provider || {};
227
+ config.provider = providers;
228
+ providers["qwen"] = {
229
+ npm: "@ai-sdk/openai-compatible",
230
+ name: "Qwen Code",
231
+ options: {
232
+ baseURL: QWEN_API_BASE_URL,
233
+ },
234
+ models: {
235
+ "qwen3-coder-plus": {
236
+ id: "qwen3-coder-plus",
237
+ name: "Qwen3 Coder Plus",
238
+ },
239
+ "qwen3-vl-plus": {
240
+ id: "qwen3-vl-plus",
241
+ name: "Qwen3 VL Plus",
242
+ attachment: true,
243
+ },
244
+ },
245
+ };
246
+ },
247
+ };
248
+ };
249
+ export default QwenOAuthPlugin;
250
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "opencode-qwen-oauth",
3
+ "version": "1.0.0",
4
+ "description": "Qwen OAuth authentication plugin for OpenCode - authenticate with Qwen.ai using OAuth device flow",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "bin": {
9
+ "opencode-qwen-oauth": "./bin/install.js"
10
+ },
11
+ "files": [
12
+ "dist",
13
+ "bin",
14
+ "README.md",
15
+ "LICENSE"
16
+ ],
17
+ "scripts": {
18
+ "build": "tsc -p tsconfig.json",
19
+ "dev": "tsc -p tsconfig.json --watch",
20
+ "prepublishOnly": "npm run build",
21
+ "test": "echo \"No tests specified\" && exit 0"
22
+ },
23
+ "keywords": [
24
+ "opencode",
25
+ "opencode-plugin",
26
+ "qwen",
27
+ "oauth",
28
+ "authentication",
29
+ "ai",
30
+ "llm"
31
+ ],
32
+ "author": "Your Name <your.email@example.com>",
33
+ "license": "MIT",
34
+ "repository": {
35
+ "type": "git",
36
+ "url": "https://github.com/yourusername/opencode-qwen-oauth.git"
37
+ },
38
+ "bugs": {
39
+ "url": "https://github.com/yourusername/opencode-qwen-oauth/issues"
40
+ },
41
+ "homepage": "https://github.com/yourusername/opencode-qwen-oauth#readme",
42
+ "dependencies": {
43
+ "@opencode-ai/plugin": "^1.2.15"
44
+ },
45
+ "devDependencies": {
46
+ "@types/node": "^20.10.0",
47
+ "typescript": "^5.3.0"
48
+ },
49
+ "engines": {
50
+ "node": ">=18.0.0"
51
+ },
52
+ "peerDependencies": {
53
+ "@ai-sdk/openai-compatible": "^0.0.1"
54
+ },
55
+ "peerDependenciesMeta": {
56
+ "@ai-sdk/openai-compatible": {
57
+ "optional": true
58
+ }
59
+ }
60
+ }