@mmmbuto/nexuscli 0.5.1 → 0.5.2

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
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,8 @@
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.2",
4
+
5
+ "description": "NexusCLI - TRI CLI Control Plane (Claude/Codex/Gemini)",
6
6
  "main": "lib/server/server.js",
7
7
  "bin": {
8
8
  "nexuscli": "./bin/nexuscli.js"
@@ -24,19 +24,16 @@
24
24
  "claude",
25
25
  "codex",
26
26
  "gemini",
27
+ "tri-cli",
27
28
  "control-plane",
28
29
  "mobile-first",
29
30
  "android"
30
31
  ],
31
- "author": "DioNanos",
32
+ "author": "Davide A. Guglielmi <dev@mmmbuto.com>",
32
33
  "license": "MIT",
33
- "homepage": "https://github.com/DioNanos/nexuscli",
34
+ "homepage": "https://npmjs.com/package/@mmmbuto/nexuscli",
34
35
  "bugs": {
35
- "url": "https://github.com/DioNanos/nexuscli/issues"
36
- },
37
- "repository": {
38
- "type": "git",
39
- "url": "git+https://github.com/DioNanos/nexuscli.git"
36
+ "email": "dev@mmmbuto.com"
40
37
  },
41
38
  "publishConfig": {
42
39
  "access": "public",
@@ -68,6 +65,7 @@
68
65
  "cors": "^2.8.5",
69
66
  "express": "^4.18.2",
70
67
  "express-rate-limit": "^8.2.1",
68
+ "form-data": "^4.0.5",
71
69
  "inquirer": "^8.2.6",
72
70
  "jsonwebtoken": "^9.0.2",
73
71
  "multer": "^2.0.2",