feat(extension): complete VS Code extension with kanban board interface (#997)

---------
Co-authored-by: DavidMaliglowka <13022280+DavidMaliglowka@users.noreply.github.com>
Co-authored-by: Ralph Khreish <35776126+Crunchyman-ralph@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
This commit is contained in:
DavidMaliglowka
2025-08-01 07:04:22 -05:00
committed by GitHub
parent 60c03c548d
commit 64302dc191
101 changed files with 20608 additions and 181 deletions

View File

@@ -0,0 +1,514 @@
import * as vscode from 'vscode';
import { logger } from './logger';
import type { MCPConfig } from './mcpClient';
export interface TaskMasterConfig {
mcp: MCPServerConfig;
ui: UIConfig;
performance: PerformanceConfig;
debug: DebugConfig;
}
export interface MCPServerConfig {
command: string;
args: string[];
cwd?: string;
env?: Record<string, string>;
timeout: number;
maxReconnectAttempts: number;
reconnectBackoffMs: number;
maxBackoffMs: number;
healthCheckIntervalMs: number;
}
export interface UIConfig {
autoRefresh: boolean;
refreshIntervalMs: number;
theme: 'auto' | 'light' | 'dark';
showCompletedTasks: boolean;
taskDisplayLimit: number;
showPriority: boolean;
showTaskIds: boolean;
}
export interface PerformanceConfig {
maxConcurrentRequests: number;
requestTimeoutMs: number;
cacheTasksMs: number;
lazyLoadThreshold: number;
}
export interface DebugConfig {
enableLogging: boolean;
logLevel: 'error' | 'warn' | 'info' | 'debug';
enableConnectionMetrics: boolean;
saveEventLogs: boolean;
maxEventLogSize: number;
}
export interface ConfigValidationResult {
isValid: boolean;
errors: string[];
warnings: string[];
}
export class ConfigManager {
private static instance: ConfigManager | null = null;
private config: TaskMasterConfig;
private configListeners: ((config: TaskMasterConfig) => void)[] = [];
private constructor() {
this.config = this.loadConfig();
this.setupConfigWatcher();
}
/**
* Get singleton instance
*/
static getInstance(): ConfigManager {
if (!ConfigManager.instance) {
ConfigManager.instance = new ConfigManager();
}
return ConfigManager.instance;
}
/**
* Get current configuration
*/
getConfig(): TaskMasterConfig {
return { ...this.config };
}
/**
* Get MCP configuration for the client
*/
getMCPConfig(): MCPConfig {
const mcpConfig = this.config.mcp;
return {
command: mcpConfig.command,
args: mcpConfig.args,
cwd: mcpConfig.cwd,
env: mcpConfig.env
};
}
/**
* Update configuration (programmatically)
*/
async updateConfig(updates: Partial<TaskMasterConfig>): Promise<void> {
const newConfig = this.mergeConfig(this.config, updates);
const validation = this.validateConfig(newConfig);
if (!validation.isValid) {
throw new Error(
`Configuration validation failed: ${validation.errors.join(', ')}`
);
}
// Update VS Code settings
const vsConfig = vscode.workspace.getConfiguration('taskmaster');
if (updates.mcp) {
if (updates.mcp.command !== undefined) {
await vsConfig.update(
'mcp.command',
updates.mcp.command,
vscode.ConfigurationTarget.Workspace
);
}
if (updates.mcp.args !== undefined) {
await vsConfig.update(
'mcp.args',
updates.mcp.args,
vscode.ConfigurationTarget.Workspace
);
}
if (updates.mcp.cwd !== undefined) {
await vsConfig.update(
'mcp.cwd',
updates.mcp.cwd,
vscode.ConfigurationTarget.Workspace
);
}
if (updates.mcp.timeout !== undefined) {
await vsConfig.update(
'mcp.timeout',
updates.mcp.timeout,
vscode.ConfigurationTarget.Workspace
);
}
}
if (updates.ui) {
if (updates.ui.autoRefresh !== undefined) {
await vsConfig.update(
'ui.autoRefresh',
updates.ui.autoRefresh,
vscode.ConfigurationTarget.Workspace
);
}
if (updates.ui.theme !== undefined) {
await vsConfig.update(
'ui.theme',
updates.ui.theme,
vscode.ConfigurationTarget.Workspace
);
}
}
if (updates.debug) {
if (updates.debug.enableLogging !== undefined) {
await vsConfig.update(
'debug.enableLogging',
updates.debug.enableLogging,
vscode.ConfigurationTarget.Workspace
);
}
if (updates.debug.logLevel !== undefined) {
await vsConfig.update(
'debug.logLevel',
updates.debug.logLevel,
vscode.ConfigurationTarget.Workspace
);
}
}
this.config = newConfig;
this.notifyConfigChange();
}
/**
* Validate configuration
*/
validateConfig(config: TaskMasterConfig): ConfigValidationResult {
const errors: string[] = [];
const warnings: string[] = [];
// Validate MCP configuration
if (!config.mcp.command || config.mcp.command.trim() === '') {
errors.push('MCP command cannot be empty');
}
if (config.mcp.timeout < 1000) {
warnings.push(
'MCP timeout is very low (< 1s), this may cause connection issues'
);
} else if (config.mcp.timeout > 60000) {
warnings.push(
'MCP timeout is very high (> 60s), this may cause slow responses'
);
}
if (config.mcp.maxReconnectAttempts < 1) {
errors.push('Max reconnect attempts must be at least 1');
} else if (config.mcp.maxReconnectAttempts > 10) {
warnings.push(
'Max reconnect attempts is very high, this may cause long delays'
);
}
// Validate UI configuration
if (config.ui.refreshIntervalMs < 1000) {
warnings.push(
'UI refresh interval is very low (< 1s), this may impact performance'
);
}
if (config.ui.taskDisplayLimit < 1) {
errors.push('Task display limit must be at least 1');
} else if (config.ui.taskDisplayLimit > 1000) {
warnings.push(
'Task display limit is very high, this may impact performance'
);
}
// Validate performance configuration
if (config.performance.maxConcurrentRequests < 1) {
errors.push('Max concurrent requests must be at least 1');
} else if (config.performance.maxConcurrentRequests > 20) {
warnings.push(
'Max concurrent requests is very high, this may overwhelm the server'
);
}
if (config.performance.requestTimeoutMs < 1000) {
warnings.push(
'Request timeout is very low (< 1s), this may cause premature timeouts'
);
}
// Validate debug configuration
if (config.debug.maxEventLogSize < 10) {
errors.push('Max event log size must be at least 10');
} else if (config.debug.maxEventLogSize > 10000) {
warnings.push(
'Max event log size is very high, this may consume significant memory'
);
}
return {
isValid: errors.length === 0,
errors,
warnings
};
}
/**
* Reset configuration to defaults
*/
async resetToDefaults(): Promise<void> {
const defaultConfig = this.getDefaultConfig();
await this.updateConfig(defaultConfig);
}
/**
* Export configuration to JSON
*/
exportConfig(): string {
return JSON.stringify(this.config, null, 2);
}
/**
* Import configuration from JSON
*/
async importConfig(jsonConfig: string): Promise<void> {
try {
const importedConfig = JSON.parse(jsonConfig) as TaskMasterConfig;
const validation = this.validateConfig(importedConfig);
if (!validation.isValid) {
throw new Error(
`Invalid configuration: ${validation.errors.join(', ')}`
);
}
if (validation.warnings.length > 0) {
const proceed = await vscode.window.showWarningMessage(
`Configuration has warnings: ${validation.warnings.join(', ')}. Import anyway?`,
'Yes',
'No'
);
if (proceed !== 'Yes') {
return;
}
}
await this.updateConfig(importedConfig);
vscode.window.showInformationMessage(
'Configuration imported successfully'
);
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : 'Unknown error';
vscode.window.showErrorMessage(
`Failed to import configuration: ${errorMessage}`
);
throw error;
}
}
/**
* Add configuration change listener
*/
onConfigChange(listener: (config: TaskMasterConfig) => void): void {
this.configListeners.push(listener);
}
/**
* Remove configuration change listener
*/
removeConfigListener(listener: (config: TaskMasterConfig) => void): void {
const index = this.configListeners.indexOf(listener);
if (index !== -1) {
this.configListeners.splice(index, 1);
}
}
/**
* Load configuration from VS Code settings
*/
private loadConfig(): TaskMasterConfig {
const vsConfig = vscode.workspace.getConfiguration('taskmaster');
const defaultConfig = this.getDefaultConfig();
return {
mcp: {
command: vsConfig.get('mcp.command', defaultConfig.mcp.command),
args: vsConfig.get('mcp.args', defaultConfig.mcp.args),
cwd: vsConfig.get('mcp.cwd', defaultConfig.mcp.cwd),
env: vsConfig.get('mcp.env', defaultConfig.mcp.env),
timeout: vsConfig.get('mcp.timeout', defaultConfig.mcp.timeout),
maxReconnectAttempts: vsConfig.get(
'mcp.maxReconnectAttempts',
defaultConfig.mcp.maxReconnectAttempts
),
reconnectBackoffMs: vsConfig.get(
'mcp.reconnectBackoffMs',
defaultConfig.mcp.reconnectBackoffMs
),
maxBackoffMs: vsConfig.get(
'mcp.maxBackoffMs',
defaultConfig.mcp.maxBackoffMs
),
healthCheckIntervalMs: vsConfig.get(
'mcp.healthCheckIntervalMs',
defaultConfig.mcp.healthCheckIntervalMs
)
},
ui: {
autoRefresh: vsConfig.get(
'ui.autoRefresh',
defaultConfig.ui.autoRefresh
),
refreshIntervalMs: vsConfig.get(
'ui.refreshIntervalMs',
defaultConfig.ui.refreshIntervalMs
),
theme: vsConfig.get('ui.theme', defaultConfig.ui.theme),
showCompletedTasks: vsConfig.get(
'ui.showCompletedTasks',
defaultConfig.ui.showCompletedTasks
),
taskDisplayLimit: vsConfig.get(
'ui.taskDisplayLimit',
defaultConfig.ui.taskDisplayLimit
),
showPriority: vsConfig.get(
'ui.showPriority',
defaultConfig.ui.showPriority
),
showTaskIds: vsConfig.get(
'ui.showTaskIds',
defaultConfig.ui.showTaskIds
)
},
performance: {
maxConcurrentRequests: vsConfig.get(
'performance.maxConcurrentRequests',
defaultConfig.performance.maxConcurrentRequests
),
requestTimeoutMs: vsConfig.get(
'performance.requestTimeoutMs',
defaultConfig.performance.requestTimeoutMs
),
cacheTasksMs: vsConfig.get(
'performance.cacheTasksMs',
defaultConfig.performance.cacheTasksMs
),
lazyLoadThreshold: vsConfig.get(
'performance.lazyLoadThreshold',
defaultConfig.performance.lazyLoadThreshold
)
},
debug: {
enableLogging: vsConfig.get(
'debug.enableLogging',
defaultConfig.debug.enableLogging
),
logLevel: vsConfig.get('debug.logLevel', defaultConfig.debug.logLevel),
enableConnectionMetrics: vsConfig.get(
'debug.enableConnectionMetrics',
defaultConfig.debug.enableConnectionMetrics
),
saveEventLogs: vsConfig.get(
'debug.saveEventLogs',
defaultConfig.debug.saveEventLogs
),
maxEventLogSize: vsConfig.get(
'debug.maxEventLogSize',
defaultConfig.debug.maxEventLogSize
)
}
};
}
/**
* Get default configuration
*/
private getDefaultConfig(): TaskMasterConfig {
return {
mcp: {
command: 'npx',
args: ['task-master-ai'],
cwd: vscode.workspace.rootPath || '',
env: undefined,
timeout: 30000,
maxReconnectAttempts: 5,
reconnectBackoffMs: 1000,
maxBackoffMs: 30000,
healthCheckIntervalMs: 15000
},
ui: {
autoRefresh: true,
refreshIntervalMs: 10000,
theme: 'auto',
showCompletedTasks: true,
taskDisplayLimit: 100,
showPriority: true,
showTaskIds: true
},
performance: {
maxConcurrentRequests: 5,
requestTimeoutMs: 30000,
cacheTasksMs: 5000,
lazyLoadThreshold: 50
},
debug: {
enableLogging: true,
logLevel: 'info',
enableConnectionMetrics: true,
saveEventLogs: false,
maxEventLogSize: 1000
}
};
}
/**
* Setup configuration watcher
*/
private setupConfigWatcher(): void {
vscode.workspace.onDidChangeConfiguration((event) => {
if (event.affectsConfiguration('taskmaster')) {
logger.log('Task Master configuration changed, reloading...');
this.config = this.loadConfig();
this.notifyConfigChange();
}
});
}
/**
* Merge configurations
*/
private mergeConfig(
baseConfig: TaskMasterConfig,
updates: Partial<TaskMasterConfig>
): TaskMasterConfig {
return {
mcp: { ...baseConfig.mcp, ...updates.mcp },
ui: { ...baseConfig.ui, ...updates.ui },
performance: { ...baseConfig.performance, ...updates.performance },
debug: { ...baseConfig.debug, ...updates.debug }
};
}
/**
* Notify configuration change listeners
*/
private notifyConfigChange(): void {
this.configListeners.forEach((listener) => {
try {
listener(this.config);
} catch (error) {
logger.error('Error in configuration change listener:', error);
}
});
}
}
/**
* Utility function to get configuration manager instance
*/
export function getConfigManager(): ConfigManager {
return ConfigManager.getInstance();
}

View File

@@ -0,0 +1,387 @@
import * as vscode from 'vscode';
import { logger } from './logger';
import {
MCPClientManager,
type MCPConfig,
type MCPServerStatus
} from './mcpClient';
export interface ConnectionEvent {
type: 'connected' | 'disconnected' | 'error' | 'reconnecting';
timestamp: Date;
data?: any;
}
export interface ConnectionHealth {
isHealthy: boolean;
lastSuccessfulCall?: Date;
consecutiveFailures: number;
averageResponseTime: number;
uptime: number;
}
export class ConnectionManager {
private mcpClient: MCPClientManager | null = null;
private config: MCPConfig;
private connectionEvents: ConnectionEvent[] = [];
private health: ConnectionHealth = {
isHealthy: false,
consecutiveFailures: 0,
averageResponseTime: 0,
uptime: 0
};
private startTime: Date | null = null;
private healthCheckInterval: NodeJS.Timeout | null = null;
private reconnectAttempts = 0;
private maxReconnectAttempts = 5;
private reconnectBackoffMs = 1000; // Start with 1 second
private maxBackoffMs = 30000; // Max 30 seconds
private isReconnecting = false;
// Event handlers
private onConnectionChange?: (
status: MCPServerStatus,
health: ConnectionHealth
) => void;
private onConnectionEvent?: (event: ConnectionEvent) => void;
constructor(config: MCPConfig) {
this.config = config;
this.mcpClient = new MCPClientManager(config);
}
/**
* Set event handlers
*/
setEventHandlers(handlers: {
onConnectionChange?: (
status: MCPServerStatus,
health: ConnectionHealth
) => void;
onConnectionEvent?: (event: ConnectionEvent) => void;
}) {
this.onConnectionChange = handlers.onConnectionChange;
this.onConnectionEvent = handlers.onConnectionEvent;
}
/**
* Connect with automatic retry and health monitoring
*/
async connect(): Promise<void> {
try {
if (!this.mcpClient) {
throw new Error('MCP client not initialized');
}
this.logEvent({ type: 'reconnecting', timestamp: new Date() });
await this.mcpClient.connect();
this.reconnectAttempts = 0;
this.reconnectBackoffMs = 1000;
this.isReconnecting = false;
this.startTime = new Date();
this.updateHealth();
this.startHealthMonitoring();
this.logEvent({ type: 'connected', timestamp: new Date() });
logger.log('Connection manager: Successfully connected');
} catch (error) {
this.logEvent({
type: 'error',
timestamp: new Date(),
data: {
error: error instanceof Error ? error.message : 'Unknown error'
}
});
await this.handleConnectionFailure(error);
throw error;
}
}
/**
* Disconnect and stop health monitoring
*/
async disconnect(): Promise<void> {
this.stopHealthMonitoring();
this.isReconnecting = false;
if (this.mcpClient) {
await this.mcpClient.disconnect();
}
this.health.isHealthy = false;
this.startTime = null;
this.logEvent({ type: 'disconnected', timestamp: new Date() });
this.notifyConnectionChange();
}
/**
* Get current connection status
*/
getStatus(): MCPServerStatus {
return this.mcpClient?.getStatus() || { isRunning: false };
}
/**
* Get connection health metrics
*/
getHealth(): ConnectionHealth {
this.updateHealth();
return { ...this.health };
}
/**
* Get recent connection events
*/
getEvents(limit = 10): ConnectionEvent[] {
return this.connectionEvents.slice(-limit);
}
/**
* Test connection with performance monitoring
*/
async testConnection(): Promise<{
success: boolean;
responseTime: number;
error?: string;
}> {
if (!this.mcpClient) {
return {
success: false,
responseTime: 0,
error: 'Client not initialized'
};
}
const startTime = Date.now();
try {
const success = await this.mcpClient.testConnection();
const responseTime = Date.now() - startTime;
if (success) {
this.health.lastSuccessfulCall = new Date();
this.health.consecutiveFailures = 0;
this.updateAverageResponseTime(responseTime);
} else {
this.health.consecutiveFailures++;
}
this.updateHealth();
this.notifyConnectionChange();
return { success, responseTime };
} catch (error) {
const responseTime = Date.now() - startTime;
this.health.consecutiveFailures++;
this.updateHealth();
this.notifyConnectionChange();
return {
success: false,
responseTime,
error: error instanceof Error ? error.message : 'Unknown error'
};
}
}
/**
* Call MCP tool with automatic retry and health monitoring
*/
async callTool(
toolName: string,
arguments_: Record<string, unknown>
): Promise<any> {
if (!this.mcpClient) {
throw new Error('MCP client not initialized');
}
const startTime = Date.now();
try {
const result = await this.mcpClient.callTool(toolName, arguments_);
const responseTime = Date.now() - startTime;
this.health.lastSuccessfulCall = new Date();
this.health.consecutiveFailures = 0;
this.updateAverageResponseTime(responseTime);
this.updateHealth();
this.notifyConnectionChange();
return result;
} catch (error) {
this.health.consecutiveFailures++;
this.updateHealth();
// Attempt reconnection if connection seems lost
if (this.health.consecutiveFailures >= 3 && !this.isReconnecting) {
logger.log(
'Multiple consecutive failures detected, attempting reconnection...'
);
this.reconnectWithBackoff().catch((err) => {
logger.error('Reconnection failed:', err);
});
}
this.notifyConnectionChange();
throw error;
}
}
/**
* Update configuration and reconnect
*/
async updateConfig(newConfig: MCPConfig): Promise<void> {
this.config = newConfig;
await this.disconnect();
this.mcpClient = new MCPClientManager(newConfig);
// Attempt to reconnect with new config
try {
await this.connect();
} catch (error) {
logger.error('Failed to connect with new configuration:', error);
}
}
/**
* Start health monitoring
*/
private startHealthMonitoring(): void {
this.stopHealthMonitoring();
this.healthCheckInterval = setInterval(async () => {
try {
await this.testConnection();
} catch (error) {
logger.error('Health check failed:', error);
}
}, 15000); // Check every 15 seconds
}
/**
* Stop health monitoring
*/
private stopHealthMonitoring(): void {
if (this.healthCheckInterval) {
clearInterval(this.healthCheckInterval);
this.healthCheckInterval = null;
}
}
/**
* Handle connection failure with exponential backoff
*/
private async handleConnectionFailure(error: any): Promise<void> {
this.health.consecutiveFailures++;
this.updateHealth();
this.notifyConnectionChange();
if (
this.reconnectAttempts < this.maxReconnectAttempts &&
!this.isReconnecting
) {
await this.reconnectWithBackoff();
}
}
/**
* Reconnect with exponential backoff
*/
private async reconnectWithBackoff(): Promise<void> {
if (this.isReconnecting) {
return;
}
this.isReconnecting = true;
this.reconnectAttempts++;
const backoffMs = Math.min(
this.reconnectBackoffMs * 2 ** (this.reconnectAttempts - 1),
this.maxBackoffMs
);
logger.log(
`Attempting reconnection ${this.reconnectAttempts}/${this.maxReconnectAttempts} in ${backoffMs}ms...`
);
await new Promise((resolve) => setTimeout(resolve, backoffMs));
try {
await this.connect();
} catch (error) {
logger.error(
`Reconnection attempt ${this.reconnectAttempts} failed:`,
error
);
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
this.isReconnecting = false;
vscode.window.showErrorMessage(
`Failed to reconnect to Task Master after ${this.maxReconnectAttempts} attempts. Please check your configuration and try manually reconnecting.`
);
} else {
// Try again
await this.reconnectWithBackoff();
}
}
}
/**
* Update health metrics
*/
private updateHealth(): void {
const status = this.getStatus();
this.health.isHealthy =
status.isRunning && this.health.consecutiveFailures < 3;
if (this.startTime) {
this.health.uptime = Date.now() - this.startTime.getTime();
}
}
/**
* Update average response time
*/
private updateAverageResponseTime(responseTime: number): void {
// Simple moving average calculation
if (this.health.averageResponseTime === 0) {
this.health.averageResponseTime = responseTime;
} else {
this.health.averageResponseTime =
this.health.averageResponseTime * 0.8 + responseTime * 0.2;
}
}
/**
* Log connection event
*/
private logEvent(event: ConnectionEvent): void {
this.connectionEvents.push(event);
// Keep only last 100 events
if (this.connectionEvents.length > 100) {
this.connectionEvents = this.connectionEvents.slice(-100);
}
if (this.onConnectionEvent) {
this.onConnectionEvent(event);
}
}
/**
* Notify connection change
*/
private notifyConnectionChange(): void {
if (this.onConnectionChange) {
this.onConnectionChange(this.getStatus(), this.getHealth());
}
}
}

View File

@@ -0,0 +1,858 @@
import * as vscode from 'vscode';
import { logger } from './logger';
import {
getNotificationType,
getToastDuration,
shouldShowNotification
} from './notificationPreferences';
export enum ErrorSeverity {
LOW = 'low',
MEDIUM = 'medium',
HIGH = 'high',
CRITICAL = 'critical'
}
export enum ErrorCategory {
MCP_CONNECTION = 'mcp_connection',
CONFIGURATION = 'configuration',
TASK_LOADING = 'task_loading',
UI_RENDERING = 'ui_rendering',
VALIDATION = 'validation',
NETWORK = 'network',
INTERNAL = 'internal',
TASK_MASTER_API = 'TASK_MASTER_API',
DATA_VALIDATION = 'DATA_VALIDATION',
DATA_PARSING = 'DATA_PARSING',
TASK_DATA_CORRUPTION = 'TASK_DATA_CORRUPTION',
VSCODE_API = 'VSCODE_API',
WEBVIEW = 'WEBVIEW',
EXTENSION_HOST = 'EXTENSION_HOST',
USER_INTERACTION = 'USER_INTERACTION',
DRAG_DROP = 'DRAG_DROP',
COMPONENT_RENDER = 'COMPONENT_RENDER',
PERMISSION = 'PERMISSION',
FILE_SYSTEM = 'FILE_SYSTEM',
UNKNOWN = 'UNKNOWN'
}
export enum NotificationType {
VSCODE_INFO = 'VSCODE_INFO',
VSCODE_WARNING = 'VSCODE_WARNING',
VSCODE_ERROR = 'VSCODE_ERROR',
TOAST_SUCCESS = 'TOAST_SUCCESS',
TOAST_INFO = 'TOAST_INFO',
TOAST_WARNING = 'TOAST_WARNING',
TOAST_ERROR = 'TOAST_ERROR',
CONSOLE_ONLY = 'CONSOLE_ONLY',
SILENT = 'SILENT'
}
export interface ErrorContext {
// Core error information
category: ErrorCategory;
severity: ErrorSeverity;
message: string;
originalError?: Error | unknown;
// Contextual information
operation?: string; // What operation was being performed
taskId?: string; // Related task ID if applicable
userId?: string; // User context if applicable
sessionId?: string; // Session context
// Technical details
stackTrace?: string;
userAgent?: string;
timestamp?: number;
// Recovery information
isRecoverable?: boolean;
suggestedActions?: string[];
documentationLink?: string;
// Notification preferences
notificationType?: NotificationType;
showToUser?: boolean;
logToConsole?: boolean;
logToFile?: boolean;
}
export interface ErrorDetails {
code: string;
message: string;
category: ErrorCategory;
severity: ErrorSeverity;
timestamp: Date;
context?: Record<string, any>;
stack?: string;
userAction?: string;
recovery?: {
automatic: boolean;
action?: () => Promise<void>;
description?: string;
};
}
export interface ErrorLogEntry {
id: string;
error: ErrorDetails;
resolved: boolean;
resolvedAt?: Date;
attempts: number;
lastAttempt?: Date;
}
/**
* Base class for all Task Master errors
*/
export abstract class TaskMasterError extends Error {
public readonly code: string;
public readonly category: ErrorCategory;
public readonly severity: ErrorSeverity;
public readonly timestamp: Date;
public readonly context?: Record<string, any>;
public readonly userAction?: string;
public readonly recovery?: {
automatic: boolean;
action?: () => Promise<void>;
description?: string;
};
constructor(
message: string,
code: string,
category: ErrorCategory,
severity: ErrorSeverity = ErrorSeverity.MEDIUM,
context?: Record<string, any>,
userAction?: string,
recovery?: {
automatic: boolean;
action?: () => Promise<void>;
description?: string;
}
) {
super(message);
this.name = this.constructor.name;
this.code = code;
this.category = category;
this.severity = severity;
this.timestamp = new Date();
this.context = context;
this.userAction = userAction;
this.recovery = recovery;
// Capture stack trace
if (Error.captureStackTrace) {
Error.captureStackTrace(this, this.constructor);
}
}
public toErrorDetails(): ErrorDetails {
return {
code: this.code,
message: this.message,
category: this.category,
severity: this.severity,
timestamp: this.timestamp,
context: this.context,
stack: this.stack,
userAction: this.userAction,
recovery: this.recovery
};
}
}
/**
* MCP Connection related errors
*/
export class MCPConnectionError extends TaskMasterError {
constructor(
message: string,
code = 'MCP_CONNECTION_FAILED',
context?: Record<string, any>,
recovery?: {
automatic: boolean;
action?: () => Promise<void>;
description?: string;
}
) {
super(
message,
code,
ErrorCategory.MCP_CONNECTION,
ErrorSeverity.HIGH,
context,
'Check your Task Master configuration and ensure the MCP server is accessible.',
recovery
);
}
}
/**
* Configuration related errors
*/
export class ConfigurationError extends TaskMasterError {
constructor(
message: string,
code = 'CONFIGURATION_INVALID',
context?: Record<string, any>
) {
super(
message,
code,
ErrorCategory.CONFIGURATION,
ErrorSeverity.MEDIUM,
context,
'Check your Task Master configuration in VS Code settings.'
);
}
}
/**
* Task loading related errors
*/
export class TaskLoadingError extends TaskMasterError {
constructor(
message: string,
code = 'TASK_LOADING_FAILED',
context?: Record<string, any>,
recovery?: {
automatic: boolean;
action?: () => Promise<void>;
description?: string;
}
) {
super(
message,
code,
ErrorCategory.TASK_LOADING,
ErrorSeverity.MEDIUM,
context,
'Try refreshing the task list or check your project configuration.',
recovery
);
}
}
/**
* UI rendering related errors
*/
export class UIRenderingError extends TaskMasterError {
constructor(
message: string,
code = 'UI_RENDERING_FAILED',
context?: Record<string, any>
) {
super(
message,
code,
ErrorCategory.UI_RENDERING,
ErrorSeverity.LOW,
context,
'Try closing and reopening the Kanban board.'
);
}
}
/**
* Network related errors
*/
export class NetworkError extends TaskMasterError {
constructor(
message: string,
code = 'NETWORK_ERROR',
context?: Record<string, any>,
recovery?: {
automatic: boolean;
action?: () => Promise<void>;
description?: string;
}
) {
super(
message,
code,
ErrorCategory.NETWORK,
ErrorSeverity.MEDIUM,
context,
'Check your network connection and firewall settings.',
recovery
);
}
}
/**
* Centralized error handler
*/
export class ErrorHandler {
private static instance: ErrorHandler | null = null;
private errorLog: ErrorLogEntry[] = [];
private maxLogSize = 1000;
private errorListeners: ((error: ErrorDetails) => void)[] = [];
private constructor() {
this.setupGlobalErrorHandlers();
}
static getInstance(): ErrorHandler {
if (!ErrorHandler.instance) {
ErrorHandler.instance = new ErrorHandler();
}
return ErrorHandler.instance;
}
/**
* Handle an error with comprehensive logging and recovery
*/
async handleError(
error: Error | TaskMasterError,
context?: Record<string, any>
): Promise<void> {
const errorDetails = this.createErrorDetails(error, context);
const logEntry = this.logError(errorDetails);
// Notify listeners
this.notifyErrorListeners(errorDetails);
// Show user notification based on severity
await this.showUserNotification(errorDetails);
// Attempt recovery if available
if (errorDetails.recovery?.automatic && errorDetails.recovery.action) {
try {
await errorDetails.recovery.action();
this.markErrorResolved(logEntry.id);
} catch (recoveryError) {
logger.error('Error recovery failed:', recoveryError);
logEntry.attempts++;
logEntry.lastAttempt = new Date();
}
}
// Log to console with appropriate level
this.logToConsole(errorDetails);
}
/**
* Handle critical errors that should stop execution
*/
async handleCriticalError(
error: Error | TaskMasterError,
context?: Record<string, any>
): Promise<void> {
const errorDetails = this.createErrorDetails(error, context);
errorDetails.severity = ErrorSeverity.CRITICAL;
await this.handleError(error, context);
// Show critical error dialog
const action = await vscode.window.showErrorMessage(
`Critical Error in Task Master: ${errorDetails.message}`,
'View Details',
'Report Issue',
'Restart Extension'
);
switch (action) {
case 'View Details':
await this.showErrorDetails(errorDetails);
break;
case 'Report Issue':
await this.openIssueReport(errorDetails);
break;
case 'Restart Extension':
await vscode.commands.executeCommand('workbench.action.reloadWindow');
break;
}
}
/**
* Add error event listener
*/
onError(listener: (error: ErrorDetails) => void): void {
this.errorListeners.push(listener);
}
/**
* Remove error event listener
*/
removeErrorListener(listener: (error: ErrorDetails) => void): void {
const index = this.errorListeners.indexOf(listener);
if (index !== -1) {
this.errorListeners.splice(index, 1);
}
}
/**
* Get error log
*/
getErrorLog(
category?: ErrorCategory,
severity?: ErrorSeverity
): ErrorLogEntry[] {
let filteredLog = this.errorLog;
if (category) {
filteredLog = filteredLog.filter(
(entry) => entry.error.category === category
);
}
if (severity) {
filteredLog = filteredLog.filter(
(entry) => entry.error.severity === severity
);
}
return filteredLog.slice().reverse(); // Most recent first
}
/**
* Clear error log
*/
clearErrorLog(): void {
this.errorLog = [];
}
/**
* Export error log for debugging
*/
exportErrorLog(): string {
return JSON.stringify(this.errorLog, null, 2);
}
/**
* Create error details from error instance
*/
private createErrorDetails(
error: Error | TaskMasterError,
context?: Record<string, any>
): ErrorDetails {
if (error instanceof TaskMasterError) {
const details = error.toErrorDetails();
if (context) {
details.context = { ...details.context, ...context };
}
return details;
}
// Handle standard Error objects
return {
code: 'UNKNOWN_ERROR',
message: error.message || 'An unknown error occurred',
category: ErrorCategory.INTERNAL,
severity: ErrorSeverity.MEDIUM,
timestamp: new Date(),
context: { ...context, errorName: error.name },
stack: error.stack
};
}
/**
* Log error to internal log
*/
private logError(errorDetails: ErrorDetails): ErrorLogEntry {
const logEntry: ErrorLogEntry = {
id: `err_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
error: errorDetails,
resolved: false,
attempts: 0
};
this.errorLog.push(logEntry);
// Maintain log size limit
if (this.errorLog.length > this.maxLogSize) {
this.errorLog = this.errorLog.slice(-this.maxLogSize);
}
return logEntry;
}
/**
* Mark error as resolved
*/
private markErrorResolved(errorId: string): void {
const entry = this.errorLog.find((e) => e.id === errorId);
if (entry) {
entry.resolved = true;
entry.resolvedAt = new Date();
}
}
/**
* Show user notification based on error severity and user preferences
*/
private async showUserNotification(
errorDetails: ErrorDetails
): Promise<void> {
// Check if user wants to see this notification
if (!shouldShowNotification(errorDetails.category, errorDetails.severity)) {
return;
}
const notificationType = getNotificationType(
errorDetails.category,
errorDetails.severity
);
const message = errorDetails.userAction
? `${errorDetails.message} ${errorDetails.userAction}`
: errorDetails.message;
// Handle different notification types based on user preferences
switch (notificationType) {
case 'VSCODE_ERROR':
await vscode.window.showErrorMessage(message);
break;
case 'VSCODE_WARNING':
await vscode.window.showWarningMessage(message);
break;
case 'VSCODE_INFO':
await vscode.window.showInformationMessage(message);
break;
case 'TOAST_SUCCESS':
case 'TOAST_INFO':
case 'TOAST_WARNING':
case 'TOAST_ERROR':
// These will be handled by the webview toast system
// The error listener in extension.ts will send these to webview
break;
case 'CONSOLE_ONLY':
case 'SILENT':
// No user notification, just console logging
break;
default:
// Fallback to severity-based notifications
switch (errorDetails.severity) {
case ErrorSeverity.CRITICAL:
await vscode.window.showErrorMessage(message);
break;
case ErrorSeverity.HIGH:
await vscode.window.showErrorMessage(message);
break;
case ErrorSeverity.MEDIUM:
await vscode.window.showWarningMessage(message);
break;
case ErrorSeverity.LOW:
await vscode.window.showInformationMessage(message);
break;
}
}
}
/**
* Log to console with appropriate level
*/
private logToConsole(errorDetails: ErrorDetails): void {
const logMessage = `[${errorDetails.category}] ${errorDetails.code}: ${errorDetails.message}`;
switch (errorDetails.severity) {
case ErrorSeverity.CRITICAL:
case ErrorSeverity.HIGH:
logger.error(logMessage, errorDetails);
break;
case ErrorSeverity.MEDIUM:
logger.warn(logMessage, errorDetails);
break;
case ErrorSeverity.LOW:
console.info(logMessage, errorDetails);
break;
}
}
/**
* Show detailed error information
*/
private async showErrorDetails(errorDetails: ErrorDetails): Promise<void> {
const details = [
`Error Code: ${errorDetails.code}`,
`Category: ${errorDetails.category}`,
`Severity: ${errorDetails.severity}`,
`Time: ${errorDetails.timestamp.toISOString()}`,
`Message: ${errorDetails.message}`
];
if (errorDetails.context) {
details.push(`Context: ${JSON.stringify(errorDetails.context, null, 2)}`);
}
if (errorDetails.stack) {
details.push(`Stack Trace: ${errorDetails.stack}`);
}
const content = details.join('\n\n');
// Create temporary document to show error details
const doc = await vscode.workspace.openTextDocument({
content,
language: 'plaintext'
});
await vscode.window.showTextDocument(doc);
}
/**
* Open GitHub issue report
*/
private async openIssueReport(errorDetails: ErrorDetails): Promise<void> {
const issueTitle = encodeURIComponent(
`Error: ${errorDetails.code} - ${errorDetails.message}`
);
const issueBody = encodeURIComponent(`
**Error Details:**
- Code: ${errorDetails.code}
- Category: ${errorDetails.category}
- Severity: ${errorDetails.severity}
- Time: ${errorDetails.timestamp.toISOString()}
**Message:**
${errorDetails.message}
**Context:**
${errorDetails.context ? JSON.stringify(errorDetails.context, null, 2) : 'None'}
**Steps to Reproduce:**
1.
2.
3.
**Expected Behavior:**
**Additional Notes:**
`);
const issueUrl = `https://github.com/eyaltoledano/claude-task-master/issues/new?title=${issueTitle}&body=${issueBody}`;
await vscode.env.openExternal(vscode.Uri.parse(issueUrl));
}
/**
* Notify error listeners
*/
private notifyErrorListeners(errorDetails: ErrorDetails): void {
this.errorListeners.forEach((listener) => {
try {
listener(errorDetails);
} catch (error) {
logger.error('Error in error listener:', error);
}
});
}
/**
* Setup global error handlers
*/
private setupGlobalErrorHandlers(): void {
// Handle unhandled promise rejections
process.on('unhandledRejection', (reason, promise) => {
// Create a concrete error class for internal errors
class InternalError extends TaskMasterError {
constructor(
message: string,
code: string,
severity: ErrorSeverity,
context?: Record<string, any>
) {
super(message, code, ErrorCategory.INTERNAL, severity, context);
}
}
const error = new InternalError(
'Unhandled Promise Rejection',
'UNHANDLED_REJECTION',
ErrorSeverity.HIGH,
{ reason: String(reason), promise: String(promise) }
);
this.handleError(error);
});
// Handle uncaught exceptions
process.on('uncaughtException', (error) => {
// Create a concrete error class for internal errors
class InternalError extends TaskMasterError {
constructor(
message: string,
code: string,
severity: ErrorSeverity,
context?: Record<string, any>
) {
super(message, code, ErrorCategory.INTERNAL, severity, context);
}
}
const taskMasterError = new InternalError(
'Uncaught Exception',
'UNCAUGHT_EXCEPTION',
ErrorSeverity.CRITICAL,
{ originalError: error.message, stack: error.stack }
);
this.handleCriticalError(taskMasterError);
});
}
}
/**
* Utility functions for error handling
*/
export function getErrorHandler(): ErrorHandler {
return ErrorHandler.getInstance();
}
export function createRecoveryAction(
action: () => Promise<void>,
description: string
) {
return {
automatic: false,
action,
description
};
}
export function createAutoRecoveryAction(
action: () => Promise<void>,
description: string
) {
return {
automatic: true,
action,
description
};
}
// Default error categorization rules
export const ERROR_CATEGORIZATION_RULES: Record<string, ErrorCategory> = {
// Network patterns
ECONNREFUSED: ErrorCategory.NETWORK,
ENOTFOUND: ErrorCategory.NETWORK,
ETIMEDOUT: ErrorCategory.NETWORK,
'Network request failed': ErrorCategory.NETWORK,
'fetch failed': ErrorCategory.NETWORK,
// MCP patterns
MCP: ErrorCategory.MCP_CONNECTION,
'Task Master': ErrorCategory.TASK_MASTER_API,
polling: ErrorCategory.TASK_MASTER_API,
// VS Code patterns
vscode: ErrorCategory.VSCODE_API,
webview: ErrorCategory.WEBVIEW,
extension: ErrorCategory.EXTENSION_HOST,
// Data patterns
JSON: ErrorCategory.DATA_PARSING,
parse: ErrorCategory.DATA_PARSING,
validation: ErrorCategory.DATA_VALIDATION,
invalid: ErrorCategory.DATA_VALIDATION,
// Permission patterns
EACCES: ErrorCategory.PERMISSION,
EPERM: ErrorCategory.PERMISSION,
permission: ErrorCategory.PERMISSION,
// File system patterns
ENOENT: ErrorCategory.FILE_SYSTEM,
EISDIR: ErrorCategory.FILE_SYSTEM,
file: ErrorCategory.FILE_SYSTEM
};
// Severity mapping based on error categories
export const CATEGORY_SEVERITY_MAPPING: Record<ErrorCategory, ErrorSeverity> = {
[ErrorCategory.NETWORK]: ErrorSeverity.MEDIUM,
[ErrorCategory.MCP_CONNECTION]: ErrorSeverity.HIGH,
[ErrorCategory.TASK_MASTER_API]: ErrorSeverity.HIGH,
[ErrorCategory.DATA_VALIDATION]: ErrorSeverity.MEDIUM,
[ErrorCategory.DATA_PARSING]: ErrorSeverity.HIGH,
[ErrorCategory.TASK_DATA_CORRUPTION]: ErrorSeverity.CRITICAL,
[ErrorCategory.VSCODE_API]: ErrorSeverity.HIGH,
[ErrorCategory.WEBVIEW]: ErrorSeverity.MEDIUM,
[ErrorCategory.EXTENSION_HOST]: ErrorSeverity.CRITICAL,
[ErrorCategory.USER_INTERACTION]: ErrorSeverity.LOW,
[ErrorCategory.DRAG_DROP]: ErrorSeverity.MEDIUM,
[ErrorCategory.COMPONENT_RENDER]: ErrorSeverity.MEDIUM,
[ErrorCategory.PERMISSION]: ErrorSeverity.CRITICAL,
[ErrorCategory.FILE_SYSTEM]: ErrorSeverity.HIGH,
[ErrorCategory.CONFIGURATION]: ErrorSeverity.MEDIUM,
[ErrorCategory.UNKNOWN]: ErrorSeverity.HIGH,
// Legacy mappings for existing categories
[ErrorCategory.TASK_LOADING]: ErrorSeverity.HIGH,
[ErrorCategory.UI_RENDERING]: ErrorSeverity.MEDIUM,
[ErrorCategory.VALIDATION]: ErrorSeverity.MEDIUM,
[ErrorCategory.INTERNAL]: ErrorSeverity.HIGH
};
// Notification type mapping based on severity
export const SEVERITY_NOTIFICATION_MAPPING: Record<
ErrorSeverity,
NotificationType
> = {
[ErrorSeverity.LOW]: NotificationType.TOAST_INFO,
[ErrorSeverity.MEDIUM]: NotificationType.TOAST_WARNING,
[ErrorSeverity.HIGH]: NotificationType.VSCODE_WARNING,
[ErrorSeverity.CRITICAL]: NotificationType.VSCODE_ERROR
};
/**
* Automatically categorize an error based on its message and type
*/
export function categorizeError(
error: Error | unknown,
operation?: string
): ErrorCategory {
const errorMessage = error instanceof Error ? error.message : String(error);
const errorStack = error instanceof Error ? error.stack : undefined;
const searchText =
`${errorMessage} ${errorStack || ''} ${operation || ''}`.toLowerCase();
for (const [pattern, category] of Object.entries(
ERROR_CATEGORIZATION_RULES
)) {
if (searchText.includes(pattern.toLowerCase())) {
return category;
}
}
return ErrorCategory.UNKNOWN;
}
export function getSuggestedSeverity(category: ErrorCategory): ErrorSeverity {
return CATEGORY_SEVERITY_MAPPING[category] || ErrorSeverity.HIGH;
}
export function getSuggestedNotificationType(
severity: ErrorSeverity
): NotificationType {
return (
SEVERITY_NOTIFICATION_MAPPING[severity] || NotificationType.CONSOLE_ONLY
);
}
export function createErrorContext(
error: Error | unknown,
operation?: string,
overrides?: Partial<ErrorContext>
): ErrorContext {
const category = categorizeError(error, operation);
const severity = getSuggestedSeverity(category);
const notificationType = getSuggestedNotificationType(severity);
const baseContext: ErrorContext = {
category,
severity,
message: error instanceof Error ? error.message : String(error),
originalError: error,
operation,
timestamp: Date.now(),
stackTrace: error instanceof Error ? error.stack : undefined,
isRecoverable: severity !== ErrorSeverity.CRITICAL,
notificationType,
showToUser:
severity === ErrorSeverity.HIGH || severity === ErrorSeverity.CRITICAL,
logToConsole: true,
logToFile:
severity === ErrorSeverity.HIGH || severity === ErrorSeverity.CRITICAL
};
return { ...baseContext, ...overrides };
}

View File

@@ -0,0 +1,34 @@
/**
* Simple Event Emitter
* Lightweight alternative to complex event bus
*/
export type EventHandler = (...args: any[]) => void | Promise<void>;
export class EventEmitter {
private handlers = new Map<string, Set<EventHandler>>();
on(event: string, handler: EventHandler): () => void {
if (!this.handlers.has(event)) {
this.handlers.set(event, new Set());
}
this.handlers.get(event)?.add(handler);
// Return unsubscribe function
return () => this.off(event, handler);
}
off(event: string, handler: EventHandler): void {
this.handlers.get(event)?.delete(handler);
}
emit(event: string, ...args: any[]): void {
this.handlers.get(event)?.forEach((handler) => {
try {
handler(...args);
} catch (error) {
console.error(`Error in event handler for ${event}:`, error);
}
});
}
}

View File

@@ -0,0 +1,104 @@
import * as vscode from 'vscode';
/**
* Logger interface for dependency injection
*/
export interface ILogger {
log(message: string, ...args: any[]): void;
error(message: string, ...args: any[]): void;
warn(message: string, ...args: any[]): void;
debug(message: string, ...args: any[]): void;
show(): void;
dispose(): void;
}
/**
* Logger that outputs to VS Code's output channel instead of console
* This prevents interference with MCP stdio communication
*/
export class ExtensionLogger implements ILogger {
private static instance: ExtensionLogger;
private outputChannel: vscode.OutputChannel;
private debugMode: boolean;
private constructor() {
this.outputChannel = vscode.window.createOutputChannel('TaskMaster');
const config = vscode.workspace.getConfiguration('taskmaster');
this.debugMode = config.get<boolean>('debug.enableLogging', true);
}
static getInstance(): ExtensionLogger {
if (!ExtensionLogger.instance) {
ExtensionLogger.instance = new ExtensionLogger();
}
return ExtensionLogger.instance;
}
log(message: string, ...args: any[]): void {
if (!this.debugMode) {
return;
}
const timestamp = new Date().toISOString();
const formattedMessage = this.formatMessage(message, args);
this.outputChannel.appendLine(`[${timestamp}] ${formattedMessage}`);
}
error(message: string, ...args: any[]): void {
const timestamp = new Date().toISOString();
const formattedMessage = this.formatMessage(message, args);
this.outputChannel.appendLine(`[${timestamp}] ERROR: ${formattedMessage}`);
}
warn(message: string, ...args: any[]): void {
if (!this.debugMode) {
return;
}
const timestamp = new Date().toISOString();
const formattedMessage = this.formatMessage(message, args);
this.outputChannel.appendLine(`[${timestamp}] WARN: ${formattedMessage}`);
}
debug(message: string, ...args: any[]): void {
if (!this.debugMode) {
return;
}
const timestamp = new Date().toISOString();
const formattedMessage = this.formatMessage(message, args);
this.outputChannel.appendLine(`[${timestamp}] DEBUG: ${formattedMessage}`);
}
private formatMessage(message: string, args: any[]): string {
if (args.length === 0) {
return message;
}
// Convert objects to JSON for better readability
const formattedArgs = args.map((arg) => {
if (typeof arg === 'object' && arg !== null) {
try {
return JSON.stringify(arg, null, 2);
} catch {
return String(arg);
}
}
return String(arg);
});
return `${message} ${formattedArgs.join(' ')}`;
}
show(): void {
this.outputChannel.show();
}
dispose(): void {
this.outputChannel.dispose();
}
setDebugMode(enabled: boolean): void {
this.debugMode = enabled;
}
}
// Export a singleton instance for convenience
export const logger = ExtensionLogger.getInstance();

View File

@@ -0,0 +1,390 @@
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
import * as vscode from 'vscode';
import { logger } from './logger';
export interface MCPConfig {
command: string;
args: string[];
cwd?: string;
env?: Record<string, string>;
timeout?: number;
}
export interface MCPServerStatus {
isRunning: boolean;
pid?: number;
error?: string;
}
export class MCPClientManager {
private client: Client | null = null;
private transport: StdioClientTransport | null = null;
private config: MCPConfig;
private status: MCPServerStatus = { isRunning: false };
private connectionPromise: Promise<void> | null = null;
constructor(config: MCPConfig) {
logger.log(
'🔍 DEBUGGING: MCPClientManager constructor called with config:',
config
);
this.config = config;
}
/**
* Get the current server status
*/
getStatus(): MCPServerStatus {
return { ...this.status };
}
/**
* Start the MCP server process and establish client connection
*/
async connect(): Promise<void> {
if (this.connectionPromise) {
return this.connectionPromise;
}
this.connectionPromise = this._doConnect();
return this.connectionPromise;
}
private async _doConnect(): Promise<void> {
try {
// Clean up any existing connections
await this.disconnect();
// Create the transport - it will handle spawning the server process internally
logger.log(
`Starting MCP server: ${this.config.command} ${this.config.args?.join(' ') || ''}`
);
logger.log('🔍 DEBUGGING: Transport config cwd:', this.config.cwd);
logger.log('🔍 DEBUGGING: Process cwd before spawn:', process.cwd());
// Test if the target directory and .taskmaster exist
const fs = require('fs');
const path = require('path');
try {
const targetDir = this.config.cwd;
const taskmasterDir = path.join(targetDir, '.taskmaster');
const tasksFile = path.join(taskmasterDir, 'tasks', 'tasks.json');
logger.log(
'🔍 DEBUGGING: Checking target directory:',
targetDir,
'exists:',
fs.existsSync(targetDir)
);
logger.log(
'🔍 DEBUGGING: Checking .taskmaster dir:',
taskmasterDir,
'exists:',
fs.existsSync(taskmasterDir)
);
logger.log(
'🔍 DEBUGGING: Checking tasks.json:',
tasksFile,
'exists:',
fs.existsSync(tasksFile)
);
if (fs.existsSync(tasksFile)) {
const stats = fs.statSync(tasksFile);
logger.log('🔍 DEBUGGING: tasks.json size:', stats.size, 'bytes');
}
} catch (error) {
logger.log('🔍 DEBUGGING: Error checking filesystem:', error);
}
this.transport = new StdioClientTransport({
command: this.config.command,
args: this.config.args || [],
cwd: this.config.cwd,
env: {
...(Object.fromEntries(
Object.entries(process.env).filter(([, v]) => v !== undefined)
) as Record<string, string>),
...this.config.env
}
});
logger.log('🔍 DEBUGGING: Transport created, checking process...');
// Set up transport event handlers
this.transport.onerror = (error: Error) => {
logger.error('❌ MCP transport error:', error);
logger.error('Transport error details:', {
message: error.message,
stack: error.stack,
code: (error as any).code,
errno: (error as any).errno,
syscall: (error as any).syscall
});
this.status = { isRunning: false, error: error.message };
vscode.window.showErrorMessage(
`TaskMaster MCP transport error: ${error.message}`
);
};
this.transport.onclose = () => {
logger.log('🔌 MCP transport closed');
this.status = { isRunning: false };
this.client = null;
this.transport = null;
};
// Add message handler like the working debug script
this.transport.onmessage = (message: any) => {
logger.log('📤 MCP server message:', message);
};
// Create the client
this.client = new Client(
{
name: 'taskr-vscode-extension',
version: '1.0.0'
},
{
capabilities: {
tools: {}
}
}
);
// Connect the client to the transport (this automatically starts the transport)
logger.log('🔄 Attempting MCP client connection...');
logger.log('MCP config:', {
command: this.config.command,
args: this.config.args,
cwd: this.config.cwd
});
logger.log('Current working directory:', process.cwd());
logger.log(
'VS Code workspace folders:',
vscode.workspace.workspaceFolders?.map((f) => f.uri.fsPath)
);
// Check if process was created before connecting
if (this.transport && (this.transport as any).process) {
const proc = (this.transport as any).process;
logger.log('📝 MCP server process PID:', proc.pid);
logger.log('📝 Process working directory will be:', this.config.cwd);
proc.on('exit', (code: number, signal: string) => {
logger.log(
`🔚 MCP server process exited with code ${code}, signal ${signal}`
);
if (code !== 0) {
logger.log('❌ Non-zero exit code indicates server failure');
}
});
proc.on('error', (error: Error) => {
logger.log('❌ MCP server process error:', error);
});
// Listen to stderr to see server-side errors
if (proc.stderr) {
proc.stderr.on('data', (data: Buffer) => {
logger.log('📥 MCP server stderr:', data.toString());
});
}
// Listen to stdout for server messages
if (proc.stdout) {
proc.stdout.on('data', (data: Buffer) => {
logger.log('📤 MCP server stdout:', data.toString());
});
}
} else {
logger.log('⚠️ No process found in transport before connection');
}
await this.client.connect(this.transport);
// Update status
this.status = {
isRunning: true,
pid: this.transport.pid || undefined
};
logger.log('MCP client connected successfully');
} catch (error) {
logger.error('Failed to connect to MCP server:', error);
this.status = {
isRunning: false,
error: error instanceof Error ? error.message : 'Unknown error'
};
// Clean up on error
await this.disconnect();
throw error;
} finally {
this.connectionPromise = null;
}
}
/**
* Disconnect from the MCP server and clean up resources
*/
async disconnect(): Promise<void> {
logger.log('Disconnecting from MCP server');
if (this.client) {
try {
await this.client.close();
} catch (error) {
logger.error('Error closing MCP client:', error);
}
this.client = null;
}
if (this.transport) {
try {
await this.transport.close();
} catch (error) {
logger.error('Error closing MCP transport:', error);
}
this.transport = null;
}
this.status = { isRunning: false };
}
/**
* Get the MCP client instance (if connected)
*/
getClient(): Client | null {
return this.client;
}
/**
* Call an MCP tool
*/
async callTool(
toolName: string,
arguments_: Record<string, unknown>
): Promise<any> {
if (!this.client) {
throw new Error('MCP client is not connected');
}
try {
// Use the configured timeout or default to 5 minutes
const timeout = this.config.timeout || 300000; // 5 minutes default
logger.log(`Calling MCP tool "${toolName}" with timeout: ${timeout}ms`);
const result = await this.client.callTool(
{
name: toolName,
arguments: arguments_
},
undefined,
{
timeout: timeout
}
);
return result;
} catch (error) {
logger.error(`Error calling MCP tool "${toolName}":`, error);
throw error;
}
}
/**
* Test the connection by calling a simple MCP tool
*/
async testConnection(): Promise<boolean> {
try {
// Try to list available tools as a connection test
if (!this.client) {
return false;
}
// listTools is a simple metadata request, no need for extended timeout
const result = await this.client.listTools();
logger.log(
'Available MCP tools:',
result.tools?.map((t) => t.name) || []
);
return true;
} catch (error) {
logger.error('Connection test failed:', error);
return false;
}
}
/**
* Get stderr stream from the transport (if available)
*/
getStderr(): NodeJS.ReadableStream | null {
const stderr = this.transport?.stderr;
return stderr ? (stderr as unknown as NodeJS.ReadableStream) : null;
}
/**
* Get the process ID of the spawned server
*/
getPid(): number | null {
return this.transport?.pid || null;
}
}
/**
* Create MCP configuration from VS Code settings
*/
export function createMCPConfigFromSettings(): MCPConfig {
logger.log(
'🔍 DEBUGGING: createMCPConfigFromSettings called at',
new Date().toISOString()
);
const config = vscode.workspace.getConfiguration('taskmaster');
let command = config.get<string>('mcp.command', 'npx');
const args = config.get<string[]>('mcp.args', ['task-master-ai']);
// Use proper VS Code workspace detection
const defaultCwd =
vscode.workspace.workspaceFolders?.[0]?.uri.fsPath || process.cwd();
const cwd = config.get<string>('mcp.cwd', defaultCwd);
const env = config.get<Record<string, string>>('mcp.env');
const timeout = config.get<number>('mcp.requestTimeoutMs', 300000);
logger.log('✅ Using workspace directory:', defaultCwd);
// If using default 'npx', try to find the full path on macOS/Linux
if (command === 'npx') {
const fs = require('fs');
const npxPaths = [
'/opt/homebrew/bin/npx', // Homebrew on Apple Silicon
'/usr/local/bin/npx', // Homebrew on Intel
'/usr/bin/npx', // System npm
'npx' // Final fallback to PATH
];
for (const path of npxPaths) {
try {
if (path === 'npx' || fs.existsSync(path)) {
command = path;
logger.log(`✅ Using npx at: ${path}`);
break;
}
} catch (error) {
// Continue to next path
}
}
}
return {
command,
args,
cwd: cwd || defaultCwd,
env,
timeout
};
}

View File

@@ -0,0 +1,463 @@
import * as vscode from 'vscode';
import { ErrorCategory, ErrorSeverity, NotificationType } from './errorHandler';
import { logger } from './logger';
export interface NotificationPreferences {
// Global notification toggles
enableToastNotifications: boolean;
enableVSCodeNotifications: boolean;
enableConsoleLogging: boolean;
// Toast notification settings
toastDuration: {
info: number;
warning: number;
error: number;
};
// Category-based preferences
categoryPreferences: Record<
ErrorCategory,
{
showToUser: boolean;
notificationType: NotificationType;
logToConsole: boolean;
}
>;
// Severity-based preferences
severityPreferences: Record<
ErrorSeverity,
{
showToUser: boolean;
notificationType: NotificationType;
minToastDuration: number;
}
>;
// Advanced settings
maxToastCount: number;
enableErrorTracking: boolean;
enableDetailedErrorInfo: boolean;
}
export class NotificationPreferencesManager {
private static instance: NotificationPreferencesManager | null = null;
private readonly configSection = 'taskMasterKanban';
private constructor() {}
static getInstance(): NotificationPreferencesManager {
if (!NotificationPreferencesManager.instance) {
NotificationPreferencesManager.instance =
new NotificationPreferencesManager();
}
return NotificationPreferencesManager.instance;
}
/**
* Get current notification preferences from VS Code settings
*/
getPreferences(): NotificationPreferences {
const config = vscode.workspace.getConfiguration(this.configSection);
return {
enableToastNotifications: config.get('notifications.enableToast', true),
enableVSCodeNotifications: config.get('notifications.enableVSCode', true),
enableConsoleLogging: config.get('notifications.enableConsole', true),
toastDuration: {
info: config.get('notifications.toastDuration.info', 5000),
warning: config.get('notifications.toastDuration.warning', 7000),
error: config.get('notifications.toastDuration.error', 10000)
},
categoryPreferences: this.getCategoryPreferences(config),
severityPreferences: this.getSeverityPreferences(config),
maxToastCount: config.get('notifications.maxToastCount', 5),
enableErrorTracking: config.get(
'notifications.enableErrorTracking',
true
),
enableDetailedErrorInfo: config.get(
'notifications.enableDetailedErrorInfo',
false
)
};
}
/**
* Update notification preferences in VS Code settings
*/
async updatePreferences(
preferences: Partial<NotificationPreferences>
): Promise<void> {
const config = vscode.workspace.getConfiguration(this.configSection);
if (preferences.enableToastNotifications !== undefined) {
await config.update(
'notifications.enableToast',
preferences.enableToastNotifications,
vscode.ConfigurationTarget.Global
);
}
if (preferences.enableVSCodeNotifications !== undefined) {
await config.update(
'notifications.enableVSCode',
preferences.enableVSCodeNotifications,
vscode.ConfigurationTarget.Global
);
}
if (preferences.enableConsoleLogging !== undefined) {
await config.update(
'notifications.enableConsole',
preferences.enableConsoleLogging,
vscode.ConfigurationTarget.Global
);
}
if (preferences.toastDuration) {
await config.update(
'notifications.toastDuration',
preferences.toastDuration,
vscode.ConfigurationTarget.Global
);
}
if (preferences.maxToastCount !== undefined) {
await config.update(
'notifications.maxToastCount',
preferences.maxToastCount,
vscode.ConfigurationTarget.Global
);
}
if (preferences.enableErrorTracking !== undefined) {
await config.update(
'notifications.enableErrorTracking',
preferences.enableErrorTracking,
vscode.ConfigurationTarget.Global
);
}
if (preferences.enableDetailedErrorInfo !== undefined) {
await config.update(
'notifications.enableDetailedErrorInfo',
preferences.enableDetailedErrorInfo,
vscode.ConfigurationTarget.Global
);
}
}
/**
* Check if notifications should be shown for a specific error category and severity
*/
shouldShowNotification(
category: ErrorCategory,
severity: ErrorSeverity
): boolean {
const preferences = this.getPreferences();
// Check global toggles first
if (
!preferences.enableToastNotifications &&
!preferences.enableVSCodeNotifications
) {
return false;
}
// Check category preferences
const categoryPref = preferences.categoryPreferences[category];
if (categoryPref && !categoryPref.showToUser) {
return false;
}
// Check severity preferences
const severityPref = preferences.severityPreferences[severity];
if (severityPref && !severityPref.showToUser) {
return false;
}
return true;
}
/**
* Get the appropriate notification type for an error
*/
getNotificationType(
category: ErrorCategory,
severity: ErrorSeverity
): NotificationType {
const preferences = this.getPreferences();
// Check category preference first
const categoryPref = preferences.categoryPreferences[category];
if (categoryPref) {
return categoryPref.notificationType;
}
// Fall back to severity preference
const severityPref = preferences.severityPreferences[severity];
if (severityPref) {
return severityPref.notificationType;
}
// Default fallback
return this.getDefaultNotificationType(severity);
}
/**
* Get toast duration for a specific severity
*/
getToastDuration(severity: ErrorSeverity): number {
const preferences = this.getPreferences();
switch (severity) {
case ErrorSeverity.LOW:
return preferences.toastDuration.info;
case ErrorSeverity.MEDIUM:
return preferences.toastDuration.warning;
case ErrorSeverity.HIGH:
case ErrorSeverity.CRITICAL:
return preferences.toastDuration.error;
default:
return preferences.toastDuration.warning;
}
}
/**
* Reset preferences to defaults
*/
async resetToDefaults(): Promise<void> {
const config = vscode.workspace.getConfiguration(this.configSection);
// Reset all notification settings
await config.update(
'notifications',
undefined,
vscode.ConfigurationTarget.Global
);
logger.log('Task Master Kanban notification preferences reset to defaults');
}
/**
* Get category-based preferences with defaults
*/
private getCategoryPreferences(config: vscode.WorkspaceConfiguration): Record<
ErrorCategory,
{
showToUser: boolean;
notificationType: NotificationType;
logToConsole: boolean;
}
> {
const defaults = {
[ErrorCategory.MCP_CONNECTION]: {
showToUser: true,
notificationType: NotificationType.VSCODE_ERROR,
logToConsole: true
},
[ErrorCategory.CONFIGURATION]: {
showToUser: true,
notificationType: NotificationType.VSCODE_WARNING,
logToConsole: true
},
[ErrorCategory.TASK_LOADING]: {
showToUser: true,
notificationType: NotificationType.TOAST_WARNING,
logToConsole: true
},
[ErrorCategory.UI_RENDERING]: {
showToUser: true,
notificationType: NotificationType.TOAST_INFO,
logToConsole: false
},
[ErrorCategory.VALIDATION]: {
showToUser: true,
notificationType: NotificationType.TOAST_WARNING,
logToConsole: true
},
[ErrorCategory.NETWORK]: {
showToUser: true,
notificationType: NotificationType.TOAST_WARNING,
logToConsole: true
},
[ErrorCategory.INTERNAL]: {
showToUser: true,
notificationType: NotificationType.VSCODE_ERROR,
logToConsole: true
},
[ErrorCategory.TASK_MASTER_API]: {
showToUser: true,
notificationType: NotificationType.TOAST_ERROR,
logToConsole: true
},
[ErrorCategory.DATA_VALIDATION]: {
showToUser: true,
notificationType: NotificationType.TOAST_WARNING,
logToConsole: true
},
[ErrorCategory.DATA_PARSING]: {
showToUser: true,
notificationType: NotificationType.VSCODE_ERROR,
logToConsole: true
},
[ErrorCategory.TASK_DATA_CORRUPTION]: {
showToUser: true,
notificationType: NotificationType.VSCODE_ERROR,
logToConsole: true
},
[ErrorCategory.VSCODE_API]: {
showToUser: true,
notificationType: NotificationType.VSCODE_ERROR,
logToConsole: true
},
[ErrorCategory.WEBVIEW]: {
showToUser: true,
notificationType: NotificationType.TOAST_WARNING,
logToConsole: true
},
[ErrorCategory.EXTENSION_HOST]: {
showToUser: true,
notificationType: NotificationType.VSCODE_ERROR,
logToConsole: true
},
[ErrorCategory.USER_INTERACTION]: {
showToUser: false,
notificationType: NotificationType.CONSOLE_ONLY,
logToConsole: true
},
[ErrorCategory.DRAG_DROP]: {
showToUser: true,
notificationType: NotificationType.TOAST_INFO,
logToConsole: false
},
[ErrorCategory.COMPONENT_RENDER]: {
showToUser: true,
notificationType: NotificationType.TOAST_WARNING,
logToConsole: true
},
[ErrorCategory.PERMISSION]: {
showToUser: true,
notificationType: NotificationType.VSCODE_ERROR,
logToConsole: true
},
[ErrorCategory.FILE_SYSTEM]: {
showToUser: true,
notificationType: NotificationType.VSCODE_ERROR,
logToConsole: true
},
[ErrorCategory.UNKNOWN]: {
showToUser: true,
notificationType: NotificationType.VSCODE_WARNING,
logToConsole: true
}
};
// Allow user overrides from settings
const userPreferences = config.get('notifications.categoryPreferences', {});
return { ...defaults, ...userPreferences };
}
/**
* Get severity-based preferences with defaults
*/
private getSeverityPreferences(config: vscode.WorkspaceConfiguration): Record<
ErrorSeverity,
{
showToUser: boolean;
notificationType: NotificationType;
minToastDuration: number;
}
> {
const defaults = {
[ErrorSeverity.LOW]: {
showToUser: true,
notificationType: NotificationType.TOAST_INFO,
minToastDuration: 3000
},
[ErrorSeverity.MEDIUM]: {
showToUser: true,
notificationType: NotificationType.TOAST_WARNING,
minToastDuration: 5000
},
[ErrorSeverity.HIGH]: {
showToUser: true,
notificationType: NotificationType.VSCODE_WARNING,
minToastDuration: 7000
},
[ErrorSeverity.CRITICAL]: {
showToUser: true,
notificationType: NotificationType.VSCODE_ERROR,
minToastDuration: 10000
}
};
// Allow user overrides from settings
const userPreferences = config.get('notifications.severityPreferences', {});
return { ...defaults, ...userPreferences };
}
/**
* Get default notification type for severity
*/
private getDefaultNotificationType(
severity: ErrorSeverity
): NotificationType {
switch (severity) {
case ErrorSeverity.LOW:
return NotificationType.TOAST_INFO;
case ErrorSeverity.MEDIUM:
return NotificationType.TOAST_WARNING;
case ErrorSeverity.HIGH:
return NotificationType.VSCODE_WARNING;
case ErrorSeverity.CRITICAL:
return NotificationType.VSCODE_ERROR;
default:
return NotificationType.CONSOLE_ONLY;
}
}
}
// Export convenience functions
export function getNotificationPreferences(): NotificationPreferences {
return NotificationPreferencesManager.getInstance().getPreferences();
}
export function updateNotificationPreferences(
preferences: Partial<NotificationPreferences>
): Promise<void> {
return NotificationPreferencesManager.getInstance().updatePreferences(
preferences
);
}
export function shouldShowNotification(
category: ErrorCategory,
severity: ErrorSeverity
): boolean {
return NotificationPreferencesManager.getInstance().shouldShowNotification(
category,
severity
);
}
export function getNotificationType(
category: ErrorCategory,
severity: ErrorSeverity
): NotificationType {
return NotificationPreferencesManager.getInstance().getNotificationType(
category,
severity
);
}
export function getToastDuration(severity: ErrorSeverity): number {
return NotificationPreferencesManager.getInstance().getToastDuration(
severity
);
}

View File

@@ -0,0 +1,253 @@
/**
* Cache Manager
* Handles all caching logic with LRU eviction and analytics
*/
import type { ExtensionLogger } from '../../logger';
import type { CacheAnalytics, CacheConfig, CacheEntry } from '../types';
export class CacheManager {
private cache = new Map<string, CacheEntry>();
private analytics: CacheAnalytics = {
hits: 0,
misses: 0,
evictions: 0,
refreshes: 0,
totalSize: 0,
averageAccessTime: 0,
hitRate: 0
};
private backgroundRefreshTimer?: NodeJS.Timeout;
constructor(
private config: CacheConfig & { cacheDuration: number },
private logger: ExtensionLogger
) {
if (config.enableBackgroundRefresh) {
this.initializeBackgroundRefresh();
}
}
/**
* Get data from cache if not expired
*/
get(key: string): any {
const startTime = Date.now();
const cached = this.cache.get(key);
if (cached) {
const isExpired =
Date.now() - cached.timestamp >=
(cached.ttl || this.config.cacheDuration);
if (!isExpired) {
// Update access statistics
cached.accessCount++;
cached.lastAccessed = Date.now();
if (this.config.enableAnalytics) {
this.analytics.hits++;
}
const accessTime = Date.now() - startTime;
this.logger.debug(
`Cache hit for ${key} (${accessTime}ms, ${cached.accessCount} accesses)`
);
return cached.data;
} else {
// Remove expired entry
this.cache.delete(key);
this.logger.debug(`Cache entry expired and removed: ${key}`);
}
}
if (this.config.enableAnalytics) {
this.analytics.misses++;
}
this.logger.debug(`Cache miss for ${key}`);
return null;
}
/**
* Set data in cache with LRU eviction
*/
set(
key: string,
data: any,
options?: { ttl?: number; tags?: string[] }
): void {
const now = Date.now();
const dataSize = this.estimateDataSize(data);
// Create cache entry
const entry: CacheEntry = {
data,
timestamp: now,
accessCount: 1,
lastAccessed: now,
size: dataSize,
ttl: options?.ttl,
tags: options?.tags || [key.split('_')[0]]
};
// Check if we need to evict entries (LRU strategy)
if (this.cache.size >= this.config.maxSize) {
this.evictLRUEntries(Math.max(1, Math.floor(this.config.maxSize * 0.1)));
}
this.cache.set(key, entry);
this.logger.debug(
`Cached data for ${key} (size: ${dataSize} bytes, TTL: ${entry.ttl || this.config.cacheDuration}ms)`
);
// Trigger prefetch if enabled
if (this.config.enablePrefetch) {
this.scheduleRelatedDataPrefetch(key, data);
}
}
/**
* Clear cache entries matching a pattern
*/
clearPattern(pattern: string): void {
let evictedCount = 0;
for (const key of this.cache.keys()) {
if (key.includes(pattern)) {
this.cache.delete(key);
evictedCount++;
}
}
if (evictedCount > 0) {
this.analytics.evictions += evictedCount;
this.logger.debug(
`Evicted ${evictedCount} cache entries matching pattern: ${pattern}`
);
}
}
/**
* Clear all cached data
*/
clear(): void {
this.cache.clear();
this.resetAnalytics();
}
/**
* Get cache analytics
*/
getAnalytics(): CacheAnalytics {
this.updateAnalytics();
return { ...this.analytics };
}
/**
* Get frequently accessed entries for background refresh
*/
getRefreshCandidates(): Array<[string, CacheEntry]> {
return Array.from(this.cache.entries())
.filter(([key, entry]) => {
const age = Date.now() - entry.timestamp;
const isNearExpiration = age > this.config.cacheDuration * 0.7;
const isFrequentlyAccessed = entry.accessCount >= 3;
return (
isNearExpiration && isFrequentlyAccessed && key.includes('get_tasks')
);
})
.sort((a, b) => b[1].accessCount - a[1].accessCount)
.slice(0, 5);
}
/**
* Update refresh count for analytics
*/
incrementRefreshes(): void {
this.analytics.refreshes++;
}
/**
* Cleanup resources
*/
destroy(): void {
if (this.backgroundRefreshTimer) {
clearInterval(this.backgroundRefreshTimer);
this.backgroundRefreshTimer = undefined;
}
this.clear();
}
private initializeBackgroundRefresh(): void {
if (this.backgroundRefreshTimer) {
clearInterval(this.backgroundRefreshTimer);
}
const interval = this.config.refreshInterval;
this.backgroundRefreshTimer = setInterval(() => {
// Background refresh is handled by the main API class
// This just maintains the timer
}, interval);
this.logger.debug(
`Cache background refresh initialized with ${interval}ms interval`
);
}
private evictLRUEntries(count: number): void {
const entries = Array.from(this.cache.entries())
.sort((a, b) => a[1].lastAccessed - b[1].lastAccessed)
.slice(0, count);
for (const [key] of entries) {
this.cache.delete(key);
this.analytics.evictions++;
}
if (entries.length > 0) {
this.logger.debug(`Evicted ${entries.length} LRU cache entries`);
}
}
private estimateDataSize(data: any): number {
try {
return JSON.stringify(data).length * 2; // Rough estimate
} catch {
return 1000; // Default fallback
}
}
private scheduleRelatedDataPrefetch(key: string, data: any): void {
if (key.includes('get_tasks') && Array.isArray(data)) {
this.logger.debug(
`Scheduled prefetch for ${data.length} tasks related to ${key}`
);
}
}
private resetAnalytics(): void {
this.analytics = {
hits: 0,
misses: 0,
evictions: 0,
refreshes: 0,
totalSize: 0,
averageAccessTime: 0,
hitRate: 0
};
}
private updateAnalytics(): void {
const total = this.analytics.hits + this.analytics.misses;
this.analytics.hitRate = total > 0 ? this.analytics.hits / total : 0;
this.analytics.totalSize = this.cache.size;
if (this.cache.size > 0) {
const totalAccessTime = Array.from(this.cache.values()).reduce(
(sum, entry) => sum + (entry.lastAccessed - entry.timestamp),
0
);
this.analytics.averageAccessTime = totalAccessTime / this.cache.size;
}
}
}

View File

@@ -0,0 +1,471 @@
/**
* TaskMaster API
* Main API class that coordinates all modules
*/
import * as vscode from 'vscode';
import { ExtensionLogger } from '../logger';
import type { MCPClientManager } from '../mcpClient';
import { CacheManager } from './cache/cache-manager';
import { MCPClient } from './mcp-client';
import { TaskTransformer } from './transformers/task-transformer';
import type {
AddSubtaskOptions,
CacheConfig,
GetTasksOptions,
SubtaskData,
TaskMasterApiConfig,
TaskMasterApiResponse,
TaskMasterTask,
TaskUpdate,
UpdateSubtaskOptions,
UpdateTaskOptions,
UpdateTaskStatusOptions
} from './types';
// Re-export types for backward compatibility
export * from './types';
export class TaskMasterApi {
private mcpWrapper: MCPClient;
private cache: CacheManager;
private transformer: TaskTransformer;
private config: TaskMasterApiConfig;
private logger: ExtensionLogger;
private readonly defaultCacheConfig: CacheConfig = {
maxSize: 100,
enableBackgroundRefresh: true,
refreshInterval: 5 * 60 * 1000, // 5 minutes
enableAnalytics: true,
enablePrefetch: true,
compressionEnabled: false,
persistToDisk: false
};
private readonly defaultConfig: TaskMasterApiConfig = {
timeout: 30000,
retryAttempts: 3,
cacheDuration: 5 * 60 * 1000, // 5 minutes
cache: this.defaultCacheConfig
};
constructor(
mcpClient: MCPClientManager,
config?: Partial<TaskMasterApiConfig>
) {
this.logger = ExtensionLogger.getInstance();
// Merge config - ensure cache is always fully defined
const mergedCache: CacheConfig = {
maxSize: config?.cache?.maxSize ?? this.defaultCacheConfig.maxSize,
enableBackgroundRefresh:
config?.cache?.enableBackgroundRefresh ??
this.defaultCacheConfig.enableBackgroundRefresh,
refreshInterval:
config?.cache?.refreshInterval ??
this.defaultCacheConfig.refreshInterval,
enableAnalytics:
config?.cache?.enableAnalytics ??
this.defaultCacheConfig.enableAnalytics,
enablePrefetch:
config?.cache?.enablePrefetch ?? this.defaultCacheConfig.enablePrefetch,
compressionEnabled:
config?.cache?.compressionEnabled ??
this.defaultCacheConfig.compressionEnabled,
persistToDisk:
config?.cache?.persistToDisk ?? this.defaultCacheConfig.persistToDisk
};
this.config = {
...this.defaultConfig,
...config,
cache: mergedCache
};
// Initialize modules
this.mcpWrapper = new MCPClient(mcpClient, this.logger, {
timeout: this.config.timeout,
retryAttempts: this.config.retryAttempts
});
this.cache = new CacheManager(
{ ...mergedCache, cacheDuration: this.config.cacheDuration },
this.logger
);
this.transformer = new TaskTransformer(this.logger);
// Start background refresh if enabled
if (this.config.cache?.enableBackgroundRefresh) {
this.startBackgroundRefresh();
}
this.logger.log('TaskMasterApi: Initialized with modular architecture');
}
/**
* Get tasks from TaskMaster
*/
async getTasks(
options?: GetTasksOptions
): Promise<TaskMasterApiResponse<TaskMasterTask[]>> {
const startTime = Date.now();
const cacheKey = `get_tasks_${JSON.stringify(options || {})}`;
try {
// Check cache first
const cached = this.cache.get(cacheKey);
if (cached) {
return {
success: true,
data: cached,
requestDuration: Date.now() - startTime
};
}
// Prepare MCP tool arguments
const mcpArgs: Record<string, unknown> = {
projectRoot: options?.projectRoot || this.getWorkspaceRoot(),
withSubtasks: options?.withSubtasks ?? true
};
if (options?.status) {
mcpArgs.status = options.status;
}
if (options?.tag) {
mcpArgs.tag = options.tag;
}
this.logger.log('Calling get_tasks with args:', mcpArgs);
// Call MCP tool
const mcpResponse = await this.mcpWrapper.callTool('get_tasks', mcpArgs);
// Transform response
const transformedTasks =
this.transformer.transformMCPTasksResponse(mcpResponse);
// Cache the result
this.cache.set(cacheKey, transformedTasks);
return {
success: true,
data: transformedTasks,
requestDuration: Date.now() - startTime
};
} catch (error) {
this.logger.error('Error getting tasks:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
requestDuration: Date.now() - startTime
};
}
}
/**
* Update task status
*/
async updateTaskStatus(
taskId: string,
status: string,
options?: UpdateTaskStatusOptions
): Promise<TaskMasterApiResponse<boolean>> {
const startTime = Date.now();
try {
const mcpArgs: Record<string, unknown> = {
id: String(taskId),
status: status,
projectRoot: options?.projectRoot || this.getWorkspaceRoot()
};
this.logger.log('Calling set_task_status with args:', mcpArgs);
await this.mcpWrapper.callTool('set_task_status', mcpArgs);
// Clear relevant caches
this.cache.clearPattern('get_tasks');
return {
success: true,
data: true,
requestDuration: Date.now() - startTime
};
} catch (error) {
this.logger.error('Error updating task status:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
requestDuration: Date.now() - startTime
};
}
}
/**
* Update task content
*/
async updateTask(
taskId: string,
updates: TaskUpdate,
options?: UpdateTaskOptions
): Promise<TaskMasterApiResponse<boolean>> {
const startTime = Date.now();
try {
// Build update prompt
const updateFields: string[] = [];
if (updates.title !== undefined) {
updateFields.push(`Title: ${updates.title}`);
}
if (updates.description !== undefined) {
updateFields.push(`Description: ${updates.description}`);
}
if (updates.details !== undefined) {
updateFields.push(`Details: ${updates.details}`);
}
if (updates.priority !== undefined) {
updateFields.push(`Priority: ${updates.priority}`);
}
if (updates.testStrategy !== undefined) {
updateFields.push(`Test Strategy: ${updates.testStrategy}`);
}
if (updates.dependencies !== undefined) {
updateFields.push(`Dependencies: ${updates.dependencies.join(', ')}`);
}
const prompt = `Update task with the following changes:\n${updateFields.join('\n')}`;
const mcpArgs: Record<string, unknown> = {
id: String(taskId),
prompt: prompt,
projectRoot: options?.projectRoot || this.getWorkspaceRoot()
};
if (options?.append !== undefined) {
mcpArgs.append = options.append;
}
if (options?.research !== undefined) {
mcpArgs.research = options.research;
}
this.logger.log('Calling update_task with args:', mcpArgs);
await this.mcpWrapper.callTool('update_task', mcpArgs);
// Clear relevant caches
this.cache.clearPattern('get_tasks');
return {
success: true,
data: true,
requestDuration: Date.now() - startTime
};
} catch (error) {
this.logger.error('Error updating task:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
requestDuration: Date.now() - startTime
};
}
}
/**
* Update subtask content
*/
async updateSubtask(
taskId: string,
prompt: string,
options?: UpdateSubtaskOptions
): Promise<TaskMasterApiResponse<boolean>> {
const startTime = Date.now();
try {
const mcpArgs: Record<string, unknown> = {
id: String(taskId),
prompt: prompt,
projectRoot: options?.projectRoot || this.getWorkspaceRoot()
};
if (options?.research !== undefined) {
mcpArgs.research = options.research;
}
this.logger.log('Calling update_subtask with args:', mcpArgs);
await this.mcpWrapper.callTool('update_subtask', mcpArgs);
// Clear relevant caches
this.cache.clearPattern('get_tasks');
return {
success: true,
data: true,
requestDuration: Date.now() - startTime
};
} catch (error) {
this.logger.error('Error updating subtask:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
requestDuration: Date.now() - startTime
};
}
}
/**
* Add a new subtask
*/
async addSubtask(
parentTaskId: string,
subtaskData: SubtaskData,
options?: AddSubtaskOptions
): Promise<TaskMasterApiResponse<boolean>> {
const startTime = Date.now();
try {
const mcpArgs: Record<string, unknown> = {
id: String(parentTaskId),
title: subtaskData.title,
projectRoot: options?.projectRoot || this.getWorkspaceRoot()
};
if (subtaskData.description) {
mcpArgs.description = subtaskData.description;
}
if (subtaskData.dependencies && subtaskData.dependencies.length > 0) {
mcpArgs.dependencies = subtaskData.dependencies.join(',');
}
if (subtaskData.status) {
mcpArgs.status = subtaskData.status;
}
this.logger.log('Calling add_subtask with args:', mcpArgs);
await this.mcpWrapper.callTool('add_subtask', mcpArgs);
// Clear relevant caches
this.cache.clearPattern('get_tasks');
return {
success: true,
data: true,
requestDuration: Date.now() - startTime
};
} catch (error) {
this.logger.error('Error adding subtask:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
requestDuration: Date.now() - startTime
};
}
}
/**
* Get connection status
*/
getConnectionStatus(): { isConnected: boolean; error?: string } {
const status = this.mcpWrapper.getStatus();
return {
isConnected: status.isRunning,
error: status.error
};
}
/**
* Test connection
*/
async testConnection(): Promise<TaskMasterApiResponse<boolean>> {
const startTime = Date.now();
try {
const isConnected = await this.mcpWrapper.testConnection();
return {
success: true,
data: isConnected,
requestDuration: Date.now() - startTime
};
} catch (error) {
this.logger.error('Connection test failed:', error);
return {
success: false,
error:
error instanceof Error ? error.message : 'Connection test failed',
requestDuration: Date.now() - startTime
};
}
}
/**
* Clear all cached data
*/
clearCache(): void {
this.cache.clear();
}
/**
* Get cache analytics
*/
getCacheAnalytics() {
return this.cache.getAnalytics();
}
/**
* Cleanup resources
*/
destroy(): void {
this.cache.destroy();
this.logger.log('TaskMasterApi: Destroyed and cleaned up resources');
}
/**
* Start background refresh
*/
private startBackgroundRefresh(): void {
const interval = this.config.cache?.refreshInterval || 5 * 60 * 1000;
setInterval(() => {
this.performBackgroundRefresh();
}, interval);
}
/**
* Perform background refresh of frequently accessed cache entries
*/
private async performBackgroundRefresh(): Promise<void> {
if (!this.config.cache?.enableBackgroundRefresh) {
return;
}
this.logger.log('Starting background cache refresh');
const candidates = this.cache.getRefreshCandidates();
let refreshedCount = 0;
for (const [key, entry] of candidates) {
try {
const optionsMatch = key.match(/get_tasks_(.+)/);
if (optionsMatch) {
const options = JSON.parse(optionsMatch[1]);
await this.getTasks(options);
refreshedCount++;
this.cache.incrementRefreshes();
}
} catch (error) {
this.logger.warn(`Background refresh failed for key ${key}:`, error);
}
}
this.logger.log(
`Background refresh completed, refreshed ${refreshedCount} entries`
);
}
/**
* Get workspace root path
*/
private getWorkspaceRoot(): string {
return vscode.workspace.workspaceFolders?.[0]?.uri.fsPath || process.cwd();
}
}

View File

@@ -0,0 +1,98 @@
/**
* MCP Client Wrapper
* Handles MCP tool calls with retry logic
*/
import type { ExtensionLogger } from '../logger';
import type { MCPClientManager } from '../mcpClient';
export class MCPClient {
constructor(
private mcpClient: MCPClientManager,
private logger: ExtensionLogger,
private config: { timeout: number; retryAttempts: number }
) {}
/**
* Call MCP tool with retry logic
*/
async callTool(
toolName: string,
args: Record<string, unknown>
): Promise<any> {
let lastError: Error | null = null;
for (let attempt = 1; attempt <= this.config.retryAttempts; attempt++) {
try {
const rawResponse = await this.mcpClient.callTool(toolName, args);
this.logger.debug(
`Raw MCP response for ${toolName}:`,
JSON.stringify(rawResponse, null, 2)
);
// Parse MCP response format
if (
rawResponse &&
rawResponse.content &&
Array.isArray(rawResponse.content) &&
rawResponse.content[0]
) {
const contentItem = rawResponse.content[0];
if (contentItem.type === 'text' && contentItem.text) {
try {
const parsedData = JSON.parse(contentItem.text);
this.logger.debug(`Parsed MCP data for ${toolName}:`, parsedData);
return parsedData;
} catch (parseError) {
this.logger.error(
`Failed to parse MCP response text for ${toolName}:`,
parseError
);
this.logger.error(`Raw text was:`, contentItem.text);
return rawResponse; // Fall back to original response
}
}
}
// If not in expected format, return as-is
this.logger.warn(
`Unexpected MCP response format for ${toolName}, returning raw response`
);
return rawResponse;
} catch (error) {
lastError = error instanceof Error ? error : new Error('Unknown error');
this.logger.warn(
`Attempt ${attempt}/${this.config.retryAttempts} failed for ${toolName}:`,
lastError.message
);
if (attempt < this.config.retryAttempts) {
// Exponential backoff
const delay = Math.min(1000 * 2 ** (attempt - 1), 5000);
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
}
throw (
lastError ||
new Error(
`Failed to call ${toolName} after ${this.config.retryAttempts} attempts`
)
);
}
/**
* Get connection status
*/
getStatus(): { isRunning: boolean; error?: string } {
return this.mcpClient.getStatus();
}
/**
* Test connection
*/
async testConnection(): Promise<boolean> {
return this.mcpClient.testConnection();
}
}

View File

@@ -0,0 +1,482 @@
/**
* Task Transformer
* Handles transformation and validation of MCP responses to internal format
*/
import type { ExtensionLogger } from '../../logger';
import { MCPTaskResponse, type TaskMasterTask } from '../types';
export class TaskTransformer {
constructor(private logger: ExtensionLogger) {}
/**
* Transform MCP tasks response to internal format
*/
transformMCPTasksResponse(mcpResponse: any): TaskMasterTask[] {
const transformStartTime = Date.now();
try {
// Validate response structure
const validationResult = this.validateMCPResponse(mcpResponse);
if (!validationResult.isValid) {
this.logger.warn(
'MCP response validation failed:',
validationResult.errors
);
return [];
}
// Handle different response structures
let tasks = [];
if (Array.isArray(mcpResponse)) {
tasks = mcpResponse;
} else if (mcpResponse.data) {
if (Array.isArray(mcpResponse.data)) {
tasks = mcpResponse.data;
} else if (
mcpResponse.data.tasks &&
Array.isArray(mcpResponse.data.tasks)
) {
tasks = mcpResponse.data.tasks;
}
} else if (mcpResponse.tasks && Array.isArray(mcpResponse.tasks)) {
tasks = mcpResponse.tasks;
}
this.logger.log(`Transforming ${tasks.length} tasks from MCP response`, {
responseStructure: {
isArray: Array.isArray(mcpResponse),
hasData: !!mcpResponse.data,
dataIsArray: Array.isArray(mcpResponse.data),
hasDataTasks: !!mcpResponse.data?.tasks,
hasTasks: !!mcpResponse.tasks
}
});
const transformedTasks: TaskMasterTask[] = [];
const transformationErrors: Array<{
taskId: any;
error: string;
task: any;
}> = [];
for (let i = 0; i < tasks.length; i++) {
try {
const task = tasks[i];
const transformedTask = this.transformSingleTask(task, i);
if (transformedTask) {
transformedTasks.push(transformedTask);
}
} catch (error) {
const errorMsg =
error instanceof Error
? error.message
: 'Unknown transformation error';
transformationErrors.push({
taskId: tasks[i]?.id || `unknown_${i}`,
error: errorMsg,
task: tasks[i]
});
this.logger.error(
`Failed to transform task at index ${i}:`,
errorMsg,
tasks[i]
);
}
}
// Log transformation summary
const transformDuration = Date.now() - transformStartTime;
this.logger.log(`Transformation completed in ${transformDuration}ms`, {
totalTasks: tasks.length,
successfulTransformations: transformedTasks.length,
errors: transformationErrors.length,
errorSummary: transformationErrors.map((e) => ({
id: e.taskId,
error: e.error
}))
});
return transformedTasks;
} catch (error) {
this.logger.error(
'Critical error during response transformation:',
error
);
return [];
}
}
/**
* Validate MCP response structure
*/
private validateMCPResponse(mcpResponse: any): {
isValid: boolean;
errors: string[];
} {
const errors: string[] = [];
if (!mcpResponse) {
errors.push('Response is null or undefined');
return { isValid: false, errors };
}
// Arrays are valid responses
if (Array.isArray(mcpResponse)) {
return { isValid: true, errors };
}
if (typeof mcpResponse !== 'object') {
errors.push('Response is not an object or array');
return { isValid: false, errors };
}
if (mcpResponse.error) {
errors.push(`MCP error: ${mcpResponse.error}`);
}
// Check for valid task structure
const hasValidTasksStructure =
(mcpResponse.data && Array.isArray(mcpResponse.data)) ||
(mcpResponse.data?.tasks && Array.isArray(mcpResponse.data.tasks)) ||
(mcpResponse.tasks && Array.isArray(mcpResponse.tasks));
if (!hasValidTasksStructure && !mcpResponse.error) {
errors.push('Response does not contain a valid tasks array structure');
}
return { isValid: errors.length === 0, errors };
}
/**
* Transform a single task with validation
*/
private transformSingleTask(task: any, index: number): TaskMasterTask | null {
if (!task || typeof task !== 'object') {
this.logger.warn(`Task at index ${index} is not a valid object:`, task);
return null;
}
try {
// Validate required fields
const taskId = this.validateAndNormalizeId(task.id, index);
const title =
this.validateAndNormalizeString(
task.title,
'Untitled Task',
`title for task ${taskId}`
) || 'Untitled Task';
const description =
this.validateAndNormalizeString(
task.description,
'',
`description for task ${taskId}`
) || '';
// Normalize and validate status/priority
const status = this.normalizeStatus(task.status);
const priority = this.normalizePriority(task.priority);
// Handle optional fields
const details = this.validateAndNormalizeString(
task.details,
undefined,
`details for task ${taskId}`
);
const testStrategy = this.validateAndNormalizeString(
task.testStrategy,
undefined,
`testStrategy for task ${taskId}`
);
// Handle complexity score
const complexityScore =
typeof task.complexityScore === 'number'
? task.complexityScore
: undefined;
// Transform dependencies
const dependencies = this.transformDependencies(
task.dependencies,
taskId
);
// Transform subtasks
const subtasks = this.transformSubtasks(task.subtasks, taskId);
const transformedTask: TaskMasterTask = {
id: taskId,
title,
description,
status,
priority,
details,
testStrategy,
complexityScore,
dependencies,
subtasks
};
// Log successful transformation for complex tasks
if (
(subtasks && subtasks.length > 0) ||
dependencies.length > 0 ||
complexityScore !== undefined
) {
this.logger.debug(`Successfully transformed complex task ${taskId}:`, {
subtaskCount: subtasks?.length ?? 0,
dependencyCount: dependencies.length,
status,
priority,
complexityScore
});
}
return transformedTask;
} catch (error) {
this.logger.error(
`Error transforming task at index ${index}:`,
error,
task
);
return null;
}
}
private validateAndNormalizeId(id: any, fallbackIndex: number): string {
if (id === null || id === undefined) {
const generatedId = `generated_${fallbackIndex}_${Date.now()}`;
this.logger.warn(`Task missing ID, generated: ${generatedId}`);
return generatedId;
}
const stringId = String(id).trim();
if (stringId === '') {
const generatedId = `empty_${fallbackIndex}_${Date.now()}`;
this.logger.warn(`Task has empty ID, generated: ${generatedId}`);
return generatedId;
}
return stringId;
}
private validateAndNormalizeString(
value: any,
defaultValue: string | undefined,
fieldName: string
): string | undefined {
if (value === null || value === undefined) {
return defaultValue;
}
if (typeof value !== 'string') {
this.logger.warn(`${fieldName} is not a string, converting:`, value);
return String(value).trim() || defaultValue;
}
const trimmed = value.trim();
if (trimmed === '' && defaultValue !== undefined) {
return defaultValue;
}
return trimmed || defaultValue;
}
private transformDependencies(dependencies: any, taskId: string): string[] {
if (!dependencies) {
return [];
}
if (!Array.isArray(dependencies)) {
this.logger.warn(
`Dependencies for task ${taskId} is not an array:`,
dependencies
);
return [];
}
const validDependencies: string[] = [];
for (let i = 0; i < dependencies.length; i++) {
const dep = dependencies[i];
if (dep === null || dep === undefined) {
this.logger.warn(`Null dependency at index ${i} for task ${taskId}`);
continue;
}
const stringDep = String(dep).trim();
if (stringDep === '') {
this.logger.warn(`Empty dependency at index ${i} for task ${taskId}`);
continue;
}
// Check for self-dependency
if (stringDep === taskId) {
this.logger.warn(
`Self-dependency detected for task ${taskId}, skipping`
);
continue;
}
validDependencies.push(stringDep);
}
return validDependencies;
}
private transformSubtasks(
subtasks: any,
parentTaskId: string
): TaskMasterTask['subtasks'] {
if (!subtasks) {
return [];
}
if (!Array.isArray(subtasks)) {
this.logger.warn(
`Subtasks for task ${parentTaskId} is not an array:`,
subtasks
);
return [];
}
const validSubtasks = [];
for (let i = 0; i < subtasks.length; i++) {
try {
const subtask = subtasks[i];
if (!subtask || typeof subtask !== 'object') {
this.logger.warn(
`Invalid subtask at index ${i} for task ${parentTaskId}:`,
subtask
);
continue;
}
const transformedSubtask = {
id: typeof subtask.id === 'number' ? subtask.id : i + 1,
title:
this.validateAndNormalizeString(
subtask.title,
`Subtask ${i + 1}`,
`subtask title for parent ${parentTaskId}`
) || `Subtask ${i + 1}`,
description: this.validateAndNormalizeString(
subtask.description,
undefined,
`subtask description for parent ${parentTaskId}`
),
status:
this.validateAndNormalizeString(
subtask.status,
'pending',
`subtask status for parent ${parentTaskId}`
) || 'pending',
details: this.validateAndNormalizeString(
subtask.details,
undefined,
`subtask details for parent ${parentTaskId}`
),
testStrategy: this.validateAndNormalizeString(
subtask.testStrategy,
undefined,
`subtask testStrategy for parent ${parentTaskId}`
),
dependencies: subtask.dependencies || []
};
validSubtasks.push(transformedSubtask);
} catch (error) {
this.logger.error(
`Error transforming subtask at index ${i} for task ${parentTaskId}:`,
error
);
}
}
return validSubtasks;
}
private normalizeStatus(status: string): TaskMasterTask['status'] {
const original = status;
const normalized = status?.toLowerCase()?.trim() || 'pending';
const statusMap: Record<string, TaskMasterTask['status']> = {
pending: 'pending',
'in-progress': 'in-progress',
in_progress: 'in-progress',
inprogress: 'in-progress',
progress: 'in-progress',
working: 'in-progress',
active: 'in-progress',
review: 'review',
reviewing: 'review',
'in-review': 'review',
in_review: 'review',
done: 'done',
completed: 'done',
complete: 'done',
finished: 'done',
closed: 'done',
resolved: 'done',
blocked: 'deferred',
block: 'deferred',
stuck: 'deferred',
waiting: 'deferred',
cancelled: 'cancelled',
canceled: 'cancelled',
cancel: 'cancelled',
abandoned: 'cancelled',
deferred: 'deferred',
defer: 'deferred',
postponed: 'deferred',
later: 'deferred'
};
const result = statusMap[normalized] || 'pending';
if (original && original !== result) {
this.logger.debug(`Normalized status '${original}' -> '${result}'`);
}
return result;
}
private normalizePriority(priority: string): TaskMasterTask['priority'] {
const original = priority;
const normalized = priority?.toLowerCase()?.trim() || 'medium';
let result: TaskMasterTask['priority'] = 'medium';
if (
normalized.includes('high') ||
normalized.includes('urgent') ||
normalized.includes('critical') ||
normalized.includes('important') ||
normalized === 'h' ||
normalized === '3'
) {
result = 'high';
} else if (
normalized.includes('low') ||
normalized.includes('minor') ||
normalized.includes('trivial') ||
normalized === 'l' ||
normalized === '1'
) {
result = 'low';
} else if (
normalized.includes('medium') ||
normalized.includes('normal') ||
normalized.includes('standard') ||
normalized === 'm' ||
normalized === '2'
) {
result = 'medium';
}
if (original && original !== result) {
this.logger.debug(`Normalized priority '${original}' -> '${result}'`);
}
return result;
}
}

View File

@@ -0,0 +1,157 @@
/**
* TaskMaster API Types
* All type definitions for the TaskMaster API
*/
// MCP Response Types
export interface MCPTaskResponse {
data?: {
tasks?: Array<{
id: number | string;
title: string;
description: string;
status: string;
priority: string;
details?: string;
testStrategy?: string;
dependencies?: Array<number | string>;
complexityScore?: number;
subtasks?: Array<{
id: number;
title: string;
description?: string;
status: string;
details?: string;
dependencies?: Array<number | string>;
}>;
}>;
tag?: {
currentTag: string;
availableTags: string[];
};
};
version?: {
version: string;
name: string;
};
error?: string;
}
// Internal Task Interface
export interface TaskMasterTask {
id: string;
title: string;
description: string;
status:
| 'pending'
| 'in-progress'
| 'review'
| 'done'
| 'deferred'
| 'cancelled';
priority: 'high' | 'medium' | 'low';
details?: string;
testStrategy?: string;
dependencies?: string[];
complexityScore?: number;
subtasks?: Array<{
id: number;
title: string;
description?: string;
status: string;
details?: string;
testStrategy?: string;
dependencies?: Array<number | string>;
}>;
}
// API Response Wrapper
export interface TaskMasterApiResponse<T = any> {
success: boolean;
data?: T;
error?: string;
requestDuration?: number;
}
// API Configuration
export interface TaskMasterApiConfig {
timeout: number;
retryAttempts: number;
cacheDuration: number;
projectRoot?: string;
cache?: CacheConfig;
}
export interface CacheConfig {
maxSize: number;
enableBackgroundRefresh: boolean;
refreshInterval: number;
enableAnalytics: boolean;
enablePrefetch: boolean;
compressionEnabled: boolean;
persistToDisk: boolean;
}
// Cache Types
export interface CacheEntry {
data: any;
timestamp: number;
accessCount: number;
lastAccessed: number;
size: number;
ttl?: number;
tags: string[];
}
export interface CacheAnalytics {
hits: number;
misses: number;
evictions: number;
refreshes: number;
totalSize: number;
averageAccessTime: number;
hitRate: number;
}
// Method Options
export interface GetTasksOptions {
status?: string;
withSubtasks?: boolean;
tag?: string;
projectRoot?: string;
}
export interface UpdateTaskStatusOptions {
projectRoot?: string;
}
export interface UpdateTaskOptions {
projectRoot?: string;
append?: boolean;
research?: boolean;
}
export interface UpdateSubtaskOptions {
projectRoot?: string;
research?: boolean;
}
export interface AddSubtaskOptions {
projectRoot?: string;
}
export interface TaskUpdate {
title?: string;
description?: string;
details?: string;
priority?: 'high' | 'medium' | 'low';
testStrategy?: string;
dependencies?: string[];
}
export interface SubtaskData {
title: string;
description?: string;
dependencies?: string[];
status?: string;
}