Integration with TaskMaster AI

This commit is contained in:
simos
2025-08-28 12:11:42 +03:00
parent c1e7bb6c10
commit 75e8161213
33 changed files with 7856 additions and 111 deletions

View File

@@ -43,6 +43,8 @@ import gitRoutes from './routes/git.js';
import authRoutes from './routes/auth.js';
import mcpRoutes from './routes/mcp.js';
import cursorRoutes from './routes/cursor.js';
import taskmasterRoutes from './routes/taskmaster.js';
import mcpUtilsRoutes from './routes/mcp-utils.js';
import { initializeDatabase } from './database/db.js';
import { validateApiKey, authenticateToken, authenticateWebSocket } from './middleware/auth.js';
@@ -162,6 +164,9 @@ const wss = new WebSocketServer({
}
});
// Make WebSocket server available to routes
app.locals.wss = wss;
app.use(cors());
app.use(express.json());
@@ -180,6 +185,12 @@ app.use('/api/mcp', authenticateToken, mcpRoutes);
// Cursor API Routes (protected)
app.use('/api/cursor', authenticateToken, cursorRoutes);
// TaskMaster API Routes (protected)
app.use('/api/taskmaster', authenticateToken, taskmasterRoutes);
// MCP utilities
app.use('/api/mcp-utils', authenticateToken, mcpUtilsRoutes);
// Static files served after API routes
app.use(express.static(path.join(__dirname, '../dist')));
@@ -547,16 +558,26 @@ function handleShellConnection(ws) {
const sessionId = data.sessionId;
const hasSession = data.hasSession;
const provider = data.provider || 'claude';
const initialCommand = data.initialCommand;
const isPlainShell = data.isPlainShell || (!!initialCommand && !hasSession) || provider === 'plain-shell';
console.log('🚀 Starting shell in:', projectPath);
console.log('📋 Session info:', hasSession ? `Resume session ${sessionId}` : 'New session');
console.log('🤖 Provider:', provider);
console.log('📋 Session info:', hasSession ? `Resume session ${sessionId}` : (isPlainShell ? 'Plain shell mode' : 'New session'));
console.log('🤖 Provider:', isPlainShell ? 'plain-shell' : provider);
if (initialCommand) {
console.log('⚡ Initial command:', initialCommand);
}
// First send a welcome message
const providerName = provider === 'cursor' ? 'Cursor' : 'Claude';
const welcomeMsg = hasSession ?
`\x1b[36mResuming ${providerName} session ${sessionId} in: ${projectPath}\x1b[0m\r\n` :
`\x1b[36mStarting new ${providerName} session in: ${projectPath}\x1b[0m\r\n`;
let welcomeMsg;
if (isPlainShell) {
welcomeMsg = `\x1b[36mStarting terminal in: ${projectPath}\x1b[0m\r\n`;
} else {
const providerName = provider === 'cursor' ? 'Cursor' : 'Claude';
welcomeMsg = hasSession ?
`\x1b[36mResuming ${providerName} session ${sessionId} in: ${projectPath}\x1b[0m\r\n` :
`\x1b[36mStarting new ${providerName} session in: ${projectPath}\x1b[0m\r\n`;
}
ws.send(JSON.stringify({
type: 'output',
@@ -566,7 +587,14 @@ function handleShellConnection(ws) {
try {
// Prepare the shell command adapted to the platform and provider
let shellCommand;
if (provider === 'cursor') {
if (isPlainShell) {
// Plain shell mode - just run the initial command in the project directory
if (os.platform() === 'win32') {
shellCommand = `Set-Location -Path "${projectPath}"; ${initialCommand}`;
} else {
shellCommand = `cd "${projectPath}" && ${initialCommand}`;
}
} else if (provider === 'cursor') {
// Use cursor-agent command
if (os.platform() === 'win32') {
if (hasSession && sessionId) {
@@ -582,19 +610,20 @@ function handleShellConnection(ws) {
}
}
} else {
// Use claude command (default)
// Use claude command (default) or initialCommand if provided
const command = initialCommand || 'claude';
if (os.platform() === 'win32') {
if (hasSession && sessionId) {
// Try to resume session, but with fallback to new session if it fails
shellCommand = `Set-Location -Path "${projectPath}"; claude --resume ${sessionId}; if ($LASTEXITCODE -ne 0) { claude }`;
} else {
shellCommand = `Set-Location -Path "${projectPath}"; claude`;
shellCommand = `Set-Location -Path "${projectPath}"; ${command}`;
}
} else {
if (hasSession && sessionId) {
shellCommand = `cd "${projectPath}" && claude --resume ${sessionId} || claude`;
} else {
shellCommand = `cd "${projectPath}" && claude`;
shellCommand = `cd "${projectPath}" && ${command}`;
}
}
}

View File

@@ -66,6 +66,134 @@ import sqlite3 from 'sqlite3';
import { open } from 'sqlite';
import os from 'os';
// Import TaskMaster detection functions
async function detectTaskMasterFolder(projectPath) {
try {
const taskMasterPath = path.join(projectPath, '.taskmaster');
// Check if .taskmaster directory exists
try {
const stats = await fs.stat(taskMasterPath);
if (!stats.isDirectory()) {
return {
hasTaskmaster: false,
reason: '.taskmaster exists but is not a directory'
};
}
} catch (error) {
if (error.code === 'ENOENT') {
return {
hasTaskmaster: false,
reason: '.taskmaster directory not found'
};
}
throw error;
}
// Check for key TaskMaster files
const keyFiles = [
'tasks/tasks.json',
'config.json'
];
const fileStatus = {};
let hasEssentialFiles = true;
for (const file of keyFiles) {
const filePath = path.join(taskMasterPath, file);
try {
await fs.access(filePath);
fileStatus[file] = true;
} catch (error) {
fileStatus[file] = false;
if (file === 'tasks/tasks.json') {
hasEssentialFiles = false;
}
}
}
// Parse tasks.json if it exists for metadata
let taskMetadata = null;
if (fileStatus['tasks/tasks.json']) {
try {
const tasksPath = path.join(taskMasterPath, 'tasks/tasks.json');
const tasksContent = await fs.readFile(tasksPath, 'utf8');
const tasksData = JSON.parse(tasksContent);
// Handle both tagged and legacy formats
let tasks = [];
if (tasksData.tasks) {
// Legacy format
tasks = tasksData.tasks;
} else {
// Tagged format - get tasks from all tags
Object.values(tasksData).forEach(tagData => {
if (tagData.tasks) {
tasks = tasks.concat(tagData.tasks);
}
});
}
// Calculate task statistics
const stats = tasks.reduce((acc, task) => {
acc.total++;
acc[task.status] = (acc[task.status] || 0) + 1;
// Count subtasks
if (task.subtasks) {
task.subtasks.forEach(subtask => {
acc.subtotalTasks++;
acc.subtasks = acc.subtasks || {};
acc.subtasks[subtask.status] = (acc.subtasks[subtask.status] || 0) + 1;
});
}
return acc;
}, {
total: 0,
subtotalTasks: 0,
pending: 0,
'in-progress': 0,
done: 0,
review: 0,
deferred: 0,
cancelled: 0,
subtasks: {}
});
taskMetadata = {
taskCount: stats.total,
subtaskCount: stats.subtotalTasks,
completed: stats.done || 0,
pending: stats.pending || 0,
inProgress: stats['in-progress'] || 0,
review: stats.review || 0,
completionPercentage: stats.total > 0 ? Math.round((stats.done / stats.total) * 100) : 0,
lastModified: (await fs.stat(tasksPath)).mtime.toISOString()
};
} catch (parseError) {
console.warn('Failed to parse tasks.json:', parseError.message);
taskMetadata = { error: 'Failed to parse tasks.json' };
}
}
return {
hasTaskmaster: true,
hasEssentialFiles,
files: fileStatus,
metadata: taskMetadata,
path: taskMasterPath
};
} catch (error) {
console.error('Error detecting TaskMaster folder:', error);
return {
hasTaskmaster: false,
reason: `Error checking directory: ${error.message}`
};
}
}
// Cache for extracted project directories
const projectDirectoryCache = new Map();
@@ -298,6 +426,25 @@ async function getProjects() {
project.cursorSessions = [];
}
// Add TaskMaster detection
try {
const taskMasterResult = await detectTaskMasterFolder(actualProjectDir);
project.taskmaster = {
hasTaskmaster: taskMasterResult.hasTaskmaster,
hasEssentialFiles: taskMasterResult.hasEssentialFiles,
metadata: taskMasterResult.metadata,
status: taskMasterResult.hasTaskmaster && taskMasterResult.hasEssentialFiles ? 'configured' : 'not-configured'
};
} catch (e) {
console.warn(`Could not detect TaskMaster for project ${entry.name}:`, e.message);
project.taskmaster = {
hasTaskmaster: false,
hasEssentialFiles: false,
metadata: null,
status: 'error'
};
}
projects.push(project);
}
}
@@ -341,6 +488,32 @@ async function getProjects() {
console.warn(`Could not load Cursor sessions for manual project ${projectName}:`, e.message);
}
// Add TaskMaster detection for manual projects
try {
const taskMasterResult = await detectTaskMasterFolder(actualProjectDir);
// Determine TaskMaster status
let taskMasterStatus = 'not-configured';
if (taskMasterResult.hasTaskmaster && taskMasterResult.hasEssentialFiles) {
taskMasterStatus = 'taskmaster-only'; // We don't check MCP for manual projects in bulk
}
project.taskmaster = {
status: taskMasterStatus,
hasTaskmaster: taskMasterResult.hasTaskmaster,
hasEssentialFiles: taskMasterResult.hasEssentialFiles,
metadata: taskMasterResult.metadata
};
} catch (error) {
console.warn(`TaskMaster detection failed for manual project ${projectName}:`, error.message);
project.taskmaster = {
status: 'error',
hasTaskmaster: false,
hasEssentialFiles: false,
error: error.message
};
}
projects.push(project);
}
}

View File

@@ -0,0 +1,48 @@
/**
* MCP UTILITIES API ROUTES
* ========================
*
* API endpoints for MCP server detection and configuration utilities.
* These endpoints expose centralized MCP detection functionality.
*/
import express from 'express';
import { detectTaskMasterMCPServer, getAllMCPServers } from '../utils/mcp-detector.js';
const router = express.Router();
/**
* GET /api/mcp-utils/taskmaster-server
* Check if TaskMaster MCP server is configured
*/
router.get('/taskmaster-server', async (req, res) => {
try {
const result = await detectTaskMasterMCPServer();
res.json(result);
} catch (error) {
console.error('TaskMaster MCP detection error:', error);
res.status(500).json({
error: 'Failed to detect TaskMaster MCP server',
message: error.message
});
}
});
/**
* GET /api/mcp-utils/all-servers
* Get all configured MCP servers
*/
router.get('/all-servers', async (req, res) => {
try {
const result = await getAllMCPServers();
res.json(result);
} catch (error) {
console.error('MCP servers detection error:', error);
res.status(500).json({
error: 'Failed to get MCP servers',
message: error.message
});
}
});
export default router;

1971
server/routes/taskmaster.js Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,198 @@
/**
* MCP SERVER DETECTION UTILITY
* ============================
*
* Centralized utility for detecting MCP server configurations.
* Used across TaskMaster integration and other MCP-dependent features.
*/
import { promises as fsPromises } from 'fs';
import path from 'path';
import os from 'os';
/**
* Check if task-master-ai MCP server is configured
* Reads directly from Claude configuration files like claude-cli.js does
* @returns {Promise<Object>} MCP detection result
*/
export async function detectTaskMasterMCPServer() {
try {
// Read Claude configuration files directly (same logic as mcp.js)
const homeDir = os.homedir();
const configPaths = [
path.join(homeDir, '.claude.json'),
path.join(homeDir, '.claude', 'settings.json')
];
let configData = null;
let configPath = null;
// Try to read from either config file
for (const filepath of configPaths) {
try {
const fileContent = await fsPromises.readFile(filepath, 'utf8');
configData = JSON.parse(fileContent);
configPath = filepath;
break;
} catch (error) {
// File doesn't exist or is not valid JSON, try next
continue;
}
}
if (!configData) {
return {
hasMCPServer: false,
reason: 'No Claude configuration file found',
hasConfig: false
};
}
// Look for task-master-ai in user-scoped MCP servers
let taskMasterServer = null;
if (configData.mcpServers && typeof configData.mcpServers === 'object') {
const serverEntry = Object.entries(configData.mcpServers).find(([name, config]) =>
name === 'task-master-ai' ||
name.includes('task-master') ||
(config && config.command && config.command.includes('task-master'))
);
if (serverEntry) {
const [name, config] = serverEntry;
taskMasterServer = {
name,
scope: 'user',
config,
type: config.command ? 'stdio' : (config.url ? 'http' : 'unknown')
};
}
}
// Also check project-specific MCP servers if not found globally
if (!taskMasterServer && configData.projects) {
for (const [projectPath, projectConfig] of Object.entries(configData.projects)) {
if (projectConfig.mcpServers && typeof projectConfig.mcpServers === 'object') {
const serverEntry = Object.entries(projectConfig.mcpServers).find(([name, config]) =>
name === 'task-master-ai' ||
name.includes('task-master') ||
(config && config.command && config.command.includes('task-master'))
);
if (serverEntry) {
const [name, config] = serverEntry;
taskMasterServer = {
name,
scope: 'local',
projectPath,
config,
type: config.command ? 'stdio' : (config.url ? 'http' : 'unknown')
};
break;
}
}
}
}
if (taskMasterServer) {
const isValid = !!(taskMasterServer.config &&
(taskMasterServer.config.command || taskMasterServer.config.url));
const hasEnvVars = !!(taskMasterServer.config &&
taskMasterServer.config.env &&
Object.keys(taskMasterServer.config.env).length > 0);
return {
hasMCPServer: true,
isConfigured: isValid,
hasApiKeys: hasEnvVars,
scope: taskMasterServer.scope,
config: {
command: taskMasterServer.config?.command,
args: taskMasterServer.config?.args || [],
url: taskMasterServer.config?.url,
envVars: hasEnvVars ? Object.keys(taskMasterServer.config.env) : [],
type: taskMasterServer.type
}
};
} else {
// Get list of available servers for debugging
const availableServers = [];
if (configData.mcpServers) {
availableServers.push(...Object.keys(configData.mcpServers));
}
if (configData.projects) {
for (const projectConfig of Object.values(configData.projects)) {
if (projectConfig.mcpServers) {
availableServers.push(...Object.keys(projectConfig.mcpServers).map(name => `local:${name}`));
}
}
}
return {
hasMCPServer: false,
reason: 'task-master-ai not found in configured MCP servers',
hasConfig: true,
configPath,
availableServers
};
}
} catch (error) {
console.error('Error detecting MCP server config:', error);
return {
hasMCPServer: false,
reason: `Error checking MCP config: ${error.message}`,
hasConfig: false
};
}
}
/**
* Get all configured MCP servers (not just TaskMaster)
* @returns {Promise<Object>} All MCP servers configuration
*/
export async function getAllMCPServers() {
try {
const homeDir = os.homedir();
const configPaths = [
path.join(homeDir, '.claude.json'),
path.join(homeDir, '.claude', 'settings.json')
];
let configData = null;
let configPath = null;
// Try to read from either config file
for (const filepath of configPaths) {
try {
const fileContent = await fsPromises.readFile(filepath, 'utf8');
configData = JSON.parse(fileContent);
configPath = filepath;
break;
} catch (error) {
continue;
}
}
if (!configData) {
return {
hasConfig: false,
servers: {},
projectServers: {}
};
}
return {
hasConfig: true,
configPath,
servers: configData.mcpServers || {},
projectServers: configData.projects || {}
};
} catch (error) {
console.error('Error getting all MCP servers:', error);
return {
hasConfig: false,
error: error.message,
servers: {},
projectServers: {}
};
}
}

View File

@@ -0,0 +1,129 @@
/**
* TASKMASTER WEBSOCKET UTILITIES
* ==============================
*
* Utilities for broadcasting TaskMaster state changes via WebSocket.
* Integrates with the existing WebSocket system to provide real-time updates.
*/
/**
* Broadcast TaskMaster project update to all connected clients
* @param {WebSocket.Server} wss - WebSocket server instance
* @param {string} projectName - Name of the updated project
* @param {Object} taskMasterData - Updated TaskMaster data
*/
export function broadcastTaskMasterProjectUpdate(wss, projectName, taskMasterData) {
if (!wss || !projectName) {
console.warn('TaskMaster WebSocket broadcast: Missing wss or projectName');
return;
}
const message = {
type: 'taskmaster-project-updated',
projectName,
taskMasterData,
timestamp: new Date().toISOString()
};
wss.clients.forEach((client) => {
if (client.readyState === 1) { // WebSocket.OPEN
try {
client.send(JSON.stringify(message));
} catch (error) {
console.error('Error sending TaskMaster project update:', error);
}
}
});
}
/**
* Broadcast TaskMaster tasks update for a specific project
* @param {WebSocket.Server} wss - WebSocket server instance
* @param {string} projectName - Name of the project with updated tasks
* @param {Object} tasksData - Updated tasks data
*/
export function broadcastTaskMasterTasksUpdate(wss, projectName, tasksData) {
if (!wss || !projectName) {
console.warn('TaskMaster WebSocket broadcast: Missing wss or projectName');
return;
}
const message = {
type: 'taskmaster-tasks-updated',
projectName,
tasksData,
timestamp: new Date().toISOString()
};
wss.clients.forEach((client) => {
if (client.readyState === 1) { // WebSocket.OPEN
try {
client.send(JSON.stringify(message));
} catch (error) {
console.error('Error sending TaskMaster tasks update:', error);
}
}
});
}
/**
* Broadcast MCP server status change
* @param {WebSocket.Server} wss - WebSocket server instance
* @param {Object} mcpStatus - Updated MCP server status
*/
export function broadcastMCPStatusChange(wss, mcpStatus) {
if (!wss) {
console.warn('TaskMaster WebSocket broadcast: Missing wss');
return;
}
const message = {
type: 'taskmaster-mcp-status-changed',
mcpStatus,
timestamp: new Date().toISOString()
};
wss.clients.forEach((client) => {
if (client.readyState === 1) { // WebSocket.OPEN
try {
client.send(JSON.stringify(message));
} catch (error) {
console.error('Error sending TaskMaster MCP status update:', error);
}
}
});
}
/**
* Broadcast general TaskMaster update notification
* @param {WebSocket.Server} wss - WebSocket server instance
* @param {string} updateType - Type of update (e.g., 'initialization', 'configuration')
* @param {Object} data - Additional data about the update
*/
export function broadcastTaskMasterUpdate(wss, updateType, data = {}) {
if (!wss || !updateType) {
console.warn('TaskMaster WebSocket broadcast: Missing wss or updateType');
return;
}
const message = {
type: 'taskmaster-update',
updateType,
data,
timestamp: new Date().toISOString()
};
wss.clients.forEach((client) => {
if (client.readyState === 1) { // WebSocket.OPEN
try {
client.send(JSON.stringify(message));
} catch (error) {
console.error('Error sending TaskMaster update:', error);
}
}
});
}