@siteboon/claude-code-ui 1.13.6 → 1.14.1
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 +2 -0
- package/README.zh-CN.md +371 -0
- package/dist/assets/index-BQGOOBNa.css +32 -0
- package/dist/assets/index-Dqg05I_l.js +1239 -0
- package/dist/assets/{vendor-codemirror-CnTQH7Pk.js → vendor-codemirror-CJLzwpLB.js} +3 -3
- package/dist/assets/{vendor-react-DVSKlM5e.js → vendor-react-DcyRfQm3.js} +10 -10
- package/dist/index.html +4 -4
- package/package.json +5 -1
- package/server/claude-sdk.js +217 -23
- package/server/cursor-cli.js +17 -9
- package/server/index.js +102 -9
- package/server/middleware/auth.js +6 -1
- package/server/openai-codex.js +4 -2
- package/server/projects.js +119 -34
- package/server/routes/codex.js +56 -22
- package/server/routes/projects.js +204 -32
- package/dist/assets/index-Cc6pl7ji.css +0 -32
- package/dist/assets/index-lf1GuHwT.js +0 -1206
- package/server/database/auth.db +0 -0
package/server/projects.js
CHANGED
|
@@ -379,22 +379,46 @@ async function extractProjectDirectory(projectName) {
|
|
|
379
379
|
}
|
|
380
380
|
}
|
|
381
381
|
|
|
382
|
-
async function getProjects() {
|
|
382
|
+
async function getProjects(progressCallback = null) {
|
|
383
383
|
const claudeDir = path.join(os.homedir(), '.claude', 'projects');
|
|
384
384
|
const config = await loadProjectConfig();
|
|
385
385
|
const projects = [];
|
|
386
386
|
const existingProjects = new Set();
|
|
387
|
-
|
|
387
|
+
let totalProjects = 0;
|
|
388
|
+
let processedProjects = 0;
|
|
389
|
+
let directories = [];
|
|
390
|
+
|
|
388
391
|
try {
|
|
389
392
|
// Check if the .claude/projects directory exists
|
|
390
393
|
await fs.access(claudeDir);
|
|
391
|
-
|
|
394
|
+
|
|
392
395
|
// First, get existing Claude projects from the file system
|
|
393
396
|
const entries = await fs.readdir(claudeDir, { withFileTypes: true });
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
397
|
+
directories = entries.filter(e => e.isDirectory());
|
|
398
|
+
|
|
399
|
+
// Build set of existing project names for later
|
|
400
|
+
directories.forEach(e => existingProjects.add(e.name));
|
|
401
|
+
|
|
402
|
+
// Count manual projects not already in directories
|
|
403
|
+
const manualProjectsCount = Object.entries(config)
|
|
404
|
+
.filter(([name, cfg]) => cfg.manuallyAdded && !existingProjects.has(name))
|
|
405
|
+
.length;
|
|
406
|
+
|
|
407
|
+
totalProjects = directories.length + manualProjectsCount;
|
|
408
|
+
|
|
409
|
+
for (const entry of directories) {
|
|
410
|
+
processedProjects++;
|
|
411
|
+
|
|
412
|
+
// Emit progress
|
|
413
|
+
if (progressCallback) {
|
|
414
|
+
progressCallback({
|
|
415
|
+
phase: 'loading',
|
|
416
|
+
current: processedProjects,
|
|
417
|
+
total: totalProjects,
|
|
418
|
+
currentProject: entry.name
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
|
|
398
422
|
const projectPath = path.join(claudeDir, entry.name);
|
|
399
423
|
|
|
400
424
|
// Extract actual project directory from JSONL sessions
|
|
@@ -460,20 +484,35 @@ async function getProjects() {
|
|
|
460
484
|
status: 'error'
|
|
461
485
|
};
|
|
462
486
|
}
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
}
|
|
487
|
+
|
|
488
|
+
projects.push(project);
|
|
466
489
|
}
|
|
467
490
|
} catch (error) {
|
|
468
491
|
// If the directory doesn't exist (ENOENT), that's okay - just continue with empty projects
|
|
469
492
|
if (error.code !== 'ENOENT') {
|
|
470
493
|
console.error('Error reading projects directory:', error);
|
|
471
494
|
}
|
|
495
|
+
// Calculate total for manual projects only (no directories exist)
|
|
496
|
+
totalProjects = Object.entries(config)
|
|
497
|
+
.filter(([name, cfg]) => cfg.manuallyAdded)
|
|
498
|
+
.length;
|
|
472
499
|
}
|
|
473
500
|
|
|
474
501
|
// Add manually configured projects that don't exist as folders yet
|
|
475
502
|
for (const [projectName, projectConfig] of Object.entries(config)) {
|
|
476
503
|
if (!existingProjects.has(projectName) && projectConfig.manuallyAdded) {
|
|
504
|
+
processedProjects++;
|
|
505
|
+
|
|
506
|
+
// Emit progress for manual projects
|
|
507
|
+
if (progressCallback) {
|
|
508
|
+
progressCallback({
|
|
509
|
+
phase: 'loading',
|
|
510
|
+
current: processedProjects,
|
|
511
|
+
total: totalProjects,
|
|
512
|
+
currentProject: projectName
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
|
|
477
516
|
// Use the original path if available, otherwise extract from potential sessions
|
|
478
517
|
let actualProjectDir = projectConfig.originalPath;
|
|
479
518
|
|
|
@@ -541,7 +580,16 @@ async function getProjects() {
|
|
|
541
580
|
projects.push(project);
|
|
542
581
|
}
|
|
543
582
|
}
|
|
544
|
-
|
|
583
|
+
|
|
584
|
+
// Emit completion after all projects (including manual) are processed
|
|
585
|
+
if (progressCallback) {
|
|
586
|
+
progressCallback({
|
|
587
|
+
phase: 'complete',
|
|
588
|
+
current: totalProjects,
|
|
589
|
+
total: totalProjects
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
|
|
545
593
|
return projects;
|
|
546
594
|
}
|
|
547
595
|
|
|
@@ -978,25 +1026,56 @@ async function isProjectEmpty(projectName) {
|
|
|
978
1026
|
}
|
|
979
1027
|
}
|
|
980
1028
|
|
|
981
|
-
// Delete
|
|
982
|
-
async function deleteProject(projectName) {
|
|
1029
|
+
// Delete a project (force=true to delete even with sessions)
|
|
1030
|
+
async function deleteProject(projectName, force = false) {
|
|
983
1031
|
const projectDir = path.join(os.homedir(), '.claude', 'projects', projectName);
|
|
984
|
-
|
|
1032
|
+
|
|
985
1033
|
try {
|
|
986
|
-
// First check if the project is empty
|
|
987
1034
|
const isEmpty = await isProjectEmpty(projectName);
|
|
988
|
-
if (!isEmpty) {
|
|
1035
|
+
if (!isEmpty && !force) {
|
|
989
1036
|
throw new Error('Cannot delete project with existing sessions');
|
|
990
1037
|
}
|
|
991
|
-
|
|
992
|
-
|
|
1038
|
+
|
|
1039
|
+
const config = await loadProjectConfig();
|
|
1040
|
+
let projectPath = config[projectName]?.path || config[projectName]?.originalPath;
|
|
1041
|
+
|
|
1042
|
+
// Fallback to extractProjectDirectory if projectPath is not in config
|
|
1043
|
+
if (!projectPath) {
|
|
1044
|
+
projectPath = await extractProjectDirectory(projectName);
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
// Remove the project directory (includes all Claude sessions)
|
|
993
1048
|
await fs.rm(projectDir, { recursive: true, force: true });
|
|
994
|
-
|
|
1049
|
+
|
|
1050
|
+
// Delete all Codex sessions associated with this project
|
|
1051
|
+
if (projectPath) {
|
|
1052
|
+
try {
|
|
1053
|
+
const codexSessions = await getCodexSessions(projectPath, { limit: 0 });
|
|
1054
|
+
for (const session of codexSessions) {
|
|
1055
|
+
try {
|
|
1056
|
+
await deleteCodexSession(session.id);
|
|
1057
|
+
} catch (err) {
|
|
1058
|
+
console.warn(`Failed to delete Codex session ${session.id}:`, err.message);
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
} catch (err) {
|
|
1062
|
+
console.warn('Failed to delete Codex sessions:', err.message);
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
// Delete Cursor sessions directory if it exists
|
|
1066
|
+
try {
|
|
1067
|
+
const hash = crypto.createHash('md5').update(projectPath).digest('hex');
|
|
1068
|
+
const cursorProjectDir = path.join(os.homedir(), '.cursor', 'chats', hash);
|
|
1069
|
+
await fs.rm(cursorProjectDir, { recursive: true, force: true });
|
|
1070
|
+
} catch (err) {
|
|
1071
|
+
// Cursor dir may not exist, ignore
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
|
|
995
1075
|
// Remove from project config
|
|
996
|
-
const config = await loadProjectConfig();
|
|
997
1076
|
delete config[projectName];
|
|
998
1077
|
await saveProjectConfig(config);
|
|
999
|
-
|
|
1078
|
+
|
|
1000
1079
|
return true;
|
|
1001
1080
|
} catch (error) {
|
|
1002
1081
|
console.error(`Error deleting project ${projectName}:`, error);
|
|
@@ -1007,17 +1086,17 @@ async function deleteProject(projectName) {
|
|
|
1007
1086
|
// Add a project manually to the config (without creating folders)
|
|
1008
1087
|
async function addProjectManually(projectPath, displayName = null) {
|
|
1009
1088
|
const absolutePath = path.resolve(projectPath);
|
|
1010
|
-
|
|
1089
|
+
|
|
1011
1090
|
try {
|
|
1012
1091
|
// Check if the path exists
|
|
1013
1092
|
await fs.access(absolutePath);
|
|
1014
1093
|
} catch (error) {
|
|
1015
1094
|
throw new Error(`Path does not exist: ${absolutePath}`);
|
|
1016
1095
|
}
|
|
1017
|
-
|
|
1096
|
+
|
|
1018
1097
|
// Generate project name (encode path for use as directory name)
|
|
1019
|
-
const projectName = absolutePath.replace(
|
|
1020
|
-
|
|
1098
|
+
const projectName = absolutePath.replace(/[\\/:\s~_]/g, '-');
|
|
1099
|
+
|
|
1021
1100
|
// Check if project already exists in config
|
|
1022
1101
|
const config = await loadProjectConfig();
|
|
1023
1102
|
const projectDir = path.join(os.homedir(), '.claude', 'projects', projectName);
|
|
@@ -1028,13 +1107,13 @@ async function addProjectManually(projectPath, displayName = null) {
|
|
|
1028
1107
|
|
|
1029
1108
|
// Allow adding projects even if the directory exists - this enables tracking
|
|
1030
1109
|
// existing Claude Code or Cursor projects in the UI
|
|
1031
|
-
|
|
1110
|
+
|
|
1032
1111
|
// Add to config as manually added project
|
|
1033
1112
|
config[projectName] = {
|
|
1034
1113
|
manuallyAdded: true,
|
|
1035
1114
|
originalPath: absolutePath
|
|
1036
1115
|
};
|
|
1037
|
-
|
|
1116
|
+
|
|
1038
1117
|
if (displayName) {
|
|
1039
1118
|
config[projectName].displayName = displayName;
|
|
1040
1119
|
}
|
|
@@ -1166,7 +1245,8 @@ async function getCursorSessions(projectPath) {
|
|
|
1166
1245
|
|
|
1167
1246
|
|
|
1168
1247
|
// Fetch Codex sessions for a given project path
|
|
1169
|
-
async function getCodexSessions(projectPath) {
|
|
1248
|
+
async function getCodexSessions(projectPath, options = {}) {
|
|
1249
|
+
const { limit = 5 } = options;
|
|
1170
1250
|
try {
|
|
1171
1251
|
const codexSessionsDir = path.join(os.homedir(), '.codex', 'sessions');
|
|
1172
1252
|
const sessions = [];
|
|
@@ -1206,7 +1286,12 @@ async function getCodexSessions(projectPath) {
|
|
|
1206
1286
|
const sessionData = await parseCodexSessionFile(filePath);
|
|
1207
1287
|
|
|
1208
1288
|
// Check if this session matches the project path
|
|
1209
|
-
|
|
1289
|
+
// Handle Windows long paths with \\?\ prefix
|
|
1290
|
+
const sessionCwd = sessionData?.cwd || '';
|
|
1291
|
+
const cleanSessionCwd = sessionCwd.startsWith('\\\\?\\') ? sessionCwd.slice(4) : sessionCwd;
|
|
1292
|
+
const cleanProjectPath = projectPath.startsWith('\\\\?\\') ? projectPath.slice(4) : projectPath;
|
|
1293
|
+
|
|
1294
|
+
if (sessionData && (sessionData.cwd === projectPath || cleanSessionCwd === cleanProjectPath || path.relative(cleanSessionCwd, cleanProjectPath) === '')) {
|
|
1210
1295
|
sessions.push({
|
|
1211
1296
|
id: sessionData.id,
|
|
1212
1297
|
summary: sessionData.summary || 'Codex Session',
|
|
@@ -1226,8 +1311,8 @@ async function getCodexSessions(projectPath) {
|
|
|
1226
1311
|
// Sort sessions by last activity (newest first)
|
|
1227
1312
|
sessions.sort((a, b) => new Date(b.lastActivity) - new Date(a.lastActivity));
|
|
1228
1313
|
|
|
1229
|
-
// Return
|
|
1230
|
-
return sessions.slice(0,
|
|
1314
|
+
// Return limited sessions for performance (0 = unlimited for deletion)
|
|
1315
|
+
return limit > 0 ? sessions.slice(0, limit) : sessions;
|
|
1231
1316
|
|
|
1232
1317
|
} catch (error) {
|
|
1233
1318
|
console.error('Error fetching Codex sessions:', error);
|
|
@@ -1273,12 +1358,12 @@ async function parseCodexSessionFile(filePath) {
|
|
|
1273
1358
|
// Count messages and extract user messages for summary
|
|
1274
1359
|
if (entry.type === 'event_msg' && entry.payload?.type === 'user_message') {
|
|
1275
1360
|
messageCount++;
|
|
1276
|
-
if (entry.payload.
|
|
1277
|
-
lastUserMessage = entry.payload.
|
|
1361
|
+
if (entry.payload.message) {
|
|
1362
|
+
lastUserMessage = entry.payload.message;
|
|
1278
1363
|
}
|
|
1279
1364
|
}
|
|
1280
1365
|
|
|
1281
|
-
if (entry.type === 'response_item' && entry.payload?.type === 'message') {
|
|
1366
|
+
if (entry.type === 'response_item' && entry.payload?.type === 'message' && entry.payload.role === 'assistant') {
|
|
1282
1367
|
messageCount++;
|
|
1283
1368
|
}
|
|
1284
1369
|
|
package/server/routes/codex.js
CHANGED
|
@@ -8,6 +8,17 @@ import { getCodexSessions, getCodexSessionMessages, deleteCodexSession } from '.
|
|
|
8
8
|
|
|
9
9
|
const router = express.Router();
|
|
10
10
|
|
|
11
|
+
function createCliResponder(res) {
|
|
12
|
+
let responded = false;
|
|
13
|
+
return (status, payload) => {
|
|
14
|
+
if (responded || res.headersSent) {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
responded = true;
|
|
18
|
+
res.status(status).json(payload);
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
11
22
|
router.get('/config', async (req, res) => {
|
|
12
23
|
try {
|
|
13
24
|
const configPath = path.join(os.homedir(), '.codex', 'config.toml');
|
|
@@ -88,24 +99,30 @@ router.delete('/sessions/:sessionId', async (req, res) => {
|
|
|
88
99
|
|
|
89
100
|
router.get('/mcp/cli/list', async (req, res) => {
|
|
90
101
|
try {
|
|
102
|
+
const respond = createCliResponder(res);
|
|
91
103
|
const proc = spawn('codex', ['mcp', 'list'], { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
92
104
|
|
|
93
105
|
let stdout = '';
|
|
94
106
|
let stderr = '';
|
|
95
107
|
|
|
96
|
-
proc.stdout
|
|
97
|
-
proc.stderr
|
|
108
|
+
proc.stdout?.on('data', (data) => { stdout += data.toString(); });
|
|
109
|
+
proc.stderr?.on('data', (data) => { stderr += data.toString(); });
|
|
98
110
|
|
|
99
111
|
proc.on('close', (code) => {
|
|
100
112
|
if (code === 0) {
|
|
101
|
-
|
|
113
|
+
respond(200, { success: true, output: stdout, servers: parseCodexListOutput(stdout) });
|
|
102
114
|
} else {
|
|
103
|
-
|
|
115
|
+
respond(500, { error: 'Codex CLI command failed', details: stderr || `Exited with code ${code}` });
|
|
104
116
|
}
|
|
105
117
|
});
|
|
106
118
|
|
|
107
119
|
proc.on('error', (error) => {
|
|
108
|
-
|
|
120
|
+
const isMissing = error?.code === 'ENOENT';
|
|
121
|
+
respond(isMissing ? 503 : 500, {
|
|
122
|
+
error: isMissing ? 'Codex CLI not installed' : 'Failed to run Codex CLI',
|
|
123
|
+
details: error.message,
|
|
124
|
+
code: error.code
|
|
125
|
+
});
|
|
109
126
|
});
|
|
110
127
|
} catch (error) {
|
|
111
128
|
res.status(500).json({ error: 'Failed to list MCP servers', details: error.message });
|
|
@@ -133,24 +150,30 @@ router.post('/mcp/cli/add', async (req, res) => {
|
|
|
133
150
|
cliArgs.push(...args);
|
|
134
151
|
}
|
|
135
152
|
|
|
153
|
+
const respond = createCliResponder(res);
|
|
136
154
|
const proc = spawn('codex', cliArgs, { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
137
155
|
|
|
138
156
|
let stdout = '';
|
|
139
157
|
let stderr = '';
|
|
140
158
|
|
|
141
|
-
proc.stdout
|
|
142
|
-
proc.stderr
|
|
159
|
+
proc.stdout?.on('data', (data) => { stdout += data.toString(); });
|
|
160
|
+
proc.stderr?.on('data', (data) => { stderr += data.toString(); });
|
|
143
161
|
|
|
144
162
|
proc.on('close', (code) => {
|
|
145
163
|
if (code === 0) {
|
|
146
|
-
|
|
164
|
+
respond(200, { success: true, output: stdout, message: `MCP server "${name}" added successfully` });
|
|
147
165
|
} else {
|
|
148
|
-
|
|
166
|
+
respond(400, { error: 'Codex CLI command failed', details: stderr || `Exited with code ${code}` });
|
|
149
167
|
}
|
|
150
168
|
});
|
|
151
169
|
|
|
152
170
|
proc.on('error', (error) => {
|
|
153
|
-
|
|
171
|
+
const isMissing = error?.code === 'ENOENT';
|
|
172
|
+
respond(isMissing ? 503 : 500, {
|
|
173
|
+
error: isMissing ? 'Codex CLI not installed' : 'Failed to run Codex CLI',
|
|
174
|
+
details: error.message,
|
|
175
|
+
code: error.code
|
|
176
|
+
});
|
|
154
177
|
});
|
|
155
178
|
} catch (error) {
|
|
156
179
|
res.status(500).json({ error: 'Failed to add MCP server', details: error.message });
|
|
@@ -161,24 +184,30 @@ router.delete('/mcp/cli/remove/:name', async (req, res) => {
|
|
|
161
184
|
try {
|
|
162
185
|
const { name } = req.params;
|
|
163
186
|
|
|
187
|
+
const respond = createCliResponder(res);
|
|
164
188
|
const proc = spawn('codex', ['mcp', 'remove', name], { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
165
189
|
|
|
166
190
|
let stdout = '';
|
|
167
191
|
let stderr = '';
|
|
168
192
|
|
|
169
|
-
proc.stdout
|
|
170
|
-
proc.stderr
|
|
193
|
+
proc.stdout?.on('data', (data) => { stdout += data.toString(); });
|
|
194
|
+
proc.stderr?.on('data', (data) => { stderr += data.toString(); });
|
|
171
195
|
|
|
172
196
|
proc.on('close', (code) => {
|
|
173
197
|
if (code === 0) {
|
|
174
|
-
|
|
198
|
+
respond(200, { success: true, output: stdout, message: `MCP server "${name}" removed successfully` });
|
|
175
199
|
} else {
|
|
176
|
-
|
|
200
|
+
respond(400, { error: 'Codex CLI command failed', details: stderr || `Exited with code ${code}` });
|
|
177
201
|
}
|
|
178
202
|
});
|
|
179
203
|
|
|
180
204
|
proc.on('error', (error) => {
|
|
181
|
-
|
|
205
|
+
const isMissing = error?.code === 'ENOENT';
|
|
206
|
+
respond(isMissing ? 503 : 500, {
|
|
207
|
+
error: isMissing ? 'Codex CLI not installed' : 'Failed to run Codex CLI',
|
|
208
|
+
details: error.message,
|
|
209
|
+
code: error.code
|
|
210
|
+
});
|
|
182
211
|
});
|
|
183
212
|
} catch (error) {
|
|
184
213
|
res.status(500).json({ error: 'Failed to remove MCP server', details: error.message });
|
|
@@ -189,24 +218,30 @@ router.get('/mcp/cli/get/:name', async (req, res) => {
|
|
|
189
218
|
try {
|
|
190
219
|
const { name } = req.params;
|
|
191
220
|
|
|
221
|
+
const respond = createCliResponder(res);
|
|
192
222
|
const proc = spawn('codex', ['mcp', 'get', name], { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
193
223
|
|
|
194
224
|
let stdout = '';
|
|
195
225
|
let stderr = '';
|
|
196
226
|
|
|
197
|
-
proc.stdout
|
|
198
|
-
proc.stderr
|
|
227
|
+
proc.stdout?.on('data', (data) => { stdout += data.toString(); });
|
|
228
|
+
proc.stderr?.on('data', (data) => { stderr += data.toString(); });
|
|
199
229
|
|
|
200
230
|
proc.on('close', (code) => {
|
|
201
231
|
if (code === 0) {
|
|
202
|
-
|
|
232
|
+
respond(200, { success: true, output: stdout, server: parseCodexGetOutput(stdout) });
|
|
203
233
|
} else {
|
|
204
|
-
|
|
234
|
+
respond(404, { error: 'Codex CLI command failed', details: stderr || `Exited with code ${code}` });
|
|
205
235
|
}
|
|
206
236
|
});
|
|
207
237
|
|
|
208
238
|
proc.on('error', (error) => {
|
|
209
|
-
|
|
239
|
+
const isMissing = error?.code === 'ENOENT';
|
|
240
|
+
respond(isMissing ? 503 : 500, {
|
|
241
|
+
error: isMissing ? 'Codex CLI not installed' : 'Failed to run Codex CLI',
|
|
242
|
+
details: error.message,
|
|
243
|
+
code: error.code
|
|
244
|
+
});
|
|
210
245
|
});
|
|
211
246
|
} catch (error) {
|
|
212
247
|
res.status(500).json({ error: 'Failed to get MCP server details', details: error.message });
|
|
@@ -227,8 +262,7 @@ router.get('/mcp/config/read', async (req, res) => {
|
|
|
227
262
|
}
|
|
228
263
|
|
|
229
264
|
if (!configData) {
|
|
230
|
-
return res.json({ success:
|
|
231
|
-
}
|
|
265
|
+
return res.json({ success: true, configPath, servers: [] }); }
|
|
232
266
|
|
|
233
267
|
const servers = [];
|
|
234
268
|
|