@siteboon/claude-code-ui 1.12.0 → 1.13.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 +19 -16
- package/dist/assets/index-Cc6pl7ji.css +32 -0
- package/dist/assets/index-Zq2roSUR.js +1206 -0
- package/dist/assets/{vendor-xterm-jI4BCHEb.js → vendor-xterm-DfaPXD3y.js} +12 -12
- package/dist/icons/codex-white.svg +3 -0
- package/dist/icons/codex.svg +3 -0
- package/dist/icons/cursor-white.svg +12 -0
- package/dist/index.html +4 -4
- package/dist/logo-128.png +0 -0
- package/dist/logo-256.png +0 -0
- package/dist/logo-32.png +0 -0
- package/dist/logo-512.png +0 -0
- package/dist/logo-64.png +0 -0
- package/dist/logo.svg +17 -9
- package/package.json +4 -1
- package/server/claude-sdk.js +20 -19
- package/server/database/auth.db +0 -0
- package/server/database/db.js +64 -0
- package/server/database/init.sql +4 -1
- package/server/index.js +236 -18
- package/server/openai-codex.js +387 -0
- package/server/projects.js +448 -7
- package/server/routes/agent.js +42 -4
- package/server/routes/cli-auth.js +263 -0
- package/server/routes/codex.js +310 -0
- package/server/routes/git.js +123 -28
- package/server/routes/taskmaster.js +2 -10
- package/server/routes/user.js +106 -0
- package/server/utils/gitConfig.js +24 -0
- package/dist/assets/index-DXtzL-q9.css +0 -32
- package/dist/assets/index-Do2w3FiK.js +0 -1189
package/server/index.js
CHANGED
|
@@ -60,6 +60,7 @@ import mime from 'mime-types';
|
|
|
60
60
|
import { getProjects, getSessions, getSessionMessages, renameProject, deleteSession, deleteProject, addProjectManually, extractProjectDirectory, clearProjectDirectoryCache } from './projects.js';
|
|
61
61
|
import { queryClaudeSDK, abortClaudeSDKSession, isClaudeSDKSessionActive, getActiveClaudeSDKSessions } from './claude-sdk.js';
|
|
62
62
|
import { spawnCursor, abortCursorSession, isCursorSessionActive, getActiveCursorSessions } from './cursor-cli.js';
|
|
63
|
+
import { queryCodex, abortCodexSession, isCodexSessionActive, getActiveCodexSessions } from './openai-codex.js';
|
|
63
64
|
import gitRoutes from './routes/git.js';
|
|
64
65
|
import authRoutes from './routes/auth.js';
|
|
65
66
|
import mcpRoutes from './routes/mcp.js';
|
|
@@ -70,6 +71,9 @@ import commandsRoutes from './routes/commands.js';
|
|
|
70
71
|
import settingsRoutes from './routes/settings.js';
|
|
71
72
|
import agentRoutes from './routes/agent.js';
|
|
72
73
|
import projectsRoutes from './routes/projects.js';
|
|
74
|
+
import cliAuthRoutes from './routes/cli-auth.js';
|
|
75
|
+
import userRoutes from './routes/user.js';
|
|
76
|
+
import codexRoutes from './routes/codex.js';
|
|
73
77
|
import { initializeDatabase } from './database/db.js';
|
|
74
78
|
import { validateApiKey, authenticateToken, authenticateWebSocket } from './middleware/auth.js';
|
|
75
79
|
|
|
@@ -164,6 +168,9 @@ async function setupProjectsWatcher() {
|
|
|
164
168
|
const app = express();
|
|
165
169
|
const server = http.createServer(app);
|
|
166
170
|
|
|
171
|
+
const ptySessionsMap = new Map();
|
|
172
|
+
const PTY_SESSION_TIMEOUT = 30 * 60 * 1000;
|
|
173
|
+
|
|
167
174
|
// Single WebSocket server that handles both paths
|
|
168
175
|
const wss = new WebSocketServer({
|
|
169
176
|
server,
|
|
@@ -206,7 +213,17 @@ const wss = new WebSocketServer({
|
|
|
206
213
|
app.locals.wss = wss;
|
|
207
214
|
|
|
208
215
|
app.use(cors());
|
|
209
|
-
app.use(express.json({
|
|
216
|
+
app.use(express.json({
|
|
217
|
+
limit: '50mb',
|
|
218
|
+
type: (req) => {
|
|
219
|
+
// Skip multipart/form-data requests (for file uploads like images)
|
|
220
|
+
const contentType = req.headers['content-type'] || '';
|
|
221
|
+
if (contentType.includes('multipart/form-data')) {
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
224
|
+
return contentType.includes('json');
|
|
225
|
+
}
|
|
226
|
+
}));
|
|
210
227
|
app.use(express.urlencoded({ limit: '50mb', extended: true }));
|
|
211
228
|
|
|
212
229
|
// Public health check endpoint (no authentication required)
|
|
@@ -247,6 +264,15 @@ app.use('/api/commands', authenticateToken, commandsRoutes);
|
|
|
247
264
|
// Settings API Routes (protected)
|
|
248
265
|
app.use('/api/settings', authenticateToken, settingsRoutes);
|
|
249
266
|
|
|
267
|
+
// CLI Authentication API Routes (protected)
|
|
268
|
+
app.use('/api/cli', authenticateToken, cliAuthRoutes);
|
|
269
|
+
|
|
270
|
+
// User API Routes (protected)
|
|
271
|
+
app.use('/api/user', authenticateToken, userRoutes);
|
|
272
|
+
|
|
273
|
+
// Codex API Routes (protected)
|
|
274
|
+
app.use('/api/codex', authenticateToken, codexRoutes);
|
|
275
|
+
|
|
250
276
|
// Agent API Routes (uses API key authentication)
|
|
251
277
|
app.use('/api/agent', agentRoutes);
|
|
252
278
|
|
|
@@ -397,9 +423,12 @@ app.put('/api/projects/:projectName/rename', authenticateToken, async (req, res)
|
|
|
397
423
|
app.delete('/api/projects/:projectName/sessions/:sessionId', authenticateToken, async (req, res) => {
|
|
398
424
|
try {
|
|
399
425
|
const { projectName, sessionId } = req.params;
|
|
426
|
+
console.log(`[API] Deleting session: ${sessionId} from project: ${projectName}`);
|
|
400
427
|
await deleteSession(projectName, sessionId);
|
|
428
|
+
console.log(`[API] Session ${sessionId} deleted successfully`);
|
|
401
429
|
res.json({ success: true });
|
|
402
430
|
} catch (error) {
|
|
431
|
+
console.error(`[API] Error deleting session ${req.params.sessionId}:`, error);
|
|
403
432
|
res.status(500).json({ error: error.message });
|
|
404
433
|
}
|
|
405
434
|
});
|
|
@@ -712,6 +741,12 @@ function handleChatConnection(ws) {
|
|
|
712
741
|
console.log('🔄 Session:', data.options?.sessionId ? 'Resume' : 'New');
|
|
713
742
|
console.log('🤖 Model:', data.options?.model || 'default');
|
|
714
743
|
await spawnCursor(data.command, data.options, ws);
|
|
744
|
+
} else if (data.type === 'codex-command') {
|
|
745
|
+
console.log('[DEBUG] Codex message:', data.command || '[Continue/Resume]');
|
|
746
|
+
console.log('📁 Project:', data.options?.projectPath || data.options?.cwd || 'Unknown');
|
|
747
|
+
console.log('🔄 Session:', data.options?.sessionId ? 'Resume' : 'New');
|
|
748
|
+
console.log('🤖 Model:', data.options?.model || 'default');
|
|
749
|
+
await queryCodex(data.command, data.options, ws);
|
|
715
750
|
} else if (data.type === 'cursor-resume') {
|
|
716
751
|
// Backward compatibility: treat as cursor-command with resume and no prompt
|
|
717
752
|
console.log('[DEBUG] Cursor resume session (compat):', data.sessionId);
|
|
@@ -727,6 +762,8 @@ function handleChatConnection(ws) {
|
|
|
727
762
|
|
|
728
763
|
if (provider === 'cursor') {
|
|
729
764
|
success = abortCursorSession(data.sessionId);
|
|
765
|
+
} else if (provider === 'codex') {
|
|
766
|
+
success = abortCodexSession(data.sessionId);
|
|
730
767
|
} else {
|
|
731
768
|
// Use Claude Agents SDK
|
|
732
769
|
success = await abortClaudeSDKSession(data.sessionId);
|
|
@@ -755,6 +792,8 @@ function handleChatConnection(ws) {
|
|
|
755
792
|
|
|
756
793
|
if (provider === 'cursor') {
|
|
757
794
|
isActive = isCursorSessionActive(sessionId);
|
|
795
|
+
} else if (provider === 'codex') {
|
|
796
|
+
isActive = isCodexSessionActive(sessionId);
|
|
758
797
|
} else {
|
|
759
798
|
// Use Claude Agents SDK
|
|
760
799
|
isActive = isClaudeSDKSessionActive(sessionId);
|
|
@@ -770,7 +809,8 @@ function handleChatConnection(ws) {
|
|
|
770
809
|
// Get all currently active sessions
|
|
771
810
|
const activeSessions = {
|
|
772
811
|
claude: getActiveClaudeSDKSessions(),
|
|
773
|
-
cursor: getActiveCursorSessions()
|
|
812
|
+
cursor: getActiveCursorSessions(),
|
|
813
|
+
codex: getActiveCodexSessions()
|
|
774
814
|
};
|
|
775
815
|
ws.send(JSON.stringify({
|
|
776
816
|
type: 'active-sessions',
|
|
@@ -797,6 +837,8 @@ function handleChatConnection(ws) {
|
|
|
797
837
|
function handleShellConnection(ws) {
|
|
798
838
|
console.log('🐚 Shell client connected');
|
|
799
839
|
let shellProcess = null;
|
|
840
|
+
let ptySessionKey = null;
|
|
841
|
+
let outputBuffer = [];
|
|
800
842
|
|
|
801
843
|
ws.on('message', async (message) => {
|
|
802
844
|
try {
|
|
@@ -804,7 +846,6 @@ function handleShellConnection(ws) {
|
|
|
804
846
|
console.log('📨 Shell message received:', data.type);
|
|
805
847
|
|
|
806
848
|
if (data.type === 'init') {
|
|
807
|
-
// Initialize shell with project path and session info
|
|
808
849
|
const projectPath = data.projectPath || process.cwd();
|
|
809
850
|
const sessionId = data.sessionId;
|
|
810
851
|
const hasSession = data.hasSession;
|
|
@@ -812,6 +853,57 @@ function handleShellConnection(ws) {
|
|
|
812
853
|
const initialCommand = data.initialCommand;
|
|
813
854
|
const isPlainShell = data.isPlainShell || (!!initialCommand && !hasSession) || provider === 'plain-shell';
|
|
814
855
|
|
|
856
|
+
// Login commands (Claude/Cursor auth) should never reuse cached sessions
|
|
857
|
+
const isLoginCommand = initialCommand && (
|
|
858
|
+
initialCommand.includes('setup-token') ||
|
|
859
|
+
initialCommand.includes('cursor-agent login') ||
|
|
860
|
+
initialCommand.includes('auth login')
|
|
861
|
+
);
|
|
862
|
+
|
|
863
|
+
// Include command hash in session key so different commands get separate sessions
|
|
864
|
+
const commandSuffix = isPlainShell && initialCommand
|
|
865
|
+
? `_cmd_${Buffer.from(initialCommand).toString('base64').slice(0, 16)}`
|
|
866
|
+
: '';
|
|
867
|
+
ptySessionKey = `${projectPath}_${sessionId || 'default'}${commandSuffix}`;
|
|
868
|
+
|
|
869
|
+
// Kill any existing login session before starting fresh
|
|
870
|
+
if (isLoginCommand) {
|
|
871
|
+
const oldSession = ptySessionsMap.get(ptySessionKey);
|
|
872
|
+
if (oldSession) {
|
|
873
|
+
console.log('🧹 Cleaning up existing login session:', ptySessionKey);
|
|
874
|
+
if (oldSession.timeoutId) clearTimeout(oldSession.timeoutId);
|
|
875
|
+
if (oldSession.pty && oldSession.pty.kill) oldSession.pty.kill();
|
|
876
|
+
ptySessionsMap.delete(ptySessionKey);
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
const existingSession = isLoginCommand ? null : ptySessionsMap.get(ptySessionKey);
|
|
881
|
+
if (existingSession) {
|
|
882
|
+
console.log('♻️ Reconnecting to existing PTY session:', ptySessionKey);
|
|
883
|
+
shellProcess = existingSession.pty;
|
|
884
|
+
|
|
885
|
+
clearTimeout(existingSession.timeoutId);
|
|
886
|
+
|
|
887
|
+
ws.send(JSON.stringify({
|
|
888
|
+
type: 'output',
|
|
889
|
+
data: `\x1b[36m[Reconnected to existing session]\x1b[0m\r\n`
|
|
890
|
+
}));
|
|
891
|
+
|
|
892
|
+
if (existingSession.buffer && existingSession.buffer.length > 0) {
|
|
893
|
+
console.log(`📜 Sending ${existingSession.buffer.length} buffered messages`);
|
|
894
|
+
existingSession.buffer.forEach(bufferedData => {
|
|
895
|
+
ws.send(JSON.stringify({
|
|
896
|
+
type: 'output',
|
|
897
|
+
data: bufferedData
|
|
898
|
+
}));
|
|
899
|
+
});
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
existingSession.ws = ws;
|
|
903
|
+
|
|
904
|
+
return;
|
|
905
|
+
}
|
|
906
|
+
|
|
815
907
|
console.log('[INFO] Starting shell in:', projectPath);
|
|
816
908
|
console.log('📋 Session info:', hasSession ? `Resume session ${sessionId}` : (isPlainShell ? 'Plain shell mode' : 'New session'));
|
|
817
909
|
console.log('🤖 Provider:', isPlainShell ? 'plain-shell' : provider);
|
|
@@ -885,10 +977,15 @@ function handleShellConnection(ws) {
|
|
|
885
977
|
const shell = os.platform() === 'win32' ? 'powershell.exe' : 'bash';
|
|
886
978
|
const shellArgs = os.platform() === 'win32' ? ['-Command', shellCommand] : ['-c', shellCommand];
|
|
887
979
|
|
|
980
|
+
// Use terminal dimensions from client if provided, otherwise use defaults
|
|
981
|
+
const termCols = data.cols || 80;
|
|
982
|
+
const termRows = data.rows || 24;
|
|
983
|
+
console.log('📐 Using terminal dimensions:', termCols, 'x', termRows);
|
|
984
|
+
|
|
888
985
|
shellProcess = pty.spawn(shell, shellArgs, {
|
|
889
986
|
name: 'xterm-256color',
|
|
890
|
-
cols:
|
|
891
|
-
rows:
|
|
987
|
+
cols: termCols,
|
|
988
|
+
rows: termRows,
|
|
892
989
|
cwd: process.env.HOME || (os.platform() === 'win32' ? process.env.USERPROFILE : '/'),
|
|
893
990
|
env: {
|
|
894
991
|
...process.env,
|
|
@@ -902,9 +999,28 @@ function handleShellConnection(ws) {
|
|
|
902
999
|
|
|
903
1000
|
console.log('🟢 Shell process started with PTY, PID:', shellProcess.pid);
|
|
904
1001
|
|
|
1002
|
+
ptySessionsMap.set(ptySessionKey, {
|
|
1003
|
+
pty: shellProcess,
|
|
1004
|
+
ws: ws,
|
|
1005
|
+
buffer: [],
|
|
1006
|
+
timeoutId: null,
|
|
1007
|
+
projectPath,
|
|
1008
|
+
sessionId
|
|
1009
|
+
});
|
|
1010
|
+
|
|
905
1011
|
// Handle data output
|
|
906
1012
|
shellProcess.onData((data) => {
|
|
907
|
-
|
|
1013
|
+
const session = ptySessionsMap.get(ptySessionKey);
|
|
1014
|
+
if (!session) return;
|
|
1015
|
+
|
|
1016
|
+
if (session.buffer.length < 5000) {
|
|
1017
|
+
session.buffer.push(data);
|
|
1018
|
+
} else {
|
|
1019
|
+
session.buffer.shift();
|
|
1020
|
+
session.buffer.push(data);
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
if (session.ws && session.ws.readyState === WebSocket.OPEN) {
|
|
908
1024
|
let outputData = data;
|
|
909
1025
|
|
|
910
1026
|
// Check for various URL opening patterns
|
|
@@ -928,7 +1044,7 @@ function handleShellConnection(ws) {
|
|
|
928
1044
|
console.log('[DEBUG] Detected URL for opening:', url);
|
|
929
1045
|
|
|
930
1046
|
// Send URL opening message to client
|
|
931
|
-
ws.send(JSON.stringify({
|
|
1047
|
+
session.ws.send(JSON.stringify({
|
|
932
1048
|
type: 'url_open',
|
|
933
1049
|
url: url
|
|
934
1050
|
}));
|
|
@@ -941,7 +1057,7 @@ function handleShellConnection(ws) {
|
|
|
941
1057
|
});
|
|
942
1058
|
|
|
943
1059
|
// Send regular output
|
|
944
|
-
ws.send(JSON.stringify({
|
|
1060
|
+
session.ws.send(JSON.stringify({
|
|
945
1061
|
type: 'output',
|
|
946
1062
|
data: outputData
|
|
947
1063
|
}));
|
|
@@ -951,12 +1067,17 @@ function handleShellConnection(ws) {
|
|
|
951
1067
|
// Handle process exit
|
|
952
1068
|
shellProcess.onExit((exitCode) => {
|
|
953
1069
|
console.log('🔚 Shell process exited with code:', exitCode.exitCode, 'signal:', exitCode.signal);
|
|
954
|
-
|
|
955
|
-
|
|
1070
|
+
const session = ptySessionsMap.get(ptySessionKey);
|
|
1071
|
+
if (session && session.ws && session.ws.readyState === WebSocket.OPEN) {
|
|
1072
|
+
session.ws.send(JSON.stringify({
|
|
956
1073
|
type: 'output',
|
|
957
1074
|
data: `\r\n\x1b[33mProcess exited with code ${exitCode.exitCode}${exitCode.signal ? ` (${exitCode.signal})` : ''}\x1b[0m\r\n`
|
|
958
1075
|
}));
|
|
959
1076
|
}
|
|
1077
|
+
if (session && session.timeoutId) {
|
|
1078
|
+
clearTimeout(session.timeoutId);
|
|
1079
|
+
}
|
|
1080
|
+
ptySessionsMap.delete(ptySessionKey);
|
|
960
1081
|
shellProcess = null;
|
|
961
1082
|
});
|
|
962
1083
|
|
|
@@ -999,9 +1120,21 @@ function handleShellConnection(ws) {
|
|
|
999
1120
|
|
|
1000
1121
|
ws.on('close', () => {
|
|
1001
1122
|
console.log('🔌 Shell client disconnected');
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1123
|
+
|
|
1124
|
+
if (ptySessionKey) {
|
|
1125
|
+
const session = ptySessionsMap.get(ptySessionKey);
|
|
1126
|
+
if (session) {
|
|
1127
|
+
console.log('⏳ PTY session kept alive, will timeout in 30 minutes:', ptySessionKey);
|
|
1128
|
+
session.ws = null;
|
|
1129
|
+
|
|
1130
|
+
session.timeoutId = setTimeout(() => {
|
|
1131
|
+
console.log('⏰ PTY session timeout, killing process:', ptySessionKey);
|
|
1132
|
+
if (session.pty && session.pty.kill) {
|
|
1133
|
+
session.pty.kill();
|
|
1134
|
+
}
|
|
1135
|
+
ptySessionsMap.delete(ptySessionKey);
|
|
1136
|
+
}, PTY_SESSION_TIMEOUT);
|
|
1137
|
+
}
|
|
1005
1138
|
}
|
|
1006
1139
|
});
|
|
1007
1140
|
|
|
@@ -1247,8 +1380,98 @@ app.post('/api/projects/:projectName/upload-images', authenticateToken, async (r
|
|
|
1247
1380
|
app.get('/api/projects/:projectName/sessions/:sessionId/token-usage', authenticateToken, async (req, res) => {
|
|
1248
1381
|
try {
|
|
1249
1382
|
const { projectName, sessionId } = req.params;
|
|
1383
|
+
const { provider = 'claude' } = req.query;
|
|
1250
1384
|
const homeDir = os.homedir();
|
|
1251
1385
|
|
|
1386
|
+
// Allow only safe characters in sessionId
|
|
1387
|
+
const safeSessionId = String(sessionId).replace(/[^a-zA-Z0-9._-]/g, '');
|
|
1388
|
+
if (!safeSessionId) {
|
|
1389
|
+
return res.status(400).json({ error: 'Invalid sessionId' });
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
// Handle Cursor sessions - they use SQLite and don't have token usage info
|
|
1393
|
+
if (provider === 'cursor') {
|
|
1394
|
+
return res.json({
|
|
1395
|
+
used: 0,
|
|
1396
|
+
total: 0,
|
|
1397
|
+
breakdown: { input: 0, cacheCreation: 0, cacheRead: 0 },
|
|
1398
|
+
unsupported: true,
|
|
1399
|
+
message: 'Token usage tracking not available for Cursor sessions'
|
|
1400
|
+
});
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
// Handle Codex sessions
|
|
1404
|
+
if (provider === 'codex') {
|
|
1405
|
+
const codexSessionsDir = path.join(homeDir, '.codex', 'sessions');
|
|
1406
|
+
|
|
1407
|
+
// Find the session file by searching for the session ID
|
|
1408
|
+
const findSessionFile = async (dir) => {
|
|
1409
|
+
try {
|
|
1410
|
+
const entries = await fsPromises.readdir(dir, { withFileTypes: true });
|
|
1411
|
+
for (const entry of entries) {
|
|
1412
|
+
const fullPath = path.join(dir, entry.name);
|
|
1413
|
+
if (entry.isDirectory()) {
|
|
1414
|
+
const found = await findSessionFile(fullPath);
|
|
1415
|
+
if (found) return found;
|
|
1416
|
+
} else if (entry.name.includes(safeSessionId) && entry.name.endsWith('.jsonl')) {
|
|
1417
|
+
return fullPath;
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
} catch (error) {
|
|
1421
|
+
// Skip directories we can't read
|
|
1422
|
+
}
|
|
1423
|
+
return null;
|
|
1424
|
+
};
|
|
1425
|
+
|
|
1426
|
+
const sessionFilePath = await findSessionFile(codexSessionsDir);
|
|
1427
|
+
|
|
1428
|
+
if (!sessionFilePath) {
|
|
1429
|
+
return res.status(404).json({ error: 'Codex session file not found', sessionId: safeSessionId });
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
// Read and parse the Codex JSONL file
|
|
1433
|
+
let fileContent;
|
|
1434
|
+
try {
|
|
1435
|
+
fileContent = await fsPromises.readFile(sessionFilePath, 'utf8');
|
|
1436
|
+
} catch (error) {
|
|
1437
|
+
if (error.code === 'ENOENT') {
|
|
1438
|
+
return res.status(404).json({ error: 'Session file not found', path: sessionFilePath });
|
|
1439
|
+
}
|
|
1440
|
+
throw error;
|
|
1441
|
+
}
|
|
1442
|
+
const lines = fileContent.trim().split('\n');
|
|
1443
|
+
let totalTokens = 0;
|
|
1444
|
+
let contextWindow = 200000; // Default for Codex/OpenAI
|
|
1445
|
+
|
|
1446
|
+
// Find the latest token_count event with info (scan from end)
|
|
1447
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
1448
|
+
try {
|
|
1449
|
+
const entry = JSON.parse(lines[i]);
|
|
1450
|
+
|
|
1451
|
+
// Codex stores token info in event_msg with type: "token_count"
|
|
1452
|
+
if (entry.type === 'event_msg' && entry.payload?.type === 'token_count' && entry.payload?.info) {
|
|
1453
|
+
const tokenInfo = entry.payload.info;
|
|
1454
|
+
if (tokenInfo.total_token_usage) {
|
|
1455
|
+
totalTokens = tokenInfo.total_token_usage.total_tokens || 0;
|
|
1456
|
+
}
|
|
1457
|
+
if (tokenInfo.model_context_window) {
|
|
1458
|
+
contextWindow = tokenInfo.model_context_window;
|
|
1459
|
+
}
|
|
1460
|
+
break; // Stop after finding the latest token count
|
|
1461
|
+
}
|
|
1462
|
+
} catch (parseError) {
|
|
1463
|
+
// Skip lines that can't be parsed
|
|
1464
|
+
continue;
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
return res.json({
|
|
1469
|
+
used: totalTokens,
|
|
1470
|
+
total: contextWindow
|
|
1471
|
+
});
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
// Handle Claude sessions (default)
|
|
1252
1475
|
// Extract actual project path
|
|
1253
1476
|
let projectPath;
|
|
1254
1477
|
try {
|
|
@@ -1264,11 +1487,6 @@ app.get('/api/projects/:projectName/sessions/:sessionId/token-usage', authentica
|
|
|
1264
1487
|
const encodedPath = projectPath.replace(/[\\/:\s~_]/g, '-');
|
|
1265
1488
|
const projectDir = path.join(homeDir, '.claude', 'projects', encodedPath);
|
|
1266
1489
|
|
|
1267
|
-
// Allow only safe characters in sessionId
|
|
1268
|
-
const safeSessionId = String(sessionId).replace(/[^a-zA-Z0-9._-]/g, '');
|
|
1269
|
-
if (!safeSessionId) {
|
|
1270
|
-
return res.status(400).json({ error: 'Invalid sessionId' });
|
|
1271
|
-
}
|
|
1272
1490
|
const jsonlPath = path.join(projectDir, `${safeSessionId}.jsonl`);
|
|
1273
1491
|
|
|
1274
1492
|
// Constrain to projectDir
|