@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
|
@@ -27,7 +27,10 @@ class LogWatcher extends EventEmitter {
|
|
|
27
27
|
|
|
28
28
|
const state = {
|
|
29
29
|
offset: 0,
|
|
30
|
-
|
|
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
|
-
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
|
|
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
|
|
2
|
+
* SessionStore — SQLite persistence for nexuscrew
|
|
3
3
|
*
|
|
4
4
|
* Schema:
|
|
5
|
-
* - conversations: id, title, engine, model,
|
|
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
|
-
|
|
17
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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', '
|
|
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
|
-
|
|
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
|
-
|
|
67
|
-
return stmt.all(...params);
|
|
110
|
+
return this.db.prepare(sql).all(...params);
|
|
68
111
|
}
|
|
69
112
|
|
|
70
113
|
deleteConversation(id) {
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
80
|
-
|
|
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,
|
|
85
|
-
const convs = this.listConversations({ workspace,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
139
|
-
return stmt.all(...params);
|
|
162
|
+
return this.db.prepare(sql).all(...params);
|
|
140
163
|
}
|
|
141
164
|
|
|
142
165
|
getMessageCount(conversationId) {
|
|
143
|
-
|
|
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
|
|
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.
|
|
158
|
-
const exists = tmuxManager.
|
|
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
|
-
|
|
194
|
+
this.db.close();
|
|
169
195
|
}
|
|
170
196
|
}
|
|
171
197
|
|