@mmmbuto/nexuscrew 0.2.1 → 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 +122 -67
- package/bin/nexuscrew.js +194 -470
- package/frontend/dist/assets/{index-BG1N7bSL.js → index-BsldfYnr.js} +1704 -1711
- package/frontend/dist/assets/index-DcQGg3dp.css +1 -0
- package/frontend/dist/index.html +3 -11
- package/lib/server/routes/hosts.js +22 -12
- 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 -258
- package/lib/server/routes/sessions.js +13 -42
- package/lib/server/routes/status.js +18 -22
- package/lib/server/routes/tmux.js +88 -0
- package/lib/server/server.js +60 -81
- package/lib/services/engine-discovery.js +192 -445
- package/lib/services/log-watcher.js +22 -40
- package/lib/services/session-store.js +88 -62
- package/lib/services/tmux-manager.js +111 -173
- package/package.json +5 -9
- package/frontend/dist/apple-touch-icon.png +0 -0
- package/frontend/dist/assets/index-CiAtinNP.css +0 -1
- package/frontend/dist/favicon.svg +0 -20
- package/frontend/dist/icon-192.png +0 -0
- package/frontend/dist/icon-512.png +0 -0
- package/frontend/dist/site.webmanifest +0 -29
- package/lib/config/hosts.js +0 -67
- package/lib/config/manager.js +0 -379
- 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 -28
- package/lib/server/routes/runtimes.js +0 -34
- package/lib/server/routes/speech.js +0 -46
- package/lib/server/routes/upload.js +0 -135
- package/lib/server/routes/wake-lock.js +0 -95
- package/lib/server/routes/workspaces.js +0 -101
- package/lib/server/services/attachment-manager.js +0 -57
- package/lib/server/services/context-bridge.js +0 -425
- package/lib/server/services/runtime-manager.js +0 -462
- package/lib/server/services/speech-manager.js +0 -76
- package/lib/server/services/summary-generator.js +0 -309
- package/lib/server/services/workspace-manager.js +0 -79
- package/lib/services/remote-pane-watcher.js +0 -155
- package/lib/setup/postinstall.js +0 -38
- package/lib/utils/paths.js +0 -124
- package/lib/utils/termux.js +0 -182
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
const { Router } = require('express');
|
|
2
|
+
|
|
3
|
+
const router = Router();
|
|
4
|
+
|
|
5
|
+
router.get('/sessions', (req, res) => {
|
|
6
|
+
const { tmux, store } = req;
|
|
7
|
+
const sessions = tmux.listSessions().map((session) => {
|
|
8
|
+
const conversation = store.findByTmuxSession(session.sessionName);
|
|
9
|
+
return {
|
|
10
|
+
...session,
|
|
11
|
+
host: conversation?.host || 'local',
|
|
12
|
+
workspace: conversation?.workspace || '',
|
|
13
|
+
conversationId: conversation?.id || null,
|
|
14
|
+
launcherId: conversation?.launcher_id || null,
|
|
15
|
+
engine: conversation?.engine || null,
|
|
16
|
+
status: conversation?.status || 'external'
|
|
17
|
+
};
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
res.json({ sessions });
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
router.post('/sessions', (req, res) => {
|
|
24
|
+
const { tmux, store, engines } = req;
|
|
25
|
+
const {
|
|
26
|
+
sessionName,
|
|
27
|
+
launcherId,
|
|
28
|
+
workspace,
|
|
29
|
+
host
|
|
30
|
+
} = req.body;
|
|
31
|
+
|
|
32
|
+
if (!sessionName || !sessionName.trim()) {
|
|
33
|
+
return res.status(400).json({ error: 'sessionName is required' });
|
|
34
|
+
}
|
|
35
|
+
if (!launcherId) {
|
|
36
|
+
return res.status(400).json({ error: 'launcherId is required' });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const launcher = engines.getLauncher(launcherId);
|
|
40
|
+
if (!launcher) {
|
|
41
|
+
return res.status(404).json({ error: `Launcher "${launcherId}" not found` });
|
|
42
|
+
}
|
|
43
|
+
if (!launcher.runnable) {
|
|
44
|
+
return res.status(409).json({
|
|
45
|
+
error: `Launcher "${launcher.label}" was detected from config but is not verified runnable in the current shell`
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const targetHost = host || 'local';
|
|
50
|
+
if (tmux.hasSession(sessionName.trim(), targetHost)) {
|
|
51
|
+
return res.status(409).json({ error: `tmux session "${sessionName.trim()}" already exists` });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const runtime = engines.getRuntimeInfo();
|
|
55
|
+
const createResult = tmux.createSession({
|
|
56
|
+
sessionName: sessionName.trim(),
|
|
57
|
+
cwd: (workspace || runtime.homeDir || '').trim() || runtime.homeDir,
|
|
58
|
+
host: targetHost,
|
|
59
|
+
shell: runtime.shell,
|
|
60
|
+
launcherCommand: launcher.command
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const conversation = store.createConversation({
|
|
64
|
+
title: sessionName.trim(),
|
|
65
|
+
engine: launcher.family,
|
|
66
|
+
workspace: (workspace || runtime.homeDir || '').trim() || runtime.homeDir,
|
|
67
|
+
host: targetHost,
|
|
68
|
+
tmuxSession: createResult.sessionName,
|
|
69
|
+
logPath: createResult.logPath,
|
|
70
|
+
launcherId: launcher.id
|
|
71
|
+
});
|
|
72
|
+
store.updateConversation(conversation.id, { title: sessionName.trim() });
|
|
73
|
+
|
|
74
|
+
res.status(201).json({
|
|
75
|
+
session: {
|
|
76
|
+
sessionName: createResult.sessionName,
|
|
77
|
+
logPath: createResult.logPath,
|
|
78
|
+
host: targetHost,
|
|
79
|
+
workspace: conversation.workspace,
|
|
80
|
+
conversationId: conversation.id,
|
|
81
|
+
launcherId: launcher.id,
|
|
82
|
+
engine: launcher.family,
|
|
83
|
+
runnable: true
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
module.exports = router;
|
package/lib/server/server.js
CHANGED
|
@@ -10,134 +10,117 @@ const cors = require('cors');
|
|
|
10
10
|
const path = require('path');
|
|
11
11
|
const fs = require('fs');
|
|
12
12
|
const http = require('http');
|
|
13
|
+
const os = require('os');
|
|
14
|
+
const packageJson = require('../../package.json');
|
|
13
15
|
|
|
14
|
-
const { initDb, saveDb } = require('./db/adapter');
|
|
15
16
|
const TmuxManager = require('../services/tmux-manager');
|
|
16
17
|
const EngineDiscovery = require('../services/engine-discovery');
|
|
17
18
|
const SessionStore = require('../services/session-store');
|
|
18
19
|
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');
|
|
23
20
|
|
|
24
|
-
// Routes
|
|
25
21
|
const sendRouter = require('./routes/send');
|
|
26
22
|
const sessionsRouter = require('./routes/sessions');
|
|
27
23
|
const modelsRouter = require('./routes/models');
|
|
28
|
-
const runtimesRouter = require('./routes/runtimes');
|
|
29
24
|
const statusRouter = require('./routes/status');
|
|
30
25
|
const hostsRouter = require('./routes/hosts');
|
|
31
|
-
const
|
|
32
|
-
const
|
|
33
|
-
const
|
|
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');
|
|
26
|
+
const tmuxRouter = require('./routes/tmux');
|
|
27
|
+
const launchersRouter = require('./routes/launchers');
|
|
28
|
+
const runtimeRouter = require('./routes/runtime');
|
|
40
29
|
|
|
41
30
|
class NexusCrewServer {
|
|
42
31
|
constructor(config = {}) {
|
|
43
|
-
this.
|
|
32
|
+
this.port = config.port || process.env.PORT || 41820;
|
|
33
|
+
this.configDir = config.configDir || path.join(os.homedir(), '.nexuscrew');
|
|
44
34
|
this.config = this._loadConfig();
|
|
45
|
-
this.port = config.port || process.env.PORT || this.config.server?.port || 41820;
|
|
46
35
|
this.app = express();
|
|
47
36
|
this.server = null;
|
|
48
37
|
}
|
|
49
38
|
|
|
50
39
|
_loadConfig() {
|
|
51
|
-
|
|
40
|
+
const configPath = path.join(this.configDir, 'config.json');
|
|
41
|
+
if (fs.existsSync(configPath)) {
|
|
42
|
+
const loaded = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
43
|
+
if (loaded.providersPath && !loaded.extraShellSources) {
|
|
44
|
+
loaded.extraShellSources = [loaded.providersPath].filter(Boolean);
|
|
45
|
+
}
|
|
46
|
+
return loaded;
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
port: this.port,
|
|
50
|
+
autoDiscovery: true,
|
|
51
|
+
preferredShell: '',
|
|
52
|
+
extraShellSources: []
|
|
53
|
+
};
|
|
52
54
|
}
|
|
53
55
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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');
|
|
56
|
+
_loadHosts() {
|
|
57
|
+
const hostsPath = path.join(this.configDir, 'hosts.json');
|
|
58
|
+
if (fs.existsSync(hostsPath)) {
|
|
59
|
+
return JSON.parse(fs.readFileSync(hostsPath, 'utf8'));
|
|
63
60
|
}
|
|
61
|
+
return [{ name: 'local', type: 'local', default: true }];
|
|
62
|
+
}
|
|
64
63
|
|
|
65
|
-
|
|
64
|
+
async start() {
|
|
65
|
+
const hosts = this._loadHosts();
|
|
66
66
|
const tmuxManager = new TmuxManager({
|
|
67
|
-
|
|
68
|
-
|
|
67
|
+
logDir: this.config.logDir || path.join(this.configDir, 'logs'),
|
|
68
|
+
preferredShell: this.config.preferredShell || process.env.SHELL || '/bin/sh',
|
|
69
|
+
hosts
|
|
69
70
|
});
|
|
70
71
|
|
|
71
|
-
const engineDiscovery = new EngineDiscovery(
|
|
72
|
-
|
|
73
|
-
|
|
72
|
+
const engineDiscovery = new EngineDiscovery({
|
|
73
|
+
preferredShell: this.config.preferredShell,
|
|
74
|
+
extraShellSources: this.config.extraShellSources || []
|
|
75
|
+
});
|
|
76
|
+
const sessionStore = new SessionStore(path.join(this.configDir, 'nexuscrew.db'));
|
|
74
77
|
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;
|
|
80
78
|
|
|
81
|
-
// Ensure tmux master session
|
|
82
|
-
tmuxManager.ensureSession();
|
|
83
|
-
|
|
84
|
-
// Sync DB with tmux reality
|
|
85
79
|
sessionStore.syncWithTmux(tmuxManager);
|
|
86
80
|
|
|
87
|
-
// Resume watching logs for active conversations
|
|
88
81
|
const active = sessionStore.listConversations({ status: 'active' });
|
|
89
82
|
for (const conv of active) {
|
|
90
83
|
if (conv.log_path && fs.existsSync(conv.log_path)) {
|
|
91
|
-
logWatcher.watch(conv.log_path, conv.engine, conv.id);
|
|
84
|
+
logWatcher.watch(conv.log_path, conv.engine || 'generic', conv.id);
|
|
92
85
|
}
|
|
93
86
|
}
|
|
94
87
|
|
|
95
|
-
//
|
|
88
|
+
// Middleware
|
|
96
89
|
this.app.use(cors());
|
|
97
90
|
this.app.use(express.json({ limit: '10mb' }));
|
|
98
91
|
|
|
99
|
-
//
|
|
92
|
+
// Health check
|
|
100
93
|
this.app.get('/health', (req, res) => {
|
|
101
94
|
res.json({
|
|
102
95
|
status: 'ok',
|
|
103
96
|
service: 'nexuscrew',
|
|
104
|
-
version:
|
|
105
|
-
|
|
106
|
-
windows: tmuxManager.listWindows().length,
|
|
97
|
+
version: packageJson.version,
|
|
98
|
+
tmuxSessions: tmuxManager.listSessions().length,
|
|
107
99
|
uptime: process.uptime()
|
|
108
100
|
});
|
|
109
101
|
});
|
|
110
102
|
|
|
111
|
-
//
|
|
103
|
+
// Inject services into request
|
|
112
104
|
this.app.use((req, res, next) => {
|
|
113
105
|
req.tmux = tmuxManager;
|
|
114
106
|
req.engines = engineDiscovery;
|
|
115
107
|
req.store = sessionStore;
|
|
116
108
|
req.logWatcher = logWatcher;
|
|
117
|
-
req.remotePaneWatcher = remotePaneWatcher;
|
|
118
|
-
req.attachments = attachmentManager;
|
|
119
|
-
req.speech = speechManager;
|
|
120
109
|
req.config = this.config;
|
|
121
110
|
next();
|
|
122
111
|
});
|
|
123
112
|
|
|
124
|
-
//
|
|
125
|
-
this.app.use('/api/v1/
|
|
126
|
-
this.app.use('/api/v1',
|
|
127
|
-
this.app.use('/api/v1/runtimes', runtimesRouter); // public for login screen
|
|
113
|
+
// API routes
|
|
114
|
+
this.app.use('/api/v1/send', sendRouter);
|
|
115
|
+
this.app.use('/api/v1/sessions', sessionsRouter);
|
|
128
116
|
this.app.use('/api/v1/models', modelsRouter);
|
|
129
|
-
this.app.use('/api/v1/
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
this.app.use('/api/v1/
|
|
133
|
-
this.app.use('/api/v1/
|
|
134
|
-
|
|
135
|
-
|
|
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
|
|
117
|
+
this.app.use('/api/v1/status', statusRouter);
|
|
118
|
+
this.app.use('/api/v1/hosts', hostsRouter);
|
|
119
|
+
this.app.use('/api/v1/tmux', tmuxRouter);
|
|
120
|
+
this.app.use('/api/v1/launchers', launchersRouter);
|
|
121
|
+
this.app.use('/api/v1/runtime', runtimeRouter);
|
|
122
|
+
|
|
123
|
+
// Serve frontend
|
|
141
124
|
const frontendDist = path.join(__dirname, '../../frontend/dist');
|
|
142
125
|
if (fs.existsSync(frontendDist)) {
|
|
143
126
|
this.app.use(express.static(frontendDist));
|
|
@@ -148,13 +131,13 @@ class NexusCrewServer {
|
|
|
148
131
|
});
|
|
149
132
|
}
|
|
150
133
|
|
|
151
|
-
//
|
|
134
|
+
// Error handler
|
|
152
135
|
this.app.use((err, req, res, next) => {
|
|
153
136
|
console.error('[Error]', err.message);
|
|
154
137
|
res.status(500).json({ error: err.message });
|
|
155
138
|
});
|
|
156
139
|
|
|
157
|
-
//
|
|
140
|
+
// Start HTTP server
|
|
158
141
|
this.server = http.createServer(this.app);
|
|
159
142
|
|
|
160
143
|
return new Promise((resolve) => {
|
|
@@ -165,13 +148,13 @@ class NexusCrewServer {
|
|
|
165
148
|
console.log('╚══════════════════════════════════════════════╝');
|
|
166
149
|
console.log('');
|
|
167
150
|
console.log(` HTTP: http://localhost:${this.port}`);
|
|
168
|
-
console.log(` tmux:
|
|
169
|
-
console.log(` Hosts: ${
|
|
151
|
+
console.log(` tmux: ${tmuxManager.listSessions().length} active sessions`);
|
|
152
|
+
console.log(` Hosts: ${hosts.map(h => h.name).join(', ')}`);
|
|
170
153
|
console.log('');
|
|
171
154
|
|
|
172
|
-
const
|
|
173
|
-
for (const
|
|
174
|
-
console.log(` ${
|
|
155
|
+
const discovery = engineDiscovery.discover();
|
|
156
|
+
for (const launcher of discovery.launchers) {
|
|
157
|
+
console.log(` OK ${launcher.label} [${launcher.kind}]`);
|
|
175
158
|
}
|
|
176
159
|
console.log('');
|
|
177
160
|
|
|
@@ -182,10 +165,6 @@ class NexusCrewServer {
|
|
|
182
165
|
|
|
183
166
|
stop() {
|
|
184
167
|
if (this.server) {
|
|
185
|
-
const appLocals = this.app?.locals || {};
|
|
186
|
-
appLocals.logWatcher?.unwatchAll?.();
|
|
187
|
-
appLocals.remotePaneWatcher?.unwatchAll?.();
|
|
188
|
-
saveDb();
|
|
189
168
|
this.server.close();
|
|
190
169
|
}
|
|
191
170
|
}
|