@mmmbuto/nexuscli 0.5.1 → 0.5.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.
@@ -59,7 +59,7 @@
59
59
 
60
60
  <!-- Prevent Scaling on iOS -->
61
61
  <meta name="format-detection" content="telephone=no" />
62
- <script type="module" crossorigin src="/assets/index-CikJbUR5.js"></script>
62
+ <script type="module" crossorigin src="/assets/index-DaG2hvsF.js"></script>
63
63
  <link rel="stylesheet" crossorigin href="/assets/index-Bn_l1e6e.css">
64
64
  </head>
65
65
  <body>
package/lib/cli/api.js CHANGED
@@ -64,7 +64,7 @@ async function apiCommand(action, provider, key) {
64
64
  console.log(` ${configured} ${id.padEnd(12)} - ${info.description}`);
65
65
  });
66
66
  console.log('');
67
- return;
67
+ process.exit(0);
68
68
  }
69
69
 
70
70
  if (action === 'set') {
@@ -98,11 +98,11 @@ async function apiCommand(action, provider, key) {
98
98
  console.log(`\n✅ ${info.name} API key saved successfully!\n`);
99
99
  console.log(` Provider: ${providerLower}`);
100
100
  console.log(` Key: ${key.substring(0, 8)}${'*'.repeat(key.length - 12)}${key.slice(-4)}\n`);
101
+ process.exit(0);
101
102
  } else {
102
103
  console.error('\n❌ Failed to save API key. Check database.\n');
103
104
  process.exit(1);
104
105
  }
105
- return;
106
106
  }
107
107
 
108
108
  if (action === 'delete' || action === 'remove') {
@@ -116,11 +116,11 @@ async function apiCommand(action, provider, key) {
116
116
 
117
117
  if (success) {
118
118
  console.log(`\n✅ API key for ${providerLower} deleted.\n`);
119
+ process.exit(0);
119
120
  } else {
120
121
  console.error(`\n❌ Failed to delete API key for ${providerLower}.\n`);
121
122
  process.exit(1);
122
123
  }
123
- return;
124
124
  }
125
125
 
126
126
  if (action === 'test') {
@@ -140,7 +140,7 @@ async function apiCommand(action, provider, key) {
140
140
 
141
141
  console.log(`\n🔑 ${providerLower} API key found: ${apiKey.substring(0, 8)}...${apiKey.slice(-4)}\n`);
142
142
  // TODO: Add actual API test call
143
- return;
143
+ process.exit(0);
144
144
  }
145
145
 
146
146
  // Unknown action
@@ -0,0 +1,213 @@
1
+ /**
2
+ * nexuscli setup-termux - Bootstrap Termux for remote development
3
+ */
4
+
5
+ const chalk = require('chalk');
6
+ const { execSync, spawnSync } = require('child_process');
7
+ const os = require('os');
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+
11
+ const { isTermux } = require('../utils/termux');
12
+ const { getConfig } = require('../config/manager');
13
+
14
+ /**
15
+ * Get local IP address
16
+ */
17
+ function getLocalIP() {
18
+ const interfaces = os.networkInterfaces();
19
+ for (const name of Object.keys(interfaces)) {
20
+ for (const iface of interfaces[name]) {
21
+ if (iface.family === 'IPv4' && !iface.internal) {
22
+ return iface.address;
23
+ }
24
+ }
25
+ }
26
+ return '127.0.0.1';
27
+ }
28
+
29
+ // Map package names to their binary names for checking
30
+ const packageBinaries = {
31
+ 'openssh': 'ssh',
32
+ 'openssl': 'openssl',
33
+ 'tmux': 'tmux',
34
+ 'git': 'git',
35
+ 'curl': 'curl',
36
+ 'wget': 'wget'
37
+ };
38
+
39
+ /**
40
+ * Check if package is installed
41
+ */
42
+ function isPackageInstalled(pkg) {
43
+ const binary = packageBinaries[pkg] || pkg;
44
+ try {
45
+ execSync(`which ${binary}`, { stdio: 'ignore' });
46
+ return true;
47
+ } catch {
48
+ return false;
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Install package via pkg
54
+ */
55
+ function installPackage(pkg) {
56
+ try {
57
+ console.log(chalk.gray(` Installing ${pkg}...`));
58
+ execSync(`pkg install -y ${pkg}`, { stdio: 'inherit' });
59
+ return true;
60
+ } catch {
61
+ return false;
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Check if sshd is running
67
+ */
68
+ function isSshdRunning() {
69
+ try {
70
+ execSync('pgrep -x sshd', { stdio: 'ignore' });
71
+ return true;
72
+ } catch {
73
+ return false;
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Start sshd
79
+ */
80
+ function startSshd() {
81
+ try {
82
+ execSync('sshd', { stdio: 'ignore' });
83
+ return true;
84
+ } catch {
85
+ return false;
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Setup SSH auto-start in shell rc
91
+ */
92
+ function setupSshAutoStart() {
93
+ const home = process.env.HOME;
94
+ const rcFiles = ['.zshrc', '.bashrc'];
95
+ const autoStartLine = '# Auto-start SSH\npgrep -x sshd >/dev/null || sshd';
96
+
97
+ for (const rcFile of rcFiles) {
98
+ const rcPath = path.join(home, rcFile);
99
+ if (fs.existsSync(rcPath)) {
100
+ const content = fs.readFileSync(rcPath, 'utf8');
101
+ if (!content.includes('pgrep -x sshd')) {
102
+ fs.appendFileSync(rcPath, `\n${autoStartLine}\n`);
103
+ return rcFile;
104
+ }
105
+ return null; // Already configured
106
+ }
107
+ }
108
+ return null;
109
+ }
110
+
111
+ /**
112
+ * Main setup command
113
+ */
114
+ async function setupTermux() {
115
+ console.log('');
116
+
117
+ // Check Termux
118
+ if (!isTermux()) {
119
+ console.log(chalk.red('This command is for Termux only.'));
120
+ console.log('');
121
+ process.exit(1);
122
+ }
123
+
124
+ console.log(chalk.bold('╔═══════════════════════════════════════════════════╗'));
125
+ console.log(chalk.bold('║ 📱 NexusCLI Termux Setup ║'));
126
+ console.log(chalk.bold('╚═══════════════════════════════════════════════════╝'));
127
+ console.log('');
128
+
129
+ // Essential packages
130
+ const packages = ['openssh', 'tmux', 'git', 'curl', 'wget', 'openssl'];
131
+
132
+ console.log(chalk.cyan('Installing packages:'));
133
+ for (const pkg of packages) {
134
+ if (isPackageInstalled(pkg)) {
135
+ console.log(chalk.green(` ✓ ${pkg}`));
136
+ } else {
137
+ const installed = installPackage(pkg);
138
+ if (installed) {
139
+ console.log(chalk.green(` ✓ ${pkg} installed`));
140
+ } else {
141
+ console.log(chalk.yellow(` ⚠ ${pkg} failed`));
142
+ }
143
+ }
144
+ }
145
+ console.log('');
146
+
147
+ // Setup SSH
148
+ console.log(chalk.cyan('Setting up SSH:'));
149
+
150
+ // Generate host keys if needed
151
+ const sshDir = path.join(process.env.PREFIX, 'etc', 'ssh');
152
+ if (!fs.existsSync(path.join(sshDir, 'ssh_host_rsa_key'))) {
153
+ console.log(chalk.gray(' Generating host keys...'));
154
+ try {
155
+ execSync('ssh-keygen -A', { stdio: 'ignore' });
156
+ console.log(chalk.green(' ✓ Host keys generated'));
157
+ } catch {
158
+ console.log(chalk.yellow(' ⚠ Could not generate host keys'));
159
+ }
160
+ }
161
+
162
+ // Start sshd
163
+ if (isSshdRunning()) {
164
+ console.log(chalk.green(' ✓ SSH server already running'));
165
+ } else {
166
+ const started = startSshd();
167
+ if (started) {
168
+ console.log(chalk.green(' ✓ SSH server started'));
169
+ } else {
170
+ console.log(chalk.yellow(' ⚠ Could not start SSH server'));
171
+ }
172
+ }
173
+
174
+ // Setup auto-start
175
+ const rcFile = setupSshAutoStart();
176
+ if (rcFile) {
177
+ console.log(chalk.green(` ✓ Auto-start added to ${rcFile}`));
178
+ } else {
179
+ console.log(chalk.gray(' Auto-start already configured'));
180
+ }
181
+ console.log('');
182
+
183
+ // Get connection info
184
+ const ip = getLocalIP();
185
+ const user = process.env.USER || 'u0_a362';
186
+ const sshPort = 8022;
187
+
188
+ let config;
189
+ try {
190
+ config = getConfig();
191
+ } catch {
192
+ config = { server: { port: 41800 } };
193
+ }
194
+ const webPort = config.server?.port || 41800;
195
+
196
+ // Output connection info
197
+ console.log(chalk.bold('╔═══════════════════════════════════════════════════╗'));
198
+ console.log(chalk.bold('║ 🔗 Connection Info ║'));
199
+ console.log(chalk.bold('╚═══════════════════════════════════════════════════╝'));
200
+ console.log('');
201
+ console.log(chalk.cyan(' SSH:'));
202
+ console.log(chalk.white(` ssh ${user}@${ip} -p ${sshPort}`));
203
+ console.log('');
204
+ console.log(chalk.cyan(' NexusCLI:'));
205
+ console.log(chalk.white(` https://${ip}:${webPort}`));
206
+ console.log('');
207
+ console.log(chalk.gray(' Note: Set password with: passwd'));
208
+ console.log('');
209
+
210
+ process.exit(0);
211
+ }
212
+
213
+ module.exports = setupTermux;
package/lib/cli/start.js CHANGED
@@ -77,7 +77,8 @@ function askYesNo(question) {
77
77
  rl.question(question, (answer) => {
78
78
  rl.close();
79
79
  const normalized = answer.trim().toLowerCase();
80
- resolve(normalized === 'y' || normalized === 'yes' || normalized === 's' || normalized === 'si');
80
+ // Empty = default (Y), or explicit yes
81
+ resolve(normalized === '' || normalized === 'y' || normalized === 'yes' || normalized === 's' || normalized === 'si');
81
82
  });
82
83
  });
83
84
  }
@@ -0,0 +1,22 @@
1
+ const express = require('express');
2
+ const router = express.Router();
3
+ const { getApiKey } = require('../db/adapter');
4
+
5
+ /**
6
+ * GET /api/v1/keys/check/:provider
7
+ * Check if API key exists for provider
8
+ * Returns: { exists: boolean }
9
+ *
10
+ * Public endpoint - needed for STT provider auto-detection
11
+ */
12
+ router.get('/check/:provider', (req, res) => {
13
+ try {
14
+ const key = getApiKey(req.params.provider);
15
+ res.json({ exists: !!key });
16
+ } catch (error) {
17
+ console.error(`[Keys] Error checking ${req.params.provider}:`, error);
18
+ res.status(500).json({ error: error.message });
19
+ }
20
+ });
21
+
22
+ module.exports = router;
@@ -0,0 +1,75 @@
1
+ const express = require('express');
2
+ const router = express.Router();
3
+ const multer = require('multer');
4
+ const { getApiKey } = require('../db/adapter');
5
+
6
+ const upload = multer({
7
+ storage: multer.memoryStorage(),
8
+ limits: { fileSize: 25 * 1024 * 1024 } // 25MB max (Whisper limit)
9
+ });
10
+
11
+ /**
12
+ * POST /api/v1/speech/transcribe
13
+ * Transcribe audio using OpenAI Whisper API
14
+ * Body: multipart/form-data with 'audio' file and 'language' (optional)
15
+ * Returns: { text: string }
16
+ *
17
+ * Supported languages (aligned with UI): it, en, es, ja, ru, zh
18
+ */
19
+ router.post('/transcribe', upload.single('audio'), async (req, res) => {
20
+ try {
21
+ const apiKey = getApiKey('openai');
22
+ if (!apiKey) {
23
+ return res.status(400).json({ error: 'OpenAI API key not configured' });
24
+ }
25
+
26
+ if (!req.file) {
27
+ return res.status(400).json({ error: 'No audio file provided' });
28
+ }
29
+
30
+ // Build FormData using native Node.js 22+ FormData and File
31
+ const formData = new FormData();
32
+ const audioFile = new File([req.file.buffer], 'audio.webm', {
33
+ type: req.file.mimetype || 'audio/webm'
34
+ });
35
+ formData.append('file', audioFile);
36
+ formData.append('model', 'whisper-1');
37
+
38
+ // Use language from request - extract base language (it-IT -> it)
39
+ if (req.body.language) {
40
+ const lang = req.body.language.split('-')[0]; // it-IT -> it
41
+ formData.append('language', lang);
42
+ }
43
+
44
+ console.log('[Speech] Transcribing audio:', {
45
+ size: req.file.size,
46
+ mimetype: req.file.mimetype,
47
+ language: req.body.language || 'auto-detect'
48
+ });
49
+
50
+ // Use native fetch (Node.js 22+)
51
+ const response = await fetch('https://api.openai.com/v1/audio/transcriptions', {
52
+ method: 'POST',
53
+ headers: {
54
+ 'Authorization': `Bearer ${apiKey}`
55
+ },
56
+ body: formData
57
+ });
58
+
59
+ if (!response.ok) {
60
+ const error = await response.json();
61
+ console.error('[Speech] OpenAI error:', error);
62
+ return res.status(response.status).json(error);
63
+ }
64
+
65
+ const data = await response.json();
66
+ console.log('[Speech] Transcription success:', data.text?.substring(0, 50) + '...');
67
+ res.json(data);
68
+
69
+ } catch (error) {
70
+ console.error('[Speech] Transcription error:', error);
71
+ res.status(500).json({ error: error.message });
72
+ }
73
+ });
74
+
75
+ module.exports = router;
@@ -26,6 +26,8 @@ const workspacesRouter = require('./routes/workspaces');
26
26
  const sessionsRouter = require('./routes/sessions');
27
27
  const wakeLockRouter = require('./routes/wake-lock');
28
28
  const uploadRouter = require('./routes/upload');
29
+ const keysRouter = require('./routes/keys');
30
+ const speechRouter = require('./routes/speech');
29
31
 
30
32
  const app = express();
31
33
  const PORT = process.env.PORT || 41800;
@@ -74,6 +76,10 @@ app.use('/api/v1/codex', authMiddleware, codexRouter);
74
76
  app.use('/api/v1/gemini', authMiddleware, geminiRouter);
75
77
  app.use('/api/v1/upload', authMiddleware, uploadRouter); // File upload
76
78
 
79
+ // STT routes
80
+ app.use('/api/v1/keys', keysRouter); // Public - needed for STT provider detection
81
+ app.use('/api/v1/speech', authMiddleware, speechRouter); // Protected - Whisper proxy
82
+
77
83
  // Root endpoint
78
84
  app.get('/', (req, res) => {
79
85
  res.json({
@@ -149,6 +149,7 @@ async function main() {
149
149
  path.join(HOME, '.nexuscli', 'data'),
150
150
  path.join(HOME, '.nexuscli', 'logs'),
151
151
  path.join(HOME, '.nexuscli', 'engines'),
152
+ path.join(HOME, '.nexuscli', 'certs'),
152
153
  path.join(HOME, '.termux', 'boot')
153
154
  ];
154
155
 
@@ -163,6 +164,29 @@ async function main() {
163
164
  }
164
165
  console.log('');
165
166
 
167
+ // Generate HTTPS certificates (required for microphone access from remote devices)
168
+ const certPath = path.join(HOME, '.nexuscli', 'certs', 'cert.pem');
169
+ const keyPath = path.join(HOME, '.nexuscli', 'certs', 'key.pem');
170
+
171
+ log(colors.cyan('Setting up HTTPS (required for microphone):'));
172
+ if (fs.existsSync(certPath) && fs.existsSync(keyPath)) {
173
+ success('HTTPS certificates already exist');
174
+ } else {
175
+ try {
176
+ execSync(
177
+ `openssl req -x509 -newkey rsa:2048 -keyout "${keyPath}" -out "${certPath}" -days 365 -nodes -subj "/CN=nexuscli.local"`,
178
+ { stdio: 'ignore' }
179
+ );
180
+ success('HTTPS certificates generated (self-signed)');
181
+ log(colors.gray(' Note: Browser will show security warning - click "Advanced" > "Proceed"'));
182
+ } catch (err) {
183
+ warn('Failed to generate HTTPS certificates');
184
+ log(colors.gray(' Microphone may not work from remote devices'));
185
+ log(colors.gray(' Run: openssl req -x509 -newkey rsa:2048 -keyout ~/.nexuscli/certs/key.pem -out ~/.nexuscli/certs/cert.pem -days 365 -nodes'));
186
+ }
187
+ }
188
+ console.log('');
189
+
166
190
  // Check Termux:API app
167
191
  log(colors.cyan('Checking Termux apps:'));
168
192
  try {
package/package.json CHANGED
@@ -1,8 +1,7 @@
1
1
  {
2
2
  "name": "@mmmbuto/nexuscli",
3
- "version": "0.5.1",
4
- "private": false,
5
- "description": "Mobile-first AI Control Plane - Web UI for Claude/Codex/Gemini CLI",
3
+ "version": "0.5.3",
4
+ "description": "NexusCLI - TRI CLI Control Plane (Claude/Codex/Gemini)",
6
5
  "main": "lib/server/server.js",
7
6
  "bin": {
8
7
  "nexuscli": "./bin/nexuscli.js"
@@ -24,19 +23,16 @@
24
23
  "claude",
25
24
  "codex",
26
25
  "gemini",
26
+ "tri-cli",
27
27
  "control-plane",
28
28
  "mobile-first",
29
29
  "android"
30
30
  ],
31
- "author": "DioNanos",
31
+ "author": "Davide A. Guglielmi <dev@mmmbuto.com>",
32
32
  "license": "MIT",
33
- "homepage": "https://github.com/DioNanos/nexuscli",
33
+ "homepage": "https://npmjs.com/package/@mmmbuto/nexuscli",
34
34
  "bugs": {
35
- "url": "https://github.com/DioNanos/nexuscli/issues"
36
- },
37
- "repository": {
38
- "type": "git",
39
- "url": "git+https://github.com/DioNanos/nexuscli.git"
35
+ "email": "dev@mmmbuto.com"
40
36
  },
41
37
  "publishConfig": {
42
38
  "access": "public",
@@ -68,6 +64,7 @@
68
64
  "cors": "^2.8.5",
69
65
  "express": "^4.18.2",
70
66
  "express-rate-limit": "^8.2.1",
67
+ "form-data": "^4.0.5",
71
68
  "inquirer": "^8.2.6",
72
69
  "jsonwebtoken": "^9.0.2",
73
70
  "multer": "^2.0.2",