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

@@ -21,4 +21,4 @@
"mcp__puppeteer__puppeteer_evaluate"
]
}
}
}

View File

@@ -1,28 +1,28 @@
name: "Setup Project"
description: "Common setup steps for CI workflows - checkout, Node.js, dependencies, and native modules"
name: 'Setup Project'
description: 'Common setup steps for CI workflows - checkout, Node.js, dependencies, and native modules'
inputs:
node-version:
description: "Node.js version to use"
description: 'Node.js version to use'
required: false
default: "22"
default: '22'
check-lockfile:
description: "Run lockfile lint check for SSH URLs"
description: 'Run lockfile lint check for SSH URLs'
required: false
default: "false"
default: 'false'
rebuild-node-pty-path:
description: "Working directory for node-pty rebuild (empty = root)"
description: 'Working directory for node-pty rebuild (empty = root)'
required: false
default: ""
default: ''
runs:
using: "composite"
using: 'composite'
steps:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
cache: "npm"
cache: 'npm'
cache-dependency-path: package-lock.json
- name: Check for SSH URLs in lockfile

View File

@@ -1,15 +1,11 @@
const {
S3Client,
PutObjectCommand,
GetObjectCommand,
} = require("@aws-sdk/client-s3");
const fs = require("fs");
const path = require("path");
const https = require("https");
const { pipeline } = require("stream/promises");
const { S3Client, PutObjectCommand, GetObjectCommand } = require('@aws-sdk/client-s3');
const fs = require('fs');
const path = require('path');
const https = require('https');
const { pipeline } = require('stream/promises');
const s3Client = new S3Client({
region: "auto",
region: 'auto',
endpoint: process.env.R2_ENDPOINT,
credentials: {
accessKeyId: process.env.R2_ACCESS_KEY_ID,
@@ -28,14 +24,14 @@ async function fetchExistingReleases() {
const response = await s3Client.send(
new GetObjectCommand({
Bucket: BUCKET,
Key: "releases.json",
Key: 'releases.json',
})
);
const body = await response.Body.transformToString();
return JSON.parse(body);
} catch (error) {
if (error.name === "NoSuchKey" || error.$metadata?.httpStatusCode === 404) {
console.log("No existing releases.json found, creating new one");
if (error.name === 'NoSuchKey' || error.$metadata?.httpStatusCode === 404) {
console.log('No existing releases.json found, creating new one');
return { latestVersion: null, releases: [] };
}
throw error;
@@ -85,7 +81,7 @@ async function checkUrlAccessible(url, maxRetries = 10, initialDelay = 1000) {
resolve({
accessible: false,
statusCode,
error: "Redirect without location header",
error: 'Redirect without location header',
});
return;
}
@@ -93,18 +89,16 @@ async function checkUrlAccessible(url, maxRetries = 10, initialDelay = 1000) {
return https
.get(redirectUrl, { timeout: 10000 }, (redirectResponse) => {
const redirectStatus = redirectResponse.statusCode;
const contentType =
redirectResponse.headers["content-type"] || "";
const contentType = redirectResponse.headers['content-type'] || '';
// Check if it's actually a file (zip/tar.gz) and not HTML
const isFile =
contentType.includes("application/zip") ||
contentType.includes("application/gzip") ||
contentType.includes("application/x-gzip") ||
contentType.includes("application/x-tar") ||
redirectUrl.includes(".zip") ||
redirectUrl.includes(".tar.gz");
const isGood =
redirectStatus >= 200 && redirectStatus < 300 && isFile;
contentType.includes('application/zip') ||
contentType.includes('application/gzip') ||
contentType.includes('application/x-gzip') ||
contentType.includes('application/x-tar') ||
redirectUrl.includes('.zip') ||
redirectUrl.includes('.tar.gz');
const isGood = redirectStatus >= 200 && redirectStatus < 300 && isFile;
redirectResponse.destroy();
resolve({
accessible: isGood,
@@ -113,38 +107,38 @@ async function checkUrlAccessible(url, maxRetries = 10, initialDelay = 1000) {
contentType,
});
})
.on("error", (error) => {
.on('error', (error) => {
resolve({
accessible: false,
statusCode,
error: error.message,
});
})
.on("timeout", function () {
.on('timeout', function () {
this.destroy();
resolve({
accessible: false,
statusCode,
error: "Timeout following redirect",
error: 'Timeout following redirect',
});
});
}
// Check if status is good (200-299 range) and it's actually a file
const contentType = response.headers["content-type"] || "";
const contentType = response.headers['content-type'] || '';
const isFile =
contentType.includes("application/zip") ||
contentType.includes("application/gzip") ||
contentType.includes("application/x-gzip") ||
contentType.includes("application/x-tar") ||
url.includes(".zip") ||
url.includes(".tar.gz");
contentType.includes('application/zip') ||
contentType.includes('application/gzip') ||
contentType.includes('application/x-gzip') ||
contentType.includes('application/x-tar') ||
url.includes('.zip') ||
url.includes('.tar.gz');
const isGood = statusCode >= 200 && statusCode < 300 && isFile;
response.destroy();
resolve({ accessible: isGood, statusCode, contentType });
});
request.on("error", (error) => {
request.on('error', (error) => {
resolve({
accessible: false,
statusCode: null,
@@ -152,12 +146,12 @@ async function checkUrlAccessible(url, maxRetries = 10, initialDelay = 1000) {
});
});
request.on("timeout", () => {
request.on('timeout', () => {
request.destroy();
resolve({
accessible: false,
statusCode: null,
error: "Request timeout",
error: 'Request timeout',
});
});
});
@@ -168,22 +162,14 @@ async function checkUrlAccessible(url, maxRetries = 10, initialDelay = 1000) {
`✓ URL ${url} is now accessible after ${attempt} retries (status: ${result.statusCode})`
);
} else {
console.log(
`✓ URL ${url} is accessible (status: ${result.statusCode})`
);
console.log(`✓ URL ${url} is accessible (status: ${result.statusCode})`);
}
return result.finalUrl || url; // Return the final URL (after redirects) if available
} else {
const errorMsg = result.error ? ` - ${result.error}` : "";
const statusMsg = result.statusCode
? ` (status: ${result.statusCode})`
: "";
const contentTypeMsg = result.contentType
? ` [content-type: ${result.contentType}]`
: "";
console.log(
`✗ URL ${url} not accessible${statusMsg}${contentTypeMsg}${errorMsg}`
);
const errorMsg = result.error ? ` - ${result.error}` : '';
const statusMsg = result.statusCode ? ` (status: ${result.statusCode})` : '';
const contentTypeMsg = result.contentType ? ` [content-type: ${result.contentType}]` : '';
console.log(`✗ URL ${url} not accessible${statusMsg}${contentTypeMsg}${errorMsg}`);
}
} catch (error) {
console.log(`✗ URL ${url} check failed: ${error.message}`);
@@ -191,9 +177,7 @@ async function checkUrlAccessible(url, maxRetries = 10, initialDelay = 1000) {
if (attempt < maxRetries - 1) {
const delay = initialDelay * Math.pow(2, attempt);
console.log(
` Retrying in ${delay}ms... (attempt ${attempt + 1}/${maxRetries})`
);
console.log(` Retrying in ${delay}ms... (attempt ${attempt + 1}/${maxRetries})`);
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
@@ -207,12 +191,7 @@ async function downloadFromGitHub(url, outputPath) {
const statusCode = response.statusCode;
// Follow redirects (all redirect types)
if (
statusCode === 301 ||
statusCode === 302 ||
statusCode === 307 ||
statusCode === 308
) {
if (statusCode === 301 || statusCode === 302 || statusCode === 307 || statusCode === 308) {
const redirectUrl = response.headers.location;
response.destroy();
if (!redirectUrl) {
@@ -220,39 +199,33 @@ async function downloadFromGitHub(url, outputPath) {
return;
}
// Resolve relative redirects
const finalRedirectUrl = redirectUrl.startsWith("http")
const finalRedirectUrl = redirectUrl.startsWith('http')
? redirectUrl
: new URL(redirectUrl, url).href;
console.log(` Following redirect: ${finalRedirectUrl}`);
return downloadFromGitHub(finalRedirectUrl, outputPath)
.then(resolve)
.catch(reject);
return downloadFromGitHub(finalRedirectUrl, outputPath).then(resolve).catch(reject);
}
if (statusCode !== 200) {
response.destroy();
reject(
new Error(
`Failed to download ${url}: ${statusCode} ${response.statusMessage}`
)
);
reject(new Error(`Failed to download ${url}: ${statusCode} ${response.statusMessage}`));
return;
}
const fileStream = fs.createWriteStream(outputPath);
response.pipe(fileStream);
fileStream.on("finish", () => {
fileStream.on('finish', () => {
fileStream.close();
resolve();
});
fileStream.on("error", (error) => {
fileStream.on('error', (error) => {
response.destroy();
reject(error);
});
});
request.on("error", reject);
request.on("timeout", () => {
request.on('error', reject);
request.on('timeout', () => {
request.destroy();
reject(new Error(`Request timeout for ${url}`));
});
@@ -260,8 +233,8 @@ async function downloadFromGitHub(url, outputPath) {
}
async function main() {
const artifactsDir = "artifacts";
const tempDir = path.join(artifactsDir, "temp");
const artifactsDir = 'artifacts';
const tempDir = path.join(artifactsDir, 'temp');
// Create temp directory for downloaded GitHub archives
if (!fs.existsSync(tempDir)) {
@@ -292,40 +265,30 @@ async function main() {
// Find all artifacts
const artifacts = {
windows: findArtifacts(path.join(artifactsDir, "windows-builds"), /\.exe$/),
macos: findArtifacts(path.join(artifactsDir, "macos-builds"), /-x64\.dmg$/),
macosArm: findArtifacts(
path.join(artifactsDir, "macos-builds"),
/-arm64\.dmg$/
),
linux: findArtifacts(
path.join(artifactsDir, "linux-builds"),
/\.AppImage$/
),
windows: findArtifacts(path.join(artifactsDir, 'windows-builds'), /\.exe$/),
macos: findArtifacts(path.join(artifactsDir, 'macos-builds'), /-x64\.dmg$/),
macosArm: findArtifacts(path.join(artifactsDir, 'macos-builds'), /-arm64\.dmg$/),
linux: findArtifacts(path.join(artifactsDir, 'linux-builds'), /\.AppImage$/),
sourceZip: [sourceZipPath],
sourceTarGz: [sourceTarGzPath],
};
console.log("Found artifacts:");
console.log('Found artifacts:');
for (const [platform, files] of Object.entries(artifacts)) {
console.log(
` ${platform}: ${
files.length > 0
? files.map((f) => path.basename(f)).join(", ")
: "none"
}`
` ${platform}: ${files.length > 0 ? files.map((f) => path.basename(f)).join(', ') : 'none'}`
);
}
// Upload each artifact to R2
const assets = {};
const contentTypes = {
windows: "application/x-msdownload",
macos: "application/x-apple-diskimage",
macosArm: "application/x-apple-diskimage",
linux: "application/x-executable",
sourceZip: "application/zip",
sourceTarGz: "application/gzip",
windows: 'application/x-msdownload',
macos: 'application/x-apple-diskimage',
macosArm: 'application/x-apple-diskimage',
linux: 'application/x-executable',
sourceZip: 'application/zip',
sourceTarGz: 'application/gzip',
};
for (const [platform, files] of Object.entries(artifacts)) {
@@ -345,11 +308,11 @@ async function main() {
filename,
size,
arch:
platform === "macosArm"
? "arm64"
: platform === "sourceZip" || platform === "sourceTarGz"
? "source"
: "x64",
platform === 'macosArm'
? 'arm64'
: platform === 'sourceZip' || platform === 'sourceTarGz'
? 'source'
: 'x64',
};
}
@@ -364,9 +327,7 @@ async function main() {
};
// Remove existing entry for this version if re-running
releasesData.releases = releasesData.releases.filter(
(r) => r.version !== VERSION
);
releasesData.releases = releasesData.releases.filter((r) => r.version !== VERSION);
// Prepend new release
releasesData.releases.unshift(newRelease);
@@ -376,19 +337,19 @@ async function main() {
await s3Client.send(
new PutObjectCommand({
Bucket: BUCKET,
Key: "releases.json",
Key: 'releases.json',
Body: JSON.stringify(releasesData, null, 2),
ContentType: "application/json",
CacheControl: "public, max-age=60",
ContentType: 'application/json',
CacheControl: 'public, max-age=60',
})
);
console.log("Successfully updated releases.json");
console.log('Successfully updated releases.json');
console.log(`Latest version: ${VERSION}`);
console.log(`Total releases: ${releasesData.releases.length}`);
}
main().catch((err) => {
console.error("Failed to upload to R2:", err);
console.error('Failed to upload to R2:', err);
process.exit(1);
});

View File

@@ -47,4 +47,3 @@ jobs:
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
# or https://code.claude.com/docs/en/cli-reference for available options
# claude_args: '--allowed-tools Bash(gh pr:*)'

View File

@@ -3,7 +3,7 @@ name: E2E Tests
on:
pull_request:
branches:
- "*"
- '*'
push:
branches:
- main
@@ -21,8 +21,8 @@ jobs:
- name: Setup project
uses: ./.github/actions/setup-project
with:
check-lockfile: "true"
rebuild-node-pty-path: "apps/server"
check-lockfile: 'true'
rebuild-node-pty-path: 'apps/server'
- name: Install Playwright browsers
run: npx playwright install --with-deps chromium
@@ -58,7 +58,7 @@ jobs:
env:
CI: true
VITE_SERVER_URL: http://localhost:3008
VITE_SKIP_SETUP: "true"
VITE_SKIP_SETUP: 'true'
- name: Upload Playwright report
uses: actions/upload-artifact@v4

View File

@@ -3,7 +3,7 @@ name: PR Build Check
on:
pull_request:
branches:
- "*"
- '*'
push:
branches:
- main
@@ -20,7 +20,7 @@ jobs:
- name: Setup project
uses: ./.github/actions/setup-project
with:
check-lockfile: "true"
check-lockfile: 'true'
- name: Run build:electron (dir only - faster CI)
run: npm run build:electron:dir

View File

@@ -3,7 +3,7 @@ name: Test Suite
on:
pull_request:
branches:
- "*"
- '*'
push:
branches:
- main
@@ -20,8 +20,8 @@ jobs:
- name: Setup project
uses: ./.github/actions/setup-project
with:
check-lockfile: "true"
rebuild-node-pty-path: "apps/server"
check-lockfile: 'true'
rebuild-node-pty-path: 'apps/server'
- name: Run package tests
run: npm run test:packages

View File

@@ -1,6 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/dev/types/routes.d.ts";
import './.next/dev/types/routes.d.ts';
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

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',
});
}
};
}

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