@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
@@ -27,7 +27,10 @@ class LogWatcher extends EventEmitter {
27
27
 
28
28
  const state = {
29
29
  offset: 0,
30
- ...this.createParserState(engine, conversationId)
30
+ engine,
31
+ conversationId,
32
+ buffer: '',
33
+ parser: this._getParser(engine)
31
34
  };
32
35
 
33
36
  // Read existing content (in case log already has data)
@@ -95,60 +98,39 @@ class LogWatcher extends EventEmitter {
95
98
 
96
99
  state.offset = stat.size;
97
100
  const chunk = buf.toString('utf8');
98
- this.processTextChunk(chunk, state);
101
+ state.buffer += chunk;
102
+
103
+ // Process complete lines
104
+ this._processBuffer(state);
99
105
  } catch (err) {
100
106
  // File might be temporarily unavailable
101
107
  }
102
108
  }
103
109
 
104
- createParserState(engine, conversationId) {
105
- return {
106
- engine,
107
- conversationId,
108
- buffer: '',
109
- parser: this._getParser(engine)
110
- };
111
- }
112
-
113
- processTextChunk(chunk, state) {
114
- state.buffer += chunk;
115
- this._processBuffer(state);
116
- }
117
-
118
- flushState(state) {
119
- if (!state.buffer?.trim()) return;
120
- const line = state.buffer;
121
- state.buffer = '';
122
- this._emitParsedLine(line, state);
123
- }
124
-
125
110
  _processBuffer(state) {
126
111
  const lines = state.buffer.split('\n');
127
112
  state.buffer = lines.pop() || ''; // Keep incomplete line in buffer
128
113
 
129
114
  for (const line of lines) {
130
- this._emitParsedLine(line, state);
131
- }
132
- }
133
-
134
- _emitParsedLine(line, state) {
135
- if (!line.trim()) return;
136
- try {
137
- const events = state.parser(line, state);
138
- for (const event of events) {
115
+ if (!line.trim()) continue;
116
+ try {
117
+ const events = state.parser(line, state);
118
+ for (const event of events) {
119
+ this.emit('data', {
120
+ ...event,
121
+ conversationId: state.conversationId,
122
+ engine: state.engine
123
+ });
124
+ }
125
+ } catch {
126
+ // Emit raw text if parser fails
139
127
  this.emit('data', {
140
- ...event,
128
+ type: 'raw',
129
+ content: line,
141
130
  conversationId: state.conversationId,
142
131
  engine: state.engine
143
132
  });
144
133
  }
145
- } catch {
146
- this.emit('data', {
147
- type: 'raw',
148
- content: line,
149
- conversationId: state.conversationId,
150
- engine: state.engine
151
- });
152
134
  }
153
135
  }
154
136
 
@@ -1,49 +1,94 @@
1
1
  /**
2
- * SessionStore — SQLite persistence for nexuscrew (sql.js version)
2
+ * SessionStore — SQLite persistence for nexuscrew
3
3
  *
4
4
  * Schema:
5
- * - conversations: id, title, engine, model, tmux_window, log_path, workspace, host, status, pinned, created_at, updated_at
5
+ * - conversations: id, title, engine, model, tmux_session, log_path, workspace, host, status, launcher_id, created_at, updated_at
6
6
  * - messages: id, conversation_id, role, content, engine, model, tokens_in, tokens_out, created_at
7
7
  */
8
8
 
9
+ const Database = require('better-sqlite3');
9
10
  const path = require('path');
10
11
  const fs = require('fs');
11
12
  const { v4: uuidv4 } = require('uuid');
12
- const { prepare, saveDb } = require('../server/db/adapter');
13
13
 
14
14
  class SessionStore {
15
- constructor() {
16
- // DB is managed by adapter - no direct connection here
17
- this.initialized = false;
15
+ constructor(dbPath) {
16
+ const dir = path.dirname(dbPath);
17
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
18
+
19
+ this.db = new Database(dbPath);
20
+ this.db.pragma('journal_mode = WAL');
21
+ this.db.pragma('foreign_keys = ON');
22
+ this._migrate();
23
+ }
24
+
25
+ _migrate() {
26
+ this.db.exec(`
27
+ CREATE TABLE IF NOT EXISTS conversations (
28
+ id TEXT PRIMARY KEY,
29
+ title TEXT DEFAULT 'New Chat',
30
+ engine TEXT,
31
+ model TEXT,
32
+ tmux_window TEXT,
33
+ tmux_session TEXT,
34
+ log_path TEXT,
35
+ workspace TEXT DEFAULT '',
36
+ host TEXT DEFAULT 'local',
37
+ launcher_id TEXT,
38
+ status TEXT DEFAULT 'active',
39
+ created_at INTEGER DEFAULT (unixepoch()),
40
+ updated_at INTEGER DEFAULT (unixepoch())
41
+ );
42
+
43
+ CREATE TABLE IF NOT EXISTS messages (
44
+ id TEXT PRIMARY KEY,
45
+ conversation_id TEXT NOT NULL,
46
+ role TEXT NOT NULL,
47
+ content TEXT DEFAULT '',
48
+ engine TEXT,
49
+ model TEXT,
50
+ tokens_in INTEGER DEFAULT 0,
51
+ tokens_out INTEGER DEFAULT 0,
52
+ created_at INTEGER DEFAULT (unixepoch()),
53
+ FOREIGN KEY (conversation_id) REFERENCES conversations(id)
54
+ );
55
+
56
+ CREATE INDEX IF NOT EXISTS idx_messages_conv ON messages(conversation_id, created_at);
57
+ CREATE INDEX IF NOT EXISTS idx_conversations_status ON conversations(status, updated_at);
58
+ `);
59
+
60
+ this._ensureColumn('conversations', 'tmux_session', 'TEXT');
61
+ this._ensureColumn('conversations', 'launcher_id', 'TEXT');
62
+ this.db.exec('CREATE INDEX IF NOT EXISTS idx_conversations_tmux_session ON conversations(tmux_session)');
18
63
  }
19
64
 
20
- async init() {
21
- // Adapter is already initialized by server.js
22
- this.initialized = true;
65
+ _ensureColumn(tableName, columnName, definition) {
66
+ const columns = this.db.prepare(`PRAGMA table_info(${tableName})`).all();
67
+ if (!columns.find((column) => column.name === columnName)) {
68
+ this.db.exec(`ALTER TABLE ${tableName} ADD COLUMN ${columnName} ${definition}`);
69
+ }
23
70
  }
24
71
 
25
72
  // --- Conversations ---
26
73
 
27
- createConversation({ engine, model, workspace, host, tmuxWindow, logPath } = {}) {
74
+ createConversation({ engine, model, workspace, host, tmuxWindow, tmuxSession, logPath, launcherId } = {}) {
28
75
  const id = uuidv4();
29
- const stmt = prepare(`
30
- INSERT INTO conversations (id, engine, model, workspace, host, tmux_window, log_path)
31
- VALUES (?, ?, ?, ?, ?, ?, ?)
32
- `);
33
- stmt.run(id, engine || 'claude', model || null, workspace || '', host || 'local', tmuxWindow || null, logPath || null);
76
+ this.db.prepare(`
77
+ INSERT INTO conversations (id, engine, model, workspace, host, tmux_window, tmux_session, log_path, launcher_id)
78
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
79
+ `).run(id, engine || null, model || null, workspace || '', host || 'local', tmuxWindow || null, tmuxSession || null, logPath || null, launcherId || null);
34
80
  return this.getConversation(id);
35
81
  }
36
82
 
37
83
  getConversation(id) {
38
- const stmt = prepare('SELECT * FROM conversations WHERE id = ?');
39
- return stmt.get(id);
84
+ return this.db.prepare('SELECT * FROM conversations WHERE id = ?').get(id);
40
85
  }
41
86
 
42
87
  updateConversation(id, updates) {
43
88
  const fields = [];
44
89
  const values = [];
45
90
  for (const [key, value] of Object.entries(updates)) {
46
- if (['title', 'engine', 'model', 'tmux_window', 'log_path', 'workspace', 'host', 'status', 'pinned'].includes(key)) {
91
+ if (['title', 'engine', 'model', 'tmux_window', 'tmux_session', 'log_path', 'workspace', 'host', 'status', 'launcher_id'].includes(key)) {
47
92
  fields.push(`${key} = ?`);
48
93
  values.push(value);
49
94
  }
@@ -51,8 +96,7 @@ class SessionStore {
51
96
  if (fields.length === 0) return;
52
97
  fields.push('updated_at = unixepoch()');
53
98
  values.push(id);
54
- const stmt = prepare(`UPDATE conversations SET ${fields.join(', ')} WHERE id = ?`);
55
- stmt.run(...values);
99
+ this.db.prepare(`UPDATE conversations SET ${fields.join(', ')} WHERE id = ?`).run(...values);
56
100
  }
57
101
 
58
102
  listConversations({ workspace, host, status, limit = 50 } = {}) {
@@ -63,26 +107,26 @@ class SessionStore {
63
107
  if (status) { sql += ' AND status = ?'; params.push(status); }
64
108
  sql += ' ORDER BY updated_at DESC LIMIT ?';
65
109
  params.push(limit);
66
- const stmt = prepare(sql);
67
- return stmt.all(...params);
110
+ return this.db.prepare(sql).all(...params);
68
111
  }
69
112
 
70
113
  deleteConversation(id) {
71
- const stmt1 = prepare('DELETE FROM messages WHERE conversation_id = ?');
72
- stmt1.run(id);
73
- const stmt2 = prepare('DELETE FROM conversations WHERE id = ?');
74
- stmt2.run(id);
114
+ this.db.prepare('DELETE FROM messages WHERE conversation_id = ?').run(id);
115
+ this.db.prepare('DELETE FROM conversations WHERE id = ?').run(id);
75
116
  }
76
117
 
77
118
  /** Find conversation by tmux window name */
78
119
  findByWindow(windowName) {
79
- const stmt = prepare('SELECT * FROM conversations WHERE tmux_window = ?');
80
- return stmt.get(windowName);
120
+ return this.db.prepare('SELECT * FROM conversations WHERE tmux_window = ?').get(windowName);
121
+ }
122
+
123
+ findByTmuxSession(sessionName) {
124
+ return this.db.prepare('SELECT * FROM conversations WHERE tmux_session = ?').get(sessionName);
81
125
  }
82
126
 
83
127
  /** Get conversations grouped by date */
84
- listGroupedByDate({ workspace, host, limit = 100 } = {}) {
85
- const convs = this.listConversations({ workspace, host, limit });
128
+ listGroupedByDate({ workspace, limit = 100 } = {}) {
129
+ const convs = this.listConversations({ workspace, limit });
86
130
  const grouped = {};
87
131
  for (const c of convs) {
88
132
  const date = new Date(c.updated_at * 1000).toISOString().split('T')[0];
@@ -92,37 +136,17 @@ class SessionStore {
92
136
  return grouped;
93
137
  }
94
138
 
95
- /** Toggle conversation pinned status */
96
- pinConversation(id) {
97
- const stmt = prepare('SELECT pinned FROM conversations WHERE id = ?');
98
- const conv = stmt.get(id);
99
- if (!conv) return;
100
-
101
- const newPinned = conv.pinned ? 0 : 1;
102
- const updateStmt = prepare('UPDATE conversations SET pinned = ? WHERE id = ?');
103
- updateStmt.run(newPinned, id);
104
- return newPinned === 1;
105
- }
106
-
107
- /** Search conversations by title */
108
- searchConversations(query) {
109
- const stmt = prepare('SELECT * FROM conversations WHERE title LIKE ? ORDER BY updated_at DESC LIMIT 50');
110
- return stmt.all(`%${query}%`);
111
- }
112
-
113
139
  // --- Messages ---
114
140
 
115
141
  addMessage({ conversationId, role, content, engine, model, tokensIn, tokensOut } = {}) {
116
142
  const id = uuidv4();
117
- const stmt = prepare(`
143
+ this.db.prepare(`
118
144
  INSERT INTO messages (id, conversation_id, role, content, engine, model, tokens_in, tokens_out)
119
145
  VALUES (?, ?, ?, ?, ?, ?, ?, ?)
120
- `);
121
- stmt.run(id, conversationId, role, content || '', engine || null, model || null, tokensIn || 0, tokensOut || 0);
146
+ `).run(id, conversationId, role, content || '', engine || null, model || null, tokensIn || 0, tokensOut || 0);
122
147
 
123
148
  // Update conversation timestamp
124
- const updateStmt = prepare('UPDATE conversations SET updated_at = unixepoch() WHERE id = ?');
125
- updateStmt.run(conversationId);
149
+ this.db.prepare('UPDATE conversations SET updated_at = unixepoch() WHERE id = ?').run(conversationId);
126
150
  return id;
127
151
  }
128
152
 
@@ -135,13 +159,11 @@ class SessionStore {
135
159
  }
136
160
  sql += ' ORDER BY created_at ASC LIMIT ?';
137
161
  params.push(limit);
138
- const stmt = prepare(sql);
139
- return stmt.all(...params);
162
+ return this.db.prepare(sql).all(...params);
140
163
  }
141
164
 
142
165
  getMessageCount(conversationId) {
143
- const stmt = prepare('SELECT COUNT(*) as count FROM messages WHERE conversation_id = ?');
144
- return stmt.get(conversationId).count;
166
+ return this.db.prepare('SELECT COUNT(*) as count FROM messages WHERE conversation_id = ?').get(conversationId).count;
145
167
  }
146
168
 
147
169
  // --- Recovery ---
@@ -151,11 +173,15 @@ class SessionStore {
151
173
  * Mark conversations as 'dead' if their tmux window no longer exists
152
174
  */
153
175
  syncWithTmux(tmuxManager) {
154
- const stmt = prepare("SELECT * FROM conversations WHERE status = 'active'");
155
- const active = stmt.all();
176
+ const active = this.db.prepare("SELECT * FROM conversations WHERE status = 'active'").all();
156
177
  for (const conv of active) {
157
- if (conv.tmux_window) {
158
- const exists = tmuxManager.hasWindow(conv.tmux_window, conv.host || 'local');
178
+ if (conv.tmux_session) {
179
+ const exists = tmuxManager.hasSession(conv.tmux_session, conv.host !== 'local' ? conv.host : null);
180
+ if (!exists) {
181
+ this.updateConversation(conv.id, { status: 'dead' });
182
+ }
183
+ } else if (conv.tmux_window) {
184
+ const exists = tmuxManager.hasSession(conv.tmux_window, conv.host !== 'local' ? conv.host : null);
159
185
  if (!exists) {
160
186
  this.updateConversation(conv.id, { status: 'dead' });
161
187
  }
@@ -165,7 +191,7 @@ class SessionStore {
165
191
 
166
192
  /** Close DB connection */
167
193
  close() {
168
- saveDb();
194
+ this.db.close();
169
195
  }
170
196
  }
171
197