@mmmbuto/nexuscrew 0.2.0 → 0.2.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.
Files changed (41) hide show
  1. package/README.md +124 -56
  2. package/bin/nexuscrew.js +79 -152
  3. package/frontend/dist/assets/{index-8pw4eMB-.js → index-BsldfYnr.js} +1704 -1716
  4. package/frontend/dist/assets/index-DcQGg3dp.css +1 -0
  5. package/frontend/dist/index.html +2 -2
  6. package/lib/server/routes/hosts.js +3 -2
  7. package/lib/server/routes/launchers.js +12 -0
  8. package/lib/server/routes/models.js +16 -63
  9. package/lib/server/routes/runtime.js +10 -0
  10. package/lib/server/routes/send.js +97 -218
  11. package/lib/server/routes/sessions.js +7 -39
  12. package/lib/server/routes/status.js +16 -18
  13. package/lib/server/routes/tmux.js +88 -0
  14. package/lib/server/server.js +45 -65
  15. package/lib/services/engine-discovery.js +192 -445
  16. package/lib/services/session-store.js +86 -60
  17. package/lib/services/tmux-manager.js +103 -154
  18. package/package.json +3 -7
  19. package/frontend/dist/assets/index-BF0tdvNT.css +0 -1
  20. package/lib/config/manager.js +0 -362
  21. package/lib/config/models.js +0 -408
  22. package/lib/server/db/adapter.js +0 -274
  23. package/lib/server/db/drivers/sql-js.js +0 -75
  24. package/lib/server/db/migrate.js +0 -174
  25. package/lib/server/db/migrations/001_base_schema.sql +0 -70
  26. package/lib/server/middleware/auth.js +0 -134
  27. package/lib/server/middleware/rate-limit.js +0 -63
  28. package/lib/server/models/User.js +0 -128
  29. package/lib/server/routes/auth.js +0 -168
  30. package/lib/server/routes/keys.js +0 -15
  31. package/lib/server/routes/runtimes.js +0 -34
  32. package/lib/server/routes/speech.js +0 -75
  33. package/lib/server/routes/upload.js +0 -134
  34. package/lib/server/routes/wake-lock.js +0 -95
  35. package/lib/server/routes/workspaces.js +0 -101
  36. package/lib/server/services/context-bridge.js +0 -425
  37. package/lib/server/services/runtime-manager.js +0 -462
  38. package/lib/server/services/summary-generator.js +0 -309
  39. package/lib/server/services/workspace-manager.js +0 -79
  40. package/lib/utils/paths.js +0 -107
  41. package/lib/utils/termux.js +0 -145
@@ -1,168 +0,0 @@
1
- const express = require('express');
2
- const rateLimit = require('express-rate-limit');
3
- const bcrypt = require('bcryptjs');
4
- const User = require('../models/User');
5
- const { generateToken, authMiddleware } = require('../middleware/auth');
6
- const { getConfig } = require('../../config/manager');
7
-
8
- const router = express.Router();
9
-
10
- /**
11
- * Check config user (admin from init)
12
- * Returns user object compatible with DB user structure
13
- */
14
- function findConfigUser(username) {
15
- const config = getConfig();
16
- if (config.auth && config.auth.user === username && config.auth.pass_hash) {
17
- return {
18
- id: 'config-admin',
19
- username: config.auth.user,
20
- password_hash: config.auth.pass_hash,
21
- role: 'admin',
22
- is_locked: false,
23
- failed_attempts: 0,
24
- created_at: Date.now()
25
- };
26
- }
27
- return null;
28
- }
29
-
30
- /**
31
- * Verify password for config user
32
- */
33
- function verifyConfigPassword(passHash, password) {
34
- return bcrypt.compareSync(password, passHash);
35
- }
36
-
37
- // Rate limiter: max 5 login attempts per 15 minutes per IP
38
- const loginLimiter = rateLimit({
39
- windowMs: 15 * 60 * 1000, // 15 minutes
40
- max: 10,
41
- message: {
42
- error: 'Too many login attempts, please try again later',
43
- retry_after: 15 * 60
44
- },
45
- standardHeaders: true,
46
- legacyHeaders: false
47
- });
48
-
49
- // POST /api/v1/auth/login
50
- router.post('/login', loginLimiter, async (req, res) => {
51
- try {
52
- const { username, password } = req.body;
53
- const ipAddress = req.ip || req.connection.remoteAddress;
54
-
55
- if (!username || !password) {
56
- return res.status(400).json({ error: 'Username and password required' });
57
- }
58
-
59
- // Check IP rate limiting (additional layer)
60
- const recentAttempts = User.getRecentLoginAttempts(ipAddress);
61
- if (recentAttempts > 20) {
62
- return res.status(429).json({
63
- error: 'Too many failed attempts from this IP',
64
- retry_after: 15 * 60
65
- });
66
- }
67
-
68
- // First check config user (admin from init)
69
- let user = findConfigUser(username);
70
- let isConfigUser = !!user;
71
-
72
- // If not config user, check database
73
- if (!user) {
74
- user = User.findByUsername(username);
75
- }
76
-
77
- if (!user) {
78
- // Log failed attempt even for non-existent user
79
- User.logLoginAttempt(ipAddress, username, false);
80
- return res.status(401).json({ error: 'Invalid credentials' });
81
- }
82
-
83
- // Check if account is locked (only for DB users)
84
- if (!isConfigUser && User.isAccountLocked(user)) {
85
- User.logLoginAttempt(ipAddress, username, false);
86
- const remainingMs = user.locked_until - Date.now();
87
- return res.status(403).json({
88
- error: 'Account locked due to failed login attempts',
89
- locked_until: user.locked_until,
90
- retry_after: Math.ceil(remainingMs / 1000)
91
- });
92
- }
93
-
94
- // Verify password
95
- let isValid;
96
- if (isConfigUser) {
97
- isValid = verifyConfigPassword(user.password_hash, password);
98
- } else {
99
- isValid = User.verifyPassword(user, password);
100
- }
101
-
102
- if (!isValid) {
103
- User.logLoginAttempt(ipAddress, username, false);
104
- if (!isConfigUser) {
105
- User.incrementFailedAttempts(user.id);
106
- }
107
- return res.status(401).json({ error: 'Invalid credentials' });
108
- }
109
-
110
- // Success
111
- User.logLoginAttempt(ipAddress, username, true);
112
- if (!isConfigUser) {
113
- User.resetFailedAttempts(user.id);
114
- User.updateLastLogin(user.id);
115
- }
116
-
117
- const token = generateToken(user);
118
-
119
- res.json({
120
- token,
121
- user: {
122
- id: user.id,
123
- username: user.username,
124
- role: user.role
125
- }
126
- });
127
- } catch (error) {
128
- console.error('Login error:', error);
129
- res.status(500).json({ error: 'Internal server error' });
130
- }
131
- });
132
-
133
- // GET /api/v1/auth/me
134
- router.get('/me', authMiddleware, (req, res) => {
135
- // Config user is already validated by authMiddleware
136
- // Just return the user info from req.user
137
- if (req.user.id === 'config-admin') {
138
- const config = getConfig();
139
- return res.json({
140
- id: req.user.id,
141
- username: req.user.username,
142
- role: req.user.role,
143
- created_at: Date.now(),
144
- last_login: null
145
- });
146
- }
147
-
148
- const user = User.findById(req.user.id);
149
- if (!user) {
150
- return res.status(404).json({ error: 'User not found' });
151
- }
152
-
153
- res.json({
154
- id: user.id,
155
- username: user.username,
156
- role: user.role,
157
- created_at: user.created_at,
158
- last_login: user.last_login
159
- });
160
- });
161
-
162
- // POST /api/v1/auth/logout
163
- router.post('/logout', authMiddleware, (req, res) => {
164
- // With JWT, logout is handled client-side by removing token
165
- res.json({ message: 'Logged out successfully' });
166
- });
167
-
168
- module.exports = router;
@@ -1,15 +0,0 @@
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 an API key is configured for a provider
8
- * Returns: { configured: boolean }
9
- */
10
- router.get('/check/:provider', (req, res) => {
11
- const key = getApiKey(req.params.provider);
12
- res.json({ configured: !!key });
13
- });
14
-
15
- module.exports = router;
@@ -1,34 +0,0 @@
1
- const express = require('express');
2
- const RuntimeManager = require('../services/runtime-manager');
3
-
4
- const router = express.Router();
5
- const runtimeManager = new RuntimeManager();
6
-
7
- router.get('/', async (_req, res) => {
8
- try {
9
- const inventory = await runtimeManager.getRuntimeInventory();
10
- res.json({
11
- platform: runtimeManager.platformId,
12
- runtimes: inventory,
13
- });
14
- } catch (error) {
15
- console.error('[Runtimes] Inventory error:', error);
16
- res.status(500).json({ error: 'Failed to fetch runtime inventory' });
17
- }
18
- });
19
-
20
- router.post('/check', async (_req, res) => {
21
- try {
22
- const inventory = await runtimeManager.getRuntimeInventory();
23
- res.json({
24
- platform: runtimeManager.platformId,
25
- runtimes: inventory,
26
- checkedAt: new Date().toISOString(),
27
- });
28
- } catch (error) {
29
- console.error('[Runtimes] Check error:', error);
30
- res.status(500).json({ error: 'Failed to check runtimes' });
31
- }
32
- });
33
-
34
- module.exports = router;
@@ -1,75 +0,0 @@
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;
@@ -1,134 +0,0 @@
1
- const express = require('express');
2
- const multer = require('multer');
3
- const path = require('path');
4
- const fs = require('fs');
5
-
6
- const router = express.Router();
7
-
8
- // NexusCrew: attachments in ~/.nexuscrew/attachments
9
- const ATTACHMENTS_DIR = path.join(process.env.HOME, '.nexuscrew', 'attachments');
10
-
11
- // Ensure directory exists
12
- if (!fs.existsSync(ATTACHMENTS_DIR)) {
13
- fs.mkdirSync(ATTACHMENTS_DIR, { recursive: true });
14
- console.log(`[Upload] Created attachments directory: ${ATTACHMENTS_DIR}`);
15
- }
16
-
17
- // Multer storage config
18
- const storage = multer.diskStorage({
19
- destination: (req, file, cb) => {
20
- cb(null, ATTACHMENTS_DIR);
21
- },
22
- filename: (req, file, cb) => {
23
- // Unique filename: timestamp_originalname
24
- const timestamp = Date.now();
25
- const safeName = file.originalname.replace(/[^a-zA-Z0-9._-]/g, '_');
26
- cb(null, `${timestamp}_${safeName}`);
27
- }
28
- });
29
-
30
- // File filter - allow images and documents
31
- const fileFilter = (req, file, cb) => {
32
- const allowedMimes = [
33
- // Images
34
- 'image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/svg+xml',
35
- // Documents
36
- 'text/plain', 'text/markdown', 'text/csv', 'text/html', 'text/css',
37
- 'application/json', 'application/xml', 'application/pdf',
38
- 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
39
- 'application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
40
- // Code files (often sent as octet-stream)
41
- 'application/octet-stream', 'text/x-python', 'text/javascript', 'application/javascript'
42
- ];
43
-
44
- // Also allow by extension for code files
45
- const allowedExts = ['.js', '.ts', '.py', '.java', '.c', '.cpp', '.h', '.rb', '.php', '.go', '.rs', '.sh', '.bash', '.zsh', '.md', '.txt', '.json', '.yaml', '.yml', '.toml', '.xml', '.html', '.css', '.sql', '.log'];
46
- const ext = path.extname(file.originalname).toLowerCase();
47
-
48
- if (allowedMimes.includes(file.mimetype) || allowedExts.includes(ext)) {
49
- cb(null, true);
50
- } else {
51
- cb(new Error(`File type not allowed: ${file.mimetype} (${ext})`), false);
52
- }
53
- };
54
-
55
- // Multer config - 50MB limit
56
- const upload = multer({
57
- storage,
58
- fileFilter,
59
- limits: { fileSize: 50 * 1024 * 1024 }
60
- });
61
-
62
- // POST /api/v1/upload - Single file upload
63
- router.post('/', upload.single('file'), (req, res) => {
64
- try {
65
- if (!req.file) {
66
- return res.status(400).json({ error: 'No file uploaded' });
67
- }
68
-
69
- const filePath = req.file.path;
70
- const fileName = req.file.filename;
71
- const originalName = req.file.originalname;
72
- const mimeType = req.file.mimetype;
73
- const size = req.file.size;
74
-
75
- console.log(`[Upload] File saved: ${filePath} (${size} bytes, ${mimeType})`);
76
-
77
- res.json({
78
- success: true,
79
- file: {
80
- path: filePath,
81
- name: fileName,
82
- originalName,
83
- mimeType,
84
- size
85
- }
86
- });
87
- } catch (error) {
88
- console.error('[Upload] Error:', error);
89
- res.status(500).json({ error: error.message });
90
- }
91
- });
92
-
93
- // POST /api/v1/upload/multiple - Multiple files upload
94
- router.post('/multiple', upload.array('files', 10), (req, res) => {
95
- try {
96
- if (!req.files || req.files.length === 0) {
97
- return res.status(400).json({ error: 'No files uploaded' });
98
- }
99
-
100
- const files = req.files.map(f => ({
101
- path: f.path,
102
- name: f.filename,
103
- originalName: f.originalname,
104
- mimeType: f.mimetype,
105
- size: f.size
106
- }));
107
-
108
- console.log(`[Upload] ${files.length} files saved`);
109
-
110
- res.json({
111
- success: true,
112
- files
113
- });
114
- } catch (error) {
115
- console.error('[Upload] Error:', error);
116
- res.status(500).json({ error: error.message });
117
- }
118
- });
119
-
120
- // Error handler for multer
121
- router.use((error, req, res, next) => {
122
- if (error instanceof multer.MulterError) {
123
- if (error.code === 'LIMIT_FILE_SIZE') {
124
- return res.status(400).json({ error: 'File too large (max 50MB)' });
125
- }
126
- return res.status(400).json({ error: error.message });
127
- }
128
- if (error) {
129
- return res.status(400).json({ error: error.message });
130
- }
131
- next();
132
- });
133
-
134
- module.exports = router;
@@ -1,95 +0,0 @@
1
- const express = require('express');
2
- const { execSync } = require('child_process');
3
- const router = express.Router();
4
-
5
- // State to track wake lock status
6
- let wakeLockAcquired = false;
7
-
8
- /**
9
- * POST /api/v1/wake-lock
10
- * Acquire wake lock (prevent Android from killing process)
11
- * On non-Termux systems, this is a no-op but returns success to avoid errors
12
- */
13
- router.post('/wake-lock', (req, res) => {
14
- try {
15
- console.log('[WakeLock] Acquiring wake lock...');
16
-
17
- const isTermux = process.env.PREFIX?.includes('com.termux');
18
-
19
- if (isTermux) {
20
- // Only execute on Termux/Android
21
- try {
22
- execSync('termux-wake-lock', { stdio: 'ignore' });
23
- } catch (e) {
24
- console.warn('[WakeLock] termux-wake-lock command failed (may not be available)');
25
- }
26
- } else {
27
- console.log('[WakeLock] Not on Termux - wake-lock is a no-op');
28
- }
29
-
30
- wakeLockAcquired = true;
31
-
32
- console.log('[WakeLock] ✅ Wake lock acquired (or skipped on non-Termux)');
33
-
34
- res.json({
35
- status: 'ok',
36
- message: 'Wake lock acquired',
37
- acquired: wakeLockAcquired,
38
- platform: isTermux ? 'termux' : 'linux'
39
- });
40
- } catch (err) {
41
- console.error('[WakeLock] ❌ Unexpected error:', err.message);
42
-
43
- // Still return success to avoid breaking the app
44
- res.status(200).json({
45
- status: 'ok',
46
- message: 'Wake lock handler executed',
47
- acquired: true,
48
- error: err.message // Log the error but don't fail the request
49
- });
50
- }
51
- });
52
-
53
- /**
54
- * DELETE /api/v1/wake-lock
55
- * Release wake lock
56
- */
57
- router.delete('/wake-lock', (req, res) => {
58
- try {
59
- console.log('[WakeLock] Releasing wake lock...');
60
-
61
- // Execute termux-wake-unlock
62
- execSync('termux-wake-unlock', { stdio: 'ignore' });
63
-
64
- wakeLockAcquired = false;
65
-
66
- console.log('[WakeLock] ✅ Wake lock released');
67
-
68
- res.json({
69
- status: 'ok',
70
- message: 'Wake lock released',
71
- acquired: wakeLockAcquired
72
- });
73
- } catch (err) {
74
- console.error('[WakeLock] ❌ Failed to release wake lock:', err.message);
75
-
76
- res.status(500).json({
77
- status: 'error',
78
- message: 'Failed to release wake lock',
79
- error: err.message
80
- });
81
- }
82
- });
83
-
84
- /**
85
- * GET /api/v1/wake-lock
86
- * Get wake lock status
87
- */
88
- router.get('/wake-lock', (req, res) => {
89
- res.json({
90
- status: 'ok',
91
- acquired: wakeLockAcquired
92
- });
93
- });
94
-
95
- module.exports = router;
@@ -1,101 +0,0 @@
1
- /**
2
- * Workspace Routes - Express router for workspace management
3
- *
4
- * Endpoints:
5
- * - GET /api/v1/workspaces - List discovered workspaces
6
- * - POST /api/v1/workspaces/mount - Validate and set active workspace
7
- */
8
-
9
- const express = require('express');
10
- const router = express.Router();
11
-
12
- /**
13
- * GET /api/v1/workspaces
14
- * List all discovered workspaces from conversations table
15
- */
16
- router.get('/', (req, res) => {
17
- try {
18
- const WorkspaceManager = require('../services/workspace-manager');
19
- const wm = new WorkspaceManager();
20
- const workspaces = wm.discoverWorkspaces();
21
-
22
- res.json({
23
- workspaces,
24
- count: workspaces.length
25
- });
26
- } catch (error) {
27
- console.error('[Workspaces] Error listing workspaces:', error);
28
- res.status(500).json({ error: 'Failed to list workspaces' });
29
- }
30
- });
31
-
32
- /**
33
- * POST /api/v1/workspaces/mount
34
- * Validate workspace path and optionally return workspace details
35
- * Body: { path: string }
36
- */
37
- router.post('/mount', (req, res) => {
38
- try {
39
- const { path: wsPath } = req.body;
40
-
41
- if (!wsPath) {
42
- return res.status(400).json({ error: 'Workspace path is required' });
43
- }
44
-
45
- const WorkspaceManager = require('../services/workspace-manager');
46
- const wm = new WorkspaceManager();
47
-
48
- // Validate workspace exists
49
- const valid = wm.validateWorkspace(wsPath);
50
-
51
- if (!valid) {
52
- return res.status(400).json({ error: 'Invalid workspace path' });
53
- }
54
-
55
- // Get workspace details if it exists in conversations
56
- const stats = wm.getWorkspaceStats(wsPath);
57
- const conversations = wm.getWorkspaceConversations(wsPath);
58
-
59
- res.json({
60
- workspace: wsPath,
61
- mounted: true,
62
- stats,
63
- conversations: conversations.slice(0, 10) // Return first 10
64
- });
65
- } catch (error) {
66
- console.error('[Workspaces] Error mounting workspace:', error);
67
- res.status(500).json({ error: 'Failed to mount workspace' });
68
- }
69
- });
70
-
71
- /**
72
- * GET /api/v1/workspaces/:path/stats
73
- * Get statistics for a specific workspace
74
- */
75
- router.get('/:path/stats', (req, res) => {
76
- try {
77
- const { path: wsPath } = req.params;
78
-
79
- const WorkspaceManager = require('../services/workspace-manager');
80
- const wm = new WorkspaceManager();
81
-
82
- // Validate workspace exists
83
- const valid = wm.validateWorkspace(wsPath);
84
-
85
- if (!valid) {
86
- return res.status(404).json({ error: 'Workspace not found' });
87
- }
88
-
89
- const stats = wm.getWorkspaceStats(wsPath);
90
-
91
- res.json({
92
- workspace: wsPath,
93
- stats
94
- });
95
- } catch (error) {
96
- console.error('[Workspaces] Error getting workspace stats:', error);
97
- res.status(500).json({ error: 'Failed to get workspace stats' });
98
- }
99
- });
100
-
101
- module.exports = router;