@mmmbuto/nexuscrew 0.1.0-beta.1 → 0.2.1
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 +67 -120
- package/bin/nexuscrew.js +468 -264
- package/frontend/dist/apple-touch-icon.png +0 -0
- package/frontend/dist/assets/{index-C7bAndew.js → index-BG1N7bSL.js} +1710 -1702
- package/frontend/dist/assets/index-CiAtinNP.css +1 -0
- package/frontend/dist/favicon.svg +20 -0
- package/frontend/dist/icon-192.png +0 -0
- package/frontend/dist/icon-512.png +0 -0
- package/frontend/dist/index.html +11 -3
- package/frontend/dist/site.webmanifest +29 -0
- package/lib/config/hosts.js +67 -0
- package/lib/config/manager.js +379 -0
- package/lib/config/models.js +408 -0
- package/lib/server/db/adapter.js +274 -0
- package/lib/server/db/drivers/sql-js.js +75 -0
- package/lib/server/db/migrate.js +174 -0
- package/lib/server/db/migrations/001_base_schema.sql +70 -0
- package/lib/server/middleware/auth.js +134 -0
- package/lib/server/middleware/rate-limit.js +63 -0
- package/lib/server/models/User.js +128 -0
- package/lib/server/routes/auth.js +168 -0
- package/lib/server/routes/hosts.js +11 -20
- package/lib/server/routes/keys.js +28 -0
- package/lib/server/routes/models.js +59 -4
- package/lib/server/routes/runtimes.js +34 -0
- package/lib/server/routes/send.js +76 -12
- package/lib/server/routes/sessions.js +39 -10
- package/lib/server/routes/speech.js +46 -0
- package/lib/server/routes/status.js +8 -6
- package/lib/server/routes/upload.js +135 -0
- package/lib/server/routes/wake-lock.js +95 -0
- package/lib/server/routes/workspaces.js +101 -0
- package/lib/server/server.js +66 -33
- package/lib/server/services/attachment-manager.js +57 -0
- package/lib/server/services/context-bridge.js +425 -0
- package/lib/server/services/runtime-manager.js +462 -0
- package/lib/server/services/speech-manager.js +76 -0
- package/lib/server/services/summary-generator.js +309 -0
- package/lib/server/services/workspace-manager.js +79 -0
- package/lib/services/engine-discovery.js +198 -13
- package/lib/services/log-watcher.js +40 -22
- package/lib/services/remote-pane-watcher.js +155 -0
- package/lib/services/session-store.js +60 -64
- package/lib/services/tmux-manager.js +127 -116
- package/lib/setup/postinstall.js +38 -0
- package/lib/utils/paths.js +124 -0
- package/lib/utils/termux.js +182 -0
- package/package.json +8 -4
- package/frontend/dist/assets/index-OENqI1_9.css +0 -1
|
@@ -10,13 +10,14 @@ const router = Router();
|
|
|
10
10
|
// Overall status
|
|
11
11
|
router.get('/', (req, res) => {
|
|
12
12
|
const { tmux, store } = req;
|
|
13
|
-
const
|
|
13
|
+
const { host = 'local' } = req.query;
|
|
14
|
+
const windows = tmux.listWindows(host);
|
|
14
15
|
const hosts = tmux.getAllHosts();
|
|
15
16
|
|
|
16
17
|
res.json({
|
|
17
18
|
tmux: {
|
|
18
19
|
session: tmux.sessionName,
|
|
19
|
-
alive: tmux.hasSession(),
|
|
20
|
+
alive: tmux.hasSession(host),
|
|
20
21
|
windowCount: windows.length,
|
|
21
22
|
windows
|
|
22
23
|
},
|
|
@@ -30,7 +31,8 @@ router.get('/', (req, res) => {
|
|
|
30
31
|
// Detailed tmux windows
|
|
31
32
|
router.get('/tmux', (req, res) => {
|
|
32
33
|
const { tmux, store } = req;
|
|
33
|
-
const
|
|
34
|
+
const { host = 'local' } = req.query;
|
|
35
|
+
const windows = tmux.listWindows(host);
|
|
34
36
|
|
|
35
37
|
// Enrich with DB data
|
|
36
38
|
const enriched = windows.map(w => {
|
|
@@ -40,7 +42,7 @@ router.get('/tmux', (req, res) => {
|
|
|
40
42
|
conversationId: conv?.id || null,
|
|
41
43
|
engine: conv?.engine || null,
|
|
42
44
|
model: conv?.model || null,
|
|
43
|
-
host: conv?.host || 'local',
|
|
45
|
+
host: conv?.host || host || 'local',
|
|
44
46
|
title: conv?.title || w.name
|
|
45
47
|
};
|
|
46
48
|
});
|
|
@@ -51,8 +53,8 @@ router.get('/tmux', (req, res) => {
|
|
|
51
53
|
// Capture pane content
|
|
52
54
|
router.get('/pane/:window', (req, res) => {
|
|
53
55
|
const { tmux } = req;
|
|
54
|
-
const { host } = req.query;
|
|
55
|
-
const content = tmux.capturePane(req.params.window, host
|
|
56
|
+
const { host = 'local' } = req.query;
|
|
57
|
+
const content = tmux.capturePane(req.params.window, host);
|
|
56
58
|
|
|
57
59
|
res.json({ window: req.params.window, content });
|
|
58
60
|
});
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const multer = require('multer');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const { PATHS } = require('../../utils/paths');
|
|
6
|
+
|
|
7
|
+
const router = express.Router();
|
|
8
|
+
|
|
9
|
+
// NexusCrew: attachments in ~/.nexuscrew/attachments
|
|
10
|
+
const ATTACHMENTS_DIR = path.join(PATHS.ATTACHMENTS_DIR, 'incoming');
|
|
11
|
+
|
|
12
|
+
// Ensure directory exists
|
|
13
|
+
if (!fs.existsSync(ATTACHMENTS_DIR)) {
|
|
14
|
+
fs.mkdirSync(ATTACHMENTS_DIR, { recursive: true });
|
|
15
|
+
console.log(`[Upload] Created attachments directory: ${ATTACHMENTS_DIR}`);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Multer storage config
|
|
19
|
+
const storage = multer.diskStorage({
|
|
20
|
+
destination: (req, file, cb) => {
|
|
21
|
+
cb(null, ATTACHMENTS_DIR);
|
|
22
|
+
},
|
|
23
|
+
filename: (req, file, cb) => {
|
|
24
|
+
// Unique filename: timestamp_originalname
|
|
25
|
+
const timestamp = Date.now();
|
|
26
|
+
const safeName = file.originalname.replace(/[^a-zA-Z0-9._-]/g, '_');
|
|
27
|
+
cb(null, `${timestamp}_${safeName}`);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// File filter - allow images and documents
|
|
32
|
+
const fileFilter = (req, file, cb) => {
|
|
33
|
+
const allowedMimes = [
|
|
34
|
+
// Images
|
|
35
|
+
'image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/svg+xml',
|
|
36
|
+
// Documents
|
|
37
|
+
'text/plain', 'text/markdown', 'text/csv', 'text/html', 'text/css',
|
|
38
|
+
'application/json', 'application/xml', 'application/pdf',
|
|
39
|
+
'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
40
|
+
'application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
41
|
+
// Code files (often sent as octet-stream)
|
|
42
|
+
'application/octet-stream', 'text/x-python', 'text/javascript', 'application/javascript'
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
// Also allow by extension for code files
|
|
46
|
+
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'];
|
|
47
|
+
const ext = path.extname(file.originalname).toLowerCase();
|
|
48
|
+
|
|
49
|
+
if (allowedMimes.includes(file.mimetype) || allowedExts.includes(ext)) {
|
|
50
|
+
cb(null, true);
|
|
51
|
+
} else {
|
|
52
|
+
cb(new Error(`File type not allowed: ${file.mimetype} (${ext})`), false);
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// Multer config - 50MB limit
|
|
57
|
+
const upload = multer({
|
|
58
|
+
storage,
|
|
59
|
+
fileFilter,
|
|
60
|
+
limits: { fileSize: 50 * 1024 * 1024 }
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// POST /api/v1/upload - Single file upload
|
|
64
|
+
router.post('/', upload.single('file'), (req, res) => {
|
|
65
|
+
try {
|
|
66
|
+
if (!req.file) {
|
|
67
|
+
return res.status(400).json({ error: 'No file uploaded' });
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const filePath = req.file.path;
|
|
71
|
+
const fileName = req.file.filename;
|
|
72
|
+
const originalName = req.file.originalname;
|
|
73
|
+
const mimeType = req.file.mimetype;
|
|
74
|
+
const size = req.file.size;
|
|
75
|
+
|
|
76
|
+
console.log(`[Upload] File saved: ${filePath} (${size} bytes, ${mimeType})`);
|
|
77
|
+
|
|
78
|
+
res.json({
|
|
79
|
+
success: true,
|
|
80
|
+
file: {
|
|
81
|
+
path: filePath,
|
|
82
|
+
name: fileName,
|
|
83
|
+
originalName,
|
|
84
|
+
mimeType,
|
|
85
|
+
size
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
} catch (error) {
|
|
89
|
+
console.error('[Upload] Error:', error);
|
|
90
|
+
res.status(500).json({ error: error.message });
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// POST /api/v1/upload/multiple - Multiple files upload
|
|
95
|
+
router.post('/multiple', upload.array('files', 10), (req, res) => {
|
|
96
|
+
try {
|
|
97
|
+
if (!req.files || req.files.length === 0) {
|
|
98
|
+
return res.status(400).json({ error: 'No files uploaded' });
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const files = req.files.map(f => ({
|
|
102
|
+
path: f.path,
|
|
103
|
+
name: f.filename,
|
|
104
|
+
originalName: f.originalname,
|
|
105
|
+
mimeType: f.mimetype,
|
|
106
|
+
size: f.size
|
|
107
|
+
}));
|
|
108
|
+
|
|
109
|
+
console.log(`[Upload] ${files.length} files saved`);
|
|
110
|
+
|
|
111
|
+
res.json({
|
|
112
|
+
success: true,
|
|
113
|
+
files
|
|
114
|
+
});
|
|
115
|
+
} catch (error) {
|
|
116
|
+
console.error('[Upload] Error:', error);
|
|
117
|
+
res.status(500).json({ error: error.message });
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// Error handler for multer
|
|
122
|
+
router.use((error, req, res, next) => {
|
|
123
|
+
if (error instanceof multer.MulterError) {
|
|
124
|
+
if (error.code === 'LIMIT_FILE_SIZE') {
|
|
125
|
+
return res.status(400).json({ error: 'File too large (max 50MB)' });
|
|
126
|
+
}
|
|
127
|
+
return res.status(400).json({ error: error.message });
|
|
128
|
+
}
|
|
129
|
+
if (error) {
|
|
130
|
+
return res.status(400).json({ error: error.message });
|
|
131
|
+
}
|
|
132
|
+
next();
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
module.exports = router;
|
|
@@ -0,0 +1,95 @@
|
|
|
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;
|
|
@@ -0,0 +1,101 @@
|
|
|
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;
|
package/lib/server/server.js
CHANGED
|
@@ -10,56 +10,73 @@ const cors = require('cors');
|
|
|
10
10
|
const path = require('path');
|
|
11
11
|
const fs = require('fs');
|
|
12
12
|
const http = require('http');
|
|
13
|
-
const https = require('https');
|
|
14
13
|
|
|
14
|
+
const { initDb, saveDb } = require('./db/adapter');
|
|
15
15
|
const TmuxManager = require('../services/tmux-manager');
|
|
16
16
|
const EngineDiscovery = require('../services/engine-discovery');
|
|
17
17
|
const SessionStore = require('../services/session-store');
|
|
18
18
|
const LogWatcher = require('../services/log-watcher');
|
|
19
|
+
const RemotePaneWatcher = require('../services/remote-pane-watcher');
|
|
20
|
+
const AttachmentManager = require('./services/attachment-manager');
|
|
21
|
+
const SpeechManager = require('./services/speech-manager');
|
|
22
|
+
const { getConfig } = require('../config/manager');
|
|
19
23
|
|
|
24
|
+
// Routes
|
|
20
25
|
const sendRouter = require('./routes/send');
|
|
21
26
|
const sessionsRouter = require('./routes/sessions');
|
|
22
27
|
const modelsRouter = require('./routes/models');
|
|
28
|
+
const runtimesRouter = require('./routes/runtimes');
|
|
23
29
|
const statusRouter = require('./routes/status');
|
|
24
30
|
const hostsRouter = require('./routes/hosts');
|
|
31
|
+
const wakeLockRouter = require('./routes/wake-lock');
|
|
32
|
+
const authRouter = require('./routes/auth');
|
|
33
|
+
const speechRouter = require('./routes/speech');
|
|
34
|
+
const keysRouter = require('./routes/keys');
|
|
35
|
+
const uploadRouter = require('./routes/upload');
|
|
36
|
+
const workspacesRouter = require('./routes/workspaces');
|
|
37
|
+
|
|
38
|
+
// Middleware
|
|
39
|
+
const { authMiddleware } = require('./middleware/auth');
|
|
25
40
|
|
|
26
41
|
class NexusCrewServer {
|
|
27
42
|
constructor(config = {}) {
|
|
28
|
-
this.port = config.port || process.env.PORT || 41820;
|
|
29
43
|
this.configDir = config.configDir || path.join(process.env.HOME, '.nexuscrew');
|
|
30
44
|
this.config = this._loadConfig();
|
|
45
|
+
this.port = config.port || process.env.PORT || this.config.server?.port || 41820;
|
|
31
46
|
this.app = express();
|
|
32
47
|
this.server = null;
|
|
33
48
|
}
|
|
34
49
|
|
|
35
50
|
_loadConfig() {
|
|
36
|
-
|
|
37
|
-
if (fs.existsSync(configPath)) {
|
|
38
|
-
return JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
39
|
-
}
|
|
40
|
-
return { port: this.port, tmuxSession: 'nexuscrew', autoDiscovery: true };
|
|
51
|
+
return getConfig();
|
|
41
52
|
}
|
|
42
53
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
54
|
+
async start() {
|
|
55
|
+
// 1. Init database
|
|
56
|
+
await initDb();
|
|
57
|
+
|
|
58
|
+
// 2. Seed admin user if no users exist
|
|
59
|
+
const User = require('./models/User');
|
|
60
|
+
if (User.count() === 0) {
|
|
61
|
+
User.create('admin', 'admin', 'admin');
|
|
62
|
+
console.log(' Created initial admin account from local defaults');
|
|
47
63
|
}
|
|
48
|
-
return [{ name: 'local', type: 'local', default: true }];
|
|
49
|
-
}
|
|
50
64
|
|
|
51
|
-
|
|
52
|
-
// Init services
|
|
53
|
-
const hosts = this._loadHosts();
|
|
65
|
+
// 3. Init services
|
|
54
66
|
const tmuxManager = new TmuxManager({
|
|
55
67
|
tmuxSession: this.config.tmuxSession || 'nexuscrew',
|
|
56
|
-
logDir: this.config.logDir || path.join(this.configDir, 'logs')
|
|
57
|
-
hosts
|
|
68
|
+
logDir: this.config.logDir || path.join(this.configDir, 'logs')
|
|
58
69
|
});
|
|
59
70
|
|
|
60
71
|
const engineDiscovery = new EngineDiscovery(this.config.providersPath);
|
|
61
|
-
const sessionStore = new SessionStore(
|
|
72
|
+
const sessionStore = new SessionStore();
|
|
73
|
+
await sessionStore.init();
|
|
62
74
|
const logWatcher = new LogWatcher();
|
|
75
|
+
const remotePaneWatcher = new RemotePaneWatcher(tmuxManager);
|
|
76
|
+
const attachmentManager = new AttachmentManager(tmuxManager);
|
|
77
|
+
const speechManager = new SpeechManager();
|
|
78
|
+
this.app.locals.logWatcher = logWatcher;
|
|
79
|
+
this.app.locals.remotePaneWatcher = remotePaneWatcher;
|
|
63
80
|
|
|
64
81
|
// Ensure tmux master session
|
|
65
82
|
tmuxManager.ensureSession();
|
|
@@ -75,40 +92,52 @@ class NexusCrewServer {
|
|
|
75
92
|
}
|
|
76
93
|
}
|
|
77
94
|
|
|
78
|
-
// Middleware
|
|
95
|
+
// 4. Middleware
|
|
79
96
|
this.app.use(cors());
|
|
80
97
|
this.app.use(express.json({ limit: '10mb' }));
|
|
81
98
|
|
|
82
|
-
// Health check
|
|
99
|
+
// 5. Health check (public)
|
|
83
100
|
this.app.get('/health', (req, res) => {
|
|
84
101
|
res.json({
|
|
85
102
|
status: 'ok',
|
|
86
103
|
service: 'nexuscrew',
|
|
87
|
-
version: '0.1
|
|
104
|
+
version: '0.2.1',
|
|
88
105
|
tmux: tmuxManager.hasSession(),
|
|
89
106
|
windows: tmuxManager.listWindows().length,
|
|
90
107
|
uptime: process.uptime()
|
|
91
108
|
});
|
|
92
109
|
});
|
|
93
110
|
|
|
94
|
-
// Inject services into request
|
|
111
|
+
// 6. Inject services into request
|
|
95
112
|
this.app.use((req, res, next) => {
|
|
96
113
|
req.tmux = tmuxManager;
|
|
97
114
|
req.engines = engineDiscovery;
|
|
98
115
|
req.store = sessionStore;
|
|
99
116
|
req.logWatcher = logWatcher;
|
|
117
|
+
req.remotePaneWatcher = remotePaneWatcher;
|
|
118
|
+
req.attachments = attachmentManager;
|
|
119
|
+
req.speech = speechManager;
|
|
100
120
|
req.config = this.config;
|
|
101
121
|
next();
|
|
102
122
|
});
|
|
103
123
|
|
|
104
|
-
//
|
|
105
|
-
this.app.use('/api/v1/
|
|
106
|
-
this.app.use('/api/v1
|
|
124
|
+
// 7. Public routes
|
|
125
|
+
this.app.use('/api/v1/auth', authRouter);
|
|
126
|
+
this.app.use('/api/v1', wakeLockRouter);
|
|
127
|
+
this.app.use('/api/v1/runtimes', runtimesRouter); // public for login screen
|
|
107
128
|
this.app.use('/api/v1/models', modelsRouter);
|
|
108
|
-
this.app.use('/api/v1/
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
129
|
+
this.app.use('/api/v1/keys', keysRouter); // public for STT key check
|
|
130
|
+
|
|
131
|
+
// 8. Protected routes
|
|
132
|
+
this.app.use('/api/v1/send', authMiddleware, sendRouter);
|
|
133
|
+
this.app.use('/api/v1/sessions', authMiddleware, sessionsRouter);
|
|
134
|
+
this.app.use('/api/v1/workspaces', authMiddleware, workspacesRouter);
|
|
135
|
+
this.app.use('/api/v1/status', authMiddleware, statusRouter);
|
|
136
|
+
this.app.use('/api/v1/hosts', authMiddleware, hostsRouter);
|
|
137
|
+
this.app.use('/api/v1/speech', authMiddleware, speechRouter);
|
|
138
|
+
this.app.use('/api/v1/upload', authMiddleware, uploadRouter);
|
|
139
|
+
|
|
140
|
+
// 9. Serve frontend
|
|
112
141
|
const frontendDist = path.join(__dirname, '../../frontend/dist');
|
|
113
142
|
if (fs.existsSync(frontendDist)) {
|
|
114
143
|
this.app.use(express.static(frontendDist));
|
|
@@ -119,13 +148,13 @@ class NexusCrewServer {
|
|
|
119
148
|
});
|
|
120
149
|
}
|
|
121
150
|
|
|
122
|
-
// Error handler
|
|
151
|
+
// 10. Error handler
|
|
123
152
|
this.app.use((err, req, res, next) => {
|
|
124
153
|
console.error('[Error]', err.message);
|
|
125
154
|
res.status(500).json({ error: err.message });
|
|
126
155
|
});
|
|
127
156
|
|
|
128
|
-
// Start HTTP server
|
|
157
|
+
// 11. Start HTTP server
|
|
129
158
|
this.server = http.createServer(this.app);
|
|
130
159
|
|
|
131
160
|
return new Promise((resolve) => {
|
|
@@ -137,7 +166,7 @@ class NexusCrewServer {
|
|
|
137
166
|
console.log('');
|
|
138
167
|
console.log(` HTTP: http://localhost:${this.port}`);
|
|
139
168
|
console.log(` tmux: session "${tmuxManager.sessionName}" (${tmuxManager.listWindows().length} windows)`);
|
|
140
|
-
console.log(` Hosts: ${
|
|
169
|
+
console.log(` Hosts: ${tmuxManager.getAllHosts().map(h => h.name).join(', ')}`);
|
|
141
170
|
console.log('');
|
|
142
171
|
|
|
143
172
|
const engines = engineDiscovery.discover();
|
|
@@ -153,6 +182,10 @@ class NexusCrewServer {
|
|
|
153
182
|
|
|
154
183
|
stop() {
|
|
155
184
|
if (this.server) {
|
|
185
|
+
const appLocals = this.app?.locals || {};
|
|
186
|
+
appLocals.logWatcher?.unwatchAll?.();
|
|
187
|
+
appLocals.remotePaneWatcher?.unwatchAll?.();
|
|
188
|
+
saveDb();
|
|
156
189
|
this.server.close();
|
|
157
190
|
}
|
|
158
191
|
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { PATHS } = require('../../utils/paths');
|
|
4
|
+
|
|
5
|
+
class AttachmentManager {
|
|
6
|
+
constructor(tmux) {
|
|
7
|
+
this.tmux = tmux;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
getLocalAttachmentDir(conversationId = 'shared') {
|
|
11
|
+
return path.join(PATHS.ATTACHMENTS_DIR, 'local', conversationId);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
getRemoteAttachmentDir(conversationId = 'shared') {
|
|
15
|
+
return path.posix.join('.nexuscrew', 'attachments', conversationId);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
stageForConversation(file, conversationId, hostRef) {
|
|
19
|
+
if (!file?.path) {
|
|
20
|
+
throw new Error('Attachment path is required');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const host = this.tmux.resolveHost(hostRef);
|
|
24
|
+
const baseName = path.basename(file.path);
|
|
25
|
+
|
|
26
|
+
if (!host || host.type === 'local') {
|
|
27
|
+
const localDir = this.getLocalAttachmentDir(conversationId);
|
|
28
|
+
fs.mkdirSync(localDir, { recursive: true });
|
|
29
|
+
const localPath = path.join(localDir, baseName);
|
|
30
|
+
if (file.path !== localPath) {
|
|
31
|
+
fs.copyFileSync(file.path, localPath);
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
...file,
|
|
35
|
+
host: 'local',
|
|
36
|
+
localPath,
|
|
37
|
+
promptPath: localPath,
|
|
38
|
+
ready: true
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const remoteDir = this.getRemoteAttachmentDir(conversationId);
|
|
43
|
+
const remotePath = path.posix.join(remoteDir, baseName);
|
|
44
|
+
this.tmux.copyToHost(file.path, remotePath, host);
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
...file,
|
|
48
|
+
host: host.name,
|
|
49
|
+
localPath: file.path,
|
|
50
|
+
remotePath,
|
|
51
|
+
promptPath: `~/${remotePath}`,
|
|
52
|
+
ready: true
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
module.exports = AttachmentManager;
|