@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.
- package/README.md +124 -56
- package/bin/nexuscrew.js +79 -152
- package/frontend/dist/assets/{index-8pw4eMB-.js → index-BsldfYnr.js} +1704 -1716
- package/frontend/dist/assets/index-DcQGg3dp.css +1 -0
- package/frontend/dist/index.html +2 -2
- package/lib/server/routes/hosts.js +3 -2
- package/lib/server/routes/launchers.js +12 -0
- package/lib/server/routes/models.js +16 -63
- package/lib/server/routes/runtime.js +10 -0
- package/lib/server/routes/send.js +97 -218
- package/lib/server/routes/sessions.js +7 -39
- package/lib/server/routes/status.js +16 -18
- package/lib/server/routes/tmux.js +88 -0
- package/lib/server/server.js +45 -65
- package/lib/services/engine-discovery.js +192 -445
- package/lib/services/session-store.js +86 -60
- package/lib/services/tmux-manager.js +103 -154
- package/package.json +3 -7
- package/frontend/dist/assets/index-BF0tdvNT.css +0 -1
- package/lib/config/manager.js +0 -362
- package/lib/config/models.js +0 -408
- package/lib/server/db/adapter.js +0 -274
- package/lib/server/db/drivers/sql-js.js +0 -75
- package/lib/server/db/migrate.js +0 -174
- package/lib/server/db/migrations/001_base_schema.sql +0 -70
- package/lib/server/middleware/auth.js +0 -134
- package/lib/server/middleware/rate-limit.js +0 -63
- package/lib/server/models/User.js +0 -128
- package/lib/server/routes/auth.js +0 -168
- package/lib/server/routes/keys.js +0 -15
- package/lib/server/routes/runtimes.js +0 -34
- package/lib/server/routes/speech.js +0 -75
- package/lib/server/routes/upload.js +0 -134
- package/lib/server/routes/wake-lock.js +0 -95
- package/lib/server/routes/workspaces.js +0 -101
- package/lib/server/services/context-bridge.js +0 -425
- package/lib/server/services/runtime-manager.js +0 -462
- package/lib/server/services/summary-generator.js +0 -309
- package/lib/server/services/workspace-manager.js +0 -79
- package/lib/utils/paths.js +0 -107
- 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;
|