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
634 lines (614 loc) • 25.9 kB
JavaScript
/**
* Phoenix LiveView Enhanced Handler - Advanced LiveView Debugging
*
* Based on Cycles 50-51 feedback: "Would be amazing to have phoenix_liveview_state_inspector"
* Provides deep LiveView state inspection, process monitoring, and message tracing
*/
import { BaseToolHandler } from './base-handler.js';
/**
* Phoenix LiveView Enhanced Handler - Advanced debugging for Phoenix/LiveView applications
* Based on real-world feedback from Code Quality Sprint (614-line refactoring success)
*/
export class PhoenixLiveViewEnhancedHandler extends BaseToolHandler {
tools;
stateSnapshots = new Map();
processTree = new Map();
pubsubMessages = [];
constructor() {
super();
this.tools = this.getTools();
}
/**
* Get available Phoenix LiveView enhanced debugging tools
*/
getTools() {
return [
{
name: 'phoenix_liveview_state_inspector',
description: 'Deep inspection of LiveView socket state, assigns, and process information',
inputSchema: {
type: 'object',
properties: {
sessionId: {
type: 'string',
description: 'Debug session ID'
},
socketId: {
type: 'string',
description: 'LiveView socket ID (optional)',
},
includePrivate: {
type: 'boolean',
default: false,
description: 'Include private assigns and internal state'
},
captureSnapshot: {
type: 'boolean',
default: true,
description: 'Capture state snapshot for comparison'
}
},
required: ['sessionId']
}
},
{
name: 'phoenix_process_tree_monitor',
description: 'Monitor Phoenix process tree including supervisors, LiveViews, and channels',
inputSchema: {
type: 'object',
properties: {
sessionId: {
type: 'string',
description: 'Debug session ID'
},
rootProcess: {
type: 'string',
description: 'Root process to start monitoring from (optional)',
},
includeMemory: {
type: 'boolean',
default: true,
description: 'Include memory usage information'
},
monitorDuration: {
type: 'number',
default: 30000,
description: 'Monitoring duration in milliseconds'
}
},
required: ['sessionId']
}
},
{
name: 'phoenix_pubsub_message_tracer',
description: 'Trace PubSub messages across topics with filtering and analysis',
inputSchema: {
type: 'object',
properties: {
sessionId: {
type: 'string',
description: 'Debug session ID'
},
topics: {
type: 'array',
items: { type: 'string' },
description: 'Specific topics to trace (optional, defaults to all)'
},
eventFilter: {
type: 'string',
description: 'Event name filter pattern (supports wildcards)'
},
traceDuration: {
type: 'number',
default: 60000,
description: 'Trace duration in milliseconds'
}
},
required: ['sessionId']
}
},
{
name: 'phoenix_file_size_monitor',
description: 'Monitor file sizes with threshold alerts (300-line rule enforcement)',
inputSchema: {
type: 'object',
properties: {
sessionId: {
type: 'string',
description: 'Debug session ID'
},
projectPath: {
type: 'string',
description: 'Phoenix project root path'
},
lineThreshold: {
type: 'number',
default: 300,
description: 'Line count threshold for alerts'
},
filePatterns: {
type: 'array',
items: { type: 'string' },
default: ['**/*.ex', '**/*.exs'],
description: 'File patterns to monitor'
},
enableRealTime: {
type: 'boolean',
default: true,
description: 'Enable real-time file monitoring'
}
},
required: ['sessionId', 'projectPath']
}
}
];
}
/**
* Handle tool requests by routing to appropriate methods
*/
async handle(toolName, args, sessions) {
switch (toolName) {
case 'phoenix_liveview_state_inspector':
return this.phoenixLiveViewStateInspector(args, sessions);
case 'phoenix_process_tree_monitor':
return this.phoenixProcessTreeMonitor(args, sessions);
case 'phoenix_pubsub_message_tracer':
return this.phoenixPubSubMessageTracer(args, sessions);
case 'phoenix_file_size_monitor':
return this.phoenixFileSizeMonitor(args, sessions);
default:
throw new Error(`Unknown tool: ${toolName}`);
}
}
/**
* Deep inspection of LiveView socket state and assigns
* Addresses user feedback: "Would be amazing to have phoenix_liveview_state_inspector"
*/
async phoenixLiveViewStateInspector(args, sessions) {
const { sessionId, socketId, includePrivate = false, captureSnapshot = true } = args;
const session = sessions.get(sessionId);
if (!session) {
throw new Error(`Session ${sessionId} not found`);
}
try {
// Capture current LiveView state through browser evaluation
const liveViewState = await this.captureLiveViewState(session, socketId, includePrivate);
if (captureSnapshot) {
this.stateSnapshots.set(socketId || 'default', liveViewState);
}
// Analyze state for insights and recommendations
const analysis = this.analyzeLiveViewState(liveViewState);
// Format data as JSON for display
const responseData = {
liveViewState,
analysis,
snapshot: captureSnapshot ? `snapshot_${Date.now()}` : undefined
};
return this.createTextResponse(`🔥 **Phoenix LiveView State Inspector**
**Socket ID:** ${liveViewState.socket.id}
**View:** ${liveViewState.socket.view}
**Connected:** ${liveViewState.socket.connected ? '✅' : '❌'}
**Endpoint:** ${liveViewState.socket.endpoint}
**Assigns:** ${analysis.assignsCount} total
**Changed Assigns:** ${analysis.changedAssigns.join(', ') || 'None'}
**Memory Usage:** ${analysis.memoryUsage}
**Performance Analysis:**
${analysis.performanceWarnings.length > 0 ? analysis.performanceWarnings.map((w) => `⚠️ ${w}`).join('\n') : '✅ No performance warnings'}
**Recommendations:**
${analysis.recommendations.length > 0 ? analysis.recommendations.map((r) => `💡 ${r}`).join('\n') : '✅ No recommendations'}
${captureSnapshot ? `📸 **Snapshot:** ${responseData.snapshot}` : ''}
\`\`\`json
${JSON.stringify(responseData, null, 2)}
\`\`\``);
}
catch (error) {
return this.createErrorResponse(error);
}
}
/**
* Monitor Phoenix process tree with memory and performance metrics
* Addresses user feedback: "phoenix_process_tree_monitor"
*/
async phoenixProcessTreeMonitor(args, sessions) {
const { sessionId, rootProcess, includeMemory = true, monitorDuration = 30000 } = args;
const session = sessions.get(sessionId);
if (!session) {
throw new Error(`Session ${sessionId} not found`);
}
try {
// Monitor process tree for specified duration
const processTree = await this.captureProcessTree(session, rootProcess, includeMemory, monitorDuration);
// Calculate statistics
const statistics = this.calculateProcessStatistics(processTree);
// Generate warnings for potential issues
const warnings = this.generateProcessWarnings(processTree);
// Format response data
const responseData = { processTree, statistics, warnings };
return this.createTextResponse(`🌲 **Phoenix Process Tree Monitor**
**Total Processes:** ${statistics.totalProcesses}
**Memory Total:** ${statistics.memoryTotal}
**LiveView Processes:** ${statistics.liveViewProcesses}
**Supervisor Processes:** ${statistics.supervisorProcesses}
**Avg Message Queue Length:** ${statistics.averageMessageQueueLength}
**Root Process:** ${processTree.name} (${processTree.pid})
**Status:** ${processTree.status}
**Memory:** ${Math.round(processTree.memory / 1024 / 1024)}MB
**Warnings:**
${warnings.length > 0 ? warnings.map(w => `⚠️ ${w}`).join('\n') : '✅ No warnings detected'}
\`\`\`json
${JSON.stringify(responseData, null, 2)}
\`\`\``);
}
catch (error) {
return this.createErrorResponse(error);
}
}
/**
* Trace PubSub messages with filtering and flow analysis
* Addresses user feedback: "phoenix_pubsub_message_tracer"
*/
async phoenixPubSubMessageTracer(args, sessions) {
const { sessionId, topics, eventFilter, traceDuration = 60000 } = args;
const session = sessions.get(sessionId);
if (!session) {
throw new Error(`Session ${sessionId} not found`);
}
try {
// Start PubSub message tracing
const messages = await this.tracePubSubMessages(session, topics, eventFilter, traceDuration);
// Analyze message patterns
const analysis = this.analyzePubSubMessages(messages);
// Generate recommendations
const recommendations = this.generatePubSubRecommendations(analysis);
// Format response data
const responseData = { messages, analysis, recommendations };
return this.createTextResponse(`📡 **Phoenix PubSub Message Tracer**
**Total Messages:** ${analysis.totalMessages}
**Unique Topics:** ${analysis.uniqueTopics.length} (${analysis.uniqueTopics.join(', ')})
**Average Payload Size:** ${analysis.averagePayloadSize} bytes
**Trace Duration:** ${traceDuration / 1000}s
**Message Frequency:**
${Object.entries(analysis.messageFrequency).map(([event, count]) => ` • ${event}: ${count}`).join('\n')}
**Message Flow:**
${analysis.messageFlow.map((flow) => ` • ${flow.from} → ${flow.to}: ${flow.count} messages`).join('\n')}
**Recommendations:**
${recommendations.length > 0 ? recommendations.map((r) => `💡 ${r}`).join('\n') : '✅ No recommendations'}
\`\`\`json
${JSON.stringify(responseData, null, 2)}
\`\`\``);
}
catch (error) {
return this.createErrorResponse(error);
}
}
/**
* Monitor file sizes with threshold alerts for 300-line rule
* Addresses user feedback: "File size monitoring: Alert when files exceed thresholds (like our 300-line rule)"
*/
async phoenixFileSizeMonitor(args, sessions) {
const { sessionId, projectPath, lineThreshold = 300, filePatterns = ['**/*.ex', '**/*.exs'], enableRealTime = true } = args;
const session = sessions.get(sessionId);
if (!session) {
throw new Error(`Session ${sessionId} not found`);
}
try {
// Scan files and check sizes
const violations = await this.scanFileViolations(projectPath, lineThreshold, filePatterns);
// Calculate summary statistics
const summary = this.calculateFileSummary(violations, projectPath, filePatterns);
// Set up real-time monitoring if requested
let monitoring = { enabled: false };
if (enableRealTime) {
monitoring = await this.setupRealTimeFileMonitoring(sessionId, projectPath, lineThreshold, filePatterns);
}
// Format response data
const responseData = { violations, summary, monitoring };
return this.createTextResponse(`📈 **Phoenix File Size Monitor**
**Project Path:** ${projectPath}
**Line Threshold:** ${lineThreshold} lines
**File Patterns:** ${filePatterns.join(', ')}
**Summary:**
• Total Files: ${summary.totalFiles}
• Violating Files: ${summary.violatingFiles}
• Average File Size: ${summary.averageFileSize} lines
• Largest File: ${summary.largestFile.path} (${summary.largestFile.lines} lines)
**Violations:**
${violations.length > 0 ? violations.map(v => `${v.violationLevel === 'error' ? '🔴' : v.violationLevel === 'warning' ? '🟡' : '⚠️'} ${v.file}: ${v.currentLines} lines (${v.violationLevel})`).join('\n') : '✅ No violations found'}
**Real-time Monitoring:** ${monitoring.enabled ? `✅ Enabled ${monitoring.monitoringId ? `(${monitoring.monitoringId})` : ''}` : '❌ Disabled'}
\`\`\`json
${JSON.stringify(responseData, null, 2)}
\`\`\``);
}
catch (error) {
return this.createErrorResponse(error);
}
}
/**
* Capture LiveView state through browser-side evaluation
*/
async captureLiveViewState(session, socketId, includePrivate = false) {
if (!session.page) {
throw new Error('No active page in session');
}
// Execute JavaScript to capture LiveView state
const liveViewState = await session.page.evaluate((socketId, includePrivate) => {
// Access LiveView socket through window.liveSocket
const liveSocket = window.liveSocket;
if (!liveSocket) {
throw new Error('LiveView socket not found');
}
// Get specific socket or first available
let socket;
if (socketId) {
socket = Object.values(liveSocket.sockets).find((s) => s.id === socketId);
}
else {
socket = Object.values(liveSocket.sockets)[0];
}
if (!socket) {
throw new Error('No LiveView socket found');
}
return {
socket: {
id: socket.id,
endpoint: socket.endpointURL,
view: socket.view?.constructor?.name || 'unknown',
assigns: includePrivate ? socket.assigns : Object.fromEntries(Object.entries(socket.assigns || {}).filter(([key]) => !key.startsWith('_'))),
changed: socket.changed || {},
connected: socket.isConnected(),
joinRef: socket.joinRef,
ref: socket.ref,
topic: socket.topic
},
processes: {
liveViewPid: 'browser_side', // Would need server-side integration for real PIDs
parentPid: 'unknown',
supervisorPid: 'unknown',
channelPid: 'unknown'
},
pubsub: {
subscriptions: [],
recentMessages: []
},
performance: {
renderTime: socket.lastRenderTime || 0,
diffSize: socket.lastDiffSize || 0,
mountTime: socket.mountTime || 0,
lastEventTime: socket.lastEventTime || 0
}
};
}, socketId || '', includePrivate);
return liveViewState;
}
/**
* Analyze LiveView state for insights and recommendations
*/
analyzeLiveViewState(state) {
const assigns = state.socket.assigns;
const assignsCount = Object.keys(assigns).length;
const changedAssigns = Object.keys(state.socket.changed || {});
const performanceWarnings = [];
const recommendations = [];
// Performance analysis
if (state.performance.renderTime > 100) {
performanceWarnings.push(`Slow render time: ${state.performance.renderTime}ms`);
recommendations.push('Consider optimizing template rendering or breaking down large assigns');
}
if (state.performance.diffSize > 10000) {
performanceWarnings.push(`Large diff size: ${state.performance.diffSize} bytes`);
recommendations.push('Consider using streams or pagination for large data sets');
}
if (assignsCount > 20) {
performanceWarnings.push(`Many assigns: ${assignsCount}`);
recommendations.push('Consider grouping related assigns into nested structures');
}
// Connection analysis
if (!state.socket.connected) {
performanceWarnings.push('Socket not connected');
recommendations.push('Check WebSocket connection and network connectivity');
}
return {
assignsCount,
changedAssigns,
performanceWarnings,
memoryUsage: `${JSON.stringify(assigns).length} bytes`,
recommendations
};
}
/**
* Capture process tree (mock implementation - would need server-side integration)
*/
async captureProcessTree(session, rootProcess, includeMemory = true, duration = 30000) {
// Mock implementation - in real scenario would integrate with :observer or custom Phoenix endpoint
return {
pid: '<0.100.0>',
name: 'MyAppWeb.Endpoint',
type: 'supervisor',
status: 'running',
memory: 1024000,
messageQueueLength: 0,
children: [
{
pid: '<0.101.0>',
name: 'Phoenix.LiveView.Socket',
type: 'liveview',
status: 'running',
memory: 512000,
messageQueueLength: 2,
children: []
},
{
pid: '<0.102.0>',
name: 'Phoenix.Channel',
type: 'channel',
status: 'running',
memory: 256000,
messageQueueLength: 0,
children: []
}
]
};
}
/**
* Calculate process statistics
*/
calculateProcessStatistics(tree) {
const flatten = (node) => [
node,
...node.children.flatMap(flatten)
];
const allProcesses = flatten(tree);
const totalMemory = allProcesses.reduce((sum, p) => sum + p.memory, 0);
const avgQueueLength = allProcesses.reduce((sum, p) => sum + p.messageQueueLength, 0) / allProcesses.length;
return {
totalProcesses: allProcesses.length,
memoryTotal: `${Math.round(totalMemory / 1024 / 1024)} MB`,
liveViewProcesses: allProcesses.filter(p => p.type === 'liveview').length,
supervisorProcesses: allProcesses.filter(p => p.type === 'supervisor').length,
averageMessageQueueLength: Math.round(avgQueueLength * 100) / 100
};
}
/**
* Generate process warnings
*/
generateProcessWarnings(tree) {
const warnings = [];
const flatten = (node) => [
node,
...node.children.flatMap(flatten)
];
const allProcesses = flatten(tree);
allProcesses.forEach(process => {
if (process.memory > 10 * 1024 * 1024) { // 10MB
warnings.push(`High memory usage: ${process.name} (${Math.round(process.memory / 1024 / 1024)}MB)`);
}
if (process.messageQueueLength > 100) {
warnings.push(`Large message queue: ${process.name} (${process.messageQueueLength} messages)`);
}
});
return warnings;
}
/**
* Trace PubSub messages (mock implementation)
*/
async tracePubSubMessages(session, topics, eventFilter, duration = 60000) {
// Mock implementation - would integrate with Phoenix.PubSub tracing
return [
{
topic: 'users:lobby',
event: 'user_joined',
payload: { user_id: 123, name: 'John' },
timestamp: Date.now(),
source: 'UserChannel',
destination: ['LiveView', 'UserNotifier']
},
{
topic: 'system:stats',
event: 'memory_update',
payload: { memory: '1.2GB', cpu: '45%' },
timestamp: Date.now() - 5000,
source: 'SystemMonitor',
destination: ['AdminLiveView']
}
];
}
/**
* Analyze PubSub message patterns
*/
analyzePubSubMessages(messages) {
const uniqueTopics = [...new Set(messages.map(m => m.topic))];
const messageFrequency = messages.reduce((acc, msg) => {
acc[msg.event] = (acc[msg.event] || 0) + 1;
return acc;
}, {});
const avgPayloadSize = messages.reduce((sum, msg) => sum + JSON.stringify(msg.payload).length, 0) / messages.length;
const messageFlow = messages.reduce((acc, msg) => {
msg.destination.forEach(dest => {
const key = `${msg.source} -> ${dest}`;
acc[key] = (acc[key] || 0) + 1;
});
return acc;
}, {});
return {
totalMessages: messages.length,
uniqueTopics,
messageFrequency,
averagePayloadSize: Math.round(avgPayloadSize),
messageFlow: Object.entries(messageFlow).map(([flow, count]) => {
const [from, to] = flow.split(' -> ');
return { from, to, count };
})
};
}
/**
* Generate PubSub recommendations
*/
generatePubSubRecommendations(analysis) {
const recommendations = [];
if (analysis.averagePayloadSize > 1000) {
recommendations.push('Consider reducing PubSub payload size for better performance');
}
if (analysis.totalMessages > 100) {
recommendations.push('High message volume detected - consider message batching or filtering');
}
if (analysis.uniqueTopics.length > 20) {
recommendations.push('Many unique topics - consider topic namespacing strategy');
}
return recommendations;
}
/**
* Scan files for line count violations
*/
async scanFileViolations(projectPath, threshold, patterns) {
// Mock implementation - would use glob and fs to scan actual files
return [
{
file: 'lib/my_app_web/live/dashboard_live.ex',
currentLines: 450,
threshold: 300,
violationLevel: 'error',
recommendations: [
'Extract helper functions to separate module',
'Break down complex handle_event functions',
'Consider splitting into multiple LiveViews'
]
},
{
file: 'lib/my_app/accounts/user.ex',
currentLines: 320,
threshold: 300,
violationLevel: 'warning',
recommendations: [
'Extract validation logic to separate module',
'Move complex queries to context module'
]
}
];
}
/**
* Calculate file summary statistics
*/
calculateFileSummary(violations, projectPath, patterns) {
// Mock implementation
return {
totalFiles: 45,
violatingFiles: violations.length,
averageFileSize: 180,
largestFile: {
path: 'lib/my_app_web/live/dashboard_live.ex',
lines: 450
}
};
}
/**
* Set up real-time file monitoring
*/
async setupRealTimeFileMonitoring(sessionId, projectPath, threshold, patterns) {
// Mock implementation - would set up file watchers
return {
enabled: true,
monitoringId: `monitor_${sessionId}_${Date.now()}`
};
}
}
//# sourceMappingURL=phoenix-liveview-enhanced-handler.js.map