mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-05-16 17:16:19 +00:00
refactor(backend): move remaining routes declared in index.js to thier own module
This commit is contained in:
226
server/src/modules/sessions/sessions.inline.routes.js
Normal file
226
server/src/modules/sessions/sessions.inline.routes.js
Normal file
@@ -0,0 +1,226 @@
|
||||
import express from 'express';
|
||||
import path from 'path';
|
||||
import os from 'os';
|
||||
import { promises as fsPromises } from 'fs';
|
||||
import { sessionNamesDb } from '../../../database/db.js';
|
||||
import { extractProjectDirectory } from '../../../projects.js';
|
||||
import { authenticateToken } from '../auth/auth.middleware.js';
|
||||
|
||||
const router = express.Router();
|
||||
const VALID_PROVIDERS = ['claude', 'codex', 'cursor', 'gemini'];
|
||||
|
||||
// Rename session endpoint
|
||||
router.put('/api/sessions/:sessionId/rename', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const { sessionId } = req.params;
|
||||
const safeSessionId = String(sessionId).replace(/[^a-zA-Z0-9._-]/g, '');
|
||||
if (!safeSessionId || safeSessionId !== String(sessionId)) {
|
||||
return res.status(400).json({ error: 'Invalid sessionId' });
|
||||
}
|
||||
const { summary, provider } = req.body;
|
||||
if (!summary || typeof summary !== 'string' || summary.trim() === '') {
|
||||
return res.status(400).json({ error: 'Summary is required' });
|
||||
}
|
||||
if (summary.trim().length > 500) {
|
||||
return res.status(400).json({ error: 'Summary must not exceed 500 characters' });
|
||||
}
|
||||
if (!provider || !VALID_PROVIDERS.includes(provider)) {
|
||||
return res.status(400).json({ error: `Provider must be one of: ${VALID_PROVIDERS.join(', ')}` });
|
||||
}
|
||||
sessionNamesDb.setName(safeSessionId, provider, summary.trim());
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error(`[API] Error renaming session ${req.params.sessionId}:`, error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Get token usage for a specific session
|
||||
router.get('/api/projects/:projectName/sessions/:sessionId/token-usage', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const { projectName, sessionId } = req.params;
|
||||
const { provider = 'claude' } = req.query;
|
||||
const homeDir = os.homedir();
|
||||
|
||||
// Allow only safe characters in sessionId
|
||||
const safeSessionId = String(sessionId).replace(/[^a-zA-Z0-9._-]/g, '');
|
||||
if (!safeSessionId || safeSessionId !== String(sessionId)) {
|
||||
return res.status(400).json({ error: 'Invalid sessionId' });
|
||||
}
|
||||
|
||||
// Handle Cursor sessions - they use SQLite and don't have token usage info
|
||||
if (provider === 'cursor') {
|
||||
return res.json({
|
||||
used: 0,
|
||||
total: 0,
|
||||
breakdown: { input: 0, cacheCreation: 0, cacheRead: 0 },
|
||||
unsupported: true,
|
||||
message: 'Token usage tracking not available for Cursor sessions'
|
||||
});
|
||||
}
|
||||
|
||||
// Handle Gemini sessions - they are raw logs in our current setup
|
||||
if (provider === 'gemini') {
|
||||
return res.json({
|
||||
used: 0,
|
||||
total: 0,
|
||||
breakdown: { input: 0, cacheCreation: 0, cacheRead: 0 },
|
||||
unsupported: true,
|
||||
message: 'Token usage tracking not available for Gemini sessions'
|
||||
});
|
||||
}
|
||||
|
||||
// Handle Codex sessions
|
||||
if (provider === 'codex') {
|
||||
const codexSessionsDir = path.join(homeDir, '.codex', 'sessions');
|
||||
|
||||
// Find the session file by searching for the session ID
|
||||
const findSessionFile = async (dir) => {
|
||||
try {
|
||||
const entries = await fsPromises.readdir(dir, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
const found = await findSessionFile(fullPath);
|
||||
if (found) return found;
|
||||
} else if (entry.name.includes(safeSessionId) && entry.name.endsWith('.jsonl')) {
|
||||
return fullPath;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Skip directories we can't read
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const sessionFilePath = await findSessionFile(codexSessionsDir);
|
||||
|
||||
if (!sessionFilePath) {
|
||||
return res.status(404).json({ error: 'Codex session file not found', sessionId: safeSessionId });
|
||||
}
|
||||
|
||||
// Read and parse the Codex JSONL file
|
||||
let fileContent;
|
||||
try {
|
||||
fileContent = await fsPromises.readFile(sessionFilePath, 'utf8');
|
||||
} catch (error) {
|
||||
if (error.code === 'ENOENT') {
|
||||
return res.status(404).json({ error: 'Session file not found', path: sessionFilePath });
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
const lines = fileContent.trim().split('\n');
|
||||
let totalTokens = 0;
|
||||
let contextWindow = 200000; // Default for Codex/OpenAI
|
||||
|
||||
// Find the latest token_count event with info (scan from end)
|
||||
for (let i = lines.length - 1; i >= 0; i--) {
|
||||
try {
|
||||
const entry = JSON.parse(lines[i]);
|
||||
|
||||
// Codex stores token info in event_msg with type: "token_count"
|
||||
if (entry.type === 'event_msg' && entry.payload?.type === 'token_count' && entry.payload?.info) {
|
||||
const tokenInfo = entry.payload.info;
|
||||
if (tokenInfo.total_token_usage) {
|
||||
totalTokens = tokenInfo.total_token_usage.total_tokens || 0;
|
||||
}
|
||||
if (tokenInfo.model_context_window) {
|
||||
contextWindow = tokenInfo.model_context_window;
|
||||
}
|
||||
break; // Stop after finding the latest token count
|
||||
}
|
||||
} catch (parseError) {
|
||||
// Skip lines that can't be parsed
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return res.json({
|
||||
used: totalTokens,
|
||||
total: contextWindow
|
||||
});
|
||||
}
|
||||
|
||||
// Handle Claude sessions (default)
|
||||
// Extract actual project path
|
||||
let projectPath;
|
||||
try {
|
||||
projectPath = await extractProjectDirectory(projectName);
|
||||
} catch (error) {
|
||||
console.error('Error extracting project directory:', error);
|
||||
return res.status(500).json({ error: 'Failed to determine project path' });
|
||||
}
|
||||
|
||||
// Construct the JSONL file path
|
||||
// Claude stores session files in ~/.claude/projects/[encoded-project-path]/[session-id].jsonl
|
||||
// The encoding replaces any non-alphanumeric character (except -) with -
|
||||
const encodedPath = projectPath.replace(/[^a-zA-Z0-9-]/g, '-');
|
||||
const projectDir = path.join(homeDir, '.claude', 'projects', encodedPath);
|
||||
|
||||
const jsonlPath = path.join(projectDir, `${safeSessionId}.jsonl`);
|
||||
|
||||
// Constrain to projectDir
|
||||
const rel = path.relative(path.resolve(projectDir), path.resolve(jsonlPath));
|
||||
if (rel.startsWith('..') || path.isAbsolute(rel)) {
|
||||
return res.status(400).json({ error: 'Invalid path' });
|
||||
}
|
||||
|
||||
// Read and parse the JSONL file
|
||||
let fileContent;
|
||||
try {
|
||||
fileContent = await fsPromises.readFile(jsonlPath, 'utf8');
|
||||
} catch (error) {
|
||||
if (error.code === 'ENOENT') {
|
||||
return res.status(404).json({ error: 'Session file not found', path: jsonlPath });
|
||||
}
|
||||
throw error; // Re-throw other errors to be caught by outer try-catch
|
||||
}
|
||||
const lines = fileContent.trim().split('\n');
|
||||
|
||||
const parsedContextWindow = parseInt(process.env.CONTEXT_WINDOW, 10);
|
||||
const contextWindow = Number.isFinite(parsedContextWindow) ? parsedContextWindow : 160000;
|
||||
let inputTokens = 0;
|
||||
let cacheCreationTokens = 0;
|
||||
let cacheReadTokens = 0;
|
||||
|
||||
// Find the latest assistant message with usage data (scan from end)
|
||||
for (let i = lines.length - 1; i >= 0; i--) {
|
||||
try {
|
||||
const entry = JSON.parse(lines[i]);
|
||||
|
||||
// Only count assistant messages which have usage data
|
||||
if (entry.type === 'assistant' && entry.message?.usage) {
|
||||
const usage = entry.message.usage;
|
||||
|
||||
// Use token counts from latest assistant message only
|
||||
inputTokens = usage.input_tokens || 0;
|
||||
cacheCreationTokens = usage.cache_creation_input_tokens || 0;
|
||||
cacheReadTokens = usage.cache_read_input_tokens || 0;
|
||||
|
||||
break; // Stop after finding the latest assistant message
|
||||
}
|
||||
} catch (parseError) {
|
||||
// Skip lines that can't be parsed
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate total context usage (excluding output_tokens, as per ccusage)
|
||||
const totalUsed = inputTokens + cacheCreationTokens + cacheReadTokens;
|
||||
|
||||
res.json({
|
||||
used: totalUsed,
|
||||
total: contextWindow,
|
||||
breakdown: {
|
||||
input: inputTokens,
|
||||
cacheCreation: cacheCreationTokens,
|
||||
cacheRead: cacheReadTokens
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error reading session token usage:', error);
|
||||
res.status(500).json({ error: 'Failed to read session token usage' });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
Reference in New Issue
Block a user