mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-04-30 09:21:33 +00:00
refactor: remove dead code
This commit is contained in:
@@ -19,7 +19,6 @@ import { getConnectableHost } from '../shared/networkHosts.js';
|
||||
|
||||
import { findAppRoot, getModuleDir } from './utils/runtime-paths.js';
|
||||
import {
|
||||
deleteSessionById,
|
||||
getProjectPathById,
|
||||
} from './projects.js';
|
||||
import {
|
||||
@@ -66,12 +65,11 @@ import settingsRoutes from './routes/settings.js';
|
||||
import agentRoutes from './routes/agent.js';
|
||||
import projectModuleRoutes from './modules/projects/projects.routes.js';
|
||||
import userRoutes from './routes/user.js';
|
||||
import codexRoutes from './routes/codex.js';
|
||||
import geminiRoutes from './routes/gemini.js';
|
||||
import pluginsRoutes from './routes/plugins.js';
|
||||
import providerRoutes from './modules/providers/provider.routes.js';
|
||||
import { startEnabledPluginServers, stopAllPlugins, getPluginPort } from './utils/plugin-process-manager.js';
|
||||
import { initializeDatabase, sessionsDb } from './modules/database/index.js';
|
||||
import { initializeDatabase } from './modules/database/index.js';
|
||||
import { configureWebPush } from './services/vapid-keys.js';
|
||||
import { validateApiKey, authenticateToken, authenticateWebSocket } from './middleware/auth.js';
|
||||
import { IS_PLATFORM } from './constants/config.js';
|
||||
@@ -181,9 +179,6 @@ app.use('/api/settings', authenticateToken, settingsRoutes);
|
||||
// User API Routes (protected)
|
||||
app.use('/api/user', authenticateToken, userRoutes);
|
||||
|
||||
// Codex API Routes (protected)
|
||||
app.use('/api/codex', authenticateToken, codexRoutes);
|
||||
|
||||
// Gemini API Routes (protected)
|
||||
app.use('/api/gemini', authenticateToken, geminiRoutes);
|
||||
|
||||
@@ -293,44 +288,6 @@ app.post('/api/system/update', authenticateToken, async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Delete session endpoint; resolves `projectId` to path before touching disk.
|
||||
app.delete('/api/projects/:projectId/sessions/:sessionId', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const { projectId, sessionId } = req.params;
|
||||
console.log(`[API] Deleting session: ${sessionId} from project: ${projectId}`);
|
||||
await deleteSessionById(projectId, sessionId);
|
||||
sessionsDb.deleteSessionById(sessionId);
|
||||
console.log(`[API] Session ${sessionId} deleted successfully`);
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error(`[API] Error deleting session ${req.params.sessionId}:`, error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Rename session endpoint
|
||||
app.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 } = 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' });
|
||||
}
|
||||
sessionsDb.updateSessionCustomName(safeSessionId, 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 });
|
||||
}
|
||||
});
|
||||
|
||||
const expandWorkspacePath = (inputPath) => {
|
||||
if (!inputPath) return inputPath;
|
||||
if (inputPath === '~') {
|
||||
|
||||
@@ -1,40 +1,18 @@
|
||||
/**
|
||||
* PROJECT DISCOVERY AND MANAGEMENT
|
||||
* ================================
|
||||
* PROJECT PATH RESOLUTION
|
||||
* =======================
|
||||
*
|
||||
* After the projectName → projectId migration, project and session listings
|
||||
* for `GET /api/projects` are sourced entirely from the database:
|
||||
*
|
||||
* - `projects` table (via `projectsDb`) — the canonical list of projects and
|
||||
* their absolute `project_path`.
|
||||
* - `sessions` table (via `sessionsDb`) — every provider's sessions for a
|
||||
* given project, keyed by `project_path`.
|
||||
*
|
||||
* Routes always address a project by its DB `projectId` and resolve the real
|
||||
* directory through `getProjectPathById` before touching disk.
|
||||
*
|
||||
* The filesystem-aware helpers kept in this module serve the remaining
|
||||
* features that still need on-disk data:
|
||||
* - Session message reads for each provider (Claude/Codex/Gemini) for
|
||||
* `GET /api/providers/sessions/:sessionId/messages`.
|
||||
* - (Project row removal / JSONL cleanup is handled in
|
||||
* `modules/projects/services/project-delete.service.ts`.)
|
||||
* Routes address projects by DB `projectId` and resolve their absolute
|
||||
* workspace path through this module.
|
||||
*/
|
||||
|
||||
import fsSync, { promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
import readline from 'readline';
|
||||
import os from 'os';
|
||||
|
||||
import { projectsDb } from './modules/database/index.js';
|
||||
|
||||
/**
|
||||
* Resolve the absolute project path for a database `projectId`.
|
||||
*
|
||||
* After the projectName → projectId migration, every API route receives a
|
||||
* `projectId` (the primary key from the `projects` table) and must translate
|
||||
* it into the real directory on disk through this helper. Returns `null` when
|
||||
* the id doesn't match any row so callers can respond with a 404.
|
||||
* Returns `null` when the id doesn't match any row so callers can respond
|
||||
* with a 404.
|
||||
*/
|
||||
async function getProjectPathById(projectId) {
|
||||
if (!projectId) {
|
||||
@@ -44,619 +22,6 @@ async function getProjectPathById(projectId) {
|
||||
return projectsDb.getProjectPathById(projectId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the Claude CLI project folder name for an absolute path.
|
||||
*
|
||||
* Claude stores its JSONL history per project under
|
||||
* `~/.claude/projects/<encoded-path>/`. The folder name is derived from the
|
||||
* absolute path by replacing every non-alphanumeric character (except `-`) with
|
||||
* `-`. Filesystem helpers like `getSessions`/`deleteSession` still work on that
|
||||
* folder name, so routes that receive a `projectId` compute it from the path
|
||||
* resolved through the DB instead of keeping the encoded name as an identifier.
|
||||
*/
|
||||
function claudeFolderNameFromPath(projectPath) {
|
||||
if (!projectPath) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return projectPath.replace(/[^a-zA-Z0-9-]/g, '-');
|
||||
}
|
||||
|
||||
async function getSessions(projectName, limit = 5, offset = 0) {
|
||||
const projectDir = path.join(os.homedir(), '.claude', 'projects', projectName);
|
||||
|
||||
try {
|
||||
const files = await fs.readdir(projectDir);
|
||||
// agent-*.jsonl files contain session start data at this point. This needs to be revisited
|
||||
// periodically to make sure only accurate data is there and no new functionality is added there
|
||||
const jsonlFiles = files.filter(file => file.endsWith('.jsonl') && !file.startsWith('agent-'));
|
||||
|
||||
if (jsonlFiles.length === 0) {
|
||||
return { sessions: [], hasMore: false, total: 0 };
|
||||
}
|
||||
|
||||
// Sort files by modification time (newest first)
|
||||
const filesWithStats = await Promise.all(
|
||||
jsonlFiles.map(async (file) => {
|
||||
const filePath = path.join(projectDir, file);
|
||||
const stats = await fs.stat(filePath);
|
||||
return { file, mtime: stats.mtime };
|
||||
})
|
||||
);
|
||||
filesWithStats.sort((a, b) => b.mtime - a.mtime);
|
||||
|
||||
const allSessions = new Map();
|
||||
const allEntries = [];
|
||||
const uuidToSessionMap = new Map();
|
||||
|
||||
// Collect all sessions and entries from all files
|
||||
for (const { file } of filesWithStats) {
|
||||
const jsonlFile = path.join(projectDir, file);
|
||||
const result = await parseJsonlSessions(jsonlFile);
|
||||
|
||||
result.sessions.forEach(session => {
|
||||
if (!allSessions.has(session.id)) {
|
||||
allSessions.set(session.id, session);
|
||||
}
|
||||
});
|
||||
|
||||
allEntries.push(...result.entries);
|
||||
|
||||
// Early exit optimization for large projects
|
||||
if (allSessions.size >= (limit + offset) * 2 && allEntries.length >= Math.min(3, filesWithStats.length)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Build UUID-to-session mapping for timeline detection
|
||||
allEntries.forEach(entry => {
|
||||
if (entry.uuid && entry.sessionId) {
|
||||
uuidToSessionMap.set(entry.uuid, entry.sessionId);
|
||||
}
|
||||
});
|
||||
|
||||
// Group sessions by first user message ID
|
||||
const sessionGroups = new Map(); // firstUserMsgId -> { latestSession, allSessions[] }
|
||||
const sessionToFirstUserMsgId = new Map(); // sessionId -> firstUserMsgId
|
||||
|
||||
// Find the first user message for each session
|
||||
allEntries.forEach(entry => {
|
||||
if (entry.sessionId && entry.type === 'user' && entry.parentUuid === null && entry.uuid) {
|
||||
// This is a first user message in a session (parentUuid is null)
|
||||
const firstUserMsgId = entry.uuid;
|
||||
|
||||
if (!sessionToFirstUserMsgId.has(entry.sessionId)) {
|
||||
sessionToFirstUserMsgId.set(entry.sessionId, firstUserMsgId);
|
||||
|
||||
const session = allSessions.get(entry.sessionId);
|
||||
if (session) {
|
||||
if (!sessionGroups.has(firstUserMsgId)) {
|
||||
sessionGroups.set(firstUserMsgId, {
|
||||
latestSession: session,
|
||||
allSessions: [session]
|
||||
});
|
||||
} else {
|
||||
const group = sessionGroups.get(firstUserMsgId);
|
||||
group.allSessions.push(session);
|
||||
|
||||
// Update latest session if this one is more recent
|
||||
if (new Date(session.lastActivity) > new Date(group.latestSession.lastActivity)) {
|
||||
group.latestSession = session;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Collect all sessions that don't belong to any group (standalone sessions)
|
||||
const groupedSessionIds = new Set();
|
||||
sessionGroups.forEach(group => {
|
||||
group.allSessions.forEach(session => groupedSessionIds.add(session.id));
|
||||
});
|
||||
|
||||
const standaloneSessionsArray = Array.from(allSessions.values())
|
||||
.filter(session => !groupedSessionIds.has(session.id));
|
||||
|
||||
// Combine grouped sessions (only show latest from each group) + standalone sessions
|
||||
const latestFromGroups = Array.from(sessionGroups.values()).map(group => {
|
||||
const session = { ...group.latestSession };
|
||||
// Add metadata about grouping
|
||||
if (group.allSessions.length > 1) {
|
||||
session.isGrouped = true;
|
||||
session.groupSize = group.allSessions.length;
|
||||
session.groupSessions = group.allSessions.map(s => s.id);
|
||||
}
|
||||
return session;
|
||||
});
|
||||
const visibleSessions = [...latestFromGroups, ...standaloneSessionsArray]
|
||||
.filter(session => !session.summary.startsWith('{ "'))
|
||||
.sort((a, b) => new Date(b.lastActivity) - new Date(a.lastActivity));
|
||||
|
||||
const total = visibleSessions.length;
|
||||
const paginatedSessions = visibleSessions.slice(offset, offset + limit);
|
||||
const hasMore = offset + limit < total;
|
||||
|
||||
return {
|
||||
sessions: paginatedSessions,
|
||||
hasMore,
|
||||
total,
|
||||
offset,
|
||||
limit
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(`Error reading sessions for project ${projectName}:`, error);
|
||||
return { sessions: [], hasMore: false, total: 0 };
|
||||
}
|
||||
}
|
||||
|
||||
async function parseJsonlSessions(filePath) {
|
||||
const sessions = new Map();
|
||||
const entries = [];
|
||||
const pendingSummaries = new Map(); // leafUuid -> summary for entries without sessionId
|
||||
const latestUserTextBySession = new Map();
|
||||
const latestAssistantTextBySession = new Map();
|
||||
|
||||
try {
|
||||
const fileStream = fsSync.createReadStream(filePath);
|
||||
const rl = readline.createInterface({
|
||||
input: fileStream,
|
||||
crlfDelay: Infinity
|
||||
});
|
||||
|
||||
for await (const line of rl) {
|
||||
if (line.trim()) {
|
||||
try {
|
||||
const entry = JSON.parse(line);
|
||||
entries.push(entry);
|
||||
|
||||
// Handle summary entries that don't have sessionId yet
|
||||
if (entry.type === 'summary' && entry.summary && !entry.sessionId && entry.leafUuid) {
|
||||
pendingSummaries.set(entry.leafUuid, entry.summary);
|
||||
}
|
||||
|
||||
if (entry.sessionId) {
|
||||
if (!sessions.has(entry.sessionId)) {
|
||||
sessions.set(entry.sessionId, {
|
||||
id: entry.sessionId,
|
||||
summary: 'New Session',
|
||||
messageCount: 0,
|
||||
lastActivity: new Date(),
|
||||
});
|
||||
}
|
||||
|
||||
const session = sessions.get(entry.sessionId);
|
||||
|
||||
// Apply pending summary if this entry has a parentUuid that matches a pending summary
|
||||
if (session.summary === 'New Session' && entry.parentUuid && pendingSummaries.has(entry.parentUuid)) {
|
||||
session.summary = pendingSummaries.get(entry.parentUuid);
|
||||
}
|
||||
|
||||
// Update summary from summary entries with sessionId
|
||||
if (entry.type === 'summary' && entry.summary) {
|
||||
session.summary = entry.summary;
|
||||
}
|
||||
|
||||
// Track last user and assistant messages (skip system messages)
|
||||
if (entry.message?.role === 'user' && entry.message?.content) {
|
||||
const content = entry.message.content;
|
||||
|
||||
// Extract text from array format if needed
|
||||
let textContent = content;
|
||||
if (Array.isArray(content) && content.length > 0 && content[0].type === 'text') {
|
||||
textContent = content[0].text;
|
||||
}
|
||||
|
||||
const isSystemMessage = typeof textContent === 'string' && (
|
||||
textContent.startsWith('<command-name>') ||
|
||||
textContent.startsWith('<command-message>') ||
|
||||
textContent.startsWith('<command-args>') ||
|
||||
textContent.startsWith('<local-command-stdout>') ||
|
||||
textContent.startsWith('<system-reminder>') ||
|
||||
textContent.startsWith('Caveat:') ||
|
||||
textContent.startsWith('This session is being continued from a previous') ||
|
||||
textContent.startsWith('Invalid API key') ||
|
||||
textContent.includes('{"subtasks":') || // Filter Task Master prompts
|
||||
textContent.includes('CRITICAL: You MUST respond with ONLY a JSON') || // Filter Task Master system prompts
|
||||
textContent === 'Warmup' // Explicitly filter out "Warmup"
|
||||
);
|
||||
|
||||
if (typeof textContent === 'string' && textContent.length > 0 && !isSystemMessage) {
|
||||
latestUserTextBySession.set(entry.sessionId, textContent);
|
||||
}
|
||||
} else if (entry.message?.role === 'assistant' && entry.message?.content) {
|
||||
// Skip API error messages using the isApiErrorMessage flag
|
||||
if (entry.isApiErrorMessage === true) {
|
||||
// Skip this message entirely
|
||||
} else {
|
||||
// Track last assistant text message
|
||||
let assistantText = null;
|
||||
|
||||
if (Array.isArray(entry.message.content)) {
|
||||
for (const part of entry.message.content) {
|
||||
if (part.type === 'text' && part.text) {
|
||||
assistantText = part.text;
|
||||
}
|
||||
}
|
||||
} else if (typeof entry.message.content === 'string') {
|
||||
assistantText = entry.message.content;
|
||||
}
|
||||
|
||||
// Additional filter for assistant messages with system content
|
||||
const isSystemAssistantMessage = typeof assistantText === 'string' && (
|
||||
assistantText.startsWith('Invalid API key') ||
|
||||
assistantText.includes('{"subtasks":') ||
|
||||
assistantText.includes('CRITICAL: You MUST respond with ONLY a JSON')
|
||||
);
|
||||
|
||||
if (assistantText && !isSystemAssistantMessage) {
|
||||
latestAssistantTextBySession.set(entry.sessionId, assistantText);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
session.messageCount++;
|
||||
|
||||
if (entry.timestamp) {
|
||||
session.lastActivity = new Date(entry.timestamp);
|
||||
}
|
||||
}
|
||||
} catch (parseError) {
|
||||
// Skip malformed lines silently
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// After processing all entries, set final summary based on last message if no summary exists
|
||||
for (const session of sessions.values()) {
|
||||
if (session.summary === 'New Session') {
|
||||
// Prefer last user message, fall back to last assistant message.
|
||||
const fallbackMessage = latestUserTextBySession.get(session.id) || latestAssistantTextBySession.get(session.id);
|
||||
if (fallbackMessage) {
|
||||
session.summary = fallbackMessage.length > 50 ? `${fallbackMessage.substring(0, 50)}...` : fallbackMessage;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Filter out sessions that contain JSON responses (Task Master errors)
|
||||
const allSessions = Array.from(sessions.values());
|
||||
const filteredSessions = allSessions.filter(session => {
|
||||
const shouldFilter = session.summary.startsWith('{ "');
|
||||
if (shouldFilter) {
|
||||
}
|
||||
// Log a sample of summaries to debug
|
||||
if (Math.random() < 0.01) { // Log 1% of sessions
|
||||
}
|
||||
return !shouldFilter;
|
||||
});
|
||||
|
||||
|
||||
return {
|
||||
sessions: filteredSessions,
|
||||
entries: entries
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error reading JSONL file:', error);
|
||||
return { sessions: [], entries: [] };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ID-based wrapper around `deleteSession`.
|
||||
*
|
||||
* Resolves the real Claude history folder via the DB-backed path, then defers
|
||||
* to the filesystem deletion routine. Callers should still clean up any DB
|
||||
* bookkeeping (e.g. the sessions table) at the route layer.
|
||||
*/
|
||||
async function deleteSessionById(projectId, sessionId) {
|
||||
const projectPath = await getProjectPathById(projectId);
|
||||
if (!projectPath) {
|
||||
throw new Error(`Unknown projectId: ${projectId}`);
|
||||
}
|
||||
|
||||
const claudeFolderName = claudeFolderNameFromPath(projectPath);
|
||||
return deleteSession(claudeFolderName, sessionId);
|
||||
}
|
||||
|
||||
// Delete a session from a project
|
||||
async function deleteSession(projectName, sessionId) {
|
||||
const projectDir = path.join(os.homedir(), '.claude', 'projects', projectName);
|
||||
|
||||
try {
|
||||
const files = await fs.readdir(projectDir);
|
||||
const jsonlFiles = files.filter(file => file.endsWith('.jsonl'));
|
||||
|
||||
if (jsonlFiles.length === 0) {
|
||||
throw new Error('No session files found for this project');
|
||||
}
|
||||
|
||||
// Check all JSONL files to find which one contains the session
|
||||
for (const file of jsonlFiles) {
|
||||
const jsonlFile = path.join(projectDir, file);
|
||||
const content = await fs.readFile(jsonlFile, 'utf8');
|
||||
const lines = content.split('\n').filter(line => line.trim());
|
||||
|
||||
// Check if this file contains the session
|
||||
const hasSession = lines.some(line => {
|
||||
try {
|
||||
const data = JSON.parse(line);
|
||||
return data.sessionId === sessionId;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
if (hasSession) {
|
||||
// Filter out all entries for this session
|
||||
const filteredLines = lines.filter(line => {
|
||||
try {
|
||||
const data = JSON.parse(line);
|
||||
return data.sessionId !== sessionId;
|
||||
} catch {
|
||||
return true; // Keep malformed lines
|
||||
}
|
||||
});
|
||||
|
||||
// Write back the filtered content
|
||||
await fs.writeFile(jsonlFile, filteredLines.join('\n') + (filteredLines.length > 0 ? '\n' : ''));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`Session ${sessionId} not found in any files`);
|
||||
} catch (error) {
|
||||
console.error(`Error deleting session ${sessionId} from project ${projectName}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeComparablePath(inputPath) {
|
||||
if (!inputPath || typeof inputPath !== 'string') {
|
||||
return '';
|
||||
}
|
||||
|
||||
const withoutLongPathPrefix = inputPath.startsWith('\\\\?\\')
|
||||
? inputPath.slice(4)
|
||||
: inputPath;
|
||||
const normalized = path.normalize(withoutLongPathPrefix.trim());
|
||||
|
||||
if (!normalized) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const resolved = path.resolve(normalized);
|
||||
return process.platform === 'win32' ? resolved.toLowerCase() : resolved;
|
||||
}
|
||||
|
||||
async function findCodexJsonlFiles(dir) {
|
||||
const files = [];
|
||||
|
||||
try {
|
||||
const entries = await fs.readdir(dir, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
files.push(...await findCodexJsonlFiles(fullPath));
|
||||
} else if (entry.name.endsWith('.jsonl')) {
|
||||
files.push(fullPath);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Skip directories we can't read
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
async function buildCodexSessionsIndex() {
|
||||
const codexSessionsDir = path.join(os.homedir(), '.codex', 'sessions');
|
||||
const sessionsByProject = new Map();
|
||||
|
||||
try {
|
||||
await fs.access(codexSessionsDir);
|
||||
} catch (error) {
|
||||
return sessionsByProject;
|
||||
}
|
||||
|
||||
const jsonlFiles = await findCodexJsonlFiles(codexSessionsDir);
|
||||
|
||||
for (const filePath of jsonlFiles) {
|
||||
try {
|
||||
const sessionData = await parseCodexSessionFile(filePath);
|
||||
if (!sessionData || !sessionData.id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const normalizedProjectPath = normalizeComparablePath(sessionData.cwd);
|
||||
if (!normalizedProjectPath) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const session = {
|
||||
id: sessionData.id,
|
||||
summary: sessionData.summary || 'Codex Session',
|
||||
messageCount: sessionData.messageCount || 0,
|
||||
lastActivity: sessionData.timestamp ? new Date(sessionData.timestamp) : new Date(),
|
||||
model: sessionData.model,
|
||||
filePath,
|
||||
provider: 'codex',
|
||||
};
|
||||
|
||||
if (!sessionsByProject.has(normalizedProjectPath)) {
|
||||
sessionsByProject.set(normalizedProjectPath, []);
|
||||
}
|
||||
|
||||
sessionsByProject.get(normalizedProjectPath).push(session);
|
||||
} catch (error) {
|
||||
console.warn(`Could not parse Codex session file ${filePath}:`, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
for (const sessions of sessionsByProject.values()) {
|
||||
sessions.sort((a, b) => new Date(b.lastActivity) - new Date(a.lastActivity));
|
||||
}
|
||||
|
||||
return sessionsByProject;
|
||||
}
|
||||
|
||||
// Fetch Codex sessions for a given project path
|
||||
async function getCodexSessions(projectPath, options = {}) {
|
||||
const { limit = 5, indexRef = null } = options;
|
||||
try {
|
||||
const normalizedProjectPath = normalizeComparablePath(projectPath);
|
||||
if (!normalizedProjectPath) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (indexRef && !indexRef.sessionsByProject) {
|
||||
indexRef.sessionsByProject = await buildCodexSessionsIndex();
|
||||
}
|
||||
|
||||
const sessionsByProject = indexRef?.sessionsByProject || await buildCodexSessionsIndex();
|
||||
const sessions = sessionsByProject.get(normalizedProjectPath) || [];
|
||||
|
||||
// Return limited sessions for performance (0 = unlimited for deletion)
|
||||
return limit > 0 ? sessions.slice(0, limit) : [...sessions];
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error fetching Codex sessions:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function isVisibleCodexUserMessage(payload) {
|
||||
if (!payload || payload.type !== 'user_message') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Codex logs internal context (environment, instructions) as non-plain user_message kinds.
|
||||
if (payload.kind && payload.kind !== 'plain') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeof payload.message !== 'string' || payload.message.trim().length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Parse a Codex session JSONL file to extract metadata
|
||||
async function parseCodexSessionFile(filePath) {
|
||||
try {
|
||||
const fileStream = fsSync.createReadStream(filePath);
|
||||
const rl = readline.createInterface({
|
||||
input: fileStream,
|
||||
crlfDelay: Infinity
|
||||
});
|
||||
|
||||
let sessionMeta = null;
|
||||
let lastTimestamp = null;
|
||||
let latestVisibleUserMessage = null;
|
||||
let messageCount = 0;
|
||||
|
||||
for await (const line of rl) {
|
||||
if (line.trim()) {
|
||||
try {
|
||||
const entry = JSON.parse(line);
|
||||
|
||||
// Track timestamp
|
||||
if (entry.timestamp) {
|
||||
lastTimestamp = entry.timestamp;
|
||||
}
|
||||
|
||||
// Extract session metadata
|
||||
if (entry.type === 'session_meta' && entry.payload) {
|
||||
sessionMeta = {
|
||||
id: entry.payload.id,
|
||||
cwd: entry.payload.cwd,
|
||||
model: entry.payload.model || entry.payload.model_provider,
|
||||
timestamp: entry.timestamp,
|
||||
git: entry.payload.git
|
||||
};
|
||||
}
|
||||
|
||||
// Count visible user messages and extract summary from the latest plain user input.
|
||||
if (entry.type === 'event_msg' && isVisibleCodexUserMessage(entry.payload)) {
|
||||
messageCount++;
|
||||
if (entry.payload.message) {
|
||||
latestVisibleUserMessage = entry.payload.message;
|
||||
}
|
||||
}
|
||||
|
||||
if (entry.type === 'response_item' && entry.payload?.type === 'message' && entry.payload.role === 'assistant') {
|
||||
messageCount++;
|
||||
}
|
||||
|
||||
} catch (parseError) {
|
||||
// Skip malformed lines
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (sessionMeta) {
|
||||
return {
|
||||
...sessionMeta,
|
||||
timestamp: lastTimestamp || sessionMeta.timestamp,
|
||||
summary: latestVisibleUserMessage ?
|
||||
(latestVisibleUserMessage.length > 50 ? latestVisibleUserMessage.substring(0, 50) + '...' : latestVisibleUserMessage) :
|
||||
'Codex Session',
|
||||
messageCount
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error parsing Codex session file:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteCodexSession(sessionId) {
|
||||
try {
|
||||
const codexSessionsDir = path.join(os.homedir(), '.codex', 'sessions');
|
||||
|
||||
const findJsonlFiles = async (dir) => {
|
||||
const files = [];
|
||||
try {
|
||||
const entries = await fs.readdir(dir, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
files.push(...await findJsonlFiles(fullPath));
|
||||
} else if (entry.name.endsWith('.jsonl')) {
|
||||
files.push(fullPath);
|
||||
}
|
||||
}
|
||||
} catch (error) { }
|
||||
return files;
|
||||
};
|
||||
|
||||
const jsonlFiles = await findJsonlFiles(codexSessionsDir);
|
||||
|
||||
for (const filePath of jsonlFiles) {
|
||||
const sessionData = await parseCodexSessionFile(filePath);
|
||||
if (sessionData && sessionData.id === sessionId) {
|
||||
await fs.unlink(filePath);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`Codex session file not found for session ${sessionId}`);
|
||||
} catch (error) {
|
||||
console.error(`Error deleting Codex session ${sessionId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Only functions with consumers outside this module are exported. Folder-name
|
||||
// based helpers (`getSessions`, `deleteSession`, etc.) are kept as internal
|
||||
// implementation details of the id-based wrappers below.
|
||||
export {
|
||||
deleteSessionById,
|
||||
getProjectPathById,
|
||||
claudeFolderNameFromPath,
|
||||
deleteCodexSession
|
||||
getProjectPathById
|
||||
};
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import express from 'express';
|
||||
|
||||
import { deleteCodexSession } from '../projects.js';
|
||||
import { sessionsDb } from '../modules/database/index.js';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.delete('/sessions/:sessionId', async (req, res) => {
|
||||
try {
|
||||
const { sessionId } = req.params;
|
||||
await deleteCodexSession(sessionId);
|
||||
sessionsDb.deleteSessionById(sessionId);
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error(`Error deleting Codex session ${req.params.sessionId}:`, error);
|
||||
res.status(500).json({ success: false, error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
Reference in New Issue
Block a user