@mmmbuto/nexuscli 0.9.7004-termux → 0.9.7005-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
|
|
11
|
+
NexusCLI is a lightweight, Termux-first AI cockpit that orchestrates Claude Code, Codex CLI, Gemini CLI, and Qwen Code CLI from a single web/terminal UI. It supports live streaming, interrupts, session resume, workspace isolation, and remote voice input with auto HTTPS setup.
|
|
12
12
|
|
|
13
13
|
---
|
|
14
14
|
|
|
@@ -29,39 +29,28 @@ NexusCLI is a lightweight, Termux-first AI cockpit to orchestrate Claude Code, C
|
|
|
29
29
|
|
|
30
30
|
## Highlights (v0.9.7004-termux)
|
|
31
31
|
|
|
32
|
-
- **QWEN
|
|
32
|
+
- **QWEN CLI**: Integrated Qwen Code CLI with `coder-model` and `vision-model`
|
|
33
33
|
- **Statusbar Realtime**: Qwen tool events streamed live in the UI
|
|
34
|
-
- **Light Theme**:
|
|
34
|
+
- **Light Theme**: Higher contrast + correct mobile statusbar colors
|
|
35
35
|
|
|
36
|
-
### v0.9.6
|
|
36
|
+
### Stable (v0.9.6)
|
|
37
37
|
|
|
38
|
-
-
|
|
39
|
-
-
|
|
40
|
-
- **Cleaner Errors**: job SSE errors now display correctly in UI
|
|
41
|
-
|
|
42
|
-
### v0.9.5
|
|
43
|
-
|
|
44
|
-
- **GPT-5.2 Codex**: Added latest frontier agentic coding model as default
|
|
45
|
-
- Updated Codex model catalog to match latest OpenAI CLI
|
|
46
|
-
|
|
47
|
-
### v0.9.4
|
|
48
|
-
|
|
49
|
-
- **Dark/Light Theme**: Toggle between themes via UserMenu (persisted in localStorage)
|
|
50
|
-
- **Rate Limiting**: Chat endpoints now protected (10 req/min per user)
|
|
51
|
-
- **Architecture Docs**: New `docs/ARCHITECTURE.md` with system diagrams
|
|
52
|
-
- CSS refactored to use CSS variables for theming support
|
|
38
|
+
- Jobs runner restored with Termux-safe execution
|
|
39
|
+
- Cleaner job SSE errors in UI
|
|
53
40
|
|
|
54
41
|
## Features
|
|
55
42
|
|
|
56
|
-
- Multi-engine
|
|
57
|
-
-
|
|
58
|
-
-
|
|
59
|
-
-
|
|
60
|
-
- Workspace
|
|
61
|
-
-
|
|
62
|
-
-
|
|
63
|
-
-
|
|
64
|
-
-
|
|
43
|
+
- Multi-engine orchestration (Claude, Codex, Gemini, Qwen)
|
|
44
|
+
- SSE streaming with realtime tool statusbar
|
|
45
|
+
- Interrupt/stop per engine
|
|
46
|
+
- Session resume + native session import across engines
|
|
47
|
+
- Workspace isolation, switching, and history
|
|
48
|
+
- File & image attachments (vision models supported)
|
|
49
|
+
- Model selector (think mode + reasoning levels where available)
|
|
50
|
+
- Voice input (browser STT, optional Whisper via OpenAI key)
|
|
51
|
+
- Conversation search + pin/bookmark
|
|
52
|
+
- Built-in jobs runner API for shell tasks
|
|
53
|
+
- Config API + rate limiting on chat endpoints
|
|
65
54
|
|
|
66
55
|
## Supported Engines
|
|
67
56
|
|
|
@@ -88,11 +77,11 @@ npm install -g github:DioNanos/nexuscli
|
|
|
88
77
|
### Release Channels
|
|
89
78
|
|
|
90
79
|
```bash
|
|
80
|
+
# Latest (default)
|
|
81
|
+
npm install -g @mmmbuto/nexuscli
|
|
82
|
+
|
|
91
83
|
# Stable channel (pinned)
|
|
92
84
|
npm install -g @mmmbuto/nexuscli@stable
|
|
93
|
-
|
|
94
|
-
# Testing channel
|
|
95
|
-
npm install -g @mmmbuto/nexuscli@testing
|
|
96
85
|
```
|
|
97
86
|
|
|
98
87
|
## Setup
|
|
@@ -131,8 +120,10 @@ nexuscli start
|
|
|
131
120
|
| `nexuscli workspaces` | Manage workspaces |
|
|
132
121
|
| `nexuscli model` | Default model |
|
|
133
122
|
| `nexuscli api` | Additional API keys (e.g., Whisper) |
|
|
123
|
+
| `nexuscli logs` | View server logs |
|
|
134
124
|
| `nexuscli users` | Users |
|
|
135
|
-
| `
|
|
125
|
+
| `nexuscli setup-termux` | Termux helpers (services, paths) |
|
|
126
|
+
| `nexuscli uninstall` | Remove NexusCLI |
|
|
136
127
|
|
|
137
128
|
---
|
|
138
129
|
|
|
@@ -202,6 +193,9 @@ It is a **research and learning tool**.
|
|
|
202
193
|
| `POST /api/v1/qwen/interrupt` | Qwen | Stop running generation |
|
|
203
194
|
| `GET /api/v1/models` | All | List available models |
|
|
204
195
|
| `GET /api/v1/config` | - | Get user preferences (default model) |
|
|
196
|
+
| `POST /api/v1/sessions/import` | - | Import native sessions (admin) |
|
|
197
|
+
| `POST /api/v1/jobs` | - | Run a background job (SSE stream) |
|
|
198
|
+
| `GET /api/v1/workspaces` | - | List workspaces from sessions |
|
|
205
199
|
| `GET /health` | - | Health check |
|
|
206
200
|
|
|
207
201
|
---
|
|
@@ -161,14 +161,19 @@ class SessionImporter {
|
|
|
161
161
|
|
|
162
162
|
/**
|
|
163
163
|
* Controlla se la sessione esiste già
|
|
164
|
+
* Returns false if sessions table doesn't exist (allows import)
|
|
164
165
|
*/
|
|
165
166
|
sessionExists(sessionId) {
|
|
166
167
|
try {
|
|
167
168
|
const stmt = prepare('SELECT 1 FROM sessions WHERE id = ?');
|
|
168
169
|
return !!stmt.get(sessionId);
|
|
169
170
|
} catch (err) {
|
|
171
|
+
// If sessions table doesn't exist, return false to allow import
|
|
172
|
+
if (err.message && err.message.includes('no such table')) {
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
170
175
|
console.warn(`[SessionImporter] exists check failed: ${err.message}`);
|
|
171
|
-
return true; // default to skip to avoid duplicates
|
|
176
|
+
return true; // default to skip to avoid duplicates on other errors
|
|
172
177
|
}
|
|
173
178
|
}
|
|
174
179
|
|
|
@@ -11,6 +11,7 @@ class WorkspaceManager {
|
|
|
11
11
|
this.claudePath = path.join(process.env.HOME, '.claude');
|
|
12
12
|
this.historyPath = path.join(this.claudePath, 'history.jsonl');
|
|
13
13
|
this.projectsPath = path.join(this.claudePath, 'projects');
|
|
14
|
+
this.qwenPath = path.join(process.env.HOME, '.qwen');
|
|
14
15
|
this.cacheTtlMs = 5 * 60 * 1000; // 5 minutes
|
|
15
16
|
this.historyCache = {
|
|
16
17
|
entries: null,
|
|
@@ -38,6 +39,17 @@ class WorkspaceManager {
|
|
|
38
39
|
return '/' + slug.replace(/^-/, '').replace(/-/g, '/');
|
|
39
40
|
}
|
|
40
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Convert workspace path to Qwen project directory name
|
|
44
|
+
* Matches Qwen Storage.sanitizeCwd behavior
|
|
45
|
+
* @param {string} workspacePath - Absolute path
|
|
46
|
+
* @returns {string} Qwen project directory name
|
|
47
|
+
*/
|
|
48
|
+
qwenPathToProject(workspacePath) {
|
|
49
|
+
if (!workspacePath) return 'default';
|
|
50
|
+
return workspacePath.replace(/[^a-zA-Z0-9]/g, '-');
|
|
51
|
+
}
|
|
52
|
+
|
|
41
53
|
/**
|
|
42
54
|
* Discover all workspaces from .claude/projects/
|
|
43
55
|
* Reads real workspace path from session file 'cwd' field
|
|
@@ -169,6 +181,10 @@ class WorkspaceManager {
|
|
|
169
181
|
const claudeSessions = await this.indexClaudeCodeSessions(workspacePath);
|
|
170
182
|
sessions.push(...claudeSessions);
|
|
171
183
|
|
|
184
|
+
// Index Qwen sessions
|
|
185
|
+
const qwenSessions = await this.indexQwenSessions(workspacePath);
|
|
186
|
+
sessions.push(...qwenSessions);
|
|
187
|
+
|
|
172
188
|
// TODO: Index other CLI tools (Codex, Aider, etc.)
|
|
173
189
|
|
|
174
190
|
// Sync sessions to database (batch mode to avoid "Statement closed" errors)
|
|
@@ -332,6 +348,111 @@ class WorkspaceManager {
|
|
|
332
348
|
return sessions;
|
|
333
349
|
}
|
|
334
350
|
|
|
351
|
+
/**
|
|
352
|
+
* Index Qwen sessions from .qwen/projects/<workspace-sanitized>/chats/
|
|
353
|
+
* @param {string} workspacePath
|
|
354
|
+
* @returns {Promise<Array>}
|
|
355
|
+
*/
|
|
356
|
+
async indexQwenSessions(workspacePath) {
|
|
357
|
+
const project = this.qwenPathToProject(workspacePath);
|
|
358
|
+
const projectDir = path.join(this.qwenPath, 'projects', project);
|
|
359
|
+
|
|
360
|
+
if (!fs.existsSync(projectDir)) {
|
|
361
|
+
// Silently skip - Qwen may not be used for this workspace
|
|
362
|
+
return [];
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const chatsDir = path.join(projectDir, 'chats');
|
|
366
|
+
if (!fs.existsSync(chatsDir)) {
|
|
367
|
+
return [];
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// List session files
|
|
371
|
+
const sessionFiles = fs.readdirSync(chatsDir)
|
|
372
|
+
.filter(f => f.endsWith('.jsonl'));
|
|
373
|
+
|
|
374
|
+
console.log(`[WorkspaceManager] Found ${sessionFiles.length} Qwen session files in ${project}`);
|
|
375
|
+
|
|
376
|
+
const sessions = [];
|
|
377
|
+
|
|
378
|
+
for (const file of sessionFiles) {
|
|
379
|
+
const sessionId = file.replace('.jsonl', '');
|
|
380
|
+
const sessionPath = path.join(chatsDir, file);
|
|
381
|
+
|
|
382
|
+
// Read session file line by line
|
|
383
|
+
const messages = [];
|
|
384
|
+
let firstTimestamp = null;
|
|
385
|
+
let lastTimestamp = null;
|
|
386
|
+
|
|
387
|
+
try {
|
|
388
|
+
const fileStream = fs.createReadStream(sessionPath);
|
|
389
|
+
const rl = readline.createInterface({
|
|
390
|
+
input: fileStream,
|
|
391
|
+
crlfDelay: Infinity
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
for await (const line of rl) {
|
|
395
|
+
if (!line.trim()) continue;
|
|
396
|
+
try {
|
|
397
|
+
const entry = JSON.parse(line);
|
|
398
|
+
|
|
399
|
+
// Only include user/assistant messages (skip system/ui_telemetry)
|
|
400
|
+
if (entry.type === 'user' || entry.type === 'assistant') {
|
|
401
|
+
const timestamp = new Date(entry.timestamp).getTime();
|
|
402
|
+
if (!firstTimestamp || timestamp < firstTimestamp) firstTimestamp = timestamp;
|
|
403
|
+
if (!lastTimestamp || timestamp > lastTimestamp) lastTimestamp = timestamp;
|
|
404
|
+
|
|
405
|
+
// Extract content from Qwen format
|
|
406
|
+
let content = '';
|
|
407
|
+
const parts = entry.message?.parts;
|
|
408
|
+
if (Array.isArray(parts)) {
|
|
409
|
+
content = parts
|
|
410
|
+
.filter(p => p && p.text)
|
|
411
|
+
.map(p => p.text)
|
|
412
|
+
.join('\n');
|
|
413
|
+
} else if (typeof entry.message?.content === 'string') {
|
|
414
|
+
content = entry.message.content;
|
|
415
|
+
} else if (entry.text) {
|
|
416
|
+
content = entry.text;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
messages.push({
|
|
420
|
+
role: entry.type,
|
|
421
|
+
content: content,
|
|
422
|
+
timestamp: entry.timestamp
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
} catch (e) {
|
|
426
|
+
// Skip malformed lines
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
} catch (error) {
|
|
430
|
+
console.error(`[WorkspaceManager] Error reading Qwen session ${sessionId}:`, error.message);
|
|
431
|
+
continue;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
if (messages.length > 0) {
|
|
435
|
+
sessions.push({
|
|
436
|
+
id: sessionId,
|
|
437
|
+
engine: 'qwen',
|
|
438
|
+
workspace_path: workspacePath,
|
|
439
|
+
session_path: sessionPath,
|
|
440
|
+
title: this.extractTitle(messages),
|
|
441
|
+
message_count: messages.length,
|
|
442
|
+
last_used_at: lastTimestamp,
|
|
443
|
+
created_at: firstTimestamp
|
|
444
|
+
// NOTE: messages NOT included - loaded on-demand by CliLoader (filesystem = source of truth)
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Sort by most recent first
|
|
450
|
+
sessions.sort((a, b) => b.last_used_at - a.last_used_at);
|
|
451
|
+
|
|
452
|
+
console.log(`[WorkspaceManager] Loaded ${sessions.length} Qwen sessions (sorted by recency)`);
|
|
453
|
+
return sessions;
|
|
454
|
+
}
|
|
455
|
+
|
|
335
456
|
/**
|
|
336
457
|
* Return cached history entries with TTL and fs.watch invalidation.
|
|
337
458
|
*/
|