import { parametersSchema as z, defineCustomTool, CustomToolContext } from "@roo-code/types" // @ts-ignore - Node built-ins import { spawnSync } from "child_process" export default defineCustomTool({ name: "port_watch", description: "Check what process holds a given TCP/UDP port. Replaces 'lsof -i :PORT' shell calls when debugging dev servers.", parameters: z.object({ port: z.number().describe("TCP port number to check (1-65535)"), protocol: z.enum(["tcp", "udp", "both"]).optional().describe("Protocol filter (default: tcp)"), }), async execute({ port, protocol = "tcp" }, context: CustomToolContext) { try { if (port < 1 || port > 65535) { return JSON.stringify({ error: "Invalid port: must be between 1 and 65535" }, null, 2) } const listeners: any[] = [] const runLsof = (proto: string) => { const flag = proto === "tcp" ? `-iTCP:${port}` : `-iUDP:${port}` const stateFlag = proto === "tcp" ? ["-sTCP:LISTEN"] : [] const result = spawnSync("lsof", [flag, ...stateFlag, "-P", "-n"], { encoding: "utf-8", timeout: 10_000, }) if (!result.stdout) return const lines = result.stdout.split("\n").filter((l: string) => l.trim()) // Skip header line for (let i = 1; i < lines.length; i++) { const parts = lines[i].split(/\s+/) if (parts.length < 9) continue listeners.push({ pid: parseInt(parts[1], 10) || 0, command: parts[0], user: parts[2], type: parts[4], // IPv4 or IPv6 state: parts[9] || (proto === "udp" ? "UDP" : "UNKNOWN"), address: parts[8] || `*:${port}`, }) } } if (protocol === "tcp" || protocol === "both") { runLsof("tcp") } if (protocol === "udp" || protocol === "both") { runLsof("udp") } return JSON.stringify({ port, protocol, inUse: listeners.length > 0, listeners, }, null, 2) } catch (err: any) { return JSON.stringify({ error: err.message ?? String(err) }, null, 2) } }, })