@mmmbuto/nexuscrew 0.1.0-beta.1 → 0.2.1
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 +67 -120
- package/bin/nexuscrew.js +468 -264
- package/frontend/dist/apple-touch-icon.png +0 -0
- package/frontend/dist/assets/{index-C7bAndew.js → index-BG1N7bSL.js} +1710 -1702
- package/frontend/dist/assets/index-CiAtinNP.css +1 -0
- package/frontend/dist/favicon.svg +20 -0
- package/frontend/dist/icon-192.png +0 -0
- package/frontend/dist/icon-512.png +0 -0
- package/frontend/dist/index.html +11 -3
- package/frontend/dist/site.webmanifest +29 -0
- package/lib/config/hosts.js +67 -0
- package/lib/config/manager.js +379 -0
- package/lib/config/models.js +408 -0
- package/lib/server/db/adapter.js +274 -0
- package/lib/server/db/drivers/sql-js.js +75 -0
- package/lib/server/db/migrate.js +174 -0
- package/lib/server/db/migrations/001_base_schema.sql +70 -0
- package/lib/server/middleware/auth.js +134 -0
- package/lib/server/middleware/rate-limit.js +63 -0
- package/lib/server/models/User.js +128 -0
- package/lib/server/routes/auth.js +168 -0
- package/lib/server/routes/hosts.js +11 -20
- package/lib/server/routes/keys.js +28 -0
- package/lib/server/routes/models.js +59 -4
- package/lib/server/routes/runtimes.js +34 -0
- package/lib/server/routes/send.js +76 -12
- package/lib/server/routes/sessions.js +39 -10
- package/lib/server/routes/speech.js +46 -0
- package/lib/server/routes/status.js +8 -6
- package/lib/server/routes/upload.js +135 -0
- package/lib/server/routes/wake-lock.js +95 -0
- package/lib/server/routes/workspaces.js +101 -0
- package/lib/server/server.js +66 -33
- package/lib/server/services/attachment-manager.js +57 -0
- package/lib/server/services/context-bridge.js +425 -0
- package/lib/server/services/runtime-manager.js +462 -0
- package/lib/server/services/speech-manager.js +76 -0
- package/lib/server/services/summary-generator.js +309 -0
- package/lib/server/services/workspace-manager.js +79 -0
- package/lib/services/engine-discovery.js +198 -13
- package/lib/services/log-watcher.js +40 -22
- package/lib/services/remote-pane-watcher.js +155 -0
- package/lib/services/session-store.js +60 -64
- package/lib/services/tmux-manager.js +127 -116
- package/lib/setup/postinstall.js +38 -0
- package/lib/utils/paths.js +124 -0
- package/lib/utils/termux.js +182 -0
- package/package.json +8 -4
- package/frontend/dist/assets/index-OENqI1_9.css +0 -1
|
@@ -27,10 +27,7 @@ class LogWatcher extends EventEmitter {
|
|
|
27
27
|
|
|
28
28
|
const state = {
|
|
29
29
|
offset: 0,
|
|
30
|
-
engine,
|
|
31
|
-
conversationId,
|
|
32
|
-
buffer: '',
|
|
33
|
-
parser: this._getParser(engine)
|
|
30
|
+
...this.createParserState(engine, conversationId)
|
|
34
31
|
};
|
|
35
32
|
|
|
36
33
|
// Read existing content (in case log already has data)
|
|
@@ -98,39 +95,60 @@ class LogWatcher extends EventEmitter {
|
|
|
98
95
|
|
|
99
96
|
state.offset = stat.size;
|
|
100
97
|
const chunk = buf.toString('utf8');
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
// Process complete lines
|
|
104
|
-
this._processBuffer(state);
|
|
98
|
+
this.processTextChunk(chunk, state);
|
|
105
99
|
} catch (err) {
|
|
106
100
|
// File might be temporarily unavailable
|
|
107
101
|
}
|
|
108
102
|
}
|
|
109
103
|
|
|
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
|
+
|
|
110
125
|
_processBuffer(state) {
|
|
111
126
|
const lines = state.buffer.split('\n');
|
|
112
127
|
state.buffer = lines.pop() || ''; // Keep incomplete line in buffer
|
|
113
128
|
|
|
114
129
|
for (const line of lines) {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
}
|
|
125
|
-
} catch {
|
|
126
|
-
// Emit raw text if parser fails
|
|
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) {
|
|
127
139
|
this.emit('data', {
|
|
128
|
-
|
|
129
|
-
content: line,
|
|
140
|
+
...event,
|
|
130
141
|
conversationId: state.conversationId,
|
|
131
142
|
engine: state.engine
|
|
132
143
|
});
|
|
133
144
|
}
|
|
145
|
+
} catch {
|
|
146
|
+
this.emit('data', {
|
|
147
|
+
type: 'raw',
|
|
148
|
+
content: line,
|
|
149
|
+
conversationId: state.conversationId,
|
|
150
|
+
engine: state.engine
|
|
151
|
+
});
|
|
134
152
|
}
|
|
135
153
|
}
|
|
136
154
|
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
const { EventEmitter } = require('events');
|
|
2
|
+
const LogWatcher = require('./log-watcher');
|
|
3
|
+
|
|
4
|
+
function normalizeSnapshot(text = '') {
|
|
5
|
+
return String(text).replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function commonPrefixLength(a = '', b = '') {
|
|
9
|
+
const max = Math.min(a.length, b.length);
|
|
10
|
+
let index = 0;
|
|
11
|
+
while (index < max && a[index] === b[index]) {
|
|
12
|
+
index += 1;
|
|
13
|
+
}
|
|
14
|
+
return index;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
class RemotePaneWatcher extends EventEmitter {
|
|
18
|
+
constructor(tmux, options = {}) {
|
|
19
|
+
super();
|
|
20
|
+
this.tmux = tmux;
|
|
21
|
+
this.parserAdapter = new LogWatcher();
|
|
22
|
+
this.watchers = new Map();
|
|
23
|
+
this.pollMs = options.pollMs || 400;
|
|
24
|
+
this.idleMs = options.idleMs || 4500;
|
|
25
|
+
this.timeoutMs = options.timeoutMs || 120000;
|
|
26
|
+
|
|
27
|
+
this.parserAdapter.on('data', (event) => this.emit('data', event));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
watch({ conversationId, engine, host, windowName, promptText = '', initialSnapshot = '' }) {
|
|
31
|
+
const key = `${host?.name || host || 'local'}:${windowName}:${conversationId}`;
|
|
32
|
+
if (this.watchers.has(key)) return key;
|
|
33
|
+
|
|
34
|
+
const parserState = this.parserAdapter.createParserState(engine, conversationId);
|
|
35
|
+
const state = {
|
|
36
|
+
key,
|
|
37
|
+
conversationId,
|
|
38
|
+
engine,
|
|
39
|
+
host,
|
|
40
|
+
windowName,
|
|
41
|
+
parserState,
|
|
42
|
+
promptText: String(promptText || ''),
|
|
43
|
+
lastSnapshot: normalizeSnapshot(initialSnapshot),
|
|
44
|
+
startedAt: Date.now(),
|
|
45
|
+
lastChangeAt: Date.now(),
|
|
46
|
+
lastEmitAt: 0,
|
|
47
|
+
stopped: false,
|
|
48
|
+
sawAnyOutput: false
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const intervalId = setInterval(() => this._poll(state), this.pollMs);
|
|
52
|
+
state.intervalId = intervalId;
|
|
53
|
+
this.watchers.set(key, state);
|
|
54
|
+
return key;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
unwatch(key) {
|
|
58
|
+
const state = this.watchers.get(key);
|
|
59
|
+
if (!state) return;
|
|
60
|
+
clearInterval(state.intervalId);
|
|
61
|
+
state.stopped = true;
|
|
62
|
+
this.watchers.delete(key);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
unwatchByConversation(conversationId) {
|
|
66
|
+
for (const [key, state] of this.watchers.entries()) {
|
|
67
|
+
if (state.conversationId === conversationId) {
|
|
68
|
+
this.unwatch(key);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
unwatchAll() {
|
|
74
|
+
for (const key of this.watchers.keys()) {
|
|
75
|
+
this.unwatch(key);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
_poll(state) {
|
|
80
|
+
if (state.stopped) return;
|
|
81
|
+
|
|
82
|
+
if (Date.now() - state.startedAt > this.timeoutMs) {
|
|
83
|
+
this._finish(state, true);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const snapshot = normalizeSnapshot(
|
|
88
|
+
this.tmux.capturePane(state.windowName, state.host, 250)
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
if (!snapshot) {
|
|
92
|
+
if (Date.now() - state.lastChangeAt > this.idleMs) {
|
|
93
|
+
this._finish(state, true);
|
|
94
|
+
}
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (snapshot === state.lastSnapshot) {
|
|
99
|
+
if (state.sawAnyOutput && Date.now() - state.lastChangeAt > this.idleMs) {
|
|
100
|
+
this._finish(state, true);
|
|
101
|
+
}
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const prefixLength = commonPrefixLength(state.lastSnapshot, snapshot);
|
|
106
|
+
let delta = snapshot.slice(prefixLength);
|
|
107
|
+
state.lastSnapshot = snapshot;
|
|
108
|
+
state.lastChangeAt = Date.now();
|
|
109
|
+
|
|
110
|
+
if (!delta.trim()) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
delta = this._stripPromptEcho(delta, state);
|
|
115
|
+
if (!delta.trim()) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
state.sawAnyOutput = true;
|
|
120
|
+
this.parserAdapter.processTextChunk(delta, state.parserState);
|
|
121
|
+
state.lastEmitAt = Date.now();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
_stripPromptEcho(delta, state) {
|
|
125
|
+
if (!state.promptText) return delta;
|
|
126
|
+
|
|
127
|
+
const prompt = state.promptText.trim();
|
|
128
|
+
if (!prompt) return delta;
|
|
129
|
+
|
|
130
|
+
if (delta.includes(prompt)) {
|
|
131
|
+
return delta.replace(prompt, '');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const compact = delta.trim();
|
|
135
|
+
if (prompt.startsWith(compact) && compact.length > 10) {
|
|
136
|
+
return '';
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return delta;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
_finish(state, emitDone = false) {
|
|
143
|
+
this.parserAdapter.flushState(state.parserState);
|
|
144
|
+
if (emitDone) {
|
|
145
|
+
this.emit('data', {
|
|
146
|
+
type: 'done',
|
|
147
|
+
conversationId: state.conversationId,
|
|
148
|
+
engine: state.engine
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
this.unwatch(state.key);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
module.exports = RemotePaneWatcher;
|
|
@@ -1,81 +1,49 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* SessionStore — SQLite persistence for nexuscrew
|
|
2
|
+
* SessionStore — SQLite persistence for nexuscrew (sql.js version)
|
|
3
3
|
*
|
|
4
4
|
* Schema:
|
|
5
|
-
* - conversations: id, title, engine, model, tmux_window, log_path, workspace, host, status, created_at, updated_at
|
|
5
|
+
* - conversations: id, title, engine, model, tmux_window, log_path, workspace, host, status, pinned, 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');
|
|
10
9
|
const path = require('path');
|
|
11
10
|
const fs = require('fs');
|
|
12
11
|
const { v4: uuidv4 } = require('uuid');
|
|
12
|
+
const { prepare, saveDb } = require('../server/db/adapter');
|
|
13
13
|
|
|
14
14
|
class SessionStore {
|
|
15
|
-
constructor(
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
this.
|
|
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 NOT NULL DEFAULT 'claude',
|
|
31
|
-
model TEXT,
|
|
32
|
-
tmux_window TEXT,
|
|
33
|
-
log_path TEXT,
|
|
34
|
-
workspace TEXT DEFAULT '',
|
|
35
|
-
host TEXT DEFAULT 'local',
|
|
36
|
-
status TEXT DEFAULT 'active',
|
|
37
|
-
created_at INTEGER DEFAULT (unixepoch()),
|
|
38
|
-
updated_at INTEGER DEFAULT (unixepoch())
|
|
39
|
-
);
|
|
40
|
-
|
|
41
|
-
CREATE TABLE IF NOT EXISTS messages (
|
|
42
|
-
id TEXT PRIMARY KEY,
|
|
43
|
-
conversation_id TEXT NOT NULL,
|
|
44
|
-
role TEXT NOT NULL,
|
|
45
|
-
content TEXT DEFAULT '',
|
|
46
|
-
engine TEXT,
|
|
47
|
-
model TEXT,
|
|
48
|
-
tokens_in INTEGER DEFAULT 0,
|
|
49
|
-
tokens_out INTEGER DEFAULT 0,
|
|
50
|
-
created_at INTEGER DEFAULT (unixepoch()),
|
|
51
|
-
FOREIGN KEY (conversation_id) REFERENCES conversations(id)
|
|
52
|
-
);
|
|
53
|
-
|
|
54
|
-
CREATE INDEX IF NOT EXISTS idx_messages_conv ON messages(conversation_id, created_at);
|
|
55
|
-
CREATE INDEX IF NOT EXISTS idx_conversations_status ON conversations(status, updated_at);
|
|
56
|
-
`);
|
|
15
|
+
constructor() {
|
|
16
|
+
// DB is managed by adapter - no direct connection here
|
|
17
|
+
this.initialized = false;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async init() {
|
|
21
|
+
// Adapter is already initialized by server.js
|
|
22
|
+
this.initialized = true;
|
|
57
23
|
}
|
|
58
24
|
|
|
59
25
|
// --- Conversations ---
|
|
60
26
|
|
|
61
27
|
createConversation({ engine, model, workspace, host, tmuxWindow, logPath } = {}) {
|
|
62
28
|
const id = uuidv4();
|
|
63
|
-
|
|
29
|
+
const stmt = prepare(`
|
|
64
30
|
INSERT INTO conversations (id, engine, model, workspace, host, tmux_window, log_path)
|
|
65
31
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
66
|
-
`)
|
|
32
|
+
`);
|
|
33
|
+
stmt.run(id, engine || 'claude', model || null, workspace || '', host || 'local', tmuxWindow || null, logPath || null);
|
|
67
34
|
return this.getConversation(id);
|
|
68
35
|
}
|
|
69
36
|
|
|
70
37
|
getConversation(id) {
|
|
71
|
-
|
|
38
|
+
const stmt = prepare('SELECT * FROM conversations WHERE id = ?');
|
|
39
|
+
return stmt.get(id);
|
|
72
40
|
}
|
|
73
41
|
|
|
74
42
|
updateConversation(id, updates) {
|
|
75
43
|
const fields = [];
|
|
76
44
|
const values = [];
|
|
77
45
|
for (const [key, value] of Object.entries(updates)) {
|
|
78
|
-
if (['title', 'engine', 'model', 'tmux_window', 'log_path', 'workspace', 'host', 'status'].includes(key)) {
|
|
46
|
+
if (['title', 'engine', 'model', 'tmux_window', 'log_path', 'workspace', 'host', 'status', 'pinned'].includes(key)) {
|
|
79
47
|
fields.push(`${key} = ?`);
|
|
80
48
|
values.push(value);
|
|
81
49
|
}
|
|
@@ -83,7 +51,8 @@ class SessionStore {
|
|
|
83
51
|
if (fields.length === 0) return;
|
|
84
52
|
fields.push('updated_at = unixepoch()');
|
|
85
53
|
values.push(id);
|
|
86
|
-
|
|
54
|
+
const stmt = prepare(`UPDATE conversations SET ${fields.join(', ')} WHERE id = ?`);
|
|
55
|
+
stmt.run(...values);
|
|
87
56
|
}
|
|
88
57
|
|
|
89
58
|
listConversations({ workspace, host, status, limit = 50 } = {}) {
|
|
@@ -94,22 +63,26 @@ class SessionStore {
|
|
|
94
63
|
if (status) { sql += ' AND status = ?'; params.push(status); }
|
|
95
64
|
sql += ' ORDER BY updated_at DESC LIMIT ?';
|
|
96
65
|
params.push(limit);
|
|
97
|
-
|
|
66
|
+
const stmt = prepare(sql);
|
|
67
|
+
return stmt.all(...params);
|
|
98
68
|
}
|
|
99
69
|
|
|
100
70
|
deleteConversation(id) {
|
|
101
|
-
|
|
102
|
-
|
|
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);
|
|
103
75
|
}
|
|
104
76
|
|
|
105
77
|
/** Find conversation by tmux window name */
|
|
106
78
|
findByWindow(windowName) {
|
|
107
|
-
|
|
79
|
+
const stmt = prepare('SELECT * FROM conversations WHERE tmux_window = ?');
|
|
80
|
+
return stmt.get(windowName);
|
|
108
81
|
}
|
|
109
82
|
|
|
110
83
|
/** Get conversations grouped by date */
|
|
111
|
-
listGroupedByDate({ workspace, limit = 100 } = {}) {
|
|
112
|
-
const convs = this.listConversations({ workspace, limit });
|
|
84
|
+
listGroupedByDate({ workspace, host, limit = 100 } = {}) {
|
|
85
|
+
const convs = this.listConversations({ workspace, host, limit });
|
|
113
86
|
const grouped = {};
|
|
114
87
|
for (const c of convs) {
|
|
115
88
|
const date = new Date(c.updated_at * 1000).toISOString().split('T')[0];
|
|
@@ -119,17 +92,37 @@ class SessionStore {
|
|
|
119
92
|
return grouped;
|
|
120
93
|
}
|
|
121
94
|
|
|
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
|
+
|
|
122
113
|
// --- Messages ---
|
|
123
114
|
|
|
124
115
|
addMessage({ conversationId, role, content, engine, model, tokensIn, tokensOut } = {}) {
|
|
125
116
|
const id = uuidv4();
|
|
126
|
-
|
|
117
|
+
const stmt = prepare(`
|
|
127
118
|
INSERT INTO messages (id, conversation_id, role, content, engine, model, tokens_in, tokens_out)
|
|
128
119
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
129
|
-
`)
|
|
120
|
+
`);
|
|
121
|
+
stmt.run(id, conversationId, role, content || '', engine || null, model || null, tokensIn || 0, tokensOut || 0);
|
|
130
122
|
|
|
131
123
|
// Update conversation timestamp
|
|
132
|
-
|
|
124
|
+
const updateStmt = prepare('UPDATE conversations SET updated_at = unixepoch() WHERE id = ?');
|
|
125
|
+
updateStmt.run(conversationId);
|
|
133
126
|
return id;
|
|
134
127
|
}
|
|
135
128
|
|
|
@@ -142,11 +135,13 @@ class SessionStore {
|
|
|
142
135
|
}
|
|
143
136
|
sql += ' ORDER BY created_at ASC LIMIT ?';
|
|
144
137
|
params.push(limit);
|
|
145
|
-
|
|
138
|
+
const stmt = prepare(sql);
|
|
139
|
+
return stmt.all(...params);
|
|
146
140
|
}
|
|
147
141
|
|
|
148
142
|
getMessageCount(conversationId) {
|
|
149
|
-
|
|
143
|
+
const stmt = prepare('SELECT COUNT(*) as count FROM messages WHERE conversation_id = ?');
|
|
144
|
+
return stmt.get(conversationId).count;
|
|
150
145
|
}
|
|
151
146
|
|
|
152
147
|
// --- Recovery ---
|
|
@@ -156,10 +151,11 @@ class SessionStore {
|
|
|
156
151
|
* Mark conversations as 'dead' if their tmux window no longer exists
|
|
157
152
|
*/
|
|
158
153
|
syncWithTmux(tmuxManager) {
|
|
159
|
-
const
|
|
154
|
+
const stmt = prepare("SELECT * FROM conversations WHERE status = 'active'");
|
|
155
|
+
const active = stmt.all();
|
|
160
156
|
for (const conv of active) {
|
|
161
157
|
if (conv.tmux_window) {
|
|
162
|
-
const exists = tmuxManager.hasWindow(conv.tmux_window, conv.host
|
|
158
|
+
const exists = tmuxManager.hasWindow(conv.tmux_window, conv.host || 'local');
|
|
163
159
|
if (!exists) {
|
|
164
160
|
this.updateConversation(conv.id, { status: 'dead' });
|
|
165
161
|
}
|
|
@@ -169,7 +165,7 @@ class SessionStore {
|
|
|
169
165
|
|
|
170
166
|
/** Close DB connection */
|
|
171
167
|
close() {
|
|
172
|
-
|
|
168
|
+
saveDb();
|
|
173
169
|
}
|
|
174
170
|
}
|
|
175
171
|
|