feat: add smart project suggestions endpoint
Added GET /api/projects/suggestions endpoint that provides intelligent project suggestions based on session context. The endpoint: - Takes sessionId as a required query parameter - Retrieves session from in-memory or historical sessions - Calculates scores for each project using multiple criteria: * Directory match (90 points): session workingDir === project path * Subdirectory match (50 points): session workingDir starts with project path * Used today (20 points): project lastActivity < 1 day ago * Used this week (10 points): project lastActivity < 7 days ago * Name similarity (15 points): overlap between session dir name and project name - Returns top 3 scored suggestions with reasons - Also returns all projects sorted alphabetically - Filters out projects with zero scores from suggestions - Handles missing sessions with appropriate error responses Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
114
server.js
114
server.js
@@ -1199,6 +1199,120 @@ app.get('/api/recycle-bin', requireAuth, (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/projects/suggestions - Get smart project suggestions for a session
|
||||
app.get('/api/projects/suggestions', requireAuth, (req, res) => {
|
||||
try {
|
||||
const { sessionId } = req.query;
|
||||
|
||||
// Validate sessionId parameter
|
||||
if (!sessionId) {
|
||||
return res.status(400).json({ error: 'sessionId parameter is required' });
|
||||
}
|
||||
|
||||
// Get the session from in-memory or historical
|
||||
let session = claudeService.sessions.get(sessionId);
|
||||
if (!session) {
|
||||
// Try historical sessions
|
||||
const historicalSessions = claudeService.loadHistoricalSessions();
|
||||
session = historicalSessions.find(s => s.id === sessionId);
|
||||
}
|
||||
|
||||
if (!session) {
|
||||
return res.status(404).json({ error: 'Session not found' });
|
||||
}
|
||||
|
||||
// Get all active projects
|
||||
const projects = db.prepare(`
|
||||
SELECT id, name, icon, color, path, lastActivity
|
||||
FROM projects
|
||||
WHERE deletedAt IS NULL
|
||||
`).all();
|
||||
|
||||
const sessionWorkingDir = session.workingDir || '';
|
||||
const now = new Date();
|
||||
|
||||
// Calculate scores for each project
|
||||
const scoredProjects = projects.map(project => {
|
||||
let score = 0;
|
||||
const reasons = [];
|
||||
|
||||
// 1. Directory match: session workingDir === project path (90 points)
|
||||
if (sessionWorkingDir === project.path) {
|
||||
score += 90;
|
||||
reasons.push('Same directory');
|
||||
}
|
||||
|
||||
// 2. Subdirectory: session workingDir startsWith project path (50 points)
|
||||
// Only if not already an exact match
|
||||
else if (sessionWorkingDir.startsWith(project.path + '/')) {
|
||||
score += 50;
|
||||
reasons.push('Subdirectory');
|
||||
}
|
||||
|
||||
// 3. Recent use: project used today (<1 day ago) (20 points)
|
||||
if (project.lastActivity) {
|
||||
const lastActivityDate = new Date(project.lastActivity);
|
||||
const daysSinceActivity = Math.floor((now - lastActivityDate) / (1000 * 60 * 60 * 24));
|
||||
|
||||
if (daysSinceActivity < 1) {
|
||||
score += 20;
|
||||
reasons.push('Used today');
|
||||
}
|
||||
// 4. Week-old use: project used this week (<7 days ago) (10 points)
|
||||
else if (daysSinceActivity < 7) {
|
||||
score += 10;
|
||||
reasons.push(`Used ${daysSinceActivity} days ago`);
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Name overlap: check if session workingDir contains project name or vice versa (15 points)
|
||||
const sessionDirName = path.basename(sessionWorkingDir).toLowerCase();
|
||||
const projectNameLower = project.name.toLowerCase();
|
||||
|
||||
if (sessionDirName.includes(projectNameLower) || projectNameLower.includes(sessionDirName)) {
|
||||
score += 15;
|
||||
reasons.push('Similar name');
|
||||
}
|
||||
|
||||
return {
|
||||
id: project.id,
|
||||
name: project.name,
|
||||
icon: project.icon,
|
||||
color: project.color,
|
||||
score,
|
||||
reasons
|
||||
};
|
||||
});
|
||||
|
||||
// Sort by score descending
|
||||
scoredProjects.sort((a, b) => b.score - a.score);
|
||||
|
||||
// Get top 3 suggestions
|
||||
const suggestions = scoredProjects
|
||||
.filter(p => p.score > 0) // Only include projects with positive scores
|
||||
.slice(0, 3);
|
||||
|
||||
// Get all projects sorted by name
|
||||
const allProjects = projects
|
||||
.map(p => ({
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
icon: p.icon,
|
||||
color: p.color
|
||||
}))
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
suggestions,
|
||||
allProjects
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error getting project suggestions:', error);
|
||||
res.status(500).json({ error: 'Failed to get project suggestions' });
|
||||
}
|
||||
});
|
||||
|
||||
// ===== Terminal API Endpoints =====
|
||||
|
||||
// Create a new terminal
|
||||
|
||||
Reference in New Issue
Block a user