@mmmbuto/nexuscli 0.9.6 → 0.9.7-termux

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
@@ -8,7 +8,7 @@
8
8
 
9
9
  ## Overview
10
10
 
11
- NexusCLI is a lightweight, Termux-first AI cockpit to orchestrate Claude Code, Codex CLI, and Gemini CLI from a single web/terminal UI. It supports live interrupts, native session resume, and voice input with HTTPS auto-setup for remote devices.
11
+ NexusCLI is a lightweight, Termux-first AI cockpit to orchestrate Claude Code, Codex CLI, Gemini CLI, and Qwen Code CLI from a single web/terminal UI. It supports live interrupts, native session resume, and voice input with HTTPS auto-setup for remote devices.
12
12
 
13
13
  ---
14
14
 
@@ -27,7 +27,13 @@ NexusCLI is a lightweight, Termux-first AI cockpit to orchestrate Claude Code, C
27
27
 
28
28
  ---
29
29
 
30
- ## Highlights (v0.9.6)
30
+ ## Highlights (v0.9.7-termux, testing)
31
+
32
+ - **QWEN Engine**: Integrated Qwen Code CLI with streaming statusbar events
33
+ - **QWEN Models**: `coder-model` and `vision-model` available in UI
34
+ - **Session Import**: Qwen sessions indexed alongside Claude/Codex/Gemini
35
+
36
+ ### v0.9.6
31
37
 
32
38
  - **Jobs Runner Restored**: `jobs` route works again after cleanup regression
33
39
  - **Termux-Safe Execution**: no hardcoded `/bin` or `/usr/bin` paths for job tools
@@ -47,7 +53,7 @@ NexusCLI is a lightweight, Termux-first AI cockpit to orchestrate Claude Code, C
47
53
 
48
54
  ## Features
49
55
 
50
- - Multi-engine support (Claude, Codex, Gemini)
56
+ - Multi-engine support (Claude, Codex, Gemini, Qwen)
51
57
  - Session continuity with explicit workspace selection
52
58
  - SSE streaming responses
53
59
  - Model selector with think-mode toggle and default model preference
@@ -65,6 +71,7 @@ NexusCLI is a lightweight, Termux-first AI cockpit to orchestrate Claude Code, C
65
71
  | **Claude-compatible** | DeepSeek (deepseek-*), GLM-4.6 | DeepSeek, Z.ai |
66
72
  | **Codex** | GPT-5.2 Codex, GPT-5.2, GPT-5.1 Codex (Mini/Max), GPT-5.1 | OpenAI |
67
73
  | **Gemini** | Gemini 3 Pro Preview, Gemini 3 Flash Preview | Google |
74
+ | **Qwen** | coder-model, vision-model | Alibaba |
68
75
 
69
76
  ---
70
77
 
@@ -78,6 +85,13 @@ npm install -g @mmmbuto/nexuscli
78
85
  npm install -g github:DioNanos/nexuscli
79
86
  ```
80
87
 
88
+ ### Release Channels
89
+
90
+ ```bash
91
+ # Testing channel
92
+ npm install -g @mmmbuto/nexuscli@testing
93
+ ```
94
+
81
95
  ## Setup
82
96
 
83
97
  ```bash
@@ -132,7 +146,7 @@ nexuscli api set openrouter <key> # Future: Multi-provider gateway
132
146
  nexuscli api delete <provider> # Remove key
133
147
  ```
134
148
 
135
- > **Note**: Claude/Codex/Gemini keys are managed by their respective CLIs.
149
+ > **Note**: Claude/Codex/Gemini/Qwen keys are managed by their respective CLIs.
136
150
  > OpenAI key enables voice input via Whisper. HTTPS auto-generated for remote mic access.
137
151
 
138
152
  ---
@@ -144,6 +158,7 @@ nexuscli api delete <provider> # Remove key
144
158
  - Claude Code CLI (`claude`)
145
159
  - Codex CLI (`codex`)
146
160
  - Gemini CLI (`gemini`)
161
+ - Qwen Code CLI (`qwen`)
147
162
 
148
163
  ---
149
164
 
@@ -177,9 +192,11 @@ It is a **research and learning tool**.
177
192
  | `POST /api/v1/chat` | Claude | SSE streaming chat |
178
193
  | `POST /api/v1/codex` | Codex | SSE streaming chat |
179
194
  | `POST /api/v1/gemini` | Gemini | SSE streaming chat |
195
+ | `POST /api/v1/qwen` | Qwen | SSE streaming chat |
180
196
  | `POST /api/v1/chat/interrupt` | Claude | Stop running generation |
181
197
  | `POST /api/v1/codex/interrupt` | Codex | Stop running generation |
182
198
  | `POST /api/v1/gemini/interrupt` | Gemini | Stop running generation |
199
+ | `POST /api/v1/qwen/interrupt` | Qwen | Stop running generation |
183
200
  | `GET /api/v1/models` | All | List available models |
184
201
  | `GET /api/v1/config` | - | Get user preferences (default model) |
185
202
  | `GET /health` | - | Health check |
@@ -18,7 +18,8 @@ function detectEngines() {
18
18
  const engines = {
19
19
  claude: { available: false, path: null, version: null },
20
20
  codex: { available: false, path: null, version: null },
21
- gemini: { available: false, path: null, version: null }
21
+ gemini: { available: false, path: null, version: null },
22
+ qwen: { available: false, path: null, version: null }
22
23
  };
23
24
 
24
25
  // Claude
@@ -42,6 +43,13 @@ function detectEngines() {
42
43
  engines.gemini = { available: true, path: geminiPath, version: geminiVersion };
43
44
  } catch {}
44
45
 
46
+ // Qwen
47
+ try {
48
+ const qwenPath = execSync('which qwen 2>/dev/null', { encoding: 'utf8' }).trim();
49
+ const qwenVersion = execSync('qwen --version 2>/dev/null', { encoding: 'utf8' }).trim().split('\n')[0];
50
+ engines.qwen = { available: true, path: qwenPath, version: qwenVersion };
51
+ } catch {}
52
+
45
53
  return engines;
46
54
  }
47
55
 
@@ -87,6 +95,16 @@ function listEngines() {
87
95
  console.log(chalk.bold(`║ Gemini: ${chalk.gray('○ not installed')}`));
88
96
  }
89
97
 
98
+ // Qwen
99
+ const qwenEnabled = config.engines?.qwen?.enabled === true;
100
+ if (detected.qwen.available) {
101
+ const status = qwenEnabled ? chalk.green('✓ enabled') : chalk.gray('○ disabled');
102
+ console.log(chalk.bold(`║ QWEN: ${status}`));
103
+ console.log(chalk.gray(`║ ${detected.qwen.version}`));
104
+ } else {
105
+ console.log(chalk.bold(`║ QWEN: ${chalk.gray('○ not installed')}`));
106
+ }
107
+
90
108
  console.log(chalk.bold('╚═══════════════════════════════════════════╝'));
91
109
  }
92
110
 
@@ -129,6 +147,17 @@ async function testEngine(engine) {
129
147
  }
130
148
  }
131
149
 
150
+ if (engine === 'qwen') {
151
+ try {
152
+ execSync('qwen --version', { stdio: 'inherit' });
153
+ console.log(chalk.green(` ✓ Qwen CLI is working`));
154
+ return true;
155
+ } catch {
156
+ console.log(chalk.red(` ✗ Qwen CLI not found`));
157
+ return false;
158
+ }
159
+ }
160
+
132
161
  console.log(chalk.yellow(` Engine ${engine} not testable`));
133
162
  return false;
134
163
  }
@@ -149,10 +178,13 @@ async function addEngine() {
149
178
  if (detected.gemini.available) {
150
179
  choices.push({ name: `Gemini (${detected.gemini.version})`, value: 'gemini' });
151
180
  }
181
+ if (detected.qwen.available) {
182
+ choices.push({ name: `QWEN (${detected.qwen.version})`, value: 'qwen' });
183
+ }
152
184
 
153
185
  if (choices.length === 0) {
154
186
  console.log(chalk.yellow(' No AI engines detected.'));
155
- console.log(chalk.gray(' Install: claude, codex, or gemini CLI'));
187
+ console.log(chalk.gray(' Install: claude, codex, gemini, or qwen CLI'));
156
188
  return;
157
189
  }
158
190
 
@@ -206,6 +238,23 @@ async function addEngine() {
206
238
  setConfigValue('engines.gemini.model', 'gemini-3-pro-preview');
207
239
  console.log(chalk.green(` ✓ Gemini configured (gemini-3-pro-preview)`));
208
240
  }
241
+
242
+ if (engine === 'qwen') {
243
+ const { model } = await inquirer.prompt([{
244
+ type: 'list',
245
+ name: 'model',
246
+ message: 'Default Qwen model:',
247
+ choices: [
248
+ { name: 'Coder (default)', value: 'coder-model' },
249
+ { name: 'Vision', value: 'vision-model' }
250
+ ],
251
+ default: 'coder-model'
252
+ }]);
253
+
254
+ setConfigValue('engines.qwen.enabled', true);
255
+ setConfigValue('engines.qwen.model', model);
256
+ console.log(chalk.green(` ✓ QWEN configured (${model})`));
257
+ }
209
258
  }
210
259
 
211
260
  /**
@@ -32,6 +32,11 @@ const DEFAULT_CONFIG = {
32
32
  codex: {
33
33
  enabled: false,
34
34
  path: null
35
+ },
36
+ qwen: {
37
+ enabled: false,
38
+ path: null,
39
+ model: 'coder-model'
35
40
  }
36
41
  },
37
42
  workspaces: {
@@ -164,6 +164,34 @@ function getCliTools() {
164
164
  }
165
165
  ]
166
166
  }
167
+ ,
168
+
169
+ // ============================================================
170
+ // QWEN - Qwen Code CLI (Termux fork)
171
+ // ============================================================
172
+ 'qwen': {
173
+ name: 'QWEN',
174
+ icon: 'Cpu',
175
+ enabled: true,
176
+ endpoint: '/api/v1/qwen',
177
+ models: [
178
+ {
179
+ id: 'coder-model',
180
+ name: 'coder-model',
181
+ label: 'Qwen Coder',
182
+ description: '🔧 Default Qwen Coder model',
183
+ category: 'qwen',
184
+ default: true
185
+ },
186
+ {
187
+ id: 'vision-model',
188
+ name: 'vision-model',
189
+ label: 'Qwen Vision',
190
+ description: '👁️ Vision-capable model',
191
+ category: 'qwen'
192
+ }
193
+ ]
194
+ }
167
195
  };
168
196
  }
169
197
 
@@ -10,7 +10,7 @@ const rateLimit = require('express-rate-limit');
10
10
  /**
11
11
  * Chat endpoints rate limiter
12
12
  * - 10 requests per minute per user
13
- * - Applies to: /api/v1/chat, /api/v1/codex, /api/v1/gemini
13
+ * - Applies to: /api/v1/chat, /api/v1/codex, /api/v1/gemini, /api/v1/qwen
14
14
  */
15
15
  const chatRateLimiter = rateLimit({
16
16
  windowMs: 60 * 1000, // 1 minute window
@@ -13,7 +13,7 @@ class Message {
13
13
  * @param {string} content - Message content (markdown)
14
14
  * @param {Object} metadata - Optional metadata
15
15
  * @param {number} createdAt - Optional timestamp override
16
- * @param {string} engine - Engine used ('claude' | 'codex')
16
+ * @param {string} engine - Engine used ('claude' | 'codex' | 'gemini' | 'qwen')
17
17
  * @returns {Object} Created message
18
18
  */
19
19
  static create(conversationId, role, content, metadata = null, createdAt = Date.now(), engine = 'claude') {
@@ -10,6 +10,7 @@ const { getCliTools } = require('../../config/models');
10
10
  * - Claude: Opus 4.5, Sonnet 4.5, Haiku 4.5
11
11
  * - Codex: GPT-5.1 variants
12
12
  * - Gemini: Gemini 3 Pro Preview, Gemini 3 Flash Preview
13
+ * - Qwen: coder-model, vision-model
13
14
  */
14
15
  router.get('/', (req, res) => {
15
16
  try {
@@ -35,6 +36,7 @@ router.get('/:engine', (req, res) => {
35
36
  if (normalizedEngine.includes('claude')) normalizedEngine = 'claude';
36
37
  if (normalizedEngine.includes('codex') || normalizedEngine.includes('openai')) normalizedEngine = 'codex';
37
38
  if (normalizedEngine.includes('gemini') || normalizedEngine.includes('google')) normalizedEngine = 'gemini';
39
+ if (normalizedEngine.includes('qwen')) normalizedEngine = 'qwen';
38
40
 
39
41
  if (!cliTools[normalizedEngine]) {
40
42
  return res.status(404).json({ error: `Engine not found: ${engine}` });
@@ -0,0 +1,240 @@
1
+ /**
2
+ * Qwen Route - /api/v1/qwen
3
+ *
4
+ * Send messages to Qwen Code CLI with SSE streaming.
5
+ */
6
+
7
+ const express = require('express');
8
+ const QwenWrapper = require('../services/qwen-wrapper');
9
+ const Message = require('../models/Message');
10
+ const { v4: uuidv4 } = require('uuid');
11
+ const sessionManager = require('../services/session-manager');
12
+ const contextBridge = require('../services/context-bridge');
13
+ const { resolveWorkspacePath } = require('../../utils/workspace');
14
+
15
+ const router = express.Router();
16
+ const qwenWrapper = new QwenWrapper();
17
+
18
+ function ensureConversation(conversationId, workspacePath) {
19
+ try {
20
+ const stmt = require('../db').prepare(`
21
+ INSERT OR IGNORE INTO conversations (id, title, created_at, updated_at, metadata)
22
+ VALUES (?, ?, ?, ?, ?)
23
+ `);
24
+ const now = Date.now();
25
+ const metadata = workspacePath ? JSON.stringify({ workspace: workspacePath }) : null;
26
+ stmt.run(conversationId, 'New Chat', now, now, metadata);
27
+ } catch (err) {
28
+ console.warn('[Qwen] Failed to ensure conversation exists:', err.message);
29
+ }
30
+ }
31
+
32
+ /**
33
+ * POST /api/v1/qwen
34
+ * Body:
35
+ * {
36
+ * conversationId?: string,
37
+ * message: string,
38
+ * model?: string,
39
+ * workspace?: string
40
+ * }
41
+ */
42
+ router.post('/', async (req, res) => {
43
+ try {
44
+ console.log('[Qwen] === NEW QWEN REQUEST ===');
45
+ console.log('[Qwen] Body:', JSON.stringify(req.body, null, 2));
46
+
47
+ const { conversationId, message, model = 'coder-model', workspace } = req.body;
48
+
49
+ if (!message) {
50
+ return res.status(400).json({ error: 'message required' });
51
+ }
52
+
53
+ const isAvailable = await qwenWrapper.isAvailable();
54
+ if (!isAvailable) {
55
+ return res.status(503).json({
56
+ error: 'Qwen CLI not available',
57
+ details: 'Please install Qwen CLI: npm install -g @mmmbuto/qwen-code-termux'
58
+ });
59
+ }
60
+
61
+ const workspacePath = resolveWorkspacePath(workspace, process.cwd());
62
+ if (workspace && workspacePath !== workspace) {
63
+ console.warn(`[Qwen] Workspace corrected: ${workspace} → ${workspacePath}`);
64
+ }
65
+
66
+ const frontendConversationId = conversationId || uuidv4();
67
+ ensureConversation(frontendConversationId, workspacePath);
68
+
69
+ const { sessionId, isNew: isNewSession } = await sessionManager.getOrCreateSession(
70
+ frontendConversationId,
71
+ 'qwen',
72
+ workspacePath
73
+ );
74
+
75
+ const nativeSessionId = isNewSession ? null : sessionManager.getNativeThreadId(sessionId);
76
+
77
+ // SSE headers
78
+ res.setHeader('Content-Type', 'text/event-stream');
79
+ res.setHeader('Cache-Control', 'no-cache');
80
+ res.setHeader('Connection', 'keep-alive');
81
+ res.setHeader('X-Accel-Buffering', 'no');
82
+
83
+ res.write(`data: ${JSON.stringify({
84
+ type: 'message_start',
85
+ messageId: `user-${Date.now()}`,
86
+ sessionId,
87
+ conversationId: frontendConversationId,
88
+ engine: 'qwen'
89
+ })}\n\n`);
90
+
91
+ const lastEngine = Message.getLastEngine(frontendConversationId);
92
+ const isEngineBridge = lastEngine && lastEngine !== 'qwen';
93
+
94
+ let promptToSend = message;
95
+
96
+ if (isEngineBridge) {
97
+ const contextResult = await contextBridge.buildContext({
98
+ conversationId: frontendConversationId,
99
+ sessionId,
100
+ fromEngine: lastEngine,
101
+ toEngine: 'qwen',
102
+ userMessage: message
103
+ });
104
+ promptToSend = contextResult.prompt;
105
+
106
+ res.write(`data: ${JSON.stringify({
107
+ type: 'status',
108
+ category: 'system',
109
+ message: `Context bridged from ${lastEngine}`,
110
+ icon: '🔄'
111
+ })}\n\n`);
112
+ } else if (nativeSessionId) {
113
+ console.log(`[Qwen] Native resume: qwen --resume ${nativeSessionId}`);
114
+ }
115
+
116
+ // Save user message
117
+ try {
118
+ Message.create(
119
+ frontendConversationId,
120
+ 'user',
121
+ message,
122
+ { workspace: workspacePath, model },
123
+ Date.now(),
124
+ 'qwen'
125
+ );
126
+ sessionManager.bumpSessionActivity(sessionId, 1);
127
+ } catch (dbErr) {
128
+ console.warn('[Qwen] Failed to save user message:', dbErr.message);
129
+ }
130
+
131
+ if (isNewSession) {
132
+ const title = sessionManager.extractTitle(message);
133
+ sessionManager.updateSessionTitle(sessionId, title);
134
+ }
135
+
136
+ const result = await qwenWrapper.sendMessage({
137
+ prompt: promptToSend,
138
+ threadId: nativeSessionId,
139
+ model,
140
+ workspacePath,
141
+ processId: sessionId,
142
+ onStatus: (event) => {
143
+ res.write(`data: ${JSON.stringify(event)}\n\n`);
144
+ }
145
+ });
146
+
147
+ if (result.sessionId) {
148
+ sessionManager.setNativeThreadId(sessionId, result.sessionId);
149
+ }
150
+
151
+ try {
152
+ Message.create(
153
+ frontendConversationId,
154
+ 'assistant',
155
+ result.text,
156
+ { model, usage: result.usage },
157
+ Date.now(),
158
+ 'qwen'
159
+ );
160
+ sessionManager.bumpSessionActivity(sessionId, 1);
161
+ } catch (dbErr) {
162
+ console.warn('[Qwen] Failed to save assistant message:', dbErr.message);
163
+ }
164
+
165
+ if (contextBridge.shouldTriggerSummary(frontendConversationId, isEngineBridge)) {
166
+ contextBridge.triggerSummaryGeneration(frontendConversationId, '[Qwen]');
167
+ }
168
+
169
+ res.write(`data: ${JSON.stringify({
170
+ type: 'message_done',
171
+ content: result.text,
172
+ usage: result.usage,
173
+ sessionId,
174
+ conversationId: frontendConversationId,
175
+ engine: 'qwen',
176
+ model
177
+ })}\n\n`);
178
+
179
+ res.end();
180
+ console.log('[Qwen] === REQUEST COMPLETE ===');
181
+ } catch (error) {
182
+ console.error('[Qwen] Error:', error);
183
+ if (!res.headersSent) {
184
+ res.status(500).json({ error: error.message });
185
+ } else {
186
+ res.write(`data: ${JSON.stringify({
187
+ type: 'error',
188
+ error: error.message
189
+ })}\n\n`);
190
+ res.end();
191
+ }
192
+ }
193
+ });
194
+
195
+ /**
196
+ * GET /api/v1/qwen/status
197
+ */
198
+ router.get('/status', async (_req, res) => {
199
+ try {
200
+ const isAvailable = await qwenWrapper.isAvailable();
201
+ res.json({
202
+ available: isAvailable,
203
+ defaultModel: qwenWrapper.getDefaultModel(),
204
+ models: qwenWrapper.getAvailableModels()
205
+ });
206
+ } catch (error) {
207
+ console.error('[Qwen] Status check error:', error);
208
+ res.status(500).json({ error: error.message });
209
+ }
210
+ });
211
+
212
+ /**
213
+ * GET /api/v1/qwen/models
214
+ */
215
+ router.get('/models', (_req, res) => {
216
+ res.json({
217
+ models: qwenWrapper.getAvailableModels(),
218
+ default: qwenWrapper.getDefaultModel()
219
+ });
220
+ });
221
+
222
+ /**
223
+ * POST /api/v1/qwen/interrupt
224
+ */
225
+ router.post('/interrupt', async (req, res) => {
226
+ try {
227
+ const { sessionId } = req.body;
228
+ if (!sessionId) {
229
+ return res.status(400).json({ error: 'sessionId required' });
230
+ }
231
+
232
+ const result = qwenWrapper.interrupt(sessionId);
233
+ res.json(result);
234
+ } catch (error) {
235
+ console.error('[Qwen] Interrupt error:', error);
236
+ res.status(500).json({ error: error.message });
237
+ }
238
+ });
239
+
240
+ module.exports = router;
@@ -15,11 +15,12 @@ const SESSION_DIRS = {
15
15
  claude: path.join(process.env.HOME || '', '.claude', 'projects'),
16
16
  codex: path.join(process.env.HOME || '', '.codex', 'sessions'),
17
17
  gemini: path.join(process.env.HOME || '', '.gemini', 'sessions'),
18
+ qwen: path.join(process.env.HOME || '', '.qwen', 'projects'),
18
19
  };
19
20
 
20
21
  /**
21
22
  * POST /api/v1/sessions/import
22
- * Importa tutte le sessioni native (Claude/Codex/Gemini) nel DB
23
+ * Importa tutte le sessioni native (Claude/Codex/Gemini/Qwen) nel DB
23
24
  */
24
25
  router.post('/import', async (_req, res) => {
25
26
  try {
@@ -280,6 +281,11 @@ function pathToSlug(workspacePath) {
280
281
  return workspacePath.replace(/[\/\.]/g, '-');
281
282
  }
282
283
 
284
+ function qwenProjectDir(workspacePath) {
285
+ if (!workspacePath) return 'default';
286
+ return workspacePath.replace(/[^a-zA-Z0-9]/g, '-');
287
+ }
288
+
283
289
  /**
284
290
  * Helper: Get the filesystem path for a session file
285
291
  */
@@ -287,6 +293,7 @@ function getSessionFilePath(sessionId, engine, workspacePath, sessionPath) {
287
293
  const normalizedEngine = engine?.toLowerCase().includes('claude') ? 'claude'
288
294
  : engine?.toLowerCase().includes('codex') ? 'codex'
289
295
  : engine?.toLowerCase().includes('gemini') ? 'gemini'
296
+ : engine?.toLowerCase().includes('qwen') ? 'qwen'
290
297
  : 'claude';
291
298
 
292
299
  switch (normalizedEngine) {
@@ -303,6 +310,11 @@ function getSessionFilePath(sessionId, engine, workspacePath, sessionPath) {
303
310
  return findCodexSessionFile(baseDir, nativeId);
304
311
  case 'gemini':
305
312
  return path.join(SESSION_DIRS.gemini, `${sessionId}.jsonl`);
313
+ case 'qwen': {
314
+ const project = qwenProjectDir(workspacePath);
315
+ const fileId = sessionPath || sessionId;
316
+ return path.join(SESSION_DIRS.qwen, project, 'chats', `${fileId}.jsonl`);
317
+ }
306
318
  default:
307
319
  return null;
308
320
  }
@@ -22,6 +22,7 @@ const jobsRouter = require('./routes/jobs');
22
22
  const chatRouter = require('./routes/chat');
23
23
  const codexRouter = require('./routes/codex');
24
24
  const geminiRouter = require('./routes/gemini');
25
+ const qwenRouter = require('./routes/qwen');
25
26
  const modelsRouter = require('./routes/models');
26
27
  const workspaceRouter = require('./routes/workspace');
27
28
  const workspacesRouter = require('./routes/workspaces');
@@ -51,7 +52,7 @@ app.get('/health', (req, res) => {
51
52
  status: 'ok',
52
53
  service: 'nexuscli-backend',
53
54
  version: pkg.version,
54
- engines: ['claude', 'codex', 'gemini'],
55
+ engines: ['claude', 'codex', 'gemini', 'qwen'],
55
56
  port: PORT,
56
57
  timestamp: new Date().toISOString()
57
58
  });
@@ -78,6 +79,7 @@ app.use('/api/v1/jobs', authMiddleware, jobsRouter);
78
79
  app.use('/api/v1/chat', authMiddleware, chatRateLimiter, chatRouter);
79
80
  app.use('/api/v1/codex', authMiddleware, chatRateLimiter, codexRouter);
80
81
  app.use('/api/v1/gemini', authMiddleware, chatRateLimiter, geminiRouter);
82
+ app.use('/api/v1/qwen', authMiddleware, chatRateLimiter, qwenRouter);
81
83
  app.use('/api/v1/upload', authMiddleware, uploadRouter); // File upload
82
84
 
83
85
  // STT routes
@@ -89,13 +91,14 @@ app.get('/', (req, res) => {
89
91
  res.json({
90
92
  service: 'NexusCLI Backend',
91
93
  version: pkg.version,
92
- engines: ['claude', 'codex', 'gemini'],
94
+ engines: ['claude', 'codex', 'gemini', 'qwen'],
93
95
  endpoints: {
94
96
  health: '/health',
95
97
  models: '/api/v1/models',
96
98
  chat: '/api/v1/chat (Claude)',
97
99
  codex: '/api/v1/codex (OpenAI)',
98
100
  gemini: '/api/v1/gemini (Google)',
101
+ qwen: '/api/v1/qwen (Qwen)',
99
102
  conversations: '/api/v1/conversations',
100
103
  jobs: '/api/v1/jobs'
101
104
  }
@@ -154,10 +157,10 @@ async function start() {
154
157
  // Continue anyway - database will sync on first workspace mount
155
158
  }
156
159
 
157
- // Import native sessions from all engines (Claude/Codex/Gemini)
160
+ // Import native sessions from all engines (Claude/Codex/Gemini/Qwen)
158
161
  try {
159
162
  const imported = sessionImporter.importAll();
160
- console.log(`[Startup] Imported sessions → Claude:${imported.claude} Codex:${imported.codex} Gemini:${imported.gemini}`);
163
+ console.log(`[Startup] Imported sessions → Claude:${imported.claude} Codex:${imported.codex} Gemini:${imported.gemini} Qwen:${imported.qwen}`);
161
164
  } catch (error) {
162
165
  console.error('[Startup] ⚠️ Session import failed:', error.message);
163
166
  }