@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
|
@@ -1,39 +1,33 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* POST /api/v1/send —
|
|
2
|
+
* POST /api/v1/send — send a message to an existing tmux session
|
|
3
|
+
* POST /api/v1/send/interrupt — interrupt current generation
|
|
3
4
|
*
|
|
4
|
-
*
|
|
5
|
-
* Creates new window if needed, resumes existing.
|
|
5
|
+
* This route never creates tmux sessions implicitly.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
const { Router } = require('express');
|
|
9
|
-
const
|
|
10
|
-
const path = require('path');
|
|
9
|
+
const os = require('os');
|
|
11
10
|
|
|
12
11
|
const router = Router();
|
|
13
12
|
|
|
14
13
|
router.post('/', async (req, res) => {
|
|
15
|
-
const { tmux,
|
|
14
|
+
const { tmux, store, logWatcher } = req;
|
|
16
15
|
const {
|
|
17
16
|
conversationId,
|
|
18
17
|
message,
|
|
19
|
-
|
|
20
|
-
model,
|
|
21
|
-
engine: engineOverride,
|
|
18
|
+
tmuxSession,
|
|
22
19
|
workspace,
|
|
23
|
-
host: hostName
|
|
24
|
-
alias,
|
|
25
|
-
resumeSession
|
|
20
|
+
host: hostName
|
|
26
21
|
} = req.body;
|
|
27
22
|
|
|
28
23
|
if (!message || !message.trim()) {
|
|
29
24
|
return res.status(400).json({ error: 'Message is required' });
|
|
30
25
|
}
|
|
31
26
|
|
|
32
|
-
// Set SSE headers
|
|
33
27
|
res.writeHead(200, {
|
|
34
28
|
'Content-Type': 'text/event-stream',
|
|
35
29
|
'Cache-Control': 'no-cache',
|
|
36
|
-
|
|
30
|
+
Connection: 'keep-alive',
|
|
37
31
|
'X-Accel-Buffering': 'no'
|
|
38
32
|
});
|
|
39
33
|
|
|
@@ -42,232 +36,127 @@ router.post('/', async (req, res) => {
|
|
|
42
36
|
};
|
|
43
37
|
|
|
44
38
|
try {
|
|
45
|
-
|
|
46
|
-
const
|
|
47
|
-
const
|
|
48
|
-
const hostObj = tmux.resolveHost(host);
|
|
49
|
-
if (!hostObj) {
|
|
50
|
-
throw new Error(`Unknown host "${host}"`);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// Get or create conversation
|
|
54
|
-
let conversation;
|
|
55
|
-
let isNew = false;
|
|
39
|
+
let conversation = conversationId ? store.getConversation(conversationId) : null;
|
|
40
|
+
const resolvedHost = hostName || conversation?.host || 'local';
|
|
41
|
+
const resolvedSession = (tmuxSession || conversation?.tmux_session || '').trim();
|
|
56
42
|
|
|
57
|
-
if (
|
|
58
|
-
|
|
43
|
+
if (!resolvedSession) {
|
|
44
|
+
sendSSE('error', { error: 'tmuxSession is required' });
|
|
45
|
+
return res.end();
|
|
46
|
+
}
|
|
47
|
+
if (!tmux.hasSession(resolvedSession, resolvedHost)) {
|
|
48
|
+
sendSSE('error', { error: `tmux session "${resolvedSession}" is not active` });
|
|
49
|
+
return res.end();
|
|
59
50
|
}
|
|
60
51
|
|
|
61
52
|
if (!conversation) {
|
|
62
|
-
|
|
63
|
-
conversation = store.createConversation({
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
workspace: workspace ||
|
|
67
|
-
host
|
|
53
|
+
const existing = store.findByTmuxSession(resolvedSession);
|
|
54
|
+
conversation = existing || store.createConversation({
|
|
55
|
+
title: resolvedSession,
|
|
56
|
+
engine: null,
|
|
57
|
+
workspace: (workspace || os.homedir()).trim() || os.homedir(),
|
|
58
|
+
host: resolvedHost,
|
|
59
|
+
tmuxSession: resolvedSession,
|
|
60
|
+
logPath: null
|
|
68
61
|
});
|
|
62
|
+
store.updateConversation(conversation.id, { title: resolvedSession });
|
|
69
63
|
sendSSE('session_created', {
|
|
70
64
|
conversationId: conversation.id,
|
|
71
65
|
sessionId: conversation.id
|
|
72
66
|
});
|
|
73
67
|
}
|
|
74
68
|
|
|
75
|
-
let
|
|
76
|
-
if (
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
// Context Bridge: if engine changed, add context to message
|
|
86
|
-
if (!isNew && conversationId && conversation.engine && conversation.engine !== resolvedEngine) {
|
|
87
|
-
try {
|
|
88
|
-
const { default: ContextBridge } = require('../services/context-bridge');
|
|
89
|
-
const ctx = await ContextBridge.buildContext({
|
|
90
|
-
conversationId: conversation.id,
|
|
91
|
-
fromEngine: conversation.engine,
|
|
92
|
-
toEngine: resolvedEngine,
|
|
93
|
-
userMessage: message
|
|
94
|
-
});
|
|
95
|
-
if (ctx.prompt) {
|
|
96
|
-
fullMessage = ctx.prompt;
|
|
97
|
-
console.log(`[send] Applied context bridge: ${conversation.engine} -> ${resolvedEngine} (${ctx.contextTokens} tokens)`);
|
|
98
|
-
}
|
|
99
|
-
} catch (err) {
|
|
100
|
-
console.warn('[send] Context bridge failed, proceeding without context:', err.message);
|
|
101
|
-
// Continue without context - non-fatal
|
|
102
|
-
}
|
|
69
|
+
let logPath = conversation.log_path;
|
|
70
|
+
if (!logPath) {
|
|
71
|
+
logPath = tmux.ensureSessionLogging(resolvedSession, resolvedHost);
|
|
72
|
+
store.updateConversation(conversation.id, {
|
|
73
|
+
tmux_session: resolvedSession,
|
|
74
|
+
workspace: conversation.workspace || (workspace || os.homedir()),
|
|
75
|
+
log_path: logPath,
|
|
76
|
+
status: 'active'
|
|
77
|
+
});
|
|
78
|
+
conversation = store.getConversation(conversation.id);
|
|
103
79
|
}
|
|
104
80
|
|
|
105
|
-
// Save user message
|
|
106
81
|
store.addMessage({
|
|
107
82
|
conversationId: conversation.id,
|
|
108
83
|
role: 'user',
|
|
109
|
-
content:
|
|
110
|
-
engine:
|
|
111
|
-
model: model
|
|
84
|
+
content: message,
|
|
85
|
+
engine: conversation.engine,
|
|
86
|
+
model: conversation.model
|
|
112
87
|
});
|
|
113
88
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
// Determine tmux window name
|
|
117
|
-
const windowName = `nc-${conversation.id.substring(0, 8)}`;
|
|
118
|
-
|
|
119
|
-
// Build command for the engine
|
|
120
|
-
const engineInfo = engines.discover()[resolvedEngine];
|
|
121
|
-
let command;
|
|
89
|
+
logWatcher.watch(logPath, conversation.engine || 'generic', conversation.id);
|
|
122
90
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
command = alias;
|
|
126
|
-
} else if (engineInfo?.available) {
|
|
127
|
-
command = buildEngineCommand(resolvedEngine, engineInfo, {
|
|
128
|
-
model,
|
|
129
|
-
message: fullMessage, // Pass fullMessage for codex exec mode
|
|
130
|
-
resume: resumeSession || (conversation.tmux_window ? conversation.id : null)
|
|
131
|
-
});
|
|
132
|
-
} else {
|
|
133
|
-
sendSSE('error', { error: `Engine "${resolvedEngine}" not available` });
|
|
134
|
-
res.end();
|
|
135
|
-
return;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// Create or reuse tmux window
|
|
139
|
-
let logPath;
|
|
140
|
-
let remoteInitialSnapshot = '';
|
|
141
|
-
|
|
142
|
-
if (conversation.tmux_window && tmux.hasWindow(conversation.tmux_window, hostObj)) {
|
|
143
|
-
if (hostObj.type !== 'local') {
|
|
144
|
-
remoteInitialSnapshot = tmux.capturePane(conversation.tmux_window, hostObj, 250);
|
|
145
|
-
}
|
|
146
|
-
// Resume existing window — send message directly
|
|
147
|
-
tmux.sendKeys(conversation.tmux_window, fullMessage, { enter: true, host: hostObj });
|
|
148
|
-
logPath = conversation.log_path;
|
|
149
|
-
} else {
|
|
150
|
-
// Create new tmux window
|
|
151
|
-
const result = tmux.createWindow({
|
|
152
|
-
windowName,
|
|
153
|
-
command,
|
|
154
|
-
cwd: conversation.workspace || workspace || process.env.HOME,
|
|
155
|
-
host: hostObj
|
|
156
|
-
});
|
|
157
|
-
logPath = result.logPath;
|
|
158
|
-
|
|
159
|
-
store.updateConversation(conversation.id, {
|
|
160
|
-
tmux_window: windowName,
|
|
161
|
-
log_path: logPath,
|
|
162
|
-
status: 'active'
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
// Wait a moment for the CLI to start, then send the message
|
|
166
|
-
if (resolvedEngine !== 'codex') {
|
|
167
|
-
// Interactive CLIs need a moment to start
|
|
168
|
-
await new Promise(r => setTimeout(r, 1500));
|
|
169
|
-
if (hostObj.type !== 'local') {
|
|
170
|
-
remoteInitialSnapshot = tmux.capturePane(windowName, hostObj, 250);
|
|
171
|
-
}
|
|
172
|
-
tmux.sendKeys(windowName, fullMessage, { enter: true, host: hostObj });
|
|
173
|
-
} else if (hostObj.type !== 'local') {
|
|
174
|
-
remoteInitialSnapshot = tmux.capturePane(windowName, hostObj, 250);
|
|
175
|
-
}
|
|
176
|
-
// Codex exec mode gets the message via the command itself
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
// Start watching the log
|
|
180
|
-
if (hostObj.type === 'local' && logPath) {
|
|
181
|
-
logWatcher.watch(logPath, resolvedEngine, conversation.id);
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
sendSSE('status', { content: 'Sending to tmux...' });
|
|
91
|
+
sendSSE('message_start', { role: 'user' });
|
|
92
|
+
sendSSE('status', { content: `Sending to tmux session ${resolvedSession}...` });
|
|
185
93
|
|
|
186
|
-
// Collect response from log watcher
|
|
187
94
|
let responseBuffer = '';
|
|
188
|
-
let
|
|
189
|
-
|
|
190
|
-
if (!doneFired) {
|
|
191
|
-
doneFired = true;
|
|
192
|
-
finishResponse(responseBuffer || '(timeout — check tmux window directly)');
|
|
193
|
-
}
|
|
194
|
-
}, 120000); // 2 min timeout
|
|
195
|
-
|
|
196
|
-
const onData = (data) => {
|
|
197
|
-
if (data.conversationId !== conversation.id) return;
|
|
198
|
-
|
|
199
|
-
switch (data.type) {
|
|
200
|
-
case 'status':
|
|
201
|
-
sendSSE('status', { content: data.content });
|
|
202
|
-
break;
|
|
203
|
-
case 'chunk':
|
|
204
|
-
responseBuffer += data.content;
|
|
205
|
-
sendSSE('chunk', { content: data.content });
|
|
206
|
-
break;
|
|
207
|
-
case 'raw':
|
|
208
|
-
responseBuffer += data.content;
|
|
209
|
-
sendSSE('chunk', { content: data.content });
|
|
210
|
-
break;
|
|
211
|
-
case 'message_done':
|
|
212
|
-
if (!doneFired) {
|
|
213
|
-
doneFired = true;
|
|
214
|
-
clearTimeout(timeout);
|
|
215
|
-
finishResponse(data.content || responseBuffer, data.usage);
|
|
216
|
-
}
|
|
217
|
-
break;
|
|
218
|
-
case 'done':
|
|
219
|
-
if (!doneFired) {
|
|
220
|
-
doneFired = true;
|
|
221
|
-
clearTimeout(timeout);
|
|
222
|
-
finishResponse(responseBuffer || '(completed)');
|
|
223
|
-
}
|
|
224
|
-
break;
|
|
225
|
-
}
|
|
226
|
-
};
|
|
227
|
-
|
|
228
|
-
const activeWatcher = hostObj.type === 'local' ? logWatcher : remotePaneWatcher;
|
|
229
|
-
activeWatcher.on('data', onData);
|
|
230
|
-
|
|
231
|
-
if (hostObj.type !== 'local') {
|
|
232
|
-
remotePaneWatcher.watch({
|
|
233
|
-
conversationId: conversation.id,
|
|
234
|
-
engine: resolvedEngine,
|
|
235
|
-
host: hostObj,
|
|
236
|
-
windowName: conversation.tmux_window || windowName,
|
|
237
|
-
promptText: fullMessage,
|
|
238
|
-
initialSnapshot: remoteInitialSnapshot
|
|
239
|
-
});
|
|
240
|
-
}
|
|
95
|
+
let finished = false;
|
|
96
|
+
let activitySeen = false;
|
|
241
97
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
98
|
+
const finishResponse = (content, usage) => {
|
|
99
|
+
if (finished) return;
|
|
100
|
+
finished = true;
|
|
101
|
+
clearTimeout(maxTimer);
|
|
102
|
+
clearTimeout(idleTimer);
|
|
103
|
+
logWatcher.off('data', onData);
|
|
248
104
|
|
|
249
|
-
|
|
105
|
+
const finalContent = content || responseBuffer || tmux.capturePane(resolvedSession, resolvedHost) || '(no output captured)';
|
|
250
106
|
store.addMessage({
|
|
251
107
|
conversationId: conversation.id,
|
|
252
108
|
role: 'assistant',
|
|
253
|
-
content,
|
|
254
|
-
engine:
|
|
255
|
-
model: model,
|
|
109
|
+
content: finalContent,
|
|
110
|
+
engine: conversation.engine,
|
|
111
|
+
model: conversation.model,
|
|
256
112
|
tokensIn: usage?.input_tokens || 0,
|
|
257
113
|
tokensOut: usage?.output_tokens || 0
|
|
258
114
|
});
|
|
259
115
|
|
|
260
116
|
sendSSE('message_done', {
|
|
261
|
-
content,
|
|
117
|
+
content: finalContent,
|
|
262
118
|
usage,
|
|
263
119
|
conversationId: conversation.id,
|
|
264
|
-
|
|
265
|
-
model: model
|
|
120
|
+
tmuxSession: resolvedSession
|
|
266
121
|
});
|
|
267
122
|
|
|
268
123
|
res.end();
|
|
269
|
-
}
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const bumpIdleTimer = () => {
|
|
127
|
+
clearTimeout(idleTimer);
|
|
128
|
+
idleTimer = setTimeout(() => {
|
|
129
|
+
finishResponse(responseBuffer);
|
|
130
|
+
}, activitySeen ? 15000 : 20000);
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const onData = (data) => {
|
|
134
|
+
if (data.conversationId !== conversation.id) return;
|
|
135
|
+
activitySeen = true;
|
|
136
|
+
|
|
137
|
+
if (data.type === 'status') {
|
|
138
|
+
sendSSE('status', { content: data.content });
|
|
139
|
+
} else if (data.type === 'chunk' || data.type === 'raw') {
|
|
140
|
+
responseBuffer += `${data.content}\n`;
|
|
141
|
+
sendSSE('chunk', { content: data.content });
|
|
142
|
+
} else if (data.type === 'message_done') {
|
|
143
|
+
responseBuffer += data.content || '';
|
|
144
|
+
return finishResponse(data.content || responseBuffer, data.usage);
|
|
145
|
+
} else if (data.type === 'done') {
|
|
146
|
+
return finishResponse(responseBuffer);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
bumpIdleTimer();
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
logWatcher.on('data', onData);
|
|
270
153
|
|
|
154
|
+
const maxTimer = setTimeout(() => {
|
|
155
|
+
finishResponse(responseBuffer || '(timeout — check tmux session directly)');
|
|
156
|
+
}, 120000);
|
|
157
|
+
let idleTimer = setTimeout(() => finishResponse(responseBuffer), 20000);
|
|
158
|
+
|
|
159
|
+
tmux.sendKeys(resolvedSession, message, { enter: true, host: resolvedHost });
|
|
271
160
|
} catch (err) {
|
|
272
161
|
console.error('[send] Error:', err);
|
|
273
162
|
sendSSE('error', { error: err.message });
|
|
@@ -275,67 +164,17 @@ router.post('/', async (req, res) => {
|
|
|
275
164
|
}
|
|
276
165
|
});
|
|
277
166
|
|
|
278
|
-
/**
|
|
279
|
-
* POST /api/v1/send/interrupt — Interrupt current generation
|
|
280
|
-
*/
|
|
281
167
|
router.post('/interrupt', (req, res) => {
|
|
282
168
|
const { tmux, store } = req;
|
|
283
169
|
const { conversationId } = req.body;
|
|
170
|
+
const conversation = conversationId ? store.getConversation(conversationId) : null;
|
|
284
171
|
|
|
285
|
-
if (!
|
|
286
|
-
return res.status(
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
const conversation = store.getConversation(conversationId);
|
|
290
|
-
if (!conversation || !conversation.tmux_window) {
|
|
291
|
-
return res.json({ success: false, reason: 'No active tmux window' });
|
|
172
|
+
if (!conversation?.tmux_session) {
|
|
173
|
+
return res.status(404).json({ error: 'Conversation has no active tmux session' });
|
|
292
174
|
}
|
|
293
175
|
|
|
294
|
-
tmux.interrupt(conversation.
|
|
295
|
-
|
|
296
|
-
res.json({ success: true, method: 'tmux interrupt' });
|
|
176
|
+
tmux.interrupt(conversation.tmux_session, conversation.host || 'local');
|
|
177
|
+
res.json({ success: true });
|
|
297
178
|
});
|
|
298
179
|
|
|
299
|
-
// --- Helpers ---
|
|
300
|
-
|
|
301
|
-
function resolveEngine(model, engineDiscovery) {
|
|
302
|
-
if (!model) return 'claude';
|
|
303
|
-
const allModels = engineDiscovery.getAllModels();
|
|
304
|
-
const found = allModels.find(m => m.id === model);
|
|
305
|
-
return found ? found.engine : 'claude';
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
function buildEngineCommand(engine, engineInfo, opts = {}) {
|
|
309
|
-
const bin = engineInfo.path;
|
|
310
|
-
if (!bin) return engine;
|
|
311
|
-
|
|
312
|
-
switch (engine) {
|
|
313
|
-
case 'claude':
|
|
314
|
-
if (opts.resume) {
|
|
315
|
-
return `${bin} --output-format json -r ${opts.resume}${opts.model ? ` --model ${opts.model}` : ''}`;
|
|
316
|
-
}
|
|
317
|
-
return `${bin} --output-format json${opts.model ? ` --model ${opts.model}` : ''}`;
|
|
318
|
-
|
|
319
|
-
case 'codex':
|
|
320
|
-
// For codex, message is passed via --prompt flag in exec mode
|
|
321
|
-
const prompt = opts.message ? ` --prompt ${JSON.stringify(opts.message)}` : '';
|
|
322
|
-
return `${bin} exec --json${opts.model ? ` -m ${opts.model}` : ''}${opts.reasoningEffort ? ` --reasoning ${opts.reasoningEffort}` : ''}${prompt}`;
|
|
323
|
-
|
|
324
|
-
case 'gemini':
|
|
325
|
-
if (opts.resume) {
|
|
326
|
-
return `${bin} --resume ${opts.resume}${opts.model ? ` -m ${opts.model}` : ''}`;
|
|
327
|
-
}
|
|
328
|
-
return `${bin}${opts.model ? ` -m ${opts.model}` : ''}`;
|
|
329
|
-
|
|
330
|
-
case 'qwen':
|
|
331
|
-
if (opts.resume) {
|
|
332
|
-
return `${bin} --resume ${opts.resume}${opts.model ? ` -m ${opts.model}` : ''}`;
|
|
333
|
-
}
|
|
334
|
-
return `${bin}${opts.model ? ` -m ${opts.model}` : ''}`;
|
|
335
|
-
|
|
336
|
-
default:
|
|
337
|
-
return bin;
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
|
|
341
180
|
module.exports = router;
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* GET /api/v1/sessions — List/load sessions
|
|
2
|
+
* GET /api/v1/sessions — List/load sessions
|
|
3
3
|
* GET /api/v1/sessions/:id/messages — Get session messages
|
|
4
4
|
* GET /api/v1/sessions/:id/summary — Get session summary
|
|
5
5
|
* POST /api/v1/sessions/:id/rename — Rename session
|
|
6
|
-
* POST /api/v1/sessions/:id/pin — Pin/unpin session
|
|
7
6
|
* DELETE /api/v1/sessions/:id — Delete session
|
|
8
7
|
*/
|
|
9
8
|
|
|
@@ -12,37 +11,15 @@ const router = Router();
|
|
|
12
11
|
|
|
13
12
|
// List conversations (optionally grouped by date)
|
|
14
13
|
router.get('/', (req, res) => {
|
|
15
|
-
const { store
|
|
16
|
-
const { workspace, groupBy
|
|
14
|
+
const { store } = req;
|
|
15
|
+
const { workspace, groupBy } = req.query;
|
|
17
16
|
|
|
18
17
|
if (groupBy === 'date') {
|
|
19
|
-
const grouped = store.listGroupedByDate({ workspace
|
|
20
|
-
|
|
21
|
-
// Enrich with tmux_alive status
|
|
22
|
-
Object.keys(grouped).forEach(dateGroup => {
|
|
23
|
-
grouped[dateGroup].forEach(conv => {
|
|
24
|
-
if (conv.tmux_window) {
|
|
25
|
-
conv.tmux_alive = tmux.hasWindow(conv.tmux_window, conv.host || 'local');
|
|
26
|
-
} else {
|
|
27
|
-
conv.tmux_alive = false;
|
|
28
|
-
}
|
|
29
|
-
});
|
|
30
|
-
});
|
|
31
|
-
|
|
18
|
+
const grouped = store.listGroupedByDate({ workspace });
|
|
32
19
|
return res.json(grouped);
|
|
33
20
|
}
|
|
34
21
|
|
|
35
|
-
const convs = store.listConversations({ workspace
|
|
36
|
-
|
|
37
|
-
// Enrich with tmux_alive status
|
|
38
|
-
convs.forEach(conv => {
|
|
39
|
-
if (conv.tmux_window) {
|
|
40
|
-
conv.tmux_alive = tmux.hasWindow(conv.tmux_window, conv.host || 'local');
|
|
41
|
-
} else {
|
|
42
|
-
conv.tmux_alive = false;
|
|
43
|
-
}
|
|
44
|
-
});
|
|
45
|
-
|
|
22
|
+
const convs = store.listConversations({ workspace });
|
|
46
23
|
res.json({ conversations: convs });
|
|
47
24
|
});
|
|
48
25
|
|
|
@@ -56,7 +33,9 @@ router.get('/:id', (req, res) => {
|
|
|
56
33
|
}
|
|
57
34
|
|
|
58
35
|
// Check tmux window status
|
|
59
|
-
const
|
|
36
|
+
const host = conv.host !== 'local' ? conv.host : null;
|
|
37
|
+
const target = conv.tmux_session || conv.tmux_window;
|
|
38
|
+
const alive = target ? tmux.hasSession(target, host) : false;
|
|
60
39
|
|
|
61
40
|
res.json({
|
|
62
41
|
session: {
|
|
@@ -101,30 +80,22 @@ router.post('/:id/rename', (req, res) => {
|
|
|
101
80
|
res.json({ success: true, title });
|
|
102
81
|
});
|
|
103
82
|
|
|
104
|
-
//
|
|
105
|
-
router.post('/:id/pin', (req, res) => {
|
|
106
|
-
const { store } = req;
|
|
107
|
-
const result = store.pinConversation(req.params.id);
|
|
108
|
-
res.json({ pinned: result });
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
// Delete conversation (kills tmux window too)
|
|
83
|
+
// Delete conversation (kills tmux session too)
|
|
112
84
|
router.delete('/:id', (req, res) => {
|
|
113
|
-
const { store, tmux, logWatcher
|
|
85
|
+
const { store, tmux, logWatcher } = req;
|
|
114
86
|
const conv = store.getConversation(req.params.id);
|
|
115
87
|
|
|
116
88
|
if (!conv) return res.status(404).json({ error: 'Not found' });
|
|
117
89
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
tmux.
|
|
90
|
+
if (conv.tmux_session) {
|
|
91
|
+
const host = conv.host !== 'local' ? conv.host : null;
|
|
92
|
+
tmux.killSession(conv.tmux_session, host);
|
|
121
93
|
}
|
|
122
94
|
|
|
123
95
|
// Stop log watcher
|
|
124
96
|
if (conv.log_path) {
|
|
125
97
|
logWatcher.unwatch(conv.log_path);
|
|
126
98
|
}
|
|
127
|
-
remotePaneWatcher?.unwatchByConversation?.(req.params.id);
|
|
128
99
|
|
|
129
100
|
store.deleteConversation(req.params.id);
|
|
130
101
|
res.json({ success: true });
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* GET /api/v1/status —
|
|
3
|
-
* GET /api/v1/status/tmux — Detailed tmux info
|
|
4
|
-
* GET /api/v1/status/pane/:
|
|
2
|
+
* GET /api/v1/status — tmux status
|
|
3
|
+
* GET /api/v1/status/tmux — Detailed tmux session info
|
|
4
|
+
* GET /api/v1/status/pane/:session — Capture pane content
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
const { Router } = require('express');
|
|
@@ -10,16 +10,13 @@ const router = Router();
|
|
|
10
10
|
// Overall status
|
|
11
11
|
router.get('/', (req, res) => {
|
|
12
12
|
const { tmux, store } = req;
|
|
13
|
-
const
|
|
14
|
-
const windows = tmux.listWindows(host);
|
|
13
|
+
const sessions = tmux.listSessions();
|
|
15
14
|
const hosts = tmux.getAllHosts();
|
|
16
15
|
|
|
17
16
|
res.json({
|
|
18
17
|
tmux: {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
windowCount: windows.length,
|
|
22
|
-
windows
|
|
18
|
+
sessionCount: sessions.length,
|
|
19
|
+
sessions
|
|
23
20
|
},
|
|
24
21
|
hosts: hosts.map(h => ({
|
|
25
22
|
...h,
|
|
@@ -31,32 +28,31 @@ router.get('/', (req, res) => {
|
|
|
31
28
|
// Detailed tmux windows
|
|
32
29
|
router.get('/tmux', (req, res) => {
|
|
33
30
|
const { tmux, store } = req;
|
|
34
|
-
const
|
|
35
|
-
const windows = tmux.listWindows(host);
|
|
31
|
+
const sessions = tmux.listSessions();
|
|
36
32
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const conv = store.findByWindow(w.name);
|
|
33
|
+
const enriched = sessions.map((session) => {
|
|
34
|
+
const conv = store.findByTmuxSession(session.sessionName);
|
|
40
35
|
return {
|
|
41
|
-
...
|
|
36
|
+
...session,
|
|
42
37
|
conversationId: conv?.id || null,
|
|
43
38
|
engine: conv?.engine || null,
|
|
44
39
|
model: conv?.model || null,
|
|
45
|
-
host: conv?.host ||
|
|
46
|
-
title: conv?.title ||
|
|
40
|
+
host: conv?.host || 'local',
|
|
41
|
+
title: conv?.title || session.sessionName,
|
|
42
|
+
workspace: conv?.workspace || ''
|
|
47
43
|
};
|
|
48
44
|
});
|
|
49
45
|
|
|
50
|
-
res.json({
|
|
46
|
+
res.json({ sessions: enriched });
|
|
51
47
|
});
|
|
52
48
|
|
|
53
49
|
// Capture pane content
|
|
54
|
-
router.get('/pane/:
|
|
50
|
+
router.get('/pane/:session', (req, res) => {
|
|
55
51
|
const { tmux } = req;
|
|
56
|
-
const { host
|
|
57
|
-
const content = tmux.capturePane(req.params.
|
|
52
|
+
const { host } = req.query;
|
|
53
|
+
const content = tmux.capturePane(req.params.session, host || null);
|
|
58
54
|
|
|
59
|
-
res.json({
|
|
55
|
+
res.json({ session: req.params.session, content });
|
|
60
56
|
});
|
|
61
57
|
|
|
62
58
|
module.exports = router;
|