@siteboon/claude-code-ui 1.13.5 → 1.14.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.
@@ -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
- for (const entry of entries) {
396
- if (entry.isDirectory()) {
397
- existingProjects.add(entry.name);
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
- projects.push(project);
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 an empty project
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
- // Remove the project directory
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
1098
  const projectName = absolutePath.replace(/\//g, '-');
1020
-
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
- if (sessionData && sessionData.cwd === projectPath) {
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 only the first 5 sessions for performance
1230
- return sessions.slice(0, 5);
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.text) {
1277
- lastUserMessage = entry.payload.text;
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
 
@@ -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.on('data', (data) => { stdout += data.toString(); });
97
- proc.stderr.on('data', (data) => { stderr += data.toString(); });
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
- res.json({ success: true, output: stdout, servers: parseCodexListOutput(stdout) });
113
+ respond(200, { success: true, output: stdout, servers: parseCodexListOutput(stdout) });
102
114
  } else {
103
- res.status(500).json({ error: 'Codex CLI command failed', details: stderr });
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
- res.status(500).json({ error: 'Failed to run Codex CLI', details: error.message });
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.on('data', (data) => { stdout += data.toString(); });
142
- proc.stderr.on('data', (data) => { stderr += data.toString(); });
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
- res.json({ success: true, output: stdout, message: `MCP server "${name}" added successfully` });
164
+ respond(200, { success: true, output: stdout, message: `MCP server "${name}" added successfully` });
147
165
  } else {
148
- res.status(400).json({ error: 'Codex CLI command failed', details: stderr });
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
- res.status(500).json({ error: 'Failed to run Codex CLI', details: error.message });
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.on('data', (data) => { stdout += data.toString(); });
170
- proc.stderr.on('data', (data) => { stderr += data.toString(); });
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
- res.json({ success: true, output: stdout, message: `MCP server "${name}" removed successfully` });
198
+ respond(200, { success: true, output: stdout, message: `MCP server "${name}" removed successfully` });
175
199
  } else {
176
- res.status(400).json({ error: 'Codex CLI command failed', details: stderr });
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
- res.status(500).json({ error: 'Failed to run Codex CLI', details: error.message });
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.on('data', (data) => { stdout += data.toString(); });
198
- proc.stderr.on('data', (data) => { stderr += data.toString(); });
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
- res.json({ success: true, output: stdout, server: parseCodexGetOutput(stdout) });
232
+ respond(200, { success: true, output: stdout, server: parseCodexGetOutput(stdout) });
203
233
  } else {
204
- res.status(404).json({ error: 'Codex CLI command failed', details: stderr });
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
- res.status(500).json({ error: 'Failed to run Codex CLI', details: error.message });
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: false, message: 'No Codex configuration file found', servers: [] });
231
- }
265
+ return res.json({ success: true, configPath, servers: [] }); }
232
266
 
233
267
  const servers = [];
234
268