@claude-vector/claude-tools
Version:
Claude integration tools for AI-powered development assistance
381 lines (323 loc) • 9.82 kB
JavaScript
/**
* Session Manager for Claude development tasks
* Enhanced with context persistence and integration
*/
import { promises as fs } from 'fs';
import path from 'path';
import crypto from 'crypto';
import os from 'os';
export class SessionManager {
constructor(config = {}) {
this.sessionsDir = config.sessionsDir || path.join(process.cwd(), '.claude-sessions');
this.currentSessionFile = path.join(os.homedir(), '.claude-session');
this.maxSessions = config.maxSessions || 100;
this.contextManager = null; // Will be set by dependency injection
this.persistentContext = new Map(); // Store context across sessions
}
/**
* Set context manager for enhanced functionality
*/
setContextManager(contextManager) {
this.contextManager = contextManager;
}
/**
* Start a new session with enhanced context
*/
async startSession(task, metadata = {}) {
const session = {
id: this.generateSessionId(),
task,
startTime: new Date().toISOString(),
activities: [],
context: [],
metadata: {
...metadata,
projectPath: process.cwd(),
environment: process.env.NODE_ENV || 'development'
},
status: 'active',
contextState: null // Will store ContextManager state
};
// Find related sessions for context
const relatedSessions = await this.findRelatedSessions(task, 3);
if (relatedSessions.length > 0) {
session.metadata.relatedSessions = relatedSessions.map(s => s.id);
}
// Initialize context if ContextManager is available
if (this.contextManager) {
this.contextManager.setTaskDescription(task);
// Add context from related sessions
await this.loadRelatedContext(relatedSessions);
session.contextState = this.contextManager.getStats();
}
// Save as current session
await this.saveCurrentSession(session);
// Save to sessions directory
await this.saveSession(session);
return session;
}
/**
* Get current session
*/
async getCurrentSession() {
try {
const data = await fs.readFile(this.currentSessionFile, 'utf-8');
return JSON.parse(data);
} catch (error) {
return null;
}
}
/**
* Add activity to current session
*/
async addActivity(type, data) {
const session = await this.getCurrentSession();
if (!session) {
throw new Error('No active session');
}
const activity = {
type,
timestamp: new Date().toISOString(),
data
};
session.activities.push(activity);
session.lastActivity = activity.timestamp;
await this.saveCurrentSession(session);
await this.saveSession(session);
return activity;
}
/**
* Complete current session
*/
async completeSession(summary = '') {
const session = await this.getCurrentSession();
if (!session) {
throw new Error('No active session');
}
session.status = 'completed';
session.endTime = new Date().toISOString();
session.summary = summary;
await this.saveSession(session);
await this.clearCurrentSession();
return session;
}
/**
* Get all sessions
*/
async getAllSessions() {
try {
await fs.mkdir(this.sessionsDir, { recursive: true });
const files = await fs.readdir(this.sessionsDir);
const sessions = [];
for (const file of files) {
if (file.endsWith('.json')) {
try {
const data = await fs.readFile(path.join(this.sessionsDir, file), 'utf-8');
sessions.push(JSON.parse(data));
} catch (error) {
// Skip invalid files
}
}
}
// Sort by start time (newest first)
sessions.sort((a, b) => new Date(b.startTime) - new Date(a.startTime));
return sessions;
} catch (error) {
return [];
}
}
/**
* Get session by ID
*/
async getSession(sessionId) {
try {
const filePath = path.join(this.sessionsDir, `${sessionId}.json`);
const data = await fs.readFile(filePath, 'utf-8');
return JSON.parse(data);
} catch (error) {
return null;
}
}
/**
* Find related sessions
*/
async findRelatedSessions(task, limit = 5) {
const sessions = await this.getAllSessions();
const taskWords = task.toLowerCase().split(/\s+/);
// Score sessions by keyword overlap
const scored = sessions.map(session => {
const sessionWords = session.task.toLowerCase().split(/\s+/);
const overlap = taskWords.filter(word => sessionWords.includes(word)).length;
return { session, score: overlap };
});
// Sort by score and return top matches
scored.sort((a, b) => b.score - a.score);
return scored
.filter(item => item.score > 0)
.slice(0, limit)
.map(item => item.session);
}
/**
* Clean up old sessions
*/
async cleanup(daysToKeep = 30) {
const sessions = await this.getAllSessions();
const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - daysToKeep);
let deleted = 0;
for (const session of sessions) {
const sessionDate = new Date(session.startTime);
if (sessionDate < cutoffDate) {
try {
await fs.unlink(path.join(this.sessionsDir, `${session.id}.json`));
deleted++;
} catch (error) {
// Ignore errors
}
}
}
return { deleted, remaining: sessions.length - deleted };
}
/**
* Private: Generate session ID
*/
generateSessionId() {
const timestamp = Date.now();
const random = crypto.randomBytes(4).toString('hex');
return `session-${timestamp}-${random}`;
}
/**
* Private: Save current session
*/
async saveCurrentSession(session) {
await fs.writeFile(this.currentSessionFile, JSON.stringify(session, null, 2));
}
/**
* Private: Save session to directory
*/
async saveSession(session) {
await fs.mkdir(this.sessionsDir, { recursive: true });
const filePath = path.join(this.sessionsDir, `${session.id}.json`);
await fs.writeFile(filePath, JSON.stringify(session, null, 2));
}
/**
* Load context from related sessions
*/
async loadRelatedContext(relatedSessions) {
if (!this.contextManager) return;
for (const session of relatedSessions) {
// Add key activities as context
const relevantActivities = session.activities
.filter(activity => activity.type === 'solution' || activity.type === 'error_fix')
.slice(-3); // Last 3 relevant activities
for (const activity of relevantActivities) {
this.contextManager.addItem({
type: 'context',
content: `Previous session "${session.task}": ${JSON.stringify(activity.data)}`,
metadata: {
sessionId: session.id,
activityType: activity.type,
timestamp: activity.timestamp
}
}, 'medium');
}
}
}
/**
* Add context item to current session
*/
async addContext(item, priority = 'medium') {
const session = await this.getCurrentSession();
if (!session) {
throw new Error('No active session');
}
// Store in session
session.context.push({
...item,
timestamp: new Date().toISOString(),
priority
});
// Add to ContextManager if available
if (this.contextManager) {
this.contextManager.addItem(item, priority);
session.contextState = this.contextManager.getStats();
}
await this.saveCurrentSession(session);
await this.saveSession(session);
return item;
}
/**
* Get formatted context for Claude
*/
async getFormattedContext() {
if (this.contextManager) {
return this.contextManager.getFormattedContext();
}
const session = await this.getCurrentSession();
if (!session || session.context.length === 0) {
return 'No context available.';
}
let formatted = `# Current Task\n${session.task}\n\n`;
formatted += '# Session Context\n\n';
for (const item of session.context) {
formatted += `## ${item.type || 'Information'}\n`;
formatted += `${item.content}\n\n`;
}
return formatted;
}
/**
* Update session with context state
*/
async updateContextState() {
const session = await this.getCurrentSession();
if (!session || !this.contextManager) return;
session.contextState = this.contextManager.getStats();
await this.saveCurrentSession(session);
await this.saveSession(session);
}
/**
* Get session statistics
*/
async getSessionStats() {
const session = await this.getCurrentSession();
if (!session) return null;
const duration = session.endTime
? new Date(session.endTime) - new Date(session.startTime)
: Date.now() - new Date(session.startTime);
return {
id: session.id,
task: session.task,
status: session.status,
duration: Math.round(duration / 1000), // seconds
activitiesCount: session.activities.length,
contextItemsCount: session.context.length,
contextState: session.contextState,
startTime: session.startTime,
lastActivity: session.lastActivity
};
}
/**
* Export session data
*/
async exportSession(sessionId = null) {
const session = sessionId
? await this.getSession(sessionId)
: await this.getCurrentSession();
if (!session) return null;
return {
...session,
exportTime: new Date().toISOString(),
formattedContext: await this.getFormattedContext()
};
}
/**
* Private: Clear current session
*/
async clearCurrentSession() {
try {
await fs.unlink(this.currentSessionFile);
} catch (error) {
// File might not exist
}
}
}