mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-05-16 17:16:19 +00:00
feat: setup delete session by id
This commit is contained in:
@@ -4,8 +4,9 @@ import { promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
import os from 'os';
|
||||
import TOML from '@iarna/toml';
|
||||
import { getCodexSessions, deleteCodexSession } from '../../../projects.js';
|
||||
import { getCodexSessions } from '../../../projects.js';
|
||||
import { sessionsDb } from '@/shared/database/repositories/sessions.db.js';
|
||||
import { deleteSession as deleteSessionFromProviders } from '@/modules/sessions/sessions.service.js';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
@@ -71,8 +72,7 @@ router.get('/sessions', async (req, res) => {
|
||||
router.delete('/sessions/:sessionId', async (req, res) => {
|
||||
try {
|
||||
const { sessionId } = req.params;
|
||||
await deleteCodexSession(sessionId);
|
||||
sessionsDb.deleteSession(sessionId);
|
||||
await deleteSessionFromProviders(sessionId);
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error(`Error deleting Codex session ${req.params.sessionId}:`, error);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import express from 'express';
|
||||
import sessionManager from '../../../sessionManager.js';
|
||||
import { sessionsDb } from '@/shared/database/repositories/sessions.db.js';
|
||||
import { deleteSession as deleteSessionFromProviders } from '@/modules/sessions/sessions.service.js';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
@@ -13,7 +13,7 @@ router.delete('/sessions/:sessionId', async (req, res) => {
|
||||
}
|
||||
|
||||
await sessionManager.deleteSession(sessionId);
|
||||
sessionsDb.deleteSession(sessionId);
|
||||
await deleteSessionFromProviders(sessionId);
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error(`Error deleting Gemini session ${req.params.sessionId}:`, error);
|
||||
|
||||
@@ -6,12 +6,12 @@ import {
|
||||
getProjects,
|
||||
getSessions,
|
||||
renameProject,
|
||||
deleteSession,
|
||||
deleteProject,
|
||||
searchConversations
|
||||
} from '../../../projects.js';
|
||||
import { sessionsDb } from '@/shared/database/repositories/sessions.db.js';
|
||||
import { workspaceOriginalPathsDb } from '@/shared/database/repositories/workspace-original-paths.db.js';
|
||||
import { deleteSession as deleteSessionFromProviders } from '@/modules/sessions/sessions.service.js';
|
||||
import { authenticateToken } from '../auth/auth.middleware.js';
|
||||
import { getWorkspaceNameFromPath, WORKSPACES_ROOT, validateWorkspacePath } from './projects.utils.js';
|
||||
|
||||
@@ -69,8 +69,7 @@ router.delete('/api/projects/:projectName/sessions/:sessionId', authenticateToke
|
||||
try {
|
||||
const { projectName, sessionId } = req.params;
|
||||
console.log(`[API] Deleting session: ${sessionId} from project: ${projectName}`);
|
||||
await deleteSession(projectName, sessionId);
|
||||
sessionsDb.deleteSession(sessionId);
|
||||
await deleteSessionFromProviders(sessionId);
|
||||
console.log(`[API] Session ${sessionId} deleted successfully`);
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
|
||||
@@ -49,3 +49,67 @@ export async function processClaudeSessions() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function encodeClaudeProjectPath(projectPath: string): string {
|
||||
return projectPath.replace(/[^a-zA-Z0-9-]/g, '-');
|
||||
}
|
||||
|
||||
async function removeFileIfExists(filePath: string): Promise<boolean> {
|
||||
try {
|
||||
await fsp.unlink(filePath);
|
||||
return true;
|
||||
} catch (error: any) {
|
||||
if (error?.code === 'ENOENT') {
|
||||
return false;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function listDirectoryEntriesSafe(directoryPath: string): Promise<import('node:fs').Dirent[]> {
|
||||
try {
|
||||
return await fsp.readdir(directoryPath, { withFileTypes: true });
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async function findFilesByName(rootPath: string, fileName: string): Promise<string[]> {
|
||||
const matches: string[] = [];
|
||||
const stack = [rootPath];
|
||||
|
||||
while (stack.length > 0) {
|
||||
const currentPath = stack.pop() as string;
|
||||
const entries = await listDirectoryEntriesSafe(currentPath);
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(currentPath, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
stack.push(fullPath);
|
||||
} else if (entry.isFile() && entry.name === fileName) {
|
||||
matches.push(fullPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return matches;
|
||||
}
|
||||
|
||||
export async function deleteClaudeSession(sessionId: string, workspacePath?: string): Promise<boolean> {
|
||||
const claudeProjectsDir = path.join(os.homedir(), '.claude', 'projects');
|
||||
const fileName = `${sessionId}.jsonl`;
|
||||
let deleted = false;
|
||||
|
||||
if (workspacePath) {
|
||||
const encodedPath = encodeClaudeProjectPath(workspacePath);
|
||||
const candidateFilePath = path.join(claudeProjectsDir, encodedPath, fileName);
|
||||
deleted = (await removeFileIfExists(candidateFilePath)) || deleted;
|
||||
}
|
||||
|
||||
const matches = await findFilesByName(claudeProjectsDir, fileName);
|
||||
for (const filePath of matches) {
|
||||
deleted = (await removeFileIfExists(filePath)) || deleted;
|
||||
}
|
||||
|
||||
return deleted;
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
import fsp from 'node:fs/promises';
|
||||
import { sessionsDb } from '@/shared/database/repositories/sessions.db.js';
|
||||
import { buildLookupMap, extractFirstValidJsonlData, findFilesRecursivelyCreatedAfterLastScan } from '@/modules/providers/shared/session-parser.utils.js';
|
||||
import { SessionData } from '@/shared/types/session.js';
|
||||
|
||||
export async function processCodexSessionFile(file: string, nameMap?: Map<string, string>): Promise<SessionData | null> {
|
||||
if (!nameMap) {
|
||||
const base = path.join(os.homedir(), '.codex');
|
||||
nameMap = await buildLookupMap(path.join(base, 'session_index.jsonl'), 'id', 'thread_name');
|
||||
}
|
||||
|
||||
// Codex nests the required data inside a `payload` object
|
||||
return extractFirstValidJsonlData(file, (data) => ({
|
||||
workspacePath: data?.payload?.cwd,
|
||||
sessionId: data?.payload?.id,
|
||||
sessionName: nameMap!.get(data?.payload?.id) || 'Untitled Codex Session'
|
||||
}));
|
||||
}
|
||||
|
||||
export async function processCodexSessions() {
|
||||
const base = path.join(os.homedir(), '.codex');
|
||||
// Use the thread_name attribute as requested
|
||||
const nameMap = await buildLookupMap(path.join(base, 'session_index.jsonl'), 'id', 'thread_name');
|
||||
|
||||
const files = await findFilesRecursivelyCreatedAfterLastScan(path.join(base, 'sessions'), '.jsonl');
|
||||
|
||||
for (const file of files) {
|
||||
const result = await processCodexSessionFile(file, nameMap);
|
||||
|
||||
if (result) {
|
||||
let createdAt: string | undefined;
|
||||
let updatedAt: string | undefined;
|
||||
try {
|
||||
const stat = await fsp.stat(file);
|
||||
createdAt = stat.birthtime.toISOString();
|
||||
updatedAt = stat.mtime.toISOString();
|
||||
} catch {
|
||||
// Ignore stat failures and let DB defaults handle created_at/updated_at.
|
||||
}
|
||||
sessionsDb.createSession(
|
||||
result.sessionId,
|
||||
'codex',
|
||||
result.workspacePath,
|
||||
result.sessionName,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
151
server/src/modules/providers/codex/codex.session-processor.ts
Normal file
151
server/src/modules/providers/codex/codex.session-processor.ts
Normal file
@@ -0,0 +1,151 @@
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
import fsp from 'node:fs/promises';
|
||||
import { sessionsDb } from '@/shared/database/repositories/sessions.db.js';
|
||||
import { buildLookupMap, extractFirstValidJsonlData, findFilesRecursivelyCreatedAfterLastScan } from '@/modules/providers/shared/session-parser.utils.js';
|
||||
import { SessionData } from '@/shared/types/session.js';
|
||||
|
||||
export async function processCodexSessionFile(file: string, nameMap?: Map<string, string>): Promise<SessionData | null> {
|
||||
if (!nameMap) {
|
||||
const base = path.join(os.homedir(), '.codex');
|
||||
nameMap = await buildLookupMap(path.join(base, 'session_index.jsonl'), 'id', 'thread_name');
|
||||
}
|
||||
|
||||
// Codex nests the required data inside a `payload` object
|
||||
return extractFirstValidJsonlData(file, (data) => ({
|
||||
workspacePath: data?.payload?.cwd,
|
||||
sessionId: data?.payload?.id,
|
||||
sessionName: nameMap!.get(data?.payload?.id) || 'Untitled Codex Session'
|
||||
}));
|
||||
}
|
||||
|
||||
export async function processCodexSessions() {
|
||||
const base = path.join(os.homedir(), '.codex');
|
||||
// Use the thread_name attribute as requested
|
||||
const nameMap = await buildLookupMap(path.join(base, 'session_index.jsonl'), 'id', 'thread_name');
|
||||
|
||||
const files = await findFilesRecursivelyCreatedAfterLastScan(path.join(base, 'sessions'), '.jsonl');
|
||||
|
||||
for (const file of files) {
|
||||
const result = await processCodexSessionFile(file, nameMap);
|
||||
|
||||
if (result) {
|
||||
let createdAt: string | undefined;
|
||||
let updatedAt: string | undefined;
|
||||
try {
|
||||
const stat = await fsp.stat(file);
|
||||
createdAt = stat.birthtime.toISOString();
|
||||
updatedAt = stat.mtime.toISOString();
|
||||
} catch {
|
||||
// Ignore stat failures and let DB defaults handle created_at/updated_at.
|
||||
}
|
||||
sessionsDb.createSession(
|
||||
result.sessionId,
|
||||
'codex',
|
||||
result.workspacePath,
|
||||
result.sessionName,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function buildCodexDatePathParts(createdAt: string): Array<{ year: string; month: string; day: string }> {
|
||||
const parsedDate = new Date(createdAt);
|
||||
if (Number.isNaN(parsedDate.getTime())) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const localDate = {
|
||||
year: String(parsedDate.getFullYear()),
|
||||
month: String(parsedDate.getMonth() + 1),
|
||||
day: String(parsedDate.getDate()),
|
||||
};
|
||||
|
||||
const utcDate = {
|
||||
year: String(parsedDate.getUTCFullYear()),
|
||||
month: String(parsedDate.getUTCMonth() + 1),
|
||||
day: String(parsedDate.getUTCDate()),
|
||||
};
|
||||
|
||||
if (
|
||||
localDate.year === utcDate.year &&
|
||||
localDate.month === utcDate.month &&
|
||||
localDate.day === utcDate.day
|
||||
) {
|
||||
return [localDate];
|
||||
}
|
||||
|
||||
return [localDate, utcDate];
|
||||
}
|
||||
|
||||
async function removeFileIfExists(filePath: string): Promise<boolean> {
|
||||
try {
|
||||
await fsp.unlink(filePath);
|
||||
return true;
|
||||
} catch (error: any) {
|
||||
if (error?.code === 'ENOENT') {
|
||||
return false;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function listDirectoryEntriesSafe(directoryPath: string): Promise<import('node:fs').Dirent[]> {
|
||||
try {
|
||||
return await fsp.readdir(directoryPath, { withFileTypes: true });
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async function findFilesByName(rootPath: string, fileName: string): Promise<string[]> {
|
||||
const matches: string[] = [];
|
||||
const stack = [rootPath];
|
||||
|
||||
while (stack.length > 0) {
|
||||
const currentPath = stack.pop() as string;
|
||||
const entries = await listDirectoryEntriesSafe(currentPath);
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(currentPath, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
stack.push(fullPath);
|
||||
} else if (entry.isFile() && entry.name === fileName) {
|
||||
matches.push(fullPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return matches;
|
||||
}
|
||||
|
||||
export async function deleteCodexSession(sessionId: string, createdAt?: string): Promise<boolean> {
|
||||
const codexSessionsDir = path.join(os.homedir(), '.codex', 'sessions');
|
||||
const fileName = `${sessionId}.jsonl`;
|
||||
let deleted = false;
|
||||
|
||||
if (createdAt) {
|
||||
const datePathParts = buildCodexDatePathParts(createdAt);
|
||||
for (const parts of datePathParts) {
|
||||
const candidateFilePath = path.join(
|
||||
codexSessionsDir,
|
||||
parts.year,
|
||||
parts.month,
|
||||
parts.day,
|
||||
fileName,
|
||||
);
|
||||
deleted = (await removeFileIfExists(candidateFilePath)) || deleted;
|
||||
}
|
||||
}
|
||||
|
||||
if (!deleted) {
|
||||
const matches = await findFilesByName(codexSessionsDir, fileName);
|
||||
for (const filePath of matches) {
|
||||
deleted = (await removeFileIfExists(filePath)) || deleted;
|
||||
}
|
||||
}
|
||||
|
||||
return deleted;
|
||||
}
|
||||
@@ -12,6 +12,84 @@ function md5(input: string): string {
|
||||
return crypto.createHash('md5').update(input).digest('hex');
|
||||
}
|
||||
|
||||
async function removeFileIfExists(filePath: string): Promise<boolean> {
|
||||
try {
|
||||
await fsp.unlink(filePath);
|
||||
return true;
|
||||
} catch (error: any) {
|
||||
if (error?.code === 'ENOENT') {
|
||||
return false;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function removeDirectoryIfExists(directoryPath: string): Promise<boolean> {
|
||||
try {
|
||||
await fsp.rm(directoryPath, { recursive: true, force: false });
|
||||
return true;
|
||||
} catch (error: any) {
|
||||
if (error?.code === 'ENOENT') {
|
||||
return false;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function listDirectoryEntriesSafe(directoryPath: string): Promise<import('node:fs').Dirent[]> {
|
||||
try {
|
||||
return await fsp.readdir(directoryPath, { withFileTypes: true });
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async function findDirectoriesByName(rootPath: string, directoryName: string): Promise<string[]> {
|
||||
const matches: string[] = [];
|
||||
const stack = [rootPath];
|
||||
|
||||
while (stack.length > 0) {
|
||||
const currentPath = stack.pop() as string;
|
||||
const entries = await listDirectoryEntriesSafe(currentPath);
|
||||
|
||||
for (const entry of entries) {
|
||||
if (!entry.isDirectory()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const fullPath = path.join(currentPath, entry.name);
|
||||
if (entry.name === directoryName) {
|
||||
matches.push(fullPath);
|
||||
}
|
||||
|
||||
stack.push(fullPath);
|
||||
}
|
||||
}
|
||||
|
||||
return matches;
|
||||
}
|
||||
|
||||
async function findFilesByName(rootPath: string, fileName: string): Promise<string[]> {
|
||||
const matches: string[] = [];
|
||||
const stack = [rootPath];
|
||||
|
||||
while (stack.length > 0) {
|
||||
const currentPath = stack.pop() as string;
|
||||
const entries = await listDirectoryEntriesSafe(currentPath);
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(currentPath, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
stack.push(fullPath);
|
||||
} else if (entry.isFile() && entry.name === fileName) {
|
||||
matches.push(fullPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return matches;
|
||||
}
|
||||
|
||||
export async function extractWorkspacePathFromWorkerLog(filePath: string): Promise<string | null> {
|
||||
try {
|
||||
const fileStream = fs.createReadStream(filePath, { encoding: 'utf8' });
|
||||
@@ -104,3 +182,26 @@ export async function processCursorSessions() {
|
||||
// Base cursor directory or projects directory likely doesn't exist
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteCursorSession(sessionId: string, workspacePath?: string): Promise<boolean> {
|
||||
const cursorChatsDir = path.join(os.homedir(), '.cursor', 'chats');
|
||||
let deleted = false;
|
||||
|
||||
if (workspacePath) {
|
||||
const cwdId = md5(workspacePath);
|
||||
const candidateDir = path.join(cursorChatsDir, cwdId, sessionId);
|
||||
deleted = (await removeDirectoryIfExists(candidateDir)) || deleted;
|
||||
}
|
||||
|
||||
const sessionDirs = await findDirectoriesByName(cursorChatsDir, sessionId);
|
||||
for (const directoryPath of sessionDirs) {
|
||||
deleted = (await removeDirectoryIfExists(directoryPath)) || deleted;
|
||||
}
|
||||
|
||||
const jsonlFiles = await findFilesByName(cursorChatsDir, `${sessionId}.jsonl`);
|
||||
for (const filePath of jsonlFiles) {
|
||||
deleted = (await removeFileIfExists(filePath)) || deleted;
|
||||
}
|
||||
|
||||
return deleted;
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
import fsp from 'node:fs/promises';
|
||||
import { sessionsDb } from '@/shared/database/repositories/sessions.db.js';
|
||||
import { findFilesRecursivelyCreatedAfterLastScan } from '@/modules/providers/shared/session-parser.utils.js';
|
||||
import { SessionData } from '@/shared/types/session.js';
|
||||
|
||||
export async function processGeminiSessionFile(file: string): Promise<SessionData | null> {
|
||||
try {
|
||||
// Gemini uses standard JSON (not JSONL), so we read the whole file at once
|
||||
|
||||
const fileContent = await fsp.readFile(file, 'utf8');
|
||||
const data = JSON.parse(fileContent);
|
||||
if (data?.id && data?.projectPath) {
|
||||
return {
|
||||
sessionId: data.id,
|
||||
workspacePath: data.projectPath,
|
||||
sessionName: data.messages?.[0]?.content || 'New Gemini Chat'
|
||||
};
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore parsing error for gemini
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function processGeminiSessions() {
|
||||
const geminiPath = path.join(os.homedir(), '.gemini', 'sessions');
|
||||
const files = await findFilesRecursivelyCreatedAfterLastScan(geminiPath, '.json');
|
||||
|
||||
for (const file of files) {
|
||||
const result = await processGeminiSessionFile(file);
|
||||
if (result) {
|
||||
let createdAt: string | undefined;
|
||||
let updatedAt: string | undefined;
|
||||
try {
|
||||
const stat = await fsp.stat(file);
|
||||
createdAt = stat.birthtime.toISOString();
|
||||
updatedAt = stat.mtime.toISOString();
|
||||
} catch {
|
||||
// Ignore stat failures and let DB defaults handle created_at/updated_at.
|
||||
}
|
||||
sessionsDb.createSession(
|
||||
result.sessionId,
|
||||
'gemini',
|
||||
result.workspacePath,
|
||||
result.sessionName,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
118
server/src/modules/providers/gemini/gemini.session-processor.ts
Normal file
118
server/src/modules/providers/gemini/gemini.session-processor.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
import fsp from 'node:fs/promises';
|
||||
import { sessionsDb } from '@/shared/database/repositories/sessions.db.js';
|
||||
import { findFilesRecursivelyCreatedAfterLastScan } from '@/modules/providers/shared/session-parser.utils.js';
|
||||
import { SessionData } from '@/shared/types/session.js';
|
||||
|
||||
export async function processGeminiSessionFile(file: string): Promise<SessionData | null> {
|
||||
try {
|
||||
// Gemini uses standard JSON (not JSONL), so we read the whole file at once
|
||||
|
||||
const fileContent = await fsp.readFile(file, 'utf8');
|
||||
const data = JSON.parse(fileContent);
|
||||
if (data?.id && data?.projectPath) {
|
||||
return {
|
||||
sessionId: data.id,
|
||||
workspacePath: data.projectPath,
|
||||
sessionName: data.messages?.[0]?.content || 'New Gemini Chat'
|
||||
};
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore parsing error for gemini
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function processGeminiSessions() {
|
||||
const geminiPath = path.join(os.homedir(), '.gemini', 'sessions');
|
||||
const files = await findFilesRecursivelyCreatedAfterLastScan(geminiPath, '.json');
|
||||
|
||||
for (const file of files) {
|
||||
const result = await processGeminiSessionFile(file);
|
||||
if (result) {
|
||||
let createdAt: string | undefined;
|
||||
let updatedAt: string | undefined;
|
||||
try {
|
||||
const stat = await fsp.stat(file);
|
||||
createdAt = stat.birthtime.toISOString();
|
||||
updatedAt = stat.mtime.toISOString();
|
||||
} catch {
|
||||
// Ignore stat failures and let DB defaults handle created_at/updated_at.
|
||||
}
|
||||
sessionsDb.createSession(
|
||||
result.sessionId,
|
||||
'gemini',
|
||||
result.workspacePath,
|
||||
result.sessionName,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function removeFileIfExists(filePath: string): Promise<boolean> {
|
||||
try {
|
||||
await fsp.unlink(filePath);
|
||||
return true;
|
||||
} catch (error: any) {
|
||||
if (error?.code === 'ENOENT') {
|
||||
return false;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function listDirectoryEntriesSafe(directoryPath: string): Promise<import('node:fs').Dirent[]> {
|
||||
try {
|
||||
return await fsp.readdir(directoryPath, { withFileTypes: true });
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteGeminiSession(sessionId: string): Promise<boolean> {
|
||||
const geminiHome = path.join(os.homedir(), '.gemini');
|
||||
const geminiSessionsDir = path.join(geminiHome, 'sessions');
|
||||
const geminiTmpDir = path.join(geminiHome, 'tmp');
|
||||
let deleted = false;
|
||||
|
||||
deleted = (await removeFileIfExists(path.join(geminiSessionsDir, `${sessionId}.json`))) || deleted;
|
||||
deleted = (await removeFileIfExists(path.join(geminiSessionsDir, `${sessionId}.jsonl`))) || deleted;
|
||||
|
||||
const projectDirs = await listDirectoryEntriesSafe(geminiTmpDir);
|
||||
for (const projectDir of projectDirs) {
|
||||
if (!projectDir.isDirectory()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const chatsDir = path.join(geminiTmpDir, projectDir.name, 'chats');
|
||||
const chatFiles = await listDirectoryEntriesSafe(chatsDir);
|
||||
|
||||
for (const chatFile of chatFiles) {
|
||||
if (!chatFile.isFile() || !chatFile.name.endsWith('.json')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const chatFilePath = path.join(chatsDir, chatFile.name);
|
||||
if (chatFile.name === `${sessionId}.json`) {
|
||||
deleted = (await removeFileIfExists(chatFilePath)) || deleted;
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const content = await fsp.readFile(chatFilePath, 'utf8');
|
||||
const parsed = JSON.parse(content);
|
||||
const parsedId = parsed?.sessionId || parsed?.id;
|
||||
if (parsedId === sessionId) {
|
||||
deleted = (await removeFileIfExists(chatFilePath)) || deleted;
|
||||
}
|
||||
} catch {
|
||||
// Ignore unreadable/malformed session files.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return deleted;
|
||||
}
|
||||
@@ -24,10 +24,10 @@ router.put('/api/sessions/:sessionId/rename', authenticateToken, async (req, res
|
||||
if (summary.trim().length > 500) {
|
||||
return res.status(400).json({ error: 'Summary must not exceed 500 characters' });
|
||||
}
|
||||
if (!provider || !VALID_PROVIDERS.includes(provider)) {
|
||||
if (provider && !VALID_PROVIDERS.includes(provider)) {
|
||||
return res.status(400).json({ error: `Provider must be one of: ${VALID_PROVIDERS.join(', ')}` });
|
||||
}
|
||||
sessionsDb.createSessionName(safeSessionId, provider, summary.trim());
|
||||
sessionsDb.updateSessionCustomName(safeSessionId, summary.trim());
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error(`[API] Error renaming session ${req.params.sessionId}:`, error);
|
||||
|
||||
@@ -1,15 +1,26 @@
|
||||
import { scanStateDb } from '@/shared/database/repositories/scan-state.db.js';
|
||||
import { processClaudeSessions } from '@/modules/providers/claude/claude.session-parser.js';
|
||||
import { processCodexSessions } from '@/modules/providers/codex/codex.session-parser.js';
|
||||
import { processGeminiSessions } from '@/modules/providers/gemini/gemini.session-parser.js';
|
||||
import { processCursorSessions } from '@/modules/providers/cursor/cursor.session-parser.js';
|
||||
import { sessionsDb } from '@/shared/database/repositories/sessions.db.js';
|
||||
import { processClaudeSessions, deleteClaudeSession } from '@/modules/providers/claude/claude.session-processor.js';
|
||||
import { processCodexSessions, deleteCodexSession } from '@/modules/providers/codex/codex.session-processor.js';
|
||||
import { processGeminiSessions, deleteGeminiSession } from '@/modules/providers/gemini/gemini.session-processor.js';
|
||||
import { processCursorSessions, deleteCursorSession } from '@/modules/providers/cursor/cursor.session-processor.js';
|
||||
|
||||
const SESSION_ID_PATTERN = /^[a-zA-Z0-9._-]{1,120}$/;
|
||||
|
||||
function sanitizeSessionId(sessionId: string): string {
|
||||
const value = String(sessionId || '').trim();
|
||||
if (!SESSION_ID_PATTERN.test(value)) {
|
||||
throw new Error('Invalid session ID format');
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
export async function processSessions() {
|
||||
|
||||
// 1. Start the timer with a unique label
|
||||
console.time("🚀 Workspace sync total time");
|
||||
console.time('Workspace sync total time');
|
||||
|
||||
console.log("Starting workspace sync...");
|
||||
console.log('Starting workspace sync...');
|
||||
try {
|
||||
// Wrapping in Promise.all allows these to process concurrently, speeding up the boot time
|
||||
await Promise.allSettled([
|
||||
@@ -21,12 +32,33 @@ export async function processSessions() {
|
||||
|
||||
scanStateDb.updateLastScannedAt();
|
||||
} catch (error) {
|
||||
console.error("An error occurred during sync:", error);
|
||||
console.error('An error occurred during sync:', error);
|
||||
} finally {
|
||||
console.log("----------------------------------");
|
||||
console.log('----------------------------------');
|
||||
// 2. Stop the timer using the exact same label
|
||||
// This will print: 🚀 Workspace sync total time: 123.456ms
|
||||
console.timeEnd("🚀 Workspace sync total time");
|
||||
console.log("Workspace synchronization complete.");
|
||||
// This will print: Workspace sync total time: 123.456ms
|
||||
console.timeEnd('Workspace sync total time');
|
||||
console.log('Workspace synchronization complete.');
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteSession(sessionId: string): Promise<void> {
|
||||
const safeSessionId = sanitizeSessionId(sessionId);
|
||||
const existingSession = sessionsDb.getSessionById(safeSessionId);
|
||||
const workspacePath = existingSession?.workspace_path;
|
||||
const createdAt = existingSession?.created_at;
|
||||
|
||||
const deletionResults = await Promise.allSettled([
|
||||
deleteClaudeSession(safeSessionId, workspacePath),
|
||||
deleteCodexSession(safeSessionId, createdAt),
|
||||
deleteGeminiSession(safeSessionId),
|
||||
deleteCursorSession(safeSessionId, workspacePath),
|
||||
]);
|
||||
|
||||
const rejectedResult = deletionResults.find((result) => result.status === 'rejected') as PromiseRejectedResult | undefined;
|
||||
if (rejectedResult) {
|
||||
throw rejectedResult.reason;
|
||||
}
|
||||
|
||||
sessionsDb.deleteSession(safeSessionId);
|
||||
}
|
||||
|
||||
@@ -4,10 +4,10 @@ import os from "os";
|
||||
import { promises as fsPromises } from "fs";
|
||||
import { logger } from "@/shared/utils/logger.js";
|
||||
import { processSessions } from "@/modules/sessions/sessions.service.js";
|
||||
import { processClaudeSessionFile } from "@/modules/providers/claude/claude.session-parser.js";
|
||||
import { processCodexSessionFile } from "@/modules/providers/codex/codex.session-parser.js";
|
||||
import { processGeminiSessionFile } from "@/modules/providers/gemini/gemini.session-parser.js";
|
||||
import { processCursorSessionFile } from "@/modules/providers/cursor/cursor.session-parser.js";
|
||||
import { processClaudeSessionFile } from "@/modules/providers/claude/claude.session-processor.js";
|
||||
import { processCodexSessionFile } from "@/modules/providers/codex/codex.session-processor.js";
|
||||
import { processGeminiSessionFile } from "@/modules/providers/gemini/gemini.session-processor.js";
|
||||
import { processCursorSessionFile } from "@/modules/providers/cursor/cursor.session-processor.js";
|
||||
import { sessionsDb } from "@/shared/database/repositories/sessions.db.js";
|
||||
import { LLMProvider } from "@/shared/types/app.js";
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { workspaceOriginalPathsDb } from '@/shared/database/repositories/workspace-original-paths.db.js';
|
||||
import { getConnection } from '@/shared/database/connection.js';
|
||||
import type { SessionWithSummary } from '@/shared/database/types.js';
|
||||
import path from 'node:path';
|
||||
import type { SessionsRow, SessionWithSummary } from '@/shared/database/types.js';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Queries
|
||||
@@ -11,6 +12,11 @@ type SessionNameLookupRow = {
|
||||
custom_name: string;
|
||||
};
|
||||
|
||||
type SessionMetadataLookupRow = Pick<
|
||||
SessionsRow,
|
||||
'session_id' | 'provider' | 'workspace_path' | 'created_at' | 'updated_at'
|
||||
>;
|
||||
|
||||
function normalizeTimestamp(value?: string): string | null {
|
||||
if (!value) return null;
|
||||
|
||||
@@ -22,6 +28,34 @@ function normalizeTimestamp(value?: string): string | null {
|
||||
return parsed.toISOString();
|
||||
}
|
||||
|
||||
function normalizeCodexWorkspacePath(workspacePath: string): string {
|
||||
const trimmedPath = workspacePath.trim();
|
||||
if (!trimmedPath) {
|
||||
return workspacePath;
|
||||
}
|
||||
|
||||
if (process.platform !== 'win32') {
|
||||
return path.normalize(trimmedPath);
|
||||
}
|
||||
|
||||
let strippedPath = trimmedPath;
|
||||
if (strippedPath.startsWith('\\\\?\\UNC\\')) {
|
||||
strippedPath = `\\\\${strippedPath.slice('\\\\?\\UNC\\'.length)}`;
|
||||
} else if (strippedPath.startsWith('\\\\?\\')) {
|
||||
strippedPath = strippedPath.slice('\\\\?\\'.length);
|
||||
}
|
||||
|
||||
return path.win32.normalize(strippedPath);
|
||||
}
|
||||
|
||||
function normalizeWorkspacePathForProvider(provider: string, workspacePath: string): string {
|
||||
if (provider !== 'codex') {
|
||||
return workspacePath;
|
||||
}
|
||||
|
||||
return normalizeCodexWorkspacePath(workspacePath);
|
||||
}
|
||||
|
||||
export const sessionsDb = {
|
||||
|
||||
createSession(
|
||||
@@ -35,17 +69,30 @@ export const sessionsDb = {
|
||||
const db = getConnection();
|
||||
const createdAtValue = normalizeTimestamp(createdAt);
|
||||
const updatedAtValue = normalizeTimestamp(updatedAt);
|
||||
const normalizedWorkspacePath = normalizeWorkspacePathForProvider(provider, workspacePath);
|
||||
|
||||
// First, ensure the workspace path is recorded in the workspace_original_paths table
|
||||
// since it's a foreign key in the sessions table.
|
||||
workspaceOriginalPathsDb.createWorkspacePath(workspacePath);
|
||||
workspaceOriginalPathsDb.createWorkspacePath(normalizedWorkspacePath);
|
||||
|
||||
db.prepare(
|
||||
`INSERT INTO sessions (session_id, provider, custom_name, workspace_path, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, COALESCE(?, CURRENT_TIMESTAMP), COALESCE(?, CURRENT_TIMESTAMP))
|
||||
ON CONFLICT(session_id) DO UPDATE SET updated_at = excluded.updated_at
|
||||
ON CONFLICT(session_id) DO UPDATE SET
|
||||
updated_at = excluded.updated_at,
|
||||
workspace_path = excluded.workspace_path
|
||||
WHERE sessions.provider = excluded.provider`
|
||||
).run(session_id, provider, customName, workspacePath, createdAtValue, updatedAtValue);
|
||||
).run(session_id, provider, customName, normalizedWorkspacePath, createdAtValue, updatedAtValue);
|
||||
},
|
||||
|
||||
/** Updates a custom session name by session id, regardless of provider. */
|
||||
updateSessionCustomName(sessionId: string, customName: string): void {
|
||||
const db = getConnection();
|
||||
db.prepare(
|
||||
`UPDATE sessions
|
||||
SET custom_name = ?, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE session_id = ?`
|
||||
).run(customName, sessionId);
|
||||
},
|
||||
|
||||
/** Updates a custom session name for an existing session row. */
|
||||
@@ -58,6 +105,19 @@ export const sessionsDb = {
|
||||
).run(customName, sessionId, provider);
|
||||
},
|
||||
|
||||
getSessionById(sessionId: string): SessionMetadataLookupRow | null {
|
||||
const db = getConnection();
|
||||
const row = db
|
||||
.prepare(
|
||||
`SELECT session_id, provider, workspace_path, created_at, updated_at
|
||||
FROM sessions
|
||||
WHERE session_id = ?`
|
||||
)
|
||||
.get(sessionId) as SessionMetadataLookupRow | undefined;
|
||||
|
||||
return row ?? null;
|
||||
},
|
||||
|
||||
getSessionName(sessionId: string, provider: string): string | null {
|
||||
const db = getConnection();
|
||||
const row = db
|
||||
|
||||
Reference in New Issue
Block a user