vibe-coder-mcp
Version:
Production-ready MCP server with complete agent integration, multi-transport support, and comprehensive development automation tools for AI-assisted workflows.
775 lines (774 loc) • 34.7 kB
JavaScript
import { getStorageManager } from '../storage/storage-manager.js';
import { getVibeTaskManagerConfig } from '../../utils/config-loader.js';
import { getIdGenerator } from '../../utils/id-generator.js';
import logger from '../../../../logger.js';
export class ProjectOperations {
static instance;
constructor() { }
static getInstance() {
if (!ProjectOperations.instance) {
ProjectOperations.instance = new ProjectOperations();
}
return ProjectOperations.instance;
}
resolveProjectRootPath(providedPath) {
if (providedPath && providedPath !== '/' && providedPath.length > 1) {
return providedPath;
}
const envProjectPath = process.env.VIBE_TASK_MANAGER_READ_DIR;
if (envProjectPath && envProjectPath !== '/' && envProjectPath.length > 1) {
return envProjectPath;
}
const cwd = process.cwd();
logger.debug({ providedPath, envProjectPath, cwd }, 'Project root path resolution completed');
return cwd;
}
async createProject(params, createdBy = 'system') {
try {
logger.info({ projectName: params.name, createdBy }, 'Creating new project');
const validationResult = this.validateCreateParams(params);
if (!validationResult.valid) {
return {
success: false,
error: `Project creation validation failed: ${validationResult.errors.join(', ')}`,
metadata: {
filePath: 'project-operations',
operation: 'create_project',
timestamp: new Date()
}
};
}
const config = await getVibeTaskManagerConfig();
if (!config) {
return {
success: false,
error: 'Failed to load task manager configuration',
metadata: {
filePath: 'project-operations',
operation: 'create_project',
timestamp: new Date()
}
};
}
const idGenerator = getIdGenerator();
const idResult = await idGenerator.generateProjectId(params.name);
if (!idResult.success) {
return {
success: false,
error: `Failed to generate project ID: ${idResult.error}`,
metadata: {
filePath: 'project-operations',
operation: 'create_project',
timestamp: new Date()
}
};
}
const projectId = idResult.id;
const agentResult = await this.determineOptimalAgentConfig(params, config);
const defaultConfig = {
maxConcurrentTasks: config.taskManager.maxConcurrentTasks,
defaultTaskTemplate: config.taskManager.defaultTaskTemplate,
agentConfig: {
maxAgents: agentResult.maxAgents,
defaultAgent: agentResult.defaultAgent,
agentCapabilities: agentResult.agentCapabilities
},
performanceTargets: {
maxResponseTime: config.taskManager.performanceTargets.maxResponseTime,
maxMemoryUsage: config.taskManager.performanceTargets.maxMemoryUsage,
minTestCoverage: config.taskManager.performanceTargets.minTestCoverage
},
integrationSettings: {
codeMapEnabled: true,
researchEnabled: true,
notificationsEnabled: true
},
fileSystemSettings: {
cacheSize: 100,
cacheTTL: 3600,
backupEnabled: true
}
};
const projectConfig = {
...defaultConfig,
...params.config,
agentConfig: {
...defaultConfig.agentConfig,
...params.config?.agentConfig
},
performanceTargets: {
...defaultConfig.performanceTargets,
...params.config?.performanceTargets
},
integrationSettings: {
...defaultConfig.integrationSettings,
...params.config?.integrationSettings
},
fileSystemSettings: {
...defaultConfig.fileSystemSettings,
...params.config?.fileSystemSettings
}
};
const finalTechStack = {
languages: params.techStack?.languages?.length ? params.techStack.languages :
(agentResult.detectedTechStack?.languages || []),
frameworks: params.techStack?.frameworks?.length ? params.techStack.frameworks :
(agentResult.detectedTechStack?.frameworks || []),
tools: params.techStack?.tools?.length ? params.techStack.tools :
(agentResult.detectedTechStack?.tools || [])
};
const project = {
id: projectId,
name: params.name,
description: params.description,
status: 'pending',
config: projectConfig,
epicIds: [],
rootPath: this.resolveProjectRootPath(params.rootPath),
techStack: finalTechStack,
metadata: {
createdAt: new Date(),
updatedAt: new Date(),
createdBy,
tags: params.tags || [],
version: '1.0.0'
}
};
const storageManager = await getStorageManager();
const createResult = await storageManager.createProject(project);
if (!createResult.success) {
return {
success: false,
error: `Failed to save project: ${createResult.error}`,
metadata: createResult.metadata
};
}
logger.info({ projectId, projectName: params.name }, 'Project created successfully');
try {
const { getEpicService } = await import('../../services/epic-service.js');
const epicService = getEpicService();
const epicResult = await epicService.createEpic({
title: `${params.name} Main Development`,
description: `Main epic for ${params.name} project. All initial tasks will be organized under this epic.`,
projectId: projectId,
priority: 'high',
functionalArea: 'data-management',
tags: ['main-epic', 'auto-generated']
}, createdBy);
if (epicResult.success) {
logger.info({
projectId,
epicId: epicResult.data.id,
epicTitle: epicResult.data.title
}, 'Main epic created for project');
const project = createResult.data;
project.epicIds.push(epicResult.data.id);
}
else {
logger.warn({
projectId,
error: epicResult.error
}, 'Failed to create main epic for project - project creation will continue');
}
}
catch (error) {
logger.warn({
err: error,
projectId
}, 'Error creating main epic - project creation will continue');
}
return {
success: true,
data: createResult.data,
metadata: {
filePath: 'project-operations',
operation: 'create_project',
timestamp: new Date()
}
};
}
catch (error) {
logger.error({ err: error, projectName: params.name }, 'Failed to create project');
return {
success: false,
error: error instanceof Error ? error.message : String(error),
metadata: {
filePath: 'project-operations',
operation: 'create_project',
timestamp: new Date()
}
};
}
}
async getProject(projectId) {
try {
logger.debug({ projectId }, 'Getting project');
const storageManager = await getStorageManager();
return await storageManager.getProject(projectId);
}
catch (error) {
logger.error({ err: error, projectId }, 'Failed to get project');
return {
success: false,
error: error instanceof Error ? error.message : String(error),
metadata: {
filePath: 'project-operations',
operation: 'get_project',
timestamp: new Date()
}
};
}
}
async updateProject(projectId, params, updatedBy = 'system') {
try {
logger.info({ projectId, updates: Object.keys(params), updatedBy }, 'Updating project');
const validationResult = this.validateUpdateParams(params);
if (!validationResult.valid) {
return {
success: false,
error: `Project update validation failed: ${validationResult.errors.join(', ')}`,
metadata: {
filePath: 'project-operations',
operation: 'update_project',
timestamp: new Date()
}
};
}
const storageManager = await getStorageManager();
const existingResult = await storageManager.getProject(projectId);
if (!existingResult.success) {
return {
success: false,
error: `Project not found: ${existingResult.error}`,
metadata: existingResult.metadata
};
}
const existingProject = existingResult.data;
const updates = {
...params,
metadata: {
...existingProject.metadata,
updatedAt: new Date(),
...(params.tags && { tags: params.tags })
}
};
const updateResult = await storageManager.updateProject(projectId, updates);
if (!updateResult.success) {
return {
success: false,
error: `Failed to update project: ${updateResult.error}`,
metadata: updateResult.metadata
};
}
logger.info({ projectId }, 'Project updated successfully');
return {
success: true,
data: updateResult.data,
metadata: {
filePath: 'project-operations',
operation: 'update_project',
timestamp: new Date()
}
};
}
catch (error) {
logger.error({ err: error, projectId }, 'Failed to update project');
return {
success: false,
error: error instanceof Error ? error.message : String(error),
metadata: {
filePath: 'project-operations',
operation: 'update_project',
timestamp: new Date()
}
};
}
}
async createProjectFromPRD(prdData, createdBy = 'system') {
try {
logger.info({ projectName: prdData.metadata?.projectName, createdBy }, 'Creating project from PRD');
const technical = prdData.technical;
let languages = Array.isArray(technical?.techStack) ? technical.techStack : [];
let frameworks = Array.isArray(technical?.architecturalPatterns) ? technical.architecturalPatterns : [];
let tools = [];
if (languages.length === 0 || frameworks.length === 0) {
logger.info({
prdLanguages: languages.length,
prdFrameworks: frameworks.length
}, 'PRD tech stack insufficient, using ProjectAnalyzer for detection');
try {
const { ProjectAnalyzer } = await import('../../utils/project-analyzer.js');
const projectAnalyzer = ProjectAnalyzer.getInstance();
const projectPath = this.resolveProjectRootPath();
if (languages.length === 0) {
languages = await projectAnalyzer.detectProjectLanguages(projectPath);
logger.debug({ detectedLanguages: languages }, 'Languages detected by ProjectAnalyzer');
}
if (frameworks.length === 0) {
frameworks = await projectAnalyzer.detectProjectFrameworks(projectPath);
logger.debug({ detectedFrameworks: frameworks }, 'Frameworks detected by ProjectAnalyzer');
}
tools = await projectAnalyzer.detectProjectTools(projectPath);
logger.debug({ detectedTools: tools }, 'Tools detected by ProjectAnalyzer');
}
catch (analyzerError) {
logger.warn({
err: analyzerError,
projectName: prdData.metadata?.projectName
}, 'ProjectAnalyzer detection failed, using fallback values');
if (languages.length === 0)
languages = ['typescript', 'javascript'];
if (frameworks.length === 0)
frameworks = ['node.js'];
if (tools.length === 0)
tools = ['git', 'npm'];
}
}
const metadata = prdData.metadata;
const overview = prdData.overview;
const projectParams = {
name: (typeof metadata?.projectName === 'string' ? metadata.projectName : 'Untitled Project'),
description: (typeof overview?.description === 'string' ? overview.description : 'Project created from PRD'),
tags: ['prd-generated'],
techStack: {
languages,
frameworks,
tools
}
};
logger.info({
projectName: projectParams.name,
techStack: projectParams.techStack,
source: 'PRD + ProjectAnalyzer'
}, 'Enhanced project tech stack for PRD project creation');
return await this.createProject(projectParams, createdBy);
}
catch (error) {
logger.error({ err: error, prdData }, 'Failed to create project from PRD');
return {
success: false,
error: error instanceof Error ? error.message : String(error),
metadata: {
filePath: 'project-operations',
operation: 'create_project_from_prd',
timestamp: new Date()
}
};
}
}
async createProjectFromTaskList(taskListData, createdBy = 'system') {
try {
logger.info({ projectName: taskListData.metadata?.projectName, createdBy }, 'Creating project from task list');
const metadata = taskListData.metadata;
const techStack = metadata?.techStack;
const projectParams = {
name: (typeof metadata?.projectName === 'string' ? metadata.projectName : 'Untitled Project'),
description: (typeof metadata?.description === 'string' ? metadata.description : 'Project created from task list'),
tags: ['task-list-generated'],
techStack: {
languages: Array.isArray(techStack?.languages) ? techStack.languages : [],
frameworks: Array.isArray(techStack?.frameworks) ? techStack.frameworks : [],
tools: Array.isArray(techStack?.tools) ? techStack.tools : []
}
};
return await this.createProject(projectParams, createdBy);
}
catch (error) {
logger.error({ err: error, taskListData }, 'Failed to create project from task list');
return {
success: false,
error: error instanceof Error ? error.message : String(error),
metadata: {
filePath: 'project-operations',
operation: 'create_project_from_task_list',
timestamp: new Date()
}
};
}
}
async deleteProject(projectId, deletedBy = 'system') {
try {
logger.info({ projectId, deletedBy }, 'Deleting project');
const storageManager = await getStorageManager();
const projectExists = await storageManager.projectExists(projectId);
if (!projectExists) {
return {
success: false,
error: `Project ${projectId} not found`,
metadata: {
filePath: 'project-operations',
operation: 'delete_project',
timestamp: new Date()
}
};
}
const deleteResult = await storageManager.deleteProject(projectId);
if (!deleteResult.success) {
return {
success: false,
error: `Failed to delete project: ${deleteResult.error}`,
metadata: deleteResult.metadata
};
}
logger.info({ projectId }, 'Project deleted successfully');
return {
success: true,
metadata: {
filePath: 'project-operations',
operation: 'delete_project',
timestamp: new Date()
}
};
}
catch (error) {
logger.error({ err: error, projectId }, 'Failed to delete project');
return {
success: false,
error: error instanceof Error ? error.message : String(error),
metadata: {
filePath: 'project-operations',
operation: 'delete_project',
timestamp: new Date()
}
};
}
}
async listProjects(query) {
try {
logger.debug({ query }, 'Listing projects');
const storageManager = await getStorageManager();
let result;
if (query?.status) {
result = await storageManager.getProjectsByStatus(query.status);
}
else {
result = await storageManager.listProjects();
}
if (!result.success) {
return result;
}
let projects = result.data;
if (query) {
projects = this.applyProjectFilters(projects, query);
}
return {
success: true,
data: projects,
metadata: {
filePath: 'project-operations',
operation: 'list_projects',
timestamp: new Date()
}
};
}
catch (error) {
logger.error({ err: error, query }, 'Failed to list projects');
return {
success: false,
error: error instanceof Error ? error.message : String(error),
metadata: {
filePath: 'project-operations',
operation: 'list_projects',
timestamp: new Date()
}
};
}
}
async searchProjects(searchQuery, query) {
try {
logger.debug({ searchQuery, query }, 'Searching projects');
const storageManager = await getStorageManager();
const searchResult = await storageManager.searchProjects(searchQuery);
if (!searchResult.success) {
return searchResult;
}
let projects = searchResult.data;
if (query) {
projects = this.applyProjectFilters(projects, query);
}
return {
success: true,
data: projects,
metadata: {
filePath: 'project-operations',
operation: 'search_projects',
timestamp: new Date()
}
};
}
catch (error) {
logger.error({ err: error, searchQuery }, 'Failed to search projects');
return {
success: false,
error: error instanceof Error ? error.message : String(error),
metadata: {
filePath: 'project-operations',
operation: 'search_projects',
timestamp: new Date()
}
};
}
}
async determineOptimalAgentConfig(params, config) {
try {
logger.debug({
projectName: params.name,
techStack: params.techStack
}, 'Determining optimal agent configuration');
let languages = params.techStack?.languages || [];
let frameworks = params.techStack?.frameworks || [];
let tools = params.techStack?.tools || [];
if (languages.length === 0 || frameworks.length === 0) {
try {
const { ProjectAnalyzer } = await import('../../utils/project-analyzer.js');
const projectAnalyzer = ProjectAnalyzer.getInstance();
const projectPath = this.resolveProjectRootPath(params.rootPath);
if (languages.length === 0) {
languages = await projectAnalyzer.detectProjectLanguages(projectPath);
}
if (frameworks.length === 0) {
frameworks = await projectAnalyzer.detectProjectFrameworks(projectPath);
}
if (tools.length === 0) {
tools = await projectAnalyzer.detectProjectTools(projectPath);
}
logger.debug({
detectedLanguages: languages,
detectedFrameworks: frameworks,
detectedTools: tools
}, 'ProjectAnalyzer enhanced tech stack for agent configuration');
}
catch (analyzerError) {
logger.warn({
err: analyzerError,
projectName: params.name
}, 'ProjectAnalyzer failed for agent configuration, using defaults');
}
}
const optimalAgent = this.selectOptimalAgent(languages, frameworks, tools);
const agentCapabilities = this.buildAgentCapabilities(languages, frameworks, tools);
const maxAgents = this.calculateOptimalAgentCount(languages, frameworks, tools, config);
logger.info({
projectName: params.name,
selectedAgent: optimalAgent,
maxAgents,
agentCapabilities: Object.keys(agentCapabilities),
techStackBasis: { languages, frameworks, tools }
}, 'Optimal agent configuration determined');
const wasDetected = languages.length > 0 || frameworks.length > 0 || tools.length > 0;
return {
maxAgents,
defaultAgent: optimalAgent,
agentCapabilities,
...(wasDetected && {
detectedTechStack: {
languages,
frameworks,
tools
}
})
};
}
catch (error) {
logger.warn({
err: error,
projectName: params.name
}, 'Failed to determine optimal agent configuration, using defaults');
const taskManager = config.taskManager;
const agentSettings = taskManager?.agentSettings;
return {
maxAgents: typeof agentSettings?.maxAgents === 'number' ? agentSettings.maxAgents : 3,
defaultAgent: typeof agentSettings?.defaultAgent === 'string' ? agentSettings.defaultAgent : 'general',
agentCapabilities: {}
};
}
}
selectOptimalAgent(languages, frameworks, tools) {
const agentSpecializations = {
'frontend-specialist': {
languages: ['javascript', 'typescript', 'html', 'css'],
frameworks: ['react', 'vue', 'angular', 'svelte', 'next.js', 'nuxt.js'],
tools: ['webpack', 'vite', 'rollup', 'tailwind'],
score: 0
},
'backend-specialist': {
languages: ['javascript', 'typescript', 'python', 'java', 'csharp', 'go'],
frameworks: ['node.js', 'express', 'fastapi', 'django', 'spring', 'dotnet'],
tools: ['docker', 'kubernetes', 'nginx'],
score: 0
},
'fullstack-developer': {
languages: ['javascript', 'typescript', 'python'],
frameworks: ['react', 'node.js', 'next.js', 'django', 'fastapi'],
tools: ['docker', 'git', 'npm', 'yarn'],
score: 0
},
'mobile-specialist': {
languages: ['javascript', 'typescript', 'swift', 'kotlin', 'dart'],
frameworks: ['react-native', 'flutter', 'ionic'],
tools: ['xcode', 'android-studio'],
score: 0
},
'devops-specialist': {
languages: ['bash', 'python', 'yaml'],
frameworks: ['terraform', 'ansible'],
tools: ['docker', 'kubernetes', 'jenkins', 'github-actions'],
score: 0
},
'data-specialist': {
languages: ['python', 'r', 'sql'],
frameworks: ['pandas', 'tensorflow', 'pytorch'],
tools: ['jupyter', 'docker'],
score: 0
}
};
for (const [, spec] of Object.entries(agentSpecializations)) {
const languageMatches = languages.filter(lang => spec.languages.some(specLang => lang.toLowerCase().includes(specLang))).length;
const languageScore = (languageMatches / Math.max(languages.length, 1)) * 0.4;
const frameworkMatches = frameworks.filter(fw => spec.frameworks.some(specFw => fw.toLowerCase().includes(specFw))).length;
const frameworkScore = (frameworkMatches / Math.max(frameworks.length, 1)) * 0.35;
const toolMatches = tools.filter(tool => spec.tools.some(specTool => tool.toLowerCase().includes(specTool))).length;
const toolScore = (toolMatches / Math.max(tools.length, 1)) * 0.25;
spec.score = languageScore + frameworkScore + toolScore;
}
const bestAgent = Object.entries(agentSpecializations)
.sort(([, a], [, b]) => b.score - a.score)[0];
const selectedAgent = bestAgent[1].score > 0.3 ? bestAgent[0] : 'fullstack-developer';
logger.debug({
agentScores: Object.fromEntries(Object.entries(agentSpecializations).map(([name, spec]) => [name, spec.score])),
selectedAgent,
threshold: 0.3
}, 'Agent selection analysis completed');
return selectedAgent;
}
buildAgentCapabilities(languages, frameworks, tools) {
const capabilities = {};
if (languages.length > 0) {
capabilities.languages = languages;
capabilities.primaryLanguage = [languages[0]];
}
if (frameworks.length > 0) {
capabilities.frameworks = frameworks;
capabilities.primaryFramework = [frameworks[0]];
}
if (tools.length > 0) {
capabilities.tools = tools;
capabilities.buildTools = tools.filter(tool => ['npm', 'yarn', 'pnpm', 'webpack', 'vite', 'rollup'].includes(tool));
capabilities.deploymentTools = tools.filter(tool => ['docker', 'kubernetes', 'jenkins'].includes(tool));
}
capabilities.isFullStack = languages.includes('javascript') || languages.includes('typescript');
capabilities.isMobile = frameworks.some(fw => ['react-native', 'flutter', 'ionic'].includes(fw));
capabilities.isBackend = frameworks.some(fw => ['node.js', 'express', 'django', 'fastapi', 'spring'].includes(fw));
capabilities.isFrontend = frameworks.some(fw => ['react', 'vue', 'angular', 'svelte'].includes(fw));
return capabilities;
}
calculateOptimalAgentCount(languages, frameworks, tools, config) {
const taskManager = config.taskManager;
const agentSettings = taskManager?.agentSettings;
const baseAgents = typeof agentSettings?.maxAgents === 'number' ? agentSettings.maxAgents : 3;
let complexityScore = 0;
complexityScore += Math.min(languages.length * 0.5, 2);
complexityScore += Math.min(frameworks.length * 0.3, 1.5);
const sophisticatedTools = tools.filter(tool => ['docker', 'kubernetes', 'webpack', 'vite', 'jenkins', 'terraform'].includes(tool));
complexityScore += Math.min(sophisticatedTools.length * 0.2, 1);
const optimalCount = Math.max(1, Math.min(Math.ceil(baseAgents * (0.5 + complexityScore * 0.1)), baseAgents));
logger.debug({
complexityScore,
languageCount: languages.length,
frameworkCount: frameworks.length,
sophisticatedToolCount: sophisticatedTools.length,
baseAgents,
optimalCount
}, 'Agent count calculation completed');
return optimalCount;
}
validateCreateParams(params) {
const errors = [];
if (!params.name || typeof params.name !== 'string' || params.name.trim().length === 0) {
errors.push('Project name is required and must be a non-empty string');
}
if (params.name && params.name.length > 100) {
errors.push('Project name must be 100 characters or less');
}
if (!params.description || typeof params.description !== 'string' || params.description.trim().length === 0) {
errors.push('Project description is required and must be a non-empty string');
}
if (params.description && params.description.length > 1000) {
errors.push('Project description must be 1000 characters or less');
}
if (params.rootPath && typeof params.rootPath !== 'string') {
errors.push('Root path must be a string');
}
if (params.tags && !Array.isArray(params.tags)) {
errors.push('Tags must be an array of strings');
}
if (params.tags && params.tags.some(tag => typeof tag !== 'string')) {
errors.push('All tags must be strings');
}
return {
valid: errors.length === 0,
errors
};
}
validateUpdateParams(params) {
const errors = [];
if (params.name !== undefined) {
if (typeof params.name !== 'string' || params.name.trim().length === 0) {
errors.push('Project name must be a non-empty string');
}
if (params.name.length > 100) {
errors.push('Project name must be 100 characters or less');
}
}
if (params.description !== undefined) {
if (typeof params.description !== 'string' || params.description.trim().length === 0) {
errors.push('Project description must be a non-empty string');
}
if (params.description.length > 1000) {
errors.push('Project description must be 1000 characters or less');
}
}
if (params.status !== undefined) {
if (!['pending', 'in_progress', 'completed', 'blocked', 'cancelled'].includes(params.status)) {
errors.push('Status must be one of: pending, in_progress, completed, blocked, cancelled');
}
}
if (params.rootPath !== undefined && typeof params.rootPath !== 'string') {
errors.push('Root path must be a string');
}
if (params.tags !== undefined) {
if (!Array.isArray(params.tags)) {
errors.push('Tags must be an array of strings');
}
else if (params.tags.some(tag => typeof tag !== 'string')) {
errors.push('All tags must be strings');
}
}
return {
valid: errors.length === 0,
errors
};
}
applyProjectFilters(projects, query) {
let filtered = projects;
if (query.tags && query.tags.length > 0) {
filtered = filtered.filter(project => query.tags.some(tag => project.metadata.tags.includes(tag)));
}
if (query.createdAfter) {
filtered = filtered.filter(project => project.metadata.createdAt >= query.createdAfter);
}
if (query.createdBefore) {
filtered = filtered.filter(project => project.metadata.createdAt <= query.createdBefore);
}
if (query.offset) {
filtered = filtered.slice(query.offset);
}
if (query.limit) {
filtered = filtered.slice(0, query.limit);
}
return filtered;
}
}
export function getProjectOperations() {
return ProjectOperations.getInstance();
}