ai-debug-local-mcp
Version:
🎯 ENHANCED AI GUIDANCE v4.1.2: Dramatically improved tool descriptions help AI users choose the right tools instead of 'close enough' options. Ultra-fast keyboard automation (10x speed), universal recording, multi-ecosystem debugging support, and compreh
553 lines • 23.2 kB
JavaScript
/**
* Enhanced Framework Detector
*
* Fixes critical framework misdetection issues, especially Phoenix LiveView
* projects being incorrectly identified as Flutter. Provides accurate detection
* through multiple detection methods and confidence scoring.
*/
import { UserFriendlyLogger } from './user-friendly-logger.js';
import * as fs from 'fs/promises';
import * as path from 'path';
export class EnhancedFrameworkDetector {
logger;
frameworkProfiles;
constructor() {
this.logger = new UserFriendlyLogger('FrameworkDetector');
this.frameworkProfiles = new Map();
this.initializeFrameworkProfiles();
}
/**
* Initialize comprehensive framework profiles with proper Phoenix LiveView support
*/
initializeFrameworkProfiles() {
const profiles = [
// Phoenix LiveView (HIGH PRIORITY - should be detected first on port 4000)
{
name: 'phoenix-liveview',
priority: 100, // Highest priority for port 4000
detectionFiles: ['mix.exs', 'config/config.exs', 'lib/', 'priv/'],
requiredFiles: ['mix.exs'], // Must have mix.exs
excludeFiles: ['pubspec.yaml', 'lib/main.dart'], // Exclude Flutter files
httpHeaders: {
'server': /phoenix/i,
'x-powered-by': /phoenix/i
},
domSelectors: ['[phx-', '[data-phx-', 'script[src*="phoenix"]'],
portHints: [4000],
packageJsonDeps: [] // Elixir project doesn't have package.json
},
// Elixir/Phoenix (fallback for non-LiveView Phoenix)
{
name: 'phoenix',
priority: 95,
detectionFiles: ['mix.exs', 'config/config.exs', 'web/', 'priv/static/'],
requiredFiles: ['mix.exs'],
excludeFiles: ['pubspec.yaml', 'lib/main.dart'],
portHints: [4000, 4001],
packageJsonDeps: []
},
// Flutter Web (LOWER PRIORITY than Phoenix for port conflicts)
{
name: 'flutter-web',
priority: 60, // Lower than Phoenix
detectionFiles: ['pubspec.yaml', 'lib/main.dart', 'web/index.html'],
requiredFiles: ['pubspec.yaml', 'lib/main.dart'],
excludeFiles: ['mix.exs'], // Exclude Phoenix files
httpHeaders: {
'content-type': /text\/html/
},
domSelectors: ['flutter-view', '#flutter_bootstrap', 'script[src*="flutter"]'],
portHints: [3000, 8080],
packageJsonDeps: []
},
// React
{
name: 'react',
priority: 80,
detectionFiles: ['src/App.js', 'src/App.tsx', 'public/index.html', 'package.json'],
requiredFiles: ['package.json'],
httpHeaders: {
'content-type': /text\/html/
},
domSelectors: ['#root', '[data-reactroot]', 'script[src*="react"]'],
portHints: [3000],
packageJsonDeps: ['react', 'react-dom']
},
// Next.js (Higher priority than React for detection)
{
name: 'nextjs',
priority: 85,
detectionFiles: ['next.config.js', 'next.config.mjs', 'pages/', 'app/', 'package.json'],
requiredFiles: ['package.json'],
httpHeaders: {
'x-powered-by': /next\.js/i
},
domSelectors: ['#__next', 'script[src*="_next"]'],
portHints: [3000],
packageJsonDeps: ['next']
},
// Vue.js
{
name: 'vue',
priority: 75,
detectionFiles: ['src/main.js', 'src/main.ts', 'vue.config.js', 'package.json'],
requiredFiles: ['package.json'],
domSelectors: ['#app', '[data-v-', 'script[src*="vue"]'],
portHints: [8080, 3000],
packageJsonDeps: ['vue']
},
// Angular
{
name: 'angular',
priority: 85,
detectionFiles: ['angular.json', 'src/main.ts', 'package.json'],
requiredFiles: ['angular.json', 'package.json'],
domSelectors: ['app-root', '[ng-', 'script[src*="angular"]'],
portHints: [4200],
packageJsonDeps: ['@angular/core']
},
// Svelte/SvelteKit
{
name: 'svelte',
priority: 70,
detectionFiles: ['svelte.config.js', 'src/main.js', 'package.json'],
requiredFiles: ['package.json'],
domSelectors: ['script[src*="svelte"]'],
portHints: [5000, 3000],
packageJsonDeps: ['svelte']
},
// Rails
{
name: 'rails',
priority: 90,
detectionFiles: ['Gemfile', 'config/application.rb', 'app/', 'config/routes.rb'],
requiredFiles: ['Gemfile'],
httpHeaders: {
'server': /puma|unicorn|passenger/i
},
portHints: [3000],
packageJsonDeps: []
},
// Django
{
name: 'django',
priority: 85,
detectionFiles: ['manage.py', 'requirements.txt', 'settings.py'],
requiredFiles: ['manage.py'],
httpHeaders: {
'server': /django/i
},
portHints: [8000],
packageJsonDeps: []
}
];
for (const profile of profiles) {
this.frameworkProfiles.set(profile.name, profile);
}
}
/**
* Enhanced framework detection with multiple methods and confidence scoring
*/
async detectFramework(url, page, // Playwright page for DOM analysis
workingDirectory) {
const urlObj = new URL(url);
const port = parseInt(urlObj.port) || (urlObj.protocol === 'https:' ? 443 : 80);
const workDir = workingDirectory || process.cwd();
this.logger.info(`🔍 Enhanced framework detection for ${url} (port ${port})`);
// Detection Method 1: File System Analysis (Most Reliable)
const fileSystemResults = await this.detectByFileSystem(workDir);
// Detection Method 2: HTTP Headers Analysis
const httpHeaderResults = await this.detectByHttpHeaders(url);
// Detection Method 3: Port-based Heuristics
const portHeuristicResults = this.detectByPortHeuristics(port);
// Detection Method 4: DOM Analysis (if page available)
const domResults = page ? await this.detectByDomAnalysis(page) : null;
// Combine all detection methods with weighted scoring
const combinedResults = this.combineDetectionResults(fileSystemResults, httpHeaderResults, portHeuristicResults, domResults);
// Special handling for Phoenix LiveView on port 4000
const finalResult = this.applyPhoenixLiveViewCorrection(combinedResults, port, fileSystemResults);
this.logger.info(`✅ Framework detected: ${finalResult.framework} (${(finalResult.confidence * 100).toFixed(1)}% confidence)`);
return finalResult;
}
/**
* File system-based framework detection
*/
async detectByFileSystem(workDir) {
const results = new Map();
for (const [frameworkName, profile] of this.frameworkProfiles) {
let confidence = 0;
const evidence = [];
// Check for detection files
let filesFound = 0;
for (const file of profile.detectionFiles) {
try {
const filePath = path.join(workDir, file);
await fs.access(filePath);
filesFound++;
evidence.push(file);
}
catch {
// File doesn't exist
}
}
// Calculate base confidence from files found
if (filesFound > 0) {
confidence = (filesFound / profile.detectionFiles.length) * 0.6; // Base 60% for file detection
}
// Required files check (must all exist)
if (profile.requiredFiles) {
let requiredFound = 0;
for (const requiredFile of profile.requiredFiles) {
try {
await fs.access(path.join(workDir, requiredFile));
requiredFound++;
}
catch {
// Required file missing
}
}
if (requiredFound === profile.requiredFiles.length) {
confidence += 0.3; // Bonus for all required files
}
else if (requiredFound === 0) {
confidence = 0; // No confidence if no required files
}
}
// Exclude files check (none should exist)
if (profile.excludeFiles) {
let excludeFound = 0;
for (const excludeFile of profile.excludeFiles) {
try {
await fs.access(path.join(workDir, excludeFile));
excludeFound++;
}
catch {
// Exclude file doesn't exist (good)
}
}
if (excludeFound > 0) {
confidence *= 0.3; // Heavy penalty for conflicting files
evidence.push(`Conflicting files: ${profile.excludeFiles.filter(async (f) => {
try {
await fs.access(path.join(workDir, f));
return true;
}
catch {
return false;
}
}).join(', ')}`);
}
}
// Package.json dependency check
if (profile.packageJsonDeps && profile.packageJsonDeps.length > 0) {
try {
const packageJsonPath = path.join(workDir, 'package.json');
const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf-8'));
const deps = { ...packageJson.dependencies, ...packageJson.devDependencies };
let depsFound = 0;
for (const dep of profile.packageJsonDeps) {
if (deps[dep]) {
depsFound++;
evidence.push(`Dependency: ${dep}`);
}
}
if (depsFound > 0) {
confidence += (depsFound / profile.packageJsonDeps.length) * 0.2; // Up to 20% bonus
}
}
catch {
// No package.json or parsing error
}
}
if (confidence > 0) {
results.set(frameworkName, { confidence, evidence });
}
}
return results;
}
/**
* HTTP headers-based framework detection
*/
async detectByHttpHeaders(url) {
const results = new Map();
try {
const response = await fetch(url, {
method: 'HEAD',
signal: AbortSignal.timeout(5000)
});
const headers = {};
response.headers.forEach((value, key) => {
headers[key.toLowerCase()] = value;
});
for (const [frameworkName, profile] of this.frameworkProfiles) {
if (!profile.httpHeaders)
continue;
let confidence = 0;
const evidence = [];
for (const [headerName, expectedValue] of Object.entries(profile.httpHeaders)) {
const headerValue = headers[headerName.toLowerCase()];
if (headerValue) {
if (expectedValue instanceof RegExp) {
if (expectedValue.test(headerValue)) {
confidence += 0.3;
evidence.push(`${headerName}: ${headerValue}`);
}
}
else if (headerValue.toLowerCase().includes(expectedValue.toLowerCase())) {
confidence += 0.3;
evidence.push(`${headerName}: ${headerValue}`);
}
}
}
if (confidence > 0) {
results.set(frameworkName, { confidence, evidence });
}
}
}
catch (error) {
this.logger.warn(`HTTP headers detection failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
return results;
}
/**
* Port-based heuristic detection
*/
detectByPortHeuristics(port) {
const results = new Map();
for (const [frameworkName, profile] of this.frameworkProfiles) {
if (!profile.portHints)
continue;
if (profile.portHints.includes(port)) {
const confidence = 0.2; // Port hints give low confidence but useful for tie-breaking
const evidence = [`Common port ${port} for ${frameworkName}`];
results.set(frameworkName, { confidence, evidence });
}
}
return results;
}
/**
* DOM-based framework detection
*/
async detectByDomAnalysis(page) {
if (!page)
return null;
const results = new Map();
try {
for (const [frameworkName, profile] of this.frameworkProfiles) {
if (!profile.domSelectors)
continue;
let confidence = 0;
const evidence = [];
for (const selector of profile.domSelectors) {
try {
const elements = await page.$$eval(selector, (els) => els.length);
if (elements > 0) {
confidence += 0.25;
evidence.push(`DOM: ${selector} (${elements} elements)`);
}
}
catch {
// Selector failed, continue
}
}
if (confidence > 0) {
results.set(frameworkName, { confidence, evidence });
}
}
}
catch (error) {
this.logger.warn(`DOM analysis failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
return results;
}
/**
* Combine detection results with weighted scoring
*/
combineDetectionResults(fileSystem, httpHeaders, portHeuristics, domAnalysis) {
const combinedScores = new Map();
// Combine all detection methods
const allSources = [
{ source: fileSystem, method: 'file-system', weight: 1.0 },
{ source: httpHeaders, method: 'http-headers', weight: 0.8 },
{ source: portHeuristics, method: 'port-heuristic', weight: 0.3 },
{ source: domAnalysis, method: 'dom-analysis', weight: 0.7 }
];
for (const { source, method, weight } of allSources) {
if (!source)
continue;
for (const [framework, result] of source) {
const existing = combinedScores.get(framework) || { confidence: 0, evidence: [], methods: [] };
existing.confidence += result.confidence * weight;
existing.evidence.push(...result.evidence);
existing.methods.push(method);
combinedScores.set(framework, existing);
}
}
// Apply priority bonuses
for (const [framework, result] of combinedScores) {
const profile = this.frameworkProfiles.get(framework);
if (profile) {
const priorityBonus = (profile.priority / 100) * 0.1; // Up to 10% bonus
result.confidence += priorityBonus;
}
}
// Find best match
let bestFramework = 'unknown';
let bestConfidence = 0;
let bestEvidence = [];
let detectionMethod = 'hybrid';
for (const [framework, result] of combinedScores) {
if (result.confidence > bestConfidence) {
bestFramework = framework;
bestConfidence = result.confidence;
bestEvidence = result.evidence;
// Determine primary detection method
if (result.methods.includes('file-system')) {
detectionMethod = 'file-system';
}
else if (result.methods.includes('http-headers')) {
detectionMethod = 'http-headers';
}
else if (result.methods.includes('dom-analysis')) {
detectionMethod = 'dom-analysis';
}
else {
detectionMethod = 'port-heuristic';
}
}
}
// Generate alternative candidates
const alternatives = Array.from(combinedScores.entries())
.filter(([framework]) => framework !== bestFramework)
.sort((a, b) => b[1].confidence - a[1].confidence)
.slice(0, 3)
.map(([framework, result]) => ({
framework,
confidence: result.confidence,
reason: result.methods.join(', ')
}));
return {
framework: bestFramework,
confidence: Math.min(1.0, bestConfidence), // Cap at 100%
detectionMethod,
evidence: {
filesFound: bestEvidence.filter(e => !e.includes(':')),
httpHeaders: {},
portAnalysis: { port: 0, commonFrameworks: [] },
domIndicators: bestEvidence.filter(e => e.startsWith('DOM:'))
},
alternativeCandidates: alternatives
};
}
/**
* Apply Phoenix LiveView correction for port 4000 projects
*/
applyPhoenixLiveViewCorrection(result, port, fileSystemResults) {
// Special case: If we're on port 4000 and have Phoenix files, strongly favor Phoenix
if (port === 4000) {
const phoenixResult = fileSystemResults.get('phoenix-liveview') || fileSystemResults.get('phoenix');
if (phoenixResult && phoenixResult.confidence > 0.3) {
// Override detection if Phoenix is detected with reasonable confidence
if (result.framework === 'flutter-web' || result.framework === 'unknown') {
this.logger.info('🔧 Applying Phoenix LiveView correction for port 4000');
return {
...result,
framework: 'phoenix-liveview',
confidence: Math.max(0.8, phoenixResult.confidence), // Boost confidence
detectionMethod: 'hybrid',
evidence: {
...result.evidence,
filesFound: phoenixResult.evidence
}
};
}
}
}
return result;
}
/**
* Quick framework detection for performance-critical scenarios
*/
async quickDetect(url, workingDirectory) {
const urlObj = new URL(url);
const port = parseInt(urlObj.port) || 80;
const workDir = workingDirectory || process.cwd();
// Quick port-based detection first
if (port === 4000) {
// Check for Phoenix files quickly
try {
await fs.access(path.join(workDir, 'mix.exs'));
return 'phoenix-liveview';
}
catch {
// No Phoenix files
}
}
// Standard quick checks
const quickChecks = [
{ port: 3000, files: ['next.config.js'], framework: 'nextjs' },
{ port: 3000, files: ['src/App.jsx', 'src/App.tsx'], framework: 'react' },
{ port: 4200, files: ['angular.json'], framework: 'angular' },
{ port: 8080, files: ['vue.config.js'], framework: 'vue' },
{ port: 8080, files: ['pubspec.yaml'], framework: 'flutter-web' }
];
for (const check of quickChecks) {
if (port === check.port) {
for (const file of check.files) {
try {
await fs.access(path.join(workDir, file));
return check.framework;
}
catch {
continue;
}
}
}
}
return 'unknown';
}
/**
* Get framework-specific debugging recommendations
*/
getFrameworkSpecificRecommendations(framework) {
const recommendations = {
'phoenix-liveview': [
'Use Phoenix LiveView debugging tools',
'Monitor LiveView socket connections',
'Check for Elixir process errors in logs',
'Verify LiveView mount and update lifecycle'
],
'phoenix': [
'Check Phoenix server logs',
'Verify Plug pipeline configuration',
'Monitor Ecto database connections',
'Review controller and view rendering'
],
'react': [
'Use React Developer Tools',
'Check for React Strict Mode warnings',
'Monitor component re-renders',
'Verify prop types and state updates'
],
'nextjs': [
'Check Next.js build output',
'Monitor SSR/SSG hydration',
'Verify API routes functionality',
'Review Next.js configuration'
],
'flutter-web': [
'Use Flutter Inspector',
'Check Flutter Web console output',
'Verify Flutter Web build configuration',
'Monitor widget tree performance'
]
};
return recommendations[framework] || [
'Verify application server is running',
'Check console for JavaScript errors',
'Monitor network requests',
'Review application logs'
];
}
}
//# sourceMappingURL=enhanced-framework-detector.js.map