@mmmbuto/nexuscli 0.6.1 → 0.6.3
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 +117 -23
- package/lib/cli/setup-termux.js +8 -3
- package/lib/cli/start.js +8 -3
- package/lib/cli/status.js +6 -3
- package/lib/server/services/session-manager.js +14 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,8 +1,48 @@
|
|
|
1
|
-
# NexusCLI
|
|
1
|
+
# NexusCLI — AI Terminal Cockpit
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
<p align="center">
|
|
4
|
+
<img src=".github/header/header.png" width="900" />
|
|
5
|
+
</p>
|
|
4
6
|
|
|
5
|
-
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Overview
|
|
11
|
+
|
|
12
|
+
NexusCLI is an experimental, ultra-light terminal cockpit designed for
|
|
13
|
+
AI-assisted development workflows on Termux (Android).
|
|
14
|
+
|
|
15
|
+
**v0.6.3** - Mobile-First AI Control Plane
|
|
16
|
+
|
|
17
|
+
Web UI wrapper for Claude Code, Codex CLI, and Gemini CLI with voice input support.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
[](https://www.npmjs.com/package/@mmmbuto/nexuscli)
|
|
22
|
+
[](https://www.npmjs.com/package/@mmmbuto/nexuscli)
|
|
23
|
+
[](https://ko-fi.com/dionanos)
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Screenshots
|
|
28
|
+
|
|
29
|
+
<p align="center">
|
|
30
|
+
<img src="docs/assets/screenshots/nexuscli-multilang-preview.png" width="45%" />
|
|
31
|
+
<img src="docs/assets/screenshots/nexuscli-mobile-terminal.png" width="45%" />
|
|
32
|
+
</p>
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Features
|
|
37
|
+
|
|
38
|
+
- Multi-engine support (Claude, Codex, Gemini)
|
|
39
|
+
- **Voice input** (OpenAI Whisper STT)
|
|
40
|
+
- **Auto HTTPS** for remote microphone access
|
|
41
|
+
- Mobile-first responsive UI
|
|
42
|
+
- SSE streaming responses
|
|
43
|
+
- Workspace management
|
|
44
|
+
- Conversation history
|
|
45
|
+
- Model selector with think mode toggle
|
|
6
46
|
|
|
7
47
|
## Supported Engines
|
|
8
48
|
|
|
@@ -12,23 +52,16 @@ Web UI for Claude Code, Codex CLI, and Gemini CLI.
|
|
|
12
52
|
| **Codex** | GPT-5.1, GPT-5.1 Codex (Mini/Max) | OpenAI |
|
|
13
53
|
| **Gemini** | Gemini 3 Pro Preview | Google |
|
|
14
54
|
|
|
15
|
-
|
|
55
|
+
---
|
|
16
56
|
|
|
17
|
-
|
|
57
|
+
## Install
|
|
18
58
|
|
|
19
59
|
```bash
|
|
20
|
-
# From
|
|
21
|
-
npm install -g
|
|
60
|
+
# From npm
|
|
61
|
+
npm install -g @mmmbuto/nexuscli
|
|
22
62
|
|
|
23
|
-
# From
|
|
24
|
-
npm install -g
|
|
25
|
-
|
|
26
|
-
# Local clone install
|
|
27
|
-
cd ~/Dev/NexusCLI && npm install -g .
|
|
28
|
-
|
|
29
|
-
# Termux manual install
|
|
30
|
-
cd ~/Dev/NexusCLI
|
|
31
|
-
./scripts/install-termux.sh
|
|
63
|
+
# From GitHub
|
|
64
|
+
npm install -g github:DioNanos/nexuscli
|
|
32
65
|
```
|
|
33
66
|
|
|
34
67
|
## Setup
|
|
@@ -43,19 +76,51 @@ nexuscli init
|
|
|
43
76
|
nexuscli start
|
|
44
77
|
```
|
|
45
78
|
|
|
46
|
-
|
|
79
|
+
### Network Access
|
|
80
|
+
|
|
81
|
+
| Protocol | Port | URL | Use Case |
|
|
82
|
+
|----------|------|-----|----------|
|
|
83
|
+
| **HTTP** | 41800 | `http://localhost:41800` | Local access |
|
|
84
|
+
| **HTTPS** | 41801 | `https://<ip>:41801` | Remote access, voice input |
|
|
85
|
+
|
|
86
|
+
> **Note**: HTTPS is required for microphone access from remote devices (browser security).
|
|
87
|
+
> Self-signed certificates are auto-generated on first run.
|
|
88
|
+
|
|
89
|
+
---
|
|
47
90
|
|
|
48
91
|
## Commands
|
|
49
92
|
|
|
50
93
|
| Command | Description |
|
|
51
94
|
|---------|-------------|
|
|
52
95
|
| `nexuscli init` | Setup wizard |
|
|
53
|
-
| `nexuscli start` | Start server |
|
|
96
|
+
| `nexuscli start` | Start server (HTTP:41800 + HTTPS:41801) |
|
|
54
97
|
| `nexuscli stop` | Stop server |
|
|
55
|
-
| `nexuscli status` | Show status |
|
|
98
|
+
| `nexuscli status` | Show status, ports, and engines |
|
|
56
99
|
| `nexuscli engines` | Manage AI engines |
|
|
57
100
|
| `nexuscli workspaces` | Manage workspaces |
|
|
58
101
|
| `nexuscli config` | Configuration |
|
|
102
|
+
| `nexuscli api` | Manage API keys |
|
|
103
|
+
| `nexuscli users` | User management |
|
|
104
|
+
| `nexuscli setup-termux` | Bootstrap Termux + show network URLs |
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## API Keys
|
|
109
|
+
|
|
110
|
+
Configure API keys for additional providers:
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
nexuscli api list # List configured keys
|
|
114
|
+
nexuscli api set deepseek <key> # DeepSeek models
|
|
115
|
+
nexuscli api set openai <key> # Voice input (Whisper STT)
|
|
116
|
+
nexuscli api set openrouter <key> # Future: Multi-provider gateway
|
|
117
|
+
nexuscli api delete <provider> # Remove key
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
> **Note**: Claude/Codex/Gemini keys are managed by their respective CLIs.
|
|
121
|
+
> OpenAI key enables voice input via Whisper. HTTPS auto-generated for remote mic access.
|
|
122
|
+
|
|
123
|
+
---
|
|
59
124
|
|
|
60
125
|
## Requirements
|
|
61
126
|
|
|
@@ -64,7 +129,31 @@ Open browser: `http://localhost:41800`
|
|
|
64
129
|
- Claude Code CLI (`claude`)
|
|
65
130
|
- Codex CLI (`codex`)
|
|
66
131
|
- Gemini CLI (`gemini`)
|
|
67
|
-
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## Termux-First Architecture
|
|
136
|
+
|
|
137
|
+
NexusCLI is designed primarily for **Termux** on Android devices.
|
|
138
|
+
|
|
139
|
+
### Stack
|
|
140
|
+
|
|
141
|
+
- **Termux** - primary runtime environment
|
|
142
|
+
- **tmux** - session management
|
|
143
|
+
- **Node.js + SSE** - lightweight backend
|
|
144
|
+
- **React** - minimal UI
|
|
145
|
+
|
|
146
|
+
### Purpose
|
|
147
|
+
|
|
148
|
+
This project exists to study:
|
|
149
|
+
|
|
150
|
+
- terminal-driven AI orchestration
|
|
151
|
+
- ultra-light architectures for constrained devices
|
|
152
|
+
- mobile development workflows
|
|
153
|
+
|
|
154
|
+
It is a **research and learning tool**.
|
|
155
|
+
|
|
156
|
+
---
|
|
68
157
|
|
|
69
158
|
## API Endpoints
|
|
70
159
|
|
|
@@ -76,12 +165,14 @@ Open browser: `http://localhost:41800`
|
|
|
76
165
|
| `GET /api/v1/models` | All | List available models |
|
|
77
166
|
| `GET /health` | - | Health check |
|
|
78
167
|
|
|
168
|
+
---
|
|
169
|
+
|
|
79
170
|
## Development
|
|
80
171
|
|
|
81
172
|
```bash
|
|
82
173
|
# Clone
|
|
83
|
-
git clone
|
|
84
|
-
cd
|
|
174
|
+
git clone https://github.com/DioNanos/nexuscli.git
|
|
175
|
+
cd nexuscli
|
|
85
176
|
|
|
86
177
|
# Install deps
|
|
87
178
|
npm install
|
|
@@ -91,6 +182,9 @@ cd frontend && npm install && npm run build && cd ..
|
|
|
91
182
|
npm run dev
|
|
92
183
|
```
|
|
93
184
|
|
|
185
|
+
---
|
|
186
|
+
|
|
94
187
|
## License
|
|
95
188
|
|
|
96
|
-
MIT
|
|
189
|
+
MIT License.
|
|
190
|
+
See `LICENSE` for details.
|
package/lib/cli/setup-termux.js
CHANGED
|
@@ -191,7 +191,8 @@ async function setupTermux() {
|
|
|
191
191
|
} catch {
|
|
192
192
|
config = { server: { port: 41800 } };
|
|
193
193
|
}
|
|
194
|
-
const
|
|
194
|
+
const httpPort = config.server?.port || 41800;
|
|
195
|
+
const httpsPort = parseInt(httpPort) + 1;
|
|
195
196
|
|
|
196
197
|
// Output connection info
|
|
197
198
|
console.log(chalk.bold('╔═══════════════════════════════════════════════════╗'));
|
|
@@ -201,10 +202,14 @@ async function setupTermux() {
|
|
|
201
202
|
console.log(chalk.cyan(' SSH:'));
|
|
202
203
|
console.log(chalk.white(` ssh ${user}@${ip} -p ${sshPort}`));
|
|
203
204
|
console.log('');
|
|
204
|
-
console.log(chalk.cyan(' NexusCLI:'));
|
|
205
|
-
console.log(chalk.white(`
|
|
205
|
+
console.log(chalk.cyan(' NexusCLI (local):'));
|
|
206
|
+
console.log(chalk.white(` http://localhost:${httpPort}`));
|
|
207
|
+
console.log('');
|
|
208
|
+
console.log(chalk.cyan(' NexusCLI (remote + voice):'));
|
|
209
|
+
console.log(chalk.white(` https://${ip}:${httpsPort}`));
|
|
206
210
|
console.log('');
|
|
207
211
|
console.log(chalk.gray(' Note: Set password with: passwd'));
|
|
212
|
+
console.log(chalk.gray(' Note: HTTPS uses self-signed cert (accept in browser)'));
|
|
208
213
|
console.log('');
|
|
209
214
|
|
|
210
215
|
process.exit(0);
|
package/lib/cli/start.js
CHANGED
|
@@ -181,7 +181,10 @@ async function start(options) {
|
|
|
181
181
|
const port = options.port || config.server.port;
|
|
182
182
|
config.server.port = port;
|
|
183
183
|
|
|
184
|
-
const
|
|
184
|
+
const httpPort = port;
|
|
185
|
+
const httpsPort = parseInt(port) + 1;
|
|
186
|
+
const httpUrl = `http://localhost:${httpPort}`;
|
|
187
|
+
const httpsUrl = `https://localhost:${httpsPort}`;
|
|
185
188
|
|
|
186
189
|
// Foreground mode (--foreground flag)
|
|
187
190
|
if (options.foreground) {
|
|
@@ -198,7 +201,9 @@ async function start(options) {
|
|
|
198
201
|
console.log(chalk.green(` ✓ Server started (PID: ${pid})`));
|
|
199
202
|
console.log(chalk.gray(` Logs: ${PATHS.SERVER_LOG}`));
|
|
200
203
|
console.log('');
|
|
201
|
-
console.log(chalk.bold(
|
|
204
|
+
console.log(chalk.bold(' Network:'));
|
|
205
|
+
console.log(` HTTP: ${chalk.cyan(httpUrl)} (local)`);
|
|
206
|
+
console.log(` HTTPS: ${chalk.cyan(httpsUrl)} (remote + voice)`);
|
|
202
207
|
console.log('');
|
|
203
208
|
|
|
204
209
|
// Ask to open browser (unless --no-browser)
|
|
@@ -206,7 +211,7 @@ async function start(options) {
|
|
|
206
211
|
const shouldOpen = await askYesNo(` Open browser? ${chalk.gray('(Y/n)')} `);
|
|
207
212
|
|
|
208
213
|
if (shouldOpen) {
|
|
209
|
-
const opened = openBrowser(
|
|
214
|
+
const opened = openBrowser(httpUrl);
|
|
210
215
|
if (opened) {
|
|
211
216
|
console.log(chalk.green(' ✓ Browser opened'));
|
|
212
217
|
} else {
|
package/lib/cli/status.js
CHANGED
|
@@ -121,7 +121,9 @@ async function status() {
|
|
|
121
121
|
console.log(chalk.bold(`║ Server: ${chalk.red('✗ Stopped')}`));
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
-
|
|
124
|
+
const httpPort = config.server.port;
|
|
125
|
+
const httpsPort = parseInt(config.server.port) + 1;
|
|
126
|
+
console.log(chalk.bold(`║ Ports: HTTP ${chalk.white(httpPort)} / HTTPS ${chalk.white(httpsPort)}`));
|
|
125
127
|
console.log(chalk.bold('║'));
|
|
126
128
|
|
|
127
129
|
// Engines
|
|
@@ -183,9 +185,10 @@ async function status() {
|
|
|
183
185
|
console.log(chalk.bold('╚═══════════════════════════════════════════╝'));
|
|
184
186
|
console.log('');
|
|
185
187
|
|
|
186
|
-
//
|
|
188
|
+
// URLs if running
|
|
187
189
|
if (serverStatus.running) {
|
|
188
|
-
console.log(chalk.cyan(`
|
|
190
|
+
console.log(` HTTP: ${chalk.cyan(`http://localhost:${httpPort}`)}`);
|
|
191
|
+
console.log(` HTTPS: ${chalk.cyan(`https://localhost:${httpsPort}`)} (remote + voice)`);
|
|
189
192
|
console.log('');
|
|
190
193
|
}
|
|
191
194
|
}
|
|
@@ -147,6 +147,15 @@ class SessionManager {
|
|
|
147
147
|
return crypto.createHash('md5').update(workspacePath).digest('hex').substring(0, 8);
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
+
/**
|
|
151
|
+
* Normalize workspace path (remove trailing slashes)
|
|
152
|
+
* Prevents duplicate entries like /path and /path/
|
|
153
|
+
*/
|
|
154
|
+
_normalizePath(workspacePath) {
|
|
155
|
+
if (!workspacePath) return '';
|
|
156
|
+
return workspacePath.replace(/\/+$/, '');
|
|
157
|
+
}
|
|
158
|
+
|
|
150
159
|
/**
|
|
151
160
|
* Get or create session for conversation + engine
|
|
152
161
|
*
|
|
@@ -164,16 +173,17 @@ class SessionManager {
|
|
|
164
173
|
*/
|
|
165
174
|
async getOrCreateSession(conversationId, engine, workspacePath) {
|
|
166
175
|
const normalizedEngine = this._normalizeEngine(engine);
|
|
176
|
+
const normalizedPath = this._normalizePath(workspacePath);
|
|
167
177
|
const cacheKey = this._getCacheKey(conversationId, normalizedEngine);
|
|
168
178
|
|
|
169
|
-
console.log(`[SessionManager] getOrCreateSession(${conversationId}, ${normalizedEngine}, ${
|
|
179
|
+
console.log(`[SessionManager] getOrCreateSession(${conversationId}, ${normalizedEngine}, ${normalizedPath})`);
|
|
170
180
|
|
|
171
181
|
// 1. Check RAM cache first (fastest path)
|
|
172
182
|
if (this.sessionMap.has(cacheKey)) {
|
|
173
183
|
const cachedId = this.sessionMap.get(cacheKey);
|
|
174
184
|
|
|
175
185
|
// Verify it still exists on filesystem
|
|
176
|
-
if (this.sessionFileExists(cachedId, normalizedEngine,
|
|
186
|
+
if (this.sessionFileExists(cachedId, normalizedEngine, normalizedPath)) {
|
|
177
187
|
this.lastAccess.set(cacheKey, Date.now());
|
|
178
188
|
console.log(`[SessionManager] Cache hit: ${cachedId}`);
|
|
179
189
|
return { sessionId: cachedId, isNew: false };
|
|
@@ -195,7 +205,7 @@ class SessionManager {
|
|
|
195
205
|
|
|
196
206
|
if (row) {
|
|
197
207
|
// 3. Verify session file exists on filesystem
|
|
198
|
-
if (this.sessionFileExists(row.id, normalizedEngine, row.workspace_path ||
|
|
208
|
+
if (this.sessionFileExists(row.id, normalizedEngine, row.workspace_path || normalizedPath)) {
|
|
199
209
|
// Valid session - update cache and return
|
|
200
210
|
this.sessionMap.set(cacheKey, row.id);
|
|
201
211
|
this.lastAccess.set(cacheKey, Date.now());
|
|
@@ -223,7 +233,7 @@ class SessionManager {
|
|
|
223
233
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
224
234
|
`);
|
|
225
235
|
const title = 'New Chat'; // Will be updated after first response
|
|
226
|
-
insertStmt.run(sessionId,
|
|
236
|
+
insertStmt.run(sessionId, normalizedPath, normalizedEngine, conversationId, title, now, now);
|
|
227
237
|
saveDb();
|
|
228
238
|
console.log(`[SessionManager] Saved to DB: ${sessionId}`);
|
|
229
239
|
} catch (dbErr) {
|