UNPKG

@ace-sdk/cli

Version:

ACE CLI - Command-line tool for intelligent pattern learning and playbook management

267 lines 7.73 kB
/** * Session recording service */ import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync } from 'fs'; import { homedir } from 'os'; import { join } from 'path'; /** * Session recorder for capturing CLI interactions * * Records commands, HTTP requests/responses, logs, and errors * to ~/.ace/sessions/ for later analysis and summarization. */ export class SessionRecorder { sessionsDir; stateFile; state; constructor() { this.sessionsDir = join(homedir(), '.ace', 'sessions'); this.stateFile = join(homedir(), '.ace', 'recorder-state.json'); // Ensure sessions directory exists if (!existsSync(this.sessionsDir)) { mkdirSync(this.sessionsDir, { recursive: true }); } // Load state this.state = this.loadState(); } /** * Load recorder state from disk */ loadState() { if (!existsSync(this.stateFile)) { return { active: false, sessionId: null, startTime: null, eventCount: 0 }; } try { const data = JSON.parse(readFileSync(this.stateFile, 'utf8')); return { active: data.active || false, sessionId: data.sessionId || null, startTime: data.startTime ? new Date(data.startTime) : null, eventCount: data.eventCount || 0 }; } catch { return { active: false, sessionId: null, startTime: null, eventCount: 0 }; } } /** * Save recorder state to disk */ saveState() { const stateDir = join(homedir(), '.ace'); if (!existsSync(stateDir)) { mkdirSync(stateDir, { recursive: true }); } writeFileSync(this.stateFile, JSON.stringify({ active: this.state.active, sessionId: this.state.sessionId, startTime: this.state.startTime?.toISOString() || null, eventCount: this.state.eventCount }, null, 2)); } /** * Get current recorder state */ getState() { return { ...this.state }; } /** * Check if recorder is active */ isActive() { return this.state.active; } /** * Start a new recording session */ start(command, projectId) { if (this.state.active) { throw new Error(`Recording already active (session: ${this.state.sessionId})`); } // Generate session ID: YYYYMMDD-HHMMSS const now = new Date(); const sessionId = now.toISOString() .replace(/[:.]/g, '-') .replace('T', '-') .split('-') .slice(0, 6) .join('') .slice(0, 15); // YYYYMMDDHHMMSS // Create session metadata const metadata = { id: sessionId, startTime: now.toISOString(), command, projectId, events: 0 }; // Initialize session file const recording = { metadata, events: [] }; this.writeSession(sessionId, recording); // Update state this.state = { active: true, sessionId, startTime: now, eventCount: 0 }; this.saveState(); // Record session start event this.recordEvent('metadata', { action: 'session_start', command, projectId }); return { sessionId, message: `Recording started (session: ${sessionId})` }; } /** * Stop the current recording session */ stop() { if (!this.state.active) { throw new Error('No active recording session'); } const sessionId = this.state.sessionId; const startTime = this.state.startTime; const eventCount = this.state.eventCount; const duration = Date.now() - startTime.getTime(); // Record session end event this.recordEvent('metadata', { action: 'session_end', duration_ms: duration, event_count: eventCount }); // Update session metadata const recording = this.readSession(sessionId); recording.metadata.endTime = new Date().toISOString(); recording.metadata.events = this.state.eventCount; this.writeSession(sessionId, recording); // Reset state this.state = { active: false, sessionId: null, startTime: null, eventCount: 0 }; this.saveState(); return { sessionId, events: eventCount, duration }; } /** * Record an event to the current session */ recordEvent(type, data, duration) { if (!this.state.active) { return; // Silently ignore if not recording } const event = { timestamp: new Date().toISOString(), type, data, duration }; // Append event to session file const sessionId = this.state.sessionId; const recording = this.readSession(sessionId); recording.events.push(event); this.writeSession(sessionId, recording); // Update state this.state.eventCount++; this.saveState(); } /** * List all recorded sessions */ listSessions() { if (!existsSync(this.sessionsDir)) { return []; } const files = readdirSync(this.sessionsDir) .filter(f => f.endsWith('.json')) .sort() .reverse(); // Most recent first return files.map(file => { try { const recording = this.readSession(file.replace('.json', '')); return recording.metadata; } catch { return null; } }).filter(Boolean); } /** * Get a specific session recording */ getSession(sessionId) { return this.readSession(sessionId); } /** * Export a session to a specific file */ exportSession(sessionId, outputPath) { const recording = this.readSession(sessionId); writeFileSync(outputPath, JSON.stringify(recording, null, 2)); } /** * Delete a session recording */ deleteSession(sessionId) { const sessionFile = join(this.sessionsDir, `${sessionId}.json`); if (existsSync(sessionFile)) { const fs = require('fs'); fs.unlinkSync(sessionFile); } } /** * Read a session recording from disk */ readSession(sessionId) { const sessionFile = join(this.sessionsDir, `${sessionId}.json`); if (!existsSync(sessionFile)) { throw new Error(`Session not found: ${sessionId}`); } const data = readFileSync(sessionFile, 'utf8'); return JSON.parse(data); } /** * Write a session recording to disk */ writeSession(sessionId, recording) { const sessionFile = join(this.sessionsDir, `${sessionId}.json`); writeFileSync(sessionFile, JSON.stringify(recording, null, 2)); } } /** * Singleton recorder instance */ let recorderInstance = null; /** * Get the global recorder instance */ export function getRecorder() { if (!recorderInstance) { recorderInstance = new SessionRecorder(); } return recorderInstance; } //# sourceMappingURL=recorder.js.map