@siteboon/claude-code-ui 1.16.4 → 1.17.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 +1 -1
- package/README.zh-CN.md +1 -1
- package/dist/assets/index-1FHLTpt_.js +1239 -0
- package/dist/assets/index-BZ3x0u4p.css +32 -0
- package/dist/assets/{vendor-codemirror-CJLzwpLB.js → vendor-codemirror-BXil-2fV.js} +1 -1
- package/dist/assets/{vendor-react-DcyRfQm3.js → vendor-react-DIN4KjD2.js} +1 -1
- package/dist/index.html +4 -4
- package/package.json +1 -1
- package/server/index.js +107 -75
- package/server/openai-codex.js +25 -12
- package/server/projects.js +117 -63
- package/server/routes/commands.js +80 -0
- package/server/routes/git.js +57 -17
- package/dist/assets/index-Cep8Annb.js +0 -1239
- package/dist/assets/index-DQad8ylc.css +0 -32
package/server/index.js
CHANGED
|
@@ -63,8 +63,24 @@ import { initializeDatabase } from './database/db.js';
|
|
|
63
63
|
import { validateApiKey, authenticateToken, authenticateWebSocket } from './middleware/auth.js';
|
|
64
64
|
import { IS_PLATFORM } from './constants/config.js';
|
|
65
65
|
|
|
66
|
-
// File system
|
|
67
|
-
|
|
66
|
+
// File system watchers for provider project/session folders
|
|
67
|
+
const PROVIDER_WATCH_PATHS = [
|
|
68
|
+
{ provider: 'claude', rootPath: path.join(os.homedir(), '.claude', 'projects') },
|
|
69
|
+
{ provider: 'cursor', rootPath: path.join(os.homedir(), '.cursor', 'chats') },
|
|
70
|
+
{ provider: 'codex', rootPath: path.join(os.homedir(), '.codex', 'sessions') }
|
|
71
|
+
];
|
|
72
|
+
const WATCHER_IGNORED_PATTERNS = [
|
|
73
|
+
'**/node_modules/**',
|
|
74
|
+
'**/.git/**',
|
|
75
|
+
'**/dist/**',
|
|
76
|
+
'**/build/**',
|
|
77
|
+
'**/*.tmp',
|
|
78
|
+
'**/*.swp',
|
|
79
|
+
'**/.DS_Store'
|
|
80
|
+
];
|
|
81
|
+
const WATCHER_DEBOUNCE_MS = 300;
|
|
82
|
+
let projectsWatchers = [];
|
|
83
|
+
let projectsWatcherDebounceTimer = null;
|
|
68
84
|
const connectedClients = new Set();
|
|
69
85
|
let isGetProjectsRunning = false; // Flag to prevent reentrant calls
|
|
70
86
|
|
|
@@ -81,94 +97,110 @@ function broadcastProgress(progress) {
|
|
|
81
97
|
});
|
|
82
98
|
}
|
|
83
99
|
|
|
84
|
-
// Setup file system
|
|
100
|
+
// Setup file system watchers for Claude, Cursor, and Codex project/session folders
|
|
85
101
|
async function setupProjectsWatcher() {
|
|
86
102
|
const chokidar = (await import('chokidar')).default;
|
|
87
|
-
const claudeProjectsPath = path.join(os.homedir(), '.claude', 'projects');
|
|
88
103
|
|
|
89
|
-
if (
|
|
90
|
-
|
|
104
|
+
if (projectsWatcherDebounceTimer) {
|
|
105
|
+
clearTimeout(projectsWatcherDebounceTimer);
|
|
106
|
+
projectsWatcherDebounceTimer = null;
|
|
91
107
|
}
|
|
92
108
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
'
|
|
99
|
-
'**/dist/**',
|
|
100
|
-
'**/build/**',
|
|
101
|
-
'**/*.tmp',
|
|
102
|
-
'**/*.swp',
|
|
103
|
-
'**/.DS_Store'
|
|
104
|
-
],
|
|
105
|
-
persistent: true,
|
|
106
|
-
ignoreInitial: true, // Don't fire events for existing files on startup
|
|
107
|
-
followSymlinks: false,
|
|
108
|
-
depth: 10, // Reasonable depth limit
|
|
109
|
-
awaitWriteFinish: {
|
|
110
|
-
stabilityThreshold: 100, // Wait 100ms for file to stabilize
|
|
111
|
-
pollInterval: 50
|
|
109
|
+
await Promise.all(
|
|
110
|
+
projectsWatchers.map(async (watcher) => {
|
|
111
|
+
try {
|
|
112
|
+
await watcher.close();
|
|
113
|
+
} catch (error) {
|
|
114
|
+
console.error('[WARN] Failed to close watcher:', error);
|
|
112
115
|
}
|
|
113
|
-
})
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
let debounceTimer;
|
|
117
|
-
const debouncedUpdate = async (eventType, filePath) => {
|
|
118
|
-
clearTimeout(debounceTimer);
|
|
119
|
-
debounceTimer = setTimeout(async () => {
|
|
120
|
-
// Prevent reentrant calls
|
|
121
|
-
if (isGetProjectsRunning) {
|
|
122
|
-
return;
|
|
123
|
-
}
|
|
116
|
+
})
|
|
117
|
+
);
|
|
118
|
+
projectsWatchers = [];
|
|
124
119
|
|
|
125
|
-
|
|
126
|
-
|
|
120
|
+
const debouncedUpdate = (eventType, filePath, provider, rootPath) => {
|
|
121
|
+
if (projectsWatcherDebounceTimer) {
|
|
122
|
+
clearTimeout(projectsWatcherDebounceTimer);
|
|
123
|
+
}
|
|
127
124
|
|
|
128
|
-
|
|
129
|
-
|
|
125
|
+
projectsWatcherDebounceTimer = setTimeout(async () => {
|
|
126
|
+
// Prevent reentrant calls
|
|
127
|
+
if (isGetProjectsRunning) {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
130
|
|
|
131
|
-
|
|
132
|
-
|
|
131
|
+
try {
|
|
132
|
+
isGetProjectsRunning = true;
|
|
133
|
+
|
|
134
|
+
// Clear project directory cache when files change
|
|
135
|
+
clearProjectDirectoryCache();
|
|
136
|
+
|
|
137
|
+
// Get updated projects list
|
|
138
|
+
const updatedProjects = await getProjects(broadcastProgress);
|
|
139
|
+
|
|
140
|
+
// Notify all connected clients about the project changes
|
|
141
|
+
const updateMessage = JSON.stringify({
|
|
142
|
+
type: 'projects_updated',
|
|
143
|
+
projects: updatedProjects,
|
|
144
|
+
timestamp: new Date().toISOString(),
|
|
145
|
+
changeType: eventType,
|
|
146
|
+
changedFile: path.relative(rootPath, filePath),
|
|
147
|
+
watchProvider: provider
|
|
148
|
+
});
|
|
133
149
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
changeType: eventType,
|
|
140
|
-
changedFile: path.relative(claudeProjectsPath, filePath)
|
|
141
|
-
});
|
|
150
|
+
connectedClients.forEach(client => {
|
|
151
|
+
if (client.readyState === WebSocket.OPEN) {
|
|
152
|
+
client.send(updateMessage);
|
|
153
|
+
}
|
|
154
|
+
});
|
|
142
155
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
156
|
+
} catch (error) {
|
|
157
|
+
console.error('[ERROR] Error handling project changes:', error);
|
|
158
|
+
} finally {
|
|
159
|
+
isGetProjectsRunning = false;
|
|
160
|
+
}
|
|
161
|
+
}, WATCHER_DEBOUNCE_MS);
|
|
162
|
+
};
|
|
148
163
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
164
|
+
for (const { provider, rootPath } of PROVIDER_WATCH_PATHS) {
|
|
165
|
+
try {
|
|
166
|
+
// chokidar v4 emits ENOENT via the "error" event for missing roots and will not auto-recover.
|
|
167
|
+
// Ensure provider folders exist before creating the watcher so watching stays active.
|
|
168
|
+
await fsPromises.mkdir(rootPath, { recursive: true });
|
|
169
|
+
|
|
170
|
+
// Initialize chokidar watcher with optimized settings
|
|
171
|
+
const watcher = chokidar.watch(rootPath, {
|
|
172
|
+
ignored: WATCHER_IGNORED_PATTERNS,
|
|
173
|
+
persistent: true,
|
|
174
|
+
ignoreInitial: true, // Don't fire events for existing files on startup
|
|
175
|
+
followSymlinks: false,
|
|
176
|
+
depth: 10, // Reasonable depth limit
|
|
177
|
+
awaitWriteFinish: {
|
|
178
|
+
stabilityThreshold: 100, // Wait 100ms for file to stabilize
|
|
179
|
+
pollInterval: 50
|
|
153
180
|
}
|
|
154
|
-
}, 300); // 300ms debounce (slightly faster than before)
|
|
155
|
-
};
|
|
156
|
-
|
|
157
|
-
// Set up event listeners
|
|
158
|
-
projectsWatcher
|
|
159
|
-
.on('add', (filePath) => debouncedUpdate('add', filePath))
|
|
160
|
-
.on('change', (filePath) => debouncedUpdate('change', filePath))
|
|
161
|
-
.on('unlink', (filePath) => debouncedUpdate('unlink', filePath))
|
|
162
|
-
.on('addDir', (dirPath) => debouncedUpdate('addDir', dirPath))
|
|
163
|
-
.on('unlinkDir', (dirPath) => debouncedUpdate('unlinkDir', dirPath))
|
|
164
|
-
.on('error', (error) => {
|
|
165
|
-
console.error('[ERROR] Chokidar watcher error:', error);
|
|
166
|
-
})
|
|
167
|
-
.on('ready', () => {
|
|
168
181
|
});
|
|
169
182
|
|
|
170
|
-
|
|
171
|
-
|
|
183
|
+
// Set up event listeners
|
|
184
|
+
watcher
|
|
185
|
+
.on('add', (filePath) => debouncedUpdate('add', filePath, provider, rootPath))
|
|
186
|
+
.on('change', (filePath) => debouncedUpdate('change', filePath, provider, rootPath))
|
|
187
|
+
.on('unlink', (filePath) => debouncedUpdate('unlink', filePath, provider, rootPath))
|
|
188
|
+
.on('addDir', (dirPath) => debouncedUpdate('addDir', dirPath, provider, rootPath))
|
|
189
|
+
.on('unlinkDir', (dirPath) => debouncedUpdate('unlinkDir', dirPath, provider, rootPath))
|
|
190
|
+
.on('error', (error) => {
|
|
191
|
+
console.error(`[ERROR] ${provider} watcher error:`, error);
|
|
192
|
+
})
|
|
193
|
+
.on('ready', () => {
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
projectsWatchers.push(watcher);
|
|
197
|
+
} catch (error) {
|
|
198
|
+
console.error(`[ERROR] Failed to setup ${provider} watcher for ${rootPath}:`, error);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (projectsWatchers.length === 0) {
|
|
203
|
+
console.error('[ERROR] Failed to setup any provider watchers');
|
|
172
204
|
}
|
|
173
205
|
}
|
|
174
206
|
|
package/server/openai-codex.js
CHANGED
|
@@ -203,6 +203,7 @@ export async function queryCodex(command, options = {}, ws) {
|
|
|
203
203
|
let codex;
|
|
204
204
|
let thread;
|
|
205
205
|
let currentSessionId = sessionId;
|
|
206
|
+
const abortController = new AbortController();
|
|
206
207
|
|
|
207
208
|
try {
|
|
208
209
|
// Initialize Codex SDK
|
|
@@ -232,6 +233,7 @@ export async function queryCodex(command, options = {}, ws) {
|
|
|
232
233
|
thread,
|
|
233
234
|
codex,
|
|
234
235
|
status: 'running',
|
|
236
|
+
abortController,
|
|
235
237
|
startedAt: new Date().toISOString()
|
|
236
238
|
});
|
|
237
239
|
|
|
@@ -243,7 +245,9 @@ export async function queryCodex(command, options = {}, ws) {
|
|
|
243
245
|
});
|
|
244
246
|
|
|
245
247
|
// Execute with streaming
|
|
246
|
-
const streamedTurn = await thread.runStreamed(command
|
|
248
|
+
const streamedTurn = await thread.runStreamed(command, {
|
|
249
|
+
signal: abortController.signal
|
|
250
|
+
});
|
|
247
251
|
|
|
248
252
|
for await (const event of streamedTurn.events) {
|
|
249
253
|
// Check if session was aborted
|
|
@@ -286,20 +290,27 @@ export async function queryCodex(command, options = {}, ws) {
|
|
|
286
290
|
});
|
|
287
291
|
|
|
288
292
|
} catch (error) {
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
error
|
|
294
|
-
|
|
295
|
-
|
|
293
|
+
const session = currentSessionId ? activeCodexSessions.get(currentSessionId) : null;
|
|
294
|
+
const wasAborted =
|
|
295
|
+
session?.status === 'aborted' ||
|
|
296
|
+
error?.name === 'AbortError' ||
|
|
297
|
+
String(error?.message || '').toLowerCase().includes('aborted');
|
|
298
|
+
|
|
299
|
+
if (!wasAborted) {
|
|
300
|
+
console.error('[Codex] Error:', error);
|
|
301
|
+
sendMessage(ws, {
|
|
302
|
+
type: 'codex-error',
|
|
303
|
+
error: error.message,
|
|
304
|
+
sessionId: currentSessionId
|
|
305
|
+
});
|
|
306
|
+
}
|
|
296
307
|
|
|
297
308
|
} finally {
|
|
298
309
|
// Update session status
|
|
299
310
|
if (currentSessionId) {
|
|
300
311
|
const session = activeCodexSessions.get(currentSessionId);
|
|
301
312
|
if (session) {
|
|
302
|
-
session.status = 'completed';
|
|
313
|
+
session.status = session.status === 'aborted' ? 'aborted' : 'completed';
|
|
303
314
|
}
|
|
304
315
|
}
|
|
305
316
|
}
|
|
@@ -318,9 +329,11 @@ export function abortCodexSession(sessionId) {
|
|
|
318
329
|
}
|
|
319
330
|
|
|
320
331
|
session.status = 'aborted';
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
332
|
+
try {
|
|
333
|
+
session.abortController?.abort();
|
|
334
|
+
} catch (error) {
|
|
335
|
+
console.warn(`[Codex] Failed to abort session ${sessionId}:`, error);
|
|
336
|
+
}
|
|
324
337
|
|
|
325
338
|
return true;
|
|
326
339
|
}
|
package/server/projects.js
CHANGED
|
@@ -384,6 +384,7 @@ async function getProjects(progressCallback = null) {
|
|
|
384
384
|
const config = await loadProjectConfig();
|
|
385
385
|
const projects = [];
|
|
386
386
|
const existingProjects = new Set();
|
|
387
|
+
const codexSessionsIndexRef = { sessionsByProject: null };
|
|
387
388
|
let totalProjects = 0;
|
|
388
389
|
let processedProjects = 0;
|
|
389
390
|
let directories = [];
|
|
@@ -419,8 +420,6 @@ async function getProjects(progressCallback = null) {
|
|
|
419
420
|
});
|
|
420
421
|
}
|
|
421
422
|
|
|
422
|
-
const projectPath = path.join(claudeDir, entry.name);
|
|
423
|
-
|
|
424
423
|
// Extract actual project directory from JSONL sessions
|
|
425
424
|
const actualProjectDir = await extractProjectDirectory(entry.name);
|
|
426
425
|
|
|
@@ -435,7 +434,11 @@ async function getProjects(progressCallback = null) {
|
|
|
435
434
|
displayName: customName || autoDisplayName,
|
|
436
435
|
fullPath: fullPath,
|
|
437
436
|
isCustomName: !!customName,
|
|
438
|
-
sessions: []
|
|
437
|
+
sessions: [],
|
|
438
|
+
sessionMeta: {
|
|
439
|
+
hasMore: false,
|
|
440
|
+
total: 0
|
|
441
|
+
}
|
|
439
442
|
};
|
|
440
443
|
|
|
441
444
|
// Try to get sessions for this project (just first 5 for performance)
|
|
@@ -448,6 +451,10 @@ async function getProjects(progressCallback = null) {
|
|
|
448
451
|
};
|
|
449
452
|
} catch (e) {
|
|
450
453
|
console.warn(`Could not load sessions for project ${entry.name}:`, e.message);
|
|
454
|
+
project.sessionMeta = {
|
|
455
|
+
hasMore: false,
|
|
456
|
+
total: 0
|
|
457
|
+
};
|
|
451
458
|
}
|
|
452
459
|
|
|
453
460
|
// Also fetch Cursor sessions for this project
|
|
@@ -460,7 +467,9 @@ async function getProjects(progressCallback = null) {
|
|
|
460
467
|
|
|
461
468
|
// Also fetch Codex sessions for this project
|
|
462
469
|
try {
|
|
463
|
-
project.codexSessions = await getCodexSessions(actualProjectDir
|
|
470
|
+
project.codexSessions = await getCodexSessions(actualProjectDir, {
|
|
471
|
+
indexRef: codexSessionsIndexRef,
|
|
472
|
+
});
|
|
464
473
|
} catch (e) {
|
|
465
474
|
console.warn(`Could not load Codex sessions for project ${entry.name}:`, e.message);
|
|
466
475
|
project.codexSessions = [];
|
|
@@ -525,7 +534,7 @@ async function getProjects(progressCallback = null) {
|
|
|
525
534
|
}
|
|
526
535
|
}
|
|
527
536
|
|
|
528
|
-
|
|
537
|
+
const project = {
|
|
529
538
|
name: projectName,
|
|
530
539
|
path: actualProjectDir,
|
|
531
540
|
displayName: projectConfig.displayName || await generateDisplayName(projectName, actualProjectDir),
|
|
@@ -533,9 +542,13 @@ async function getProjects(progressCallback = null) {
|
|
|
533
542
|
isCustomName: !!projectConfig.displayName,
|
|
534
543
|
isManuallyAdded: true,
|
|
535
544
|
sessions: [],
|
|
545
|
+
sessionMeta: {
|
|
546
|
+
hasMore: false,
|
|
547
|
+
total: 0
|
|
548
|
+
},
|
|
536
549
|
cursorSessions: [],
|
|
537
550
|
codexSessions: []
|
|
538
|
-
|
|
551
|
+
};
|
|
539
552
|
|
|
540
553
|
// Try to fetch Cursor sessions for manual projects too
|
|
541
554
|
try {
|
|
@@ -546,7 +559,9 @@ async function getProjects(progressCallback = null) {
|
|
|
546
559
|
|
|
547
560
|
// Try to fetch Codex sessions for manual projects too
|
|
548
561
|
try {
|
|
549
|
-
project.codexSessions = await getCodexSessions(actualProjectDir
|
|
562
|
+
project.codexSessions = await getCodexSessions(actualProjectDir, {
|
|
563
|
+
indexRef: codexSessionsIndexRef,
|
|
564
|
+
});
|
|
550
565
|
} catch (e) {
|
|
551
566
|
console.warn(`Could not load Codex sessions for manual project ${projectName}:`, e.message);
|
|
552
567
|
}
|
|
@@ -1244,75 +1259,114 @@ async function getCursorSessions(projectPath) {
|
|
|
1244
1259
|
}
|
|
1245
1260
|
|
|
1246
1261
|
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1262
|
+
function normalizeComparablePath(inputPath) {
|
|
1263
|
+
if (!inputPath || typeof inputPath !== 'string') {
|
|
1264
|
+
return '';
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
const withoutLongPathPrefix = inputPath.startsWith('\\\\?\\')
|
|
1268
|
+
? inputPath.slice(4)
|
|
1269
|
+
: inputPath;
|
|
1270
|
+
const normalized = path.normalize(withoutLongPathPrefix.trim());
|
|
1271
|
+
|
|
1272
|
+
if (!normalized) {
|
|
1273
|
+
return '';
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
const resolved = path.resolve(normalized);
|
|
1277
|
+
return process.platform === 'win32' ? resolved.toLowerCase() : resolved;
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
async function findCodexJsonlFiles(dir) {
|
|
1281
|
+
const files = [];
|
|
1282
|
+
|
|
1250
1283
|
try {
|
|
1251
|
-
const
|
|
1252
|
-
const
|
|
1284
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
1285
|
+
for (const entry of entries) {
|
|
1286
|
+
const fullPath = path.join(dir, entry.name);
|
|
1287
|
+
if (entry.isDirectory()) {
|
|
1288
|
+
files.push(...await findCodexJsonlFiles(fullPath));
|
|
1289
|
+
} else if (entry.name.endsWith('.jsonl')) {
|
|
1290
|
+
files.push(fullPath);
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
} catch (error) {
|
|
1294
|
+
// Skip directories we can't read
|
|
1295
|
+
}
|
|
1253
1296
|
|
|
1254
|
-
|
|
1297
|
+
return files;
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
async function buildCodexSessionsIndex() {
|
|
1301
|
+
const codexSessionsDir = path.join(os.homedir(), '.codex', 'sessions');
|
|
1302
|
+
const sessionsByProject = new Map();
|
|
1303
|
+
|
|
1304
|
+
try {
|
|
1305
|
+
await fs.access(codexSessionsDir);
|
|
1306
|
+
} catch (error) {
|
|
1307
|
+
return sessionsByProject;
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
const jsonlFiles = await findCodexJsonlFiles(codexSessionsDir);
|
|
1311
|
+
|
|
1312
|
+
for (const filePath of jsonlFiles) {
|
|
1255
1313
|
try {
|
|
1256
|
-
await
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
}
|
|
1314
|
+
const sessionData = await parseCodexSessionFile(filePath);
|
|
1315
|
+
if (!sessionData || !sessionData.id) {
|
|
1316
|
+
continue;
|
|
1317
|
+
}
|
|
1261
1318
|
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
try {
|
|
1266
|
-
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
1267
|
-
for (const entry of entries) {
|
|
1268
|
-
const fullPath = path.join(dir, entry.name);
|
|
1269
|
-
if (entry.isDirectory()) {
|
|
1270
|
-
files.push(...await findJsonlFiles(fullPath));
|
|
1271
|
-
} else if (entry.name.endsWith('.jsonl')) {
|
|
1272
|
-
files.push(fullPath);
|
|
1273
|
-
}
|
|
1274
|
-
}
|
|
1275
|
-
} catch (error) {
|
|
1276
|
-
// Skip directories we can't read
|
|
1319
|
+
const normalizedProjectPath = normalizeComparablePath(sessionData.cwd);
|
|
1320
|
+
if (!normalizedProjectPath) {
|
|
1321
|
+
continue;
|
|
1277
1322
|
}
|
|
1278
|
-
return files;
|
|
1279
|
-
};
|
|
1280
1323
|
|
|
1281
|
-
|
|
1324
|
+
const session = {
|
|
1325
|
+
id: sessionData.id,
|
|
1326
|
+
summary: sessionData.summary || 'Codex Session',
|
|
1327
|
+
messageCount: sessionData.messageCount || 0,
|
|
1328
|
+
lastActivity: sessionData.timestamp ? new Date(sessionData.timestamp) : new Date(),
|
|
1329
|
+
cwd: sessionData.cwd,
|
|
1330
|
+
model: sessionData.model,
|
|
1331
|
+
filePath,
|
|
1332
|
+
provider: 'codex',
|
|
1333
|
+
};
|
|
1282
1334
|
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
try {
|
|
1286
|
-
const sessionData = await parseCodexSessionFile(filePath);
|
|
1287
|
-
|
|
1288
|
-
// Check if this session matches the project path
|
|
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) === '')) {
|
|
1295
|
-
sessions.push({
|
|
1296
|
-
id: sessionData.id,
|
|
1297
|
-
summary: sessionData.summary || 'Codex Session',
|
|
1298
|
-
messageCount: sessionData.messageCount || 0,
|
|
1299
|
-
lastActivity: sessionData.timestamp ? new Date(sessionData.timestamp) : new Date(),
|
|
1300
|
-
cwd: sessionData.cwd,
|
|
1301
|
-
model: sessionData.model,
|
|
1302
|
-
filePath: filePath,
|
|
1303
|
-
provider: 'codex'
|
|
1304
|
-
});
|
|
1305
|
-
}
|
|
1306
|
-
} catch (error) {
|
|
1307
|
-
console.warn(`Could not parse Codex session file ${filePath}:`, error.message);
|
|
1335
|
+
if (!sessionsByProject.has(normalizedProjectPath)) {
|
|
1336
|
+
sessionsByProject.set(normalizedProjectPath, []);
|
|
1308
1337
|
}
|
|
1338
|
+
|
|
1339
|
+
sessionsByProject.get(normalizedProjectPath).push(session);
|
|
1340
|
+
} catch (error) {
|
|
1341
|
+
console.warn(`Could not parse Codex session file ${filePath}:`, error.message);
|
|
1309
1342
|
}
|
|
1343
|
+
}
|
|
1310
1344
|
|
|
1311
|
-
|
|
1345
|
+
for (const sessions of sessionsByProject.values()) {
|
|
1312
1346
|
sessions.sort((a, b) => new Date(b.lastActivity) - new Date(a.lastActivity));
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
return sessionsByProject;
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
// Fetch Codex sessions for a given project path
|
|
1353
|
+
async function getCodexSessions(projectPath, options = {}) {
|
|
1354
|
+
const { limit = 5, indexRef = null } = options;
|
|
1355
|
+
try {
|
|
1356
|
+
const normalizedProjectPath = normalizeComparablePath(projectPath);
|
|
1357
|
+
if (!normalizedProjectPath) {
|
|
1358
|
+
return [];
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
if (indexRef && !indexRef.sessionsByProject) {
|
|
1362
|
+
indexRef.sessionsByProject = await buildCodexSessionsIndex();
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
const sessionsByProject = indexRef?.sessionsByProject || await buildCodexSessionsIndex();
|
|
1366
|
+
const sessions = sessionsByProject.get(normalizedProjectPath) || [];
|
|
1313
1367
|
|
|
1314
1368
|
// Return limited sessions for performance (0 = unlimited for deletion)
|
|
1315
|
-
return limit > 0 ? sessions.slice(0, limit) : sessions;
|
|
1369
|
+
return limit > 0 ? sessions.slice(0, limit) : [...sessions];
|
|
1316
1370
|
|
|
1317
1371
|
} catch (error) {
|
|
1318
1372
|
console.error('Error fetching Codex sessions:', error);
|
|
@@ -209,6 +209,86 @@ Custom commands can be created in:
|
|
|
209
209
|
};
|
|
210
210
|
},
|
|
211
211
|
|
|
212
|
+
'/cost': async (args, context) => {
|
|
213
|
+
const tokenUsage = context?.tokenUsage || {};
|
|
214
|
+
const provider = context?.provider || 'claude';
|
|
215
|
+
const model =
|
|
216
|
+
context?.model ||
|
|
217
|
+
(provider === 'cursor'
|
|
218
|
+
? CURSOR_MODELS.DEFAULT
|
|
219
|
+
: provider === 'codex'
|
|
220
|
+
? CODEX_MODELS.DEFAULT
|
|
221
|
+
: CLAUDE_MODELS.DEFAULT);
|
|
222
|
+
|
|
223
|
+
const used = Number(tokenUsage.used ?? tokenUsage.totalUsed ?? tokenUsage.total_tokens ?? 0) || 0;
|
|
224
|
+
const total =
|
|
225
|
+
Number(
|
|
226
|
+
tokenUsage.total ??
|
|
227
|
+
tokenUsage.contextWindow ??
|
|
228
|
+
parseInt(process.env.CONTEXT_WINDOW || '160000', 10),
|
|
229
|
+
) || 160000;
|
|
230
|
+
const percentage = total > 0 ? Number(((used / total) * 100).toFixed(1)) : 0;
|
|
231
|
+
|
|
232
|
+
const inputTokensRaw =
|
|
233
|
+
Number(
|
|
234
|
+
tokenUsage.inputTokens ??
|
|
235
|
+
tokenUsage.input ??
|
|
236
|
+
tokenUsage.cumulativeInputTokens ??
|
|
237
|
+
tokenUsage.promptTokens ??
|
|
238
|
+
0,
|
|
239
|
+
) || 0;
|
|
240
|
+
const outputTokens =
|
|
241
|
+
Number(
|
|
242
|
+
tokenUsage.outputTokens ??
|
|
243
|
+
tokenUsage.output ??
|
|
244
|
+
tokenUsage.cumulativeOutputTokens ??
|
|
245
|
+
tokenUsage.completionTokens ??
|
|
246
|
+
0,
|
|
247
|
+
) || 0;
|
|
248
|
+
const cacheTokens =
|
|
249
|
+
Number(
|
|
250
|
+
tokenUsage.cacheReadTokens ??
|
|
251
|
+
tokenUsage.cacheCreationTokens ??
|
|
252
|
+
tokenUsage.cacheTokens ??
|
|
253
|
+
tokenUsage.cachedTokens ??
|
|
254
|
+
0,
|
|
255
|
+
) || 0;
|
|
256
|
+
|
|
257
|
+
// If we only have total used tokens, treat them as input for display/estimation.
|
|
258
|
+
const inputTokens =
|
|
259
|
+
inputTokensRaw > 0 || outputTokens > 0 || cacheTokens > 0 ? inputTokensRaw + cacheTokens : used;
|
|
260
|
+
|
|
261
|
+
// Rough default rates by provider (USD / 1M tokens).
|
|
262
|
+
const pricingByProvider = {
|
|
263
|
+
claude: { input: 3, output: 15 },
|
|
264
|
+
cursor: { input: 3, output: 15 },
|
|
265
|
+
codex: { input: 1.5, output: 6 },
|
|
266
|
+
};
|
|
267
|
+
const rates = pricingByProvider[provider] || pricingByProvider.claude;
|
|
268
|
+
|
|
269
|
+
const inputCost = (inputTokens / 1_000_000) * rates.input;
|
|
270
|
+
const outputCost = (outputTokens / 1_000_000) * rates.output;
|
|
271
|
+
const totalCost = inputCost + outputCost;
|
|
272
|
+
|
|
273
|
+
return {
|
|
274
|
+
type: 'builtin',
|
|
275
|
+
action: 'cost',
|
|
276
|
+
data: {
|
|
277
|
+
tokenUsage: {
|
|
278
|
+
used,
|
|
279
|
+
total,
|
|
280
|
+
percentage,
|
|
281
|
+
},
|
|
282
|
+
cost: {
|
|
283
|
+
input: inputCost.toFixed(4),
|
|
284
|
+
output: outputCost.toFixed(4),
|
|
285
|
+
total: totalCost.toFixed(4),
|
|
286
|
+
},
|
|
287
|
+
model,
|
|
288
|
+
},
|
|
289
|
+
};
|
|
290
|
+
},
|
|
291
|
+
|
|
212
292
|
'/status': async (args, context) => {
|
|
213
293
|
// Read version from package.json
|
|
214
294
|
const packageJsonPath = path.join(path.dirname(__dirname), '..', 'package.json');
|