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:
514
apps/extension/src/utils/configManager.ts
Normal file
514
apps/extension/src/utils/configManager.ts
Normal 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();
|
||||
}
|
||||
387
apps/extension/src/utils/connectionManager.ts
Normal file
387
apps/extension/src/utils/connectionManager.ts
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
858
apps/extension/src/utils/errorHandler.ts
Normal file
858
apps/extension/src/utils/errorHandler.ts
Normal 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 };
|
||||
}
|
||||
34
apps/extension/src/utils/event-emitter.ts
Normal file
34
apps/extension/src/utils/event-emitter.ts
Normal 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
104
apps/extension/src/utils/logger.ts
Normal file
104
apps/extension/src/utils/logger.ts
Normal 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();
|
||||
390
apps/extension/src/utils/mcpClient.ts
Normal file
390
apps/extension/src/utils/mcpClient.ts
Normal 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
|
||||
};
|
||||
}
|
||||
463
apps/extension/src/utils/notificationPreferences.ts
Normal file
463
apps/extension/src/utils/notificationPreferences.ts
Normal 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
|
||||
);
|
||||
}
|
||||
253
apps/extension/src/utils/task-master-api/cache/cache-manager.ts
vendored
Normal file
253
apps/extension/src/utils/task-master-api/cache/cache-manager.ts
vendored
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
471
apps/extension/src/utils/task-master-api/index.ts
Normal file
471
apps/extension/src/utils/task-master-api/index.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
98
apps/extension/src/utils/task-master-api/mcp-client.ts
Normal file
98
apps/extension/src/utils/task-master-api/mcp-client.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
157
apps/extension/src/utils/task-master-api/types/index.ts
Normal file
157
apps/extension/src/utils/task-master-api/types/index.ts
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user