@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.
- package/frontend/dist/assets/{index-CikJbUR5.js → index-DaG2hvsF.js} +1703 -1703
- package/frontend/dist/index.html +1 -1
- package/lib/cli/api.js +4 -4
- package/lib/cli/start.js +2 -1
- package/lib/server/routes/keys.js +22 -0
- package/lib/server/routes/speech.js +75 -0
- package/lib/server/server.js +6 -0
- package/lib/setup/postinstall.js +24 -0
- package/package.json +8 -10
package/frontend/dist/index.html
CHANGED
|
@@ -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-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|
package/lib/server/server.js
CHANGED
|
@@ -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({
|
package/lib/setup/postinstall.js
CHANGED
|
@@ -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.
|
|
4
|
-
|
|
5
|
-
"description": "
|
|
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": "
|
|
32
|
+
"author": "Davide A. Guglielmi <dev@mmmbuto.com>",
|
|
32
33
|
"license": "MIT",
|
|
33
|
-
"homepage": "https://
|
|
34
|
+
"homepage": "https://npmjs.com/package/@mmmbuto/nexuscli",
|
|
34
35
|
"bugs": {
|
|
35
|
-
"
|
|
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",
|