chore: update project management and API integration

- Added new scripts for server development and full application startup in package.json.
- Enhanced project management by checking for existing projects to avoid duplicates.
- Improved API integration with better error handling and connection checks in the Electron API.
- Updated UI components to reflect changes in project and session management.
- Refactored authentication status display to include more detailed information on methods used.
This commit is contained in:
SuperComboGamer
2025-12-12 00:23:43 -05:00
parent 02a1af3314
commit 4b9bd2641f
44 changed files with 8287 additions and 703 deletions

View File

@@ -0,0 +1,132 @@
/**
* Agent routes - HTTP API for Claude agent interactions
*/
import { Router, type Request, type Response } from "express";
import { AgentService } from "../services/agent-service.js";
import type { EventEmitter } from "../lib/events.js";
export function createAgentRoutes(
agentService: AgentService,
_events: EventEmitter
): Router {
const router = Router();
// Start a conversation
router.post("/start", async (req: Request, res: Response) => {
try {
const { sessionId, workingDirectory } = req.body as {
sessionId: string;
workingDirectory?: string;
};
if (!sessionId) {
res.status(400).json({ success: false, error: "sessionId is required" });
return;
}
const result = await agentService.startConversation({
sessionId,
workingDirectory,
});
res.json(result);
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
res.status(500).json({ success: false, error: message });
}
});
// Send a message
router.post("/send", async (req: Request, res: Response) => {
try {
const { sessionId, message, workingDirectory, imagePaths } = req.body as {
sessionId: string;
message: string;
workingDirectory?: string;
imagePaths?: string[];
};
if (!sessionId || !message) {
res
.status(400)
.json({ success: false, error: "sessionId and message are required" });
return;
}
// Start the message processing (don't await - it streams via WebSocket)
agentService
.sendMessage({
sessionId,
message,
workingDirectory,
imagePaths,
})
.catch((error) => {
console.error("[Agent Route] Error sending message:", error);
});
// Return immediately - responses come via WebSocket
res.json({ success: true, message: "Message sent" });
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
res.status(500).json({ success: false, error: message });
}
});
// Get conversation history
router.post("/history", async (req: Request, res: Response) => {
try {
const { sessionId } = req.body as { sessionId: string };
if (!sessionId) {
res.status(400).json({ success: false, error: "sessionId is required" });
return;
}
const result = agentService.getHistory(sessionId);
res.json(result);
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
res.status(500).json({ success: false, error: message });
}
});
// Stop execution
router.post("/stop", async (req: Request, res: Response) => {
try {
const { sessionId } = req.body as { sessionId: string };
if (!sessionId) {
res.status(400).json({ success: false, error: "sessionId is required" });
return;
}
const result = await agentService.stopExecution(sessionId);
res.json(result);
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
res.status(500).json({ success: false, error: message });
}
});
// Clear conversation
router.post("/clear", async (req: Request, res: Response) => {
try {
const { sessionId } = req.body as { sessionId: string };
if (!sessionId) {
res.status(400).json({ success: false, error: "sessionId is required" });
return;
}
const result = await agentService.clearSession(sessionId);
res.json(result);
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
res.status(500).json({ success: false, error: message });
}
});
return router;
}

View File

@@ -0,0 +1,263 @@
/**
* Auto Mode routes - HTTP API for autonomous feature implementation
*
* Uses the AutoModeService for real feature execution with Claude Agent SDK
*/
import { Router, type Request, type Response } from "express";
import type { EventEmitter } from "../lib/events.js";
import { AutoModeService } from "../services/auto-mode-service.js";
export function createAutoModeRoutes(events: EventEmitter): Router {
const router = Router();
const autoModeService = new AutoModeService(events);
// Start auto mode loop
router.post("/start", async (req: Request, res: Response) => {
try {
const { projectPath, maxConcurrency } = req.body as {
projectPath: string;
maxConcurrency?: number;
};
if (!projectPath) {
res.status(400).json({ success: false, error: "projectPath is required" });
return;
}
await autoModeService.startAutoLoop(projectPath, maxConcurrency || 3);
res.json({ success: true });
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
res.status(500).json({ success: false, error: message });
}
});
// Stop auto mode loop
router.post("/stop", async (req: Request, res: Response) => {
try {
const runningCount = await autoModeService.stopAutoLoop();
res.json({ success: true, runningFeatures: runningCount });
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
res.status(500).json({ success: false, error: message });
}
});
// Stop a specific feature
router.post("/stop-feature", async (req: Request, res: Response) => {
try {
const { featureId } = req.body as { featureId: string };
if (!featureId) {
res.status(400).json({ success: false, error: "featureId is required" });
return;
}
const stopped = await autoModeService.stopFeature(featureId);
res.json({ success: true, stopped });
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
res.status(500).json({ success: false, error: message });
}
});
// Get auto mode status
router.post("/status", async (req: Request, res: Response) => {
try {
const status = autoModeService.getStatus();
res.json({
success: true,
...status,
});
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
res.status(500).json({ success: false, error: message });
}
});
// Run a single feature
router.post("/run-feature", async (req: Request, res: Response) => {
try {
const { projectPath, featureId, useWorktrees } = req.body as {
projectPath: string;
featureId: string;
useWorktrees?: boolean;
};
if (!projectPath || !featureId) {
res
.status(400)
.json({ success: false, error: "projectPath and featureId are required" });
return;
}
// Start execution in background
autoModeService
.executeFeature(projectPath, featureId, useWorktrees ?? true, false)
.catch((error) => {
console.error(`[AutoMode] Feature ${featureId} error:`, error);
});
res.json({ success: true });
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
res.status(500).json({ success: false, error: message });
}
});
// Verify a feature
router.post("/verify-feature", async (req: Request, res: Response) => {
try {
const { projectPath, featureId } = req.body as {
projectPath: string;
featureId: string;
};
if (!projectPath || !featureId) {
res
.status(400)
.json({ success: false, error: "projectPath and featureId are required" });
return;
}
const passes = await autoModeService.verifyFeature(projectPath, featureId);
res.json({ success: true, passes });
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
res.status(500).json({ success: false, error: message });
}
});
// Resume a feature
router.post("/resume-feature", async (req: Request, res: Response) => {
try {
const { projectPath, featureId, useWorktrees } = req.body as {
projectPath: string;
featureId: string;
useWorktrees?: boolean;
};
if (!projectPath || !featureId) {
res
.status(400)
.json({ success: false, error: "projectPath and featureId are required" });
return;
}
// Start resume in background
autoModeService
.resumeFeature(projectPath, featureId, useWorktrees ?? true)
.catch((error) => {
console.error(`[AutoMode] Resume feature ${featureId} error:`, error);
});
res.json({ success: true });
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
res.status(500).json({ success: false, error: message });
}
});
// Check if context exists for a feature
router.post("/context-exists", async (req: Request, res: Response) => {
try {
const { projectPath, featureId } = req.body as {
projectPath: string;
featureId: string;
};
if (!projectPath || !featureId) {
res
.status(400)
.json({ success: false, error: "projectPath and featureId are required" });
return;
}
const exists = await autoModeService.contextExists(projectPath, featureId);
res.json({ success: true, exists });
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
res.status(500).json({ success: false, error: message });
}
});
// Analyze project
router.post("/analyze-project", async (req: Request, res: Response) => {
try {
const { projectPath } = req.body as { projectPath: string };
if (!projectPath) {
res.status(400).json({ success: false, error: "projectPath is required" });
return;
}
// Start analysis in background
autoModeService.analyzeProject(projectPath).catch((error) => {
console.error(`[AutoMode] Project analysis error:`, error);
});
res.json({ success: true, message: "Project analysis started" });
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
res.status(500).json({ success: false, error: message });
}
});
// Follow up on a feature
router.post("/follow-up-feature", async (req: Request, res: Response) => {
try {
const { projectPath, featureId, prompt, imagePaths } = req.body as {
projectPath: string;
featureId: string;
prompt: string;
imagePaths?: string[];
};
if (!projectPath || !featureId || !prompt) {
res.status(400).json({
success: false,
error: "projectPath, featureId, and prompt are required",
});
return;
}
// Start follow-up in background
autoModeService
.followUpFeature(projectPath, featureId, prompt, imagePaths)
.catch((error) => {
console.error(`[AutoMode] Follow up feature ${featureId} error:`, error);
});
res.json({ success: true });
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
res.status(500).json({ success: false, error: message });
}
});
// Commit feature changes
router.post("/commit-feature", async (req: Request, res: Response) => {
try {
const { projectPath, featureId } = req.body as {
projectPath: string;
featureId: string;
};
if (!projectPath || !featureId) {
res
.status(400)
.json({ success: false, error: "projectPath and featureId are required" });
return;
}
const commitHash = await autoModeService.commitFeature(projectPath, featureId);
res.json({ success: true, commitHash });
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
res.status(500).json({ success: false, error: message });
}
});
return router;
}

View File

@@ -0,0 +1,159 @@
/**
* Features routes - HTTP API for feature management
*/
import { Router, type Request, type Response } from "express";
import { FeatureLoader, type Feature } from "../services/feature-loader.js";
import { addAllowedPath } from "../lib/security.js";
export function createFeaturesRoutes(featureLoader: FeatureLoader): Router {
const router = Router();
// List all features for a project
router.post("/list", async (req: Request, res: Response) => {
try {
const { projectPath } = req.body as { projectPath: string };
if (!projectPath) {
res.status(400).json({ success: false, error: "projectPath is required" });
return;
}
// Add project path to allowed paths
addAllowedPath(projectPath);
const features = await featureLoader.getAll(projectPath);
res.json({ success: true, features });
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
res.status(500).json({ success: false, error: message });
}
});
// Get a single feature
router.post("/get", async (req: Request, res: Response) => {
try {
const { projectPath, featureId } = req.body as {
projectPath: string;
featureId: string;
};
if (!projectPath || !featureId) {
res
.status(400)
.json({ success: false, error: "projectPath and featureId are required" });
return;
}
const feature = await featureLoader.get(projectPath, featureId);
if (!feature) {
res.status(404).json({ success: false, error: "Feature not found" });
return;
}
res.json({ success: true, feature });
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
res.status(500).json({ success: false, error: message });
}
});
// Create a new feature
router.post("/create", async (req: Request, res: Response) => {
try {
const { projectPath, feature } = req.body as {
projectPath: string;
feature: Partial<Feature>;
};
if (!projectPath || !feature) {
res
.status(400)
.json({ success: false, error: "projectPath and feature are required" });
return;
}
// Add project path to allowed paths
addAllowedPath(projectPath);
const created = await featureLoader.create(projectPath, feature);
res.json({ success: true, feature: created });
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
res.status(500).json({ success: false, error: message });
}
});
// Update a feature
router.post("/update", async (req: Request, res: Response) => {
try {
const { projectPath, featureId, updates } = req.body as {
projectPath: string;
featureId: string;
updates: Partial<Feature>;
};
if (!projectPath || !featureId || !updates) {
res.status(400).json({
success: false,
error: "projectPath, featureId, and updates are required",
});
return;
}
const updated = await featureLoader.update(projectPath, featureId, updates);
res.json({ success: true, feature: updated });
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
res.status(500).json({ success: false, error: message });
}
});
// Delete a feature
router.post("/delete", async (req: Request, res: Response) => {
try {
const { projectPath, featureId } = req.body as {
projectPath: string;
featureId: string;
};
if (!projectPath || !featureId) {
res
.status(400)
.json({ success: false, error: "projectPath and featureId are required" });
return;
}
const success = await featureLoader.delete(projectPath, featureId);
res.json({ success });
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
res.status(500).json({ success: false, error: message });
}
});
// Get agent output for a feature
router.post("/agent-output", async (req: Request, res: Response) => {
try {
const { projectPath, featureId } = req.body as {
projectPath: string;
featureId: string;
};
if (!projectPath || !featureId) {
res
.status(400)
.json({ success: false, error: "projectPath and featureId are required" });
return;
}
const content = await featureLoader.getAgentOutput(projectPath, featureId);
res.json({ success: true, content });
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
res.status(500).json({ success: false, error: message });
}
});
return router;
}

View File

@@ -0,0 +1,221 @@
/**
* File system routes
* Provides REST API equivalents for Electron IPC file operations
*/
import { Router, type Request, type Response } from "express";
import fs from "fs/promises";
import path from "path";
import { validatePath, addAllowedPath, isPathAllowed } from "../lib/security.js";
import type { EventEmitter } from "../lib/events.js";
export function createFsRoutes(_events: EventEmitter): Router {
const router = Router();
// Read file
router.post("/read", async (req: Request, res: Response) => {
try {
const { filePath } = req.body as { filePath: string };
if (!filePath) {
res.status(400).json({ success: false, error: "filePath is required" });
return;
}
const resolvedPath = validatePath(filePath);
const content = await fs.readFile(resolvedPath, "utf-8");
res.json({ success: true, content });
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
res.status(500).json({ success: false, error: message });
}
});
// Write file
router.post("/write", async (req: Request, res: Response) => {
try {
const { filePath, content } = req.body as {
filePath: string;
content: string;
};
if (!filePath) {
res.status(400).json({ success: false, error: "filePath is required" });
return;
}
const resolvedPath = validatePath(filePath);
// Ensure parent directory exists
await fs.mkdir(path.dirname(resolvedPath), { recursive: true });
await fs.writeFile(resolvedPath, content, "utf-8");
res.json({ success: true });
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
res.status(500).json({ success: false, error: message });
}
});
// Create directory
router.post("/mkdir", async (req: Request, res: Response) => {
try {
const { dirPath } = req.body as { dirPath: string };
if (!dirPath) {
res.status(400).json({ success: false, error: "dirPath is required" });
return;
}
const resolvedPath = validatePath(dirPath);
await fs.mkdir(resolvedPath, { recursive: true });
res.json({ success: true });
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
res.status(500).json({ success: false, error: message });
}
});
// Read directory
router.post("/readdir", async (req: Request, res: Response) => {
try {
const { dirPath } = req.body as { dirPath: string };
if (!dirPath) {
res.status(400).json({ success: false, error: "dirPath is required" });
return;
}
const resolvedPath = validatePath(dirPath);
const entries = await fs.readdir(resolvedPath, { withFileTypes: true });
const result = entries.map((entry) => ({
name: entry.name,
isDirectory: entry.isDirectory(),
isFile: entry.isFile(),
}));
res.json({ success: true, entries: result });
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
res.status(500).json({ success: false, error: message });
}
});
// Check if file/directory exists
router.post("/exists", async (req: Request, res: Response) => {
try {
const { filePath } = req.body as { filePath: string };
if (!filePath) {
res.status(400).json({ success: false, error: "filePath is required" });
return;
}
// For exists, we check but don't require the path to be pre-allowed
// This allows the UI to validate user-entered paths
const resolvedPath = path.resolve(filePath);
try {
await fs.access(resolvedPath);
res.json({ success: true, exists: true });
} catch {
res.json({ success: true, exists: false });
}
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
res.status(500).json({ success: false, error: message });
}
});
// Get file stats
router.post("/stat", async (req: Request, res: Response) => {
try {
const { filePath } = req.body as { filePath: string };
if (!filePath) {
res.status(400).json({ success: false, error: "filePath is required" });
return;
}
const resolvedPath = validatePath(filePath);
const stats = await fs.stat(resolvedPath);
res.json({
success: true,
stats: {
isDirectory: stats.isDirectory(),
isFile: stats.isFile(),
size: stats.size,
mtime: stats.mtime,
},
});
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
res.status(500).json({ success: false, error: message });
}
});
// Delete file
router.post("/delete", async (req: Request, res: Response) => {
try {
const { filePath } = req.body as { filePath: string };
if (!filePath) {
res.status(400).json({ success: false, error: "filePath is required" });
return;
}
const resolvedPath = validatePath(filePath);
await fs.rm(resolvedPath, { recursive: true });
res.json({ success: true });
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
res.status(500).json({ success: false, error: message });
}
});
// Validate and add path to allowed list
// This is the web equivalent of dialog:openDirectory
router.post("/validate-path", async (req: Request, res: Response) => {
try {
const { filePath } = req.body as { filePath: string };
if (!filePath) {
res.status(400).json({ success: false, error: "filePath is required" });
return;
}
const resolvedPath = path.resolve(filePath);
// Check if path exists
try {
const stats = await fs.stat(resolvedPath);
if (!stats.isDirectory()) {
res.status(400).json({ success: false, error: "Path is not a directory" });
return;
}
// Add to allowed paths
addAllowedPath(resolvedPath);
res.json({
success: true,
path: resolvedPath,
isAllowed: isPathAllowed(resolvedPath),
});
} catch {
res.status(400).json({ success: false, error: "Path does not exist" });
}
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
res.status(500).json({ success: false, error: message });
}
});
return router;
}

View File

@@ -0,0 +1,102 @@
/**
* Git routes - HTTP API for git operations (non-worktree)
*/
import { Router, type Request, type Response } from "express";
import { exec } from "child_process";
import { promisify } from "util";
const execAsync = promisify(exec);
export function createGitRoutes(): Router {
const router = Router();
// Get diffs for the main project
router.post("/diffs", async (req: Request, res: Response) => {
try {
const { projectPath } = req.body as { projectPath: string };
if (!projectPath) {
res.status(400).json({ success: false, error: "projectPath required" });
return;
}
try {
const { stdout: diff } = await execAsync("git diff HEAD", {
cwd: projectPath,
maxBuffer: 10 * 1024 * 1024,
});
const { stdout: status } = await execAsync("git status --porcelain", {
cwd: projectPath,
});
const files = status
.split("\n")
.filter(Boolean)
.map((line) => {
const statusChar = line[0];
const filePath = line.slice(3);
const statusMap: Record<string, string> = {
M: "Modified",
A: "Added",
D: "Deleted",
R: "Renamed",
C: "Copied",
U: "Updated",
"?": "Untracked",
};
return {
status: statusChar,
path: filePath,
statusText: statusMap[statusChar] || "Unknown",
};
});
res.json({
success: true,
diff,
files,
hasChanges: files.length > 0,
});
} catch {
res.json({ success: true, diff: "", files: [], hasChanges: false });
}
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
res.status(500).json({ success: false, error: message });
}
});
// Get diff for a specific file
router.post("/file-diff", async (req: Request, res: Response) => {
try {
const { projectPath, filePath } = req.body as {
projectPath: string;
filePath: string;
};
if (!projectPath || !filePath) {
res
.status(400)
.json({ success: false, error: "projectPath and filePath required" });
return;
}
try {
const { stdout: diff } = await execAsync(`git diff HEAD -- "${filePath}"`, {
cwd: projectPath,
maxBuffer: 10 * 1024 * 1024,
});
res.json({ success: true, diff, filePath });
} catch {
res.json({ success: true, diff: "", filePath });
}
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
res.status(500).json({ success: false, error: message });
}
});
return router;
}

View File

@@ -0,0 +1,39 @@
/**
* Health check routes
*/
import { Router } from "express";
import { getAuthStatus } from "../lib/auth.js";
export function createHealthRoutes(): Router {
const router = Router();
// Basic health check
router.get("/", (_req, res) => {
res.json({
status: "ok",
timestamp: new Date().toISOString(),
version: process.env.npm_package_version || "0.1.0",
});
});
// Detailed health check
router.get("/detailed", (_req, res) => {
res.json({
status: "ok",
timestamp: new Date().toISOString(),
version: process.env.npm_package_version || "0.1.0",
uptime: process.uptime(),
memory: process.memoryUsage(),
dataDir: process.env.DATA_DIR || "./data",
auth: getAuthStatus(),
env: {
nodeVersion: process.version,
platform: process.platform,
arch: process.arch,
},
});
});
return router;
}

View File

@@ -0,0 +1,128 @@
/**
* Models routes - HTTP API for model providers and availability
*/
import { Router, type Request, type Response } from "express";
interface ModelDefinition {
id: string;
name: string;
provider: string;
contextWindow: number;
maxOutputTokens: number;
supportsVision: boolean;
supportsTools: boolean;
}
interface ProviderStatus {
available: boolean;
hasApiKey: boolean;
error?: string;
}
export function createModelsRoutes(): Router {
const router = Router();
// Get available models
router.get("/available", async (_req: Request, res: Response) => {
try {
const models: ModelDefinition[] = [
{
id: "claude-opus-4-5-20251101",
name: "Claude Opus 4.5",
provider: "anthropic",
contextWindow: 200000,
maxOutputTokens: 16384,
supportsVision: true,
supportsTools: true,
},
{
id: "claude-sonnet-4-20250514",
name: "Claude Sonnet 4",
provider: "anthropic",
contextWindow: 200000,
maxOutputTokens: 16384,
supportsVision: true,
supportsTools: true,
},
{
id: "claude-3-5-sonnet-20241022",
name: "Claude 3.5 Sonnet",
provider: "anthropic",
contextWindow: 200000,
maxOutputTokens: 8192,
supportsVision: true,
supportsTools: true,
},
{
id: "claude-3-5-haiku-20241022",
name: "Claude 3.5 Haiku",
provider: "anthropic",
contextWindow: 200000,
maxOutputTokens: 8192,
supportsVision: true,
supportsTools: true,
},
{
id: "gpt-4o",
name: "GPT-4o",
provider: "openai",
contextWindow: 128000,
maxOutputTokens: 16384,
supportsVision: true,
supportsTools: true,
},
{
id: "gpt-4o-mini",
name: "GPT-4o Mini",
provider: "openai",
contextWindow: 128000,
maxOutputTokens: 16384,
supportsVision: true,
supportsTools: true,
},
{
id: "o1",
name: "o1",
provider: "openai",
contextWindow: 200000,
maxOutputTokens: 100000,
supportsVision: true,
supportsTools: false,
},
];
res.json({ success: true, models });
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
res.status(500).json({ success: false, error: message });
}
});
// Check provider status
router.get("/providers", async (_req: Request, res: Response) => {
try {
const providers: Record<string, ProviderStatus> = {
anthropic: {
available: !!process.env.ANTHROPIC_API_KEY,
hasApiKey: !!process.env.ANTHROPIC_API_KEY,
},
openai: {
available: !!process.env.OPENAI_API_KEY,
hasApiKey: !!process.env.OPENAI_API_KEY,
},
google: {
available: !!process.env.GOOGLE_API_KEY,
hasApiKey: !!process.env.GOOGLE_API_KEY,
},
};
res.json({ success: true, providers });
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
res.status(500).json({ success: false, error: message });
}
});
return router;
}

View File

@@ -0,0 +1,70 @@
/**
* Running Agents routes - HTTP API for tracking active agent executions
*/
import { Router, type Request, type Response } from "express";
import path from "path";
interface RunningAgent {
featureId: string;
projectPath: string;
projectName: string;
isAutoMode: boolean;
}
// In-memory tracking of running agents (shared with auto-mode service via reference)
const runningAgentsMap = new Map<string, RunningAgent>();
let autoLoopRunning = false;
export function createRunningAgentsRoutes(): Router {
const router = Router();
// Get all running agents
router.get("/", async (_req: Request, res: Response) => {
try {
const runningAgents = Array.from(runningAgentsMap.values());
res.json({
success: true,
runningAgents,
totalCount: runningAgents.length,
autoLoopRunning,
});
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
res.status(500).json({ success: false, error: message });
}
});
return router;
}
// Export functions to update running agents from other services
export function registerRunningAgent(
featureId: string,
projectPath: string,
isAutoMode: boolean
): void {
runningAgentsMap.set(featureId, {
featureId,
projectPath,
projectName: path.basename(projectPath),
isAutoMode,
});
}
export function unregisterRunningAgent(featureId: string): void {
runningAgentsMap.delete(featureId);
}
export function setAutoLoopRunning(running: boolean): void {
autoLoopRunning = running;
}
export function getRunningAgentsCount(): number {
return runningAgentsMap.size;
}
export function isAgentRunning(featureId: string): boolean {
return runningAgentsMap.has(featureId);
}

View File

@@ -0,0 +1,126 @@
/**
* Sessions routes - HTTP API for session management
*/
import { Router, type Request, type Response } from "express";
import { AgentService } from "../services/agent-service.js";
export function createSessionsRoutes(agentService: AgentService): Router {
const router = Router();
// List all sessions
router.get("/", async (req: Request, res: Response) => {
try {
const includeArchived = req.query.includeArchived === "true";
const sessions = await agentService.listSessions(includeArchived);
res.json({ success: true, sessions });
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
res.status(500).json({ success: false, error: message });
}
});
// Create a new session
router.post("/", async (req: Request, res: Response) => {
try {
const { name, projectPath, workingDirectory } = req.body as {
name: string;
projectPath?: string;
workingDirectory?: string;
};
if (!name) {
res.status(400).json({ success: false, error: "name is required" });
return;
}
const session = await agentService.createSession(
name,
projectPath,
workingDirectory
);
res.json({ success: true, session });
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
res.status(500).json({ success: false, error: message });
}
});
// Update a session
router.put("/:sessionId", async (req: Request, res: Response) => {
try {
const { sessionId } = req.params;
const { name, tags } = req.body as {
name?: string;
tags?: string[];
};
const session = await agentService.updateSession(sessionId, { name, tags });
if (!session) {
res.status(404).json({ success: false, error: "Session not found" });
return;
}
res.json({ success: true, session });
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
res.status(500).json({ success: false, error: message });
}
});
// Archive a session
router.post("/:sessionId/archive", async (req: Request, res: Response) => {
try {
const { sessionId } = req.params;
const success = await agentService.archiveSession(sessionId);
if (!success) {
res.status(404).json({ success: false, error: "Session not found" });
return;
}
res.json({ success: true });
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
res.status(500).json({ success: false, error: message });
}
});
// Unarchive a session
router.post("/:sessionId/unarchive", async (req: Request, res: Response) => {
try {
const { sessionId } = req.params;
const success = await agentService.unarchiveSession(sessionId);
if (!success) {
res.status(404).json({ success: false, error: "Session not found" });
return;
}
res.json({ success: true });
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
res.status(500).json({ success: false, error: message });
}
});
// Delete a session
router.delete("/:sessionId", async (req: Request, res: Response) => {
try {
const { sessionId } = req.params;
const success = await agentService.deleteSession(sessionId);
if (!success) {
res.status(404).json({ success: false, error: "Session not found" });
return;
}
res.json({ success: true });
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
res.status(500).json({ success: false, error: message });
}
});
return router;
}

View File

@@ -0,0 +1,407 @@
/**
* Setup routes - HTTP API for CLI detection, API keys, and platform info
*/
import { Router, type Request, type Response } from "express";
import { exec } from "child_process";
import { promisify } from "util";
import os from "os";
import path from "path";
import fs from "fs/promises";
const execAsync = promisify(exec);
// Storage for API keys (in-memory for now, should be persisted)
const apiKeys: Record<string, string> = {};
export function createSetupRoutes(): Router {
const router = Router();
// Get Claude CLI status
router.get("/claude-status", async (_req: Request, res: Response) => {
try {
let installed = false;
let version = "";
let cliPath = "";
let method = "none";
// Try to find Claude CLI
try {
const { stdout } = await execAsync("which claude || where claude 2>/dev/null");
cliPath = stdout.trim();
installed = true;
method = "path";
// Get version
try {
const { stdout: versionOut } = await execAsync("claude --version");
version = versionOut.trim();
} catch {
// Version command might not be available
}
} catch {
// Not in PATH, try common locations
const commonPaths = [
path.join(os.homedir(), ".local", "bin", "claude"),
"/usr/local/bin/claude",
path.join(os.homedir(), ".npm-global", "bin", "claude"),
];
for (const p of commonPaths) {
try {
await fs.access(p);
cliPath = p;
installed = true;
method = "local";
break;
} catch {
// Not found at this path
}
}
}
// Check authentication - detect all possible auth methods
let auth = {
authenticated: false,
method: "none" as string,
hasCredentialsFile: false,
hasToken: false,
hasStoredOAuthToken: false,
hasStoredApiKey: !!apiKeys.anthropic,
hasEnvApiKey: !!process.env.ANTHROPIC_API_KEY,
hasEnvOAuthToken: !!process.env.CLAUDE_CODE_OAUTH_TOKEN,
// Additional fields for detailed status
oauthTokenValid: false,
apiKeyValid: false,
};
// Check for credentials file (OAuth tokens from claude login)
const credentialsPath = path.join(os.homedir(), ".claude", "credentials.json");
try {
const credentialsContent = await fs.readFile(credentialsPath, "utf-8");
const credentials = JSON.parse(credentialsContent);
auth.hasCredentialsFile = true;
// Check what type of token is in credentials
if (credentials.oauth_token || credentials.access_token) {
auth.hasStoredOAuthToken = true;
auth.oauthTokenValid = true;
auth.authenticated = true;
auth.method = "oauth_token"; // Stored OAuth token from credentials file
} else if (credentials.api_key) {
auth.apiKeyValid = true;
auth.authenticated = true;
auth.method = "api_key"; // Stored API key in credentials file
}
} catch {
// No credentials file or invalid format
}
// Environment variables override stored credentials (higher priority)
if (auth.hasEnvOAuthToken) {
auth.authenticated = true;
auth.oauthTokenValid = true;
auth.method = "oauth_token_env"; // OAuth token from CLAUDE_CODE_OAUTH_TOKEN env var
} else if (auth.hasEnvApiKey) {
auth.authenticated = true;
auth.apiKeyValid = true;
auth.method = "api_key_env"; // API key from ANTHROPIC_API_KEY env var
}
// In-memory stored API key (from settings UI)
if (!auth.authenticated && apiKeys.anthropic) {
auth.authenticated = true;
auth.method = "api_key"; // Manually stored API key
}
res.json({
success: true,
status: installed ? "installed" : "not_installed",
installed,
method,
version,
path: cliPath,
auth,
});
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
res.status(500).json({ success: false, error: message });
}
});
// Get Codex CLI status
router.get("/codex-status", async (_req: Request, res: Response) => {
try {
let installed = false;
let version = "";
let cliPath = "";
let method = "none";
// Try to find Codex CLI
try {
const { stdout } = await execAsync("which codex || where codex 2>/dev/null");
cliPath = stdout.trim();
installed = true;
method = "path";
try {
const { stdout: versionOut } = await execAsync("codex --version");
version = versionOut.trim();
} catch {
// Version command might not be available
}
} catch {
// Not found
}
// Check for OpenAI/Codex authentication
let auth = {
authenticated: false,
method: "none" as string,
hasAuthFile: false,
hasEnvKey: !!process.env.OPENAI_API_KEY,
hasStoredApiKey: !!apiKeys.openai,
hasEnvApiKey: !!process.env.OPENAI_API_KEY,
// Additional fields for subscription/account detection
hasSubscription: false,
cliLoggedIn: false,
};
// Check for OpenAI CLI auth file (~/.codex/auth.json or similar)
const codexAuthPaths = [
path.join(os.homedir(), ".codex", "auth.json"),
path.join(os.homedir(), ".openai", "credentials"),
path.join(os.homedir(), ".config", "openai", "credentials.json"),
];
for (const authPath of codexAuthPaths) {
try {
const authContent = await fs.readFile(authPath, "utf-8");
const authData = JSON.parse(authContent);
auth.hasAuthFile = true;
// Check for subscription/tokens
if (authData.subscription || authData.plan || authData.account_type) {
auth.hasSubscription = true;
auth.authenticated = true;
auth.method = "subscription"; // Codex subscription (Plus/Team)
} else if (authData.access_token || authData.api_key) {
auth.cliLoggedIn = true;
auth.authenticated = true;
auth.method = "cli_verified"; // CLI logged in with account
}
break;
} catch {
// Auth file not found at this path
}
}
// Environment variable has highest priority
if (auth.hasEnvApiKey) {
auth.authenticated = true;
auth.method = "env"; // OPENAI_API_KEY environment variable
}
// In-memory stored API key (from settings UI)
if (!auth.authenticated && apiKeys.openai) {
auth.authenticated = true;
auth.method = "api_key"; // Manually stored API key
}
res.json({
success: true,
status: installed ? "installed" : "not_installed",
method,
version,
path: cliPath,
auth,
});
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
res.status(500).json({ success: false, error: message });
}
});
// Install Claude CLI
router.post("/install-claude", async (_req: Request, res: Response) => {
try {
// In web mode, we can't install CLIs directly
// Return instructions instead
res.json({
success: false,
error:
"CLI installation requires terminal access. Please install manually using: npm install -g @anthropic-ai/claude-code",
});
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
res.status(500).json({ success: false, error: message });
}
});
// Install Codex CLI
router.post("/install-codex", async (_req: Request, res: Response) => {
try {
res.json({
success: false,
error:
"CLI installation requires terminal access. Please install manually using: npm install -g @openai/codex",
});
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
res.status(500).json({ success: false, error: message });
}
});
// Auth Claude
router.post("/auth-claude", async (_req: Request, res: Response) => {
try {
res.json({
success: true,
requiresManualAuth: true,
command: "claude login",
message: "Please run 'claude login' in your terminal to authenticate",
});
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
res.status(500).json({ success: false, error: message });
}
});
// Auth Codex
router.post("/auth-codex", async (req: Request, res: Response) => {
try {
const { apiKey } = req.body as { apiKey?: string };
if (apiKey) {
apiKeys.openai = apiKey;
process.env.OPENAI_API_KEY = apiKey;
res.json({ success: true });
} else {
res.json({
success: true,
requiresManualAuth: true,
command: "codex auth login",
});
}
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
res.status(500).json({ success: false, error: message });
}
});
// Store API key
router.post("/store-api-key", async (req: Request, res: Response) => {
try {
const { provider, apiKey } = req.body as { provider: string; apiKey: string };
if (!provider || !apiKey) {
res.status(400).json({ success: false, error: "provider and apiKey required" });
return;
}
apiKeys[provider] = apiKey;
// Also set as environment variable
if (provider === "anthropic") {
process.env.ANTHROPIC_API_KEY = apiKey;
} else if (provider === "openai") {
process.env.OPENAI_API_KEY = apiKey;
} else if (provider === "google") {
process.env.GOOGLE_API_KEY = apiKey;
}
res.json({ success: true });
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
res.status(500).json({ success: false, error: message });
}
});
// Get API keys status
router.get("/api-keys", async (_req: Request, res: Response) => {
try {
res.json({
success: true,
hasAnthropicKey: !!apiKeys.anthropic || !!process.env.ANTHROPIC_API_KEY,
hasOpenAIKey: !!apiKeys.openai || !!process.env.OPENAI_API_KEY,
hasGoogleKey: !!apiKeys.google || !!process.env.GOOGLE_API_KEY,
});
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
res.status(500).json({ success: false, error: message });
}
});
// Configure Codex MCP
router.post("/configure-codex-mcp", async (req: Request, res: Response) => {
try {
const { projectPath } = req.body as { projectPath: string };
if (!projectPath) {
res.status(400).json({ success: false, error: "projectPath required" });
return;
}
// Create .codex directory and config
const codexDir = path.join(projectPath, ".codex");
await fs.mkdir(codexDir, { recursive: true });
const configPath = path.join(codexDir, "config.toml");
const config = `# Codex configuration
[mcp]
enabled = true
`;
await fs.writeFile(configPath, config);
res.json({ success: true, configPath });
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
res.status(500).json({ success: false, error: message });
}
});
// Get platform info
router.get("/platform", async (_req: Request, res: Response) => {
try {
const platform = os.platform();
res.json({
success: true,
platform,
arch: os.arch(),
homeDir: os.homedir(),
isWindows: platform === "win32",
isMac: platform === "darwin",
isLinux: platform === "linux",
});
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
res.status(500).json({ success: false, error: message });
}
});
// Test OpenAI connection
router.post("/test-openai", async (req: Request, res: Response) => {
try {
const { apiKey } = req.body as { apiKey?: string };
const key = apiKey || apiKeys.openai || process.env.OPENAI_API_KEY;
if (!key) {
res.json({ success: false, error: "No OpenAI API key provided" });
return;
}
// Simple test - just verify the key format
if (!key.startsWith("sk-")) {
res.json({ success: false, error: "Invalid OpenAI API key format" });
return;
}
res.json({ success: true, message: "API key format is valid" });
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
res.status(500).json({ success: false, error: message });
}
});
return router;
}

View File

@@ -0,0 +1,412 @@
/**
* Spec Regeneration routes - HTTP API for AI-powered spec generation
*/
import { Router, type Request, type Response } from "express";
import { query, type Options } from "@anthropic-ai/claude-agent-sdk";
import path from "path";
import fs from "fs/promises";
import type { EventEmitter } from "../lib/events.js";
let isRunning = false;
let currentAbortController: AbortController | null = null;
export function createSpecRegenerationRoutes(events: EventEmitter): Router {
const router = Router();
// Create project spec from overview
router.post("/create", async (req: Request, res: Response) => {
try {
const { projectPath, projectOverview, generateFeatures } = req.body as {
projectPath: string;
projectOverview: string;
generateFeatures?: boolean;
};
if (!projectPath || !projectOverview) {
res.status(400).json({
success: false,
error: "projectPath and projectOverview required",
});
return;
}
if (isRunning) {
res.json({ success: false, error: "Spec generation already running" });
return;
}
isRunning = true;
currentAbortController = new AbortController();
// Start generation in background
generateSpec(
projectPath,
projectOverview,
events,
currentAbortController,
generateFeatures
)
.catch((error) => {
console.error("[SpecRegeneration] Error:", error);
events.emit("spec-regeneration:event", {
type: "spec_error",
error: error.message,
});
})
.finally(() => {
isRunning = false;
currentAbortController = null;
});
res.json({ success: true });
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
res.status(500).json({ success: false, error: message });
}
});
// Generate from project definition
router.post("/generate", async (req: Request, res: Response) => {
try {
const { projectPath, projectDefinition } = req.body as {
projectPath: string;
projectDefinition: string;
};
if (!projectPath || !projectDefinition) {
res.status(400).json({
success: false,
error: "projectPath and projectDefinition required",
});
return;
}
if (isRunning) {
res.json({ success: false, error: "Spec generation already running" });
return;
}
isRunning = true;
currentAbortController = new AbortController();
generateSpec(
projectPath,
projectDefinition,
events,
currentAbortController,
false
)
.catch((error) => {
console.error("[SpecRegeneration] Error:", error);
events.emit("spec-regeneration:event", {
type: "spec_error",
error: error.message,
});
})
.finally(() => {
isRunning = false;
currentAbortController = null;
});
res.json({ success: true });
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
res.status(500).json({ success: false, error: message });
}
});
// Generate features from existing spec
router.post("/generate-features", async (req: Request, res: Response) => {
try {
const { projectPath } = req.body as { projectPath: string };
if (!projectPath) {
res.status(400).json({ success: false, error: "projectPath required" });
return;
}
if (isRunning) {
res.json({ success: false, error: "Generation already running" });
return;
}
isRunning = true;
currentAbortController = new AbortController();
generateFeaturesFromSpec(projectPath, events, currentAbortController)
.catch((error) => {
console.error("[SpecRegeneration] Error:", error);
events.emit("spec-regeneration:event", {
type: "features_error",
error: error.message,
});
})
.finally(() => {
isRunning = false;
currentAbortController = null;
});
res.json({ success: true });
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
res.status(500).json({ success: false, error: message });
}
});
// Stop generation
router.post("/stop", async (_req: Request, res: Response) => {
try {
if (currentAbortController) {
currentAbortController.abort();
}
isRunning = false;
res.json({ success: true });
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
res.status(500).json({ success: false, error: message });
}
});
// Get status
router.get("/status", async (_req: Request, res: Response) => {
try {
res.json({ success: true, isRunning });
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
res.status(500).json({ success: false, error: message });
}
});
return router;
}
async function generateSpec(
projectPath: string,
projectOverview: string,
events: EventEmitter,
abortController: AbortController,
generateFeatures?: boolean
) {
const prompt = `You are helping to define a software project specification.
Project Overview:
${projectOverview}
Based on this overview, analyze the project and create a comprehensive specification that includes:
1. **Project Summary** - Brief description of what the project does
2. **Core Features** - Main functionality the project needs
3. **Technical Stack** - Recommended technologies and frameworks
4. **Architecture** - High-level system design
5. **Data Models** - Key entities and their relationships
6. **API Design** - Main endpoints/interfaces needed
7. **User Experience** - Key user flows and interactions
${generateFeatures ? `
Also generate a list of features to implement. For each feature provide:
- ID (lowercase-hyphenated)
- Title
- Description
- Priority (1=high, 2=medium, 3=low)
- Estimated complexity (simple, moderate, complex)
` : ""}
Format your response as markdown. Be specific and actionable.`;
events.emit("spec-regeneration:event", {
type: "spec_progress",
content: "Starting spec generation...\n",
});
const options: Options = {
model: "claude-opus-4-5-20251101",
maxTurns: 10,
cwd: projectPath,
allowedTools: ["Read", "Glob", "Grep"],
permissionMode: "acceptEdits",
abortController,
};
const stream = query({ prompt, options });
let responseText = "";
for await (const msg of stream) {
if (msg.type === "assistant" && msg.message.content) {
for (const block of msg.message.content) {
if (block.type === "text") {
responseText = block.text;
events.emit("spec-regeneration:event", {
type: "spec_progress",
content: block.text,
});
} else if (block.type === "tool_use") {
events.emit("spec-regeneration:event", {
type: "spec_tool",
tool: block.name,
input: block.input,
});
}
}
} else if (msg.type === "result" && msg.subtype === "success") {
responseText = msg.result || responseText;
}
}
// Save spec
const specDir = path.join(projectPath, ".automaker");
const specPath = path.join(specDir, "project-spec.md");
await fs.mkdir(specDir, { recursive: true });
await fs.writeFile(specPath, responseText);
events.emit("spec-regeneration:event", {
type: "spec_complete",
specPath,
content: responseText,
});
// If generate features was requested, parse and create them
if (generateFeatures) {
await parseAndCreateFeatures(projectPath, responseText, events);
}
}
async function generateFeaturesFromSpec(
projectPath: string,
events: EventEmitter,
abortController: AbortController
) {
// Read existing spec
const specPath = path.join(projectPath, ".automaker", "project-spec.md");
let spec: string;
try {
spec = await fs.readFile(specPath, "utf-8");
} catch {
events.emit("spec-regeneration:event", {
type: "features_error",
error: "No project spec found. Generate spec first.",
});
return;
}
const prompt = `Based on this project specification:
${spec}
Generate a prioritized list of implementable features. For each feature provide:
1. **id**: A unique lowercase-hyphenated identifier
2. **title**: Short descriptive title
3. **description**: What this feature does (2-3 sentences)
4. **priority**: 1 (high), 2 (medium), or 3 (low)
5. **complexity**: "simple", "moderate", or "complex"
6. **dependencies**: Array of feature IDs this depends on (can be empty)
Format as JSON:
{
"features": [
{
"id": "feature-id",
"title": "Feature Title",
"description": "What it does",
"priority": 1,
"complexity": "moderate",
"dependencies": []
}
]
}
Generate 5-15 features that build on each other logically.`;
events.emit("spec-regeneration:event", {
type: "features_progress",
content: "Analyzing spec and generating features...\n",
});
const options: Options = {
model: "claude-sonnet-4-20250514",
maxTurns: 5,
cwd: projectPath,
allowedTools: ["Read", "Glob"],
permissionMode: "acceptEdits",
abortController,
};
const stream = query({ prompt, options });
let responseText = "";
for await (const msg of stream) {
if (msg.type === "assistant" && msg.message.content) {
for (const block of msg.message.content) {
if (block.type === "text") {
responseText = block.text;
events.emit("spec-regeneration:event", {
type: "features_progress",
content: block.text,
});
}
}
} else if (msg.type === "result" && msg.subtype === "success") {
responseText = msg.result || responseText;
}
}
await parseAndCreateFeatures(projectPath, responseText, events);
}
async function parseAndCreateFeatures(
projectPath: string,
content: string,
events: EventEmitter
) {
try {
// Extract JSON from response
const jsonMatch = content.match(/\{[\s\S]*"features"[\s\S]*\}/);
if (!jsonMatch) {
throw new Error("No valid JSON found in response");
}
const parsed = JSON.parse(jsonMatch[0]);
const featuresDir = path.join(projectPath, ".automaker", "features");
await fs.mkdir(featuresDir, { recursive: true });
const createdFeatures: Array<{ id: string; title: string }> = [];
for (const feature of parsed.features) {
const featureDir = path.join(featuresDir, feature.id);
await fs.mkdir(featureDir, { recursive: true });
const featureData = {
id: feature.id,
title: feature.title,
description: feature.description,
status: "pending",
priority: feature.priority || 2,
complexity: feature.complexity || "moderate",
dependencies: feature.dependencies || [],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
await fs.writeFile(
path.join(featureDir, "feature.json"),
JSON.stringify(featureData, null, 2)
);
createdFeatures.push({ id: feature.id, title: feature.title });
}
events.emit("spec-regeneration:event", {
type: "features_complete",
features: createdFeatures,
count: createdFeatures.length,
});
} catch (error) {
events.emit("spec-regeneration:event", {
type: "features_error",
error: (error as Error).message,
});
}
}

View File

@@ -0,0 +1,192 @@
/**
* Suggestions routes - HTTP API for AI-powered feature suggestions
*/
import { Router, type Request, type Response } from "express";
import { query, type Options } from "@anthropic-ai/claude-agent-sdk";
import type { EventEmitter } from "../lib/events.js";
let isRunning = false;
let currentAbortController: AbortController | null = null;
export function createSuggestionsRoutes(events: EventEmitter): Router {
const router = Router();
// Generate suggestions
router.post("/generate", async (req: Request, res: Response) => {
try {
const { projectPath, suggestionType = "features" } = req.body as {
projectPath: string;
suggestionType?: string;
};
if (!projectPath) {
res.status(400).json({ success: false, error: "projectPath required" });
return;
}
if (isRunning) {
res.json({ success: false, error: "Suggestions generation is already running" });
return;
}
isRunning = true;
currentAbortController = new AbortController();
// Start generation in background
generateSuggestions(projectPath, suggestionType, events, currentAbortController)
.catch((error) => {
console.error("[Suggestions] Error:", error);
events.emit("suggestions:event", {
type: "suggestions_error",
error: error.message,
});
})
.finally(() => {
isRunning = false;
currentAbortController = null;
});
res.json({ success: true });
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
res.status(500).json({ success: false, error: message });
}
});
// Stop suggestions generation
router.post("/stop", async (_req: Request, res: Response) => {
try {
if (currentAbortController) {
currentAbortController.abort();
}
isRunning = false;
res.json({ success: true });
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
res.status(500).json({ success: false, error: message });
}
});
// Get status
router.get("/status", async (_req: Request, res: Response) => {
try {
res.json({ success: true, isRunning });
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
res.status(500).json({ success: false, error: message });
}
});
return router;
}
async function generateSuggestions(
projectPath: string,
suggestionType: string,
events: EventEmitter,
abortController: AbortController
) {
const typePrompts: Record<string, string> = {
features: "Analyze this project and suggest new features that would add value.",
refactoring: "Analyze this project and identify refactoring opportunities.",
security: "Analyze this project for security vulnerabilities and suggest fixes.",
performance: "Analyze this project for performance issues and suggest optimizations.",
};
const prompt = `${typePrompts[suggestionType] || typePrompts.features}
Look at the codebase and provide 3-5 concrete suggestions.
For each suggestion, provide:
1. A category (e.g., "User Experience", "Security", "Performance")
2. A clear description of what to implement
3. Concrete steps to implement it
4. Priority (1=high, 2=medium, 3=low)
5. Brief reasoning for why this would help
Format your response as JSON:
{
"suggestions": [
{
"id": "suggestion-123",
"category": "Category",
"description": "What to implement",
"steps": ["Step 1", "Step 2"],
"priority": 1,
"reasoning": "Why this helps"
}
]
}`;
events.emit("suggestions:event", {
type: "suggestions_progress",
content: `Starting ${suggestionType} analysis...\n`,
});
const options: Options = {
model: "claude-opus-4-5-20251101",
maxTurns: 5,
cwd: projectPath,
allowedTools: ["Read", "Glob", "Grep"],
permissionMode: "acceptEdits",
abortController,
};
const stream = query({ prompt, options });
let responseText = "";
for await (const msg of stream) {
if (msg.type === "assistant" && msg.message.content) {
for (const block of msg.message.content) {
if (block.type === "text") {
responseText = block.text;
events.emit("suggestions:event", {
type: "suggestions_progress",
content: block.text,
});
} else if (block.type === "tool_use") {
events.emit("suggestions:event", {
type: "suggestions_tool",
tool: block.name,
input: block.input,
});
}
}
} else if (msg.type === "result" && msg.subtype === "success") {
responseText = msg.result || responseText;
}
}
// Parse suggestions from response
try {
const jsonMatch = responseText.match(/\{[\s\S]*"suggestions"[\s\S]*\}/);
if (jsonMatch) {
const parsed = JSON.parse(jsonMatch[0]);
events.emit("suggestions:event", {
type: "suggestions_complete",
suggestions: parsed.suggestions.map((s: Record<string, unknown>, i: number) => ({
...s,
id: s.id || `suggestion-${Date.now()}-${i}`,
})),
});
} else {
throw new Error("No valid JSON found in response");
}
} catch (error) {
// Return generic suggestions if parsing fails
events.emit("suggestions:event", {
type: "suggestions_complete",
suggestions: [
{
id: `suggestion-${Date.now()}-0`,
category: "Analysis",
description: "Review the AI analysis output for insights",
steps: ["Review the generated analysis"],
priority: 1,
reasoning: "The AI provided analysis but suggestions need manual review",
},
],
});
}
}

View File

@@ -0,0 +1,355 @@
/**
* Worktree routes - HTTP API for git worktree operations
*/
import { Router, type Request, type Response } from "express";
import { exec } from "child_process";
import { promisify } from "util";
import path from "path";
import fs from "fs/promises";
const execAsync = promisify(exec);
export function createWorktreeRoutes(): Router {
const router = Router();
// Check if a path is a git repo
async function isGitRepo(repoPath: string): Promise<boolean> {
try {
await execAsync("git rev-parse --is-inside-work-tree", { cwd: repoPath });
return true;
} catch {
return false;
}
}
// Get worktree info
router.post("/info", async (req: Request, res: Response) => {
try {
const { projectPath, featureId } = req.body as {
projectPath: string;
featureId: string;
};
if (!projectPath || !featureId) {
res
.status(400)
.json({ success: false, error: "projectPath and featureId required" });
return;
}
// Check if worktree exists
const worktreePath = path.join(projectPath, ".automaker", "worktrees", featureId);
try {
await fs.access(worktreePath);
const { stdout } = await execAsync("git rev-parse --abbrev-ref HEAD", {
cwd: worktreePath,
});
res.json({
success: true,
worktreePath,
branchName: stdout.trim(),
});
} catch {
res.json({ success: true, worktreePath: null, branchName: null });
}
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
res.status(500).json({ success: false, error: message });
}
});
// Get worktree status
router.post("/status", async (req: Request, res: Response) => {
try {
const { projectPath, featureId } = req.body as {
projectPath: string;
featureId: string;
};
if (!projectPath || !featureId) {
res
.status(400)
.json({ success: false, error: "projectPath and featureId required" });
return;
}
const worktreePath = path.join(projectPath, ".automaker", "worktrees", featureId);
try {
await fs.access(worktreePath);
const { stdout: status } = await execAsync("git status --porcelain", {
cwd: worktreePath,
});
const files = status
.split("\n")
.filter(Boolean)
.map((line) => line.slice(3));
const { stdout: diffStat } = await execAsync("git diff --stat", {
cwd: worktreePath,
});
const { stdout: logOutput } = await execAsync(
'git log --oneline -5 --format="%h %s"',
{ cwd: worktreePath }
);
res.json({
success: true,
modifiedFiles: files.length,
files,
diffStat: diffStat.trim(),
recentCommits: logOutput.trim().split("\n").filter(Boolean),
});
} catch {
res.json({
success: true,
modifiedFiles: 0,
files: [],
diffStat: "",
recentCommits: [],
});
}
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
res.status(500).json({ success: false, error: message });
}
});
// List all worktrees
router.post("/list", async (req: Request, res: Response) => {
try {
const { projectPath } = req.body as { projectPath: string };
if (!projectPath) {
res.status(400).json({ success: false, error: "projectPath required" });
return;
}
if (!(await isGitRepo(projectPath))) {
res.json({ success: true, worktrees: [] });
return;
}
const { stdout } = await execAsync("git worktree list --porcelain", {
cwd: projectPath,
});
const worktrees: Array<{ path: string; branch: string }> = [];
const lines = stdout.split("\n");
let current: { path?: string; branch?: string } = {};
for (const line of lines) {
if (line.startsWith("worktree ")) {
current.path = line.slice(9);
} else if (line.startsWith("branch ")) {
current.branch = line.slice(7).replace("refs/heads/", "");
} else if (line === "") {
if (current.path && current.branch) {
worktrees.push({ path: current.path, branch: current.branch });
}
current = {};
}
}
res.json({ success: true, worktrees });
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
res.status(500).json({ success: false, error: message });
}
});
// Get diffs for a worktree
router.post("/diffs", async (req: Request, res: Response) => {
try {
const { projectPath, featureId } = req.body as {
projectPath: string;
featureId: string;
};
if (!projectPath || !featureId) {
res
.status(400)
.json({ success: false, error: "projectPath and featureId required" });
return;
}
const worktreePath = path.join(projectPath, ".automaker", "worktrees", featureId);
try {
await fs.access(worktreePath);
const { stdout: diff } = await execAsync("git diff HEAD", {
cwd: worktreePath,
maxBuffer: 10 * 1024 * 1024,
});
const { stdout: status } = await execAsync("git status --porcelain", {
cwd: worktreePath,
});
const files = status
.split("\n")
.filter(Boolean)
.map((line) => {
const statusChar = line[0];
const filePath = line.slice(3);
const statusMap: Record<string, string> = {
M: "Modified",
A: "Added",
D: "Deleted",
R: "Renamed",
C: "Copied",
U: "Updated",
"?": "Untracked",
};
return {
status: statusChar,
path: filePath,
statusText: statusMap[statusChar] || "Unknown",
};
});
res.json({
success: true,
diff,
files,
hasChanges: files.length > 0,
});
} catch {
res.json({ success: true, diff: "", files: [], hasChanges: false });
}
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
res.status(500).json({ success: false, error: message });
}
});
// Get diff for a specific file
router.post("/file-diff", async (req: Request, res: Response) => {
try {
const { projectPath, featureId, filePath } = req.body as {
projectPath: string;
featureId: string;
filePath: string;
};
if (!projectPath || !featureId || !filePath) {
res.status(400).json({
success: false,
error: "projectPath, featureId, and filePath required",
});
return;
}
const worktreePath = path.join(projectPath, ".automaker", "worktrees", featureId);
try {
await fs.access(worktreePath);
const { stdout: diff } = await execAsync(`git diff HEAD -- "${filePath}"`, {
cwd: worktreePath,
maxBuffer: 10 * 1024 * 1024,
});
res.json({ success: true, diff, filePath });
} catch {
res.json({ success: true, diff: "", filePath });
}
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
res.status(500).json({ success: false, error: message });
}
});
// Revert feature (remove worktree)
router.post("/revert", async (req: Request, res: Response) => {
try {
const { projectPath, featureId } = req.body as {
projectPath: string;
featureId: string;
};
if (!projectPath || !featureId) {
res
.status(400)
.json({ success: false, error: "projectPath and featureId required" });
return;
}
const worktreePath = path.join(projectPath, ".automaker", "worktrees", featureId);
try {
// Remove worktree
await execAsync(`git worktree remove "${worktreePath}" --force`, {
cwd: projectPath,
});
// Delete branch
await execAsync(`git branch -D feature/${featureId}`, { cwd: projectPath });
res.json({ success: true, removedPath: worktreePath });
} catch (error) {
// Worktree might not exist
res.json({ success: true, removedPath: null });
}
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
res.status(500).json({ success: false, error: message });
}
});
// Merge feature (merge worktree branch into main)
router.post("/merge", async (req: Request, res: Response) => {
try {
const { projectPath, featureId, options } = req.body as {
projectPath: string;
featureId: string;
options?: { squash?: boolean; message?: string };
};
if (!projectPath || !featureId) {
res
.status(400)
.json({ success: false, error: "projectPath and featureId required" });
return;
}
const branchName = `feature/${featureId}`;
const worktreePath = path.join(projectPath, ".automaker", "worktrees", featureId);
// Get current branch
const { stdout: currentBranch } = await execAsync(
"git rev-parse --abbrev-ref HEAD",
{ cwd: projectPath }
);
// Merge the feature branch
const mergeCmd = options?.squash
? `git merge --squash ${branchName}`
: `git merge ${branchName} -m "${options?.message || `Merge ${branchName}`}"`;
await execAsync(mergeCmd, { cwd: projectPath });
// If squash merge, need to commit
if (options?.squash) {
await execAsync(
`git commit -m "${options?.message || `Merge ${branchName} (squash)`}"`,
{ cwd: projectPath }
);
}
// Clean up worktree and branch
try {
await execAsync(`git worktree remove "${worktreePath}" --force`, {
cwd: projectPath,
});
await execAsync(`git branch -D ${branchName}`, { cwd: projectPath });
} catch {
// Cleanup errors are non-fatal
}
res.json({ success: true, mergedBranch: branchName });
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
res.status(500).json({ success: false, error: message });
}
});
return router;
}