@ace-sdk/cli
Version:
ACE CLI - Command-line tool for intelligent pattern learning and playbook management
267 lines • 7.73 kB
JavaScript
/**
* 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