termcode
Version:
Superior terminal AI coding agent with enterprise-grade security, intelligent error recovery, performance monitoring, and plugin system - Advanced Claude Code alternative
349 lines (348 loc) • 12.1 kB
JavaScript
import { promises as fs } from "node:fs";
import path from "node:path";
import { log } from "../util/logging.js";
/**
* Enhanced workspace management inspired by Claude Code
* Provides project-aware context and persistent settings
*/
export class WorkspaceManager {
configDir;
workspacesFile;
sessionsFile;
workspaces = new Map();
currentWorkspace;
constructor() {
this.configDir = path.join(process.env.HOME || "~", ".termcode", "workspaces");
this.workspacesFile = path.join(this.configDir, "workspaces.json");
this.sessionsFile = path.join(this.configDir, "sessions.json");
}
/**
* Initialize workspace manager
*/
async initialize() {
try {
await fs.mkdir(this.configDir, { recursive: true });
await this.loadWorkspaces();
}
catch (error) {
log.warn("Failed to initialize workspace manager:", error);
}
}
/**
* Load or create workspace for a project path
*/
async loadWorkspace(projectPath, projectInfo) {
const normalizedPath = path.resolve(projectPath);
const existing = this.workspaces.get(normalizedPath);
if (existing) {
existing.lastUsed = new Date().toISOString();
this.currentWorkspace = existing;
await this.saveWorkspaces();
return existing;
}
// Create new workspace
const workspace = {
name: path.basename(normalizedPath),
path: normalizedPath,
type: projectInfo.type,
framework: projectInfo.framework,
lastUsed: new Date().toISOString(),
preferences: {
defaultProvider: "openai",
defaultModel: "gpt-4o-mini",
enabledTools: ["git", "test", "lint", "build", "shell"],
theme: "claude"
},
contexts: projectInfo.contexts || [],
bookmarks: []
};
// Auto-detect better defaults based on project
this.optimizeWorkspaceDefaults(workspace, projectInfo);
this.workspaces.set(normalizedPath, workspace);
this.currentWorkspace = workspace;
await this.saveWorkspaces();
log.info(`Created workspace: ${workspace.name} (${workspace.type})`);
return workspace;
}
/**
* Get current workspace
*/
getCurrentWorkspace() {
return this.currentWorkspace;
}
/**
* List recent workspaces
*/
getRecentWorkspaces(limit = 10) {
return Array.from(this.workspaces.values())
.sort((a, b) => new Date(b.lastUsed).getTime() - new Date(a.lastUsed).getTime())
.slice(0, limit);
}
/**
* Update workspace preferences
*/
async updateWorkspacePreferences(workspacePath, preferences) {
const workspace = this.workspaces.get(path.resolve(workspacePath));
if (workspace) {
workspace.preferences = { ...workspace.preferences, ...preferences };
await this.saveWorkspaces();
log.info("Workspace preferences updated");
}
}
/**
* Add bookmark to workspace
*/
async addBookmark(workspacePath, bookmark) {
const workspace = this.workspaces.get(path.resolve(workspacePath));
if (workspace && !workspace.bookmarks.includes(bookmark)) {
workspace.bookmarks.push(bookmark);
await this.saveWorkspaces();
log.info(`Bookmark added: ${bookmark}`);
}
}
/**
* Remove bookmark from workspace
*/
async removeBookmark(workspacePath, bookmark) {
const workspace = this.workspaces.get(path.resolve(workspacePath));
if (workspace) {
workspace.bookmarks = workspace.bookmarks.filter(b => b !== bookmark);
await this.saveWorkspaces();
log.info(`Bookmark removed: ${bookmark}`);
}
}
/**
* Create a new session for workspace
*/
async createSession(workspacePath, provider, model, branchName) {
const session = {
id: `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
workspacePath: path.resolve(workspacePath),
startedAt: new Date().toISOString(),
provider,
model,
branchName,
tasks: [],
totalTokens: 0,
totalCost: 0
};
await this.saveSession(session);
return session;
}
/**
* Update session with task completion
*/
async updateSession(sessionId, task, success, filesModified = [], tokens = 0, cost = 0) {
try {
const sessions = await this.loadSessions();
const sessionIndex = sessions.findIndex(s => s.id === sessionId);
if (sessionIndex >= 0) {
const session = sessions[sessionIndex];
session.tasks.push({
task,
timestamp: new Date().toISOString(),
success,
filesModified
});
session.totalTokens += tokens;
session.totalCost += cost;
await this.saveSessions(sessions);
}
}
catch (error) {
log.warn("Failed to update session:", error);
}
}
/**
* End a session
*/
async endSession(sessionId) {
try {
const sessions = await this.loadSessions();
const sessionIndex = sessions.findIndex(s => s.id === sessionId);
if (sessionIndex >= 0) {
sessions[sessionIndex].endedAt = new Date().toISOString();
await this.saveSessions(sessions);
}
}
catch (error) {
log.warn("Failed to end session:", error);
}
}
/**
* Get session history for workspace
*/
async getWorkspaceHistory(workspacePath, limit = 20) {
try {
const sessions = await this.loadSessions();
return sessions
.filter(s => s.workspacePath === path.resolve(workspacePath))
.sort((a, b) => new Date(b.startedAt).getTime() - new Date(a.startedAt).getTime())
.slice(0, limit);
}
catch (error) {
log.warn("Failed to get workspace history:", error);
return [];
}
}
/**
* Get workspace analytics
*/
async getWorkspaceAnalytics(workspacePath) {
const sessions = await this.getWorkspaceHistory(workspacePath, 1000);
const totalSessions = sessions.length;
const totalTasks = sessions.reduce((sum, s) => sum + s.tasks.length, 0);
const successfulTasks = sessions.reduce((sum, s) => sum + s.tasks.filter(t => t.success).length, 0);
const successRate = totalTasks > 0 ? (successfulTasks / totalTasks) * 100 : 0;
const providerCounts = sessions.reduce((counts, s) => {
counts[s.provider] = (counts[s.provider] || 0) + 1;
return counts;
}, {});
const mostUsedProvider = Object.entries(providerCounts)
.sort(([, a], [, b]) => b - a)[0]?.[0] || "none";
const totalCost = sessions.reduce((sum, s) => sum + s.totalCost, 0);
const totalTokens = sessions.reduce((sum, s) => sum + s.totalTokens, 0);
const sessionLengths = sessions
.filter(s => s.endedAt)
.map(s => new Date(s.endedAt).getTime() - new Date(s.startedAt).getTime());
const averageSessionLength = sessionLengths.length > 0 ?
sessionLengths.reduce((sum, length) => sum + length, 0) / sessionLengths.length / 1000 / 60 : 0;
return {
totalSessions,
totalTasks,
successRate,
mostUsedProvider,
totalCost,
totalTokens,
averageSessionLength
};
}
/**
* Clean up old sessions
*/
async cleanupOldSessions(maxAge = 90 * 24 * 60 * 60 * 1000) {
try {
const sessions = await this.loadSessions();
const cutoffDate = new Date(Date.now() - maxAge);
const activeSessions = sessions.filter(s => new Date(s.startedAt) > cutoffDate);
const removed = sessions.length - activeSessions.length;
if (removed > 0) {
await this.saveSessions(activeSessions);
log.info(`Cleaned up ${removed} old sessions`);
}
}
catch (error) {
log.warn("Failed to cleanup old sessions:", error);
}
}
/**
* Export workspace data
*/
async exportWorkspace(workspacePath) {
const workspace = this.workspaces.get(path.resolve(workspacePath));
const sessions = await this.getWorkspaceHistory(workspacePath);
const analytics = await this.getWorkspaceAnalytics(workspacePath);
return JSON.stringify({
workspace,
sessions,
analytics,
exportedAt: new Date().toISOString()
}, null, 2);
}
/**
* Optimize workspace defaults based on project type
*/
optimizeWorkspaceDefaults(workspace, projectInfo) {
// Optimize based on project type
switch (projectInfo.type) {
case "typescript":
case "javascript":
if (projectInfo.framework === "react") {
workspace.preferences.defaultProvider = "anthropic"; // Good for React
}
break;
case "python":
workspace.preferences.defaultProvider = "openai"; // GPT-4 good for Python
workspace.preferences.defaultModel = "gpt-4o";
break;
case "rust":
case "go":
workspace.preferences.defaultProvider = "anthropic"; // Claude good for systems
break;
}
// Add relevant contexts
if (projectInfo.hasTests) {
workspace.contexts.push("Test-driven development");
}
if (projectInfo.framework) {
workspace.contexts.push(`${projectInfo.framework} best practices`);
}
}
/**
* Load workspaces from disk
*/
async loadWorkspaces() {
try {
const data = await fs.readFile(this.workspacesFile, "utf8");
const workspaces = JSON.parse(data);
this.workspaces.clear();
for (const workspace of workspaces) {
this.workspaces.set(workspace.path, workspace);
}
}
catch (error) {
// File doesn't exist or invalid JSON, start fresh
this.workspaces.clear();
}
}
/**
* Save workspaces to disk
*/
async saveWorkspaces() {
try {
const workspaces = Array.from(this.workspaces.values());
await fs.writeFile(this.workspacesFile, JSON.stringify(workspaces, null, 2));
}
catch (error) {
log.warn("Failed to save workspaces:", error);
}
}
/**
* Load sessions from disk
*/
async loadSessions() {
try {
const data = await fs.readFile(this.sessionsFile, "utf8");
return JSON.parse(data);
}
catch (error) {
return [];
}
}
/**
* Save sessions to disk
*/
async saveSessions(sessions) {
try {
await fs.writeFile(this.sessionsFile, JSON.stringify(sessions, null, 2));
}
catch (error) {
log.warn("Failed to save sessions:", error);
}
}
/**
* Save single session
*/
async saveSession(session) {
try {
const sessions = await this.loadSessions();
sessions.push(session);
await this.saveSessions(sessions);
}
catch (error) {
log.warn("Failed to save session:", error);
}
}
}
// Export singleton instance
export const workspaceManager = new WorkspaceManager();