@siteboon/claude-code-ui 1.8.12 → 1.9.0
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/dist/assets/index-Bmo7Hu70.css +32 -0
- package/dist/assets/index-D3NZxyU6.js +793 -0
- package/dist/assets/vendor-codemirror-D2k1L1JZ.js +39 -0
- package/dist/assets/vendor-react-7V_UDHjJ.js +59 -0
- package/dist/assets/vendor-xterm-jI4BCHEb.js +66 -0
- package/dist/clear-cache.html +85 -0
- package/dist/index.html +5 -2
- package/package.json +6 -3
- package/server/claude-sdk.js +513 -0
- package/server/cursor-cli.js +14 -2
- package/server/database/auth.db +0 -0
- package/server/index.js +224 -38
- package/server/projects.js +108 -17
- package/server/routes/commands.js +572 -0
- package/server/utils/commandParser.js +303 -0
- package/dist/assets/index-Cl5xisCA.js +0 -895
- package/dist/assets/index-Co7ALK3i.css +0 -32
- package/server/claude-cli.js +0 -397
package/server/index.js
CHANGED
|
@@ -27,25 +27,26 @@ try {
|
|
|
27
27
|
console.log('PORT from env:', process.env.PORT);
|
|
28
28
|
|
|
29
29
|
import express from 'express';
|
|
30
|
-
import { WebSocketServer } from 'ws';
|
|
30
|
+
import { WebSocketServer, WebSocket } from 'ws';
|
|
31
|
+
import os from 'os';
|
|
31
32
|
import http from 'http';
|
|
32
33
|
import cors from 'cors';
|
|
33
34
|
import { promises as fsPromises } from 'fs';
|
|
34
35
|
import { spawn } from 'child_process';
|
|
35
|
-
import os from 'os';
|
|
36
36
|
import pty from 'node-pty';
|
|
37
37
|
import fetch from 'node-fetch';
|
|
38
38
|
import mime from 'mime-types';
|
|
39
39
|
|
|
40
40
|
import { getProjects, getSessions, getSessionMessages, renameProject, deleteSession, deleteProject, addProjectManually, extractProjectDirectory, clearProjectDirectoryCache } from './projects.js';
|
|
41
|
-
import {
|
|
42
|
-
import { spawnCursor, abortCursorSession } from './cursor-cli.js';
|
|
41
|
+
import { queryClaudeSDK, abortClaudeSDKSession, isClaudeSDKSessionActive, getActiveClaudeSDKSessions } from './claude-sdk.js';
|
|
42
|
+
import { spawnCursor, abortCursorSession, isCursorSessionActive, getActiveCursorSessions } from './cursor-cli.js';
|
|
43
43
|
import gitRoutes from './routes/git.js';
|
|
44
44
|
import authRoutes from './routes/auth.js';
|
|
45
45
|
import mcpRoutes from './routes/mcp.js';
|
|
46
46
|
import cursorRoutes from './routes/cursor.js';
|
|
47
47
|
import taskmasterRoutes from './routes/taskmaster.js';
|
|
48
48
|
import mcpUtilsRoutes from './routes/mcp-utils.js';
|
|
49
|
+
import commandsRoutes from './routes/commands.js';
|
|
49
50
|
import { initializeDatabase } from './database/db.js';
|
|
50
51
|
import { validateApiKey, authenticateToken, authenticateWebSocket } from './middleware/auth.js';
|
|
51
52
|
|
|
@@ -107,7 +108,7 @@ async function setupProjectsWatcher() {
|
|
|
107
108
|
});
|
|
108
109
|
|
|
109
110
|
connectedClients.forEach(client => {
|
|
110
|
-
if (client.readyState ===
|
|
111
|
+
if (client.readyState === WebSocket.OPEN) {
|
|
111
112
|
client.send(updateMessage);
|
|
112
113
|
}
|
|
113
114
|
});
|
|
@@ -192,8 +193,24 @@ app.use('/api/taskmaster', authenticateToken, taskmasterRoutes);
|
|
|
192
193
|
// MCP utilities
|
|
193
194
|
app.use('/api/mcp-utils', authenticateToken, mcpUtilsRoutes);
|
|
194
195
|
|
|
196
|
+
// Commands API Routes (protected)
|
|
197
|
+
app.use('/api/commands', authenticateToken, commandsRoutes);
|
|
198
|
+
|
|
195
199
|
// Static files served after API routes
|
|
196
|
-
|
|
200
|
+
// Add cache control: HTML files should not be cached, but assets can be cached
|
|
201
|
+
app.use(express.static(path.join(__dirname, '../dist'), {
|
|
202
|
+
setHeaders: (res, filePath) => {
|
|
203
|
+
if (filePath.endsWith('.html')) {
|
|
204
|
+
// Prevent HTML caching to avoid service worker issues after builds
|
|
205
|
+
res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
|
|
206
|
+
res.setHeader('Pragma', 'no-cache');
|
|
207
|
+
res.setHeader('Expires', '0');
|
|
208
|
+
} else if (filePath.match(/\.(js|css|woff2?|ttf|eot|svg|png|jpg|jpeg|gif|ico)$/)) {
|
|
209
|
+
// Cache static assets for 1 year (they have hashed names)
|
|
210
|
+
res.setHeader('Cache-Control', 'public, max-age=31536000, immutable');
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}));
|
|
197
214
|
|
|
198
215
|
// API Routes (protected)
|
|
199
216
|
app.get('/api/config', authenticateToken, (req, res) => {
|
|
@@ -370,15 +387,24 @@ app.get('/api/projects/:projectName/file', authenticateToken, async (req, res) =
|
|
|
370
387
|
|
|
371
388
|
console.log('📄 File read request:', projectName, filePath);
|
|
372
389
|
|
|
373
|
-
//
|
|
374
|
-
|
|
375
|
-
// Security check - ensure the path is safe and absolute
|
|
376
|
-
if (!filePath || !path.isAbsolute(filePath)) {
|
|
390
|
+
// Security: ensure the requested path is inside the project root
|
|
391
|
+
if (!filePath) {
|
|
377
392
|
return res.status(400).json({ error: 'Invalid file path' });
|
|
378
393
|
}
|
|
379
394
|
|
|
380
|
-
const
|
|
381
|
-
|
|
395
|
+
const projectRoot = await extractProjectDirectory(projectName).catch(() => null);
|
|
396
|
+
if (!projectRoot) {
|
|
397
|
+
return res.status(404).json({ error: 'Project not found' });
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const resolved = path.resolve(filePath);
|
|
401
|
+
const normalizedRoot = path.resolve(projectRoot) + path.sep;
|
|
402
|
+
if (!resolved.startsWith(normalizedRoot)) {
|
|
403
|
+
return res.status(403).json({ error: 'Path must be under project root' });
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const content = await fsPromises.readFile(resolved, 'utf8');
|
|
407
|
+
res.json({ content, path: resolved });
|
|
382
408
|
} catch (error) {
|
|
383
409
|
console.error('Error reading file:', error);
|
|
384
410
|
if (error.code === 'ENOENT') {
|
|
@@ -399,27 +425,35 @@ app.get('/api/projects/:projectName/files/content', authenticateToken, async (re
|
|
|
399
425
|
|
|
400
426
|
console.log('🖼️ Binary file serve request:', projectName, filePath);
|
|
401
427
|
|
|
402
|
-
//
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
// Security check - ensure the path is safe and absolute
|
|
406
|
-
if (!filePath || !path.isAbsolute(filePath)) {
|
|
428
|
+
// Security: ensure the requested path is inside the project root
|
|
429
|
+
if (!filePath) {
|
|
407
430
|
return res.status(400).json({ error: 'Invalid file path' });
|
|
408
431
|
}
|
|
409
432
|
|
|
433
|
+
const projectRoot = await extractProjectDirectory(projectName).catch(() => null);
|
|
434
|
+
if (!projectRoot) {
|
|
435
|
+
return res.status(404).json({ error: 'Project not found' });
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const resolved = path.resolve(filePath);
|
|
439
|
+
const normalizedRoot = path.resolve(projectRoot) + path.sep;
|
|
440
|
+
if (!resolved.startsWith(normalizedRoot)) {
|
|
441
|
+
return res.status(403).json({ error: 'Path must be under project root' });
|
|
442
|
+
}
|
|
443
|
+
|
|
410
444
|
// Check if file exists
|
|
411
445
|
try {
|
|
412
|
-
await fsPromises.access(
|
|
446
|
+
await fsPromises.access(resolved);
|
|
413
447
|
} catch (error) {
|
|
414
448
|
return res.status(404).json({ error: 'File not found' });
|
|
415
449
|
}
|
|
416
450
|
|
|
417
451
|
// Get file extension and set appropriate content type
|
|
418
|
-
const mimeType = mime.lookup(
|
|
452
|
+
const mimeType = mime.lookup(resolved) || 'application/octet-stream';
|
|
419
453
|
res.setHeader('Content-Type', mimeType);
|
|
420
454
|
|
|
421
455
|
// Stream the file
|
|
422
|
-
const fileStream = fs.createReadStream(
|
|
456
|
+
const fileStream = fs.createReadStream(resolved);
|
|
423
457
|
fileStream.pipe(res);
|
|
424
458
|
|
|
425
459
|
fileStream.on('error', (error) => {
|
|
@@ -445,10 +479,8 @@ app.put('/api/projects/:projectName/file', authenticateToken, async (req, res) =
|
|
|
445
479
|
|
|
446
480
|
console.log('💾 File save request:', projectName, filePath);
|
|
447
481
|
|
|
448
|
-
//
|
|
449
|
-
|
|
450
|
-
// Security check - ensure the path is safe and absolute
|
|
451
|
-
if (!filePath || !path.isAbsolute(filePath)) {
|
|
482
|
+
// Security: ensure the requested path is inside the project root
|
|
483
|
+
if (!filePath) {
|
|
452
484
|
return res.status(400).json({ error: 'Invalid file path' });
|
|
453
485
|
}
|
|
454
486
|
|
|
@@ -456,21 +488,32 @@ app.put('/api/projects/:projectName/file', authenticateToken, async (req, res) =
|
|
|
456
488
|
return res.status(400).json({ error: 'Content is required' });
|
|
457
489
|
}
|
|
458
490
|
|
|
491
|
+
const projectRoot = await extractProjectDirectory(projectName).catch(() => null);
|
|
492
|
+
if (!projectRoot) {
|
|
493
|
+
return res.status(404).json({ error: 'Project not found' });
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
const resolved = path.resolve(filePath);
|
|
497
|
+
const normalizedRoot = path.resolve(projectRoot) + path.sep;
|
|
498
|
+
if (!resolved.startsWith(normalizedRoot)) {
|
|
499
|
+
return res.status(403).json({ error: 'Path must be under project root' });
|
|
500
|
+
}
|
|
501
|
+
|
|
459
502
|
// Create backup of original file
|
|
460
503
|
try {
|
|
461
|
-
const backupPath =
|
|
462
|
-
await fsPromises.copyFile(
|
|
504
|
+
const backupPath = resolved + '.backup.' + Date.now();
|
|
505
|
+
await fsPromises.copyFile(resolved, backupPath);
|
|
463
506
|
console.log('📋 Created backup:', backupPath);
|
|
464
507
|
} catch (backupError) {
|
|
465
508
|
console.warn('Could not create backup:', backupError.message);
|
|
466
509
|
}
|
|
467
510
|
|
|
468
511
|
// Write the new content
|
|
469
|
-
await fsPromises.writeFile(
|
|
512
|
+
await fsPromises.writeFile(resolved, content, 'utf8');
|
|
470
513
|
|
|
471
514
|
res.json({
|
|
472
515
|
success: true,
|
|
473
|
-
path:
|
|
516
|
+
path: resolved,
|
|
474
517
|
message: 'File saved successfully'
|
|
475
518
|
});
|
|
476
519
|
} catch (error) {
|
|
@@ -550,7 +593,9 @@ function handleChatConnection(ws) {
|
|
|
550
593
|
console.log('💬 User message:', data.command || '[Continue/Resume]');
|
|
551
594
|
console.log('📁 Project:', data.options?.projectPath || 'Unknown');
|
|
552
595
|
console.log('🔄 Session:', data.options?.sessionId ? 'Resume' : 'New');
|
|
553
|
-
|
|
596
|
+
|
|
597
|
+
// Use Claude Agents SDK
|
|
598
|
+
await queryClaudeSDK(data.command, data.options, ws);
|
|
554
599
|
} else if (data.type === 'cursor-command') {
|
|
555
600
|
console.log('🖱️ Cursor message:', data.command || '[Continue/Resume]');
|
|
556
601
|
console.log('📁 Project:', data.options?.cwd || 'Unknown');
|
|
@@ -568,9 +613,15 @@ function handleChatConnection(ws) {
|
|
|
568
613
|
} else if (data.type === 'abort-session') {
|
|
569
614
|
console.log('🛑 Abort session request:', data.sessionId);
|
|
570
615
|
const provider = data.provider || 'claude';
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
616
|
+
let success;
|
|
617
|
+
|
|
618
|
+
if (provider === 'cursor') {
|
|
619
|
+
success = abortCursorSession(data.sessionId);
|
|
620
|
+
} else {
|
|
621
|
+
// Use Claude Agents SDK
|
|
622
|
+
success = await abortClaudeSDKSession(data.sessionId);
|
|
623
|
+
}
|
|
624
|
+
|
|
574
625
|
ws.send(JSON.stringify({
|
|
575
626
|
type: 'session-aborted',
|
|
576
627
|
sessionId: data.sessionId,
|
|
@@ -586,6 +637,35 @@ function handleChatConnection(ws) {
|
|
|
586
637
|
provider: 'cursor',
|
|
587
638
|
success
|
|
588
639
|
}));
|
|
640
|
+
} else if (data.type === 'check-session-status') {
|
|
641
|
+
// Check if a specific session is currently processing
|
|
642
|
+
const provider = data.provider || 'claude';
|
|
643
|
+
const sessionId = data.sessionId;
|
|
644
|
+
let isActive;
|
|
645
|
+
|
|
646
|
+
if (provider === 'cursor') {
|
|
647
|
+
isActive = isCursorSessionActive(sessionId);
|
|
648
|
+
} else {
|
|
649
|
+
// Use Claude Agents SDK
|
|
650
|
+
isActive = isClaudeSDKSessionActive(sessionId);
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
ws.send(JSON.stringify({
|
|
654
|
+
type: 'session-status',
|
|
655
|
+
sessionId,
|
|
656
|
+
provider,
|
|
657
|
+
isProcessing: isActive
|
|
658
|
+
}));
|
|
659
|
+
} else if (data.type === 'get-active-sessions') {
|
|
660
|
+
// Get all currently active sessions
|
|
661
|
+
const activeSessions = {
|
|
662
|
+
claude: getActiveClaudeSDKSessions(),
|
|
663
|
+
cursor: getActiveCursorSessions()
|
|
664
|
+
};
|
|
665
|
+
ws.send(JSON.stringify({
|
|
666
|
+
type: 'active-sessions',
|
|
667
|
+
sessions: activeSessions
|
|
668
|
+
}));
|
|
589
669
|
}
|
|
590
670
|
} catch (error) {
|
|
591
671
|
console.error('❌ Chat WebSocket error:', error.message);
|
|
@@ -714,7 +794,7 @@ function handleShellConnection(ws) {
|
|
|
714
794
|
|
|
715
795
|
// Handle data output
|
|
716
796
|
shellProcess.onData((data) => {
|
|
717
|
-
if (ws.readyState ===
|
|
797
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
718
798
|
let outputData = data;
|
|
719
799
|
|
|
720
800
|
// Check for various URL opening patterns
|
|
@@ -761,7 +841,7 @@ function handleShellConnection(ws) {
|
|
|
761
841
|
// Handle process exit
|
|
762
842
|
shellProcess.onExit((exitCode) => {
|
|
763
843
|
console.log('🔚 Shell process exited with code:', exitCode.exitCode, 'signal:', exitCode.signal);
|
|
764
|
-
if (ws.readyState ===
|
|
844
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
765
845
|
ws.send(JSON.stringify({
|
|
766
846
|
type: 'output',
|
|
767
847
|
data: `\r\n\x1b[33mProcess exited with code ${exitCode.exitCode}${exitCode.signal ? ` (${exitCode.signal})` : ''}\x1b[0m\r\n`
|
|
@@ -798,7 +878,7 @@ function handleShellConnection(ws) {
|
|
|
798
878
|
}
|
|
799
879
|
} catch (error) {
|
|
800
880
|
console.error('❌ Shell WebSocket error:', error.message);
|
|
801
|
-
if (ws.readyState ===
|
|
881
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
802
882
|
ws.send(JSON.stringify({
|
|
803
883
|
type: 'output',
|
|
804
884
|
data: `\r\n\x1b[31mError: ${error.message}\x1b[0m\r\n`
|
|
@@ -1053,13 +1133,116 @@ app.post('/api/projects/:projectName/upload-images', authenticateToken, async (r
|
|
|
1053
1133
|
}
|
|
1054
1134
|
});
|
|
1055
1135
|
|
|
1056
|
-
//
|
|
1136
|
+
// Get token usage for a specific session
|
|
1137
|
+
app.get('/api/projects/:projectName/sessions/:sessionId/token-usage', authenticateToken, async (req, res) => {
|
|
1138
|
+
try {
|
|
1139
|
+
const { projectName, sessionId } = req.params;
|
|
1140
|
+
const homeDir = os.homedir();
|
|
1141
|
+
|
|
1142
|
+
// Extract actual project path
|
|
1143
|
+
let projectPath;
|
|
1144
|
+
try {
|
|
1145
|
+
projectPath = await extractProjectDirectory(projectName);
|
|
1146
|
+
} catch (error) {
|
|
1147
|
+
console.error('Error extracting project directory:', error);
|
|
1148
|
+
return res.status(500).json({ error: 'Failed to determine project path' });
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
// Construct the JSONL file path
|
|
1152
|
+
// Claude stores session files in ~/.claude/projects/[encoded-project-path]/[session-id].jsonl
|
|
1153
|
+
// The encoding replaces /, spaces, ~, and _ with -
|
|
1154
|
+
const encodedPath = projectPath.replace(/[\\/:\s~_]/g, '-');
|
|
1155
|
+
const projectDir = path.join(homeDir, '.claude', 'projects', encodedPath);
|
|
1156
|
+
|
|
1157
|
+
// Allow only safe characters in sessionId
|
|
1158
|
+
const safeSessionId = String(sessionId).replace(/[^a-zA-Z0-9._-]/g, '');
|
|
1159
|
+
if (!safeSessionId) {
|
|
1160
|
+
return res.status(400).json({ error: 'Invalid sessionId' });
|
|
1161
|
+
}
|
|
1162
|
+
const jsonlPath = path.join(projectDir, `${safeSessionId}.jsonl`);
|
|
1163
|
+
|
|
1164
|
+
// Constrain to projectDir
|
|
1165
|
+
const rel = path.relative(path.resolve(projectDir), path.resolve(jsonlPath));
|
|
1166
|
+
if (rel.startsWith('..') || path.isAbsolute(rel)) {
|
|
1167
|
+
return res.status(400).json({ error: 'Invalid path' });
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
// Read and parse the JSONL file
|
|
1171
|
+
let fileContent;
|
|
1172
|
+
try {
|
|
1173
|
+
fileContent = await fsPromises.readFile(jsonlPath, 'utf8');
|
|
1174
|
+
} catch (error) {
|
|
1175
|
+
if (error.code === 'ENOENT') {
|
|
1176
|
+
return res.status(404).json({ error: 'Session file not found', path: jsonlPath });
|
|
1177
|
+
}
|
|
1178
|
+
throw error; // Re-throw other errors to be caught by outer try-catch
|
|
1179
|
+
}
|
|
1180
|
+
const lines = fileContent.trim().split('\n');
|
|
1181
|
+
|
|
1182
|
+
const parsedContextWindow = parseInt(process.env.CONTEXT_WINDOW, 10);
|
|
1183
|
+
const contextWindow = Number.isFinite(parsedContextWindow) ? parsedContextWindow : 160000;
|
|
1184
|
+
let inputTokens = 0;
|
|
1185
|
+
let cacheCreationTokens = 0;
|
|
1186
|
+
let cacheReadTokens = 0;
|
|
1187
|
+
|
|
1188
|
+
// Find the latest assistant message with usage data (scan from end)
|
|
1189
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
1190
|
+
try {
|
|
1191
|
+
const entry = JSON.parse(lines[i]);
|
|
1192
|
+
|
|
1193
|
+
// Only count assistant messages which have usage data
|
|
1194
|
+
if (entry.type === 'assistant' && entry.message?.usage) {
|
|
1195
|
+
const usage = entry.message.usage;
|
|
1196
|
+
|
|
1197
|
+
// Use token counts from latest assistant message only
|
|
1198
|
+
inputTokens = usage.input_tokens || 0;
|
|
1199
|
+
cacheCreationTokens = usage.cache_creation_input_tokens || 0;
|
|
1200
|
+
cacheReadTokens = usage.cache_read_input_tokens || 0;
|
|
1201
|
+
|
|
1202
|
+
break; // Stop after finding the latest assistant message
|
|
1203
|
+
}
|
|
1204
|
+
} catch (parseError) {
|
|
1205
|
+
// Skip lines that can't be parsed
|
|
1206
|
+
continue;
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
// Calculate total context usage (excluding output_tokens, as per ccusage)
|
|
1211
|
+
const totalUsed = inputTokens + cacheCreationTokens + cacheReadTokens;
|
|
1212
|
+
|
|
1213
|
+
res.json({
|
|
1214
|
+
used: totalUsed,
|
|
1215
|
+
total: contextWindow,
|
|
1216
|
+
breakdown: {
|
|
1217
|
+
input: inputTokens,
|
|
1218
|
+
cacheCreation: cacheCreationTokens,
|
|
1219
|
+
cacheRead: cacheReadTokens
|
|
1220
|
+
}
|
|
1221
|
+
});
|
|
1222
|
+
} catch (error) {
|
|
1223
|
+
console.error('Error reading session token usage:', error);
|
|
1224
|
+
res.status(500).json({ error: 'Failed to read session token usage' });
|
|
1225
|
+
}
|
|
1226
|
+
});
|
|
1227
|
+
|
|
1228
|
+
// Serve React app for all other routes (excluding static files)
|
|
1057
1229
|
app.get('*', (req, res) => {
|
|
1230
|
+
// Skip requests for static assets (files with extensions)
|
|
1231
|
+
if (path.extname(req.path)) {
|
|
1232
|
+
return res.status(404).send('Not found');
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
// Only serve index.html for HTML routes, not for static assets
|
|
1236
|
+
// Static assets should already be handled by express.static middleware above
|
|
1058
1237
|
if (process.env.NODE_ENV === 'production') {
|
|
1238
|
+
// Set no-cache headers for HTML to prevent service worker issues
|
|
1239
|
+
res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
|
|
1240
|
+
res.setHeader('Pragma', 'no-cache');
|
|
1241
|
+
res.setHeader('Expires', '0');
|
|
1059
1242
|
res.sendFile(path.join(__dirname, '../dist/index.html'));
|
|
1060
1243
|
} else {
|
|
1061
1244
|
// In development, redirect to Vite dev server
|
|
1062
|
-
res.redirect(`http://localhost:${process.env.VITE_PORT ||
|
|
1245
|
+
res.redirect(`http://localhost:${process.env.VITE_PORT || 5173}`);
|
|
1063
1246
|
}
|
|
1064
1247
|
});
|
|
1065
1248
|
|
|
@@ -1153,11 +1336,14 @@ async function startServer() {
|
|
|
1153
1336
|
await initializeDatabase();
|
|
1154
1337
|
console.log('✅ Database initialization skipped (testing)');
|
|
1155
1338
|
|
|
1339
|
+
// Log Claude implementation mode
|
|
1340
|
+
console.log('🚀 Using Claude Agents SDK for Claude integration');
|
|
1341
|
+
|
|
1156
1342
|
server.listen(PORT, '0.0.0.0', async () => {
|
|
1157
1343
|
console.log(`Claude Code UI server running on http://0.0.0.0:${PORT}`);
|
|
1158
1344
|
|
|
1159
1345
|
// Start watching the projects folder for changes
|
|
1160
|
-
await setupProjectsWatcher();
|
|
1346
|
+
await setupProjectsWatcher();
|
|
1161
1347
|
});
|
|
1162
1348
|
} catch (error) {
|
|
1163
1349
|
console.error('❌ Failed to start server:', error);
|
package/server/projects.js
CHANGED
|
@@ -627,8 +627,9 @@ async function getSessions(projectName, limit = 5, offset = 0) {
|
|
|
627
627
|
return session;
|
|
628
628
|
});
|
|
629
629
|
const visibleSessions = [...latestFromGroups, ...standaloneSessionsArray]
|
|
630
|
+
.filter(session => !session.summary.startsWith('{ "'))
|
|
630
631
|
.sort((a, b) => new Date(b.lastActivity) - new Date(a.lastActivity));
|
|
631
|
-
|
|
632
|
+
|
|
632
633
|
const total = visibleSessions.length;
|
|
633
634
|
const paginatedSessions = visibleSessions.slice(offset, offset + limit);
|
|
634
635
|
const hasMore = offset + limit < total;
|
|
@@ -649,20 +650,26 @@ async function getSessions(projectName, limit = 5, offset = 0) {
|
|
|
649
650
|
async function parseJsonlSessions(filePath) {
|
|
650
651
|
const sessions = new Map();
|
|
651
652
|
const entries = [];
|
|
652
|
-
|
|
653
|
+
const pendingSummaries = new Map(); // leafUuid -> summary for entries without sessionId
|
|
654
|
+
|
|
653
655
|
try {
|
|
654
656
|
const fileStream = fsSync.createReadStream(filePath);
|
|
655
657
|
const rl = readline.createInterface({
|
|
656
658
|
input: fileStream,
|
|
657
659
|
crlfDelay: Infinity
|
|
658
660
|
});
|
|
659
|
-
|
|
661
|
+
|
|
660
662
|
for await (const line of rl) {
|
|
661
663
|
if (line.trim()) {
|
|
662
664
|
try {
|
|
663
665
|
const entry = JSON.parse(line);
|
|
664
666
|
entries.push(entry);
|
|
665
|
-
|
|
667
|
+
|
|
668
|
+
// Handle summary entries that don't have sessionId yet
|
|
669
|
+
if (entry.type === 'summary' && entry.summary && !entry.sessionId && entry.leafUuid) {
|
|
670
|
+
pendingSummaries.set(entry.leafUuid, entry.summary);
|
|
671
|
+
}
|
|
672
|
+
|
|
666
673
|
if (entry.sessionId) {
|
|
667
674
|
if (!sessions.has(entry.sessionId)) {
|
|
668
675
|
sessions.set(entry.sessionId, {
|
|
@@ -670,24 +677,84 @@ async function parseJsonlSessions(filePath) {
|
|
|
670
677
|
summary: 'New Session',
|
|
671
678
|
messageCount: 0,
|
|
672
679
|
lastActivity: new Date(),
|
|
673
|
-
cwd: entry.cwd || ''
|
|
680
|
+
cwd: entry.cwd || '',
|
|
681
|
+
lastUserMessage: null,
|
|
682
|
+
lastAssistantMessage: null
|
|
674
683
|
});
|
|
675
684
|
}
|
|
676
|
-
|
|
685
|
+
|
|
677
686
|
const session = sessions.get(entry.sessionId);
|
|
678
|
-
|
|
679
|
-
//
|
|
687
|
+
|
|
688
|
+
// Apply pending summary if this entry has a parentUuid that matches a pending summary
|
|
689
|
+
if (session.summary === 'New Session' && entry.parentUuid && pendingSummaries.has(entry.parentUuid)) {
|
|
690
|
+
session.summary = pendingSummaries.get(entry.parentUuid);
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// Update summary from summary entries with sessionId
|
|
680
694
|
if (entry.type === 'summary' && entry.summary) {
|
|
681
695
|
session.summary = entry.summary;
|
|
682
|
-
}
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// Track last user and assistant messages (skip system messages)
|
|
699
|
+
if (entry.message?.role === 'user' && entry.message?.content) {
|
|
683
700
|
const content = entry.message.content;
|
|
684
|
-
|
|
685
|
-
|
|
701
|
+
|
|
702
|
+
// Extract text from array format if needed
|
|
703
|
+
let textContent = content;
|
|
704
|
+
if (Array.isArray(content) && content.length > 0 && content[0].type === 'text') {
|
|
705
|
+
textContent = content[0].text;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
const isSystemMessage = typeof textContent === 'string' && (
|
|
709
|
+
textContent.startsWith('<command-name>') ||
|
|
710
|
+
textContent.startsWith('<command-message>') ||
|
|
711
|
+
textContent.startsWith('<command-args>') ||
|
|
712
|
+
textContent.startsWith('<local-command-stdout>') ||
|
|
713
|
+
textContent.startsWith('<system-reminder>') ||
|
|
714
|
+
textContent.startsWith('Caveat:') ||
|
|
715
|
+
textContent.startsWith('This session is being continued from a previous') ||
|
|
716
|
+
textContent.startsWith('Invalid API key') ||
|
|
717
|
+
textContent.includes('{"subtasks":') || // Filter Task Master prompts
|
|
718
|
+
textContent.includes('CRITICAL: You MUST respond with ONLY a JSON') || // Filter Task Master system prompts
|
|
719
|
+
textContent === 'Warmup' // Explicitly filter out "Warmup"
|
|
720
|
+
);
|
|
721
|
+
|
|
722
|
+
if (typeof textContent === 'string' && textContent.length > 0 && !isSystemMessage) {
|
|
723
|
+
session.lastUserMessage = textContent;
|
|
724
|
+
}
|
|
725
|
+
} else if (entry.message?.role === 'assistant' && entry.message?.content) {
|
|
726
|
+
// Skip API error messages using the isApiErrorMessage flag
|
|
727
|
+
if (entry.isApiErrorMessage === true) {
|
|
728
|
+
// Skip this message entirely
|
|
729
|
+
} else {
|
|
730
|
+
// Track last assistant text message
|
|
731
|
+
let assistantText = null;
|
|
732
|
+
|
|
733
|
+
if (Array.isArray(entry.message.content)) {
|
|
734
|
+
for (const part of entry.message.content) {
|
|
735
|
+
if (part.type === 'text' && part.text) {
|
|
736
|
+
assistantText = part.text;
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
} else if (typeof entry.message.content === 'string') {
|
|
740
|
+
assistantText = entry.message.content;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
// Additional filter for assistant messages with system content
|
|
744
|
+
const isSystemAssistantMessage = typeof assistantText === 'string' && (
|
|
745
|
+
assistantText.startsWith('Invalid API key') ||
|
|
746
|
+
assistantText.includes('{"subtasks":') ||
|
|
747
|
+
assistantText.includes('CRITICAL: You MUST respond with ONLY a JSON')
|
|
748
|
+
);
|
|
749
|
+
|
|
750
|
+
if (assistantText && !isSystemAssistantMessage) {
|
|
751
|
+
session.lastAssistantMessage = assistantText;
|
|
752
|
+
}
|
|
686
753
|
}
|
|
687
754
|
}
|
|
688
|
-
|
|
755
|
+
|
|
689
756
|
session.messageCount++;
|
|
690
|
-
|
|
757
|
+
|
|
691
758
|
if (entry.timestamp) {
|
|
692
759
|
session.lastActivity = new Date(entry.timestamp);
|
|
693
760
|
}
|
|
@@ -697,12 +764,36 @@ async function parseJsonlSessions(filePath) {
|
|
|
697
764
|
}
|
|
698
765
|
}
|
|
699
766
|
}
|
|
700
|
-
|
|
767
|
+
|
|
768
|
+
// After processing all entries, set final summary based on last message if no summary exists
|
|
769
|
+
for (const session of sessions.values()) {
|
|
770
|
+
if (session.summary === 'New Session') {
|
|
771
|
+
// Prefer last user message, fall back to last assistant message
|
|
772
|
+
const lastMessage = session.lastUserMessage || session.lastAssistantMessage;
|
|
773
|
+
if (lastMessage) {
|
|
774
|
+
session.summary = lastMessage.length > 50 ? lastMessage.substring(0, 50) + '...' : lastMessage;
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
// Filter out sessions that contain JSON responses (Task Master errors)
|
|
780
|
+
const allSessions = Array.from(sessions.values());
|
|
781
|
+
const filteredSessions = allSessions.filter(session => {
|
|
782
|
+
const shouldFilter = session.summary.startsWith('{ "');
|
|
783
|
+
if (shouldFilter) {
|
|
784
|
+
}
|
|
785
|
+
// Log a sample of summaries to debug
|
|
786
|
+
if (Math.random() < 0.01) { // Log 1% of sessions
|
|
787
|
+
}
|
|
788
|
+
return !shouldFilter;
|
|
789
|
+
});
|
|
790
|
+
|
|
791
|
+
|
|
701
792
|
return {
|
|
702
|
-
sessions:
|
|
793
|
+
sessions: filteredSessions,
|
|
703
794
|
entries: entries
|
|
704
795
|
};
|
|
705
|
-
|
|
796
|
+
|
|
706
797
|
} catch (error) {
|
|
707
798
|
console.error('Error reading JSONL file:', error);
|
|
708
799
|
return { sessions: [], entries: [] };
|
|
@@ -1060,4 +1151,4 @@ export {
|
|
|
1060
1151
|
saveProjectConfig,
|
|
1061
1152
|
extractProjectDirectory,
|
|
1062
1153
|
clearProjectDirectoryCache
|
|
1063
|
-
};
|
|
1154
|
+
};
|