mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-01-30 06:12:03 +00:00
feat: implement server logging and event hook features
- Introduced server log level configuration and HTTP request logging settings, allowing users to control the verbosity of server logs and enable or disable request logging at runtime. - Added an Event Hook Service to execute custom actions based on system events, supporting shell commands and HTTP webhooks. - Enhanced the UI with new sections for managing server logging preferences and event hooks, including a dialog for creating and editing hooks. - Updated global settings to include server log level and request logging options, ensuring persistence across sessions. These changes aim to improve debugging capabilities and provide users with customizable event-driven actions within the application.
This commit is contained in:
@@ -17,9 +17,19 @@ import dotenv from 'dotenv';
|
|||||||
|
|
||||||
import { createEventEmitter, type EventEmitter } from './lib/events.js';
|
import { createEventEmitter, type EventEmitter } from './lib/events.js';
|
||||||
import { initAllowedPaths } from '@automaker/platform';
|
import { initAllowedPaths } from '@automaker/platform';
|
||||||
import { createLogger } from '@automaker/utils';
|
import { createLogger, setLogLevel, LogLevel } from '@automaker/utils';
|
||||||
|
|
||||||
const logger = createLogger('Server');
|
const logger = createLogger('Server');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map server log level string to LogLevel enum
|
||||||
|
*/
|
||||||
|
const LOG_LEVEL_MAP: Record<string, LogLevel> = {
|
||||||
|
error: LogLevel.ERROR,
|
||||||
|
warn: LogLevel.WARN,
|
||||||
|
info: LogLevel.INFO,
|
||||||
|
debug: LogLevel.DEBUG,
|
||||||
|
};
|
||||||
import { authMiddleware, validateWsConnectionToken, checkRawAuthentication } from './lib/auth.js';
|
import { authMiddleware, validateWsConnectionToken, checkRawAuthentication } from './lib/auth.js';
|
||||||
import { requireJsonContentType } from './middleware/require-json-content-type.js';
|
import { requireJsonContentType } from './middleware/require-json-content-type.js';
|
||||||
import { createAuthRoutes } from './routes/auth/index.js';
|
import { createAuthRoutes } from './routes/auth/index.js';
|
||||||
@@ -68,13 +78,31 @@ import { pipelineService } from './services/pipeline-service.js';
|
|||||||
import { createIdeationRoutes } from './routes/ideation/index.js';
|
import { createIdeationRoutes } from './routes/ideation/index.js';
|
||||||
import { IdeationService } from './services/ideation-service.js';
|
import { IdeationService } from './services/ideation-service.js';
|
||||||
import { getDevServerService } from './services/dev-server-service.js';
|
import { getDevServerService } from './services/dev-server-service.js';
|
||||||
|
import { eventHookService } from './services/event-hook-service.js';
|
||||||
|
|
||||||
// Load environment variables
|
// Load environment variables
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
|
|
||||||
const PORT = parseInt(process.env.PORT || '3008', 10);
|
const PORT = parseInt(process.env.PORT || '3008', 10);
|
||||||
const DATA_DIR = process.env.DATA_DIR || './data';
|
const DATA_DIR = process.env.DATA_DIR || './data';
|
||||||
const ENABLE_REQUEST_LOGGING = process.env.ENABLE_REQUEST_LOGGING !== 'false'; // Default to true
|
const ENABLE_REQUEST_LOGGING_DEFAULT = process.env.ENABLE_REQUEST_LOGGING !== 'false'; // Default to true
|
||||||
|
|
||||||
|
// Runtime-configurable request logging flag (can be changed via settings)
|
||||||
|
let requestLoggingEnabled = ENABLE_REQUEST_LOGGING_DEFAULT;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable or disable HTTP request logging at runtime
|
||||||
|
*/
|
||||||
|
export function setRequestLoggingEnabled(enabled: boolean): void {
|
||||||
|
requestLoggingEnabled = enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current request logging state
|
||||||
|
*/
|
||||||
|
export function isRequestLoggingEnabled(): boolean {
|
||||||
|
return requestLoggingEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
// Check for required environment variables
|
// Check for required environment variables
|
||||||
const hasAnthropicKey = !!process.env.ANTHROPIC_API_KEY;
|
const hasAnthropicKey = !!process.env.ANTHROPIC_API_KEY;
|
||||||
@@ -103,22 +131,21 @@ initAllowedPaths();
|
|||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
// Middleware
|
// Middleware
|
||||||
// Custom colored logger showing only endpoint and status code (configurable via ENABLE_REQUEST_LOGGING env var)
|
// Custom colored logger showing only endpoint and status code (dynamically configurable)
|
||||||
if (ENABLE_REQUEST_LOGGING) {
|
morgan.token('status-colored', (_req, res) => {
|
||||||
morgan.token('status-colored', (_req, res) => {
|
const status = res.statusCode;
|
||||||
const status = res.statusCode;
|
if (status >= 500) return `\x1b[31m${status}\x1b[0m`; // Red for server errors
|
||||||
if (status >= 500) return `\x1b[31m${status}\x1b[0m`; // Red for server errors
|
if (status >= 400) return `\x1b[33m${status}\x1b[0m`; // Yellow for client errors
|
||||||
if (status >= 400) return `\x1b[33m${status}\x1b[0m`; // Yellow for client errors
|
if (status >= 300) return `\x1b[36m${status}\x1b[0m`; // Cyan for redirects
|
||||||
if (status >= 300) return `\x1b[36m${status}\x1b[0m`; // Cyan for redirects
|
return `\x1b[32m${status}\x1b[0m`; // Green for success
|
||||||
return `\x1b[32m${status}\x1b[0m`; // Green for success
|
});
|
||||||
});
|
|
||||||
|
|
||||||
app.use(
|
app.use(
|
||||||
morgan(':method :url :status-colored', {
|
morgan(':method :url :status-colored', {
|
||||||
skip: (req) => req.url === '/api/health', // Skip health check logs
|
// Skip when request logging is disabled or for health check endpoints
|
||||||
})
|
skip: (req) => !requestLoggingEnabled || req.url === '/api/health',
|
||||||
);
|
})
|
||||||
}
|
);
|
||||||
// CORS configuration
|
// CORS configuration
|
||||||
// When using credentials (cookies), origin cannot be '*'
|
// When using credentials (cookies), origin cannot be '*'
|
||||||
// We dynamically allow the requesting origin for local development
|
// We dynamically allow the requesting origin for local development
|
||||||
@@ -181,8 +208,26 @@ const ideationService = new IdeationService(events, settingsService, featureLoad
|
|||||||
const devServerService = getDevServerService();
|
const devServerService = getDevServerService();
|
||||||
devServerService.setEventEmitter(events);
|
devServerService.setEventEmitter(events);
|
||||||
|
|
||||||
|
// Initialize Event Hook Service for custom event triggers
|
||||||
|
eventHookService.initialize(events, settingsService);
|
||||||
|
|
||||||
// Initialize services
|
// Initialize services
|
||||||
(async () => {
|
(async () => {
|
||||||
|
// Apply logging settings from saved settings
|
||||||
|
try {
|
||||||
|
const settings = await settingsService.getGlobalSettings();
|
||||||
|
if (settings.serverLogLevel && LOG_LEVEL_MAP[settings.serverLogLevel] !== undefined) {
|
||||||
|
setLogLevel(LOG_LEVEL_MAP[settings.serverLogLevel]);
|
||||||
|
logger.info(`Server log level set to: ${settings.serverLogLevel}`);
|
||||||
|
}
|
||||||
|
// Apply request logging setting (default true if not set)
|
||||||
|
const enableRequestLog = settings.enableRequestLogging ?? true;
|
||||||
|
setRequestLoggingEnabled(enableRequestLog);
|
||||||
|
logger.info(`HTTP request logging: ${enableRequestLog ? 'enabled' : 'disabled'}`);
|
||||||
|
} catch (err) {
|
||||||
|
logger.warn('Failed to load logging settings, using defaults');
|
||||||
|
}
|
||||||
|
|
||||||
await agentService.initialize();
|
await agentService.initialize();
|
||||||
logger.info('Agent service initialized');
|
logger.info('Agent service initialized');
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,18 @@ import type { Request, Response } from 'express';
|
|||||||
import type { SettingsService } from '../../../services/settings-service.js';
|
import type { SettingsService } from '../../../services/settings-service.js';
|
||||||
import type { GlobalSettings } from '../../../types/settings.js';
|
import type { GlobalSettings } from '../../../types/settings.js';
|
||||||
import { getErrorMessage, logError, logger } from '../common.js';
|
import { getErrorMessage, logError, logger } from '../common.js';
|
||||||
|
import { setLogLevel, LogLevel } from '@automaker/utils';
|
||||||
|
import { setRequestLoggingEnabled } from '../../../index.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map server log level string to LogLevel enum
|
||||||
|
*/
|
||||||
|
const LOG_LEVEL_MAP: Record<string, LogLevel> = {
|
||||||
|
error: LogLevel.ERROR,
|
||||||
|
warn: LogLevel.WARN,
|
||||||
|
info: LogLevel.INFO,
|
||||||
|
debug: LogLevel.DEBUG,
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create handler factory for PUT /api/settings/global
|
* Create handler factory for PUT /api/settings/global
|
||||||
@@ -46,6 +58,23 @@ export function createUpdateGlobalHandler(settingsService: SettingsService) {
|
|||||||
|
|
||||||
const settings = await settingsService.updateGlobalSettings(updates);
|
const settings = await settingsService.updateGlobalSettings(updates);
|
||||||
|
|
||||||
|
// Apply server log level if it was updated
|
||||||
|
if ('serverLogLevel' in updates && updates.serverLogLevel) {
|
||||||
|
const level = LOG_LEVEL_MAP[updates.serverLogLevel];
|
||||||
|
if (level !== undefined) {
|
||||||
|
setLogLevel(level);
|
||||||
|
logger.info(`Server log level changed to: ${updates.serverLogLevel}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply request logging setting if it was updated
|
||||||
|
if ('enableRequestLogging' in updates && typeof updates.enableRequestLogging === 'boolean') {
|
||||||
|
setRequestLoggingEnabled(updates.enableRequestLogging);
|
||||||
|
logger.info(
|
||||||
|
`HTTP request logging ${updates.enableRequestLogging ? 'enabled' : 'disabled'}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
settings,
|
settings,
|
||||||
|
|||||||
316
apps/server/src/services/event-hook-service.ts
Normal file
316
apps/server/src/services/event-hook-service.ts
Normal file
@@ -0,0 +1,316 @@
|
|||||||
|
/**
|
||||||
|
* Event Hook Service - Executes custom actions when system events occur
|
||||||
|
*
|
||||||
|
* Listens to the event emitter and triggers configured hooks:
|
||||||
|
* - Shell commands: Executed with configurable timeout
|
||||||
|
* - HTTP webhooks: POST/GET/PUT/PATCH requests with variable substitution
|
||||||
|
*
|
||||||
|
* Supported events:
|
||||||
|
* - feature_success: Feature completed successfully
|
||||||
|
* - feature_error: Feature failed with an error
|
||||||
|
* - auto_mode_complete: Auto mode finished all features (idle state)
|
||||||
|
* - auto_mode_error: Auto mode encountered a critical error
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { exec } from 'child_process';
|
||||||
|
import { promisify } from 'util';
|
||||||
|
import { createLogger } from '@automaker/utils';
|
||||||
|
import type { EventEmitter } from '../lib/events.js';
|
||||||
|
import type { SettingsService } from './settings-service.js';
|
||||||
|
import type {
|
||||||
|
EventHook,
|
||||||
|
EventHookTrigger,
|
||||||
|
EventHookShellAction,
|
||||||
|
EventHookHttpAction,
|
||||||
|
} from '@automaker/types';
|
||||||
|
|
||||||
|
const execAsync = promisify(exec);
|
||||||
|
const logger = createLogger('EventHooks');
|
||||||
|
|
||||||
|
/** Default timeout for shell commands (30 seconds) */
|
||||||
|
const DEFAULT_SHELL_TIMEOUT = 30000;
|
||||||
|
|
||||||
|
/** Default timeout for HTTP requests (10 seconds) */
|
||||||
|
const DEFAULT_HTTP_TIMEOUT = 10000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Context available for variable substitution in hooks
|
||||||
|
*/
|
||||||
|
interface HookContext {
|
||||||
|
featureId?: string;
|
||||||
|
featureName?: string;
|
||||||
|
projectPath?: string;
|
||||||
|
projectName?: string;
|
||||||
|
error?: string;
|
||||||
|
errorType?: string;
|
||||||
|
timestamp: string;
|
||||||
|
eventType: EventHookTrigger;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-mode event payload structure
|
||||||
|
*/
|
||||||
|
interface AutoModeEventPayload {
|
||||||
|
type?: string;
|
||||||
|
featureId?: string;
|
||||||
|
passes?: boolean;
|
||||||
|
message?: string;
|
||||||
|
error?: string;
|
||||||
|
errorType?: string;
|
||||||
|
projectPath?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Event Hook Service
|
||||||
|
*
|
||||||
|
* Manages execution of user-configured event hooks in response to system events.
|
||||||
|
*/
|
||||||
|
export class EventHookService {
|
||||||
|
private emitter: EventEmitter | null = null;
|
||||||
|
private settingsService: SettingsService | null = null;
|
||||||
|
private unsubscribe: (() => void) | null = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the service with event emitter and settings service
|
||||||
|
*/
|
||||||
|
initialize(emitter: EventEmitter, settingsService: SettingsService): void {
|
||||||
|
this.emitter = emitter;
|
||||||
|
this.settingsService = settingsService;
|
||||||
|
|
||||||
|
// Subscribe to auto-mode events
|
||||||
|
this.unsubscribe = emitter.subscribe((type, payload) => {
|
||||||
|
if (type === 'auto-mode:event') {
|
||||||
|
this.handleAutoModeEvent(payload as AutoModeEventPayload);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.info('Event hook service initialized');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cleanup subscriptions
|
||||||
|
*/
|
||||||
|
destroy(): void {
|
||||||
|
if (this.unsubscribe) {
|
||||||
|
this.unsubscribe();
|
||||||
|
this.unsubscribe = null;
|
||||||
|
}
|
||||||
|
this.emitter = null;
|
||||||
|
this.settingsService = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle auto-mode events and trigger matching hooks
|
||||||
|
*/
|
||||||
|
private async handleAutoModeEvent(payload: AutoModeEventPayload): Promise<void> {
|
||||||
|
if (!payload.type) return;
|
||||||
|
|
||||||
|
// Map internal event types to hook triggers
|
||||||
|
let trigger: EventHookTrigger | null = null;
|
||||||
|
|
||||||
|
switch (payload.type) {
|
||||||
|
case 'auto_mode_feature_complete':
|
||||||
|
trigger = payload.passes ? 'feature_success' : 'feature_error';
|
||||||
|
break;
|
||||||
|
case 'auto_mode_error':
|
||||||
|
// Feature-level error (has featureId) vs auto-mode level error
|
||||||
|
trigger = payload.featureId ? 'feature_error' : 'auto_mode_error';
|
||||||
|
break;
|
||||||
|
case 'auto_mode_idle':
|
||||||
|
trigger = 'auto_mode_complete';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// Other event types don't trigger hooks
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!trigger) return;
|
||||||
|
|
||||||
|
// Build context for variable substitution
|
||||||
|
const context: HookContext = {
|
||||||
|
featureId: payload.featureId,
|
||||||
|
projectPath: payload.projectPath,
|
||||||
|
projectName: payload.projectPath ? this.extractProjectName(payload.projectPath) : undefined,
|
||||||
|
error: payload.error || payload.message,
|
||||||
|
errorType: payload.errorType,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
eventType: trigger,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Execute matching hooks
|
||||||
|
await this.executeHooksForTrigger(trigger, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute all enabled hooks matching the given trigger
|
||||||
|
*/
|
||||||
|
private async executeHooksForTrigger(
|
||||||
|
trigger: EventHookTrigger,
|
||||||
|
context: HookContext
|
||||||
|
): Promise<void> {
|
||||||
|
if (!this.settingsService) {
|
||||||
|
logger.warn('Settings service not available');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const settings = await this.settingsService.getGlobalSettings();
|
||||||
|
const hooks = settings.eventHooks || [];
|
||||||
|
|
||||||
|
// Filter to enabled hooks matching this trigger
|
||||||
|
const matchingHooks = hooks.filter((hook) => hook.enabled && hook.trigger === trigger);
|
||||||
|
|
||||||
|
if (matchingHooks.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`Executing ${matchingHooks.length} hook(s) for trigger: ${trigger}`);
|
||||||
|
|
||||||
|
// Execute hooks in parallel (don't wait for one to finish before starting next)
|
||||||
|
await Promise.allSettled(matchingHooks.map((hook) => this.executeHook(hook, context)));
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error executing hooks:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute a single hook
|
||||||
|
*/
|
||||||
|
private async executeHook(hook: EventHook, context: HookContext): Promise<void> {
|
||||||
|
const hookName = hook.name || hook.id;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (hook.action.type === 'shell') {
|
||||||
|
await this.executeShellHook(hook.action, context, hookName);
|
||||||
|
} else if (hook.action.type === 'http') {
|
||||||
|
await this.executeHttpHook(hook.action, context, hookName);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Hook "${hookName}" failed:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute a shell command hook
|
||||||
|
*/
|
||||||
|
private async executeShellHook(
|
||||||
|
action: EventHookShellAction,
|
||||||
|
context: HookContext,
|
||||||
|
hookName: string
|
||||||
|
): Promise<void> {
|
||||||
|
const command = this.substituteVariables(action.command, context);
|
||||||
|
const timeout = action.timeout || DEFAULT_SHELL_TIMEOUT;
|
||||||
|
|
||||||
|
logger.info(`Executing shell hook "${hookName}": ${command}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { stdout, stderr } = await execAsync(command, {
|
||||||
|
timeout,
|
||||||
|
maxBuffer: 1024 * 1024, // 1MB buffer
|
||||||
|
});
|
||||||
|
|
||||||
|
if (stdout) {
|
||||||
|
logger.debug(`Hook "${hookName}" stdout: ${stdout.trim()}`);
|
||||||
|
}
|
||||||
|
if (stderr) {
|
||||||
|
logger.warn(`Hook "${hookName}" stderr: ${stderr.trim()}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`Shell hook "${hookName}" completed successfully`);
|
||||||
|
} catch (error) {
|
||||||
|
if ((error as NodeJS.ErrnoException).code === 'ETIMEDOUT') {
|
||||||
|
logger.error(`Shell hook "${hookName}" timed out after ${timeout}ms`);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute an HTTP webhook hook
|
||||||
|
*/
|
||||||
|
private async executeHttpHook(
|
||||||
|
action: EventHookHttpAction,
|
||||||
|
context: HookContext,
|
||||||
|
hookName: string
|
||||||
|
): Promise<void> {
|
||||||
|
const url = this.substituteVariables(action.url, context);
|
||||||
|
const method = action.method || 'POST';
|
||||||
|
|
||||||
|
// Substitute variables in headers
|
||||||
|
const headers: Record<string, string> = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
};
|
||||||
|
if (action.headers) {
|
||||||
|
for (const [key, value] of Object.entries(action.headers)) {
|
||||||
|
headers[key] = this.substituteVariables(value, context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Substitute variables in body
|
||||||
|
let body: string | undefined;
|
||||||
|
if (action.body) {
|
||||||
|
body = this.substituteVariables(action.body, context);
|
||||||
|
} else if (method !== 'GET') {
|
||||||
|
// Default body with context information
|
||||||
|
body = JSON.stringify({
|
||||||
|
eventType: context.eventType,
|
||||||
|
timestamp: context.timestamp,
|
||||||
|
featureId: context.featureId,
|
||||||
|
projectPath: context.projectPath,
|
||||||
|
projectName: context.projectName,
|
||||||
|
error: context.error,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`Executing HTTP hook "${hookName}": ${method} ${url}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timeoutId = setTimeout(() => controller.abort(), DEFAULT_HTTP_TIMEOUT);
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method,
|
||||||
|
headers,
|
||||||
|
body: method !== 'GET' ? body : undefined,
|
||||||
|
signal: controller.signal,
|
||||||
|
});
|
||||||
|
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
logger.warn(`HTTP hook "${hookName}" received status ${response.status}`);
|
||||||
|
} else {
|
||||||
|
logger.info(`HTTP hook "${hookName}" completed successfully (status: ${response.status})`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if ((error as Error).name === 'AbortError') {
|
||||||
|
logger.error(`HTTP hook "${hookName}" timed out after ${DEFAULT_HTTP_TIMEOUT}ms`);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Substitute {{variable}} placeholders in a string
|
||||||
|
*/
|
||||||
|
private substituteVariables(template: string, context: HookContext): string {
|
||||||
|
return template.replace(/\{\{(\w+)\}\}/g, (match, variable) => {
|
||||||
|
const value = context[variable as keyof HookContext];
|
||||||
|
if (value === undefined || value === null) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return String(value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract project name from path
|
||||||
|
*/
|
||||||
|
private extractProjectName(projectPath: string): string {
|
||||||
|
const parts = projectPath.split(/[/\\]/);
|
||||||
|
return parts[parts.length - 1] || projectPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Singleton instance
|
||||||
|
export const eventHookService = new EventHookService();
|
||||||
@@ -19,6 +19,7 @@ import { WorktreesSection } from './settings-view/worktrees';
|
|||||||
import { DangerZoneSection } from './settings-view/danger-zone/danger-zone-section';
|
import { DangerZoneSection } from './settings-view/danger-zone/danger-zone-section';
|
||||||
import { AccountSection } from './settings-view/account';
|
import { AccountSection } from './settings-view/account';
|
||||||
import { SecuritySection } from './settings-view/security';
|
import { SecuritySection } from './settings-view/security';
|
||||||
|
import { DeveloperSection } from './settings-view/developer/developer-section';
|
||||||
import {
|
import {
|
||||||
ClaudeSettingsTab,
|
ClaudeSettingsTab,
|
||||||
CursorSettingsTab,
|
CursorSettingsTab,
|
||||||
@@ -27,6 +28,7 @@ import {
|
|||||||
} from './settings-view/providers';
|
} from './settings-view/providers';
|
||||||
import { MCPServersSection } from './settings-view/mcp-servers';
|
import { MCPServersSection } from './settings-view/mcp-servers';
|
||||||
import { PromptCustomizationSection } from './settings-view/prompts';
|
import { PromptCustomizationSection } from './settings-view/prompts';
|
||||||
|
import { EventHooksSection } from './settings-view/event-hooks';
|
||||||
import type { Project as SettingsProject, Theme } from './settings-view/shared/types';
|
import type { Project as SettingsProject, Theme } from './settings-view/shared/types';
|
||||||
import type { Project as ElectronProject } from '@/lib/electron';
|
import type { Project as ElectronProject } from '@/lib/electron';
|
||||||
|
|
||||||
@@ -183,6 +185,8 @@ export function SettingsView() {
|
|||||||
return (
|
return (
|
||||||
<AudioSection muteDoneSound={muteDoneSound} onMuteDoneSoundChange={setMuteDoneSound} />
|
<AudioSection muteDoneSound={muteDoneSound} onMuteDoneSoundChange={setMuteDoneSound} />
|
||||||
);
|
);
|
||||||
|
case 'event-hooks':
|
||||||
|
return <EventHooksSection />;
|
||||||
case 'defaults':
|
case 'defaults':
|
||||||
return (
|
return (
|
||||||
<FeatureDefaultsSection
|
<FeatureDefaultsSection
|
||||||
@@ -215,6 +219,8 @@ export function SettingsView() {
|
|||||||
onSkipSandboxWarningChange={setSkipSandboxWarning}
|
onSkipSandboxWarningChange={setSkipSandboxWarning}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
case 'developer':
|
||||||
|
return <DeveloperSection />;
|
||||||
case 'danger':
|
case 'danger':
|
||||||
return (
|
return (
|
||||||
<DangerZoneSection
|
<DangerZoneSection
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ import {
|
|||||||
Shield,
|
Shield,
|
||||||
Cpu,
|
Cpu,
|
||||||
GitBranch,
|
GitBranch,
|
||||||
|
Code2,
|
||||||
|
Webhook,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { AnthropicIcon, CursorIcon, OpenAIIcon, OpenCodeIcon } from '@/components/ui/provider-icon';
|
import { AnthropicIcon, CursorIcon, OpenAIIcon, OpenCodeIcon } from '@/components/ui/provider-icon';
|
||||||
import type { SettingsViewId } from '../hooks/use-settings-view';
|
import type { SettingsViewId } from '../hooks/use-settings-view';
|
||||||
@@ -63,6 +65,7 @@ export const GLOBAL_NAV_GROUPS: NavigationGroup[] = [
|
|||||||
{ id: 'terminal', label: 'Terminal', icon: SquareTerminal },
|
{ id: 'terminal', label: 'Terminal', icon: SquareTerminal },
|
||||||
{ id: 'keyboard', label: 'Keyboard Shortcuts', icon: Settings2 },
|
{ id: 'keyboard', label: 'Keyboard Shortcuts', icon: Settings2 },
|
||||||
{ id: 'audio', label: 'Audio', icon: Volume2 },
|
{ id: 'audio', label: 'Audio', icon: Volume2 },
|
||||||
|
{ id: 'event-hooks', label: 'Event Hooks', icon: Webhook },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -72,6 +75,10 @@ export const GLOBAL_NAV_GROUPS: NavigationGroup[] = [
|
|||||||
{ id: 'security', label: 'Security', icon: Shield },
|
{ id: 'security', label: 'Security', icon: Shield },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: 'Advanced',
|
||||||
|
items: [{ id: 'developer', label: 'Developer', icon: Code2 }],
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
// Flat list of all global nav items for backwards compatibility
|
// Flat list of all global nav items for backwards compatibility
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
import { Switch } from '@/components/ui/switch';
|
||||||
|
import { Code2 } from 'lucide-react';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import { useAppStore, type ServerLogLevel } from '@/store/app-store';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
|
const LOG_LEVEL_OPTIONS: { value: ServerLogLevel; label: string; description: string }[] = [
|
||||||
|
{ value: 'error', label: 'Error', description: 'Only show error messages' },
|
||||||
|
{ value: 'warn', label: 'Warning', description: 'Show warnings and errors' },
|
||||||
|
{ value: 'info', label: 'Info', description: 'Show general information (default)' },
|
||||||
|
{ value: 'debug', label: 'Debug', description: 'Show all messages including debug' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export function DeveloperSection() {
|
||||||
|
const { serverLogLevel, setServerLogLevel, enableRequestLogging, setEnableRequestLogging } =
|
||||||
|
useAppStore();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'rounded-2xl overflow-hidden',
|
||||||
|
'border border-border/50',
|
||||||
|
'bg-gradient-to-br from-card/90 via-card/70 to-card/80 backdrop-blur-xl',
|
||||||
|
'shadow-sm shadow-black/5'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="p-6 border-b border-border/50 bg-gradient-to-r from-transparent via-accent/5 to-transparent">
|
||||||
|
<div className="flex items-center gap-3 mb-2">
|
||||||
|
<div className="w-9 h-9 rounded-xl bg-gradient-to-br from-purple-500/20 to-purple-600/10 flex items-center justify-center border border-purple-500/20">
|
||||||
|
<Code2 className="w-5 h-5 text-purple-500" />
|
||||||
|
</div>
|
||||||
|
<h2 className="text-lg font-semibold text-foreground tracking-tight">Developer</h2>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-muted-foreground/80 ml-12">
|
||||||
|
Advanced settings for debugging and development.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="p-6 space-y-6">
|
||||||
|
{/* Server Log Level */}
|
||||||
|
<div className="space-y-3">
|
||||||
|
<Label className="text-foreground font-medium">Server Log Level</Label>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Control the verbosity of API server logs. Set to "Error" to only see error messages in
|
||||||
|
the server console.
|
||||||
|
</p>
|
||||||
|
<select
|
||||||
|
value={serverLogLevel}
|
||||||
|
onChange={(e) => {
|
||||||
|
setServerLogLevel(e.target.value as ServerLogLevel);
|
||||||
|
toast.success(`Log level changed to ${e.target.value}`, {
|
||||||
|
description: 'Server logging verbosity updated',
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
className={cn(
|
||||||
|
'w-full px-3 py-2 rounded-lg',
|
||||||
|
'bg-accent/30 border border-border/50',
|
||||||
|
'text-foreground text-sm',
|
||||||
|
'focus:outline-none focus:ring-2 focus:ring-purple-500/30'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{LOG_LEVEL_OPTIONS.map((option) => (
|
||||||
|
<option key={option.value} value={option.value}>
|
||||||
|
{option.label} - {option.description}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* HTTP Request Logging */}
|
||||||
|
<div className="flex items-center justify-between pt-4 border-t border-border/30">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Label className="text-foreground font-medium">HTTP Request Logging</Label>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Log all HTTP requests (method, URL, status) to the server console.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
checked={enableRequestLogging}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
setEnableRequestLogging(checked);
|
||||||
|
toast.success(checked ? 'Request logging enabled' : 'Request logging disabled', {
|
||||||
|
description: 'HTTP request logging updated',
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,289 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from '@/components/ui/dialog';
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@/components/ui/select';
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||||
|
import { Terminal, Globe } from 'lucide-react';
|
||||||
|
import type {
|
||||||
|
EventHook,
|
||||||
|
EventHookTrigger,
|
||||||
|
EventHookHttpMethod,
|
||||||
|
EventHookShellAction,
|
||||||
|
EventHookHttpAction,
|
||||||
|
} from '@automaker/types';
|
||||||
|
import { EVENT_HOOK_TRIGGER_LABELS } from '@automaker/types';
|
||||||
|
|
||||||
|
interface EventHookDialogProps {
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
editingHook: EventHook | null;
|
||||||
|
onSave: (hook: EventHook) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
type ActionType = 'shell' | 'http';
|
||||||
|
|
||||||
|
const TRIGGER_OPTIONS: EventHookTrigger[] = [
|
||||||
|
'feature_success',
|
||||||
|
'feature_error',
|
||||||
|
'auto_mode_complete',
|
||||||
|
'auto_mode_error',
|
||||||
|
];
|
||||||
|
|
||||||
|
const HTTP_METHODS: EventHookHttpMethod[] = ['POST', 'GET', 'PUT', 'PATCH'];
|
||||||
|
|
||||||
|
export function EventHookDialog({ open, onOpenChange, editingHook, onSave }: EventHookDialogProps) {
|
||||||
|
// Form state
|
||||||
|
const [name, setName] = useState('');
|
||||||
|
const [trigger, setTrigger] = useState<EventHookTrigger>('feature_success');
|
||||||
|
const [actionType, setActionType] = useState<ActionType>('shell');
|
||||||
|
|
||||||
|
// Shell action state
|
||||||
|
const [command, setCommand] = useState('');
|
||||||
|
const [timeout, setTimeout] = useState('30000');
|
||||||
|
|
||||||
|
// HTTP action state
|
||||||
|
const [url, setUrl] = useState('');
|
||||||
|
const [method, setMethod] = useState<EventHookHttpMethod>('POST');
|
||||||
|
const [headers, setHeaders] = useState('');
|
||||||
|
const [body, setBody] = useState('');
|
||||||
|
|
||||||
|
// Reset form when dialog opens/closes or editingHook changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (open) {
|
||||||
|
if (editingHook) {
|
||||||
|
// Populate form with existing hook data
|
||||||
|
setName(editingHook.name || '');
|
||||||
|
setTrigger(editingHook.trigger);
|
||||||
|
setActionType(editingHook.action.type);
|
||||||
|
|
||||||
|
if (editingHook.action.type === 'shell') {
|
||||||
|
const shellAction = editingHook.action as EventHookShellAction;
|
||||||
|
setCommand(shellAction.command);
|
||||||
|
setTimeout(String(shellAction.timeout || 30000));
|
||||||
|
// Reset HTTP fields
|
||||||
|
setUrl('');
|
||||||
|
setMethod('POST');
|
||||||
|
setHeaders('');
|
||||||
|
setBody('');
|
||||||
|
} else {
|
||||||
|
const httpAction = editingHook.action as EventHookHttpAction;
|
||||||
|
setUrl(httpAction.url);
|
||||||
|
setMethod(httpAction.method);
|
||||||
|
setHeaders(httpAction.headers ? JSON.stringify(httpAction.headers, null, 2) : '');
|
||||||
|
setBody(httpAction.body || '');
|
||||||
|
// Reset shell fields
|
||||||
|
setCommand('');
|
||||||
|
setTimeout('30000');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Reset to defaults for new hook
|
||||||
|
setName('');
|
||||||
|
setTrigger('feature_success');
|
||||||
|
setActionType('shell');
|
||||||
|
setCommand('');
|
||||||
|
setTimeout('30000');
|
||||||
|
setUrl('');
|
||||||
|
setMethod('POST');
|
||||||
|
setHeaders('');
|
||||||
|
setBody('');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [open, editingHook]);
|
||||||
|
|
||||||
|
const handleSave = () => {
|
||||||
|
const hook: EventHook = {
|
||||||
|
id: editingHook?.id || crypto.randomUUID(),
|
||||||
|
name: name.trim() || undefined,
|
||||||
|
trigger,
|
||||||
|
enabled: editingHook?.enabled ?? true,
|
||||||
|
action:
|
||||||
|
actionType === 'shell'
|
||||||
|
? {
|
||||||
|
type: 'shell',
|
||||||
|
command,
|
||||||
|
timeout: parseInt(timeout, 10) || 30000,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
type: 'http',
|
||||||
|
url,
|
||||||
|
method,
|
||||||
|
headers: headers.trim() ? JSON.parse(headers) : undefined,
|
||||||
|
body: body.trim() || undefined,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
onSave(hook);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isValid = actionType === 'shell' ? command.trim().length > 0 : url.trim().length > 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<DialogContent className="max-w-lg max-h-[85vh] overflow-y-auto">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>{editingHook ? 'Edit Event Hook' : 'Add Event Hook'}</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Configure an action to run when a specific event occurs.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="space-y-4 py-4">
|
||||||
|
{/* Name (optional) */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="hook-name">Name (optional)</Label>
|
||||||
|
<Input
|
||||||
|
id="hook-name"
|
||||||
|
value={name}
|
||||||
|
onChange={(e) => setName(e.target.value)}
|
||||||
|
placeholder="My notification hook"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Trigger selection */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="hook-trigger">Trigger Event</Label>
|
||||||
|
<Select value={trigger} onValueChange={(v) => setTrigger(v as EventHookTrigger)}>
|
||||||
|
<SelectTrigger id="hook-trigger">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{TRIGGER_OPTIONS.map((t) => (
|
||||||
|
<SelectItem key={t} value={t}>
|
||||||
|
{EVENT_HOOK_TRIGGER_LABELS[t]}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Action type tabs */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Action Type</Label>
|
||||||
|
<Tabs value={actionType} onValueChange={(v) => setActionType(v as ActionType)}>
|
||||||
|
<TabsList className="w-full">
|
||||||
|
<TabsTrigger value="shell" className="flex-1 gap-2">
|
||||||
|
<Terminal className="w-4 h-4" />
|
||||||
|
Shell Command
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="http" className="flex-1 gap-2">
|
||||||
|
<Globe className="w-4 h-4" />
|
||||||
|
HTTP Request
|
||||||
|
</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
{/* Shell command form */}
|
||||||
|
<TabsContent value="shell" className="space-y-4 mt-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="shell-command">Command</Label>
|
||||||
|
<Textarea
|
||||||
|
id="shell-command"
|
||||||
|
value={command}
|
||||||
|
onChange={(e) => setCommand(e.target.value)}
|
||||||
|
placeholder='echo "Feature {{featureId}} completed!"'
|
||||||
|
className="font-mono text-sm"
|
||||||
|
rows={3}
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Use {'{{variable}}'} syntax for dynamic values
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="shell-timeout">Timeout (ms)</Label>
|
||||||
|
<Input
|
||||||
|
id="shell-timeout"
|
||||||
|
type="number"
|
||||||
|
value={timeout}
|
||||||
|
onChange={(e) => setTimeout(e.target.value)}
|
||||||
|
placeholder="30000"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
{/* HTTP request form */}
|
||||||
|
<TabsContent value="http" className="space-y-4 mt-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="http-url">URL</Label>
|
||||||
|
<Input
|
||||||
|
id="http-url"
|
||||||
|
value={url}
|
||||||
|
onChange={(e) => setUrl(e.target.value)}
|
||||||
|
placeholder="https://api.example.com/webhook"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="http-method">Method</Label>
|
||||||
|
<Select value={method} onValueChange={(v) => setMethod(v as EventHookHttpMethod)}>
|
||||||
|
<SelectTrigger id="http-method">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{HTTP_METHODS.map((m) => (
|
||||||
|
<SelectItem key={m} value={m}>
|
||||||
|
{m}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="http-headers">Headers (JSON, optional)</Label>
|
||||||
|
<Textarea
|
||||||
|
id="http-headers"
|
||||||
|
value={headers}
|
||||||
|
onChange={(e) => setHeaders(e.target.value)}
|
||||||
|
placeholder={'{\n "Authorization": "Bearer {{token}}"\n}'}
|
||||||
|
className="font-mono text-sm"
|
||||||
|
rows={3}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="http-body">Body (JSON, optional)</Label>
|
||||||
|
<Textarea
|
||||||
|
id="http-body"
|
||||||
|
value={body}
|
||||||
|
onChange={(e) => setBody(e.target.value)}
|
||||||
|
placeholder={'{\n "feature": "{{featureId}}",\n "status": "{{eventType}}"\n}'}
|
||||||
|
className="font-mono text-sm"
|
||||||
|
rows={4}
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Leave empty for default body with all event context
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleSave} disabled={!isValid}>
|
||||||
|
{editingHook ? 'Save Changes' : 'Add Hook'}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,202 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Switch } from '@/components/ui/switch';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import { Webhook, Plus, Trash2, Pencil, Terminal, Globe } from 'lucide-react';
|
||||||
|
import { useAppStore } from '@/store/app-store';
|
||||||
|
import type { EventHook, EventHookTrigger } from '@automaker/types';
|
||||||
|
import { EVENT_HOOK_TRIGGER_LABELS } from '@automaker/types';
|
||||||
|
import { EventHookDialog } from './event-hook-dialog';
|
||||||
|
|
||||||
|
export function EventHooksSection() {
|
||||||
|
const { eventHooks, setEventHooks } = useAppStore();
|
||||||
|
const [dialogOpen, setDialogOpen] = useState(false);
|
||||||
|
const [editingHook, setEditingHook] = useState<EventHook | null>(null);
|
||||||
|
|
||||||
|
const handleAddHook = () => {
|
||||||
|
setEditingHook(null);
|
||||||
|
setDialogOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEditHook = (hook: EventHook) => {
|
||||||
|
setEditingHook(hook);
|
||||||
|
setDialogOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteHook = (hookId: string) => {
|
||||||
|
setEventHooks(eventHooks.filter((h) => h.id !== hookId));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleToggleHook = (hookId: string, enabled: boolean) => {
|
||||||
|
setEventHooks(eventHooks.map((h) => (h.id === hookId ? { ...h, enabled } : h)));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSaveHook = (hook: EventHook) => {
|
||||||
|
if (editingHook) {
|
||||||
|
// Update existing
|
||||||
|
setEventHooks(eventHooks.map((h) => (h.id === hook.id ? hook : h)));
|
||||||
|
} else {
|
||||||
|
// Add new
|
||||||
|
setEventHooks([...eventHooks, hook]);
|
||||||
|
}
|
||||||
|
setDialogOpen(false);
|
||||||
|
setEditingHook(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Group hooks by trigger type for better organization
|
||||||
|
const hooksByTrigger = eventHooks.reduce(
|
||||||
|
(acc, hook) => {
|
||||||
|
if (!acc[hook.trigger]) {
|
||||||
|
acc[hook.trigger] = [];
|
||||||
|
}
|
||||||
|
acc[hook.trigger].push(hook);
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{} as Record<EventHookTrigger, EventHook[]>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'rounded-2xl overflow-hidden',
|
||||||
|
'border border-border/50',
|
||||||
|
'bg-gradient-to-br from-card/90 via-card/70 to-card/80 backdrop-blur-xl',
|
||||||
|
'shadow-sm shadow-black/5'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{/* Header */}
|
||||||
|
<div className="p-6 border-b border-border/50 bg-gradient-to-r from-transparent via-accent/5 to-transparent">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="w-9 h-9 rounded-xl bg-gradient-to-br from-brand-500/20 to-brand-600/10 flex items-center justify-center border border-brand-500/20">
|
||||||
|
<Webhook className="w-5 h-5 text-brand-500" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2 className="text-lg font-semibold text-foreground tracking-tight">Event Hooks</h2>
|
||||||
|
<p className="text-sm text-muted-foreground/80">
|
||||||
|
Run custom commands or webhooks when events occur
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button onClick={handleAddHook} size="sm" className="gap-2">
|
||||||
|
<Plus className="w-4 h-4" />
|
||||||
|
Add Hook
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<div className="p-6">
|
||||||
|
{eventHooks.length === 0 ? (
|
||||||
|
<div className="text-center py-8 text-muted-foreground">
|
||||||
|
<Webhook className="w-12 h-12 mx-auto mb-3 opacity-30" />
|
||||||
|
<p className="text-sm">No event hooks configured</p>
|
||||||
|
<p className="text-xs mt-1">
|
||||||
|
Add hooks to run commands or send webhooks when features complete
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Group by trigger type */}
|
||||||
|
{Object.entries(hooksByTrigger).map(([trigger, hooks]) => (
|
||||||
|
<div key={trigger} className="space-y-3">
|
||||||
|
<h3 className="text-sm font-medium text-muted-foreground">
|
||||||
|
{EVENT_HOOK_TRIGGER_LABELS[trigger as EventHookTrigger]}
|
||||||
|
</h3>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{hooks.map((hook) => (
|
||||||
|
<HookCard
|
||||||
|
key={hook.id}
|
||||||
|
hook={hook}
|
||||||
|
onEdit={() => handleEditHook(hook)}
|
||||||
|
onDelete={() => handleDeleteHook(hook.id)}
|
||||||
|
onToggle={(enabled) => handleToggleHook(hook.id, enabled)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Variable reference */}
|
||||||
|
<div className="px-6 pb-6">
|
||||||
|
<div className="rounded-lg bg-muted/30 p-4 text-xs text-muted-foreground">
|
||||||
|
<p className="font-medium mb-2">Available variables:</p>
|
||||||
|
<code className="text-[10px] leading-relaxed">
|
||||||
|
{'{{featureId}}'} {'{{featureName}}'} {'{{projectPath}}'} {'{{projectName}}'}{' '}
|
||||||
|
{'{{error}}'} {'{{timestamp}}'} {'{{eventType}}'}
|
||||||
|
</code>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Dialog */}
|
||||||
|
<EventHookDialog
|
||||||
|
open={dialogOpen}
|
||||||
|
onOpenChange={setDialogOpen}
|
||||||
|
editingHook={editingHook}
|
||||||
|
onSave={handleSaveHook}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface HookCardProps {
|
||||||
|
hook: EventHook;
|
||||||
|
onEdit: () => void;
|
||||||
|
onDelete: () => void;
|
||||||
|
onToggle: (enabled: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function HookCard({ hook, onEdit, onDelete, onToggle }: HookCardProps) {
|
||||||
|
const isShell = hook.action.type === 'shell';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'flex items-center gap-3 p-3 rounded-lg border',
|
||||||
|
'bg-background/50 hover:bg-background/80 transition-colors',
|
||||||
|
!hook.enabled && 'opacity-60'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{/* Type icon */}
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'w-8 h-8 rounded-lg flex items-center justify-center',
|
||||||
|
isShell ? 'bg-amber-500/10 text-amber-500' : 'bg-blue-500/10 text-blue-500'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{isShell ? <Terminal className="w-4 h-4" /> : <Globe className="w-4 h-4" />}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Info */}
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<p className="text-sm font-medium truncate">
|
||||||
|
{hook.name || (isShell ? 'Shell Command' : 'HTTP Webhook')}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-muted-foreground truncate">
|
||||||
|
{isShell
|
||||||
|
? (hook.action as { type: 'shell'; command: string }).command
|
||||||
|
: (hook.action as { type: 'http'; url: string }).url}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Actions */}
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Switch checked={hook.enabled} onCheckedChange={onToggle} />
|
||||||
|
<Button variant="ghost" size="icon" className="h-8 w-8" onClick={onEdit}>
|
||||||
|
<Pencil className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-8 w-8 text-destructive hover:text-destructive"
|
||||||
|
onClick={onDelete}
|
||||||
|
>
|
||||||
|
<Trash2 className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { EventHooksSection } from './event-hooks-section';
|
||||||
@@ -15,10 +15,12 @@ export type SettingsViewId =
|
|||||||
| 'terminal'
|
| 'terminal'
|
||||||
| 'keyboard'
|
| 'keyboard'
|
||||||
| 'audio'
|
| 'audio'
|
||||||
|
| 'event-hooks'
|
||||||
| 'defaults'
|
| 'defaults'
|
||||||
| 'worktrees'
|
| 'worktrees'
|
||||||
| 'account'
|
| 'account'
|
||||||
| 'security'
|
| 'security'
|
||||||
|
| 'developer'
|
||||||
| 'danger';
|
| 'danger';
|
||||||
|
|
||||||
interface UseSettingsViewOptions {
|
interface UseSettingsViewOptions {
|
||||||
|
|||||||
@@ -567,6 +567,8 @@ export function hydrateStoreFromSettings(settings: GlobalSettings): void {
|
|||||||
defaultRequirePlanApproval: settings.defaultRequirePlanApproval ?? false,
|
defaultRequirePlanApproval: settings.defaultRequirePlanApproval ?? false,
|
||||||
defaultFeatureModel: settings.defaultFeatureModel ?? { model: 'opus' },
|
defaultFeatureModel: settings.defaultFeatureModel ?? { model: 'opus' },
|
||||||
muteDoneSound: settings.muteDoneSound ?? false,
|
muteDoneSound: settings.muteDoneSound ?? false,
|
||||||
|
serverLogLevel: settings.serverLogLevel ?? 'info',
|
||||||
|
enableRequestLogging: settings.enableRequestLogging ?? true,
|
||||||
enhancementModel: settings.enhancementModel ?? 'sonnet',
|
enhancementModel: settings.enhancementModel ?? 'sonnet',
|
||||||
validationModel: settings.validationModel ?? 'opus',
|
validationModel: settings.validationModel ?? 'opus',
|
||||||
phaseModels: settings.phaseModels ?? current.phaseModels,
|
phaseModels: settings.phaseModels ?? current.phaseModels,
|
||||||
@@ -626,6 +628,8 @@ function buildSettingsUpdateFromStore(): Record<string, unknown> {
|
|||||||
defaultPlanningMode: state.defaultPlanningMode,
|
defaultPlanningMode: state.defaultPlanningMode,
|
||||||
defaultRequirePlanApproval: state.defaultRequirePlanApproval,
|
defaultRequirePlanApproval: state.defaultRequirePlanApproval,
|
||||||
muteDoneSound: state.muteDoneSound,
|
muteDoneSound: state.muteDoneSound,
|
||||||
|
serverLogLevel: state.serverLogLevel,
|
||||||
|
enableRequestLogging: state.enableRequestLogging,
|
||||||
enhancementModel: state.enhancementModel,
|
enhancementModel: state.enhancementModel,
|
||||||
validationModel: state.validationModel,
|
validationModel: state.validationModel,
|
||||||
phaseModels: state.phaseModels,
|
phaseModels: state.phaseModels,
|
||||||
|
|||||||
@@ -44,6 +44,8 @@ const SETTINGS_FIELDS_TO_SYNC = [
|
|||||||
'defaultRequirePlanApproval',
|
'defaultRequirePlanApproval',
|
||||||
'defaultFeatureModel',
|
'defaultFeatureModel',
|
||||||
'muteDoneSound',
|
'muteDoneSound',
|
||||||
|
'serverLogLevel',
|
||||||
|
'enableRequestLogging',
|
||||||
'enhancementModel',
|
'enhancementModel',
|
||||||
'validationModel',
|
'validationModel',
|
||||||
'phaseModels',
|
'phaseModels',
|
||||||
@@ -58,6 +60,7 @@ const SETTINGS_FIELDS_TO_SYNC = [
|
|||||||
'mcpServers',
|
'mcpServers',
|
||||||
'defaultEditorCommand',
|
'defaultEditorCommand',
|
||||||
'promptCustomization',
|
'promptCustomization',
|
||||||
|
'eventHooks',
|
||||||
'projects',
|
'projects',
|
||||||
'trashedProjects',
|
'trashedProjects',
|
||||||
'currentProjectId', // ID of currently open project
|
'currentProjectId', // ID of currently open project
|
||||||
@@ -470,6 +473,8 @@ export async function refreshSettingsFromServer(): Promise<boolean> {
|
|||||||
defaultRequirePlanApproval: serverSettings.defaultRequirePlanApproval,
|
defaultRequirePlanApproval: serverSettings.defaultRequirePlanApproval,
|
||||||
defaultFeatureModel: serverSettings.defaultFeatureModel ?? { model: 'opus' },
|
defaultFeatureModel: serverSettings.defaultFeatureModel ?? { model: 'opus' },
|
||||||
muteDoneSound: serverSettings.muteDoneSound,
|
muteDoneSound: serverSettings.muteDoneSound,
|
||||||
|
serverLogLevel: serverSettings.serverLogLevel ?? 'info',
|
||||||
|
enableRequestLogging: serverSettings.enableRequestLogging ?? true,
|
||||||
enhancementModel: serverSettings.enhancementModel,
|
enhancementModel: serverSettings.enhancementModel,
|
||||||
validationModel: serverSettings.validationModel,
|
validationModel: serverSettings.validationModel,
|
||||||
phaseModels: serverSettings.phaseModels,
|
phaseModels: serverSettings.phaseModels,
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ import type {
|
|||||||
PipelineStep,
|
PipelineStep,
|
||||||
PromptCustomization,
|
PromptCustomization,
|
||||||
ModelDefinition,
|
ModelDefinition,
|
||||||
|
ServerLogLevel,
|
||||||
|
EventHook,
|
||||||
} from '@automaker/types';
|
} from '@automaker/types';
|
||||||
import {
|
import {
|
||||||
getAllCursorModelIds,
|
getAllCursorModelIds,
|
||||||
@@ -43,6 +45,7 @@ export type {
|
|||||||
PlanningMode,
|
PlanningMode,
|
||||||
ThinkingLevel,
|
ThinkingLevel,
|
||||||
ModelProvider,
|
ModelProvider,
|
||||||
|
ServerLogLevel,
|
||||||
FeatureTextFilePath,
|
FeatureTextFilePath,
|
||||||
FeatureImagePath,
|
FeatureImagePath,
|
||||||
};
|
};
|
||||||
@@ -564,6 +567,10 @@ export interface AppState {
|
|||||||
// Audio Settings
|
// Audio Settings
|
||||||
muteDoneSound: boolean; // When true, mute the notification sound when agents complete (default: false)
|
muteDoneSound: boolean; // When true, mute the notification sound when agents complete (default: false)
|
||||||
|
|
||||||
|
// Server Log Level Settings
|
||||||
|
serverLogLevel: ServerLogLevel; // Log level for the API server (error, warn, info, debug)
|
||||||
|
enableRequestLogging: boolean; // Enable HTTP request logging (Morgan)
|
||||||
|
|
||||||
// Enhancement Model Settings
|
// Enhancement Model Settings
|
||||||
enhancementModel: ModelAlias; // Model used for feature enhancement (default: sonnet)
|
enhancementModel: ModelAlias; // Model used for feature enhancement (default: sonnet)
|
||||||
|
|
||||||
@@ -630,6 +637,9 @@ export interface AppState {
|
|||||||
// Prompt Customization
|
// Prompt Customization
|
||||||
promptCustomization: PromptCustomization; // Custom prompts for Auto Mode, Agent, Backlog Plan, Enhancement
|
promptCustomization: PromptCustomization; // Custom prompts for Auto Mode, Agent, Backlog Plan, Enhancement
|
||||||
|
|
||||||
|
// Event Hooks
|
||||||
|
eventHooks: EventHook[]; // Event hooks for custom commands or webhooks
|
||||||
|
|
||||||
// Project Analysis
|
// Project Analysis
|
||||||
projectAnalysis: ProjectAnalysis | null;
|
projectAnalysis: ProjectAnalysis | null;
|
||||||
isAnalyzing: boolean;
|
isAnalyzing: boolean;
|
||||||
@@ -982,6 +992,10 @@ export interface AppActions {
|
|||||||
// Audio Settings actions
|
// Audio Settings actions
|
||||||
setMuteDoneSound: (muted: boolean) => void;
|
setMuteDoneSound: (muted: boolean) => void;
|
||||||
|
|
||||||
|
// Server Log Level actions
|
||||||
|
setServerLogLevel: (level: ServerLogLevel) => void;
|
||||||
|
setEnableRequestLogging: (enabled: boolean) => void;
|
||||||
|
|
||||||
// Enhancement Model actions
|
// Enhancement Model actions
|
||||||
setEnhancementModel: (model: ModelAlias) => void;
|
setEnhancementModel: (model: ModelAlias) => void;
|
||||||
|
|
||||||
@@ -1039,6 +1053,9 @@ export interface AppActions {
|
|||||||
// Prompt Customization actions
|
// Prompt Customization actions
|
||||||
setPromptCustomization: (customization: PromptCustomization) => Promise<void>;
|
setPromptCustomization: (customization: PromptCustomization) => Promise<void>;
|
||||||
|
|
||||||
|
// Event Hook actions
|
||||||
|
setEventHooks: (hooks: EventHook[]) => void;
|
||||||
|
|
||||||
// MCP Server actions
|
// MCP Server actions
|
||||||
addMCPServer: (server: Omit<MCPServerConfig, 'id'>) => void;
|
addMCPServer: (server: Omit<MCPServerConfig, 'id'>) => void;
|
||||||
updateMCPServer: (id: string, updates: Partial<MCPServerConfig>) => void;
|
updateMCPServer: (id: string, updates: Partial<MCPServerConfig>) => void;
|
||||||
@@ -1250,6 +1267,8 @@ const initialState: AppState = {
|
|||||||
worktreesByProject: {},
|
worktreesByProject: {},
|
||||||
keyboardShortcuts: DEFAULT_KEYBOARD_SHORTCUTS, // Default keyboard shortcuts
|
keyboardShortcuts: DEFAULT_KEYBOARD_SHORTCUTS, // Default keyboard shortcuts
|
||||||
muteDoneSound: false, // Default to sound enabled (not muted)
|
muteDoneSound: false, // Default to sound enabled (not muted)
|
||||||
|
serverLogLevel: 'info', // Default to info level for server logs
|
||||||
|
enableRequestLogging: true, // Default to enabled for HTTP request logging
|
||||||
enhancementModel: 'sonnet', // Default to sonnet for feature enhancement
|
enhancementModel: 'sonnet', // Default to sonnet for feature enhancement
|
||||||
validationModel: 'opus', // Default to opus for GitHub issue validation
|
validationModel: 'opus', // Default to opus for GitHub issue validation
|
||||||
phaseModels: DEFAULT_PHASE_MODELS, // Phase-specific model configuration
|
phaseModels: DEFAULT_PHASE_MODELS, // Phase-specific model configuration
|
||||||
@@ -1282,6 +1301,7 @@ const initialState: AppState = {
|
|||||||
enableSubagents: true, // Subagents enabled by default
|
enableSubagents: true, // Subagents enabled by default
|
||||||
subagentsSources: ['user', 'project'] as Array<'user' | 'project'>, // Load from both sources by default
|
subagentsSources: ['user', 'project'] as Array<'user' | 'project'>, // Load from both sources by default
|
||||||
promptCustomization: {}, // Empty by default - all prompts use built-in defaults
|
promptCustomization: {}, // Empty by default - all prompts use built-in defaults
|
||||||
|
eventHooks: [], // No event hooks configured by default
|
||||||
projectAnalysis: null,
|
projectAnalysis: null,
|
||||||
isAnalyzing: false,
|
isAnalyzing: false,
|
||||||
boardBackgroundByProject: {},
|
boardBackgroundByProject: {},
|
||||||
@@ -2036,6 +2056,10 @@ export const useAppStore = create<AppState & AppActions>()((set, get) => ({
|
|||||||
// Audio Settings actions
|
// Audio Settings actions
|
||||||
setMuteDoneSound: (muted) => set({ muteDoneSound: muted }),
|
setMuteDoneSound: (muted) => set({ muteDoneSound: muted }),
|
||||||
|
|
||||||
|
// Server Log Level actions
|
||||||
|
setServerLogLevel: (level) => set({ serverLogLevel: level }),
|
||||||
|
setEnableRequestLogging: (enabled) => set({ enableRequestLogging: enabled }),
|
||||||
|
|
||||||
// Enhancement Model actions
|
// Enhancement Model actions
|
||||||
setEnhancementModel: (model) => set({ enhancementModel: model }),
|
setEnhancementModel: (model) => set({ enhancementModel: model }),
|
||||||
|
|
||||||
@@ -2207,6 +2231,9 @@ export const useAppStore = create<AppState & AppActions>()((set, get) => ({
|
|||||||
await syncSettingsToServer();
|
await syncSettingsToServer();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Event Hook actions
|
||||||
|
setEventHooks: (hooks) => set({ eventHooks: hooks }),
|
||||||
|
|
||||||
// MCP Server actions
|
// MCP Server actions
|
||||||
addMCPServer: (server) => {
|
addMCPServer: (server) => {
|
||||||
const id = `mcp-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
const id = `mcp-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||||
|
|||||||
@@ -128,6 +128,7 @@ export type {
|
|||||||
ThemeMode,
|
ThemeMode,
|
||||||
PlanningMode,
|
PlanningMode,
|
||||||
ThinkingLevel,
|
ThinkingLevel,
|
||||||
|
ServerLogLevel,
|
||||||
ModelProvider,
|
ModelProvider,
|
||||||
PhaseModelEntry,
|
PhaseModelEntry,
|
||||||
PhaseModelConfig,
|
PhaseModelConfig,
|
||||||
@@ -143,6 +144,13 @@ export type {
|
|||||||
BoardBackgroundSettings,
|
BoardBackgroundSettings,
|
||||||
WorktreeInfo,
|
WorktreeInfo,
|
||||||
ProjectSettings,
|
ProjectSettings,
|
||||||
|
// Event hook types
|
||||||
|
EventHookTrigger,
|
||||||
|
EventHookHttpMethod,
|
||||||
|
EventHookShellAction,
|
||||||
|
EventHookHttpAction,
|
||||||
|
EventHookAction,
|
||||||
|
EventHook,
|
||||||
} from './settings.js';
|
} from './settings.js';
|
||||||
export {
|
export {
|
||||||
DEFAULT_KEYBOARD_SHORTCUTS,
|
DEFAULT_KEYBOARD_SHORTCUTS,
|
||||||
@@ -155,6 +163,8 @@ export {
|
|||||||
PROJECT_SETTINGS_VERSION,
|
PROJECT_SETTINGS_VERSION,
|
||||||
THINKING_TOKEN_BUDGET,
|
THINKING_TOKEN_BUDGET,
|
||||||
getThinkingTokenBudget,
|
getThinkingTokenBudget,
|
||||||
|
// Event hook constants
|
||||||
|
EVENT_HOOK_TRIGGER_LABELS,
|
||||||
} from './settings.js';
|
} from './settings.js';
|
||||||
|
|
||||||
// Model display constants
|
// Model display constants
|
||||||
|
|||||||
@@ -68,6 +68,9 @@ export type ThemeMode =
|
|||||||
/** PlanningMode - Planning levels for feature generation workflows */
|
/** PlanningMode - Planning levels for feature generation workflows */
|
||||||
export type PlanningMode = 'skip' | 'lite' | 'spec' | 'full';
|
export type PlanningMode = 'skip' | 'lite' | 'spec' | 'full';
|
||||||
|
|
||||||
|
/** ServerLogLevel - Log verbosity level for the API server */
|
||||||
|
export type ServerLogLevel = 'error' | 'warn' | 'info' | 'debug';
|
||||||
|
|
||||||
/** ThinkingLevel - Extended thinking levels for Claude models (reasoning intensity) */
|
/** ThinkingLevel - Extended thinking levels for Claude models (reasoning intensity) */
|
||||||
export type ThinkingLevel = 'none' | 'low' | 'medium' | 'high' | 'ultrathink';
|
export type ThinkingLevel = 'none' | 'low' | 'medium' | 'high' | 'ultrathink';
|
||||||
|
|
||||||
@@ -98,6 +101,97 @@ export function getThinkingTokenBudget(level: ThinkingLevel | undefined): number
|
|||||||
/** ModelProvider - AI model provider for credentials and API key management */
|
/** ModelProvider - AI model provider for credentials and API key management */
|
||||||
export type ModelProvider = 'claude' | 'cursor' | 'codex' | 'opencode';
|
export type ModelProvider = 'claude' | 'cursor' | 'codex' | 'opencode';
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Event Hooks - Custom actions triggered by system events
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* EventHookTrigger - Event types that can trigger custom hooks
|
||||||
|
*
|
||||||
|
* - feature_success: Feature completed successfully
|
||||||
|
* - feature_error: Feature failed with an error
|
||||||
|
* - auto_mode_complete: Auto mode finished processing all features
|
||||||
|
* - auto_mode_error: Auto mode encountered a critical error and paused
|
||||||
|
*/
|
||||||
|
export type EventHookTrigger =
|
||||||
|
| 'feature_success'
|
||||||
|
| 'feature_error'
|
||||||
|
| 'auto_mode_complete'
|
||||||
|
| 'auto_mode_error';
|
||||||
|
|
||||||
|
/** HTTP methods supported for webhook requests */
|
||||||
|
export type EventHookHttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* EventHookShellAction - Configuration for executing a shell command
|
||||||
|
*
|
||||||
|
* Shell commands are executed in the server's working directory.
|
||||||
|
* Supports variable substitution using {{variableName}} syntax.
|
||||||
|
*/
|
||||||
|
export interface EventHookShellAction {
|
||||||
|
type: 'shell';
|
||||||
|
/** Shell command to execute. Supports {{variable}} substitution. */
|
||||||
|
command: string;
|
||||||
|
/** Timeout in milliseconds (default: 30000) */
|
||||||
|
timeout?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* EventHookHttpAction - Configuration for making an HTTP webhook request
|
||||||
|
*
|
||||||
|
* Supports variable substitution in URL, headers, and body.
|
||||||
|
*/
|
||||||
|
export interface EventHookHttpAction {
|
||||||
|
type: 'http';
|
||||||
|
/** URL to send the request to. Supports {{variable}} substitution. */
|
||||||
|
url: string;
|
||||||
|
/** HTTP method to use */
|
||||||
|
method: EventHookHttpMethod;
|
||||||
|
/** Optional headers to include. Values support {{variable}} substitution. */
|
||||||
|
headers?: Record<string, string>;
|
||||||
|
/** Optional request body (JSON string). Supports {{variable}} substitution. */
|
||||||
|
body?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Union type for all hook action configurations */
|
||||||
|
export type EventHookAction = EventHookShellAction | EventHookHttpAction;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* EventHook - Configuration for a single event hook
|
||||||
|
*
|
||||||
|
* Event hooks allow users to execute custom shell commands or HTTP requests
|
||||||
|
* when specific events occur in the system.
|
||||||
|
*
|
||||||
|
* Available variables for substitution:
|
||||||
|
* - {{featureId}} - ID of the feature (if applicable)
|
||||||
|
* - {{featureName}} - Name of the feature (if applicable)
|
||||||
|
* - {{projectPath}} - Absolute path to the project
|
||||||
|
* - {{projectName}} - Name of the project
|
||||||
|
* - {{error}} - Error message (for error events)
|
||||||
|
* - {{timestamp}} - ISO timestamp of the event
|
||||||
|
* - {{eventType}} - The event type that triggered the hook
|
||||||
|
*/
|
||||||
|
export interface EventHook {
|
||||||
|
/** Unique identifier for this hook */
|
||||||
|
id: string;
|
||||||
|
/** Which event type triggers this hook */
|
||||||
|
trigger: EventHookTrigger;
|
||||||
|
/** Whether this hook is currently enabled */
|
||||||
|
enabled: boolean;
|
||||||
|
/** The action to execute when triggered */
|
||||||
|
action: EventHookAction;
|
||||||
|
/** Optional friendly name for display */
|
||||||
|
name?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Human-readable labels for event hook triggers */
|
||||||
|
export const EVENT_HOOK_TRIGGER_LABELS: Record<EventHookTrigger, string> = {
|
||||||
|
feature_success: 'Feature completed successfully',
|
||||||
|
feature_error: 'Feature failed with error',
|
||||||
|
auto_mode_complete: 'Auto mode completed all features',
|
||||||
|
auto_mode_error: 'Auto mode paused due to error',
|
||||||
|
};
|
||||||
|
|
||||||
const DEFAULT_CODEX_AUTO_LOAD_AGENTS = false;
|
const DEFAULT_CODEX_AUTO_LOAD_AGENTS = false;
|
||||||
const DEFAULT_CODEX_SANDBOX_MODE: CodexSandboxMode = 'workspace-write';
|
const DEFAULT_CODEX_SANDBOX_MODE: CodexSandboxMode = 'workspace-write';
|
||||||
const DEFAULT_CODEX_APPROVAL_POLICY: CodexApprovalPolicy = 'on-request';
|
const DEFAULT_CODEX_APPROVAL_POLICY: CodexApprovalPolicy = 'on-request';
|
||||||
@@ -390,6 +484,12 @@ export interface GlobalSettings {
|
|||||||
/** Mute completion notification sound */
|
/** Mute completion notification sound */
|
||||||
muteDoneSound: boolean;
|
muteDoneSound: boolean;
|
||||||
|
|
||||||
|
// Server Logging Preferences
|
||||||
|
/** Log level for the API server (error, warn, info, debug). Default: info */
|
||||||
|
serverLogLevel?: ServerLogLevel;
|
||||||
|
/** Enable HTTP request logging (Morgan). Default: true */
|
||||||
|
enableRequestLogging?: boolean;
|
||||||
|
|
||||||
// AI Commit Message Generation
|
// AI Commit Message Generation
|
||||||
/** Enable AI-generated commit messages when opening commit dialog (default: true) */
|
/** Enable AI-generated commit messages when opening commit dialog (default: true) */
|
||||||
enableAiCommitMessages: boolean;
|
enableAiCommitMessages: boolean;
|
||||||
@@ -524,6 +624,13 @@ export interface GlobalSettings {
|
|||||||
* Value: agent configuration
|
* Value: agent configuration
|
||||||
*/
|
*/
|
||||||
customSubagents?: Record<string, import('./provider.js').AgentDefinition>;
|
customSubagents?: Record<string, import('./provider.js').AgentDefinition>;
|
||||||
|
|
||||||
|
// Event Hooks Configuration
|
||||||
|
/**
|
||||||
|
* Event hooks for executing custom commands or HTTP requests on events
|
||||||
|
* @see EventHook for configuration details
|
||||||
|
*/
|
||||||
|
eventHooks?: EventHook[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -725,6 +832,8 @@ export const DEFAULT_GLOBAL_SETTINGS: GlobalSettings = {
|
|||||||
defaultRequirePlanApproval: false,
|
defaultRequirePlanApproval: false,
|
||||||
defaultFeatureModel: { model: 'opus' },
|
defaultFeatureModel: { model: 'opus' },
|
||||||
muteDoneSound: false,
|
muteDoneSound: false,
|
||||||
|
serverLogLevel: 'info',
|
||||||
|
enableRequestLogging: true,
|
||||||
enableAiCommitMessages: true,
|
enableAiCommitMessages: true,
|
||||||
phaseModels: DEFAULT_PHASE_MODELS,
|
phaseModels: DEFAULT_PHASE_MODELS,
|
||||||
enhancementModel: 'sonnet',
|
enhancementModel: 'sonnet',
|
||||||
|
|||||||
Reference in New Issue
Block a user