@siteboon/claude-code-ui 1.21.0 → 1.22.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 +34 -14
- package/dist/assets/index-B6iL1dXV.css +32 -0
- package/dist/assets/index-Br2fwqOq.js +1397 -0
- package/dist/index.html +2 -2
- package/package.json +2 -1
- package/server/claude-sdk.js +4 -3
- package/server/index.js +433 -3
- package/dist/assets/index-Cxnz_sny.css +0 -32
- package/dist/assets/index-DN2ZJcRJ.js +0 -1381
package/dist/index.html
CHANGED
|
@@ -25,11 +25,11 @@
|
|
|
25
25
|
|
|
26
26
|
<!-- Prevent zoom on iOS -->
|
|
27
27
|
<meta name="format-detection" content="telephone=no" />
|
|
28
|
-
<script type="module" crossorigin src="/assets/index-
|
|
28
|
+
<script type="module" crossorigin src="/assets/index-Br2fwqOq.js"></script>
|
|
29
29
|
<link rel="modulepreload" crossorigin href="/assets/vendor-react-DIN4KjD2.js">
|
|
30
30
|
<link rel="modulepreload" crossorigin href="/assets/vendor-codemirror-BMLq5tLB.js">
|
|
31
31
|
<link rel="modulepreload" crossorigin href="/assets/vendor-xterm-CJZjLICi.js">
|
|
32
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
32
|
+
<link rel="stylesheet" crossorigin href="/assets/index-B6iL1dXV.css">
|
|
33
33
|
</head>
|
|
34
34
|
<body>
|
|
35
35
|
<div id="root"></div>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@siteboon/claude-code-ui",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.22.0",
|
|
4
4
|
"description": "A web-based UI for Claude Code CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "server/index.js",
|
|
@@ -78,6 +78,7 @@
|
|
|
78
78
|
"i18next": "^25.7.4",
|
|
79
79
|
"i18next-browser-languagedetector": "^8.2.0",
|
|
80
80
|
"jsonwebtoken": "^9.0.2",
|
|
81
|
+
"jszip": "^3.10.1",
|
|
81
82
|
"katex": "^0.16.25",
|
|
82
83
|
"lucide-react": "^0.515.0",
|
|
83
84
|
"mime-types": "^3.0.1",
|
package/server/claude-sdk.js
CHANGED
|
@@ -593,9 +593,6 @@ async function queryClaudeSDK(command, options = {}, ws) {
|
|
|
593
593
|
console.log('No session_id in message or already captured. message.session_id:', message.session_id, 'capturedSessionId:', capturedSessionId);
|
|
594
594
|
}
|
|
595
595
|
|
|
596
|
-
// logs which model was used in the message
|
|
597
|
-
console.log("---> Model was sent using:", Object.keys(message.modelUsage || {}));
|
|
598
|
-
|
|
599
596
|
// Transform and send message to WebSocket
|
|
600
597
|
const transformedMessage = transformMessage(message);
|
|
601
598
|
ws.send({
|
|
@@ -606,6 +603,10 @@ async function queryClaudeSDK(command, options = {}, ws) {
|
|
|
606
603
|
|
|
607
604
|
// Extract and send token budget updates from result messages
|
|
608
605
|
if (message.type === 'result') {
|
|
606
|
+
const models = Object.keys(message.modelUsage || {});
|
|
607
|
+
if (models.length > 0) {
|
|
608
|
+
console.log("---> Model was sent using:", models);
|
|
609
|
+
}
|
|
609
610
|
const tokenBudget = extractTokenBudget(message);
|
|
610
611
|
if (tokenBudget) {
|
|
611
612
|
console.log('Token budget from modelUsage:', tokenBudget);
|
package/server/index.js
CHANGED
|
@@ -884,6 +884,436 @@ app.get('/api/projects/:projectName/files', authenticateToken, async (req, res)
|
|
|
884
884
|
}
|
|
885
885
|
});
|
|
886
886
|
|
|
887
|
+
// ============================================================================
|
|
888
|
+
// FILE OPERATIONS API ENDPOINTS
|
|
889
|
+
// ============================================================================
|
|
890
|
+
|
|
891
|
+
/**
|
|
892
|
+
* Validate that a path is within the project root
|
|
893
|
+
* @param {string} projectRoot - The project root path
|
|
894
|
+
* @param {string} targetPath - The path to validate
|
|
895
|
+
* @returns {{ valid: boolean, resolved?: string, error?: string }}
|
|
896
|
+
*/
|
|
897
|
+
function validatePathInProject(projectRoot, targetPath) {
|
|
898
|
+
const resolved = path.isAbsolute(targetPath)
|
|
899
|
+
? path.resolve(targetPath)
|
|
900
|
+
: path.resolve(projectRoot, targetPath);
|
|
901
|
+
const normalizedRoot = path.resolve(projectRoot) + path.sep;
|
|
902
|
+
if (!resolved.startsWith(normalizedRoot)) {
|
|
903
|
+
return { valid: false, error: 'Path must be under project root' };
|
|
904
|
+
}
|
|
905
|
+
return { valid: true, resolved };
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
/**
|
|
909
|
+
* Validate filename - check for invalid characters
|
|
910
|
+
* @param {string} name - The filename to validate
|
|
911
|
+
* @returns {{ valid: boolean, error?: string }}
|
|
912
|
+
*/
|
|
913
|
+
function validateFilename(name) {
|
|
914
|
+
if (!name || !name.trim()) {
|
|
915
|
+
return { valid: false, error: 'Filename cannot be empty' };
|
|
916
|
+
}
|
|
917
|
+
// Check for invalid characters (Windows + Unix)
|
|
918
|
+
const invalidChars = /[<>:"/\\|?*\x00-\x1f]/;
|
|
919
|
+
if (invalidChars.test(name)) {
|
|
920
|
+
return { valid: false, error: 'Filename contains invalid characters' };
|
|
921
|
+
}
|
|
922
|
+
// Check for reserved names (Windows)
|
|
923
|
+
const reserved = /^(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])$/i;
|
|
924
|
+
if (reserved.test(name)) {
|
|
925
|
+
return { valid: false, error: 'Filename is a reserved name' };
|
|
926
|
+
}
|
|
927
|
+
// Check for dots only
|
|
928
|
+
if (/^\.+$/.test(name)) {
|
|
929
|
+
return { valid: false, error: 'Filename cannot be only dots' };
|
|
930
|
+
}
|
|
931
|
+
return { valid: true };
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
// POST /api/projects/:projectName/files/create - Create new file or directory
|
|
935
|
+
app.post('/api/projects/:projectName/files/create', authenticateToken, async (req, res) => {
|
|
936
|
+
try {
|
|
937
|
+
const { projectName } = req.params;
|
|
938
|
+
const { path: parentPath, type, name } = req.body;
|
|
939
|
+
|
|
940
|
+
// Validate input
|
|
941
|
+
if (!name || !type) {
|
|
942
|
+
return res.status(400).json({ error: 'Name and type are required' });
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
if (!['file', 'directory'].includes(type)) {
|
|
946
|
+
return res.status(400).json({ error: 'Type must be "file" or "directory"' });
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
const nameValidation = validateFilename(name);
|
|
950
|
+
if (!nameValidation.valid) {
|
|
951
|
+
return res.status(400).json({ error: nameValidation.error });
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
// Get project root
|
|
955
|
+
const projectRoot = await extractProjectDirectory(projectName).catch(() => null);
|
|
956
|
+
if (!projectRoot) {
|
|
957
|
+
return res.status(404).json({ error: 'Project not found' });
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
// Build and validate target path
|
|
961
|
+
const targetDir = parentPath || '';
|
|
962
|
+
const targetPath = targetDir ? path.join(targetDir, name) : name;
|
|
963
|
+
const validation = validatePathInProject(projectRoot, targetPath);
|
|
964
|
+
if (!validation.valid) {
|
|
965
|
+
return res.status(403).json({ error: validation.error });
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
const resolvedPath = validation.resolved;
|
|
969
|
+
|
|
970
|
+
// Check if already exists
|
|
971
|
+
try {
|
|
972
|
+
await fsPromises.access(resolvedPath);
|
|
973
|
+
return res.status(409).json({ error: `${type === 'file' ? 'File' : 'Directory'} already exists` });
|
|
974
|
+
} catch {
|
|
975
|
+
// Doesn't exist, which is what we want
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
// Create file or directory
|
|
979
|
+
if (type === 'directory') {
|
|
980
|
+
await fsPromises.mkdir(resolvedPath, { recursive: false });
|
|
981
|
+
} else {
|
|
982
|
+
// Ensure parent directory exists
|
|
983
|
+
const parentDir = path.dirname(resolvedPath);
|
|
984
|
+
try {
|
|
985
|
+
await fsPromises.access(parentDir);
|
|
986
|
+
} catch {
|
|
987
|
+
await fsPromises.mkdir(parentDir, { recursive: true });
|
|
988
|
+
}
|
|
989
|
+
await fsPromises.writeFile(resolvedPath, '', 'utf8');
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
res.json({
|
|
993
|
+
success: true,
|
|
994
|
+
path: resolvedPath,
|
|
995
|
+
name,
|
|
996
|
+
type,
|
|
997
|
+
message: `${type === 'file' ? 'File' : 'Directory'} created successfully`
|
|
998
|
+
});
|
|
999
|
+
} catch (error) {
|
|
1000
|
+
console.error('Error creating file/directory:', error);
|
|
1001
|
+
if (error.code === 'EACCES') {
|
|
1002
|
+
res.status(403).json({ error: 'Permission denied' });
|
|
1003
|
+
} else if (error.code === 'ENOENT') {
|
|
1004
|
+
res.status(404).json({ error: 'Parent directory not found' });
|
|
1005
|
+
} else {
|
|
1006
|
+
res.status(500).json({ error: error.message });
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
});
|
|
1010
|
+
|
|
1011
|
+
// PUT /api/projects/:projectName/files/rename - Rename file or directory
|
|
1012
|
+
app.put('/api/projects/:projectName/files/rename', authenticateToken, async (req, res) => {
|
|
1013
|
+
try {
|
|
1014
|
+
const { projectName } = req.params;
|
|
1015
|
+
const { oldPath, newName } = req.body;
|
|
1016
|
+
|
|
1017
|
+
// Validate input
|
|
1018
|
+
if (!oldPath || !newName) {
|
|
1019
|
+
return res.status(400).json({ error: 'oldPath and newName are required' });
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
const nameValidation = validateFilename(newName);
|
|
1023
|
+
if (!nameValidation.valid) {
|
|
1024
|
+
return res.status(400).json({ error: nameValidation.error });
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
// Get project root
|
|
1028
|
+
const projectRoot = await extractProjectDirectory(projectName).catch(() => null);
|
|
1029
|
+
if (!projectRoot) {
|
|
1030
|
+
return res.status(404).json({ error: 'Project not found' });
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
// Validate old path
|
|
1034
|
+
const oldValidation = validatePathInProject(projectRoot, oldPath);
|
|
1035
|
+
if (!oldValidation.valid) {
|
|
1036
|
+
return res.status(403).json({ error: oldValidation.error });
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
const resolvedOldPath = oldValidation.resolved;
|
|
1040
|
+
|
|
1041
|
+
// Check if old path exists
|
|
1042
|
+
try {
|
|
1043
|
+
await fsPromises.access(resolvedOldPath);
|
|
1044
|
+
} catch {
|
|
1045
|
+
return res.status(404).json({ error: 'File or directory not found' });
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
// Build and validate new path
|
|
1049
|
+
const parentDir = path.dirname(resolvedOldPath);
|
|
1050
|
+
const resolvedNewPath = path.join(parentDir, newName);
|
|
1051
|
+
const newValidation = validatePathInProject(projectRoot, resolvedNewPath);
|
|
1052
|
+
if (!newValidation.valid) {
|
|
1053
|
+
return res.status(403).json({ error: newValidation.error });
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
// Check if new path already exists
|
|
1057
|
+
try {
|
|
1058
|
+
await fsPromises.access(resolvedNewPath);
|
|
1059
|
+
return res.status(409).json({ error: 'A file or directory with this name already exists' });
|
|
1060
|
+
} catch {
|
|
1061
|
+
// Doesn't exist, which is what we want
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
// Rename
|
|
1065
|
+
await fsPromises.rename(resolvedOldPath, resolvedNewPath);
|
|
1066
|
+
|
|
1067
|
+
res.json({
|
|
1068
|
+
success: true,
|
|
1069
|
+
oldPath: resolvedOldPath,
|
|
1070
|
+
newPath: resolvedNewPath,
|
|
1071
|
+
newName,
|
|
1072
|
+
message: 'Renamed successfully'
|
|
1073
|
+
});
|
|
1074
|
+
} catch (error) {
|
|
1075
|
+
console.error('Error renaming file/directory:', error);
|
|
1076
|
+
if (error.code === 'EACCES') {
|
|
1077
|
+
res.status(403).json({ error: 'Permission denied' });
|
|
1078
|
+
} else if (error.code === 'ENOENT') {
|
|
1079
|
+
res.status(404).json({ error: 'File or directory not found' });
|
|
1080
|
+
} else if (error.code === 'EXDEV') {
|
|
1081
|
+
res.status(400).json({ error: 'Cannot move across different filesystems' });
|
|
1082
|
+
} else {
|
|
1083
|
+
res.status(500).json({ error: error.message });
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
});
|
|
1087
|
+
|
|
1088
|
+
// DELETE /api/projects/:projectName/files - Delete file or directory
|
|
1089
|
+
app.delete('/api/projects/:projectName/files', authenticateToken, async (req, res) => {
|
|
1090
|
+
try {
|
|
1091
|
+
const { projectName } = req.params;
|
|
1092
|
+
const { path: targetPath, type } = req.body;
|
|
1093
|
+
|
|
1094
|
+
// Validate input
|
|
1095
|
+
if (!targetPath) {
|
|
1096
|
+
return res.status(400).json({ error: 'Path is required' });
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
// Get project root
|
|
1100
|
+
const projectRoot = await extractProjectDirectory(projectName).catch(() => null);
|
|
1101
|
+
if (!projectRoot) {
|
|
1102
|
+
return res.status(404).json({ error: 'Project not found' });
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
// Validate path
|
|
1106
|
+
const validation = validatePathInProject(projectRoot, targetPath);
|
|
1107
|
+
if (!validation.valid) {
|
|
1108
|
+
return res.status(403).json({ error: validation.error });
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
const resolvedPath = validation.resolved;
|
|
1112
|
+
|
|
1113
|
+
// Check if path exists and get stats
|
|
1114
|
+
let stats;
|
|
1115
|
+
try {
|
|
1116
|
+
stats = await fsPromises.stat(resolvedPath);
|
|
1117
|
+
} catch {
|
|
1118
|
+
return res.status(404).json({ error: 'File or directory not found' });
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
// Prevent deleting the project root itself
|
|
1122
|
+
if (resolvedPath === path.resolve(projectRoot)) {
|
|
1123
|
+
return res.status(403).json({ error: 'Cannot delete project root directory' });
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
// Delete based on type
|
|
1127
|
+
if (stats.isDirectory()) {
|
|
1128
|
+
await fsPromises.rm(resolvedPath, { recursive: true, force: true });
|
|
1129
|
+
} else {
|
|
1130
|
+
await fsPromises.unlink(resolvedPath);
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
res.json({
|
|
1134
|
+
success: true,
|
|
1135
|
+
path: resolvedPath,
|
|
1136
|
+
type: stats.isDirectory() ? 'directory' : 'file',
|
|
1137
|
+
message: 'Deleted successfully'
|
|
1138
|
+
});
|
|
1139
|
+
} catch (error) {
|
|
1140
|
+
console.error('Error deleting file/directory:', error);
|
|
1141
|
+
if (error.code === 'EACCES') {
|
|
1142
|
+
res.status(403).json({ error: 'Permission denied' });
|
|
1143
|
+
} else if (error.code === 'ENOENT') {
|
|
1144
|
+
res.status(404).json({ error: 'File or directory not found' });
|
|
1145
|
+
} else if (error.code === 'ENOTEMPTY') {
|
|
1146
|
+
res.status(400).json({ error: 'Directory is not empty' });
|
|
1147
|
+
} else {
|
|
1148
|
+
res.status(500).json({ error: error.message });
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
});
|
|
1152
|
+
|
|
1153
|
+
// POST /api/projects/:projectName/files/upload - Upload files
|
|
1154
|
+
// Dynamic import of multer for file uploads
|
|
1155
|
+
const uploadFilesHandler = async (req, res) => {
|
|
1156
|
+
// Dynamic import of multer
|
|
1157
|
+
const multer = (await import('multer')).default;
|
|
1158
|
+
|
|
1159
|
+
const uploadMiddleware = multer({
|
|
1160
|
+
storage: multer.diskStorage({
|
|
1161
|
+
destination: (req, file, cb) => {
|
|
1162
|
+
cb(null, os.tmpdir());
|
|
1163
|
+
},
|
|
1164
|
+
filename: (req, file, cb) => {
|
|
1165
|
+
// Use a unique temp name, but preserve original name in file.originalname
|
|
1166
|
+
// Note: file.originalname may contain path separators for folder uploads
|
|
1167
|
+
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
|
|
1168
|
+
// For temp file, just use a safe unique name without the path
|
|
1169
|
+
cb(null, `upload-${uniqueSuffix}`);
|
|
1170
|
+
}
|
|
1171
|
+
}),
|
|
1172
|
+
limits: {
|
|
1173
|
+
fileSize: 50 * 1024 * 1024, // 50MB limit
|
|
1174
|
+
files: 20 // Max 20 files at once
|
|
1175
|
+
}
|
|
1176
|
+
});
|
|
1177
|
+
|
|
1178
|
+
// Use multer middleware
|
|
1179
|
+
uploadMiddleware.array('files', 20)(req, res, async (err) => {
|
|
1180
|
+
if (err) {
|
|
1181
|
+
console.error('Multer error:', err);
|
|
1182
|
+
if (err.code === 'LIMIT_FILE_SIZE') {
|
|
1183
|
+
return res.status(400).json({ error: 'File too large. Maximum size is 50MB.' });
|
|
1184
|
+
}
|
|
1185
|
+
if (err.code === 'LIMIT_FILE_COUNT') {
|
|
1186
|
+
return res.status(400).json({ error: 'Too many files. Maximum is 20 files.' });
|
|
1187
|
+
}
|
|
1188
|
+
return res.status(500).json({ error: err.message });
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
try {
|
|
1192
|
+
const { projectName } = req.params;
|
|
1193
|
+
const { targetPath, relativePaths } = req.body;
|
|
1194
|
+
|
|
1195
|
+
// Parse relative paths if provided (for folder uploads)
|
|
1196
|
+
let filePaths = [];
|
|
1197
|
+
if (relativePaths) {
|
|
1198
|
+
try {
|
|
1199
|
+
filePaths = JSON.parse(relativePaths);
|
|
1200
|
+
} catch (e) {
|
|
1201
|
+
console.log('[DEBUG] Failed to parse relativePaths:', relativePaths);
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
console.log('[DEBUG] File upload request:', {
|
|
1206
|
+
projectName,
|
|
1207
|
+
targetPath: JSON.stringify(targetPath),
|
|
1208
|
+
targetPathType: typeof targetPath,
|
|
1209
|
+
filesCount: req.files?.length,
|
|
1210
|
+
relativePaths: filePaths
|
|
1211
|
+
});
|
|
1212
|
+
|
|
1213
|
+
if (!req.files || req.files.length === 0) {
|
|
1214
|
+
return res.status(400).json({ error: 'No files provided' });
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
// Get project root
|
|
1218
|
+
const projectRoot = await extractProjectDirectory(projectName).catch(() => null);
|
|
1219
|
+
if (!projectRoot) {
|
|
1220
|
+
return res.status(404).json({ error: 'Project not found' });
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
console.log('[DEBUG] Project root:', projectRoot);
|
|
1224
|
+
|
|
1225
|
+
// Validate and resolve target path
|
|
1226
|
+
// If targetPath is empty or '.', use project root directly
|
|
1227
|
+
const targetDir = targetPath || '';
|
|
1228
|
+
let resolvedTargetDir;
|
|
1229
|
+
|
|
1230
|
+
console.log('[DEBUG] Target dir:', JSON.stringify(targetDir));
|
|
1231
|
+
|
|
1232
|
+
if (!targetDir || targetDir === '.' || targetDir === './') {
|
|
1233
|
+
// Empty path means upload to project root
|
|
1234
|
+
resolvedTargetDir = path.resolve(projectRoot);
|
|
1235
|
+
console.log('[DEBUG] Using project root as target:', resolvedTargetDir);
|
|
1236
|
+
} else {
|
|
1237
|
+
const validation = validatePathInProject(projectRoot, targetDir);
|
|
1238
|
+
if (!validation.valid) {
|
|
1239
|
+
console.log('[DEBUG] Path validation failed:', validation.error);
|
|
1240
|
+
return res.status(403).json({ error: validation.error });
|
|
1241
|
+
}
|
|
1242
|
+
resolvedTargetDir = validation.resolved;
|
|
1243
|
+
console.log('[DEBUG] Resolved target dir:', resolvedTargetDir);
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
// Ensure target directory exists
|
|
1247
|
+
try {
|
|
1248
|
+
await fsPromises.access(resolvedTargetDir);
|
|
1249
|
+
} catch {
|
|
1250
|
+
await fsPromises.mkdir(resolvedTargetDir, { recursive: true });
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
// Move uploaded files from temp to target directory
|
|
1254
|
+
const uploadedFiles = [];
|
|
1255
|
+
console.log('[DEBUG] Processing files:', req.files.map(f => ({ originalname: f.originalname, path: f.path })));
|
|
1256
|
+
for (let i = 0; i < req.files.length; i++) {
|
|
1257
|
+
const file = req.files[i];
|
|
1258
|
+
// Use relative path if provided (for folder uploads), otherwise use originalname
|
|
1259
|
+
const fileName = (filePaths && filePaths[i]) ? filePaths[i] : file.originalname;
|
|
1260
|
+
console.log('[DEBUG] Processing file:', fileName, '(originalname:', file.originalname + ')');
|
|
1261
|
+
const destPath = path.join(resolvedTargetDir, fileName);
|
|
1262
|
+
|
|
1263
|
+
// Validate destination path
|
|
1264
|
+
const destValidation = validatePathInProject(projectRoot, destPath);
|
|
1265
|
+
if (!destValidation.valid) {
|
|
1266
|
+
console.log('[DEBUG] Destination validation failed for:', destPath);
|
|
1267
|
+
// Clean up temp file
|
|
1268
|
+
await fsPromises.unlink(file.path).catch(() => {});
|
|
1269
|
+
continue;
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
// Ensure parent directory exists (for nested files from folder upload)
|
|
1273
|
+
const parentDir = path.dirname(destPath);
|
|
1274
|
+
try {
|
|
1275
|
+
await fsPromises.access(parentDir);
|
|
1276
|
+
} catch {
|
|
1277
|
+
await fsPromises.mkdir(parentDir, { recursive: true });
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
// Move file (copy + unlink to handle cross-device scenarios)
|
|
1281
|
+
await fsPromises.copyFile(file.path, destPath);
|
|
1282
|
+
await fsPromises.unlink(file.path);
|
|
1283
|
+
|
|
1284
|
+
uploadedFiles.push({
|
|
1285
|
+
name: fileName,
|
|
1286
|
+
path: destPath,
|
|
1287
|
+
size: file.size,
|
|
1288
|
+
mimeType: file.mimetype
|
|
1289
|
+
});
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
res.json({
|
|
1293
|
+
success: true,
|
|
1294
|
+
files: uploadedFiles,
|
|
1295
|
+
targetPath: resolvedTargetDir,
|
|
1296
|
+
message: `Uploaded ${uploadedFiles.length} file(s) successfully`
|
|
1297
|
+
});
|
|
1298
|
+
} catch (error) {
|
|
1299
|
+
console.error('Error uploading files:', error);
|
|
1300
|
+
// Clean up any remaining temp files
|
|
1301
|
+
if (req.files) {
|
|
1302
|
+
for (const file of req.files) {
|
|
1303
|
+
await fsPromises.unlink(file.path).catch(() => {});
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
if (error.code === 'EACCES') {
|
|
1307
|
+
res.status(403).json({ error: 'Permission denied' });
|
|
1308
|
+
} else {
|
|
1309
|
+
res.status(500).json({ error: error.message });
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
});
|
|
1313
|
+
};
|
|
1314
|
+
|
|
1315
|
+
app.post('/api/projects/:projectName/files/upload', authenticateToken, uploadFilesHandler);
|
|
1316
|
+
|
|
887
1317
|
// WebSocket connection handler that routes based on URL path
|
|
888
1318
|
wss.on('connection', (ws, request) => {
|
|
889
1319
|
const url = request.url;
|
|
@@ -1218,7 +1648,7 @@ function handleShellConnection(ws) {
|
|
|
1218
1648
|
if (hasSession && sessionId) {
|
|
1219
1649
|
try {
|
|
1220
1650
|
// 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).
|
|
1651
|
+
// The UI only knows about its internal generated `sessionId` (e.g. gemini_1234).
|
|
1222
1652
|
// We must fetch the mapping from the backend session manager to pass the native `cliSessionId` to the shell.
|
|
1223
1653
|
const sess = sessionManager.getSession(sessionId);
|
|
1224
1654
|
if (sess && sess.cliSessionId) {
|
|
@@ -1791,8 +2221,8 @@ app.get('/api/projects/:projectName/sessions/:sessionId/token-usage', authentica
|
|
|
1791
2221
|
|
|
1792
2222
|
// Construct the JSONL file path
|
|
1793
2223
|
// Claude stores session files in ~/.claude/projects/[encoded-project-path]/[session-id].jsonl
|
|
1794
|
-
// The encoding replaces
|
|
1795
|
-
const encodedPath = projectPath.replace(/[
|
|
2224
|
+
// The encoding replaces any non-alphanumeric character (except -) with -
|
|
2225
|
+
const encodedPath = projectPath.replace(/[^a-zA-Z0-9-]/g, '-');
|
|
1796
2226
|
const projectDir = path.join(homeDir, '.claude', 'projects', encodedPath);
|
|
1797
2227
|
|
|
1798
2228
|
const jsonlPath = path.join(projectDir, `${safeSessionId}.jsonl`);
|