175 lines
4.3 KiB
TypeScript
175 lines
4.3 KiB
TypeScript
/**
|
|
* Session management utilities
|
|
*/
|
|
|
|
import { writeFileSync, existsSync, readFileSync } from 'node:fs';
|
|
import { join } from 'node:path';
|
|
import type { AgentResponse, WorkflowState } from '../models/types.js';
|
|
import { getProjectLogsDir, getGlobalLogsDir, ensureDir } from '../config/paths.js';
|
|
|
|
/** Session log entry */
|
|
export interface SessionLog {
|
|
task: string;
|
|
projectDir: string;
|
|
workflowName: string;
|
|
iterations: number;
|
|
startTime: string;
|
|
endTime?: string;
|
|
status: 'running' | 'completed' | 'aborted';
|
|
history: Array<{
|
|
step: string;
|
|
agent: string;
|
|
status: string;
|
|
timestamp: string;
|
|
content: string;
|
|
error?: string;
|
|
}>;
|
|
}
|
|
|
|
/** Generate a session ID */
|
|
export function generateSessionId(): string {
|
|
const timestamp = Date.now().toString(36);
|
|
const random = Math.random().toString(36).slice(2, 8);
|
|
return `${timestamp}-${random}`;
|
|
}
|
|
|
|
/**
|
|
* Generate report directory name from task and timestamp.
|
|
* Format: YYYYMMDD-HHMMSS-task-summary
|
|
*/
|
|
export function generateReportDir(task: string): string {
|
|
const now = new Date();
|
|
const timestamp = now.toISOString()
|
|
.replace(/[-:T]/g, '')
|
|
.slice(0, 14)
|
|
.replace(/(\d{8})(\d{6})/, '$1-$2');
|
|
|
|
// Extract first 30 chars of task, sanitize for directory name
|
|
const summary = task
|
|
.slice(0, 30)
|
|
.toLowerCase()
|
|
.replace(/[^a-z0-9\u3040-\u309f\u30a0-\u30ff\u4e00-\u9faf]+/g, '-')
|
|
.replace(/^-+|-+$/g, '')
|
|
|| 'task';
|
|
|
|
return `${timestamp}-${summary}`;
|
|
}
|
|
|
|
/** Create a new session log */
|
|
export function createSessionLog(
|
|
task: string,
|
|
projectDir: string,
|
|
workflowName: string
|
|
): SessionLog {
|
|
return {
|
|
task,
|
|
projectDir,
|
|
workflowName,
|
|
iterations: 0,
|
|
startTime: new Date().toISOString(),
|
|
status: 'running',
|
|
history: [],
|
|
};
|
|
}
|
|
|
|
/** Add agent response to session log */
|
|
export function addToSessionLog(
|
|
log: SessionLog,
|
|
stepName: string,
|
|
response: AgentResponse
|
|
): void {
|
|
log.history.push({
|
|
step: stepName,
|
|
agent: response.agent,
|
|
status: response.status,
|
|
timestamp: response.timestamp.toISOString(),
|
|
content: response.content,
|
|
...(response.error ? { error: response.error } : {}),
|
|
});
|
|
log.iterations++;
|
|
}
|
|
|
|
/** Finalize session log */
|
|
export function finalizeSessionLog(
|
|
log: SessionLog,
|
|
status: 'completed' | 'aborted'
|
|
): void {
|
|
log.status = status;
|
|
log.endTime = new Date().toISOString();
|
|
}
|
|
|
|
/** Save session log to file */
|
|
export function saveSessionLog(
|
|
log: SessionLog,
|
|
sessionId: string,
|
|
projectDir?: string
|
|
): string {
|
|
const logsDir = projectDir
|
|
? getProjectLogsDir(projectDir)
|
|
: getGlobalLogsDir();
|
|
ensureDir(logsDir);
|
|
|
|
const filename = `${sessionId}.json`;
|
|
const filepath = join(logsDir, filename);
|
|
|
|
writeFileSync(filepath, JSON.stringify(log, null, 2), 'utf-8');
|
|
return filepath;
|
|
}
|
|
|
|
/** Load session log from file */
|
|
export function loadSessionLog(filepath: string): SessionLog | null {
|
|
if (!existsSync(filepath)) {
|
|
return null;
|
|
}
|
|
const content = readFileSync(filepath, 'utf-8');
|
|
return JSON.parse(content) as SessionLog;
|
|
}
|
|
|
|
/** Load project context (CLAUDE.md files) */
|
|
export function loadProjectContext(projectDir: string): string {
|
|
const contextParts: string[] = [];
|
|
|
|
// Check project root CLAUDE.md
|
|
const rootClaudeMd = join(projectDir, 'CLAUDE.md');
|
|
if (existsSync(rootClaudeMd)) {
|
|
contextParts.push(readFileSync(rootClaudeMd, 'utf-8'));
|
|
}
|
|
|
|
// Check .claude/CLAUDE.md
|
|
const dotClaudeMd = join(projectDir, '.claude', 'CLAUDE.md');
|
|
if (existsSync(dotClaudeMd)) {
|
|
contextParts.push(readFileSync(dotClaudeMd, 'utf-8'));
|
|
}
|
|
|
|
return contextParts.join('\n\n---\n\n');
|
|
}
|
|
|
|
/** Convert workflow state to session log */
|
|
export function workflowStateToSessionLog(
|
|
state: WorkflowState,
|
|
task: string,
|
|
projectDir: string
|
|
): SessionLog {
|
|
const log: SessionLog = {
|
|
task,
|
|
projectDir,
|
|
workflowName: state.workflowName,
|
|
iterations: state.iteration,
|
|
startTime: new Date().toISOString(),
|
|
status: state.status === 'running' ? 'running' : state.status === 'completed' ? 'completed' : 'aborted',
|
|
history: [],
|
|
};
|
|
|
|
for (const [stepName, response] of state.stepOutputs) {
|
|
log.history.push({
|
|
step: stepName,
|
|
agent: response.agent,
|
|
status: response.status,
|
|
timestamp: response.timestamp.toISOString(),
|
|
content: response.content,
|
|
});
|
|
}
|
|
|
|
return log;
|
|
}
|