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
JavaScript
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;
}
}