@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.
Files changed (52) hide show
  1. package/README.md +122 -67
  2. package/bin/nexuscrew.js +194 -470
  3. package/frontend/dist/assets/{index-BG1N7bSL.js → index-BsldfYnr.js} +1704 -1711
  4. package/frontend/dist/assets/index-DcQGg3dp.css +1 -0
  5. package/frontend/dist/index.html +3 -11
  6. package/lib/server/routes/hosts.js +22 -12
  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 -258
  11. package/lib/server/routes/sessions.js +13 -42
  12. package/lib/server/routes/status.js +18 -22
  13. package/lib/server/routes/tmux.js +88 -0
  14. package/lib/server/server.js +60 -81
  15. package/lib/services/engine-discovery.js +192 -445
  16. package/lib/services/log-watcher.js +22 -40
  17. package/lib/services/session-store.js +88 -62
  18. package/lib/services/tmux-manager.js +111 -173
  19. package/package.json +5 -9
  20. package/frontend/dist/apple-touch-icon.png +0 -0
  21. package/frontend/dist/assets/index-CiAtinNP.css +0 -1
  22. package/frontend/dist/favicon.svg +0 -20
  23. package/frontend/dist/icon-192.png +0 -0
  24. package/frontend/dist/icon-512.png +0 -0
  25. package/frontend/dist/site.webmanifest +0 -29
  26. package/lib/config/hosts.js +0 -67
  27. package/lib/config/manager.js +0 -379
  28. package/lib/config/models.js +0 -408
  29. package/lib/server/db/adapter.js +0 -274
  30. package/lib/server/db/drivers/sql-js.js +0 -75
  31. package/lib/server/db/migrate.js +0 -174
  32. package/lib/server/db/migrations/001_base_schema.sql +0 -70
  33. package/lib/server/middleware/auth.js +0 -134
  34. package/lib/server/middleware/rate-limit.js +0 -63
  35. package/lib/server/models/User.js +0 -128
  36. package/lib/server/routes/auth.js +0 -168
  37. package/lib/server/routes/keys.js +0 -28
  38. package/lib/server/routes/runtimes.js +0 -34
  39. package/lib/server/routes/speech.js +0 -46
  40. package/lib/server/routes/upload.js +0 -135
  41. package/lib/server/routes/wake-lock.js +0 -95
  42. package/lib/server/routes/workspaces.js +0 -101
  43. package/lib/server/services/attachment-manager.js +0 -57
  44. package/lib/server/services/context-bridge.js +0 -425
  45. package/lib/server/services/runtime-manager.js +0 -462
  46. package/lib/server/services/speech-manager.js +0 -76
  47. package/lib/server/services/summary-generator.js +0 -309
  48. package/lib/server/services/workspace-manager.js +0 -79
  49. package/lib/services/remote-pane-watcher.js +0 -155
  50. package/lib/setup/postinstall.js +0 -38
  51. package/lib/utils/paths.js +0 -124
  52. 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;
@@ -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 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');
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.configDir = config.configDir || path.join(process.env.HOME, '.nexuscrew');
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
- return getConfig();
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
- 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');
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
- // 3. Init services
64
+ async start() {
65
+ const hosts = this._loadHosts();
66
66
  const tmuxManager = new TmuxManager({
67
- tmuxSession: this.config.tmuxSession || 'nexuscrew',
68
- logDir: this.config.logDir || path.join(this.configDir, 'logs')
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(this.config.providersPath);
72
- const sessionStore = new SessionStore();
73
- await sessionStore.init();
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
- // 4. Middleware
88
+ // Middleware
96
89
  this.app.use(cors());
97
90
  this.app.use(express.json({ limit: '10mb' }));
98
91
 
99
- // 5. Health check (public)
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: '0.2.1',
105
- tmux: tmuxManager.hasSession(),
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
- // 6. Inject services into request
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
- // 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
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/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
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
- // 10. Error handler
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
- // 11. Start HTTP server
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: session "${tmuxManager.sessionName}" (${tmuxManager.listWindows().length} windows)`);
169
- console.log(` Hosts: ${tmuxManager.getAllHosts().map(h => h.name).join(', ')}`);
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 engines = engineDiscovery.discover();
173
- for (const [name, info] of Object.entries(engines)) {
174
- console.log(` ${info.available ? 'OK' : ' '} ${name}${info.aliases.length ? ` (+${info.aliases.length} aliases)` : ''}`);
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
  }