@mmmbuto/nexuscrew 0.2.4 → 0.4.0
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 +63 -163
- package/bin/nexuscrew.js +1 -296
- package/frontend/dist/assets/index-B41xYEDb.js +81 -0
- package/frontend/dist/assets/index-Cjqxzf2o.css +32 -0
- package/frontend/dist/icon-maskable.svg +4 -0
- package/frontend/dist/icon.svg +4 -0
- package/frontend/dist/index.html +11 -3
- package/frontend/dist/manifest.json +24 -0
- package/frontend/dist/sw.js +3 -0
- package/frontend/index.html +20 -0
- package/lib/auth/token.js +26 -0
- package/lib/config.js +25 -0
- package/lib/pty/attach.js +30 -0
- package/lib/pty/provider.js +33 -0
- package/lib/server.js +62 -0
- package/lib/tmux/actions.js +27 -0
- package/lib/tmux/list.js +33 -0
- package/lib/ws/bridge.js +66 -0
- package/package.json +20 -24
- package/frontend/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/frontend/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/frontend/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/frontend/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/frontend/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/frontend/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/frontend/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/frontend/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/frontend/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/frontend/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/frontend/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/frontend/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/frontend/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/frontend/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/frontend/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/frontend/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/frontend/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/frontend/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/frontend/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/frontend/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/frontend/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/frontend/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/frontend/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/frontend/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/frontend/dist/assets/index-CSPyKzmq.js +0 -8614
- package/frontend/dist/assets/index-Cl7bxc7X.css +0 -1
- package/lib/config/paths.js +0 -30
- package/lib/server/routes/hosts.js +0 -82
- package/lib/server/routes/launchers.js +0 -17
- package/lib/server/routes/models.js +0 -26
- package/lib/server/routes/runtime.js +0 -14
- package/lib/server/routes/send.js +0 -236
- package/lib/server/routes/sessions.js +0 -104
- package/lib/server/routes/status.js +0 -62
- package/lib/server/routes/tmux.js +0 -91
- package/lib/server/server.js +0 -189
- package/lib/services/engine-discovery.js +0 -317
- package/lib/services/log-watcher.js +0 -279
- package/lib/services/session-store.js +0 -198
- package/lib/services/tmux-manager.js +0 -288
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* GET /api/v1/sessions — List/load sessions
|
|
3
|
-
* GET /api/v1/sessions/:id/messages — Get session messages
|
|
4
|
-
* GET /api/v1/sessions/:id/summary — Get session summary
|
|
5
|
-
* POST /api/v1/sessions/:id/rename — Rename session
|
|
6
|
-
* DELETE /api/v1/sessions/:id — Delete session
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
const { Router } = require('express');
|
|
10
|
-
const router = Router();
|
|
11
|
-
|
|
12
|
-
// List conversations (optionally grouped by date)
|
|
13
|
-
router.get('/', (req, res) => {
|
|
14
|
-
const { store } = req;
|
|
15
|
-
const { workspace, groupBy } = req.query;
|
|
16
|
-
|
|
17
|
-
if (groupBy === 'date') {
|
|
18
|
-
const grouped = store.listGroupedByDate({ workspace });
|
|
19
|
-
return res.json(grouped);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const convs = store.listConversations({ workspace });
|
|
23
|
-
res.json({ conversations: convs });
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
// Get single conversation
|
|
27
|
-
router.get('/:id', (req, res) => {
|
|
28
|
-
const { store, tmux } = req;
|
|
29
|
-
const conv = store.getConversation(req.params.id);
|
|
30
|
-
|
|
31
|
-
if (!conv) {
|
|
32
|
-
return res.status(404).json({ error: 'Not found' });
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// Check tmux window status
|
|
36
|
-
const host = conv.host !== 'local' ? conv.host : null;
|
|
37
|
-
const target = conv.tmux_session || conv.tmux_window;
|
|
38
|
-
const alive = target ? tmux.hasSession(target, host) : false;
|
|
39
|
-
|
|
40
|
-
res.json({
|
|
41
|
-
session: {
|
|
42
|
-
...conv,
|
|
43
|
-
tmux_alive: alive
|
|
44
|
-
}
|
|
45
|
-
});
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
// Get messages for a session
|
|
49
|
-
router.get('/:id/messages', (req, res) => {
|
|
50
|
-
const { store } = req;
|
|
51
|
-
const { limit = 50, before } = req.query;
|
|
52
|
-
|
|
53
|
-
const conv = store.getConversation(req.params.id);
|
|
54
|
-
if (!conv) {
|
|
55
|
-
return res.status(404).json({ error: 'Not found' });
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const messages = store.getMessages(req.params.id, { limit: Number(limit), before: before ? Number(before) : undefined });
|
|
59
|
-
const total = store.getMessageCount(req.params.id);
|
|
60
|
-
|
|
61
|
-
res.json({
|
|
62
|
-
session: conv,
|
|
63
|
-
messages,
|
|
64
|
-
pagination: {
|
|
65
|
-
total,
|
|
66
|
-
hasMore: messages.length < total,
|
|
67
|
-
oldestTimestamp: messages.length > 0 ? messages[0].created_at : null
|
|
68
|
-
}
|
|
69
|
-
});
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
// Rename conversation
|
|
73
|
-
router.post('/:id/rename', (req, res) => {
|
|
74
|
-
const { store } = req;
|
|
75
|
-
const { title } = req.body;
|
|
76
|
-
|
|
77
|
-
if (!title) return res.status(400).json({ error: 'Title required' });
|
|
78
|
-
|
|
79
|
-
store.updateConversation(req.params.id, { title });
|
|
80
|
-
res.json({ success: true, title });
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
// Delete conversation (kills tmux session too)
|
|
84
|
-
router.delete('/:id', (req, res) => {
|
|
85
|
-
const { store, tmux, logWatcher } = req;
|
|
86
|
-
const conv = store.getConversation(req.params.id);
|
|
87
|
-
|
|
88
|
-
if (!conv) return res.status(404).json({ error: 'Not found' });
|
|
89
|
-
|
|
90
|
-
if (conv.tmux_session) {
|
|
91
|
-
const host = conv.host !== 'local' ? conv.host : null;
|
|
92
|
-
tmux.killSession(conv.tmux_session, host);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// Stop log watcher
|
|
96
|
-
if (conv.log_path) {
|
|
97
|
-
logWatcher.unwatch(conv.log_path);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
store.deleteConversation(req.params.id);
|
|
101
|
-
res.json({ success: true });
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
module.exports = router;
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* GET /api/v1/status — tmux status
|
|
3
|
-
* GET /api/v1/status/tmux — Detailed tmux session info
|
|
4
|
-
* GET /api/v1/status/pane/:session — Capture pane content
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
const { Router } = require('express');
|
|
8
|
-
const router = Router();
|
|
9
|
-
|
|
10
|
-
// Overall status
|
|
11
|
-
router.get('/', (req, res) => {
|
|
12
|
-
const { tmux } = req;
|
|
13
|
-
const host = req.query.host || 'local';
|
|
14
|
-
const sessions = tmux.listSessions(host === 'local' ? null : host);
|
|
15
|
-
const hosts = tmux.getAllHosts();
|
|
16
|
-
|
|
17
|
-
res.json({
|
|
18
|
-
host,
|
|
19
|
-
tmux: {
|
|
20
|
-
sessionCount: sessions.length,
|
|
21
|
-
sessions
|
|
22
|
-
},
|
|
23
|
-
hosts: hosts.map(h => ({
|
|
24
|
-
...h,
|
|
25
|
-
connected: tmux.testHost(h)
|
|
26
|
-
}))
|
|
27
|
-
});
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
// Detailed tmux windows
|
|
31
|
-
router.get('/tmux', (req, res) => {
|
|
32
|
-
const { tmux, store } = req;
|
|
33
|
-
const host = req.query.host || 'local';
|
|
34
|
-
const sessions = tmux.listSessions(host === 'local' ? null : host);
|
|
35
|
-
const conversations = store.listConversations({ host, limit: 500 });
|
|
36
|
-
|
|
37
|
-
const enriched = sessions.map((session) => {
|
|
38
|
-
const conv = conversations.find((item) => item.tmux_session === session.sessionName);
|
|
39
|
-
return {
|
|
40
|
-
...session,
|
|
41
|
-
conversationId: conv?.id || null,
|
|
42
|
-
engine: conv?.engine || null,
|
|
43
|
-
model: conv?.model || null,
|
|
44
|
-
host,
|
|
45
|
-
title: conv?.title || session.sessionName,
|
|
46
|
-
workspace: conv?.workspace || ''
|
|
47
|
-
};
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
res.json({ host, sessions: enriched });
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
// Capture pane content
|
|
54
|
-
router.get('/pane/:session', (req, res) => {
|
|
55
|
-
const { tmux } = req;
|
|
56
|
-
const { host } = req.query;
|
|
57
|
-
const content = tmux.capturePane(req.params.session, host || null);
|
|
58
|
-
|
|
59
|
-
res.json({ session: req.params.session, content });
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
module.exports = router;
|
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
const { Router } = require('express');
|
|
2
|
-
|
|
3
|
-
const router = Router();
|
|
4
|
-
|
|
5
|
-
router.get('/sessions', (req, res) => {
|
|
6
|
-
const { tmux, store } = req;
|
|
7
|
-
const host = req.query.host || 'local';
|
|
8
|
-
const resolvedHost = host === 'local' ? null : host;
|
|
9
|
-
const sessions = tmux.listSessions(resolvedHost).map((session) => {
|
|
10
|
-
const conversation = store.listConversations({ host, limit: 500 })
|
|
11
|
-
.find((item) => item.tmux_session === session.sessionName);
|
|
12
|
-
return {
|
|
13
|
-
...session,
|
|
14
|
-
host,
|
|
15
|
-
workspace: conversation?.workspace || '',
|
|
16
|
-
conversationId: conversation?.id || null,
|
|
17
|
-
launcherId: conversation?.launcher_id || null,
|
|
18
|
-
engine: conversation?.engine || null,
|
|
19
|
-
status: conversation?.status || 'external'
|
|
20
|
-
};
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
res.json({ sessions });
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
router.post('/sessions', (req, res) => {
|
|
27
|
-
const { tmux, store, engines } = req;
|
|
28
|
-
const {
|
|
29
|
-
sessionName,
|
|
30
|
-
launcherId,
|
|
31
|
-
workspace,
|
|
32
|
-
host
|
|
33
|
-
} = req.body;
|
|
34
|
-
|
|
35
|
-
if (!sessionName || !sessionName.trim()) {
|
|
36
|
-
return res.status(400).json({ error: 'sessionName is required' });
|
|
37
|
-
}
|
|
38
|
-
if (!launcherId) {
|
|
39
|
-
return res.status(400).json({ error: 'launcherId is required' });
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const launcher = engines.getLauncher(launcherId, host || 'local');
|
|
43
|
-
if (!launcher) {
|
|
44
|
-
return res.status(404).json({ error: `Launcher "${launcherId}" not found` });
|
|
45
|
-
}
|
|
46
|
-
if (!launcher.runnable || launcher.bucket !== 'runnable') {
|
|
47
|
-
return res.status(409).json({
|
|
48
|
-
error: `Launcher "${launcher.label}" was detected from config but is not verified runnable in the current shell`
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const targetHost = host || 'local';
|
|
53
|
-
if (tmux.hasSession(sessionName.trim(), targetHost)) {
|
|
54
|
-
return res.status(409).json({ error: `tmux session "${sessionName.trim()}" already exists` });
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const runtime = engines.getRuntimeInfo();
|
|
58
|
-
const createResult = tmux.createSession({
|
|
59
|
-
sessionName: sessionName.trim(),
|
|
60
|
-
cwd: (workspace || runtime.homeDir || '').trim() || runtime.homeDir,
|
|
61
|
-
host: targetHost,
|
|
62
|
-
shell: runtime.shell,
|
|
63
|
-
launcherCommand: launcher.command
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
const conversation = store.createConversation({
|
|
67
|
-
title: sessionName.trim(),
|
|
68
|
-
engine: launcher.family,
|
|
69
|
-
workspace: (workspace || runtime.homeDir || '').trim() || runtime.homeDir,
|
|
70
|
-
host: targetHost,
|
|
71
|
-
tmuxSession: createResult.sessionName,
|
|
72
|
-
logPath: createResult.logPath,
|
|
73
|
-
launcherId: launcher.id
|
|
74
|
-
});
|
|
75
|
-
store.updateConversation(conversation.id, { title: sessionName.trim() });
|
|
76
|
-
|
|
77
|
-
res.status(201).json({
|
|
78
|
-
session: {
|
|
79
|
-
sessionName: createResult.sessionName,
|
|
80
|
-
logPath: createResult.logPath,
|
|
81
|
-
host: targetHost,
|
|
82
|
-
workspace: conversation.workspace,
|
|
83
|
-
conversationId: conversation.id,
|
|
84
|
-
launcherId: launcher.id,
|
|
85
|
-
engine: launcher.family,
|
|
86
|
-
runnable: true
|
|
87
|
-
}
|
|
88
|
-
});
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
module.exports = router;
|
package/lib/server/server.js
DELETED
|
@@ -1,189 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* NexusCrew Server — tmux-based AI cockpit
|
|
3
|
-
*
|
|
4
|
-
* Unified endpoint: POST /api/v1/send
|
|
5
|
-
* All engines routed through tmux windows.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
const express = require('express');
|
|
9
|
-
const cors = require('cors');
|
|
10
|
-
const path = require('path');
|
|
11
|
-
const fs = require('fs');
|
|
12
|
-
const http = require('http');
|
|
13
|
-
const os = require('os');
|
|
14
|
-
const packageJson = require('../../package.json');
|
|
15
|
-
const { getConfigDir, getDefaultLogDir } = require('../config/paths');
|
|
16
|
-
|
|
17
|
-
const TmuxManager = require('../services/tmux-manager');
|
|
18
|
-
const EngineDiscovery = require('../services/engine-discovery');
|
|
19
|
-
const SessionStore = require('../services/session-store');
|
|
20
|
-
const LogWatcher = require('../services/log-watcher');
|
|
21
|
-
|
|
22
|
-
const sendRouter = require('./routes/send');
|
|
23
|
-
const sessionsRouter = require('./routes/sessions');
|
|
24
|
-
const modelsRouter = require('./routes/models');
|
|
25
|
-
const statusRouter = require('./routes/status');
|
|
26
|
-
const hostsRouter = require('./routes/hosts');
|
|
27
|
-
const tmuxRouter = require('./routes/tmux');
|
|
28
|
-
const launchersRouter = require('./routes/launchers');
|
|
29
|
-
const runtimeRouter = require('./routes/runtime');
|
|
30
|
-
|
|
31
|
-
class NexusCrewServer {
|
|
32
|
-
constructor(config = {}) {
|
|
33
|
-
this.port = config.port || process.env.PORT || 41820;
|
|
34
|
-
this.configDir = config.configDir || getConfigDir();
|
|
35
|
-
this.config = this._loadConfig();
|
|
36
|
-
this.app = express();
|
|
37
|
-
this.server = null;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
_loadConfig() {
|
|
41
|
-
const configPath = path.join(this.configDir, 'config.json');
|
|
42
|
-
if (fs.existsSync(configPath)) {
|
|
43
|
-
const loaded = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
44
|
-
if (loaded.providersPath && !loaded.extraShellSources) {
|
|
45
|
-
loaded.extraShellSources = [loaded.providersPath].filter(Boolean);
|
|
46
|
-
}
|
|
47
|
-
return loaded;
|
|
48
|
-
}
|
|
49
|
-
return {
|
|
50
|
-
port: this.port,
|
|
51
|
-
autoDiscovery: true,
|
|
52
|
-
preferredShell: '',
|
|
53
|
-
extraShellSources: []
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
_loadHosts() {
|
|
58
|
-
const hostsPath = path.join(this.configDir, 'hosts.json');
|
|
59
|
-
if (fs.existsSync(hostsPath)) {
|
|
60
|
-
return JSON.parse(fs.readFileSync(hostsPath, 'utf8'));
|
|
61
|
-
}
|
|
62
|
-
return [{ name: 'local', type: 'local', default: true }];
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
async start() {
|
|
66
|
-
const hosts = this._loadHosts();
|
|
67
|
-
const tmuxManager = new TmuxManager({
|
|
68
|
-
logDir: this.config.logDir || getDefaultLogDir(),
|
|
69
|
-
configDir: this.configDir,
|
|
70
|
-
preferredShell: this.config.preferredShell || process.env.SHELL || '/bin/sh',
|
|
71
|
-
hosts
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
const engineDiscovery = new EngineDiscovery({
|
|
75
|
-
tmux: tmuxManager,
|
|
76
|
-
preferredShell: this.config.preferredShell,
|
|
77
|
-
extraShellSources: this.config.extraShellSources || []
|
|
78
|
-
});
|
|
79
|
-
const sessionStore = new SessionStore(path.join(this.configDir, 'nexuscrew.db'));
|
|
80
|
-
const logWatcher = new LogWatcher();
|
|
81
|
-
|
|
82
|
-
sessionStore.syncWithTmux(tmuxManager);
|
|
83
|
-
|
|
84
|
-
const active = sessionStore.listConversations({ status: 'active' });
|
|
85
|
-
for (const conv of active) {
|
|
86
|
-
if ((conv.host || 'local') === 'local' && conv.log_path && fs.existsSync(conv.log_path)) {
|
|
87
|
-
logWatcher.watch(conv.log_path, conv.engine || 'generic', conv.id);
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// Middleware
|
|
92
|
-
this.app.use(cors());
|
|
93
|
-
this.app.use(express.json({ limit: '10mb' }));
|
|
94
|
-
|
|
95
|
-
// Health check
|
|
96
|
-
this.app.get('/health', (req, res) => {
|
|
97
|
-
res.json({
|
|
98
|
-
status: 'ok',
|
|
99
|
-
service: 'nexuscrew',
|
|
100
|
-
version: packageJson.version,
|
|
101
|
-
tmuxSessions: tmuxManager.listSessions().length,
|
|
102
|
-
uptime: process.uptime()
|
|
103
|
-
});
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
// Inject services into request
|
|
107
|
-
this.app.use((req, res, next) => {
|
|
108
|
-
req.tmux = tmuxManager;
|
|
109
|
-
req.engines = engineDiscovery;
|
|
110
|
-
req.store = sessionStore;
|
|
111
|
-
req.logWatcher = logWatcher;
|
|
112
|
-
req.config = this.config;
|
|
113
|
-
req.configDir = this.configDir;
|
|
114
|
-
next();
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
// API routes
|
|
118
|
-
this.app.use('/api/v1/send', sendRouter);
|
|
119
|
-
this.app.use('/api/v1/sessions', sessionsRouter);
|
|
120
|
-
this.app.use('/api/v1/models', modelsRouter);
|
|
121
|
-
this.app.use('/api/v1/status', statusRouter);
|
|
122
|
-
this.app.use('/api/v1/hosts', hostsRouter);
|
|
123
|
-
this.app.use('/api/v1/tmux', tmuxRouter);
|
|
124
|
-
this.app.use('/api/v1/launchers', launchersRouter);
|
|
125
|
-
this.app.use('/api/v1/runtime', runtimeRouter);
|
|
126
|
-
|
|
127
|
-
// Serve frontend
|
|
128
|
-
const frontendDist = path.join(__dirname, '../../frontend/dist');
|
|
129
|
-
if (fs.existsSync(frontendDist)) {
|
|
130
|
-
this.app.use(express.static(frontendDist));
|
|
131
|
-
this.app.get('*', (req, res) => {
|
|
132
|
-
if (!req.path.startsWith('/api/')) {
|
|
133
|
-
res.sendFile(path.join(frontendDist, 'index.html'));
|
|
134
|
-
}
|
|
135
|
-
});
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// Error handler
|
|
139
|
-
this.app.use((err, req, res, next) => {
|
|
140
|
-
console.error('[Error]', err.message);
|
|
141
|
-
res.status(500).json({ error: err.message });
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
// Start HTTP server
|
|
145
|
-
this.server = http.createServer(this.app);
|
|
146
|
-
|
|
147
|
-
return new Promise((resolve) => {
|
|
148
|
-
this.server.listen(this.port, () => {
|
|
149
|
-
console.log('');
|
|
150
|
-
console.log('╔══════════════════════════════════════════════╗');
|
|
151
|
-
console.log('║ NexusCrew — tmux AI cockpit ║');
|
|
152
|
-
console.log('╚══════════════════════════════════════════════╝');
|
|
153
|
-
console.log('');
|
|
154
|
-
console.log(` HTTP: http://localhost:${this.port}`);
|
|
155
|
-
console.log(` tmux: ${tmuxManager.listSessions().length} active sessions`);
|
|
156
|
-
console.log(` Hosts: ${hosts.map(h => h.name).join(', ')}`);
|
|
157
|
-
console.log('');
|
|
158
|
-
|
|
159
|
-
const discovery = engineDiscovery.discover();
|
|
160
|
-
for (const launcher of discovery.launchers) {
|
|
161
|
-
console.log(` OK ${launcher.label} [${launcher.kind}]`);
|
|
162
|
-
}
|
|
163
|
-
console.log('');
|
|
164
|
-
|
|
165
|
-
resolve();
|
|
166
|
-
});
|
|
167
|
-
});
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
stop() {
|
|
171
|
-
if (this.server) {
|
|
172
|
-
this.server.close();
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
// Run if called directly
|
|
178
|
-
if (require.main === module) {
|
|
179
|
-
const server = new NexusCrewServer();
|
|
180
|
-
server.start().catch(err => {
|
|
181
|
-
console.error('Failed to start:', err);
|
|
182
|
-
process.exit(1);
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
process.on('SIGTERM', () => { server.stop(); process.exit(0); });
|
|
186
|
-
process.on('SIGINT', () => { console.log('\nShutting down...'); server.stop(); process.exit(0); });
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
module.exports = NexusCrewServer;
|