agentgui 1.0.150 → 1.0.152
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/database.js +87 -66
- package/package.json +1 -1
- package/server.js +245 -119
- package/static/index.html +29 -12
- package/static/js/client.js +7 -6
- package/static/js/streaming-renderer.js +55 -18
- package/static/js/websocket-manager.js +16 -27
- package/static/styles.css +0 -1989
package/server.js
CHANGED
|
@@ -2,6 +2,7 @@ import http from 'http';
|
|
|
2
2
|
import fs from 'fs';
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import os from 'os';
|
|
5
|
+
import zlib from 'zlib';
|
|
5
6
|
import { fileURLToPath } from 'url';
|
|
6
7
|
import { WebSocketServer } from 'ws';
|
|
7
8
|
import { execSync } from 'child_process';
|
|
@@ -146,6 +147,36 @@ function parseBody(req) {
|
|
|
146
147
|
});
|
|
147
148
|
}
|
|
148
149
|
|
|
150
|
+
function acceptsEncoding(req, encoding) {
|
|
151
|
+
const accept = req.headers['accept-encoding'] || '';
|
|
152
|
+
return accept.includes(encoding);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function compressAndSend(req, res, statusCode, contentType, body) {
|
|
156
|
+
const raw = typeof body === 'string' ? Buffer.from(body) : body;
|
|
157
|
+
if (raw.length < 860) {
|
|
158
|
+
res.writeHead(statusCode, { 'Content-Type': contentType, 'Content-Length': raw.length });
|
|
159
|
+
res.end(raw);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
if (acceptsEncoding(req, 'br')) {
|
|
163
|
+
const compressed = zlib.brotliCompressSync(raw, { params: { [zlib.constants.BROTLI_PARAM_QUALITY]: 4 } });
|
|
164
|
+
res.writeHead(statusCode, { 'Content-Type': contentType, 'Content-Encoding': 'br', 'Content-Length': compressed.length });
|
|
165
|
+
res.end(compressed);
|
|
166
|
+
} else if (acceptsEncoding(req, 'gzip')) {
|
|
167
|
+
const compressed = zlib.gzipSync(raw, { level: 6 });
|
|
168
|
+
res.writeHead(statusCode, { 'Content-Type': contentType, 'Content-Encoding': 'gzip', 'Content-Length': compressed.length });
|
|
169
|
+
res.end(compressed);
|
|
170
|
+
} else {
|
|
171
|
+
res.writeHead(statusCode, { 'Content-Type': contentType, 'Content-Length': raw.length });
|
|
172
|
+
res.end(raw);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function sendJSON(req, res, statusCode, data) {
|
|
177
|
+
compressAndSend(req, res, statusCode, 'application/json', JSON.stringify(data));
|
|
178
|
+
}
|
|
179
|
+
|
|
149
180
|
const server = http.createServer(async (req, res) => {
|
|
150
181
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
151
182
|
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
|
@@ -179,8 +210,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
179
210
|
const pathOnly = routePath.split('?')[0];
|
|
180
211
|
|
|
181
212
|
if (pathOnly === '/api/conversations' && req.method === 'GET') {
|
|
182
|
-
|
|
183
|
-
res.end(JSON.stringify({ conversations: queries.getConversationsList() }));
|
|
213
|
+
sendJSON(req, res, 200, { conversations: queries.getConversationsList() });
|
|
184
214
|
return;
|
|
185
215
|
}
|
|
186
216
|
|
|
@@ -189,8 +219,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
189
219
|
const conversation = queries.createConversation(body.agentId, body.title, body.workingDirectory || null);
|
|
190
220
|
queries.createEvent('conversation.created', { agentId: body.agentId, workingDirectory: conversation.workingDirectory }, conversation.id);
|
|
191
221
|
broadcastSync({ type: 'conversation_created', conversation });
|
|
192
|
-
|
|
193
|
-
res.end(JSON.stringify({ conversation }));
|
|
222
|
+
sendJSON(req, res, 201, { conversation });
|
|
194
223
|
return;
|
|
195
224
|
}
|
|
196
225
|
|
|
@@ -198,39 +227,36 @@ const server = http.createServer(async (req, res) => {
|
|
|
198
227
|
if (convMatch) {
|
|
199
228
|
if (req.method === 'GET') {
|
|
200
229
|
const conv = queries.getConversation(convMatch[1]);
|
|
201
|
-
if (!conv) {
|
|
230
|
+
if (!conv) { sendJSON(req, res, 404, { error: 'Not found' }); return; }
|
|
202
231
|
|
|
203
232
|
// Check both in-memory and database for active streaming status
|
|
204
233
|
const latestSession = queries.getLatestSession(convMatch[1]);
|
|
205
234
|
const isActivelyStreaming = activeExecutions.has(convMatch[1]) ||
|
|
206
235
|
(latestSession && latestSession.status === 'active');
|
|
207
236
|
|
|
208
|
-
|
|
209
|
-
res.end(JSON.stringify({
|
|
237
|
+
sendJSON(req, res, 200, {
|
|
210
238
|
conversation: conv,
|
|
211
239
|
isActivelyStreaming,
|
|
212
240
|
latestSession
|
|
213
|
-
})
|
|
241
|
+
});
|
|
214
242
|
return;
|
|
215
243
|
}
|
|
216
244
|
|
|
217
245
|
if (req.method === 'POST' || req.method === 'PUT') {
|
|
218
246
|
const body = await parseBody(req);
|
|
219
247
|
const conv = queries.updateConversation(convMatch[1], body);
|
|
220
|
-
if (!conv) {
|
|
248
|
+
if (!conv) { sendJSON(req, res, 404, { error: 'Conversation not found' }); return; }
|
|
221
249
|
queries.createEvent('conversation.updated', body, convMatch[1]);
|
|
222
250
|
broadcastSync({ type: 'conversation_updated', conversation: conv });
|
|
223
|
-
|
|
224
|
-
res.end(JSON.stringify({ conversation: conv }));
|
|
251
|
+
sendJSON(req, res, 200, { conversation: conv });
|
|
225
252
|
return;
|
|
226
253
|
}
|
|
227
254
|
|
|
228
255
|
if (req.method === 'DELETE') {
|
|
229
256
|
const deleted = queries.deleteConversation(convMatch[1]);
|
|
230
|
-
if (!deleted) {
|
|
257
|
+
if (!deleted) { sendJSON(req, res, 404, { error: 'Not found' }); return; }
|
|
231
258
|
broadcastSync({ type: 'conversation_deleted', conversationId: convMatch[1] });
|
|
232
|
-
|
|
233
|
-
res.end(JSON.stringify({ deleted: true }));
|
|
259
|
+
sendJSON(req, res, 200, { deleted: true });
|
|
234
260
|
return;
|
|
235
261
|
}
|
|
236
262
|
}
|
|
@@ -242,15 +268,14 @@ const server = http.createServer(async (req, res) => {
|
|
|
242
268
|
const limit = Math.min(parseInt(url.searchParams.get('limit') || '50'), 100);
|
|
243
269
|
const offset = Math.max(parseInt(url.searchParams.get('offset') || '0'), 0);
|
|
244
270
|
const result = queries.getPaginatedMessages(messagesMatch[1], limit, offset);
|
|
245
|
-
|
|
246
|
-
res.end(JSON.stringify(result));
|
|
271
|
+
sendJSON(req, res, 200, result);
|
|
247
272
|
return;
|
|
248
273
|
}
|
|
249
274
|
|
|
250
275
|
if (req.method === 'POST') {
|
|
251
276
|
const conversationId = messagesMatch[1];
|
|
252
277
|
const conv = queries.getConversation(conversationId);
|
|
253
|
-
if (!conv) {
|
|
278
|
+
if (!conv) { sendJSON(req, res, 404, { error: 'Conversation not found' }); return; }
|
|
254
279
|
const body = await parseBody(req);
|
|
255
280
|
const idempotencyKey = body.idempotencyKey || null;
|
|
256
281
|
const message = queries.createMessage(conversationId, 'user', body.content, idempotencyKey);
|
|
@@ -258,8 +283,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
258
283
|
broadcastSync({ type: 'message_created', conversationId, message, timestamp: Date.now() });
|
|
259
284
|
const session = queries.createSession(conversationId);
|
|
260
285
|
queries.createEvent('session.created', { messageId: message.id, sessionId: session.id }, conversationId, session.id);
|
|
261
|
-
|
|
262
|
-
res.end(JSON.stringify({ message, session, idempotencyKey }));
|
|
286
|
+
sendJSON(req, res, 201, { message, session, idempotencyKey });
|
|
263
287
|
// Fire-and-forget with proper error handling
|
|
264
288
|
processMessage(conversationId, message.id, body.content, body.agentId)
|
|
265
289
|
.catch(err => debugLog(`[processMessage] Uncaught error: ${err.message}`));
|
|
@@ -272,7 +296,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
272
296
|
const conversationId = streamMatch[1];
|
|
273
297
|
const body = await parseBody(req);
|
|
274
298
|
const conv = queries.getConversation(conversationId);
|
|
275
|
-
if (!conv) {
|
|
299
|
+
if (!conv) { sendJSON(req, res, 404, { error: 'Conversation not found' }); return; }
|
|
276
300
|
|
|
277
301
|
const prompt = body.content || '';
|
|
278
302
|
const agentId = body.agentId || 'claude-code';
|
|
@@ -290,16 +314,14 @@ const server = http.createServer(async (req, res) => {
|
|
|
290
314
|
const queueLength = messageQueues.get(conversationId).length;
|
|
291
315
|
broadcastSync({ type: 'queue_status', conversationId, queueLength, messageId: userMessage.id, timestamp: Date.now() });
|
|
292
316
|
|
|
293
|
-
|
|
294
|
-
res.end(JSON.stringify({ message: userMessage, queued: true, queuePosition: queueLength }));
|
|
317
|
+
sendJSON(req, res, 200, { message: userMessage, queued: true, queuePosition: queueLength });
|
|
295
318
|
return;
|
|
296
319
|
}
|
|
297
320
|
|
|
298
321
|
const session = queries.createSession(conversationId);
|
|
299
322
|
queries.createEvent('session.created', { messageId: userMessage.id, sessionId: session.id }, conversationId, session.id);
|
|
300
323
|
|
|
301
|
-
|
|
302
|
-
res.end(JSON.stringify({ message: userMessage, session, streamId: session.id }));
|
|
324
|
+
sendJSON(req, res, 200, { message: userMessage, session, streamId: session.id });
|
|
303
325
|
|
|
304
326
|
broadcastSync({
|
|
305
327
|
type: 'streaming_start',
|
|
@@ -318,19 +340,17 @@ const server = http.createServer(async (req, res) => {
|
|
|
318
340
|
const messageMatch = pathOnly.match(/^\/api\/conversations\/([^/]+)\/messages\/([^/]+)$/);
|
|
319
341
|
if (messageMatch && req.method === 'GET') {
|
|
320
342
|
const msg = queries.getMessage(messageMatch[2]);
|
|
321
|
-
if (!msg || msg.conversationId !== messageMatch[1]) {
|
|
322
|
-
|
|
323
|
-
res.end(JSON.stringify({ message: msg }));
|
|
343
|
+
if (!msg || msg.conversationId !== messageMatch[1]) { sendJSON(req, res, 404, { error: 'Not found' }); return; }
|
|
344
|
+
sendJSON(req, res, 200, { message: msg });
|
|
324
345
|
return;
|
|
325
346
|
}
|
|
326
347
|
|
|
327
348
|
const sessionMatch = pathOnly.match(/^\/api\/sessions\/([^/]+)$/);
|
|
328
349
|
if (sessionMatch && req.method === 'GET') {
|
|
329
350
|
const sess = queries.getSession(sessionMatch[1]);
|
|
330
|
-
if (!sess) {
|
|
351
|
+
if (!sess) { sendJSON(req, res, 404, { error: 'Not found' }); return; }
|
|
331
352
|
const events = queries.getSessionEvents(sessionMatch[1]);
|
|
332
|
-
|
|
333
|
-
res.end(JSON.stringify({ session: sess, events }));
|
|
353
|
+
sendJSON(req, res, 200, { session: sess, events });
|
|
334
354
|
return;
|
|
335
355
|
}
|
|
336
356
|
|
|
@@ -338,20 +358,19 @@ const server = http.createServer(async (req, res) => {
|
|
|
338
358
|
if (fullLoadMatch && req.method === 'GET') {
|
|
339
359
|
const conversationId = fullLoadMatch[1];
|
|
340
360
|
const conv = queries.getConversation(conversationId);
|
|
341
|
-
if (!conv) {
|
|
361
|
+
if (!conv) { sendJSON(req, res, 404, { error: 'Not found' }); return; }
|
|
342
362
|
const latestSession = queries.getLatestSession(conversationId);
|
|
343
363
|
const isActivelyStreaming = activeExecutions.has(conversationId) ||
|
|
344
364
|
(latestSession && latestSession.status === 'active');
|
|
345
365
|
const chunks = queries.getConversationChunks(conversationId);
|
|
346
366
|
const msgResult = queries.getPaginatedMessages(conversationId, 100, 0);
|
|
347
|
-
|
|
348
|
-
res.end(JSON.stringify({
|
|
367
|
+
sendJSON(req, res, 200, {
|
|
349
368
|
conversation: conv,
|
|
350
369
|
isActivelyStreaming,
|
|
351
370
|
latestSession,
|
|
352
371
|
chunks,
|
|
353
372
|
messages: msgResult.messages
|
|
354
|
-
})
|
|
373
|
+
});
|
|
355
374
|
return;
|
|
356
375
|
}
|
|
357
376
|
|
|
@@ -359,7 +378,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
359
378
|
if (conversationChunksMatch && req.method === 'GET') {
|
|
360
379
|
const conversationId = conversationChunksMatch[1];
|
|
361
380
|
const conv = queries.getConversation(conversationId);
|
|
362
|
-
if (!conv) {
|
|
381
|
+
if (!conv) { sendJSON(req, res, 404, { error: 'Conversation not found' }); return; }
|
|
363
382
|
|
|
364
383
|
const url = new URL(req.url, 'http://localhost');
|
|
365
384
|
const since = parseInt(url.searchParams.get('since') || '0');
|
|
@@ -367,8 +386,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
367
386
|
const allChunks = queries.getConversationChunks(conversationId);
|
|
368
387
|
debugLog(`[chunks] Conv ${conversationId}: ${allChunks.length} total chunks`);
|
|
369
388
|
const chunks = since > 0 ? allChunks.filter(c => c.created_at > since) : allChunks;
|
|
370
|
-
|
|
371
|
-
res.end(JSON.stringify({ ok: true, chunks }));
|
|
389
|
+
sendJSON(req, res, 200, { ok: true, chunks });
|
|
372
390
|
return;
|
|
373
391
|
}
|
|
374
392
|
|
|
@@ -376,14 +394,13 @@ const server = http.createServer(async (req, res) => {
|
|
|
376
394
|
if (sessionChunksMatch && req.method === 'GET') {
|
|
377
395
|
const sessionId = sessionChunksMatch[1];
|
|
378
396
|
const sess = queries.getSession(sessionId);
|
|
379
|
-
if (!sess) {
|
|
397
|
+
if (!sess) { sendJSON(req, res, 404, { error: 'Session not found' }); return; }
|
|
380
398
|
|
|
381
399
|
const url = new URL(req.url, 'http://localhost');
|
|
382
400
|
const since = parseInt(url.searchParams.get('since') || '0');
|
|
383
401
|
|
|
384
402
|
const chunks = queries.getChunksSince(sessionId, since);
|
|
385
|
-
|
|
386
|
-
res.end(JSON.stringify({ ok: true, chunks }));
|
|
403
|
+
sendJSON(req, res, 200, { ok: true, chunks });
|
|
387
404
|
return;
|
|
388
405
|
}
|
|
389
406
|
|
|
@@ -391,13 +408,11 @@ const server = http.createServer(async (req, res) => {
|
|
|
391
408
|
const convId = pathOnly.match(/^\/api\/conversations\/([^/]+)\/sessions\/latest$/)[1];
|
|
392
409
|
const latestSession = queries.getLatestSession(convId);
|
|
393
410
|
if (!latestSession) {
|
|
394
|
-
|
|
395
|
-
res.end(JSON.stringify({ session: null }));
|
|
411
|
+
sendJSON(req, res, 200, { session: null });
|
|
396
412
|
return;
|
|
397
413
|
}
|
|
398
414
|
const events = queries.getSessionEvents(latestSession.id);
|
|
399
|
-
|
|
400
|
-
res.end(JSON.stringify({ session: latestSession, events }));
|
|
415
|
+
sendJSON(req, res, 200, { session: latestSession, events });
|
|
401
416
|
return;
|
|
402
417
|
}
|
|
403
418
|
|
|
@@ -432,39 +447,33 @@ const server = http.createServer(async (req, res) => {
|
|
|
432
447
|
executionData.events = executionData.events.filter(e => e.type === filterType);
|
|
433
448
|
}
|
|
434
449
|
|
|
435
|
-
|
|
436
|
-
res.end(JSON.stringify(executionData));
|
|
450
|
+
sendJSON(req, res, 200, executionData);
|
|
437
451
|
} catch (err) {
|
|
438
|
-
|
|
439
|
-
res.end(JSON.stringify({ error: err.message }));
|
|
452
|
+
sendJSON(req, res, 400, { error: err.message });
|
|
440
453
|
}
|
|
441
454
|
return;
|
|
442
455
|
}
|
|
443
456
|
|
|
444
457
|
if (routePath === '/api/agents' && req.method === 'GET') {
|
|
445
|
-
|
|
446
|
-
res.end(JSON.stringify({ agents: discoveredAgents }));
|
|
458
|
+
sendJSON(req, res, 200, { agents: discoveredAgents });
|
|
447
459
|
return;
|
|
448
460
|
}
|
|
449
461
|
|
|
450
462
|
|
|
451
463
|
if (routePath === '/api/import/claude-code' && req.method === 'GET') {
|
|
452
464
|
const result = queries.importClaudeCodeConversations();
|
|
453
|
-
|
|
454
|
-
res.end(JSON.stringify({ imported: result }));
|
|
465
|
+
sendJSON(req, res, 200, { imported: result });
|
|
455
466
|
return;
|
|
456
467
|
}
|
|
457
468
|
|
|
458
469
|
if (routePath === '/api/discover/claude-code' && req.method === 'GET') {
|
|
459
470
|
const discovered = queries.discoverClaudeCodeConversations();
|
|
460
|
-
|
|
461
|
-
res.end(JSON.stringify({ discovered }));
|
|
471
|
+
sendJSON(req, res, 200, { discovered });
|
|
462
472
|
return;
|
|
463
473
|
}
|
|
464
474
|
|
|
465
475
|
if (routePath === '/api/home' && req.method === 'GET') {
|
|
466
|
-
|
|
467
|
-
res.end(JSON.stringify({ home: os.homedir(), cwd: STARTUP_CWD }));
|
|
476
|
+
sendJSON(req, res, 200, { home: os.homedir(), cwd: STARTUP_CWD });
|
|
468
477
|
return;
|
|
469
478
|
}
|
|
470
479
|
|
|
@@ -474,20 +483,15 @@ const server = http.createServer(async (req, res) => {
|
|
|
474
483
|
for await (const chunk of req) chunks.push(chunk);
|
|
475
484
|
const audioBuffer = Buffer.concat(chunks);
|
|
476
485
|
if (audioBuffer.length === 0) {
|
|
477
|
-
|
|
478
|
-
res.end(JSON.stringify({ error: 'No audio data' }));
|
|
486
|
+
sendJSON(req, res, 400, { error: 'No audio data' });
|
|
479
487
|
return;
|
|
480
488
|
}
|
|
481
489
|
const { transcribe } = await getSpeech();
|
|
482
490
|
const text = await transcribe(audioBuffer);
|
|
483
|
-
|
|
484
|
-
res.end(JSON.stringify({ text: (text || '').trim() }));
|
|
491
|
+
sendJSON(req, res, 200, { text: (text || '').trim() });
|
|
485
492
|
} catch (err) {
|
|
486
493
|
debugLog('[STT] Error: ' + err.message);
|
|
487
|
-
if (!res.headersSent) {
|
|
488
|
-
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
489
|
-
}
|
|
490
|
-
res.end(JSON.stringify({ error: err.message || 'STT failed' }));
|
|
494
|
+
if (!res.headersSent) sendJSON(req, res, 500, { error: err.message || 'STT failed' });
|
|
491
495
|
}
|
|
492
496
|
return;
|
|
493
497
|
}
|
|
@@ -497,8 +501,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
497
501
|
const body = await parseBody(req);
|
|
498
502
|
const text = body.text || '';
|
|
499
503
|
if (!text) {
|
|
500
|
-
|
|
501
|
-
res.end(JSON.stringify({ error: 'No text provided' }));
|
|
504
|
+
sendJSON(req, res, 400, { error: 'No text provided' });
|
|
502
505
|
return;
|
|
503
506
|
}
|
|
504
507
|
const { synthesize } = await getSpeech();
|
|
@@ -507,10 +510,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
507
510
|
res.end(wavBuffer);
|
|
508
511
|
} catch (err) {
|
|
509
512
|
debugLog('[TTS] Error: ' + err.message);
|
|
510
|
-
if (!res.headersSent) {
|
|
511
|
-
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
512
|
-
}
|
|
513
|
-
res.end(JSON.stringify({ error: err.message || 'TTS failed' }));
|
|
513
|
+
if (!res.headersSent) sendJSON(req, res, 500, { error: err.message || 'TTS failed' });
|
|
514
514
|
}
|
|
515
515
|
return;
|
|
516
516
|
}
|
|
@@ -518,11 +518,9 @@ const server = http.createServer(async (req, res) => {
|
|
|
518
518
|
if (routePath === '/api/speech-status' && req.method === 'GET') {
|
|
519
519
|
try {
|
|
520
520
|
const { getStatus } = await getSpeech();
|
|
521
|
-
|
|
522
|
-
res.end(JSON.stringify(getStatus()));
|
|
521
|
+
sendJSON(req, res, 200, getStatus());
|
|
523
522
|
} catch (err) {
|
|
524
|
-
|
|
525
|
-
res.end(JSON.stringify({ sttReady: false, ttsReady: false, sttLoading: false, ttsLoading: false }));
|
|
523
|
+
sendJSON(req, res, 200, { sttReady: false, ttsReady: false, sttLoading: false, ttsLoading: false });
|
|
526
524
|
}
|
|
527
525
|
return;
|
|
528
526
|
}
|
|
@@ -538,11 +536,9 @@ const server = http.createServer(async (req, res) => {
|
|
|
538
536
|
.filter(e => e.isDirectory() && !e.name.startsWith('.'))
|
|
539
537
|
.map(e => ({ name: e.name }))
|
|
540
538
|
.sort((a, b) => a.name.localeCompare(b.name));
|
|
541
|
-
|
|
542
|
-
res.end(JSON.stringify({ folders }));
|
|
539
|
+
sendJSON(req, res, 200, { folders });
|
|
543
540
|
} catch (err) {
|
|
544
|
-
|
|
545
|
-
res.end(JSON.stringify({ error: err.message }));
|
|
541
|
+
sendJSON(req, res, 400, { error: err.message });
|
|
546
542
|
}
|
|
547
543
|
return;
|
|
548
544
|
}
|
|
@@ -565,8 +561,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
565
561
|
res.writeHead(200, { 'Content-Type': contentType, 'Cache-Control': 'no-cache' });
|
|
566
562
|
res.end(fileContent);
|
|
567
563
|
} catch (err) {
|
|
568
|
-
|
|
569
|
-
res.end(JSON.stringify({ error: err.message }));
|
|
564
|
+
sendJSON(req, res, 400, { error: err.message });
|
|
570
565
|
}
|
|
571
566
|
return;
|
|
572
567
|
}
|
|
@@ -574,7 +569,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
574
569
|
// Handle conversation detail routes - serve index.html for client-side routing
|
|
575
570
|
if (pathOnly.match(/^\/conversations\/[^\/]+$/)) {
|
|
576
571
|
const indexPath = path.join(staticDir, 'index.html');
|
|
577
|
-
serveFile(indexPath, res);
|
|
572
|
+
serveFile(indexPath, res, req);
|
|
578
573
|
return;
|
|
579
574
|
}
|
|
580
575
|
|
|
@@ -589,34 +584,59 @@ const server = http.createServer(async (req, res) => {
|
|
|
589
584
|
filePath = path.join(filePath, 'index.html');
|
|
590
585
|
fs.stat(filePath, (err2) => {
|
|
591
586
|
if (err2) { res.writeHead(404); res.end('Not found'); return; }
|
|
592
|
-
serveFile(filePath, res);
|
|
587
|
+
serveFile(filePath, res, req);
|
|
593
588
|
});
|
|
594
589
|
} else {
|
|
595
|
-
serveFile(filePath, res);
|
|
590
|
+
serveFile(filePath, res, req);
|
|
596
591
|
}
|
|
597
592
|
});
|
|
598
593
|
} catch (e) {
|
|
599
594
|
console.error('Server error:', e.message);
|
|
600
|
-
|
|
601
|
-
res.end(JSON.stringify({ error: e.message }));
|
|
595
|
+
sendJSON(req, res, 500, { error: e.message });
|
|
602
596
|
}
|
|
603
597
|
});
|
|
604
598
|
|
|
605
599
|
const MIME_TYPES = { '.html': 'text/html; charset=utf-8', '.js': 'application/javascript; charset=utf-8', '.css': 'text/css; charset=utf-8', '.json': 'application/json', '.png': 'image/png', '.jpg': 'image/jpeg', '.svg': 'image/svg+xml' };
|
|
606
600
|
|
|
607
|
-
function
|
|
601
|
+
function generateETag(stats) {
|
|
602
|
+
return `"${stats.mtimeMs.toString(36)}-${stats.size.toString(36)}"`;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
function serveFile(filePath, res, req) {
|
|
608
606
|
const ext = path.extname(filePath).toLowerCase();
|
|
609
607
|
const contentType = MIME_TYPES[ext] || 'application/octet-stream';
|
|
610
608
|
|
|
611
609
|
if (ext !== '.html') {
|
|
612
610
|
fs.stat(filePath, (err, stats) => {
|
|
613
611
|
if (err) { res.writeHead(500); res.end('Server error'); return; }
|
|
614
|
-
|
|
612
|
+
const etag = generateETag(stats);
|
|
613
|
+
if (req && req.headers['if-none-match'] === etag) {
|
|
614
|
+
res.writeHead(304);
|
|
615
|
+
res.end();
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
const headers = {
|
|
615
619
|
'Content-Type': contentType,
|
|
616
620
|
'Content-Length': stats.size,
|
|
617
|
-
'
|
|
618
|
-
|
|
619
|
-
|
|
621
|
+
'ETag': etag,
|
|
622
|
+
'Cache-Control': 'public, max-age=3600, must-revalidate'
|
|
623
|
+
};
|
|
624
|
+
if (acceptsEncoding(req, 'br') && stats.size > 860) {
|
|
625
|
+
const stream = fs.createReadStream(filePath);
|
|
626
|
+
headers['Content-Encoding'] = 'br';
|
|
627
|
+
delete headers['Content-Length'];
|
|
628
|
+
res.writeHead(200, headers);
|
|
629
|
+
stream.pipe(zlib.createBrotliCompress({ params: { [zlib.constants.BROTLI_PARAM_QUALITY]: 4 } })).pipe(res);
|
|
630
|
+
} else if (acceptsEncoding(req, 'gzip') && stats.size > 860) {
|
|
631
|
+
const stream = fs.createReadStream(filePath);
|
|
632
|
+
headers['Content-Encoding'] = 'gzip';
|
|
633
|
+
delete headers['Content-Length'];
|
|
634
|
+
res.writeHead(200, headers);
|
|
635
|
+
stream.pipe(zlib.createGzip({ level: 6 })).pipe(res);
|
|
636
|
+
} else {
|
|
637
|
+
res.writeHead(200, headers);
|
|
638
|
+
fs.createReadStream(filePath).pipe(res);
|
|
639
|
+
}
|
|
620
640
|
});
|
|
621
641
|
return;
|
|
622
642
|
}
|
|
@@ -629,24 +649,52 @@ function serveFile(filePath, res) {
|
|
|
629
649
|
if (watch) {
|
|
630
650
|
content += `\n<script>(function(){const ws=new WebSocket('ws://'+location.host+'${BASE_URL}/hot-reload');ws.onmessage=e=>{if(JSON.parse(e.data).type==='reload')location.reload()};})();</script>`;
|
|
631
651
|
}
|
|
632
|
-
|
|
633
|
-
res.end(content);
|
|
652
|
+
compressAndSend(req, res, 200, contentType, content);
|
|
634
653
|
});
|
|
635
654
|
}
|
|
636
655
|
|
|
637
|
-
function
|
|
638
|
-
|
|
656
|
+
function createChunkBatcher() {
|
|
657
|
+
const pending = [];
|
|
658
|
+
let timer = null;
|
|
659
|
+
const BATCH_SIZE = 10;
|
|
660
|
+
const BATCH_INTERVAL = 50;
|
|
661
|
+
|
|
662
|
+
function flush() {
|
|
663
|
+
if (pending.length === 0) return;
|
|
664
|
+
const batch = pending.splice(0);
|
|
639
665
|
try {
|
|
640
|
-
|
|
666
|
+
const tx = queries._db ? queries._db.transaction(() => {
|
|
667
|
+
for (const c of batch) queries.createChunk(c.sessionId, c.conversationId, c.sequence, c.type, c.data);
|
|
668
|
+
}) : null;
|
|
669
|
+
if (tx) { tx(); } else {
|
|
670
|
+
for (const c of batch) {
|
|
671
|
+
try { queries.createChunk(c.sessionId, c.conversationId, c.sequence, c.type, c.data); } catch (e) { debugLog(`[chunk] ${e.message}`); }
|
|
672
|
+
}
|
|
673
|
+
}
|
|
641
674
|
} catch (err) {
|
|
642
|
-
debugLog(`[chunk]
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
return null;
|
|
675
|
+
debugLog(`[chunk-batch] Batch write failed: ${err.message}`);
|
|
676
|
+
for (const c of batch) {
|
|
677
|
+
try { queries.createChunk(c.sessionId, c.conversationId, c.sequence, c.type, c.data); } catch (_) {}
|
|
646
678
|
}
|
|
647
679
|
}
|
|
648
680
|
}
|
|
649
|
-
|
|
681
|
+
|
|
682
|
+
function add(sessionId, conversationId, sequence, blockType, blockData) {
|
|
683
|
+
pending.push({ sessionId, conversationId, sequence, type: blockType, data: blockData });
|
|
684
|
+
if (pending.length >= BATCH_SIZE) {
|
|
685
|
+
if (timer) { clearTimeout(timer); timer = null; }
|
|
686
|
+
flush();
|
|
687
|
+
} else if (!timer) {
|
|
688
|
+
timer = setTimeout(() => { timer = null; flush(); }, BATCH_INTERVAL);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
function drain() {
|
|
693
|
+
if (timer) { clearTimeout(timer); timer = null; }
|
|
694
|
+
flush();
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
return { add, drain };
|
|
650
698
|
}
|
|
651
699
|
|
|
652
700
|
async function processMessageWithStreaming(conversationId, messageId, sessionId, content, agentId) {
|
|
@@ -665,6 +713,7 @@ async function processMessageWithStreaming(conversationId, messageId, sessionId,
|
|
|
665
713
|
let allBlocks = [];
|
|
666
714
|
let eventCount = 0;
|
|
667
715
|
let currentSequence = queries.getMaxSequence(sessionId) ?? -1;
|
|
716
|
+
const batcher = createChunkBatcher();
|
|
668
717
|
|
|
669
718
|
const onEvent = (parsed) => {
|
|
670
719
|
eventCount++;
|
|
@@ -683,7 +732,7 @@ async function processMessageWithStreaming(conversationId, messageId, sessionId,
|
|
|
683
732
|
};
|
|
684
733
|
|
|
685
734
|
currentSequence++;
|
|
686
|
-
|
|
735
|
+
batcher.add(sessionId, conversationId, currentSequence, 'system', systemBlock);
|
|
687
736
|
|
|
688
737
|
broadcastSync({
|
|
689
738
|
type: 'streaming_progress',
|
|
@@ -698,7 +747,7 @@ async function processMessageWithStreaming(conversationId, messageId, sessionId,
|
|
|
698
747
|
allBlocks.push(block);
|
|
699
748
|
|
|
700
749
|
currentSequence++;
|
|
701
|
-
|
|
750
|
+
batcher.add(sessionId, conversationId, currentSequence, block.type || 'assistant', block);
|
|
702
751
|
|
|
703
752
|
broadcastSync({
|
|
704
753
|
type: 'streaming_progress',
|
|
@@ -720,7 +769,7 @@ async function processMessageWithStreaming(conversationId, messageId, sessionId,
|
|
|
720
769
|
};
|
|
721
770
|
|
|
722
771
|
currentSequence++;
|
|
723
|
-
|
|
772
|
+
batcher.add(sessionId, conversationId, currentSequence, 'tool_result', toolResultBlock);
|
|
724
773
|
|
|
725
774
|
broadcastSync({
|
|
726
775
|
type: 'streaming_progress',
|
|
@@ -744,7 +793,7 @@ async function processMessageWithStreaming(conversationId, messageId, sessionId,
|
|
|
744
793
|
};
|
|
745
794
|
|
|
746
795
|
currentSequence++;
|
|
747
|
-
|
|
796
|
+
batcher.add(sessionId, conversationId, currentSequence, 'result', resultBlock);
|
|
748
797
|
|
|
749
798
|
broadcastSync({
|
|
750
799
|
type: 'streaming_progress',
|
|
@@ -777,6 +826,7 @@ async function processMessageWithStreaming(conversationId, messageId, sessionId,
|
|
|
777
826
|
};
|
|
778
827
|
|
|
779
828
|
const { outputs, sessionId: claudeSessionId } = await runClaudeWithStreaming(content, cwd, agentId || 'claude-code', config);
|
|
829
|
+
batcher.drain();
|
|
780
830
|
debugLog(`[stream] Claude returned ${outputs.length} outputs, sessionId=${claudeSessionId}`);
|
|
781
831
|
|
|
782
832
|
if (claudeSessionId && !conv?.claudeSessionId) {
|
|
@@ -828,6 +878,7 @@ async function processMessageWithStreaming(conversationId, messageId, sessionId,
|
|
|
828
878
|
timestamp: Date.now()
|
|
829
879
|
});
|
|
830
880
|
} finally {
|
|
881
|
+
batcher.drain();
|
|
831
882
|
activeExecutions.delete(conversationId);
|
|
832
883
|
queries.setIsStreaming(conversationId, false);
|
|
833
884
|
drainMessageQueue(conversationId);
|
|
@@ -915,9 +966,16 @@ async function processMessage(conversationId, messageId, content, agentId) {
|
|
|
915
966
|
}
|
|
916
967
|
}
|
|
917
968
|
|
|
918
|
-
const wss = new WebSocketServer({
|
|
969
|
+
const wss = new WebSocketServer({
|
|
970
|
+
server,
|
|
971
|
+
perMessageDeflate: {
|
|
972
|
+
zlibDeflateOptions: { level: 6 },
|
|
973
|
+
threshold: 256
|
|
974
|
+
}
|
|
975
|
+
});
|
|
919
976
|
const hotReloadClients = [];
|
|
920
977
|
const syncClients = new Set();
|
|
978
|
+
const subscriptionIndex = new Map();
|
|
921
979
|
|
|
922
980
|
wss.on('connection', (ws, req) => {
|
|
923
981
|
// req.url in WebSocket is just the path (e.g., '/gm/sync'), not a full URL
|
|
@@ -941,8 +999,17 @@ wss.on('connection', (ws, req) => {
|
|
|
941
999
|
try {
|
|
942
1000
|
const data = JSON.parse(msg);
|
|
943
1001
|
if (data.type === 'subscribe') {
|
|
944
|
-
if (data.sessionId)
|
|
945
|
-
|
|
1002
|
+
if (data.sessionId) {
|
|
1003
|
+
ws.subscriptions.add(data.sessionId);
|
|
1004
|
+
if (!subscriptionIndex.has(data.sessionId)) subscriptionIndex.set(data.sessionId, new Set());
|
|
1005
|
+
subscriptionIndex.get(data.sessionId).add(ws);
|
|
1006
|
+
}
|
|
1007
|
+
if (data.conversationId) {
|
|
1008
|
+
const key = `conv-${data.conversationId}`;
|
|
1009
|
+
ws.subscriptions.add(key);
|
|
1010
|
+
if (!subscriptionIndex.has(key)) subscriptionIndex.set(key, new Set());
|
|
1011
|
+
subscriptionIndex.get(key).add(ws);
|
|
1012
|
+
}
|
|
946
1013
|
const subTarget = data.sessionId || data.conversationId;
|
|
947
1014
|
debugLog(`[WebSocket] Client ${ws.clientId} subscribed to ${subTarget}`);
|
|
948
1015
|
ws.send(JSON.stringify({
|
|
@@ -953,6 +1020,8 @@ wss.on('connection', (ws, req) => {
|
|
|
953
1020
|
}));
|
|
954
1021
|
} else if (data.type === 'unsubscribe') {
|
|
955
1022
|
ws.subscriptions.delete(data.sessionId);
|
|
1023
|
+
const idx = subscriptionIndex.get(data.sessionId);
|
|
1024
|
+
if (idx) { idx.delete(ws); if (idx.size === 0) subscriptionIndex.delete(data.sessionId); }
|
|
956
1025
|
debugLog(`[WebSocket] Client ${ws.clientId} unsubscribed from ${data.sessionId}`);
|
|
957
1026
|
} else if (data.type === 'get_subscriptions') {
|
|
958
1027
|
ws.send(JSON.stringify({
|
|
@@ -979,6 +1048,10 @@ wss.on('connection', (ws, req) => {
|
|
|
979
1048
|
ws.on('pong', () => { ws.isAlive = true; });
|
|
980
1049
|
ws.on('close', () => {
|
|
981
1050
|
syncClients.delete(ws);
|
|
1051
|
+
for (const sub of ws.subscriptions) {
|
|
1052
|
+
const idx = subscriptionIndex.get(sub);
|
|
1053
|
+
if (idx) { idx.delete(ws); if (idx.size === 0) subscriptionIndex.delete(sub); }
|
|
1054
|
+
}
|
|
982
1055
|
console.log(`[WebSocket] Client ${ws.clientId} disconnected`);
|
|
983
1056
|
});
|
|
984
1057
|
}
|
|
@@ -990,19 +1063,52 @@ const BROADCAST_TYPES = new Set([
|
|
|
990
1063
|
'streaming_complete', 'streaming_error'
|
|
991
1064
|
]);
|
|
992
1065
|
|
|
1066
|
+
const wsBatchQueues = new Map();
|
|
1067
|
+
const WS_BATCH_INTERVAL = 16;
|
|
1068
|
+
|
|
1069
|
+
function flushWsBatch(ws) {
|
|
1070
|
+
const queue = wsBatchQueues.get(ws);
|
|
1071
|
+
if (!queue || queue.msgs.length === 0) return;
|
|
1072
|
+
if (ws.readyState !== 1) { wsBatchQueues.delete(ws); return; }
|
|
1073
|
+
if (queue.msgs.length === 1) {
|
|
1074
|
+
ws.send(queue.msgs[0]);
|
|
1075
|
+
} else {
|
|
1076
|
+
ws.send('[' + queue.msgs.join(',') + ']');
|
|
1077
|
+
}
|
|
1078
|
+
queue.msgs.length = 0;
|
|
1079
|
+
queue.timer = null;
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
function sendToClient(ws, data) {
|
|
1083
|
+
if (ws.readyState !== 1) return;
|
|
1084
|
+
let queue = wsBatchQueues.get(ws);
|
|
1085
|
+
if (!queue) { queue = { msgs: [], timer: null }; wsBatchQueues.set(ws, queue); }
|
|
1086
|
+
queue.msgs.push(data);
|
|
1087
|
+
if (!queue.timer) {
|
|
1088
|
+
queue.timer = setTimeout(() => flushWsBatch(ws), WS_BATCH_INTERVAL);
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
|
|
993
1092
|
function broadcastSync(event) {
|
|
994
1093
|
if (syncClients.size === 0) return;
|
|
995
1094
|
const data = JSON.stringify(event);
|
|
996
1095
|
const isBroadcast = BROADCAST_TYPES.has(event.type);
|
|
997
1096
|
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1097
|
+
if (isBroadcast) {
|
|
1098
|
+
for (const ws of syncClients) sendToClient(ws, data);
|
|
1099
|
+
return;
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
const targets = new Set();
|
|
1103
|
+
if (event.sessionId) {
|
|
1104
|
+
const subs = subscriptionIndex.get(event.sessionId);
|
|
1105
|
+
if (subs) for (const ws of subs) targets.add(ws);
|
|
1106
|
+
}
|
|
1107
|
+
if (event.conversationId) {
|
|
1108
|
+
const subs = subscriptionIndex.get(`conv-${event.conversationId}`);
|
|
1109
|
+
if (subs) for (const ws of subs) targets.add(ws);
|
|
1005
1110
|
}
|
|
1111
|
+
for (const ws of targets) sendToClient(ws, data);
|
|
1006
1112
|
}
|
|
1007
1113
|
|
|
1008
1114
|
// Heartbeat interval to detect stale connections
|
|
@@ -1166,18 +1272,38 @@ function onServerReady() {
|
|
|
1166
1272
|
|
|
1167
1273
|
}
|
|
1168
1274
|
|
|
1275
|
+
const importMtimeCache = new Map();
|
|
1276
|
+
|
|
1277
|
+
function hasIndexFilesChanged() {
|
|
1278
|
+
const projectsDir = path.join(os.homedir(), '.claude', 'projects');
|
|
1279
|
+
if (!fs.existsSync(projectsDir)) return false;
|
|
1280
|
+
let changed = false;
|
|
1281
|
+
try {
|
|
1282
|
+
const dirs = fs.readdirSync(projectsDir);
|
|
1283
|
+
for (const d of dirs) {
|
|
1284
|
+
const indexPath = path.join(projectsDir, d, 'sessions-index.json');
|
|
1285
|
+
try {
|
|
1286
|
+
const stat = fs.statSync(indexPath);
|
|
1287
|
+
const cached = importMtimeCache.get(indexPath);
|
|
1288
|
+
if (!cached || cached < stat.mtimeMs) {
|
|
1289
|
+
importMtimeCache.set(indexPath, stat.mtimeMs);
|
|
1290
|
+
changed = true;
|
|
1291
|
+
}
|
|
1292
|
+
} catch (_) {}
|
|
1293
|
+
}
|
|
1294
|
+
} catch (_) {}
|
|
1295
|
+
return changed;
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1169
1298
|
function performAutoImport() {
|
|
1170
1299
|
try {
|
|
1300
|
+
if (!hasIndexFilesChanged()) return;
|
|
1171
1301
|
const imported = queries.importClaudeCodeConversations();
|
|
1172
1302
|
if (imported.length > 0) {
|
|
1173
1303
|
const importedCount = imported.filter(i => i.status === 'imported').length;
|
|
1174
|
-
const skippedCount = imported.filter(i => i.status === 'skipped').length;
|
|
1175
1304
|
if (importedCount > 0) {
|
|
1176
|
-
console.log(`[AUTO-IMPORT] Imported ${importedCount} new Claude Code conversations
|
|
1177
|
-
// Broadcast to all connected clients that conversations were updated
|
|
1305
|
+
console.log(`[AUTO-IMPORT] Imported ${importedCount} new Claude Code conversations`);
|
|
1178
1306
|
broadcastSync({ type: 'conversations_updated', count: importedCount });
|
|
1179
|
-
} else if (skippedCount > 0) {
|
|
1180
|
-
// All conversations already imported, don't spam logs
|
|
1181
1307
|
}
|
|
1182
1308
|
}
|
|
1183
1309
|
} catch (err) {
|