mirror of
https://github.com/siteboon/claudecodeui.git
synced 2025-12-08 20:29:37 +00:00
Merge pull request #155 from siteboon/151-cannot-create-project-with-only-cursor-cli
This commit is contained in:
6
.gitignore
vendored
6
.gitignore
vendored
@@ -105,6 +105,8 @@ temp/
|
||||
.taskmaster/
|
||||
.cline/
|
||||
.windsurf/
|
||||
CLAUDE.md
|
||||
|
||||
|
||||
# Database files
|
||||
*.db
|
||||
@@ -124,5 +126,5 @@ dev-debug.log
|
||||
# OS specific
|
||||
|
||||
# Task files
|
||||
tasks.json
|
||||
tasks/
|
||||
# tasks.json
|
||||
# tasks/
|
||||
|
||||
@@ -1,3 +1,62 @@
|
||||
/**
|
||||
* PROJECT DISCOVERY AND MANAGEMENT SYSTEM
|
||||
* ========================================
|
||||
*
|
||||
* This module manages project discovery for both Claude CLI and Cursor CLI sessions.
|
||||
*
|
||||
* ## Architecture Overview
|
||||
*
|
||||
* 1. **Claude Projects** (stored in ~/.claude/projects/)
|
||||
* - Each project is a directory named with the project path encoded (/ replaced with -)
|
||||
* - Contains .jsonl files with conversation history including 'cwd' field
|
||||
* - Project metadata stored in ~/.claude/project-config.json
|
||||
*
|
||||
* 2. **Cursor Projects** (stored in ~/.cursor/chats/)
|
||||
* - Each project directory is named with MD5 hash of the absolute project path
|
||||
* - Example: /Users/john/myproject -> MD5 -> a1b2c3d4e5f6...
|
||||
* - Contains session directories with SQLite databases (store.db)
|
||||
* - Project path is NOT stored in the database - only in the MD5 hash
|
||||
*
|
||||
* ## Project Discovery Strategy
|
||||
*
|
||||
* 1. **Claude Projects Discovery**:
|
||||
* - Scan ~/.claude/projects/ directory for Claude project folders
|
||||
* - Extract actual project path from .jsonl files (cwd field)
|
||||
* - Fall back to decoded directory name if no sessions exist
|
||||
*
|
||||
* 2. **Cursor Sessions Discovery**:
|
||||
* - For each KNOWN project (from Claude or manually added)
|
||||
* - Compute MD5 hash of the project's absolute path
|
||||
* - Check if ~/.cursor/chats/{md5_hash}/ directory exists
|
||||
* - Read session metadata from SQLite store.db files
|
||||
*
|
||||
* 3. **Manual Project Addition**:
|
||||
* - Users can manually add project paths via UI
|
||||
* - Stored in ~/.claude/project-config.json with 'manuallyAdded' flag
|
||||
* - Allows discovering Cursor sessions for projects without Claude sessions
|
||||
*
|
||||
* ## Critical Limitations
|
||||
*
|
||||
* - **CANNOT discover Cursor-only projects**: From a quick check, there was no mention of
|
||||
* the cwd of each project. if someone has the time, you can try to reverse engineer it.
|
||||
*
|
||||
* - **Project relocation breaks history**: If a project directory is moved or renamed,
|
||||
* the MD5 hash changes, making old Cursor sessions inaccessible unless the old
|
||||
* path is known and manually added.
|
||||
*
|
||||
* ## Error Handling
|
||||
*
|
||||
* - Missing ~/.claude directory is handled gracefully with automatic creation
|
||||
* - ENOENT errors are caught and handled without crashing
|
||||
* - Empty arrays returned when no projects/sessions exist
|
||||
*
|
||||
* ## Caching Strategy
|
||||
*
|
||||
* - Project directory extraction is cached to minimize file I/O
|
||||
* - Cache is cleared when project configuration changes
|
||||
* - Session data is fetched on-demand, not cached
|
||||
*/
|
||||
|
||||
import { promises as fs } from 'fs';
|
||||
import fsSync from 'fs';
|
||||
import path from 'path';
|
||||
@@ -9,12 +68,10 @@ import os from 'os';
|
||||
|
||||
// Cache for extracted project directories
|
||||
const projectDirectoryCache = new Map();
|
||||
let cacheTimestamp = Date.now();
|
||||
|
||||
// Clear cache when needed (called when project files change)
|
||||
function clearProjectDirectoryCache() {
|
||||
projectDirectoryCache.clear();
|
||||
cacheTimestamp = Date.now();
|
||||
}
|
||||
|
||||
// Load project configuration file
|
||||
@@ -31,7 +88,18 @@ async function loadProjectConfig() {
|
||||
|
||||
// Save project configuration file
|
||||
async function saveProjectConfig(config) {
|
||||
const configPath = path.join(process.env.HOME, '.claude', 'project-config.json');
|
||||
const claudeDir = path.join(process.env.HOME, '.claude');
|
||||
const configPath = path.join(claudeDir, 'project-config.json');
|
||||
|
||||
// Ensure the .claude directory exists
|
||||
try {
|
||||
await fs.mkdir(claudeDir, { recursive: true });
|
||||
} catch (error) {
|
||||
if (error.code !== 'EEXIST') {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
await fs.writeFile(configPath, JSON.stringify(config, null, 2), 'utf8');
|
||||
}
|
||||
|
||||
@@ -79,6 +147,9 @@ async function extractProjectDirectory(projectName) {
|
||||
let extractedPath;
|
||||
|
||||
try {
|
||||
// Check if the project directory exists
|
||||
await fs.access(projectDir);
|
||||
|
||||
const files = await fs.readdir(projectDir);
|
||||
const jsonlFiles = files.filter(file => file.endsWith('.jsonl'));
|
||||
|
||||
@@ -156,9 +227,14 @@ async function extractProjectDirectory(projectName) {
|
||||
return extractedPath;
|
||||
|
||||
} catch (error) {
|
||||
console.error(`Error extracting project directory for ${projectName}:`, error);
|
||||
// Fall back to decoded project name
|
||||
extractedPath = projectName.replace(/-/g, '/');
|
||||
// If the directory doesn't exist, just use the decoded project name
|
||||
if (error.code === 'ENOENT') {
|
||||
extractedPath = projectName.replace(/-/g, '/');
|
||||
} else {
|
||||
console.error(`Error extracting project directory for ${projectName}:`, error);
|
||||
// Fall back to decoded project name for other errors
|
||||
extractedPath = projectName.replace(/-/g, '/');
|
||||
}
|
||||
|
||||
// Cache the fallback result too
|
||||
projectDirectoryCache.set(projectName, extractedPath);
|
||||
@@ -174,7 +250,10 @@ async function getProjects() {
|
||||
const existingProjects = new Set();
|
||||
|
||||
try {
|
||||
// First, get existing projects from the file system
|
||||
// Check if the .claude/projects directory exists
|
||||
await fs.access(claudeDir);
|
||||
|
||||
// First, get existing Claude projects from the file system
|
||||
const entries = await fs.readdir(claudeDir, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
@@ -223,7 +302,10 @@ async function getProjects() {
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error reading projects directory:', error);
|
||||
// If the directory doesn't exist (ENOENT), that's okay - just continue with empty projects
|
||||
if (error.code !== 'ENOENT') {
|
||||
console.error('Error reading projects directory:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Add manually configured projects that don't exist as folders yet
|
||||
@@ -631,7 +713,8 @@ async function addProjectManually(projectPath, displayName = null) {
|
||||
fullPath: absolutePath,
|
||||
displayName: displayName || await generateDisplayName(projectName, absolutePath),
|
||||
isManuallyAdded: true,
|
||||
sessions: []
|
||||
sessions: [],
|
||||
cursorSessions: []
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user