@siteboon/claude-code-ui 1.21.0 → 1.23.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/README.md +82 -213
- package/dist/assets/index-7_J3n3lH.js +1204 -0
- package/dist/assets/index-BFyod1Qa.css +32 -0
- package/dist/assets/{vendor-codemirror-BMLq5tLB.js → vendor-codemirror-C8f1vU1x.js} +9 -9
- package/dist/assets/{vendor-react-DIN4KjD2.js → vendor-react-CdSTmIF1.js} +1 -1
- package/dist/index.html +6 -6
- package/package.json +23 -2
- package/server/claude-sdk.js +63 -11
- package/server/database/db.js +68 -0
- package/server/database/init.sql +14 -1
- package/server/index.js +486 -6
- package/server/projects.js +53 -14
- package/server/routes/cli-auth.js +23 -4
- package/server/routes/codex.js +3 -0
- package/server/routes/cursor.js +5 -2
- package/server/routes/gemini.js +2 -0
- package/shared/modelConstants.js +6 -2
- package/dist/assets/index-Cxnz_sny.css +0 -32
- package/dist/assets/index-DN2ZJcRJ.js +0 -1381
package/server/index.js
CHANGED
|
@@ -45,7 +45,7 @@ import fetch from 'node-fetch';
|
|
|
45
45
|
import mime from 'mime-types';
|
|
46
46
|
|
|
47
47
|
import { getProjects, getSessions, getSessionMessages, renameProject, deleteSession, deleteProject, addProjectManually, extractProjectDirectory, clearProjectDirectoryCache } from './projects.js';
|
|
48
|
-
import { queryClaudeSDK, abortClaudeSDKSession, isClaudeSDKSessionActive, getActiveClaudeSDKSessions, resolveToolApproval } from './claude-sdk.js';
|
|
48
|
+
import { queryClaudeSDK, abortClaudeSDKSession, isClaudeSDKSessionActive, getActiveClaudeSDKSessions, resolveToolApproval, getPendingApprovalsForSession, reconnectSessionWriter } from './claude-sdk.js';
|
|
49
49
|
import { spawnCursor, abortCursorSession, isCursorSessionActive, getActiveCursorSessions } from './cursor-cli.js';
|
|
50
50
|
import { queryCodex, abortCodexSession, isCodexSessionActive, getActiveCodexSessions } from './openai-codex.js';
|
|
51
51
|
import { spawnGemini, abortGeminiSession, isGeminiSessionActive, getActiveGeminiSessions } from './gemini-cli.js';
|
|
@@ -64,10 +64,12 @@ import cliAuthRoutes from './routes/cli-auth.js';
|
|
|
64
64
|
import userRoutes from './routes/user.js';
|
|
65
65
|
import codexRoutes from './routes/codex.js';
|
|
66
66
|
import geminiRoutes from './routes/gemini.js';
|
|
67
|
-
import { initializeDatabase } from './database/db.js';
|
|
67
|
+
import { initializeDatabase, sessionNamesDb, applyCustomSessionNames } from './database/db.js';
|
|
68
68
|
import { validateApiKey, authenticateToken, authenticateWebSocket } from './middleware/auth.js';
|
|
69
69
|
import { IS_PLATFORM } from './constants/config.js';
|
|
70
70
|
|
|
71
|
+
const VALID_PROVIDERS = ['claude', 'codex', 'cursor', 'gemini'];
|
|
72
|
+
|
|
71
73
|
// File system watchers for provider project/session folders
|
|
72
74
|
const PROVIDER_WATCH_PATHS = [
|
|
73
75
|
{ provider: 'claude', rootPath: path.join(os.homedir(), '.claude', 'projects') },
|
|
@@ -493,6 +495,7 @@ app.get('/api/projects/:projectName/sessions', authenticateToken, async (req, re
|
|
|
493
495
|
try {
|
|
494
496
|
const { limit = 5, offset = 0 } = req.query;
|
|
495
497
|
const result = await getSessions(req.params.projectName, parseInt(limit), parseInt(offset));
|
|
498
|
+
applyCustomSessionNames(result.sessions, 'claude');
|
|
496
499
|
res.json(result);
|
|
497
500
|
} catch (error) {
|
|
498
501
|
res.status(500).json({ error: error.message });
|
|
@@ -541,6 +544,7 @@ app.delete('/api/projects/:projectName/sessions/:sessionId', authenticateToken,
|
|
|
541
544
|
const { projectName, sessionId } = req.params;
|
|
542
545
|
console.log(`[API] Deleting session: ${sessionId} from project: ${projectName}`);
|
|
543
546
|
await deleteSession(projectName, sessionId);
|
|
547
|
+
sessionNamesDb.deleteName(sessionId, 'claude');
|
|
544
548
|
console.log(`[API] Session ${sessionId} deleted successfully`);
|
|
545
549
|
res.json({ success: true });
|
|
546
550
|
} catch (error) {
|
|
@@ -549,6 +553,32 @@ app.delete('/api/projects/:projectName/sessions/:sessionId', authenticateToken,
|
|
|
549
553
|
}
|
|
550
554
|
});
|
|
551
555
|
|
|
556
|
+
// Rename session endpoint
|
|
557
|
+
app.put('/api/sessions/:sessionId/rename', authenticateToken, async (req, res) => {
|
|
558
|
+
try {
|
|
559
|
+
const { sessionId } = req.params;
|
|
560
|
+
const safeSessionId = String(sessionId).replace(/[^a-zA-Z0-9._-]/g, '');
|
|
561
|
+
if (!safeSessionId || safeSessionId !== String(sessionId)) {
|
|
562
|
+
return res.status(400).json({ error: 'Invalid sessionId' });
|
|
563
|
+
}
|
|
564
|
+
const { summary, provider } = req.body;
|
|
565
|
+
if (!summary || typeof summary !== 'string' || summary.trim() === '') {
|
|
566
|
+
return res.status(400).json({ error: 'Summary is required' });
|
|
567
|
+
}
|
|
568
|
+
if (summary.trim().length > 500) {
|
|
569
|
+
return res.status(400).json({ error: 'Summary must not exceed 500 characters' });
|
|
570
|
+
}
|
|
571
|
+
if (!provider || !VALID_PROVIDERS.includes(provider)) {
|
|
572
|
+
return res.status(400).json({ error: `Provider must be one of: ${VALID_PROVIDERS.join(', ')}` });
|
|
573
|
+
}
|
|
574
|
+
sessionNamesDb.setName(safeSessionId, provider, summary.trim());
|
|
575
|
+
res.json({ success: true });
|
|
576
|
+
} catch (error) {
|
|
577
|
+
console.error(`[API] Error renaming session ${req.params.sessionId}:`, error);
|
|
578
|
+
res.status(500).json({ error: error.message });
|
|
579
|
+
}
|
|
580
|
+
});
|
|
581
|
+
|
|
552
582
|
// Delete project endpoint (force=true to delete with sessions)
|
|
553
583
|
app.delete('/api/projects/:projectName', authenticateToken, async (req, res) => {
|
|
554
584
|
try {
|
|
@@ -884,6 +914,436 @@ app.get('/api/projects/:projectName/files', authenticateToken, async (req, res)
|
|
|
884
914
|
}
|
|
885
915
|
});
|
|
886
916
|
|
|
917
|
+
// ============================================================================
|
|
918
|
+
// FILE OPERATIONS API ENDPOINTS
|
|
919
|
+
// ============================================================================
|
|
920
|
+
|
|
921
|
+
/**
|
|
922
|
+
* Validate that a path is within the project root
|
|
923
|
+
* @param {string} projectRoot - The project root path
|
|
924
|
+
* @param {string} targetPath - The path to validate
|
|
925
|
+
* @returns {{ valid: boolean, resolved?: string, error?: string }}
|
|
926
|
+
*/
|
|
927
|
+
function validatePathInProject(projectRoot, targetPath) {
|
|
928
|
+
const resolved = path.isAbsolute(targetPath)
|
|
929
|
+
? path.resolve(targetPath)
|
|
930
|
+
: path.resolve(projectRoot, targetPath);
|
|
931
|
+
const normalizedRoot = path.resolve(projectRoot) + path.sep;
|
|
932
|
+
if (!resolved.startsWith(normalizedRoot)) {
|
|
933
|
+
return { valid: false, error: 'Path must be under project root' };
|
|
934
|
+
}
|
|
935
|
+
return { valid: true, resolved };
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
/**
|
|
939
|
+
* Validate filename - check for invalid characters
|
|
940
|
+
* @param {string} name - The filename to validate
|
|
941
|
+
* @returns {{ valid: boolean, error?: string }}
|
|
942
|
+
*/
|
|
943
|
+
function validateFilename(name) {
|
|
944
|
+
if (!name || !name.trim()) {
|
|
945
|
+
return { valid: false, error: 'Filename cannot be empty' };
|
|
946
|
+
}
|
|
947
|
+
// Check for invalid characters (Windows + Unix)
|
|
948
|
+
const invalidChars = /[<>:"/\\|?*\x00-\x1f]/;
|
|
949
|
+
if (invalidChars.test(name)) {
|
|
950
|
+
return { valid: false, error: 'Filename contains invalid characters' };
|
|
951
|
+
}
|
|
952
|
+
// Check for reserved names (Windows)
|
|
953
|
+
const reserved = /^(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])$/i;
|
|
954
|
+
if (reserved.test(name)) {
|
|
955
|
+
return { valid: false, error: 'Filename is a reserved name' };
|
|
956
|
+
}
|
|
957
|
+
// Check for dots only
|
|
958
|
+
if (/^\.+$/.test(name)) {
|
|
959
|
+
return { valid: false, error: 'Filename cannot be only dots' };
|
|
960
|
+
}
|
|
961
|
+
return { valid: true };
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
// POST /api/projects/:projectName/files/create - Create new file or directory
|
|
965
|
+
app.post('/api/projects/:projectName/files/create', authenticateToken, async (req, res) => {
|
|
966
|
+
try {
|
|
967
|
+
const { projectName } = req.params;
|
|
968
|
+
const { path: parentPath, type, name } = req.body;
|
|
969
|
+
|
|
970
|
+
// Validate input
|
|
971
|
+
if (!name || !type) {
|
|
972
|
+
return res.status(400).json({ error: 'Name and type are required' });
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
if (!['file', 'directory'].includes(type)) {
|
|
976
|
+
return res.status(400).json({ error: 'Type must be "file" or "directory"' });
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
const nameValidation = validateFilename(name);
|
|
980
|
+
if (!nameValidation.valid) {
|
|
981
|
+
return res.status(400).json({ error: nameValidation.error });
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
// Get project root
|
|
985
|
+
const projectRoot = await extractProjectDirectory(projectName).catch(() => null);
|
|
986
|
+
if (!projectRoot) {
|
|
987
|
+
return res.status(404).json({ error: 'Project not found' });
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
// Build and validate target path
|
|
991
|
+
const targetDir = parentPath || '';
|
|
992
|
+
const targetPath = targetDir ? path.join(targetDir, name) : name;
|
|
993
|
+
const validation = validatePathInProject(projectRoot, targetPath);
|
|
994
|
+
if (!validation.valid) {
|
|
995
|
+
return res.status(403).json({ error: validation.error });
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
const resolvedPath = validation.resolved;
|
|
999
|
+
|
|
1000
|
+
// Check if already exists
|
|
1001
|
+
try {
|
|
1002
|
+
await fsPromises.access(resolvedPath);
|
|
1003
|
+
return res.status(409).json({ error: `${type === 'file' ? 'File' : 'Directory'} already exists` });
|
|
1004
|
+
} catch {
|
|
1005
|
+
// Doesn't exist, which is what we want
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
// Create file or directory
|
|
1009
|
+
if (type === 'directory') {
|
|
1010
|
+
await fsPromises.mkdir(resolvedPath, { recursive: false });
|
|
1011
|
+
} else {
|
|
1012
|
+
// Ensure parent directory exists
|
|
1013
|
+
const parentDir = path.dirname(resolvedPath);
|
|
1014
|
+
try {
|
|
1015
|
+
await fsPromises.access(parentDir);
|
|
1016
|
+
} catch {
|
|
1017
|
+
await fsPromises.mkdir(parentDir, { recursive: true });
|
|
1018
|
+
}
|
|
1019
|
+
await fsPromises.writeFile(resolvedPath, '', 'utf8');
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
res.json({
|
|
1023
|
+
success: true,
|
|
1024
|
+
path: resolvedPath,
|
|
1025
|
+
name,
|
|
1026
|
+
type,
|
|
1027
|
+
message: `${type === 'file' ? 'File' : 'Directory'} created successfully`
|
|
1028
|
+
});
|
|
1029
|
+
} catch (error) {
|
|
1030
|
+
console.error('Error creating file/directory:', error);
|
|
1031
|
+
if (error.code === 'EACCES') {
|
|
1032
|
+
res.status(403).json({ error: 'Permission denied' });
|
|
1033
|
+
} else if (error.code === 'ENOENT') {
|
|
1034
|
+
res.status(404).json({ error: 'Parent directory not found' });
|
|
1035
|
+
} else {
|
|
1036
|
+
res.status(500).json({ error: error.message });
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
});
|
|
1040
|
+
|
|
1041
|
+
// PUT /api/projects/:projectName/files/rename - Rename file or directory
|
|
1042
|
+
app.put('/api/projects/:projectName/files/rename', authenticateToken, async (req, res) => {
|
|
1043
|
+
try {
|
|
1044
|
+
const { projectName } = req.params;
|
|
1045
|
+
const { oldPath, newName } = req.body;
|
|
1046
|
+
|
|
1047
|
+
// Validate input
|
|
1048
|
+
if (!oldPath || !newName) {
|
|
1049
|
+
return res.status(400).json({ error: 'oldPath and newName are required' });
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
const nameValidation = validateFilename(newName);
|
|
1053
|
+
if (!nameValidation.valid) {
|
|
1054
|
+
return res.status(400).json({ error: nameValidation.error });
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
// Get project root
|
|
1058
|
+
const projectRoot = await extractProjectDirectory(projectName).catch(() => null);
|
|
1059
|
+
if (!projectRoot) {
|
|
1060
|
+
return res.status(404).json({ error: 'Project not found' });
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
// Validate old path
|
|
1064
|
+
const oldValidation = validatePathInProject(projectRoot, oldPath);
|
|
1065
|
+
if (!oldValidation.valid) {
|
|
1066
|
+
return res.status(403).json({ error: oldValidation.error });
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
const resolvedOldPath = oldValidation.resolved;
|
|
1070
|
+
|
|
1071
|
+
// Check if old path exists
|
|
1072
|
+
try {
|
|
1073
|
+
await fsPromises.access(resolvedOldPath);
|
|
1074
|
+
} catch {
|
|
1075
|
+
return res.status(404).json({ error: 'File or directory not found' });
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
// Build and validate new path
|
|
1079
|
+
const parentDir = path.dirname(resolvedOldPath);
|
|
1080
|
+
const resolvedNewPath = path.join(parentDir, newName);
|
|
1081
|
+
const newValidation = validatePathInProject(projectRoot, resolvedNewPath);
|
|
1082
|
+
if (!newValidation.valid) {
|
|
1083
|
+
return res.status(403).json({ error: newValidation.error });
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
// Check if new path already exists
|
|
1087
|
+
try {
|
|
1088
|
+
await fsPromises.access(resolvedNewPath);
|
|
1089
|
+
return res.status(409).json({ error: 'A file or directory with this name already exists' });
|
|
1090
|
+
} catch {
|
|
1091
|
+
// Doesn't exist, which is what we want
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
// Rename
|
|
1095
|
+
await fsPromises.rename(resolvedOldPath, resolvedNewPath);
|
|
1096
|
+
|
|
1097
|
+
res.json({
|
|
1098
|
+
success: true,
|
|
1099
|
+
oldPath: resolvedOldPath,
|
|
1100
|
+
newPath: resolvedNewPath,
|
|
1101
|
+
newName,
|
|
1102
|
+
message: 'Renamed successfully'
|
|
1103
|
+
});
|
|
1104
|
+
} catch (error) {
|
|
1105
|
+
console.error('Error renaming file/directory:', error);
|
|
1106
|
+
if (error.code === 'EACCES') {
|
|
1107
|
+
res.status(403).json({ error: 'Permission denied' });
|
|
1108
|
+
} else if (error.code === 'ENOENT') {
|
|
1109
|
+
res.status(404).json({ error: 'File or directory not found' });
|
|
1110
|
+
} else if (error.code === 'EXDEV') {
|
|
1111
|
+
res.status(400).json({ error: 'Cannot move across different filesystems' });
|
|
1112
|
+
} else {
|
|
1113
|
+
res.status(500).json({ error: error.message });
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
});
|
|
1117
|
+
|
|
1118
|
+
// DELETE /api/projects/:projectName/files - Delete file or directory
|
|
1119
|
+
app.delete('/api/projects/:projectName/files', authenticateToken, async (req, res) => {
|
|
1120
|
+
try {
|
|
1121
|
+
const { projectName } = req.params;
|
|
1122
|
+
const { path: targetPath, type } = req.body;
|
|
1123
|
+
|
|
1124
|
+
// Validate input
|
|
1125
|
+
if (!targetPath) {
|
|
1126
|
+
return res.status(400).json({ error: 'Path is required' });
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
// Get project root
|
|
1130
|
+
const projectRoot = await extractProjectDirectory(projectName).catch(() => null);
|
|
1131
|
+
if (!projectRoot) {
|
|
1132
|
+
return res.status(404).json({ error: 'Project not found' });
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
// Validate path
|
|
1136
|
+
const validation = validatePathInProject(projectRoot, targetPath);
|
|
1137
|
+
if (!validation.valid) {
|
|
1138
|
+
return res.status(403).json({ error: validation.error });
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
const resolvedPath = validation.resolved;
|
|
1142
|
+
|
|
1143
|
+
// Check if path exists and get stats
|
|
1144
|
+
let stats;
|
|
1145
|
+
try {
|
|
1146
|
+
stats = await fsPromises.stat(resolvedPath);
|
|
1147
|
+
} catch {
|
|
1148
|
+
return res.status(404).json({ error: 'File or directory not found' });
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
// Prevent deleting the project root itself
|
|
1152
|
+
if (resolvedPath === path.resolve(projectRoot)) {
|
|
1153
|
+
return res.status(403).json({ error: 'Cannot delete project root directory' });
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
// Delete based on type
|
|
1157
|
+
if (stats.isDirectory()) {
|
|
1158
|
+
await fsPromises.rm(resolvedPath, { recursive: true, force: true });
|
|
1159
|
+
} else {
|
|
1160
|
+
await fsPromises.unlink(resolvedPath);
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
res.json({
|
|
1164
|
+
success: true,
|
|
1165
|
+
path: resolvedPath,
|
|
1166
|
+
type: stats.isDirectory() ? 'directory' : 'file',
|
|
1167
|
+
message: 'Deleted successfully'
|
|
1168
|
+
});
|
|
1169
|
+
} catch (error) {
|
|
1170
|
+
console.error('Error deleting file/directory:', error);
|
|
1171
|
+
if (error.code === 'EACCES') {
|
|
1172
|
+
res.status(403).json({ error: 'Permission denied' });
|
|
1173
|
+
} else if (error.code === 'ENOENT') {
|
|
1174
|
+
res.status(404).json({ error: 'File or directory not found' });
|
|
1175
|
+
} else if (error.code === 'ENOTEMPTY') {
|
|
1176
|
+
res.status(400).json({ error: 'Directory is not empty' });
|
|
1177
|
+
} else {
|
|
1178
|
+
res.status(500).json({ error: error.message });
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
});
|
|
1182
|
+
|
|
1183
|
+
// POST /api/projects/:projectName/files/upload - Upload files
|
|
1184
|
+
// Dynamic import of multer for file uploads
|
|
1185
|
+
const uploadFilesHandler = async (req, res) => {
|
|
1186
|
+
// Dynamic import of multer
|
|
1187
|
+
const multer = (await import('multer')).default;
|
|
1188
|
+
|
|
1189
|
+
const uploadMiddleware = multer({
|
|
1190
|
+
storage: multer.diskStorage({
|
|
1191
|
+
destination: (req, file, cb) => {
|
|
1192
|
+
cb(null, os.tmpdir());
|
|
1193
|
+
},
|
|
1194
|
+
filename: (req, file, cb) => {
|
|
1195
|
+
// Use a unique temp name, but preserve original name in file.originalname
|
|
1196
|
+
// Note: file.originalname may contain path separators for folder uploads
|
|
1197
|
+
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
|
|
1198
|
+
// For temp file, just use a safe unique name without the path
|
|
1199
|
+
cb(null, `upload-${uniqueSuffix}`);
|
|
1200
|
+
}
|
|
1201
|
+
}),
|
|
1202
|
+
limits: {
|
|
1203
|
+
fileSize: 50 * 1024 * 1024, // 50MB limit
|
|
1204
|
+
files: 20 // Max 20 files at once
|
|
1205
|
+
}
|
|
1206
|
+
});
|
|
1207
|
+
|
|
1208
|
+
// Use multer middleware
|
|
1209
|
+
uploadMiddleware.array('files', 20)(req, res, async (err) => {
|
|
1210
|
+
if (err) {
|
|
1211
|
+
console.error('Multer error:', err);
|
|
1212
|
+
if (err.code === 'LIMIT_FILE_SIZE') {
|
|
1213
|
+
return res.status(400).json({ error: 'File too large. Maximum size is 50MB.' });
|
|
1214
|
+
}
|
|
1215
|
+
if (err.code === 'LIMIT_FILE_COUNT') {
|
|
1216
|
+
return res.status(400).json({ error: 'Too many files. Maximum is 20 files.' });
|
|
1217
|
+
}
|
|
1218
|
+
return res.status(500).json({ error: err.message });
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
try {
|
|
1222
|
+
const { projectName } = req.params;
|
|
1223
|
+
const { targetPath, relativePaths } = req.body;
|
|
1224
|
+
|
|
1225
|
+
// Parse relative paths if provided (for folder uploads)
|
|
1226
|
+
let filePaths = [];
|
|
1227
|
+
if (relativePaths) {
|
|
1228
|
+
try {
|
|
1229
|
+
filePaths = JSON.parse(relativePaths);
|
|
1230
|
+
} catch (e) {
|
|
1231
|
+
console.log('[DEBUG] Failed to parse relativePaths:', relativePaths);
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
console.log('[DEBUG] File upload request:', {
|
|
1236
|
+
projectName,
|
|
1237
|
+
targetPath: JSON.stringify(targetPath),
|
|
1238
|
+
targetPathType: typeof targetPath,
|
|
1239
|
+
filesCount: req.files?.length,
|
|
1240
|
+
relativePaths: filePaths
|
|
1241
|
+
});
|
|
1242
|
+
|
|
1243
|
+
if (!req.files || req.files.length === 0) {
|
|
1244
|
+
return res.status(400).json({ error: 'No files provided' });
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
// Get project root
|
|
1248
|
+
const projectRoot = await extractProjectDirectory(projectName).catch(() => null);
|
|
1249
|
+
if (!projectRoot) {
|
|
1250
|
+
return res.status(404).json({ error: 'Project not found' });
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
console.log('[DEBUG] Project root:', projectRoot);
|
|
1254
|
+
|
|
1255
|
+
// Validate and resolve target path
|
|
1256
|
+
// If targetPath is empty or '.', use project root directly
|
|
1257
|
+
const targetDir = targetPath || '';
|
|
1258
|
+
let resolvedTargetDir;
|
|
1259
|
+
|
|
1260
|
+
console.log('[DEBUG] Target dir:', JSON.stringify(targetDir));
|
|
1261
|
+
|
|
1262
|
+
if (!targetDir || targetDir === '.' || targetDir === './') {
|
|
1263
|
+
// Empty path means upload to project root
|
|
1264
|
+
resolvedTargetDir = path.resolve(projectRoot);
|
|
1265
|
+
console.log('[DEBUG] Using project root as target:', resolvedTargetDir);
|
|
1266
|
+
} else {
|
|
1267
|
+
const validation = validatePathInProject(projectRoot, targetDir);
|
|
1268
|
+
if (!validation.valid) {
|
|
1269
|
+
console.log('[DEBUG] Path validation failed:', validation.error);
|
|
1270
|
+
return res.status(403).json({ error: validation.error });
|
|
1271
|
+
}
|
|
1272
|
+
resolvedTargetDir = validation.resolved;
|
|
1273
|
+
console.log('[DEBUG] Resolved target dir:', resolvedTargetDir);
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
// Ensure target directory exists
|
|
1277
|
+
try {
|
|
1278
|
+
await fsPromises.access(resolvedTargetDir);
|
|
1279
|
+
} catch {
|
|
1280
|
+
await fsPromises.mkdir(resolvedTargetDir, { recursive: true });
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
// Move uploaded files from temp to target directory
|
|
1284
|
+
const uploadedFiles = [];
|
|
1285
|
+
console.log('[DEBUG] Processing files:', req.files.map(f => ({ originalname: f.originalname, path: f.path })));
|
|
1286
|
+
for (let i = 0; i < req.files.length; i++) {
|
|
1287
|
+
const file = req.files[i];
|
|
1288
|
+
// Use relative path if provided (for folder uploads), otherwise use originalname
|
|
1289
|
+
const fileName = (filePaths && filePaths[i]) ? filePaths[i] : file.originalname;
|
|
1290
|
+
console.log('[DEBUG] Processing file:', fileName, '(originalname:', file.originalname + ')');
|
|
1291
|
+
const destPath = path.join(resolvedTargetDir, fileName);
|
|
1292
|
+
|
|
1293
|
+
// Validate destination path
|
|
1294
|
+
const destValidation = validatePathInProject(projectRoot, destPath);
|
|
1295
|
+
if (!destValidation.valid) {
|
|
1296
|
+
console.log('[DEBUG] Destination validation failed for:', destPath);
|
|
1297
|
+
// Clean up temp file
|
|
1298
|
+
await fsPromises.unlink(file.path).catch(() => {});
|
|
1299
|
+
continue;
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
// Ensure parent directory exists (for nested files from folder upload)
|
|
1303
|
+
const parentDir = path.dirname(destPath);
|
|
1304
|
+
try {
|
|
1305
|
+
await fsPromises.access(parentDir);
|
|
1306
|
+
} catch {
|
|
1307
|
+
await fsPromises.mkdir(parentDir, { recursive: true });
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
// Move file (copy + unlink to handle cross-device scenarios)
|
|
1311
|
+
await fsPromises.copyFile(file.path, destPath);
|
|
1312
|
+
await fsPromises.unlink(file.path);
|
|
1313
|
+
|
|
1314
|
+
uploadedFiles.push({
|
|
1315
|
+
name: fileName,
|
|
1316
|
+
path: destPath,
|
|
1317
|
+
size: file.size,
|
|
1318
|
+
mimeType: file.mimetype
|
|
1319
|
+
});
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
res.json({
|
|
1323
|
+
success: true,
|
|
1324
|
+
files: uploadedFiles,
|
|
1325
|
+
targetPath: resolvedTargetDir,
|
|
1326
|
+
message: `Uploaded ${uploadedFiles.length} file(s) successfully`
|
|
1327
|
+
});
|
|
1328
|
+
} catch (error) {
|
|
1329
|
+
console.error('Error uploading files:', error);
|
|
1330
|
+
// Clean up any remaining temp files
|
|
1331
|
+
if (req.files) {
|
|
1332
|
+
for (const file of req.files) {
|
|
1333
|
+
await fsPromises.unlink(file.path).catch(() => {});
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
if (error.code === 'EACCES') {
|
|
1337
|
+
res.status(403).json({ error: 'Permission denied' });
|
|
1338
|
+
} else {
|
|
1339
|
+
res.status(500).json({ error: error.message });
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
});
|
|
1343
|
+
};
|
|
1344
|
+
|
|
1345
|
+
app.post('/api/projects/:projectName/files/upload', authenticateToken, uploadFilesHandler);
|
|
1346
|
+
|
|
887
1347
|
// WebSocket connection handler that routes based on URL path
|
|
888
1348
|
wss.on('connection', (ws, request) => {
|
|
889
1349
|
const url = request.url;
|
|
@@ -920,6 +1380,10 @@ class WebSocketWriter {
|
|
|
920
1380
|
}
|
|
921
1381
|
}
|
|
922
1382
|
|
|
1383
|
+
updateWebSocket(newRawWs) {
|
|
1384
|
+
this.ws = newRawWs;
|
|
1385
|
+
}
|
|
1386
|
+
|
|
923
1387
|
setSessionId(sessionId) {
|
|
924
1388
|
this.sessionId = sessionId;
|
|
925
1389
|
}
|
|
@@ -1034,6 +1498,11 @@ function handleChatConnection(ws) {
|
|
|
1034
1498
|
} else {
|
|
1035
1499
|
// Use Claude Agents SDK
|
|
1036
1500
|
isActive = isClaudeSDKSessionActive(sessionId);
|
|
1501
|
+
if (isActive) {
|
|
1502
|
+
// Reconnect the session's writer to the new WebSocket so
|
|
1503
|
+
// subsequent SDK output flows to the refreshed client.
|
|
1504
|
+
reconnectSessionWriter(sessionId, ws);
|
|
1505
|
+
}
|
|
1037
1506
|
}
|
|
1038
1507
|
|
|
1039
1508
|
writer.send({
|
|
@@ -1042,6 +1511,17 @@ function handleChatConnection(ws) {
|
|
|
1042
1511
|
provider,
|
|
1043
1512
|
isProcessing: isActive
|
|
1044
1513
|
});
|
|
1514
|
+
} else if (data.type === 'get-pending-permissions') {
|
|
1515
|
+
// Return pending permission requests for a session
|
|
1516
|
+
const sessionId = data.sessionId;
|
|
1517
|
+
if (sessionId && isClaudeSDKSessionActive(sessionId)) {
|
|
1518
|
+
const pending = getPendingApprovalsForSession(sessionId);
|
|
1519
|
+
writer.send({
|
|
1520
|
+
type: 'pending-permissions-response',
|
|
1521
|
+
sessionId,
|
|
1522
|
+
data: pending
|
|
1523
|
+
});
|
|
1524
|
+
}
|
|
1045
1525
|
} else if (data.type === 'get-active-sessions') {
|
|
1046
1526
|
// Get all currently active sessions
|
|
1047
1527
|
const activeSessions = {
|
|
@@ -1218,7 +1698,7 @@ function handleShellConnection(ws) {
|
|
|
1218
1698
|
if (hasSession && sessionId) {
|
|
1219
1699
|
try {
|
|
1220
1700
|
// Gemini CLI enforces its own native session IDs, unlike other agents that accept arbitrary string names.
|
|
1221
|
-
// The UI only knows about its internal generated `sessionId` (e.g. gemini_1234).
|
|
1701
|
+
// The UI only knows about its internal generated `sessionId` (e.g. gemini_1234).
|
|
1222
1702
|
// We must fetch the mapping from the backend session manager to pass the native `cliSessionId` to the shell.
|
|
1223
1703
|
const sess = sessionManager.getSession(sessionId);
|
|
1224
1704
|
if (sess && sess.cliSessionId) {
|
|
@@ -1682,7 +2162,7 @@ app.get('/api/projects/:projectName/sessions/:sessionId/token-usage', authentica
|
|
|
1682
2162
|
|
|
1683
2163
|
// Allow only safe characters in sessionId
|
|
1684
2164
|
const safeSessionId = String(sessionId).replace(/[^a-zA-Z0-9._-]/g, '');
|
|
1685
|
-
if (!safeSessionId) {
|
|
2165
|
+
if (!safeSessionId || safeSessionId !== String(sessionId)) {
|
|
1686
2166
|
return res.status(400).json({ error: 'Invalid sessionId' });
|
|
1687
2167
|
}
|
|
1688
2168
|
|
|
@@ -1791,8 +2271,8 @@ app.get('/api/projects/:projectName/sessions/:sessionId/token-usage', authentica
|
|
|
1791
2271
|
|
|
1792
2272
|
// Construct the JSONL file path
|
|
1793
2273
|
// Claude stores session files in ~/.claude/projects/[encoded-project-path]/[session-id].jsonl
|
|
1794
|
-
// The encoding replaces
|
|
1795
|
-
const encodedPath = projectPath.replace(/[
|
|
2274
|
+
// The encoding replaces any non-alphanumeric character (except -) with -
|
|
2275
|
+
const encodedPath = projectPath.replace(/[^a-zA-Z0-9-]/g, '-');
|
|
1796
2276
|
const projectDir = path.join(homeDir, '.claude', 'projects', encodedPath);
|
|
1797
2277
|
|
|
1798
2278
|
const jsonlPath = path.join(projectDir, `${safeSessionId}.jsonl`);
|