import { tool } from "ai"; import z from "zod"; import { runnerService } from "../services/runner/index.ts"; import type { LogEntry } from "../services/runner/types.ts"; /** * Development Server Control Tool * * A comprehensive diagnostic and monitoring tool for managing and testing running development servers. * * IMPORTANT: Agents MUST check the status of development servers using this tool every time * before attempting to run or interact with dev servers. This ensures proper server state * management and prevents conflicts. * * Provides two main functionalities: * * 1. Process Discovery: Lists all currently running processes with detailed information * 2. Process Diagnostics: Tests specific processes by making HTTP calls and capturing fresh logs * * Features: * - Process listing with status, URLs, uptime, and system information * - HTTP testing with customizable endpoints, methods, and timeouts * - Fresh log capture (only logs generated after HTTP call) * - Error handling for connection failures and server issues * - Response body truncation for readability * * Usage Examples: * - devServer.execute({}) // List all processes - USE THIS FIRST ALWAYS * - devServer.execute({ processId: "server-id" }) // Test specific process * - devServer.execute({ processId: "api", endpoint: "/health", method: "POST" }) // Custom endpoint test */ export const devServer = tool({ description: `Development server control and diagnostic tool for managing running dev servers. CRITICAL: Agents MUST use this tool to check server status EVERY TIME when need to communicate with dev server, don't use terminal tool for development server. This tool provides two main functionalities: 1. PROCESS DISCOVERY (when no processId provided) - ALWAYS RUN THIS FIRST: - Lists all currently running development server processes - Shows process status, URLs, commands, and system information - Displays uptime and start times - Helps identify available processes for testing - REQUIRED before starting new servers or making server requests 2. PROCESS DIAGNOSTICS (when processId provided): - Gets server information for the specified process - Captures current logs before making HTTP call - Makes HTTP call to server to trigger activity - Captures new logs after the HTTP call - Returns only fresh logs generated by the HTTP call This is essential for: debugging server responses, checking if servers are responding correctly, discovering what processes are running, getting real-time logs, and preventing server conflicts.`, inputSchema: z.object({ processId: z .string() .optional() .describe( "The ID of the process to get logs from. If not provided, returns information about all current processes" ), endpoint: z .string() .default("/") .describe("The endpoint to call on the server (default: '/')"), method: z .enum(["GET", "POST", "PUT", "DELETE", "PATCH"]) .default("GET") .describe("HTTP method to use for the request"), timeout: z .number() .int() .positive() .max(30000) .default(10000) .describe("Request timeout in milliseconds (max 30 seconds)"), }), execute: async ({ processId, endpoint = "/", method = "GET", timeout = 10000, }) => { try { // If no processId provided, return information about all processes if (!processId) { const allProcesses = runnerService.getAllProcesses(); if (allProcesses.length === 0) { return `No running processes found. Start a development server first using one of the available runner commands.`; } let result = `Current Running Processes (${allProcesses.length}):\n\n`; allProcesses.forEach((process, index) => { const { id, info } = process; result += `${index + 1}. Process ID: ${id}\n`; result += ` Name: ${info.name}\n`; result += ` Status: ${info.status}\n`; result += ` URL: ${info.serverUrl || "N/A"}\n`; result += ` Command: ${info.command}\n`; result += ` CWD: ${info.cwd}\n`; result += ` PID: ${info.pid || "N/A"}\n`; result += ` Uptime: ${ info.uptime ? Math.round(info.uptime / 1000) : "unknown" } seconds\n`; result += ` Start Time: ${ info.startTime ? info.startTime.toISOString() : "N/A" }\n`; // Add logs for each process const processLogs = runnerService.getProcessLogs(id); if (processLogs.length > 0) { result += ` Recent Logs (last ${Math.min( processLogs.length, 10 )} entries):\n`; const recentLogs = processLogs.slice(-10); // Get last 10 log entries recentLogs.forEach((log: LogEntry) => { const timestamp = log.timestamp.toISOString(); const type = log.type.toUpperCase(); result += ` [${timestamp}] ${type}: ${log.content}\n`; }); } else { result += ` Recent Logs: No logs available\n`; } result += `\n`; }); result += `To get diagnostic logs for a specific process, use:\n`; result += `devServer({ processId: "process-id" })`; return result; } // Step 1: Get server info const serverInfo = runnerService.getServerInfo(processId); if (!serverInfo) { return `Error: Process with ID '${processId}' not found. Use the runner tool without processId to list all running processes.`; } if (serverInfo.status !== "active") { return `Error: Process '${processId}' is not active. Current status: ${serverInfo.status}`; } if (!serverInfo.serverUrl) { return `Error: No server URL found for process '${processId}'. The process might not be a web server.`; } // Step 2: Get current logs count (before HTTP call) const logsBeforeCount = runnerService.getProcessLogs(processId).length; // Step 3: Make HTTP call to the server const url = `${serverInfo.serverUrl}${ endpoint.startsWith("/") ? endpoint : "/" + endpoint }`; let httpCallResult: string; try { const response = await fetch(url, { method, headers: { "User-Agent": "Chara-Runner-Diagnostic-Tool/1.0", Accept: "text/html,application/json,*/*", }, signal: AbortSignal.timeout(timeout), }); httpCallResult = `HTTP ${method} ${url} - Status: ${response.status} ${response.statusText}`; // Try to get response body for additional context (limit to 500 chars) try { const responseText = await response.text(); if (responseText.length > 0) { const truncatedResponse = responseText.length > 500 ? responseText.substring(0, 500) + "..." : responseText; httpCallResult += `\nResponse body: ${truncatedResponse}`; } } catch { // Ignore response body read errors } } catch (error) { if (error instanceof Error) { httpCallResult = `HTTP ${method} ${url} - Error: ${error.message}`; } else { httpCallResult = `HTTP ${method} ${url} - Unknown error occurred`; } } // Step 4: Get logs after HTTP call const logsAfter = runnerService.getProcessLogs(processId); // Step 5: Extract only fresh logs (logs that were added after the HTTP call) const freshLogs = logsAfter.slice(logsBeforeCount); // Format the result let result = `Dev Server Diagnostic for Process: ${processId}\n`; result += `Server: ${serverInfo.name} (${serverInfo.status})\n`; result += `URL: ${serverInfo.serverUrl}\n`; result += `Uptime: ${ serverInfo.uptime ? Math.round(serverInfo.uptime / 1000) : "unknown" } seconds\n\n`; result += `HTTP Call Result:\n${httpCallResult}\n\n`; if (freshLogs.length === 0) { result += `No new logs generated from the HTTP call.`; } else { result += `Fresh Logs (${freshLogs.length} new entries):\n`; result += freshLogs .map((log: LogEntry) => { const timestamp = log.timestamp.toISOString(); const type = log.type.toUpperCase(); return `[${timestamp}] ${type}: ${log.content}`; }) .join("\n"); } return result; } catch (error) { if (error instanceof Error) { return `Error running diagnostic: ${error.message}`; } return `Unknown error occurred while running diagnostic`; } }, });