Merge branch 'main' of github.com:AutoMaker-Org/automaker into improve-context-page

This commit is contained in:
Test User
2025-12-22 00:50:55 -05:00
501 changed files with 17637 additions and 17437 deletions

View File

@@ -299,11 +299,34 @@ terminalWss.on('connection', (ws: WebSocket, req: import('http').IncomingMessage
switch (msg.type) {
case 'input':
// Validate input data type and length
if (typeof msg.data !== 'string') {
ws.send(JSON.stringify({ type: 'error', message: 'Invalid input type' }));
break;
}
// Limit input size to 1MB to prevent memory issues
if (msg.data.length > 1024 * 1024) {
ws.send(JSON.stringify({ type: 'error', message: 'Input too large' }));
break;
}
// Write user input to terminal
terminalService.write(sessionId, msg.data);
break;
case 'resize':
// Validate resize dimensions are positive integers within reasonable bounds
if (
typeof msg.cols !== 'number' ||
typeof msg.rows !== 'number' ||
!Number.isInteger(msg.cols) ||
!Number.isInteger(msg.rows) ||
msg.cols < 1 ||
msg.cols > 1000 ||
msg.rows < 1 ||
msg.rows > 500
) {
break; // Silently ignore invalid resize requests
}
// Resize terminal with deduplication and rate limiting
if (msg.cols && msg.rows) {
const now = Date.now();

View File

@@ -6,26 +6,26 @@
*/
// Import and re-export spec types from shared package
export type { SpecOutput } from "@automaker/types";
export { specOutputSchema } from "@automaker/types";
export type { SpecOutput } from '@automaker/types';
export { specOutputSchema } from '@automaker/types';
/**
* Escape special XML characters
*/
function escapeXml(str: string): string {
return str
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&apos;");
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&apos;');
}
/**
* Convert structured spec output to XML format
*/
export function specToXml(spec: import("@automaker/types").SpecOutput): string {
const indent = " ";
export function specToXml(spec: import('@automaker/types').SpecOutput): string {
const indent = ' ';
let xml = `<?xml version="1.0" encoding="UTF-8"?>
<project_specification>
@@ -36,11 +36,11 @@ ${indent}${indent}${escapeXml(spec.overview)}
${indent}</overview>
${indent}<technology_stack>
${spec.technology_stack.map((t) => `${indent}${indent}<technology>${escapeXml(t)}</technology>`).join("\n")}
${spec.technology_stack.map((t) => `${indent}${indent}<technology>${escapeXml(t)}</technology>`).join('\n')}
${indent}</technology_stack>
${indent}<core_capabilities>
${spec.core_capabilities.map((c) => `${indent}${indent}<capability>${escapeXml(c)}</capability>`).join("\n")}
${spec.core_capabilities.map((c) => `${indent}${indent}<capability>${escapeXml(c)}</capability>`).join('\n')}
${indent}</core_capabilities>
${indent}<implemented_features>
@@ -51,13 +51,13 @@ ${indent}${indent}${indent}<name>${escapeXml(f.name)}</name>
${indent}${indent}${indent}<description>${escapeXml(f.description)}</description>${
f.file_locations && f.file_locations.length > 0
? `\n${indent}${indent}${indent}<file_locations>
${f.file_locations.map((loc) => `${indent}${indent}${indent}${indent}<location>${escapeXml(loc)}</location>`).join("\n")}
${f.file_locations.map((loc) => `${indent}${indent}${indent}${indent}<location>${escapeXml(loc)}</location>`).join('\n')}
${indent}${indent}${indent}</file_locations>`
: ""
: ''
}
${indent}${indent}</feature>`
)
.join("\n")}
.join('\n')}
${indent}</implemented_features>`;
// Optional sections
@@ -65,7 +65,7 @@ ${indent}</implemented_features>`;
xml += `
${indent}<additional_requirements>
${spec.additional_requirements.map((r) => `${indent}${indent}<requirement>${escapeXml(r)}</requirement>`).join("\n")}
${spec.additional_requirements.map((r) => `${indent}${indent}<requirement>${escapeXml(r)}</requirement>`).join('\n')}
${indent}</additional_requirements>`;
}
@@ -73,7 +73,7 @@ ${indent}</additional_requirements>`;
xml += `
${indent}<development_guidelines>
${spec.development_guidelines.map((g) => `${indent}${indent}<guideline>${escapeXml(g)}</guideline>`).join("\n")}
${spec.development_guidelines.map((g) => `${indent}${indent}<guideline>${escapeXml(g)}</guideline>`).join('\n')}
${indent}</development_guidelines>`;
}
@@ -89,7 +89,7 @@ ${indent}${indent}${indent}<status>${escapeXml(r.status)}</status>
${indent}${indent}${indent}<description>${escapeXml(r.description)}</description>
${indent}${indent}</phase>`
)
.join("\n")}
.join('\n')}
${indent}</implementation_roadmap>`;
}

View File

@@ -4,7 +4,7 @@
* Supports API key authentication via header or environment variable.
*/
import type { Request, Response, NextFunction } from "express";
import type { Request, Response, NextFunction } from 'express';
// API key from environment (optional - if not set, auth is disabled)
const API_KEY = process.env.AUTOMAKER_API_KEY;
@@ -23,12 +23,12 @@ export function authMiddleware(req: Request, res: Response, next: NextFunction):
}
// Check for API key in header
const providedKey = req.headers["x-api-key"] as string | undefined;
const providedKey = req.headers['x-api-key'] as string | undefined;
if (!providedKey) {
res.status(401).json({
success: false,
error: "Authentication required. Provide X-API-Key header.",
error: 'Authentication required. Provide X-API-Key header.',
});
return;
}
@@ -36,7 +36,7 @@ export function authMiddleware(req: Request, res: Response, next: NextFunction):
if (providedKey !== API_KEY) {
res.status(403).json({
success: false,
error: "Invalid API key.",
error: 'Invalid API key.',
});
return;
}
@@ -57,6 +57,6 @@ export function isAuthEnabled(): boolean {
export function getAuthStatus(): { enabled: boolean; method: string } {
return {
enabled: !!API_KEY,
method: API_KEY ? "api_key" : "none",
method: API_KEY ? 'api_key' : 'none',
};
}

View File

@@ -2,7 +2,7 @@
* Event emitter for streaming events to WebSocket clients
*/
import type { EventType, EventCallback } from "@automaker/types";
import type { EventType, EventCallback } from '@automaker/types';
// Re-export event types from shared package
export type { EventType, EventCallback };
@@ -21,7 +21,7 @@ export function createEventEmitter(): EventEmitter {
try {
callback(type, payload);
} catch (error) {
console.error("Error in event subscriber:", error);
console.error('Error in event subscriber:', error);
}
}
},

View File

@@ -3,7 +3,7 @@
* This file exists for backward compatibility with existing imports
*/
import { secureFs } from "@automaker/platform";
import { secureFs } from '@automaker/platform';
export const {
access,

View File

@@ -4,8 +4,8 @@
* try-catch block in every route handler
*/
import type { Request, Response, NextFunction } from "express";
import { validatePath, PathNotAllowedError } from "@automaker/platform";
import type { Request, Response, NextFunction } from 'express';
import { validatePath, PathNotAllowedError } from '@automaker/platform';
/**
* Creates a middleware that validates specified path parameters in req.body
@@ -24,7 +24,7 @@ export function validatePathParams(...paramNames: string[]) {
try {
for (const paramName of paramNames) {
// Handle optional parameters (paramName?)
if (paramName.endsWith("?")) {
if (paramName.endsWith('?')) {
const actualName = paramName.slice(0, -1);
const value = req.body[actualName];
if (value) {
@@ -34,7 +34,7 @@ export function validatePathParams(...paramNames: string[]) {
}
// Handle array parameters (paramName[])
if (paramName.endsWith("[]")) {
if (paramName.endsWith('[]')) {
const actualName = paramName.slice(0, -2);
const values = req.body[actualName];
if (Array.isArray(values) && values.length > 0) {

View File

@@ -9,7 +9,7 @@ import type {
InstallationStatus,
ValidationResult,
ModelDefinition,
} from "./types.js";
} from './types.js';
/**
* Base provider class that all provider implementations must extend
@@ -33,9 +33,7 @@ export abstract class BaseProvider {
* @param options Execution options
* @returns AsyncGenerator yielding provider messages
*/
abstract executeQuery(
options: ExecuteOptions
): AsyncGenerator<ProviderMessage>;
abstract executeQuery(options: ExecuteOptions): AsyncGenerator<ProviderMessage>;
/**
* Detect if the provider is installed and configured
@@ -59,7 +57,7 @@ export abstract class BaseProvider {
// Base validation (can be overridden)
if (!this.config) {
errors.push("Provider config is missing");
errors.push('Provider config is missing');
}
return {
@@ -76,7 +74,7 @@ export abstract class BaseProvider {
*/
supportsFeature(feature: string): boolean {
// Default implementation - override in subclasses
const commonFeatures = ["tools", "text"];
const commonFeatures = ['tools', 'text'];
return commonFeatures.includes(feature);
}

View File

@@ -6,9 +6,9 @@
* new providers (Cursor, OpenCode, etc.) trivial - just add one line.
*/
import { BaseProvider } from "./base-provider.js";
import { ClaudeProvider } from "./claude-provider.js";
import type { InstallationStatus } from "./types.js";
import { BaseProvider } from './base-provider.js';
import { ClaudeProvider } from './claude-provider.js';
import type { InstallationStatus } from './types.js';
export class ProviderFactory {
/**
@@ -21,10 +21,7 @@ export class ProviderFactory {
const lowerModel = modelId.toLowerCase();
// Claude models (claude-*, opus, sonnet, haiku)
if (
lowerModel.startsWith("claude-") ||
["haiku", "sonnet", "opus"].includes(lowerModel)
) {
if (lowerModel.startsWith('claude-') || ['haiku', 'sonnet', 'opus'].includes(lowerModel)) {
return new ClaudeProvider();
}
@@ -37,9 +34,7 @@ export class ProviderFactory {
// }
// Default to Claude for unknown models
console.warn(
`[ProviderFactory] Unknown model prefix for "${modelId}", defaulting to Claude`
);
console.warn(`[ProviderFactory] Unknown model prefix for "${modelId}", defaulting to Claude`);
return new ClaudeProvider();
}
@@ -58,9 +53,7 @@ export class ProviderFactory {
*
* @returns Map of provider name to installation status
*/
static async checkAllProviders(): Promise<
Record<string, InstallationStatus>
> {
static async checkAllProviders(): Promise<Record<string, InstallationStatus>> {
const providers = this.getAllProviders();
const statuses: Record<string, InstallationStatus> = {};
@@ -83,8 +76,8 @@ export class ProviderFactory {
const lowerName = name.toLowerCase();
switch (lowerName) {
case "claude":
case "anthropic":
case 'claude':
case 'anthropic':
return new ClaudeProvider();
// Future providers:

View File

@@ -15,7 +15,7 @@ export interface ProviderConfig {
* Message in conversation history
*/
export interface ConversationMessage {
role: "user" | "assistant";
role: 'user' | 'assistant';
content: string | Array<{ type: string; text?: string; source?: object }>;
}
@@ -39,7 +39,7 @@ export interface ExecuteOptions {
* Content block in a provider message (matches Claude SDK format)
*/
export interface ContentBlock {
type: "text" | "tool_use" | "thinking" | "tool_result";
type: 'text' | 'tool_use' | 'thinking' | 'tool_result';
text?: string;
thinking?: string;
name?: string;
@@ -52,11 +52,11 @@ export interface ContentBlock {
* Message returned by a provider (matches Claude SDK streaming format)
*/
export interface ProviderMessage {
type: "assistant" | "user" | "error" | "result";
subtype?: "success" | "error";
type: 'assistant' | 'user' | 'error' | 'result';
subtype?: 'success' | 'error';
session_id?: string;
message?: {
role: "user" | "assistant";
role: 'user' | 'assistant';
content: ContentBlock[];
};
result?: string;
@@ -71,7 +71,7 @@ export interface InstallationStatus {
installed: boolean;
path?: string;
version?: string;
method?: "cli" | "npm" | "brew" | "sdk";
method?: 'cli' | 'npm' | 'brew' | 'sdk';
hasApiKey?: boolean;
authenticated?: boolean;
error?: string;
@@ -99,6 +99,6 @@ export interface ModelDefinition {
maxOutputTokens?: number;
supportsVision?: boolean;
supportsTools?: boolean;
tier?: "basic" | "standard" | "premium";
tier?: 'basic' | 'standard' | 'premium';
default?: boolean;
}

View File

@@ -2,13 +2,10 @@
* Common utilities for agent routes
*/
import { createLogger } from "@automaker/utils";
import {
getErrorMessage as getErrorMessageShared,
createLogError,
} from "../common.js";
import { createLogger } from '@automaker/utils';
import { getErrorMessage as getErrorMessageShared, createLogError } from '../common.js';
const logger = createLogger("Agent");
const logger = createLogger('Agent');
// Re-export shared utilities
export { getErrorMessageShared as getErrorMessage };

View File

@@ -2,29 +2,30 @@
* Agent routes - HTTP API for Claude agent interactions
*/
import { Router } from "express";
import { AgentService } from "../../services/agent-service.js";
import type { EventEmitter } from "../../lib/events.js";
import { validatePathParams } from "../../middleware/validate-paths.js";
import { createStartHandler } from "./routes/start.js";
import { createSendHandler } from "./routes/send.js";
import { createHistoryHandler } from "./routes/history.js";
import { createStopHandler } from "./routes/stop.js";
import { createClearHandler } from "./routes/clear.js";
import { createModelHandler } from "./routes/model.js";
import { Router } from 'express';
import { AgentService } from '../../services/agent-service.js';
import type { EventEmitter } from '../../lib/events.js';
import { validatePathParams } from '../../middleware/validate-paths.js';
import { createStartHandler } from './routes/start.js';
import { createSendHandler } from './routes/send.js';
import { createHistoryHandler } from './routes/history.js';
import { createStopHandler } from './routes/stop.js';
import { createClearHandler } from './routes/clear.js';
import { createModelHandler } from './routes/model.js';
export function createAgentRoutes(
agentService: AgentService,
_events: EventEmitter
): Router {
export function createAgentRoutes(agentService: AgentService, _events: EventEmitter): Router {
const router = Router();
router.post("/start", validatePathParams("workingDirectory?"), createStartHandler(agentService));
router.post("/send", validatePathParams("workingDirectory?", "imagePaths[]"), createSendHandler(agentService));
router.post("/history", createHistoryHandler(agentService));
router.post("/stop", createStopHandler(agentService));
router.post("/clear", createClearHandler(agentService));
router.post("/model", createModelHandler(agentService));
router.post('/start', validatePathParams('workingDirectory?'), createStartHandler(agentService));
router.post(
'/send',
validatePathParams('workingDirectory?', 'imagePaths[]'),
createSendHandler(agentService)
);
router.post('/history', createHistoryHandler(agentService));
router.post('/stop', createStopHandler(agentService));
router.post('/clear', createClearHandler(agentService));
router.post('/model', createModelHandler(agentService));
return router;
}

View File

@@ -2,9 +2,9 @@
* POST /clear endpoint - Clear conversation
*/
import type { Request, Response } from "express";
import { AgentService } from "../../../services/agent-service.js";
import { getErrorMessage, logError } from "../common.js";
import type { Request, Response } from 'express';
import { AgentService } from '../../../services/agent-service.js';
import { getErrorMessage, logError } from '../common.js';
export function createClearHandler(agentService: AgentService) {
return async (req: Request, res: Response): Promise<void> => {
@@ -12,16 +12,14 @@ export function createClearHandler(agentService: AgentService) {
const { sessionId } = req.body as { sessionId: string };
if (!sessionId) {
res
.status(400)
.json({ success: false, error: "sessionId is required" });
res.status(400).json({ success: false, error: 'sessionId is required' });
return;
}
const result = await agentService.clearSession(sessionId);
res.json(result);
} catch (error) {
logError(error, "Clear session failed");
logError(error, 'Clear session failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};

View File

@@ -2,9 +2,9 @@
* POST /history endpoint - Get conversation history
*/
import type { Request, Response } from "express";
import { AgentService } from "../../../services/agent-service.js";
import { getErrorMessage, logError } from "../common.js";
import type { Request, Response } from 'express';
import { AgentService } from '../../../services/agent-service.js';
import { getErrorMessage, logError } from '../common.js';
export function createHistoryHandler(agentService: AgentService) {
return async (req: Request, res: Response): Promise<void> => {
@@ -12,16 +12,14 @@ export function createHistoryHandler(agentService: AgentService) {
const { sessionId } = req.body as { sessionId: string };
if (!sessionId) {
res
.status(400)
.json({ success: false, error: "sessionId is required" });
res.status(400).json({ success: false, error: 'sessionId is required' });
return;
}
const result = agentService.getHistory(sessionId);
res.json(result);
} catch (error) {
logError(error, "Get history failed");
logError(error, 'Get history failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};

View File

@@ -2,9 +2,9 @@
* POST /model endpoint - Set session model
*/
import type { Request, Response } from "express";
import { AgentService } from "../../../services/agent-service.js";
import { getErrorMessage, logError } from "../common.js";
import type { Request, Response } from 'express';
import { AgentService } from '../../../services/agent-service.js';
import { getErrorMessage, logError } from '../common.js';
export function createModelHandler(agentService: AgentService) {
return async (req: Request, res: Response): Promise<void> => {
@@ -15,16 +15,14 @@ export function createModelHandler(agentService: AgentService) {
};
if (!sessionId || !model) {
res
.status(400)
.json({ success: false, error: "sessionId and model are required" });
res.status(400).json({ success: false, error: 'sessionId and model are required' });
return;
}
const result = await agentService.setSessionModel(sessionId, model);
res.json({ success: result });
} catch (error) {
logError(error, "Set session model failed");
logError(error, 'Set session model failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};

View File

@@ -2,28 +2,27 @@
* POST /send endpoint - Send a message
*/
import type { Request, Response } from "express";
import { AgentService } from "../../../services/agent-service.js";
import { createLogger } from "@automaker/utils";
import { getErrorMessage, logError } from "../common.js";
const logger = createLogger("Agent");
import type { Request, Response } from 'express';
import { AgentService } from '../../../services/agent-service.js';
import { createLogger } from '@automaker/utils';
import { getErrorMessage, logError } from '../common.js';
const logger = createLogger('Agent');
export function createSendHandler(agentService: AgentService) {
return async (req: Request, res: Response): Promise<void> => {
try {
const { sessionId, message, workingDirectory, imagePaths, model } =
req.body as {
sessionId: string;
message: string;
workingDirectory?: string;
imagePaths?: string[];
model?: string;
};
const { sessionId, message, workingDirectory, imagePaths, model } = req.body as {
sessionId: string;
message: string;
workingDirectory?: string;
imagePaths?: string[];
model?: string;
};
if (!sessionId || !message) {
res.status(400).json({
success: false,
error: "sessionId and message are required",
error: 'sessionId and message are required',
});
return;
}
@@ -38,13 +37,13 @@ export function createSendHandler(agentService: AgentService) {
model,
})
.catch((error) => {
logError(error, "Send message failed (background)");
logError(error, 'Send message failed (background)');
});
// Return immediately - responses come via WebSocket
res.json({ success: true, message: "Message sent" });
res.json({ success: true, message: 'Message sent' });
} catch (error) {
logError(error, "Send message failed");
logError(error, 'Send message failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};

View File

@@ -2,11 +2,11 @@
* POST /start endpoint - Start a conversation
*/
import type { Request, Response } from "express";
import { AgentService } from "../../../services/agent-service.js";
import { createLogger } from "@automaker/utils";
import { getErrorMessage, logError } from "../common.js";
const logger = createLogger("Agent");
import type { Request, Response } from 'express';
import { AgentService } from '../../../services/agent-service.js';
import { createLogger } from '@automaker/utils';
import { getErrorMessage, logError } from '../common.js';
const logger = createLogger('Agent');
export function createStartHandler(agentService: AgentService) {
return async (req: Request, res: Response): Promise<void> => {
@@ -17,9 +17,7 @@ export function createStartHandler(agentService: AgentService) {
};
if (!sessionId) {
res
.status(400)
.json({ success: false, error: "sessionId is required" });
res.status(400).json({ success: false, error: 'sessionId is required' });
return;
}
@@ -30,7 +28,7 @@ export function createStartHandler(agentService: AgentService) {
res.json(result);
} catch (error) {
logError(error, "Start conversation failed");
logError(error, 'Start conversation failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};

View File

@@ -2,9 +2,9 @@
* POST /stop endpoint - Stop execution
*/
import type { Request, Response } from "express";
import { AgentService } from "../../../services/agent-service.js";
import { getErrorMessage, logError } from "../common.js";
import type { Request, Response } from 'express';
import { AgentService } from '../../../services/agent-service.js';
import { getErrorMessage, logError } from '../common.js';
export function createStopHandler(agentService: AgentService) {
return async (req: Request, res: Response): Promise<void> => {
@@ -12,16 +12,14 @@ export function createStopHandler(agentService: AgentService) {
const { sessionId } = req.body as { sessionId: string };
if (!sessionId) {
res
.status(400)
.json({ success: false, error: "sessionId is required" });
res.status(400).json({ success: false, error: 'sessionId is required' });
return;
}
const result = await agentService.stopExecution(sessionId);
res.json(result);
} catch (error) {
logError(error, "Stop execution failed");
logError(error, 'Stop execution failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};

View File

@@ -2,9 +2,9 @@
* Common utilities and state management for spec regeneration
*/
import { createLogger } from "@automaker/utils";
import { createLogger } from '@automaker/utils';
const logger = createLogger("SpecRegeneration");
const logger = createLogger('SpecRegeneration');
// Shared state for tracking generation status - private
let isRunning = false;
@@ -23,10 +23,7 @@ export function getSpecRegenerationStatus(): {
/**
* Set the running state and abort controller
*/
export function setRunningState(
running: boolean,
controller: AbortController | null = null
): void {
export function setRunningState(running: boolean, controller: AbortController | null = null): void {
isRunning = running;
currentAbortController = controller;
}
@@ -40,14 +37,12 @@ export function logAuthStatus(context: string): void {
logger.info(`${context} - Auth Status:`);
logger.info(
` ANTHROPIC_API_KEY: ${
hasApiKey
? "SET (" + process.env.ANTHROPIC_API_KEY?.substring(0, 20) + "...)"
: "NOT SET"
hasApiKey ? 'SET (' + process.env.ANTHROPIC_API_KEY?.substring(0, 20) + '...)' : 'NOT SET'
}`
);
if (!hasApiKey) {
logger.warn("⚠️ WARNING: No authentication configured! SDK will fail.");
logger.warn('⚠️ WARNING: No authentication configured! SDK will fail.');
}
}
@@ -56,16 +51,13 @@ export function logAuthStatus(context: string): void {
*/
export function logError(error: unknown, context: string): void {
logger.error(`${context}:`);
logger.error("Error name:", (error as any)?.name);
logger.error("Error message:", (error as Error)?.message);
logger.error("Error stack:", (error as Error)?.stack);
logger.error(
"Full error object:",
JSON.stringify(error, Object.getOwnPropertyNames(error), 2)
);
logger.error('Error name:', (error as any)?.name);
logger.error('Error message:', (error as Error)?.message);
logger.error('Error stack:', (error as Error)?.stack);
logger.error('Full error object:', JSON.stringify(error, Object.getOwnPropertyNames(error), 2));
}
import { getErrorMessage as getErrorMessageShared } from "../common.js";
import { getErrorMessage as getErrorMessageShared } from '../common.js';
// Re-export shared utility
export { getErrorMessageShared as getErrorMessage };

View File

@@ -2,25 +2,22 @@
* Spec Regeneration routes - HTTP API for AI-powered spec generation
*/
import { Router } from "express";
import type { EventEmitter } from "../../lib/events.js";
import { createCreateHandler } from "./routes/create.js";
import { createGenerateHandler } from "./routes/generate.js";
import { createGenerateFeaturesHandler } from "./routes/generate-features.js";
import { createStopHandler } from "./routes/stop.js";
import { createStatusHandler } from "./routes/status.js";
import { Router } from 'express';
import type { EventEmitter } from '../../lib/events.js';
import { createCreateHandler } from './routes/create.js';
import { createGenerateHandler } from './routes/generate.js';
import { createGenerateFeaturesHandler } from './routes/generate-features.js';
import { createStopHandler } from './routes/stop.js';
import { createStatusHandler } from './routes/status.js';
export function createSpecRegenerationRoutes(events: EventEmitter): Router {
const router = Router();
router.post("/create", createCreateHandler(events));
router.post("/generate", createGenerateHandler(events));
router.post("/generate-features", createGenerateFeaturesHandler(events));
router.post("/stop", createStopHandler());
router.get("/status", createStatusHandler());
router.post('/create', createCreateHandler(events));
router.post('/generate', createGenerateHandler(events));
router.post('/generate-features', createGenerateFeaturesHandler(events));
router.post('/stop', createStopHandler());
router.get('/status', createStatusHandler());
return router;
}

View File

@@ -2,24 +2,24 @@
* POST /create endpoint - Create project spec from overview
*/
import type { Request, Response } from "express";
import type { EventEmitter } from "../../../lib/events.js";
import { createLogger } from "@automaker/utils";
import type { Request, Response } from 'express';
import type { EventEmitter } from '../../../lib/events.js';
import { createLogger } from '@automaker/utils';
import {
getSpecRegenerationStatus,
setRunningState,
logAuthStatus,
logError,
getErrorMessage,
} from "../common.js";
import { generateSpec } from "../generate-spec.js";
} from '../common.js';
import { generateSpec } from '../generate-spec.js';
const logger = createLogger("SpecRegeneration");
const logger = createLogger('SpecRegeneration');
export function createCreateHandler(events: EventEmitter) {
return async (req: Request, res: Response): Promise<void> => {
logger.info("========== /create endpoint called ==========");
logger.debug("Request body:", JSON.stringify(req.body, null, 2));
logger.info('========== /create endpoint called ==========');
logger.debug('Request body:', JSON.stringify(req.body, null, 2));
try {
const { projectPath, projectOverview, generateFeatures, analyzeProject, maxFeatures } =
@@ -31,37 +31,34 @@ export function createCreateHandler(events: EventEmitter) {
maxFeatures?: number;
};
logger.debug("Parsed params:");
logger.debug(" projectPath:", projectPath);
logger.debug(
" projectOverview length:",
`${projectOverview?.length || 0} chars`
);
logger.debug(" generateFeatures:", generateFeatures);
logger.debug(" analyzeProject:", analyzeProject);
logger.debug(" maxFeatures:", maxFeatures);
logger.debug('Parsed params:');
logger.debug(' projectPath:', projectPath);
logger.debug(' projectOverview length:', `${projectOverview?.length || 0} chars`);
logger.debug(' generateFeatures:', generateFeatures);
logger.debug(' analyzeProject:', analyzeProject);
logger.debug(' maxFeatures:', maxFeatures);
if (!projectPath || !projectOverview) {
logger.error("Missing required parameters");
logger.error('Missing required parameters');
res.status(400).json({
success: false,
error: "projectPath and projectOverview required",
error: 'projectPath and projectOverview required',
});
return;
}
const { isRunning } = getSpecRegenerationStatus();
if (isRunning) {
logger.warn("Generation already running, rejecting request");
res.json({ success: false, error: "Spec generation already running" });
logger.warn('Generation already running, rejecting request');
res.json({ success: false, error: 'Spec generation already running' });
return;
}
logAuthStatus("Before starting generation");
logAuthStatus('Before starting generation');
const abortController = new AbortController();
setRunningState(true, abortController);
logger.info("Starting background generation task...");
logger.info('Starting background generation task...');
// Start generation in background
generateSpec(
@@ -74,24 +71,22 @@ export function createCreateHandler(events: EventEmitter) {
maxFeatures
)
.catch((error) => {
logError(error, "Generation failed with error");
events.emit("spec-regeneration:event", {
type: "spec_regeneration_error",
logError(error, 'Generation failed with error');
events.emit('spec-regeneration:event', {
type: 'spec_regeneration_error',
error: getErrorMessage(error),
projectPath: projectPath,
});
})
.finally(() => {
logger.info("Generation task finished (success or error)");
logger.info('Generation task finished (success or error)');
setRunningState(false, null);
});
logger.info(
"Returning success response (generation running in background)"
);
logger.info('Returning success response (generation running in background)');
res.json({ success: true });
} catch (error) {
logError(error, "Create spec route handler failed");
logError(error, 'Create spec route handler failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};

View File

@@ -2,24 +2,24 @@
* POST /generate-features endpoint - Generate features from existing spec
*/
import type { Request, Response } from "express";
import type { EventEmitter } from "../../../lib/events.js";
import { createLogger } from "@automaker/utils";
import type { Request, Response } from 'express';
import type { EventEmitter } from '../../../lib/events.js';
import { createLogger } from '@automaker/utils';
import {
getSpecRegenerationStatus,
setRunningState,
logAuthStatus,
logError,
getErrorMessage,
} from "../common.js";
import { generateFeaturesFromSpec } from "../generate-features-from-spec.js";
} from '../common.js';
import { generateFeaturesFromSpec } from '../generate-features-from-spec.js';
const logger = createLogger("SpecRegeneration");
const logger = createLogger('SpecRegeneration');
export function createGenerateFeaturesHandler(events: EventEmitter) {
return async (req: Request, res: Response): Promise<void> => {
logger.info("========== /generate-features endpoint called ==========");
logger.debug("Request body:", JSON.stringify(req.body, null, 2));
logger.info('========== /generate-features endpoint called ==========');
logger.debug('Request body:', JSON.stringify(req.body, null, 2));
try {
const { projectPath, maxFeatures } = req.body as {
@@ -27,52 +27,45 @@ export function createGenerateFeaturesHandler(events: EventEmitter) {
maxFeatures?: number;
};
logger.debug("projectPath:", projectPath);
logger.debug("maxFeatures:", maxFeatures);
logger.debug('projectPath:', projectPath);
logger.debug('maxFeatures:', maxFeatures);
if (!projectPath) {
logger.error("Missing projectPath parameter");
res.status(400).json({ success: false, error: "projectPath required" });
logger.error('Missing projectPath parameter');
res.status(400).json({ success: false, error: 'projectPath required' });
return;
}
const { isRunning } = getSpecRegenerationStatus();
if (isRunning) {
logger.warn("Generation already running, rejecting request");
res.json({ success: false, error: "Generation already running" });
logger.warn('Generation already running, rejecting request');
res.json({ success: false, error: 'Generation already running' });
return;
}
logAuthStatus("Before starting feature generation");
logAuthStatus('Before starting feature generation');
const abortController = new AbortController();
setRunningState(true, abortController);
logger.info("Starting background feature generation task...");
logger.info('Starting background feature generation task...');
generateFeaturesFromSpec(
projectPath,
events,
abortController,
maxFeatures
)
generateFeaturesFromSpec(projectPath, events, abortController, maxFeatures)
.catch((error) => {
logError(error, "Feature generation failed with error");
events.emit("spec-regeneration:event", {
type: "features_error",
logError(error, 'Feature generation failed with error');
events.emit('spec-regeneration:event', {
type: 'features_error',
error: getErrorMessage(error),
});
})
.finally(() => {
logger.info("Feature generation task finished (success or error)");
logger.info('Feature generation task finished (success or error)');
setRunningState(false, null);
});
logger.info(
"Returning success response (generation running in background)"
);
logger.info('Returning success response (generation running in background)');
res.json({ success: true });
} catch (error) {
logError(error, "Generate features route handler failed");
logError(error, 'Generate features route handler failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};

View File

@@ -2,71 +2,63 @@
* POST /generate endpoint - Generate spec from project definition
*/
import type { Request, Response } from "express";
import type { EventEmitter } from "../../../lib/events.js";
import { createLogger } from "@automaker/utils";
import type { Request, Response } from 'express';
import type { EventEmitter } from '../../../lib/events.js';
import { createLogger } from '@automaker/utils';
import {
getSpecRegenerationStatus,
setRunningState,
logAuthStatus,
logError,
getErrorMessage,
} from "../common.js";
import { generateSpec } from "../generate-spec.js";
} from '../common.js';
import { generateSpec } from '../generate-spec.js';
const logger = createLogger("SpecRegeneration");
const logger = createLogger('SpecRegeneration');
export function createGenerateHandler(events: EventEmitter) {
return async (req: Request, res: Response): Promise<void> => {
logger.info("========== /generate endpoint called ==========");
logger.debug("Request body:", JSON.stringify(req.body, null, 2));
logger.info('========== /generate endpoint called ==========');
logger.debug('Request body:', JSON.stringify(req.body, null, 2));
try {
const {
projectPath,
projectDefinition,
generateFeatures,
analyzeProject,
maxFeatures,
} = req.body as {
projectPath: string;
projectDefinition: string;
generateFeatures?: boolean;
analyzeProject?: boolean;
maxFeatures?: number;
};
const { projectPath, projectDefinition, generateFeatures, analyzeProject, maxFeatures } =
req.body as {
projectPath: string;
projectDefinition: string;
generateFeatures?: boolean;
analyzeProject?: boolean;
maxFeatures?: number;
};
logger.debug("Parsed params:");
logger.debug(" projectPath:", projectPath);
logger.debug(
" projectDefinition length:",
`${projectDefinition?.length || 0} chars`
);
logger.debug(" generateFeatures:", generateFeatures);
logger.debug(" analyzeProject:", analyzeProject);
logger.debug(" maxFeatures:", maxFeatures);
logger.debug('Parsed params:');
logger.debug(' projectPath:', projectPath);
logger.debug(' projectDefinition length:', `${projectDefinition?.length || 0} chars`);
logger.debug(' generateFeatures:', generateFeatures);
logger.debug(' analyzeProject:', analyzeProject);
logger.debug(' maxFeatures:', maxFeatures);
if (!projectPath || !projectDefinition) {
logger.error("Missing required parameters");
logger.error('Missing required parameters');
res.status(400).json({
success: false,
error: "projectPath and projectDefinition required",
error: 'projectPath and projectDefinition required',
});
return;
}
const { isRunning } = getSpecRegenerationStatus();
if (isRunning) {
logger.warn("Generation already running, rejecting request");
res.json({ success: false, error: "Spec generation already running" });
logger.warn('Generation already running, rejecting request');
res.json({ success: false, error: 'Spec generation already running' });
return;
}
logAuthStatus("Before starting generation");
logAuthStatus('Before starting generation');
const abortController = new AbortController();
setRunningState(true, abortController);
logger.info("Starting background generation task...");
logger.info('Starting background generation task...');
generateSpec(
projectPath,
@@ -78,24 +70,22 @@ export function createGenerateHandler(events: EventEmitter) {
maxFeatures
)
.catch((error) => {
logError(error, "Generation failed with error");
events.emit("spec-regeneration:event", {
type: "spec_regeneration_error",
logError(error, 'Generation failed with error');
events.emit('spec-regeneration:event', {
type: 'spec_regeneration_error',
error: getErrorMessage(error),
projectPath: projectPath,
});
})
.finally(() => {
logger.info("Generation task finished (success or error)");
logger.info('Generation task finished (success or error)');
setRunningState(false, null);
});
logger.info(
"Returning success response (generation running in background)"
);
logger.info('Returning success response (generation running in background)');
res.json({ success: true });
} catch (error) {
logError(error, "Generate spec route handler failed");
logError(error, 'Generate spec route handler failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};

View File

@@ -2,8 +2,8 @@
* GET /status endpoint - Get generation status
*/
import type { Request, Response } from "express";
import { getSpecRegenerationStatus, getErrorMessage } from "../common.js";
import type { Request, Response } from 'express';
import { getSpecRegenerationStatus, getErrorMessage } from '../common.js';
export function createStatusHandler() {
return async (_req: Request, res: Response): Promise<void> => {

View File

@@ -2,12 +2,8 @@
* POST /stop endpoint - Stop generation
*/
import type { Request, Response } from "express";
import {
getSpecRegenerationStatus,
setRunningState,
getErrorMessage,
} from "../common.js";
import type { Request, Response } from 'express';
import { getSpecRegenerationStatus, setRunningState, getErrorMessage } from '../common.js';
export function createStopHandler() {
return async (_req: Request, res: Response): Promise<void> => {

View File

@@ -2,13 +2,10 @@
* Common utilities for auto-mode routes
*/
import { createLogger } from "@automaker/utils";
import {
getErrorMessage as getErrorMessageShared,
createLogError,
} from "../common.js";
import { createLogger } from '@automaker/utils';
import { getErrorMessage as getErrorMessageShared, createLogError } from '../common.js';
const logger = createLogger("AutoMode");
const logger = createLogger('AutoMode');
// Re-export shared utilities
export { getErrorMessageShared as getErrorMessage };

View File

@@ -4,37 +4,65 @@
* Uses the AutoModeService for real feature execution with Claude Agent SDK
*/
import { Router } from "express";
import type { AutoModeService } from "../../services/auto-mode-service.js";
import { validatePathParams } from "../../middleware/validate-paths.js";
import { createStopFeatureHandler } from "./routes/stop-feature.js";
import { createStatusHandler } from "./routes/status.js";
import { createRunFeatureHandler } from "./routes/run-feature.js";
import { createVerifyFeatureHandler } from "./routes/verify-feature.js";
import { createResumeFeatureHandler } from "./routes/resume-feature.js";
import { createContextExistsHandler } from "./routes/context-exists.js";
import { createAnalyzeProjectHandler } from "./routes/analyze-project.js";
import { createFollowUpFeatureHandler } from "./routes/follow-up-feature.js";
import { createCommitFeatureHandler } from "./routes/commit-feature.js";
import { createApprovePlanHandler } from "./routes/approve-plan.js";
import { Router } from 'express';
import type { AutoModeService } from '../../services/auto-mode-service.js';
import { validatePathParams } from '../../middleware/validate-paths.js';
import { createStopFeatureHandler } from './routes/stop-feature.js';
import { createStatusHandler } from './routes/status.js';
import { createRunFeatureHandler } from './routes/run-feature.js';
import { createVerifyFeatureHandler } from './routes/verify-feature.js';
import { createResumeFeatureHandler } from './routes/resume-feature.js';
import { createContextExistsHandler } from './routes/context-exists.js';
import { createAnalyzeProjectHandler } from './routes/analyze-project.js';
import { createFollowUpFeatureHandler } from './routes/follow-up-feature.js';
import { createCommitFeatureHandler } from './routes/commit-feature.js';
import { createApprovePlanHandler } from './routes/approve-plan.js';
export function createAutoModeRoutes(autoModeService: AutoModeService): Router {
const router = Router();
router.post("/stop-feature", createStopFeatureHandler(autoModeService));
router.post("/status", validatePathParams("projectPath?"), createStatusHandler(autoModeService));
router.post("/run-feature", validatePathParams("projectPath"), createRunFeatureHandler(autoModeService));
router.post("/verify-feature", validatePathParams("projectPath"), createVerifyFeatureHandler(autoModeService));
router.post("/resume-feature", validatePathParams("projectPath"), createResumeFeatureHandler(autoModeService));
router.post("/context-exists", validatePathParams("projectPath"), createContextExistsHandler(autoModeService));
router.post("/analyze-project", validatePathParams("projectPath"), createAnalyzeProjectHandler(autoModeService));
router.post('/stop-feature', createStopFeatureHandler(autoModeService));
router.post('/status', validatePathParams('projectPath?'), createStatusHandler(autoModeService));
router.post(
"/follow-up-feature",
validatePathParams("projectPath", "imagePaths[]"),
'/run-feature',
validatePathParams('projectPath'),
createRunFeatureHandler(autoModeService)
);
router.post(
'/verify-feature',
validatePathParams('projectPath'),
createVerifyFeatureHandler(autoModeService)
);
router.post(
'/resume-feature',
validatePathParams('projectPath'),
createResumeFeatureHandler(autoModeService)
);
router.post(
'/context-exists',
validatePathParams('projectPath'),
createContextExistsHandler(autoModeService)
);
router.post(
'/analyze-project',
validatePathParams('projectPath'),
createAnalyzeProjectHandler(autoModeService)
);
router.post(
'/follow-up-feature',
validatePathParams('projectPath', 'imagePaths[]'),
createFollowUpFeatureHandler(autoModeService)
);
router.post("/commit-feature", validatePathParams("projectPath", "worktreePath?"), createCommitFeatureHandler(autoModeService));
router.post("/approve-plan", validatePathParams("projectPath"), createApprovePlanHandler(autoModeService));
router.post(
'/commit-feature',
validatePathParams('projectPath', 'worktreePath?'),
createCommitFeatureHandler(autoModeService)
);
router.post(
'/approve-plan',
validatePathParams('projectPath'),
createApprovePlanHandler(autoModeService)
);
return router;
}

View File

@@ -2,12 +2,12 @@
* POST /analyze-project endpoint - Analyze project
*/
import type { Request, Response } from "express";
import type { AutoModeService } from "../../../services/auto-mode-service.js";
import { createLogger } from "@automaker/utils";
import { getErrorMessage, logError } from "../common.js";
import type { Request, Response } from 'express';
import type { AutoModeService } from '../../../services/auto-mode-service.js';
import { createLogger } from '@automaker/utils';
import { getErrorMessage, logError } from '../common.js';
const logger = createLogger("AutoMode");
const logger = createLogger('AutoMode');
export function createAnalyzeProjectHandler(autoModeService: AutoModeService) {
return async (req: Request, res: Response): Promise<void> => {
@@ -15,9 +15,7 @@ export function createAnalyzeProjectHandler(autoModeService: AutoModeService) {
const { projectPath } = req.body as { projectPath: string };
if (!projectPath) {
res
.status(400)
.json({ success: false, error: "projectPath is required" });
res.status(400).json({ success: false, error: 'projectPath is required' });
return;
}
@@ -26,9 +24,9 @@ export function createAnalyzeProjectHandler(autoModeService: AutoModeService) {
logger.error(`[AutoMode] Project analysis error:`, error);
});
res.json({ success: true, message: "Project analysis started" });
res.json({ success: true, message: 'Project analysis started' });
} catch (error) {
logError(error, "Analyze project failed");
logError(error, 'Analyze project failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};

View File

@@ -2,12 +2,12 @@
* POST /approve-plan endpoint - Approve or reject a generated plan/spec
*/
import type { Request, Response } from "express";
import type { AutoModeService } from "../../../services/auto-mode-service.js";
import { createLogger } from "@automaker/utils";
import { getErrorMessage, logError } from "../common.js";
import type { Request, Response } from 'express';
import type { AutoModeService } from '../../../services/auto-mode-service.js';
import { createLogger } from '@automaker/utils';
import { getErrorMessage, logError } from '../common.js';
const logger = createLogger("AutoMode");
const logger = createLogger('AutoMode');
export function createApprovePlanHandler(autoModeService: AutoModeService) {
return async (req: Request, res: Response): Promise<void> => {
@@ -23,15 +23,15 @@ export function createApprovePlanHandler(autoModeService: AutoModeService) {
if (!featureId) {
res.status(400).json({
success: false,
error: "featureId is required",
error: 'featureId is required',
});
return;
}
if (typeof approved !== "boolean") {
if (typeof approved !== 'boolean') {
res.status(400).json({
success: false,
error: "approved must be a boolean",
error: 'approved must be a boolean',
});
return;
}
@@ -41,9 +41,9 @@ export function createApprovePlanHandler(autoModeService: AutoModeService) {
// This supports cases where the server restarted while waiting for approval
logger.info(
`[AutoMode] Plan ${approved ? "approved" : "rejected"} for feature ${featureId}${
editedPlan ? " (with edits)" : ""
}${feedback ? ` - Feedback: ${feedback}` : ""}`
`[AutoMode] Plan ${approved ? 'approved' : 'rejected'} for feature ${featureId}${
editedPlan ? ' (with edits)' : ''
}${feedback ? ` - Feedback: ${feedback}` : ''}`
);
// Resolve the pending approval (with recovery support)
@@ -67,11 +67,11 @@ export function createApprovePlanHandler(autoModeService: AutoModeService) {
success: true,
approved,
message: approved
? "Plan approved - implementation will continue"
: "Plan rejected - feature execution stopped",
? 'Plan approved - implementation will continue'
: 'Plan rejected - feature execution stopped',
});
} catch (error) {
logError(error, "Approve plan failed");
logError(error, 'Approve plan failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};

View File

@@ -2,9 +2,9 @@
* POST /commit-feature endpoint - Commit feature changes
*/
import type { Request, Response } from "express";
import type { AutoModeService } from "../../../services/auto-mode-service.js";
import { getErrorMessage, logError } from "../common.js";
import type { Request, Response } from 'express';
import type { AutoModeService } from '../../../services/auto-mode-service.js';
import { getErrorMessage, logError } from '../common.js';
export function createCommitFeatureHandler(autoModeService: AutoModeService) {
return async (req: Request, res: Response): Promise<void> => {
@@ -16,23 +16,17 @@ export function createCommitFeatureHandler(autoModeService: AutoModeService) {
};
if (!projectPath || !featureId) {
res
.status(400)
.json({
success: false,
error: "projectPath and featureId are required",
});
res.status(400).json({
success: false,
error: 'projectPath and featureId are required',
});
return;
}
const commitHash = await autoModeService.commitFeature(
projectPath,
featureId,
worktreePath
);
const commitHash = await autoModeService.commitFeature(projectPath, featureId, worktreePath);
res.json({ success: true, commitHash });
} catch (error) {
logError(error, "Commit feature failed");
logError(error, 'Commit feature failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};

View File

@@ -2,9 +2,9 @@
* POST /context-exists endpoint - Check if context exists for a feature
*/
import type { Request, Response } from "express";
import type { AutoModeService } from "../../../services/auto-mode-service.js";
import { getErrorMessage, logError } from "../common.js";
import type { Request, Response } from 'express';
import type { AutoModeService } from '../../../services/auto-mode-service.js';
import { getErrorMessage, logError } from '../common.js';
export function createContextExistsHandler(autoModeService: AutoModeService) {
return async (req: Request, res: Response): Promise<void> => {
@@ -15,22 +15,17 @@ export function createContextExistsHandler(autoModeService: AutoModeService) {
};
if (!projectPath || !featureId) {
res
.status(400)
.json({
success: false,
error: "projectPath and featureId are required",
});
res.status(400).json({
success: false,
error: 'projectPath and featureId are required',
});
return;
}
const exists = await autoModeService.contextExists(
projectPath,
featureId
);
const exists = await autoModeService.contextExists(projectPath, featureId);
res.json({ success: true, exists });
} catch (error) {
logError(error, "Check context exists failed");
logError(error, 'Check context exists failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};

View File

@@ -2,29 +2,28 @@
* POST /follow-up-feature endpoint - Follow up on a feature
*/
import type { Request, Response } from "express";
import type { AutoModeService } from "../../../services/auto-mode-service.js";
import { createLogger } from "@automaker/utils";
import { getErrorMessage, logError } from "../common.js";
import type { Request, Response } from 'express';
import type { AutoModeService } from '../../../services/auto-mode-service.js';
import { createLogger } from '@automaker/utils';
import { getErrorMessage, logError } from '../common.js';
const logger = createLogger("AutoMode");
const logger = createLogger('AutoMode');
export function createFollowUpFeatureHandler(autoModeService: AutoModeService) {
return async (req: Request, res: Response): Promise<void> => {
try {
const { projectPath, featureId, prompt, imagePaths, useWorktrees } =
req.body as {
projectPath: string;
featureId: string;
prompt: string;
imagePaths?: string[];
useWorktrees?: boolean;
};
const { projectPath, featureId, prompt, imagePaths, useWorktrees } = req.body as {
projectPath: string;
featureId: string;
prompt: string;
imagePaths?: string[];
useWorktrees?: boolean;
};
if (!projectPath || !featureId || !prompt) {
res.status(400).json({
success: false,
error: "projectPath, featureId, and prompt are required",
error: 'projectPath, featureId, and prompt are required',
});
return;
}
@@ -32,18 +31,9 @@ export function createFollowUpFeatureHandler(autoModeService: AutoModeService) {
// Start follow-up in background
// followUpFeature derives workDir from feature.branchName
autoModeService
.followUpFeature(
projectPath,
featureId,
prompt,
imagePaths,
useWorktrees ?? true
)
.followUpFeature(projectPath, featureId, prompt, imagePaths, useWorktrees ?? true)
.catch((error) => {
logger.error(
`[AutoMode] Follow up feature ${featureId} error:`,
error
);
logger.error(`[AutoMode] Follow up feature ${featureId} error:`, error);
})
.finally(() => {
// Release the starting slot when follow-up completes (success or error)
@@ -52,7 +42,7 @@ export function createFollowUpFeatureHandler(autoModeService: AutoModeService) {
res.json({ success: true });
} catch (error) {
logError(error, "Follow up feature failed");
logError(error, 'Follow up feature failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};

View File

@@ -2,12 +2,12 @@
* POST /resume-feature endpoint - Resume a feature
*/
import type { Request, Response } from "express";
import type { AutoModeService } from "../../../services/auto-mode-service.js";
import { createLogger } from "@automaker/utils";
import { getErrorMessage, logError } from "../common.js";
import type { Request, Response } from 'express';
import type { AutoModeService } from '../../../services/auto-mode-service.js';
import { createLogger } from '@automaker/utils';
import { getErrorMessage, logError } from '../common.js';
const logger = createLogger("AutoMode");
const logger = createLogger('AutoMode');
export function createResumeFeatureHandler(autoModeService: AutoModeService) {
return async (req: Request, res: Response): Promise<void> => {
@@ -21,7 +21,7 @@ export function createResumeFeatureHandler(autoModeService: AutoModeService) {
if (!projectPath || !featureId) {
res.status(400).json({
success: false,
error: "projectPath and featureId are required",
error: 'projectPath and featureId are required',
});
return;
}
@@ -36,7 +36,7 @@ export function createResumeFeatureHandler(autoModeService: AutoModeService) {
res.json({ success: true });
} catch (error) {
logError(error, "Resume feature failed");
logError(error, 'Resume feature failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};

View File

@@ -2,12 +2,12 @@
* POST /run-feature endpoint - Run a single feature
*/
import type { Request, Response } from "express";
import type { AutoModeService } from "../../../services/auto-mode-service.js";
import { createLogger } from "@automaker/utils";
import { getErrorMessage, logError } from "../common.js";
import type { Request, Response } from 'express';
import type { AutoModeService } from '../../../services/auto-mode-service.js';
import { createLogger } from '@automaker/utils';
import { getErrorMessage, logError } from '../common.js';
const logger = createLogger("AutoMode");
const logger = createLogger('AutoMode');
export function createRunFeatureHandler(autoModeService: AutoModeService) {
return async (req: Request, res: Response): Promise<void> => {
@@ -21,7 +21,7 @@ export function createRunFeatureHandler(autoModeService: AutoModeService) {
if (!projectPath || !featureId) {
res.status(400).json({
success: false,
error: "projectPath and featureId are required",
error: 'projectPath and featureId are required',
});
return;
}
@@ -40,7 +40,7 @@ export function createRunFeatureHandler(autoModeService: AutoModeService) {
res.json({ success: true });
} catch (error) {
logError(error, "Run feature failed");
logError(error, 'Run feature failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};

View File

@@ -2,9 +2,9 @@
* POST /status endpoint - Get auto mode status
*/
import type { Request, Response } from "express";
import type { AutoModeService } from "../../../services/auto-mode-service.js";
import { getErrorMessage, logError } from "../common.js";
import type { Request, Response } from 'express';
import type { AutoModeService } from '../../../services/auto-mode-service.js';
import { getErrorMessage, logError } from '../common.js';
export function createStatusHandler(autoModeService: AutoModeService) {
return async (req: Request, res: Response): Promise<void> => {
@@ -15,7 +15,7 @@ export function createStatusHandler(autoModeService: AutoModeService) {
...status,
});
} catch (error) {
logError(error, "Get status failed");
logError(error, 'Get status failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};

View File

@@ -2,9 +2,9 @@
* POST /stop-feature endpoint - Stop a specific feature
*/
import type { Request, Response } from "express";
import type { AutoModeService } from "../../../services/auto-mode-service.js";
import { getErrorMessage, logError } from "../common.js";
import type { Request, Response } from 'express';
import type { AutoModeService } from '../../../services/auto-mode-service.js';
import { getErrorMessage, logError } from '../common.js';
export function createStopFeatureHandler(autoModeService: AutoModeService) {
return async (req: Request, res: Response): Promise<void> => {
@@ -12,16 +12,14 @@ export function createStopFeatureHandler(autoModeService: AutoModeService) {
const { featureId } = req.body as { featureId: string };
if (!featureId) {
res
.status(400)
.json({ success: false, error: "featureId is required" });
res.status(400).json({ success: false, error: 'featureId is required' });
return;
}
const stopped = await autoModeService.stopFeature(featureId);
res.json({ success: true, stopped });
} catch (error) {
logError(error, "Stop feature failed");
logError(error, 'Stop feature failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};

View File

@@ -2,9 +2,9 @@
* POST /verify-feature endpoint - Verify a feature
*/
import type { Request, Response } from "express";
import type { AutoModeService } from "../../../services/auto-mode-service.js";
import { getErrorMessage, logError } from "../common.js";
import type { Request, Response } from 'express';
import type { AutoModeService } from '../../../services/auto-mode-service.js';
import { getErrorMessage, logError } from '../common.js';
export function createVerifyFeatureHandler(autoModeService: AutoModeService) {
return async (req: Request, res: Response): Promise<void> => {
@@ -15,22 +15,17 @@ export function createVerifyFeatureHandler(autoModeService: AutoModeService) {
};
if (!projectPath || !featureId) {
res
.status(400)
.json({
success: false,
error: "projectPath and featureId are required",
});
res.status(400).json({
success: false,
error: 'projectPath and featureId are required',
});
return;
}
const passes = await autoModeService.verifyFeature(
projectPath,
featureId
);
const passes = await autoModeService.verifyFeature(projectPath, featureId);
res.json({ success: true, passes });
} catch (error) {
logError(error, "Verify feature failed");
logError(error, 'Verify feature failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};

View File

@@ -1,18 +1,18 @@
import { Router, Request, Response } from "express";
import { ClaudeUsageService } from "../../services/claude-usage-service.js";
import { Router, Request, Response } from 'express';
import { ClaudeUsageService } from '../../services/claude-usage-service.js';
export function createClaudeRoutes(service: ClaudeUsageService): Router {
const router = Router();
// Get current usage (fetches from Claude CLI)
router.get("/usage", async (req: Request, res: Response) => {
router.get('/usage', async (req: Request, res: Response) => {
try {
// Check if Claude CLI is available first
const isAvailable = await service.isAvailable();
if (!isAvailable) {
res.status(503).json({
error: "Claude CLI not found",
message: "Please install Claude Code CLI and run 'claude login' to authenticate"
error: 'Claude CLI not found',
message: "Please install Claude Code CLI and run 'claude login' to authenticate",
});
return;
}
@@ -20,20 +20,20 @@ export function createClaudeRoutes(service: ClaudeUsageService): Router {
const usage = await service.fetchUsageData();
res.json(usage);
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
const message = error instanceof Error ? error.message : 'Unknown error';
if (message.includes("Authentication required") || message.includes("token_expired")) {
if (message.includes('Authentication required') || message.includes('token_expired')) {
res.status(401).json({
error: "Authentication required",
message: "Please run 'claude login' to authenticate"
error: 'Authentication required',
message: "Please run 'claude login' to authenticate",
});
} else if (message.includes("timed out")) {
} else if (message.includes('timed out')) {
res.status(504).json({
error: "Command timed out",
message: "The Claude CLI took too long to respond"
error: 'Command timed out',
message: 'The Claude CLI took too long to respond',
});
} else {
console.error("Error fetching usage:", error);
console.error('Error fetching usage:', error);
res.status(500).json({ error: message });
}
}

View File

@@ -29,7 +29,7 @@ export type ClaudeUsage = {
export type ClaudeStatus = {
indicator: {
color: "green" | "yellow" | "orange" | "red" | "gray";
color: 'green' | 'yellow' | 'orange' | 'red' | 'gray';
};
description: string;
};

View File

@@ -2,7 +2,7 @@
* Common utilities shared across all route modules
*/
import { createLogger } from "@automaker/utils";
import { createLogger } from '@automaker/utils';
// Re-export git utilities from shared package
export {
@@ -16,7 +16,7 @@ export {
listAllFilesInDirectory,
generateDiffsForNonGitDirectory,
getGitRepositoryDiffs,
} from "@automaker/git-utils";
} from '@automaker/git-utils';
type Logger = ReturnType<typeof createLogger>;
@@ -24,7 +24,7 @@ type Logger = ReturnType<typeof createLogger>;
* Get error message from error object
*/
export function getErrorMessage(error: unknown): string {
return error instanceof Error ? error.message : "Unknown error";
return error instanceof Error ? error.message : 'Unknown error';
}
/**

View File

@@ -5,8 +5,8 @@
* with different enhancement modes (improve, expand, simplify, etc.)
*/
import { Router } from "express";
import { createEnhanceHandler } from "./routes/enhance.js";
import { Router } from 'express';
import { createEnhanceHandler } from './routes/enhance.js';
/**
* Create the enhance-prompt router
@@ -16,7 +16,7 @@ import { createEnhanceHandler } from "./routes/enhance.js";
export function createEnhancePromptRoutes(): Router {
const router = Router();
router.post("/", createEnhanceHandler());
router.post('/', createEnhanceHandler());
return router;
}

View File

@@ -5,19 +5,19 @@
* Supports modes: improve, technical, simplify, acceptance
*/
import type { Request, Response } from "express";
import { query } from "@anthropic-ai/claude-agent-sdk";
import { createLogger } from "@automaker/utils";
import { resolveModelString } from "@automaker/model-resolver";
import { CLAUDE_MODEL_MAP } from "@automaker/types";
import type { Request, Response } from 'express';
import { query } from '@anthropic-ai/claude-agent-sdk';
import { createLogger } from '@automaker/utils';
import { resolveModelString } from '@automaker/model-resolver';
import { CLAUDE_MODEL_MAP } from '@automaker/types';
import {
getSystemPrompt,
buildUserPrompt,
isValidEnhancementMode,
type EnhancementMode,
} from "../../../lib/enhancement-prompts.js";
} from '../../../lib/enhancement-prompts.js';
const logger = createLogger("EnhancePrompt");
const logger = createLogger('EnhancePrompt');
/**
* Request body for the enhance endpoint
@@ -63,16 +63,16 @@ async function extractTextFromStream(
};
}>
): Promise<string> {
let responseText = "";
let responseText = '';
for await (const msg of stream) {
if (msg.type === "assistant" && msg.message?.content) {
if (msg.type === 'assistant' && msg.message?.content) {
for (const block of msg.message.content) {
if (block.type === "text" && block.text) {
if (block.type === 'text' && block.text) {
responseText += block.text;
}
}
} else if (msg.type === "result" && msg.subtype === "success") {
} else if (msg.type === 'result' && msg.subtype === 'success') {
responseText = msg.result || responseText;
}
}
@@ -85,29 +85,25 @@ async function extractTextFromStream(
*
* @returns Express request handler for text enhancement
*/
export function createEnhanceHandler(): (
req: Request,
res: Response
) => Promise<void> {
export function createEnhanceHandler(): (req: Request, res: Response) => Promise<void> {
return async (req: Request, res: Response): Promise<void> => {
try {
const { originalText, enhancementMode, model } =
req.body as EnhanceRequestBody;
const { originalText, enhancementMode, model } = req.body as EnhanceRequestBody;
// Validate required fields
if (!originalText || typeof originalText !== "string") {
if (!originalText || typeof originalText !== 'string') {
const response: EnhanceErrorResponse = {
success: false,
error: "originalText is required and must be a string",
error: 'originalText is required and must be a string',
};
res.status(400).json(response);
return;
}
if (!enhancementMode || typeof enhancementMode !== "string") {
if (!enhancementMode || typeof enhancementMode !== 'string') {
const response: EnhanceErrorResponse = {
success: false,
error: "enhancementMode is required and must be a string",
error: 'enhancementMode is required and must be a string',
};
res.status(400).json(response);
return;
@@ -118,7 +114,7 @@ export function createEnhanceHandler(): (
if (trimmedText.length === 0) {
const response: EnhanceErrorResponse = {
success: false,
error: "originalText cannot be empty",
error: 'originalText cannot be empty',
};
res.status(400).json(response);
return;
@@ -128,11 +124,9 @@ export function createEnhanceHandler(): (
const normalizedMode = enhancementMode.toLowerCase();
const validMode: EnhancementMode = isValidEnhancementMode(normalizedMode)
? normalizedMode
: "improve";
: 'improve';
logger.info(
`Enhancing text with mode: ${validMode}, length: ${trimmedText.length} chars`
);
logger.info(`Enhancing text with mode: ${validMode}, length: ${trimmedText.length} chars`);
// Get the system prompt for this mode
const systemPrompt = getSystemPrompt(validMode);
@@ -155,7 +149,7 @@ export function createEnhanceHandler(): (
systemPrompt,
maxTurns: 1,
allowedTools: [],
permissionMode: "acceptEdits",
permissionMode: 'acceptEdits',
},
});
@@ -163,18 +157,16 @@ export function createEnhanceHandler(): (
const enhancedText = await extractTextFromStream(stream);
if (!enhancedText || enhancedText.trim().length === 0) {
logger.warn("Received empty response from Claude");
logger.warn('Received empty response from Claude');
const response: EnhanceErrorResponse = {
success: false,
error: "Failed to generate enhanced text - empty response",
error: 'Failed to generate enhanced text - empty response',
};
res.status(500).json(response);
return;
}
logger.info(
`Enhancement complete, output length: ${enhancedText.length} chars`
);
logger.info(`Enhancement complete, output length: ${enhancedText.length} chars`);
const response: EnhanceSuccessResponse = {
success: true,
@@ -182,9 +174,8 @@ export function createEnhanceHandler(): (
};
res.json(response);
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : "Unknown error occurred";
logger.error("Enhancement failed:", errorMessage);
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
logger.error('Enhancement failed:', errorMessage);
const response: EnhanceErrorResponse = {
success: false,

View File

@@ -2,13 +2,10 @@
* Common utilities for features routes
*/
import { createLogger } from "@automaker/utils";
import {
getErrorMessage as getErrorMessageShared,
createLogError,
} from "../common.js";
import { createLogger } from '@automaker/utils';
import { getErrorMessage as getErrorMessageShared, createLogError } from '../common.js';
const logger = createLogger("Features");
const logger = createLogger('Features');
// Re-export shared utilities
export { getErrorMessageShared as getErrorMessage };

View File

@@ -2,27 +2,27 @@
* Features routes - HTTP API for feature management
*/
import { Router } from "express";
import { FeatureLoader } from "../../services/feature-loader.js";
import { validatePathParams } from "../../middleware/validate-paths.js";
import { createListHandler } from "./routes/list.js";
import { createGetHandler } from "./routes/get.js";
import { createCreateHandler } from "./routes/create.js";
import { createUpdateHandler } from "./routes/update.js";
import { createDeleteHandler } from "./routes/delete.js";
import { createAgentOutputHandler } from "./routes/agent-output.js";
import { createGenerateTitleHandler } from "./routes/generate-title.js";
import { Router } from 'express';
import { FeatureLoader } from '../../services/feature-loader.js';
import { validatePathParams } from '../../middleware/validate-paths.js';
import { createListHandler } from './routes/list.js';
import { createGetHandler } from './routes/get.js';
import { createCreateHandler } from './routes/create.js';
import { createUpdateHandler } from './routes/update.js';
import { createDeleteHandler } from './routes/delete.js';
import { createAgentOutputHandler } from './routes/agent-output.js';
import { createGenerateTitleHandler } from './routes/generate-title.js';
export function createFeaturesRoutes(featureLoader: FeatureLoader): Router {
const router = Router();
router.post("/list", validatePathParams("projectPath"), createListHandler(featureLoader));
router.post("/get", validatePathParams("projectPath"), createGetHandler(featureLoader));
router.post("/create", validatePathParams("projectPath"), createCreateHandler(featureLoader));
router.post("/update", validatePathParams("projectPath"), createUpdateHandler(featureLoader));
router.post("/delete", validatePathParams("projectPath"), createDeleteHandler(featureLoader));
router.post("/agent-output", createAgentOutputHandler(featureLoader));
router.post("/generate-title", createGenerateTitleHandler());
router.post('/list', validatePathParams('projectPath'), createListHandler(featureLoader));
router.post('/get', validatePathParams('projectPath'), createGetHandler(featureLoader));
router.post('/create', validatePathParams('projectPath'), createCreateHandler(featureLoader));
router.post('/update', validatePathParams('projectPath'), createUpdateHandler(featureLoader));
router.post('/delete', validatePathParams('projectPath'), createDeleteHandler(featureLoader));
router.post('/agent-output', createAgentOutputHandler(featureLoader));
router.post('/generate-title', createGenerateTitleHandler());
return router;
}

View File

@@ -2,9 +2,9 @@
* POST /agent-output endpoint - Get agent output for a feature
*/
import type { Request, Response } from "express";
import { FeatureLoader } from "../../../services/feature-loader.js";
import { getErrorMessage, logError } from "../common.js";
import type { Request, Response } from 'express';
import { FeatureLoader } from '../../../services/feature-loader.js';
import { getErrorMessage, logError } from '../common.js';
export function createAgentOutputHandler(featureLoader: FeatureLoader) {
return async (req: Request, res: Response): Promise<void> => {
@@ -15,22 +15,17 @@ export function createAgentOutputHandler(featureLoader: FeatureLoader) {
};
if (!projectPath || !featureId) {
res
.status(400)
.json({
success: false,
error: "projectPath and featureId are required",
});
res.status(400).json({
success: false,
error: 'projectPath and featureId are required',
});
return;
}
const content = await featureLoader.getAgentOutput(
projectPath,
featureId
);
const content = await featureLoader.getAgentOutput(projectPath, featureId);
res.json({ success: true, content });
} catch (error) {
logError(error, "Get agent output failed");
logError(error, 'Get agent output failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};

View File

@@ -2,10 +2,10 @@
* POST /create endpoint - Create a new feature
*/
import type { Request, Response } from "express";
import { FeatureLoader } from "../../../services/feature-loader.js";
import type { Feature } from "@automaker/types";
import { getErrorMessage, logError } from "../common.js";
import type { Request, Response } from 'express';
import { FeatureLoader } from '../../../services/feature-loader.js';
import type { Feature } from '@automaker/types';
import { getErrorMessage, logError } from '../common.js';
export function createCreateHandler(featureLoader: FeatureLoader) {
return async (req: Request, res: Response): Promise<void> => {
@@ -18,7 +18,7 @@ export function createCreateHandler(featureLoader: FeatureLoader) {
if (!projectPath || !feature) {
res.status(400).json({
success: false,
error: "projectPath and feature are required",
error: 'projectPath and feature are required',
});
return;
}
@@ -26,7 +26,7 @@ export function createCreateHandler(featureLoader: FeatureLoader) {
const created = await featureLoader.create(projectPath, feature);
res.json({ success: true, feature: created });
} catch (error) {
logError(error, "Create feature failed");
logError(error, 'Create feature failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};

View File

@@ -2,9 +2,9 @@
* POST /delete endpoint - Delete a feature
*/
import type { Request, Response } from "express";
import { FeatureLoader } from "../../../services/feature-loader.js";
import { getErrorMessage, logError } from "../common.js";
import type { Request, Response } from 'express';
import { FeatureLoader } from '../../../services/feature-loader.js';
import { getErrorMessage, logError } from '../common.js';
export function createDeleteHandler(featureLoader: FeatureLoader) {
return async (req: Request, res: Response): Promise<void> => {
@@ -15,19 +15,17 @@ export function createDeleteHandler(featureLoader: FeatureLoader) {
};
if (!projectPath || !featureId) {
res
.status(400)
.json({
success: false,
error: "projectPath and featureId are required",
});
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) {
logError(error, "Delete feature failed");
logError(error, 'Delete feature failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};

View File

@@ -4,12 +4,12 @@
* Uses Claude Haiku to generate a short, descriptive title from feature description.
*/
import type { Request, Response } from "express";
import { query } from "@anthropic-ai/claude-agent-sdk";
import { createLogger } from "@automaker/utils";
import { CLAUDE_MODEL_MAP } from "@automaker/model-resolver";
import type { Request, Response } from 'express';
import { query } from '@anthropic-ai/claude-agent-sdk';
import { createLogger } from '@automaker/utils';
import { CLAUDE_MODEL_MAP } from '@automaker/model-resolver';
const logger = createLogger("GenerateTitle");
const logger = createLogger('GenerateTitle');
interface GenerateTitleRequestBody {
description: string;
@@ -44,16 +44,16 @@ async function extractTextFromStream(
};
}>
): Promise<string> {
let responseText = "";
let responseText = '';
for await (const msg of stream) {
if (msg.type === "assistant" && msg.message?.content) {
if (msg.type === 'assistant' && msg.message?.content) {
for (const block of msg.message.content) {
if (block.type === "text" && block.text) {
if (block.type === 'text' && block.text) {
responseText += block.text;
}
}
} else if (msg.type === "result" && msg.subtype === "success") {
} else if (msg.type === 'result' && msg.subtype === 'success') {
responseText = msg.result || responseText;
}
}
@@ -61,18 +61,15 @@ async function extractTextFromStream(
return responseText;
}
export function createGenerateTitleHandler(): (
req: Request,
res: Response
) => Promise<void> {
export function createGenerateTitleHandler(): (req: Request, res: Response) => Promise<void> {
return async (req: Request, res: Response): Promise<void> => {
try {
const { description } = req.body as GenerateTitleRequestBody;
if (!description || typeof description !== "string") {
if (!description || typeof description !== 'string') {
const response: GenerateTitleErrorResponse = {
success: false,
error: "description is required and must be a string",
error: 'description is required and must be a string',
};
res.status(400).json(response);
return;
@@ -82,7 +79,7 @@ export function createGenerateTitleHandler(): (
if (trimmedDescription.length === 0) {
const response: GenerateTitleErrorResponse = {
success: false,
error: "description cannot be empty",
error: 'description cannot be empty',
};
res.status(400).json(response);
return;
@@ -99,17 +96,17 @@ export function createGenerateTitleHandler(): (
systemPrompt: SYSTEM_PROMPT,
maxTurns: 1,
allowedTools: [],
permissionMode: "acceptEdits",
permissionMode: 'acceptEdits',
},
});
const title = await extractTextFromStream(stream);
if (!title || title.trim().length === 0) {
logger.warn("Received empty response from Claude");
logger.warn('Received empty response from Claude');
const response: GenerateTitleErrorResponse = {
success: false,
error: "Failed to generate title - empty response",
error: 'Failed to generate title - empty response',
};
res.status(500).json(response);
return;
@@ -123,9 +120,8 @@ export function createGenerateTitleHandler(): (
};
res.json(response);
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : "Unknown error occurred";
logger.error("Title generation failed:", errorMessage);
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
logger.error('Title generation failed:', errorMessage);
const response: GenerateTitleErrorResponse = {
success: false,

View File

@@ -2,9 +2,9 @@
* POST /get endpoint - Get a single feature
*/
import type { Request, Response } from "express";
import { FeatureLoader } from "../../../services/feature-loader.js";
import { getErrorMessage, logError } from "../common.js";
import type { Request, Response } from 'express';
import { FeatureLoader } from '../../../services/feature-loader.js';
import { getErrorMessage, logError } from '../common.js';
export function createGetHandler(featureLoader: FeatureLoader) {
return async (req: Request, res: Response): Promise<void> => {
@@ -15,24 +15,22 @@ export function createGetHandler(featureLoader: FeatureLoader) {
};
if (!projectPath || !featureId) {
res
.status(400)
.json({
success: false,
error: "projectPath and featureId are required",
});
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" });
res.status(404).json({ success: false, error: 'Feature not found' });
return;
}
res.json({ success: true, feature });
} catch (error) {
logError(error, "Get feature failed");
logError(error, 'Get feature failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};

View File

@@ -2,9 +2,9 @@
* POST /list endpoint - List all features for a project
*/
import type { Request, Response } from "express";
import { FeatureLoader } from "../../../services/feature-loader.js";
import { getErrorMessage, logError } from "../common.js";
import type { Request, Response } from 'express';
import { FeatureLoader } from '../../../services/feature-loader.js';
import { getErrorMessage, logError } from '../common.js';
export function createListHandler(featureLoader: FeatureLoader) {
return async (req: Request, res: Response): Promise<void> => {
@@ -12,16 +12,14 @@ export function createListHandler(featureLoader: FeatureLoader) {
const { projectPath } = req.body as { projectPath: string };
if (!projectPath) {
res
.status(400)
.json({ success: false, error: "projectPath is required" });
res.status(400).json({ success: false, error: 'projectPath is required' });
return;
}
const features = await featureLoader.getAll(projectPath);
res.json({ success: true, features });
} catch (error) {
logError(error, "List features failed");
logError(error, 'List features failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};

View File

@@ -2,10 +2,10 @@
* POST /update endpoint - Update a feature
*/
import type { Request, Response } from "express";
import { FeatureLoader } from "../../../services/feature-loader.js";
import type { Feature } from "@automaker/types";
import { getErrorMessage, logError } from "../common.js";
import type { Request, Response } from 'express';
import { FeatureLoader } from '../../../services/feature-loader.js';
import type { Feature } from '@automaker/types';
import { getErrorMessage, logError } from '../common.js';
export function createUpdateHandler(featureLoader: FeatureLoader) {
return async (req: Request, res: Response): Promise<void> => {
@@ -19,19 +19,15 @@ export function createUpdateHandler(featureLoader: FeatureLoader) {
if (!projectPath || !featureId || !updates) {
res.status(400).json({
success: false,
error: "projectPath, featureId, and updates are required",
error: 'projectPath, featureId, and updates are required',
});
return;
}
const updated = await featureLoader.update(
projectPath,
featureId,
updates
);
const updated = await featureLoader.update(projectPath, featureId, updates);
res.json({ success: true, feature: updated });
} catch (error) {
logError(error, "Update feature failed");
logError(error, 'Update feature failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};

View File

@@ -2,13 +2,10 @@
* Common utilities for fs routes
*/
import { createLogger } from "@automaker/utils";
import {
getErrorMessage as getErrorMessageShared,
createLogError,
} from "../common.js";
import { createLogger } from '@automaker/utils';
import { getErrorMessage as getErrorMessageShared, createLogError } from '../common.js';
const logger = createLogger("FS");
const logger = createLogger('FS');
// Re-export shared utilities
export { getErrorMessageShared as getErrorMessage };

View File

@@ -3,40 +3,40 @@
* Provides REST API equivalents for Electron IPC file operations
*/
import { Router } from "express";
import type { EventEmitter } from "../../lib/events.js";
import { createReadHandler } from "./routes/read.js";
import { createWriteHandler } from "./routes/write.js";
import { createMkdirHandler } from "./routes/mkdir.js";
import { createReaddirHandler } from "./routes/readdir.js";
import { createExistsHandler } from "./routes/exists.js";
import { createStatHandler } from "./routes/stat.js";
import { createDeleteHandler } from "./routes/delete.js";
import { createValidatePathHandler } from "./routes/validate-path.js";
import { createResolveDirectoryHandler } from "./routes/resolve-directory.js";
import { createSaveImageHandler } from "./routes/save-image.js";
import { createBrowseHandler } from "./routes/browse.js";
import { createImageHandler } from "./routes/image.js";
import { createSaveBoardBackgroundHandler } from "./routes/save-board-background.js";
import { createDeleteBoardBackgroundHandler } from "./routes/delete-board-background.js";
import { Router } from 'express';
import type { EventEmitter } from '../../lib/events.js';
import { createReadHandler } from './routes/read.js';
import { createWriteHandler } from './routes/write.js';
import { createMkdirHandler } from './routes/mkdir.js';
import { createReaddirHandler } from './routes/readdir.js';
import { createExistsHandler } from './routes/exists.js';
import { createStatHandler } from './routes/stat.js';
import { createDeleteHandler } from './routes/delete.js';
import { createValidatePathHandler } from './routes/validate-path.js';
import { createResolveDirectoryHandler } from './routes/resolve-directory.js';
import { createSaveImageHandler } from './routes/save-image.js';
import { createBrowseHandler } from './routes/browse.js';
import { createImageHandler } from './routes/image.js';
import { createSaveBoardBackgroundHandler } from './routes/save-board-background.js';
import { createDeleteBoardBackgroundHandler } from './routes/delete-board-background.js';
export function createFsRoutes(_events: EventEmitter): Router {
const router = Router();
router.post("/read", createReadHandler());
router.post("/write", createWriteHandler());
router.post("/mkdir", createMkdirHandler());
router.post("/readdir", createReaddirHandler());
router.post("/exists", createExistsHandler());
router.post("/stat", createStatHandler());
router.post("/delete", createDeleteHandler());
router.post("/validate-path", createValidatePathHandler());
router.post("/resolve-directory", createResolveDirectoryHandler());
router.post("/save-image", createSaveImageHandler());
router.post("/browse", createBrowseHandler());
router.get("/image", createImageHandler());
router.post("/save-board-background", createSaveBoardBackgroundHandler());
router.post("/delete-board-background", createDeleteBoardBackgroundHandler());
router.post('/read', createReadHandler());
router.post('/write', createWriteHandler());
router.post('/mkdir', createMkdirHandler());
router.post('/readdir', createReaddirHandler());
router.post('/exists', createExistsHandler());
router.post('/stat', createStatHandler());
router.post('/delete', createDeleteHandler());
router.post('/validate-path', createValidatePathHandler());
router.post('/resolve-directory', createResolveDirectoryHandler());
router.post('/save-image', createSaveImageHandler());
router.post('/browse', createBrowseHandler());
router.get('/image', createImageHandler());
router.post('/save-board-background', createSaveBoardBackgroundHandler());
router.post('/delete-board-background', createDeleteBoardBackgroundHandler());
return router;
}

View File

@@ -2,13 +2,10 @@
* Common utilities for git routes
*/
import { createLogger } from "@automaker/utils";
import {
getErrorMessage as getErrorMessageShared,
createLogError,
} from "../common.js";
import { createLogger } from '@automaker/utils';
import { getErrorMessage as getErrorMessageShared, createLogError } from '../common.js';
const logger = createLogger("Git");
const logger = createLogger('Git');
// Re-export shared utilities
export { getErrorMessageShared as getErrorMessage };

View File

@@ -2,16 +2,16 @@
* Git routes - HTTP API for git operations (non-worktree)
*/
import { Router } from "express";
import { validatePathParams } from "../../middleware/validate-paths.js";
import { createDiffsHandler } from "./routes/diffs.js";
import { createFileDiffHandler } from "./routes/file-diff.js";
import { Router } from 'express';
import { validatePathParams } from '../../middleware/validate-paths.js';
import { createDiffsHandler } from './routes/diffs.js';
import { createFileDiffHandler } from './routes/file-diff.js';
export function createGitRoutes(): Router {
const router = Router();
router.post("/diffs", validatePathParams("projectPath"), createDiffsHandler());
router.post("/file-diff", validatePathParams("projectPath", "filePath"), createFileDiffHandler());
router.post('/diffs', validatePathParams('projectPath'), createDiffsHandler());
router.post('/file-diff', validatePathParams('projectPath', 'filePath'), createFileDiffHandler());
return router;
}

View File

@@ -2,9 +2,9 @@
* POST /diffs endpoint - Get diffs for the main project
*/
import type { Request, Response } from "express";
import { getErrorMessage, logError } from "../common.js";
import { getGitRepositoryDiffs } from "../../common.js";
import type { Request, Response } from 'express';
import { getErrorMessage, logError } from '../common.js';
import { getGitRepositoryDiffs } from '../../common.js';
export function createDiffsHandler() {
return async (req: Request, res: Response): Promise<void> => {
@@ -12,7 +12,7 @@ export function createDiffsHandler() {
const { projectPath } = req.body as { projectPath: string };
if (!projectPath) {
res.status(400).json({ success: false, error: "projectPath required" });
res.status(400).json({ success: false, error: 'projectPath required' });
return;
}
@@ -25,11 +25,11 @@ export function createDiffsHandler() {
hasChanges: result.hasChanges,
});
} catch (innerError) {
logError(innerError, "Git diff failed");
res.json({ success: true, diff: "", files: [], hasChanges: false });
logError(innerError, 'Git diff failed');
res.json({ success: true, diff: '', files: [], hasChanges: false });
}
} catch (error) {
logError(error, "Get diffs failed");
logError(error, 'Get diffs failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};

View File

@@ -2,11 +2,11 @@
* POST /file-diff endpoint - Get diff for a specific file
*/
import type { Request, Response } from "express";
import { exec } from "child_process";
import { promisify } from "util";
import { getErrorMessage, logError } from "../common.js";
import { generateSyntheticDiffForNewFile } from "../../common.js";
import type { Request, Response } from 'express';
import { exec } from 'child_process';
import { promisify } from 'util';
import { getErrorMessage, logError } from '../common.js';
import { generateSyntheticDiffForNewFile } from '../../common.js';
const execAsync = promisify(exec);
@@ -19,20 +19,17 @@ export function createFileDiffHandler() {
};
if (!projectPath || !filePath) {
res
.status(400)
.json({ success: false, error: "projectPath and filePath required" });
res.status(400).json({ success: false, error: 'projectPath and filePath required' });
return;
}
try {
// First check if the file is untracked
const { stdout: status } = await execAsync(
`git status --porcelain -- "${filePath}"`,
{ cwd: projectPath }
);
const { stdout: status } = await execAsync(`git status --porcelain -- "${filePath}"`, {
cwd: projectPath,
});
const isUntracked = status.trim().startsWith("??");
const isUntracked = status.trim().startsWith('??');
let diff: string;
if (isUntracked) {
@@ -40,23 +37,20 @@ export function createFileDiffHandler() {
diff = await generateSyntheticDiffForNewFile(projectPath, filePath);
} else {
// Use regular git diff for tracked files
const result = await execAsync(
`git diff HEAD -- "${filePath}"`,
{
cwd: projectPath,
maxBuffer: 10 * 1024 * 1024,
}
);
const result = await execAsync(`git diff HEAD -- "${filePath}"`, {
cwd: projectPath,
maxBuffer: 10 * 1024 * 1024,
});
diff = result.stdout;
}
res.json({ success: true, diff, filePath });
} catch (innerError) {
logError(innerError, "Git file diff failed");
res.json({ success: true, diff: "", filePath });
logError(innerError, 'Git file diff failed');
res.json({ success: true, diff: '', filePath });
}
} catch (error) {
logError(error, "Get file diff failed");
logError(error, 'Get file diff failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};

View File

@@ -2,13 +2,10 @@
* Common utilities for health routes
*/
import { createLogger } from "@automaker/utils";
import {
getErrorMessage as getErrorMessageShared,
createLogError,
} from "../common.js";
import { createLogger } from '@automaker/utils';
import { getErrorMessage as getErrorMessageShared, createLogError } from '../common.js';
const logger = createLogger("Health");
const logger = createLogger('Health');
// Re-export shared utilities
export { getErrorMessageShared as getErrorMessage };

View File

@@ -2,15 +2,15 @@
* Health check routes
*/
import { Router } from "express";
import { createIndexHandler } from "./routes/index.js";
import { createDetailedHandler } from "./routes/detailed.js";
import { Router } from 'express';
import { createIndexHandler } from './routes/index.js';
import { createDetailedHandler } from './routes/detailed.js';
export function createHealthRoutes(): Router {
const router = Router();
router.get("/", createIndexHandler());
router.get("/detailed", createDetailedHandler());
router.get('/', createIndexHandler());
router.get('/detailed', createDetailedHandler());
return router;
}

View File

@@ -2,18 +2,18 @@
* GET /detailed endpoint - Detailed health check
*/
import type { Request, Response } from "express";
import { getAuthStatus } from "../../../lib/auth.js";
import type { Request, Response } from 'express';
import { getAuthStatus } from '../../../lib/auth.js';
export function createDetailedHandler() {
return (_req: Request, res: Response): void => {
res.json({
status: "ok",
status: 'ok',
timestamp: new Date().toISOString(),
version: process.env.npm_package_version || "0.1.0",
version: process.env.npm_package_version || '0.1.0',
uptime: process.uptime(),
memory: process.memoryUsage(),
dataDir: process.env.DATA_DIR || "./data",
dataDir: process.env.DATA_DIR || './data',
auth: getAuthStatus(),
env: {
nodeVersion: process.version,

View File

@@ -2,14 +2,14 @@
* GET / endpoint - Basic health check
*/
import type { Request, Response } from "express";
import type { Request, Response } from 'express';
export function createIndexHandler() {
return (_req: Request, res: Response): void => {
res.json({
status: "ok",
status: 'ok',
timestamp: new Date().toISOString(),
version: process.env.npm_package_version || "0.1.0",
version: process.env.npm_package_version || '0.1.0',
});
};
}

View File

@@ -2,13 +2,10 @@
* Common utilities for models routes
*/
import { createLogger } from "@automaker/utils";
import {
getErrorMessage as getErrorMessageShared,
createLogError,
} from "../common.js";
import { createLogger } from '@automaker/utils';
import { getErrorMessage as getErrorMessageShared, createLogError } from '../common.js';
const logger = createLogger("Models");
const logger = createLogger('Models');
// Re-export shared utilities
export { getErrorMessageShared as getErrorMessage };

View File

@@ -2,15 +2,15 @@
* Models routes - HTTP API for model providers and availability
*/
import { Router } from "express";
import { createAvailableHandler } from "./routes/available.js";
import { createProvidersHandler } from "./routes/providers.js";
import { Router } from 'express';
import { createAvailableHandler } from './routes/available.js';
import { createProvidersHandler } from './routes/providers.js';
export function createModelsRoutes(): Router {
const router = Router();
router.get("/available", createAvailableHandler());
router.get("/providers", createProvidersHandler());
router.get('/available', createAvailableHandler());
router.get('/providers', createProvidersHandler());
return router;
}

View File

@@ -2,8 +2,8 @@
* GET /available endpoint - Get available models
*/
import type { Request, Response } from "express";
import { getErrorMessage, logError } from "../common.js";
import type { Request, Response } from 'express';
import { getErrorMessage, logError } from '../common.js';
interface ModelDefinition {
id: string;
@@ -20,36 +20,36 @@ export function createAvailableHandler() {
try {
const models: ModelDefinition[] = [
{
id: "claude-opus-4-5-20251101",
name: "Claude Opus 4.5",
provider: "anthropic",
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",
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",
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",
id: 'claude-3-5-haiku-20241022',
name: 'Claude 3.5 Haiku',
provider: 'anthropic',
contextWindow: 200000,
maxOutputTokens: 8192,
supportsVision: true,
@@ -59,7 +59,7 @@ export function createAvailableHandler() {
res.json({ success: true, models });
} catch (error) {
logError(error, "Get available models failed");
logError(error, 'Get available models failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};

View File

@@ -2,9 +2,9 @@
* GET /providers endpoint - Check provider status
*/
import type { Request, Response } from "express";
import { ProviderFactory } from "../../../providers/provider-factory.js";
import { getErrorMessage, logError } from "../common.js";
import type { Request, Response } from 'express';
import { ProviderFactory } from '../../../providers/provider-factory.js';
import { getErrorMessage, logError } from '../common.js';
export function createProvidersHandler() {
return async (_req: Request, res: Response): Promise<void> => {
@@ -21,7 +21,7 @@ export function createProvidersHandler() {
res.json({ success: true, providers });
} catch (error) {
logError(error, "Get providers failed");
logError(error, 'Get providers failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};

View File

@@ -2,13 +2,10 @@
* Common utilities for running-agents routes
*/
import { createLogger } from "@automaker/utils";
import {
getErrorMessage as getErrorMessageShared,
createLogError,
} from "../common.js";
import { createLogger } from '@automaker/utils';
import { getErrorMessage as getErrorMessageShared, createLogError } from '../common.js';
const logger = createLogger("RunningAgents");
const logger = createLogger('RunningAgents');
// Re-export shared utilities
export { getErrorMessageShared as getErrorMessage };

View File

@@ -2,16 +2,14 @@
* Running Agents routes - HTTP API for tracking active agent executions
*/
import { Router } from "express";
import type { AutoModeService } from "../../services/auto-mode-service.js";
import { createIndexHandler } from "./routes/index.js";
import { Router } from 'express';
import type { AutoModeService } from '../../services/auto-mode-service.js';
import { createIndexHandler } from './routes/index.js';
export function createRunningAgentsRoutes(
autoModeService: AutoModeService
): Router {
export function createRunningAgentsRoutes(autoModeService: AutoModeService): Router {
const router = Router();
router.get("/", createIndexHandler(autoModeService));
router.get('/', createIndexHandler(autoModeService));
return router;
}

View File

@@ -2,9 +2,9 @@
* GET / endpoint - Get all running agents
*/
import type { Request, Response } from "express";
import type { AutoModeService } from "../../../services/auto-mode-service.js";
import { getErrorMessage, logError } from "../common.js";
import type { Request, Response } from 'express';
import type { AutoModeService } from '../../../services/auto-mode-service.js';
import { getErrorMessage, logError } from '../common.js';
export function createIndexHandler(autoModeService: AutoModeService) {
return async (_req: Request, res: Response): Promise<void> => {
@@ -18,7 +18,7 @@ export function createIndexHandler(autoModeService: AutoModeService) {
totalCount: runningAgents.length,
});
} catch (error) {
logError(error, "Get running agents failed");
logError(error, 'Get running agents failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};

View File

@@ -2,13 +2,10 @@
* Common utilities for sessions routes
*/
import { createLogger } from "@automaker/utils";
import {
getErrorMessage as getErrorMessageShared,
createLogError,
} from "../common.js";
import { createLogger } from '@automaker/utils';
import { getErrorMessage as getErrorMessageShared, createLogError } from '../common.js';
const logger = createLogger("Sessions");
const logger = createLogger('Sessions');
// Re-export shared utilities
export { getErrorMessageShared as getErrorMessage };

View File

@@ -2,24 +2,24 @@
* Sessions routes - HTTP API for session management
*/
import { Router } from "express";
import { AgentService } from "../../services/agent-service.js";
import { createIndexHandler } from "./routes/index.js";
import { createCreateHandler } from "./routes/create.js";
import { createUpdateHandler } from "./routes/update.js";
import { createArchiveHandler } from "./routes/archive.js";
import { createUnarchiveHandler } from "./routes/unarchive.js";
import { createDeleteHandler } from "./routes/delete.js";
import { Router } from 'express';
import { AgentService } from '../../services/agent-service.js';
import { createIndexHandler } from './routes/index.js';
import { createCreateHandler } from './routes/create.js';
import { createUpdateHandler } from './routes/update.js';
import { createArchiveHandler } from './routes/archive.js';
import { createUnarchiveHandler } from './routes/unarchive.js';
import { createDeleteHandler } from './routes/delete.js';
export function createSessionsRoutes(agentService: AgentService): Router {
const router = Router();
router.get("/", createIndexHandler(agentService));
router.post("/", createCreateHandler(agentService));
router.put("/:sessionId", createUpdateHandler(agentService));
router.post("/:sessionId/archive", createArchiveHandler(agentService));
router.post("/:sessionId/unarchive", createUnarchiveHandler(agentService));
router.delete("/:sessionId", createDeleteHandler(agentService));
router.get('/', createIndexHandler(agentService));
router.post('/', createCreateHandler(agentService));
router.put('/:sessionId', createUpdateHandler(agentService));
router.post('/:sessionId/archive', createArchiveHandler(agentService));
router.post('/:sessionId/unarchive', createUnarchiveHandler(agentService));
router.delete('/:sessionId', createDeleteHandler(agentService));
return router;
}

View File

@@ -2,9 +2,9 @@
* POST /:sessionId/archive endpoint - Archive a session
*/
import type { Request, Response } from "express";
import { AgentService } from "../../../services/agent-service.js";
import { getErrorMessage, logError } from "../common.js";
import type { Request, Response } from 'express';
import { AgentService } from '../../../services/agent-service.js';
import { getErrorMessage, logError } from '../common.js';
export function createArchiveHandler(agentService: AgentService) {
return async (req: Request, res: Response): Promise<void> => {
@@ -13,13 +13,13 @@ export function createArchiveHandler(agentService: AgentService) {
const success = await agentService.archiveSession(sessionId);
if (!success) {
res.status(404).json({ success: false, error: "Session not found" });
res.status(404).json({ success: false, error: 'Session not found' });
return;
}
res.json({ success: true });
} catch (error) {
logError(error, "Archive session failed");
logError(error, 'Archive session failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};

View File

@@ -2,9 +2,9 @@
* POST / endpoint - Create a new session
*/
import type { Request, Response } from "express";
import { AgentService } from "../../../services/agent-service.js";
import { getErrorMessage, logError } from "../common.js";
import type { Request, Response } from 'express';
import { AgentService } from '../../../services/agent-service.js';
import { getErrorMessage, logError } from '../common.js';
export function createCreateHandler(agentService: AgentService) {
return async (req: Request, res: Response): Promise<void> => {
@@ -17,19 +17,14 @@ export function createCreateHandler(agentService: AgentService) {
};
if (!name) {
res.status(400).json({ success: false, error: "name is required" });
res.status(400).json({ success: false, error: 'name is required' });
return;
}
const session = await agentService.createSession(
name,
projectPath,
workingDirectory,
model
);
const session = await agentService.createSession(name, projectPath, workingDirectory, model);
res.json({ success: true, session });
} catch (error) {
logError(error, "Create session failed");
logError(error, 'Create session failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};

View File

@@ -2,9 +2,9 @@
* DELETE /:sessionId endpoint - Delete a session
*/
import type { Request, Response } from "express";
import { AgentService } from "../../../services/agent-service.js";
import { getErrorMessage, logError } from "../common.js";
import type { Request, Response } from 'express';
import { AgentService } from '../../../services/agent-service.js';
import { getErrorMessage, logError } from '../common.js';
export function createDeleteHandler(agentService: AgentService) {
return async (req: Request, res: Response): Promise<void> => {
@@ -13,13 +13,13 @@ export function createDeleteHandler(agentService: AgentService) {
const success = await agentService.deleteSession(sessionId);
if (!success) {
res.status(404).json({ success: false, error: "Session not found" });
res.status(404).json({ success: false, error: 'Session not found' });
return;
}
res.json({ success: true });
} catch (error) {
logError(error, "Delete session failed");
logError(error, 'Delete session failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};

View File

@@ -2,14 +2,14 @@
* GET / endpoint - List all sessions
*/
import type { Request, Response } from "express";
import { AgentService } from "../../../services/agent-service.js";
import { getErrorMessage, logError } from "../common.js";
import type { Request, Response } from 'express';
import { AgentService } from '../../../services/agent-service.js';
import { getErrorMessage, logError } from '../common.js';
export function createIndexHandler(agentService: AgentService) {
return async (req: Request, res: Response): Promise<void> => {
try {
const includeArchived = req.query.includeArchived === "true";
const includeArchived = req.query.includeArchived === 'true';
const sessionsRaw = await agentService.listSessions(includeArchived);
// Transform to match frontend SessionListItem interface
@@ -17,7 +17,7 @@ export function createIndexHandler(agentService: AgentService) {
sessionsRaw.map(async (s) => {
const messages = await agentService.loadSession(s.id);
const lastMessage = messages[messages.length - 1];
const preview = lastMessage?.content?.slice(0, 100) || "";
const preview = lastMessage?.content?.slice(0, 100) || '';
return {
id: s.id,
@@ -36,7 +36,7 @@ export function createIndexHandler(agentService: AgentService) {
res.json({ success: true, sessions });
} catch (error) {
logError(error, "List sessions failed");
logError(error, 'List sessions failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};

View File

@@ -2,9 +2,9 @@
* POST /:sessionId/unarchive endpoint - Unarchive a session
*/
import type { Request, Response } from "express";
import { AgentService } from "../../../services/agent-service.js";
import { getErrorMessage, logError } from "../common.js";
import type { Request, Response } from 'express';
import { AgentService } from '../../../services/agent-service.js';
import { getErrorMessage, logError } from '../common.js';
export function createUnarchiveHandler(agentService: AgentService) {
return async (req: Request, res: Response): Promise<void> => {
@@ -13,13 +13,13 @@ export function createUnarchiveHandler(agentService: AgentService) {
const success = await agentService.unarchiveSession(sessionId);
if (!success) {
res.status(404).json({ success: false, error: "Session not found" });
res.status(404).json({ success: false, error: 'Session not found' });
return;
}
res.json({ success: true });
} catch (error) {
logError(error, "Unarchive session failed");
logError(error, 'Unarchive session failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};

View File

@@ -2,9 +2,9 @@
* PUT /:sessionId endpoint - Update a session
*/
import type { Request, Response } from "express";
import { AgentService } from "../../../services/agent-service.js";
import { getErrorMessage, logError } from "../common.js";
import type { Request, Response } from 'express';
import { AgentService } from '../../../services/agent-service.js';
import { getErrorMessage, logError } from '../common.js';
export function createUpdateHandler(agentService: AgentService) {
return async (req: Request, res: Response): Promise<void> => {
@@ -22,13 +22,13 @@ export function createUpdateHandler(agentService: AgentService) {
model,
});
if (!session) {
res.status(404).json({ success: false, error: "Session not found" });
res.status(404).json({ success: false, error: 'Session not found' });
return;
}
res.json({ success: true, session });
} catch (error) {
logError(error, "Update session failed");
logError(error, 'Update session failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};

View File

@@ -5,14 +5,11 @@
* Re-exports error handling helpers from the parent routes module.
*/
import { createLogger } from "@automaker/utils";
import {
getErrorMessage as getErrorMessageShared,
createLogError,
} from "../common.js";
import { createLogger } from '@automaker/utils';
import { getErrorMessage as getErrorMessageShared, createLogError } from '../common.js';
/** Logger instance for settings-related operations */
export const logger = createLogger("Settings");
export const logger = createLogger('Settings');
/**
* Extract user-friendly error message from error objects

View File

@@ -12,17 +12,17 @@
* Mounted at /api/settings in the main server.
*/
import { Router } from "express";
import type { SettingsService } from "../../services/settings-service.js";
import { validatePathParams } from "../../middleware/validate-paths.js";
import { createGetGlobalHandler } from "./routes/get-global.js";
import { createUpdateGlobalHandler } from "./routes/update-global.js";
import { createGetCredentialsHandler } from "./routes/get-credentials.js";
import { createUpdateCredentialsHandler } from "./routes/update-credentials.js";
import { createGetProjectHandler } from "./routes/get-project.js";
import { createUpdateProjectHandler } from "./routes/update-project.js";
import { createMigrateHandler } from "./routes/migrate.js";
import { createStatusHandler } from "./routes/status.js";
import { Router } from 'express';
import type { SettingsService } from '../../services/settings-service.js';
import { validatePathParams } from '../../middleware/validate-paths.js';
import { createGetGlobalHandler } from './routes/get-global.js';
import { createUpdateGlobalHandler } from './routes/update-global.js';
import { createGetCredentialsHandler } from './routes/get-credentials.js';
import { createUpdateCredentialsHandler } from './routes/update-credentials.js';
import { createGetProjectHandler } from './routes/get-project.js';
import { createUpdateProjectHandler } from './routes/update-project.js';
import { createMigrateHandler } from './routes/migrate.js';
import { createStatusHandler } from './routes/status.js';
/**
* Create settings router with all endpoints
@@ -47,22 +47,30 @@ export function createSettingsRoutes(settingsService: SettingsService): Router {
const router = Router();
// Status endpoint (check if migration needed)
router.get("/status", createStatusHandler(settingsService));
router.get('/status', createStatusHandler(settingsService));
// Global settings
router.get("/global", createGetGlobalHandler(settingsService));
router.put("/global", createUpdateGlobalHandler(settingsService));
router.get('/global', createGetGlobalHandler(settingsService));
router.put('/global', createUpdateGlobalHandler(settingsService));
// Credentials (separate for security)
router.get("/credentials", createGetCredentialsHandler(settingsService));
router.put("/credentials", createUpdateCredentialsHandler(settingsService));
router.get('/credentials', createGetCredentialsHandler(settingsService));
router.put('/credentials', createUpdateCredentialsHandler(settingsService));
// Project settings
router.post("/project", validatePathParams("projectPath"), createGetProjectHandler(settingsService));
router.put("/project", validatePathParams("projectPath"), createUpdateProjectHandler(settingsService));
router.post(
'/project',
validatePathParams('projectPath'),
createGetProjectHandler(settingsService)
);
router.put(
'/project',
validatePathParams('projectPath'),
createUpdateProjectHandler(settingsService)
);
// Migration from localStorage
router.post("/migrate", createMigrateHandler(settingsService));
router.post('/migrate', createMigrateHandler(settingsService));
return router;
}

View File

@@ -8,9 +8,9 @@
* Response: `{ "success": true, "credentials": { anthropic } }`
*/
import type { Request, Response } from "express";
import type { SettingsService } from "../../../services/settings-service.js";
import { getErrorMessage, logError } from "../common.js";
import type { Request, Response } from 'express';
import type { SettingsService } from '../../../services/settings-service.js';
import { getErrorMessage, logError } from '../common.js';
/**
* Create handler factory for GET /api/settings/credentials
@@ -28,7 +28,7 @@ export function createGetCredentialsHandler(settingsService: SettingsService) {
credentials,
});
} catch (error) {
logError(error, "Get credentials failed");
logError(error, 'Get credentials failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};

View File

@@ -7,9 +7,9 @@
* Response: `{ "success": true, "settings": GlobalSettings }`
*/
import type { Request, Response } from "express";
import type { SettingsService } from "../../../services/settings-service.js";
import { getErrorMessage, logError } from "../common.js";
import type { Request, Response } from 'express';
import type { SettingsService } from '../../../services/settings-service.js';
import { getErrorMessage, logError } from '../common.js';
/**
* Create handler factory for GET /api/settings/global
@@ -27,7 +27,7 @@ export function createGetGlobalHandler(settingsService: SettingsService) {
settings,
});
} catch (error) {
logError(error, "Get global settings failed");
logError(error, 'Get global settings failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};

View File

@@ -8,9 +8,9 @@
* Response: `{ "success": true, "settings": ProjectSettings }`
*/
import type { Request, Response } from "express";
import type { SettingsService } from "../../../services/settings-service.js";
import { getErrorMessage, logError } from "../common.js";
import type { Request, Response } from 'express';
import type { SettingsService } from '../../../services/settings-service.js';
import { getErrorMessage, logError } from '../common.js';
/**
* Create handler factory for POST /api/settings/project
@@ -23,10 +23,10 @@ export function createGetProjectHandler(settingsService: SettingsService) {
try {
const { projectPath } = req.body as { projectPath?: string };
if (!projectPath || typeof projectPath !== "string") {
if (!projectPath || typeof projectPath !== 'string') {
res.status(400).json({
success: false,
error: "projectPath is required",
error: 'projectPath is required',
});
return;
}
@@ -38,7 +38,7 @@ export function createGetProjectHandler(settingsService: SettingsService) {
settings,
});
} catch (error) {
logError(error, "Get project settings failed");
logError(error, 'Get project settings failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};

View File

@@ -30,9 +30,9 @@
* ```
*/
import type { Request, Response } from "express";
import type { SettingsService } from "../../../services/settings-service.js";
import { getErrorMessage, logError, logger } from "../common.js";
import type { Request, Response } from 'express';
import type { SettingsService } from '../../../services/settings-service.js';
import { getErrorMessage, logError, logger } from '../common.js';
/**
* Create handler factory for POST /api/settings/migrate
@@ -45,32 +45,30 @@ export function createMigrateHandler(settingsService: SettingsService) {
try {
const { data } = req.body as {
data?: {
"automaker-storage"?: string;
"automaker-setup"?: string;
"worktree-panel-collapsed"?: string;
"file-browser-recent-folders"?: string;
"automaker:lastProjectDir"?: string;
'automaker-storage'?: string;
'automaker-setup'?: string;
'worktree-panel-collapsed'?: string;
'file-browser-recent-folders'?: string;
'automaker:lastProjectDir'?: string;
};
};
if (!data || typeof data !== "object") {
if (!data || typeof data !== 'object') {
res.status(400).json({
success: false,
error: "data object is required containing localStorage data",
error: 'data object is required containing localStorage data',
});
return;
}
logger.info("Starting settings migration from localStorage");
logger.info('Starting settings migration from localStorage');
const result = await settingsService.migrateFromLocalStorage(data);
if (result.success) {
logger.info(
`Migration successful: ${result.migratedProjectCount} projects migrated`
);
logger.info(`Migration successful: ${result.migratedProjectCount} projects migrated`);
} else {
logger.warn(`Migration completed with errors: ${result.errors.join(", ")}`);
logger.warn(`Migration completed with errors: ${result.errors.join(', ')}`);
}
res.json({
@@ -81,7 +79,7 @@ export function createMigrateHandler(settingsService: SettingsService) {
errors: result.errors,
});
} catch (error) {
logError(error, "Migration failed");
logError(error, 'Migration failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};

View File

@@ -16,9 +16,9 @@
* ```
*/
import type { Request, Response } from "express";
import type { SettingsService } from "../../../services/settings-service.js";
import { getErrorMessage, logError } from "../common.js";
import type { Request, Response } from 'express';
import type { SettingsService } from '../../../services/settings-service.js';
import { getErrorMessage, logError } from '../common.js';
/**
* Create handler factory for GET /api/settings/status
@@ -40,7 +40,7 @@ export function createStatusHandler(settingsService: SettingsService) {
needsMigration: !hasGlobalSettings,
});
} catch (error) {
logError(error, "Get settings status failed");
logError(error, 'Get settings status failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};

View File

@@ -8,10 +8,10 @@
* Response: `{ "success": true, "credentials": { anthropic } }`
*/
import type { Request, Response } from "express";
import type { SettingsService } from "../../../services/settings-service.js";
import type { Credentials } from "../../../types/settings.js";
import { getErrorMessage, logError } from "../common.js";
import type { Request, Response } from 'express';
import type { SettingsService } from '../../../services/settings-service.js';
import type { Credentials } from '../../../types/settings.js';
import { getErrorMessage, logError } from '../common.js';
/**
* Create handler factory for PUT /api/settings/credentials
@@ -19,17 +19,15 @@ import { getErrorMessage, logError } from "../common.js";
* @param settingsService - Instance of SettingsService for file I/O
* @returns Express request handler
*/
export function createUpdateCredentialsHandler(
settingsService: SettingsService
) {
export function createUpdateCredentialsHandler(settingsService: SettingsService) {
return async (req: Request, res: Response): Promise<void> => {
try {
const updates = req.body as Partial<Credentials>;
if (!updates || typeof updates !== "object") {
if (!updates || typeof updates !== 'object') {
res.status(400).json({
success: false,
error: "Invalid request body - expected credentials object",
error: 'Invalid request body - expected credentials object',
});
return;
}
@@ -44,7 +42,7 @@ export function createUpdateCredentialsHandler(
credentials: masked,
});
} catch (error) {
logError(error, "Update credentials failed");
logError(error, 'Update credentials failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};

View File

@@ -8,10 +8,10 @@
* Response: `{ "success": true, "settings": GlobalSettings }`
*/
import type { Request, Response } from "express";
import type { SettingsService } from "../../../services/settings-service.js";
import type { GlobalSettings } from "../../../types/settings.js";
import { getErrorMessage, logError } from "../common.js";
import type { Request, Response } from 'express';
import type { SettingsService } from '../../../services/settings-service.js';
import type { GlobalSettings } from '../../../types/settings.js';
import { getErrorMessage, logError } from '../common.js';
/**
* Create handler factory for PUT /api/settings/global
@@ -24,10 +24,10 @@ export function createUpdateGlobalHandler(settingsService: SettingsService) {
try {
const updates = req.body as Partial<GlobalSettings>;
if (!updates || typeof updates !== "object") {
if (!updates || typeof updates !== 'object') {
res.status(400).json({
success: false,
error: "Invalid request body - expected settings object",
error: 'Invalid request body - expected settings object',
});
return;
}
@@ -39,7 +39,7 @@ export function createUpdateGlobalHandler(settingsService: SettingsService) {
settings,
});
} catch (error) {
logError(error, "Update global settings failed");
logError(error, 'Update global settings failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};

View File

@@ -8,10 +8,10 @@
* Response: `{ "success": true, "settings": ProjectSettings }`
*/
import type { Request, Response } from "express";
import type { SettingsService } from "../../../services/settings-service.js";
import type { ProjectSettings } from "../../../types/settings.js";
import { getErrorMessage, logError } from "../common.js";
import type { Request, Response } from 'express';
import type { SettingsService } from '../../../services/settings-service.js';
import type { ProjectSettings } from '../../../types/settings.js';
import { getErrorMessage, logError } from '../common.js';
/**
* Create handler factory for PUT /api/settings/project
@@ -27,33 +27,30 @@ export function createUpdateProjectHandler(settingsService: SettingsService) {
updates?: Partial<ProjectSettings>;
};
if (!projectPath || typeof projectPath !== "string") {
if (!projectPath || typeof projectPath !== 'string') {
res.status(400).json({
success: false,
error: "projectPath is required",
error: 'projectPath is required',
});
return;
}
if (!updates || typeof updates !== "object") {
if (!updates || typeof updates !== 'object') {
res.status(400).json({
success: false,
error: "updates object is required",
error: 'updates object is required',
});
return;
}
const settings = await settingsService.updateProjectSettings(
projectPath,
updates
);
const settings = await settingsService.updateProjectSettings(projectPath, updates);
res.json({
success: true,
settings,
});
} catch (error) {
logError(error, "Update project settings failed");
logError(error, 'Update project settings failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};

View File

@@ -2,15 +2,12 @@
* Common utilities and state for setup routes
*/
import { createLogger } from "@automaker/utils";
import path from "path";
import fs from "fs/promises";
import {
getErrorMessage as getErrorMessageShared,
createLogError,
} from "../common.js";
import { createLogger } from '@automaker/utils';
import path from 'path';
import fs from 'fs/promises';
import { getErrorMessage as getErrorMessageShared, createLogError } from '../common.js';
const logger = createLogger("Setup");
const logger = createLogger('Setup');
// Storage for API keys (in-memory cache) - private
const apiKeys: Record<string, string> = {};
@@ -39,22 +36,19 @@ export function getAllApiKeys(): Record<string, string> {
/**
* Helper to persist API keys to .env file
*/
export async function persistApiKeyToEnv(
key: string,
value: string
): Promise<void> {
const envPath = path.join(process.cwd(), ".env");
export async function persistApiKeyToEnv(key: string, value: string): Promise<void> {
const envPath = path.join(process.cwd(), '.env');
try {
let envContent = "";
let envContent = '';
try {
envContent = await fs.readFile(envPath, "utf-8");
envContent = await fs.readFile(envPath, 'utf-8');
} catch {
// .env file doesn't exist, we'll create it
}
// Parse existing env content
const lines = envContent.split("\n");
const lines = envContent.split('\n');
const keyRegex = new RegExp(`^${key}=`);
let found = false;
const newLines = lines.map((line) => {
@@ -70,7 +64,7 @@ export async function persistApiKeyToEnv(
newLines.push(`${key}=${value}`);
}
await fs.writeFile(envPath, newLines.join("\n"));
await fs.writeFile(envPath, newLines.join('\n'));
logger.info(`[Setup] Persisted ${key} to .env file`);
} catch (error) {
logger.error(`[Setup] Failed to persist ${key} to .env:`, error);

View File

@@ -2,36 +2,36 @@
* Business logic for getting Claude CLI status
*/
import { exec } from "child_process";
import { promisify } from "util";
import os from "os";
import path from "path";
import fs from "fs/promises";
import { getApiKey } from "./common.js";
import { exec } from 'child_process';
import { promisify } from 'util';
import os from 'os';
import path from 'path';
import fs from 'fs/promises';
import { getApiKey } from './common.js';
const execAsync = promisify(exec);
export async function getClaudeStatus() {
let installed = false;
let version = "";
let cliPath = "";
let method = "none";
let version = '';
let cliPath = '';
let method = 'none';
const isWindows = process.platform === "win32";
const isWindows = process.platform === 'win32';
// Try to find Claude CLI using platform-specific command
try {
// Use 'where' on Windows, 'which' on Unix-like systems
const findCommand = isWindows ? "where claude" : "which claude";
const findCommand = isWindows ? 'where claude' : 'which claude';
const { stdout } = await execAsync(findCommand);
// 'where' on Windows can return multiple paths - take the first one
cliPath = stdout.trim().split(/\r?\n/)[0];
installed = true;
method = "path";
method = 'path';
// Get version
try {
const { stdout: versionOut } = await execAsync("claude --version");
const { stdout: versionOut } = await execAsync('claude --version');
version = versionOut.trim();
} catch {
// Version command might not be available
@@ -40,22 +40,22 @@ export async function getClaudeStatus() {
// Not in PATH, try common locations based on platform
const commonPaths = isWindows
? (() => {
const appData = process.env.APPDATA || path.join(os.homedir(), "AppData", "Roaming");
const appData = process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming');
return [
// Windows-specific paths
path.join(os.homedir(), ".local", "bin", "claude.exe"),
path.join(appData, "npm", "claude.cmd"),
path.join(appData, "npm", "claude"),
path.join(appData, ".npm-global", "bin", "claude.cmd"),
path.join(appData, ".npm-global", "bin", "claude"),
path.join(os.homedir(), '.local', 'bin', 'claude.exe'),
path.join(appData, 'npm', 'claude.cmd'),
path.join(appData, 'npm', 'claude'),
path.join(appData, '.npm-global', 'bin', 'claude.cmd'),
path.join(appData, '.npm-global', 'bin', 'claude'),
];
})()
: [
// Unix (Linux/macOS) paths
path.join(os.homedir(), ".local", "bin", "claude"),
path.join(os.homedir(), ".claude", "local", "claude"),
"/usr/local/bin/claude",
path.join(os.homedir(), ".npm-global", "bin", "claude"),
path.join(os.homedir(), '.local', 'bin', 'claude'),
path.join(os.homedir(), '.claude', 'local', 'claude'),
'/usr/local/bin/claude',
path.join(os.homedir(), '.npm-global', 'bin', 'claude'),
];
for (const p of commonPaths) {
@@ -63,7 +63,7 @@ export async function getClaudeStatus() {
await fs.access(p);
cliPath = p;
installed = true;
method = "local";
method = 'local';
// Get version from this path
try {
@@ -84,11 +84,11 @@ export async function getClaudeStatus() {
// apiKeys.anthropic stores direct API keys for pay-per-use
let auth = {
authenticated: false,
method: "none" as string,
method: 'none' as string,
hasCredentialsFile: false,
hasToken: false,
hasStoredOAuthToken: !!getApiKey("anthropic_oauth_token"),
hasStoredApiKey: !!getApiKey("anthropic"),
hasStoredOAuthToken: !!getApiKey('anthropic_oauth_token'),
hasStoredApiKey: !!getApiKey('anthropic'),
hasEnvApiKey: !!process.env.ANTHROPIC_API_KEY,
// Additional fields for detailed status
oauthTokenValid: false,
@@ -97,13 +97,13 @@ export async function getClaudeStatus() {
hasRecentActivity: false,
};
const claudeDir = path.join(os.homedir(), ".claude");
const claudeDir = path.join(os.homedir(), '.claude');
// Check for recent Claude CLI activity - indicates working authentication
// The stats-cache.json file is only populated when the CLI is working properly
const statsCachePath = path.join(claudeDir, "stats-cache.json");
const statsCachePath = path.join(claudeDir, 'stats-cache.json');
try {
const statsContent = await fs.readFile(statsCachePath, "utf-8");
const statsContent = await fs.readFile(statsCachePath, 'utf-8');
const stats = JSON.parse(statsContent);
// Check if there's any activity (which means the CLI is authenticated and working)
@@ -111,26 +111,26 @@ export async function getClaudeStatus() {
auth.hasRecentActivity = true;
auth.hasCliAuth = true;
auth.authenticated = true;
auth.method = "cli_authenticated";
auth.method = 'cli_authenticated';
}
} catch {
// Stats file doesn't exist or is invalid
}
// Check for settings.json - indicates CLI has been set up
const settingsPath = path.join(claudeDir, "settings.json");
const settingsPath = path.join(claudeDir, 'settings.json');
try {
await fs.access(settingsPath);
// If settings exist but no activity, CLI might be set up but not authenticated
if (!auth.hasCliAuth) {
// Try to check for other indicators of auth
const sessionsDir = path.join(claudeDir, "projects");
const sessionsDir = path.join(claudeDir, 'projects');
try {
const sessions = await fs.readdir(sessionsDir);
if (sessions.length > 0) {
auth.hasCliAuth = true;
auth.authenticated = true;
auth.method = "cli_authenticated";
auth.method = 'cli_authenticated';
}
} catch {
// Sessions directory doesn't exist
@@ -143,13 +143,13 @@ export async function getClaudeStatus() {
// Check for credentials file (OAuth tokens from claude login)
// Note: Claude CLI may use ".credentials.json" (hidden) or "credentials.json" depending on version/platform
const credentialsPaths = [
path.join(claudeDir, ".credentials.json"),
path.join(claudeDir, "credentials.json"),
path.join(claudeDir, '.credentials.json'),
path.join(claudeDir, 'credentials.json'),
];
for (const credentialsPath of credentialsPaths) {
try {
const credentialsContent = await fs.readFile(credentialsPath, "utf-8");
const credentialsContent = await fs.readFile(credentialsPath, 'utf-8');
const credentials = JSON.parse(credentialsContent);
auth.hasCredentialsFile = true;
@@ -158,11 +158,11 @@ export async function getClaudeStatus() {
auth.hasStoredOAuthToken = true;
auth.oauthTokenValid = true;
auth.authenticated = true;
auth.method = "oauth_token"; // Stored OAuth token from credentials file
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
auth.method = 'api_key'; // Stored API key in credentials file
}
break; // Found and processed credentials file
} catch {
@@ -174,25 +174,25 @@ export async function getClaudeStatus() {
if (auth.hasEnvApiKey) {
auth.authenticated = true;
auth.apiKeyValid = true;
auth.method = "api_key_env"; // API key from ANTHROPIC_API_KEY env var
auth.method = 'api_key_env'; // API key from ANTHROPIC_API_KEY env var
}
// In-memory stored OAuth token (from setup wizard - subscription auth)
if (!auth.authenticated && getApiKey("anthropic_oauth_token")) {
if (!auth.authenticated && getApiKey('anthropic_oauth_token')) {
auth.authenticated = true;
auth.oauthTokenValid = true;
auth.method = "oauth_token"; // Stored OAuth token from setup wizard
auth.method = 'oauth_token'; // Stored OAuth token from setup wizard
}
// In-memory stored API key (from settings UI - pay-per-use)
if (!auth.authenticated && getApiKey("anthropic")) {
if (!auth.authenticated && getApiKey('anthropic')) {
auth.authenticated = true;
auth.apiKeyValid = true;
auth.method = "api_key"; // Manually stored API key
auth.method = 'api_key'; // Manually stored API key
}
return {
status: installed ? "installed" : "not_installed",
status: installed ? 'installed' : 'not_installed',
installed,
method,
version,

View File

@@ -2,29 +2,29 @@
* Setup routes - HTTP API for CLI detection, API keys, and platform info
*/
import { Router } from "express";
import { createClaudeStatusHandler } from "./routes/claude-status.js";
import { createInstallClaudeHandler } from "./routes/install-claude.js";
import { createAuthClaudeHandler } from "./routes/auth-claude.js";
import { createStoreApiKeyHandler } from "./routes/store-api-key.js";
import { createDeleteApiKeyHandler } from "./routes/delete-api-key.js";
import { createApiKeysHandler } from "./routes/api-keys.js";
import { createPlatformHandler } from "./routes/platform.js";
import { createVerifyClaudeAuthHandler } from "./routes/verify-claude-auth.js";
import { createGhStatusHandler } from "./routes/gh-status.js";
import { Router } from 'express';
import { createClaudeStatusHandler } from './routes/claude-status.js';
import { createInstallClaudeHandler } from './routes/install-claude.js';
import { createAuthClaudeHandler } from './routes/auth-claude.js';
import { createStoreApiKeyHandler } from './routes/store-api-key.js';
import { createDeleteApiKeyHandler } from './routes/delete-api-key.js';
import { createApiKeysHandler } from './routes/api-keys.js';
import { createPlatformHandler } from './routes/platform.js';
import { createVerifyClaudeAuthHandler } from './routes/verify-claude-auth.js';
import { createGhStatusHandler } from './routes/gh-status.js';
export function createSetupRoutes(): Router {
const router = Router();
router.get("/claude-status", createClaudeStatusHandler());
router.post("/install-claude", createInstallClaudeHandler());
router.post("/auth-claude", createAuthClaudeHandler());
router.post("/store-api-key", createStoreApiKeyHandler());
router.post("/delete-api-key", createDeleteApiKeyHandler());
router.get("/api-keys", createApiKeysHandler());
router.get("/platform", createPlatformHandler());
router.post("/verify-claude-auth", createVerifyClaudeAuthHandler());
router.get("/gh-status", createGhStatusHandler());
router.get('/claude-status', createClaudeStatusHandler());
router.post('/install-claude', createInstallClaudeHandler());
router.post('/auth-claude', createAuthClaudeHandler());
router.post('/store-api-key', createStoreApiKeyHandler());
router.post('/delete-api-key', createDeleteApiKeyHandler());
router.get('/api-keys', createApiKeysHandler());
router.get('/platform', createPlatformHandler());
router.post('/verify-claude-auth', createVerifyClaudeAuthHandler());
router.get('/gh-status', createGhStatusHandler());
return router;
}

View File

@@ -2,19 +2,18 @@
* GET /api-keys endpoint - Get API keys status
*/
import type { Request, Response } from "express";
import { getApiKey, getErrorMessage, logError } from "../common.js";
import type { Request, Response } from 'express';
import { getApiKey, getErrorMessage, logError } from '../common.js';
export function createApiKeysHandler() {
return async (_req: Request, res: Response): Promise<void> => {
try {
res.json({
success: true,
hasAnthropicKey:
!!getApiKey("anthropic") || !!process.env.ANTHROPIC_API_KEY,
hasAnthropicKey: !!getApiKey('anthropic') || !!process.env.ANTHROPIC_API_KEY,
});
} catch (error) {
logError(error, "Get API keys failed");
logError(error, 'Get API keys failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};

View File

@@ -2,8 +2,8 @@
* POST /auth-claude endpoint - Auth Claude
*/
import type { Request, Response } from "express";
import { getErrorMessage, logError } from "../common.js";
import type { Request, Response } from 'express';
import { getErrorMessage, logError } from '../common.js';
export function createAuthClaudeHandler() {
return async (_req: Request, res: Response): Promise<void> => {
@@ -11,11 +11,11 @@ export function createAuthClaudeHandler() {
res.json({
success: true,
requiresManualAuth: true,
command: "claude login",
command: 'claude login',
message: "Please run 'claude login' in your terminal to authenticate",
});
} catch (error) {
logError(error, "Auth Claude failed");
logError(error, 'Auth Claude failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};

View File

@@ -2,9 +2,9 @@
* GET /claude-status endpoint - Get Claude CLI status
*/
import type { Request, Response } from "express";
import { getClaudeStatus } from "../get-claude-status.js";
import { getErrorMessage, logError } from "../common.js";
import type { Request, Response } from 'express';
import { getClaudeStatus } from '../get-claude-status.js';
import { getErrorMessage, logError } from '../common.js';
export function createClaudeStatusHandler() {
return async (_req: Request, res: Response): Promise<void> => {
@@ -15,7 +15,7 @@ export function createClaudeStatusHandler() {
...status,
});
} catch (error) {
logError(error, "Get Claude status failed");
logError(error, 'Get Claude status failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};

View File

@@ -2,43 +2,43 @@
* POST /delete-api-key endpoint - Delete a stored API key
*/
import type { Request, Response } from "express";
import { createLogger } from "@automaker/utils";
import path from "path";
import fs from "fs/promises";
import type { Request, Response } from 'express';
import { createLogger } from '@automaker/utils';
import path from 'path';
import fs from 'fs/promises';
const logger = createLogger("Setup");
const logger = createLogger('Setup');
// In-memory storage reference (imported from common.ts pattern)
// We need to modify common.ts to export a deleteApiKey function
import { setApiKey } from "../common.js";
import { setApiKey } from '../common.js';
/**
* Remove an API key from the .env file
*/
async function removeApiKeyFromEnv(key: string): Promise<void> {
const envPath = path.join(process.cwd(), ".env");
const envPath = path.join(process.cwd(), '.env');
try {
let envContent = "";
let envContent = '';
try {
envContent = await fs.readFile(envPath, "utf-8");
envContent = await fs.readFile(envPath, 'utf-8');
} catch {
// .env file doesn't exist, nothing to delete
return;
}
// Parse existing env content and remove the key
const lines = envContent.split("\n");
const lines = envContent.split('\n');
const keyRegex = new RegExp(`^${key}=`);
const newLines = lines.filter((line) => !keyRegex.test(line));
// Remove empty lines at the end
while (newLines.length > 0 && newLines[newLines.length - 1].trim() === "") {
while (newLines.length > 0 && newLines[newLines.length - 1].trim() === '') {
newLines.pop();
}
await fs.writeFile(envPath, newLines.join("\n") + (newLines.length > 0 ? "\n" : ""));
await fs.writeFile(envPath, newLines.join('\n') + (newLines.length > 0 ? '\n' : ''));
logger.info(`[Setup] Removed ${key} from .env file`);
} catch (error) {
logger.error(`[Setup] Failed to remove ${key} from .env:`, error);
@@ -54,7 +54,7 @@ export function createDeleteApiKeyHandler() {
if (!provider) {
res.status(400).json({
success: false,
error: "Provider is required",
error: 'Provider is required',
});
return;
}
@@ -63,7 +63,7 @@ export function createDeleteApiKeyHandler() {
// Map provider to env key name
const envKeyMap: Record<string, string> = {
anthropic: "ANTHROPIC_API_KEY",
anthropic: 'ANTHROPIC_API_KEY',
};
const envKey = envKeyMap[provider];
@@ -76,7 +76,7 @@ export function createDeleteApiKeyHandler() {
}
// Clear from in-memory storage
setApiKey(provider, "");
setApiKey(provider, '');
// Remove from environment
delete process.env[envKey];
@@ -91,14 +91,11 @@ export function createDeleteApiKeyHandler() {
message: `API key for ${provider} has been deleted`,
});
} catch (error) {
logger.error("[Setup] Delete API key error:", error);
logger.error('[Setup] Delete API key error:', error);
res.status(500).json({
success: false,
error: error instanceof Error ? error.message : "Failed to delete API key",
error: error instanceof Error ? error.message : 'Failed to delete API key',
});
}
};
}

View File

@@ -2,24 +2,26 @@
* GET /gh-status endpoint - Get GitHub CLI status
*/
import type { Request, 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";
import { getErrorMessage, logError } from "../common.js";
import type { Request, 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';
import { getErrorMessage, logError } from '../common.js';
const execAsync = promisify(exec);
// Extended PATH to include common tool installation locations
const extendedPath = [
process.env.PATH,
"/opt/homebrew/bin",
"/usr/local/bin",
"/home/linuxbrew/.linuxbrew/bin",
'/opt/homebrew/bin',
'/usr/local/bin',
'/home/linuxbrew/.linuxbrew/bin',
`${process.env.HOME}/.local/bin`,
].filter(Boolean).join(":");
]
.filter(Boolean)
.join(':');
const execEnv = {
...process.env,
@@ -44,11 +46,11 @@ async function getGhStatus(): Promise<GhStatus> {
user: null,
};
const isWindows = process.platform === "win32";
const isWindows = process.platform === 'win32';
// Check if gh CLI is installed
try {
const findCommand = isWindows ? "where gh" : "command -v gh";
const findCommand = isWindows ? 'where gh' : 'command -v gh';
const { stdout } = await execAsync(findCommand, { env: execEnv });
status.path = stdout.trim().split(/\r?\n/)[0];
status.installed = true;
@@ -56,14 +58,14 @@ async function getGhStatus(): Promise<GhStatus> {
// gh not in PATH, try common locations
const commonPaths = isWindows
? [
path.join(process.env.LOCALAPPDATA || "", "Programs", "gh", "bin", "gh.exe"),
path.join(process.env.ProgramFiles || "", "GitHub CLI", "gh.exe"),
path.join(process.env.LOCALAPPDATA || '', 'Programs', 'gh', 'bin', 'gh.exe'),
path.join(process.env.ProgramFiles || '', 'GitHub CLI', 'gh.exe'),
]
: [
"/opt/homebrew/bin/gh",
"/usr/local/bin/gh",
path.join(os.homedir(), ".local", "bin", "gh"),
"/home/linuxbrew/.linuxbrew/bin/gh",
'/opt/homebrew/bin/gh',
'/usr/local/bin/gh',
path.join(os.homedir(), '.local', 'bin', 'gh'),
'/home/linuxbrew/.linuxbrew/bin/gh',
];
for (const p of commonPaths) {
@@ -84,30 +86,31 @@ async function getGhStatus(): Promise<GhStatus> {
// Get version
try {
const { stdout } = await execAsync("gh --version", { env: execEnv });
const { stdout } = await execAsync('gh --version', { env: execEnv });
// Extract version from output like "gh version 2.40.1 (2024-01-09)"
const versionMatch = stdout.match(/gh version ([\d.]+)/);
status.version = versionMatch ? versionMatch[1] : stdout.trim().split("\n")[0];
status.version = versionMatch ? versionMatch[1] : stdout.trim().split('\n')[0];
} catch {
// Version command failed
}
// Check authentication status
try {
const { stdout } = await execAsync("gh auth status", { env: execEnv });
const { stdout } = await execAsync('gh auth status', { env: execEnv });
// If this succeeds without error, we're authenticated
status.authenticated = true;
// Try to extract username from output
const userMatch = stdout.match(/Logged in to [^\s]+ account ([^\s]+)/i) ||
stdout.match(/Logged in to [^\s]+ as ([^\s]+)/i);
const userMatch =
stdout.match(/Logged in to [^\s]+ account ([^\s]+)/i) ||
stdout.match(/Logged in to [^\s]+ as ([^\s]+)/i);
if (userMatch) {
status.user = userMatch[1];
}
} catch (error: unknown) {
// Auth status returns non-zero if not authenticated
const err = error as { stderr?: string };
if (err.stderr?.includes("not logged in")) {
if (err.stderr?.includes('not logged in')) {
status.authenticated = false;
}
}
@@ -124,7 +127,7 @@ export function createGhStatusHandler() {
...status,
});
} catch (error) {
logError(error, "Get GitHub CLI status failed");
logError(error, 'Get GitHub CLI status failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};

View File

@@ -2,8 +2,8 @@
* POST /install-claude endpoint - Install Claude CLI
*/
import type { Request, Response } from "express";
import { getErrorMessage, logError } from "../common.js";
import type { Request, Response } from 'express';
import { getErrorMessage, logError } from '../common.js';
export function createInstallClaudeHandler() {
return async (_req: Request, res: Response): Promise<void> => {
@@ -13,10 +13,10 @@ export function createInstallClaudeHandler() {
res.json({
success: false,
error:
"CLI installation requires terminal access. Please install manually using: npm install -g @anthropic-ai/claude-code",
'CLI installation requires terminal access. Please install manually using: npm install -g @anthropic-ai/claude-code',
});
} catch (error) {
logError(error, "Install Claude CLI failed");
logError(error, 'Install Claude CLI failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};

View File

@@ -2,9 +2,9 @@
* GET /platform endpoint - Get platform info
*/
import type { Request, Response } from "express";
import os from "os";
import { getErrorMessage, logError } from "../common.js";
import type { Request, Response } from 'express';
import os from 'os';
import { getErrorMessage, logError } from '../common.js';
export function createPlatformHandler() {
return async (_req: Request, res: Response): Promise<void> => {
@@ -15,12 +15,12 @@ export function createPlatformHandler() {
platform,
arch: os.arch(),
homeDir: os.homedir(),
isWindows: platform === "win32",
isMac: platform === "darwin",
isLinux: platform === "linux",
isWindows: platform === 'win32',
isMac: platform === 'darwin',
isLinux: platform === 'linux',
});
} catch (error) {
logError(error, "Get platform info failed");
logError(error, 'Get platform info failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};

View File

@@ -2,16 +2,11 @@
* POST /store-api-key endpoint - Store API key
*/
import type { Request, Response } from "express";
import {
setApiKey,
persistApiKeyToEnv,
getErrorMessage,
logError,
} from "../common.js";
import { createLogger } from "@automaker/utils";
import type { Request, Response } from 'express';
import { setApiKey, persistApiKeyToEnv, getErrorMessage, logError } from '../common.js';
import { createLogger } from '@automaker/utils';
const logger = createLogger("Setup");
const logger = createLogger('Setup');
export function createStoreApiKeyHandler() {
return async (req: Request, res: Response): Promise<void> => {
@@ -22,20 +17,18 @@ export function createStoreApiKeyHandler() {
};
if (!provider || !apiKey) {
res
.status(400)
.json({ success: false, error: "provider and apiKey required" });
res.status(400).json({ success: false, error: 'provider and apiKey required' });
return;
}
setApiKey(provider, apiKey);
// Also set as environment variable and persist to .env
if (provider === "anthropic" || provider === "anthropic_oauth_token") {
if (provider === 'anthropic' || provider === 'anthropic_oauth_token') {
// Both API key and OAuth token use ANTHROPIC_API_KEY
process.env.ANTHROPIC_API_KEY = apiKey;
await persistApiKeyToEnv("ANTHROPIC_API_KEY", apiKey);
logger.info("[Setup] Stored API key as ANTHROPIC_API_KEY");
await persistApiKeyToEnv('ANTHROPIC_API_KEY', apiKey);
logger.info('[Setup] Stored API key as ANTHROPIC_API_KEY');
} else {
res.status(400).json({
success: false,
@@ -46,7 +39,7 @@ export function createStoreApiKeyHandler() {
res.json({ success: true });
} catch (error) {
logError(error, "Store API key failed");
logError(error, 'Store API key failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};

View File

@@ -3,50 +3,50 @@
* Supports verifying either CLI auth or API key auth independently
*/
import type { Request, Response } from "express";
import { query } from "@anthropic-ai/claude-agent-sdk";
import { createLogger } from "@automaker/utils";
import { getApiKey } from "../common.js";
import type { Request, Response } from 'express';
import { query } from '@anthropic-ai/claude-agent-sdk';
import { createLogger } from '@automaker/utils';
import { getApiKey } from '../common.js';
const logger = createLogger("Setup");
const logger = createLogger('Setup');
// Known error patterns that indicate auth failure
const AUTH_ERROR_PATTERNS = [
"OAuth token revoked",
"Please run /login",
"please run /login",
"token revoked",
"invalid_api_key",
"authentication_error",
"unauthorized",
"not authenticated",
"authentication failed",
"invalid api key",
"api key is invalid",
'OAuth token revoked',
'Please run /login',
'please run /login',
'token revoked',
'invalid_api_key',
'authentication_error',
'unauthorized',
'not authenticated',
'authentication failed',
'invalid api key',
'api key is invalid',
];
// Patterns that indicate billing/credit issues - should FAIL verification
const BILLING_ERROR_PATTERNS = [
"credit balance is too low",
"credit balance too low",
"insufficient credits",
"insufficient balance",
"no credits",
"out of credits",
"billing",
"payment required",
"add credits",
'credit balance is too low',
'credit balance too low',
'insufficient credits',
'insufficient balance',
'no credits',
'out of credits',
'billing',
'payment required',
'add credits',
];
// Patterns that indicate rate/usage limits - should FAIL verification
// Users need to wait or upgrade their plan
const RATE_LIMIT_PATTERNS = [
"limit reached",
"rate limit",
"rate_limit",
"resets", // Only valid if it's a temporary reset, not a billing issue
"/upgrade",
"extra-usage",
'limit reached',
'rate limit',
'rate_limit',
'resets', // Only valid if it's a temporary reset, not a billing issue
'/upgrade',
'extra-usage',
];
function isRateLimitError(text: string): boolean {
@@ -55,43 +55,33 @@ function isRateLimitError(text: string): boolean {
if (isBillingError(text)) {
return false;
}
return RATE_LIMIT_PATTERNS.some((pattern) =>
lowerText.includes(pattern.toLowerCase())
);
return RATE_LIMIT_PATTERNS.some((pattern) => lowerText.includes(pattern.toLowerCase()));
}
function isBillingError(text: string): boolean {
const lowerText = text.toLowerCase();
return BILLING_ERROR_PATTERNS.some((pattern) =>
lowerText.includes(pattern.toLowerCase())
);
return BILLING_ERROR_PATTERNS.some((pattern) => lowerText.includes(pattern.toLowerCase()));
}
function containsAuthError(text: string): boolean {
const lowerText = text.toLowerCase();
return AUTH_ERROR_PATTERNS.some((pattern) =>
lowerText.includes(pattern.toLowerCase())
);
return AUTH_ERROR_PATTERNS.some((pattern) => lowerText.includes(pattern.toLowerCase()));
}
export function createVerifyClaudeAuthHandler() {
return async (req: Request, res: Response): Promise<void> => {
try {
// Get the auth method from the request body
const { authMethod } = req.body as { authMethod?: "cli" | "api_key" };
const { authMethod } = req.body as { authMethod?: 'cli' | 'api_key' };
logger.info(
`[Setup] Verifying Claude authentication using method: ${
authMethod || "auto"
}`
);
logger.info(`[Setup] Verifying Claude authentication using method: ${authMethod || 'auto'}`);
// Create an AbortController with a 30-second timeout
const abortController = new AbortController();
const timeoutId = setTimeout(() => abortController.abort(), 30000);
let authenticated = false;
let errorMessage = "";
let errorMessage = '';
let receivedAnyContent = false;
// Save original env values
@@ -99,25 +89,23 @@ export function createVerifyClaudeAuthHandler() {
try {
// Configure environment based on auth method
if (authMethod === "cli") {
if (authMethod === 'cli') {
// For CLI verification, remove any API key so it uses CLI credentials only
delete process.env.ANTHROPIC_API_KEY;
logger.info(
"[Setup] Cleared API key environment for CLI verification"
);
} else if (authMethod === "api_key") {
logger.info('[Setup] Cleared API key environment for CLI verification');
} else if (authMethod === 'api_key') {
// For API key verification, ensure we're using the stored API key
const storedApiKey = getApiKey("anthropic");
const storedApiKey = getApiKey('anthropic');
if (storedApiKey) {
process.env.ANTHROPIC_API_KEY = storedApiKey;
logger.info("[Setup] Using stored API key for verification");
logger.info('[Setup] Using stored API key for verification');
} else {
// Check env var
if (!process.env.ANTHROPIC_API_KEY) {
res.json({
success: true,
authenticated: false,
error: "No API key configured. Please enter an API key first.",
error: 'No API key configured. Please enter an API key first.',
});
return;
}
@@ -128,7 +116,7 @@ export function createVerifyClaudeAuthHandler() {
const stream = query({
prompt: "Reply with only the word 'ok'",
options: {
model: "claude-sonnet-4-20250514",
model: 'claude-sonnet-4-20250514',
maxTurns: 1,
allowedTools: [],
abortController,
@@ -141,50 +129,50 @@ export function createVerifyClaudeAuthHandler() {
for await (const msg of stream) {
const msgStr = JSON.stringify(msg);
allMessages.push(msgStr);
logger.info("[Setup] Stream message:", msgStr.substring(0, 500));
logger.info('[Setup] Stream message:', msgStr.substring(0, 500));
// Check for billing errors FIRST - these should fail verification
if (isBillingError(msgStr)) {
logger.error("[Setup] Found billing error in message");
logger.error('[Setup] Found billing error in message');
errorMessage =
"Credit balance is too low. Please add credits to your Anthropic account at console.anthropic.com";
'Credit balance is too low. Please add credits to your Anthropic account at console.anthropic.com';
authenticated = false;
break;
}
// Check if any part of the message contains auth errors
if (containsAuthError(msgStr)) {
logger.error("[Setup] Found auth error in message");
if (authMethod === "cli") {
logger.error('[Setup] Found auth error in message');
if (authMethod === 'cli') {
errorMessage =
"CLI authentication failed. Please run 'claude login' in your terminal to authenticate.";
} else {
errorMessage = "API key is invalid or has been revoked.";
errorMessage = 'API key is invalid or has been revoked.';
}
break;
}
// Check specifically for assistant messages with text content
if (msg.type === "assistant" && (msg as any).message?.content) {
if (msg.type === 'assistant' && (msg as any).message?.content) {
const content = (msg as any).message.content;
if (Array.isArray(content)) {
for (const block of content) {
if (block.type === "text" && block.text) {
if (block.type === 'text' && block.text) {
const text = block.text;
logger.info("[Setup] Assistant text:", text);
logger.info('[Setup] Assistant text:', text);
if (containsAuthError(text)) {
if (authMethod === "cli") {
if (authMethod === 'cli') {
errorMessage =
"CLI authentication failed. Please run 'claude login' in your terminal to authenticate.";
} else {
errorMessage = "API key is invalid or has been revoked.";
errorMessage = 'API key is invalid or has been revoked.';
}
break;
}
// Valid text response that's not an error
if (text.toLowerCase().includes("ok") || text.length > 0) {
if (text.toLowerCase().includes('ok') || text.length > 0) {
receivedAnyContent = true;
}
}
@@ -193,34 +181,30 @@ export function createVerifyClaudeAuthHandler() {
}
// Check for result messages
if (msg.type === "result") {
if (msg.type === 'result') {
const resultStr = JSON.stringify(msg);
// First check for billing errors - these should FAIL verification
if (isBillingError(resultStr)) {
logger.error(
"[Setup] Billing error detected - insufficient credits"
);
logger.error('[Setup] Billing error detected - insufficient credits');
errorMessage =
"Credit balance is too low. Please add credits to your Anthropic account at console.anthropic.com";
'Credit balance is too low. Please add credits to your Anthropic account at console.anthropic.com';
authenticated = false;
break;
}
// Check if it's a rate limit error - should FAIL verification
else if (isRateLimitError(resultStr)) {
logger.warn(
"[Setup] Rate limit detected - treating as unverified"
);
logger.warn('[Setup] Rate limit detected - treating as unverified');
errorMessage =
"Rate limit reached. Please wait a while before trying again or upgrade your plan.";
'Rate limit reached. Please wait a while before trying again or upgrade your plan.';
authenticated = false;
break;
} else if (containsAuthError(resultStr)) {
if (authMethod === "cli") {
if (authMethod === 'cli') {
errorMessage =
"CLI authentication failed. Please run 'claude login' in your terminal to authenticate.";
} else {
errorMessage = "API key is invalid or has been revoked.";
errorMessage = 'API key is invalid or has been revoked.';
}
} else {
// Got a result without errors
@@ -236,60 +220,48 @@ export function createVerifyClaudeAuthHandler() {
authenticated = true;
} else {
// No content received - might be an issue
logger.warn("[Setup] No content received from stream");
logger.warn("[Setup] All messages:", allMessages.join("\n"));
errorMessage =
"No response received from Claude. Please check your authentication.";
logger.warn('[Setup] No content received from stream');
logger.warn('[Setup] All messages:', allMessages.join('\n'));
errorMessage = 'No response received from Claude. Please check your authentication.';
}
} catch (error: unknown) {
const errMessage =
error instanceof Error ? error.message : String(error);
const errMessage = error instanceof Error ? error.message : String(error);
logger.error("[Setup] Claude auth verification exception:", errMessage);
logger.error('[Setup] Claude auth verification exception:', errMessage);
// Check for billing errors FIRST - these always fail
if (isBillingError(errMessage)) {
authenticated = false;
errorMessage =
"Credit balance is too low. Please add credits to your Anthropic account at console.anthropic.com";
'Credit balance is too low. Please add credits to your Anthropic account at console.anthropic.com';
}
// Check for rate limit in exception - should FAIL verification
else if (isRateLimitError(errMessage)) {
authenticated = false;
errorMessage =
"Rate limit reached. Please wait a while before trying again or upgrade your plan.";
logger.warn(
"[Setup] Rate limit in exception - treating as unverified"
);
'Rate limit reached. Please wait a while before trying again or upgrade your plan.';
logger.warn('[Setup] Rate limit in exception - treating as unverified');
}
// If we already determined auth was successful, keep it
else if (authenticated) {
logger.info("[Setup] Auth already confirmed, ignoring exception");
logger.info('[Setup] Auth already confirmed, ignoring exception');
}
// Check for auth-related errors in exception
else if (containsAuthError(errMessage)) {
if (authMethod === "cli") {
if (authMethod === 'cli') {
errorMessage =
"CLI authentication failed. Please run 'claude login' in your terminal to authenticate.";
} else {
errorMessage = "API key is invalid or has been revoked.";
errorMessage = 'API key is invalid or has been revoked.';
}
} else if (
errMessage.includes("abort") ||
errMessage.includes("timeout")
) {
errorMessage = "Verification timed out. Please try again.";
} else if (
errMessage.includes("exit") &&
errMessage.includes("code 1")
) {
} else if (errMessage.includes('abort') || errMessage.includes('timeout')) {
errorMessage = 'Verification timed out. Please try again.';
} else if (errMessage.includes('exit') && errMessage.includes('code 1')) {
// Process exited with code 1 but we might have gotten rate limit info in the stream
// Check if we received any content that indicated auth worked
if (receivedAnyContent && !errorMessage) {
authenticated = true;
logger.info(
"[Setup] Process exit 1 but content received - auth valid"
);
logger.info('[Setup] Process exit 1 but content received - auth valid');
} else if (!errorMessage) {
errorMessage = errMessage;
}
@@ -301,13 +273,13 @@ export function createVerifyClaudeAuthHandler() {
// Restore original environment
if (originalAnthropicKey !== undefined) {
process.env.ANTHROPIC_API_KEY = originalAnthropicKey;
} else if (authMethod === "cli") {
} else if (authMethod === 'cli') {
// If we cleared it and there was no original, keep it cleared
delete process.env.ANTHROPIC_API_KEY;
}
}
logger.info("[Setup] Verification result:", {
logger.info('[Setup] Verification result:', {
authenticated,
errorMessage,
authMethod,
@@ -319,11 +291,11 @@ export function createVerifyClaudeAuthHandler() {
error: errorMessage || undefined,
});
} catch (error) {
logger.error("[Setup] Verify Claude auth endpoint error:", error);
logger.error('[Setup] Verify Claude auth endpoint error:', error);
res.status(500).json({
success: false,
authenticated: false,
error: error instanceof Error ? error.message : "Verification failed",
error: error instanceof Error ? error.message : 'Verification failed',
});
}
};

View File

@@ -2,13 +2,10 @@
* Common utilities and state for suggestions routes
*/
import { createLogger } from "@automaker/utils";
import {
getErrorMessage as getErrorMessageShared,
createLogError,
} from "../common.js";
import { createLogger } from '@automaker/utils';
import { getErrorMessage as getErrorMessageShared, createLogError } from '../common.js';
const logger = createLogger("Suggestions");
const logger = createLogger('Suggestions');
// Shared state for tracking generation status - private
let isRunning = false;
@@ -27,10 +24,7 @@ export function getSuggestionsStatus(): {
/**
* Set the running state and abort controller
*/
export function setRunningState(
running: boolean,
controller: AbortController | null = null
): void {
export function setRunningState(running: boolean, controller: AbortController | null = null): void {
isRunning = running;
currentAbortController = controller;
}

View File

@@ -2,43 +2,43 @@
* Business logic for generating suggestions
*/
import { query } from "@anthropic-ai/claude-agent-sdk";
import type { EventEmitter } from "../../lib/events.js";
import { createLogger } from "@automaker/utils";
import { createSuggestionsOptions } from "../../lib/sdk-options.js";
import { query } from '@anthropic-ai/claude-agent-sdk';
import type { EventEmitter } from '../../lib/events.js';
import { createLogger } from '@automaker/utils';
import { createSuggestionsOptions } from '../../lib/sdk-options.js';
const logger = createLogger("Suggestions");
const logger = createLogger('Suggestions');
/**
* JSON Schema for suggestions output
*/
const suggestionsSchema = {
type: "object",
type: 'object',
properties: {
suggestions: {
type: "array",
type: 'array',
items: {
type: "object",
type: 'object',
properties: {
id: { type: "string" },
category: { type: "string" },
description: { type: "string" },
id: { type: 'string' },
category: { type: 'string' },
description: { type: 'string' },
steps: {
type: "array",
items: { type: "string" },
type: 'array',
items: { type: 'string' },
},
priority: {
type: "number",
priority: {
type: 'number',
minimum: 1,
maximum: 3,
},
reasoning: { type: "string" },
reasoning: { type: 'string' },
},
required: ["category", "description", "steps", "priority", "reasoning"],
required: ['category', 'description', 'steps', 'priority', 'reasoning'],
},
},
},
required: ["suggestions"],
required: ['suggestions'],
additionalProperties: false,
};
@@ -49,13 +49,10 @@ export async function generateSuggestions(
abortController: AbortController
): Promise<void> {
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.",
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}
@@ -71,8 +68,8 @@ For each suggestion, provide:
The response will be automatically formatted as structured JSON.`;
events.emit("suggestions:event", {
type: "suggestions_progress",
events.emit('suggestions:event', {
type: 'suggestions_progress',
content: `Starting ${suggestionType} analysis...\n`,
});
@@ -80,48 +77,48 @@ The response will be automatically formatted as structured JSON.`;
cwd: projectPath,
abortController,
outputFormat: {
type: "json_schema",
type: 'json_schema',
schema: suggestionsSchema,
},
});
const stream = query({ prompt, options });
let responseText = "";
let responseText = '';
let structuredOutput: { suggestions: Array<Record<string, unknown>> } | null = null;
for await (const msg of stream) {
if (msg.type === "assistant" && msg.message.content) {
if (msg.type === 'assistant' && msg.message.content) {
for (const block of msg.message.content) {
if (block.type === "text") {
if (block.type === 'text') {
responseText += block.text;
events.emit("suggestions:event", {
type: "suggestions_progress",
events.emit('suggestions:event', {
type: 'suggestions_progress',
content: block.text,
});
} else if (block.type === "tool_use") {
events.emit("suggestions:event", {
type: "suggestions_tool",
} 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") {
} else if (msg.type === 'result' && msg.subtype === 'success') {
// Check for structured output
const resultMsg = msg as any;
if (resultMsg.structured_output) {
structuredOutput = resultMsg.structured_output as {
suggestions: Array<Record<string, unknown>>;
};
logger.debug("Received structured output:", structuredOutput);
logger.debug('Received structured output:', structuredOutput);
}
} else if (msg.type === "result") {
} else if (msg.type === 'result') {
const resultMsg = msg as any;
if (resultMsg.subtype === "error_max_structured_output_retries") {
logger.error("Failed to produce valid structured output after retries");
throw new Error("Could not produce valid suggestions output");
} else if (resultMsg.subtype === "error_max_turns") {
logger.error("Hit max turns limit before completing suggestions generation");
if (resultMsg.subtype === 'error_max_structured_output_retries') {
logger.error('Failed to produce valid structured output after retries');
throw new Error('Could not produce valid suggestions output');
} else if (resultMsg.subtype === 'error_max_turns') {
logger.error('Hit max turns limit before completing suggestions generation');
logger.warn(`Response text length: ${responseText.length} chars`);
// Still try to parse what we have
}
@@ -132,49 +129,44 @@ The response will be automatically formatted as structured JSON.`;
try {
if (structuredOutput && structuredOutput.suggestions) {
// Use structured output directly
events.emit("suggestions:event", {
type: "suggestions_complete",
suggestions: structuredOutput.suggestions.map(
(s: Record<string, unknown>, i: number) => ({
...s,
id: s.id || `suggestion-${Date.now()}-${i}`,
})
),
events.emit('suggestions:event', {
type: 'suggestions_complete',
suggestions: structuredOutput.suggestions.map((s: Record<string, unknown>, i: number) => ({
...s,
id: s.id || `suggestion-${Date.now()}-${i}`,
})),
});
} else {
// Fallback: try to parse from text (for backwards compatibility)
logger.warn("No structured output received, attempting to parse from text");
logger.warn('No structured output received, attempting to parse from text');
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}`,
})
),
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");
throw new Error('No valid JSON found in response');
}
}
} catch (error) {
// Log the parsing error for debugging
logger.error("Failed to parse suggestions JSON from AI response:", error);
logger.error('Failed to parse suggestions JSON from AI response:', error);
// Return generic suggestions if parsing fails
events.emit("suggestions:event", {
type: "suggestions_complete",
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"],
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",
reasoning: 'The AI provided analysis but suggestions need manual review',
},
],
});

View File

@@ -2,19 +2,19 @@
* Suggestions routes - HTTP API for AI-powered feature suggestions
*/
import { Router } from "express";
import type { EventEmitter } from "../../lib/events.js";
import { validatePathParams } from "../../middleware/validate-paths.js";
import { createGenerateHandler } from "./routes/generate.js";
import { createStopHandler } from "./routes/stop.js";
import { createStatusHandler } from "./routes/status.js";
import { Router } from 'express';
import type { EventEmitter } from '../../lib/events.js';
import { validatePathParams } from '../../middleware/validate-paths.js';
import { createGenerateHandler } from './routes/generate.js';
import { createStopHandler } from './routes/stop.js';
import { createStatusHandler } from './routes/status.js';
export function createSuggestionsRoutes(events: EventEmitter): Router {
const router = Router();
router.post("/generate", validatePathParams("projectPath"), createGenerateHandler(events));
router.post("/stop", createStopHandler());
router.get("/status", createStatusHandler());
router.post('/generate', validatePathParams('projectPath'), createGenerateHandler(events));
router.post('/stop', createStopHandler());
router.get('/status', createStatusHandler());
return router;
}

Some files were not shown because too many files have changed in this diff Show More