UNPKG

termcode

Version:

Superior terminal AI coding agent with enterprise-grade security, intelligent error recovery, performance monitoring, and plugin system - Advanced Claude Code alternative

159 lines (158 loc) 5.42 kB
import { promises as fs } from "node:fs"; import { createHash } from "node:crypto"; import os from "node:os"; import path from "node:path"; import { z } from "zod"; const sessionDir = path.join(os.homedir(), ".termcode", "sessions"); export const SessionStateSchema = z.object({ repoPath: z.string(), repoHash: z.string(), lastUsed: z.string(), provider: z.string(), model: z.string(), branchName: z.string().optional(), recentTasks: z.array(z.string()).max(10), // Last 10 tasks lastFilesChanged: z.array(z.string()).max(20), // Last 20 files lastDiff: z.string().optional(), totalTokensUsed: z.number().default(0), totalCostUSD: z.number().default(0), sessionStartTime: z.string(), }); function getRepoHash(repoPath) { const normalized = path.resolve(repoPath); return createHash('sha256').update(normalized).digest('hex').substring(0, 16); } function getSessionPath(repoPath) { const hash = getRepoHash(repoPath); return path.join(sessionDir, `${hash}.json`); } async function ensureSessionDir() { await fs.mkdir(sessionDir, { recursive: true }); } export async function loadSession(repoPath) { try { await ensureSessionDir(); const sessionPath = getSessionPath(repoPath); const content = await fs.readFile(sessionPath, "utf8"); const data = JSON.parse(content); return SessionStateSchema.parse(data); } catch (error) { return null; } } export async function saveSession(session) { try { await ensureSessionDir(); const sessionPath = getSessionPath(session.repoPath); const validated = SessionStateSchema.parse(session); await fs.writeFile(sessionPath, JSON.stringify(validated, null, 2), "utf8"); } catch (error) { console.error("Failed to save session:", error); } } export async function createSession(repoPath, provider, model, branchName) { const session = { repoPath: path.resolve(repoPath), repoHash: getRepoHash(repoPath), lastUsed: new Date().toISOString(), provider, model, branchName, recentTasks: [], lastFilesChanged: [], totalTokensUsed: 0, totalCostUSD: 0, sessionStartTime: new Date().toISOString(), }; await saveSession(session); return session; } export async function updateSession(repoPath, updates) { const existing = await loadSession(repoPath); if (!existing) return null; const updated = { ...existing, ...updates, lastUsed: new Date().toISOString(), }; await saveSession(updated); return updated; } export async function addTaskToSession(repoPath, task, filesChanged = []) { const session = await loadSession(repoPath); if (!session) return; // Add task to recent tasks (keep only last 10) session.recentTasks = [task, ...session.recentTasks.filter(t => t !== task)].slice(0, 10); // Add files to recently changed (keep only last 20) const newFiles = filesChanged.filter(f => !session.lastFilesChanged.includes(f)); session.lastFilesChanged = [...newFiles, ...session.lastFilesChanged].slice(0, 20); await saveSession(session); } export async function addUsageToSession(repoPath, tokens, cost) { const session = await loadSession(repoPath); if (!session) return; session.totalTokensUsed += tokens; session.totalCostUSD += cost; await saveSession(session); } export async function listRecentSessions(limit = 5) { try { await ensureSessionDir(); const files = await fs.readdir(sessionDir); const sessions = []; for (const file of files.filter(f => f.endsWith('.json'))) { try { const content = await fs.readFile(path.join(sessionDir, file), "utf8"); const session = SessionStateSchema.parse(JSON.parse(content)); sessions.push(session); } catch (error) { // Skip invalid session files continue; } } // Sort by lastUsed and return most recent return sessions .sort((a, b) => new Date(b.lastUsed).getTime() - new Date(a.lastUsed).getTime()) .slice(0, limit); } catch (error) { return []; } } export async function cleanupOldSessions(maxAge = 30 * 24 * 60 * 60 * 1000) { try { await ensureSessionDir(); const files = await fs.readdir(sessionDir); const cutoff = new Date(Date.now() - maxAge); let cleaned = 0; for (const file of files.filter(f => f.endsWith('.json'))) { try { const filePath = path.join(sessionDir, file); const content = await fs.readFile(filePath, "utf8"); const session = SessionStateSchema.parse(JSON.parse(content)); if (new Date(session.lastUsed) < cutoff) { await fs.unlink(filePath); cleaned++; } } catch (error) { // Delete corrupted files try { await fs.unlink(path.join(sessionDir, file)); cleaned++; } catch { } } } return cleaned; } catch (error) { return 0; } }