Code

/**
 * mcp/skills/restartProcess.ts — restart_process skill
 *
 * Terminates a process by name or PID, waits briefly, then re-launches it.
 * Use when a process is hung or unresponsive but needs to keep running
 * (e.g. security agent, VPN client, Finder).
 *
 * Platform strategy
 * -----------------
 * darwin  `kill -TERM {pid}` then optionally `open -a {name}` or exec launchPath
 * win32   PowerShell Stop-Process then Start-Process
 *
 * Smoke test
 *   npx tsx -r dotenv/config mcp/skills/restartProcess.ts --name Finder
 */
 
import * as os       from "os";
import { exec }      from "child_process";
import { promisify } from "util";
import { z }         from "zod";
 
const execAsync = promisify(exec);
 
// -- Meta ---------------------------------------------------------------------
 
export const meta = {
  name: "restart_process",
  description:
    "Terminates a process by name or PID, waits briefly, then re-launches it. " +
    "Use when a process is hung or unresponsive but needs to keep running " +
    "(e.g. security agent, VPN client, Finder).",
  riskLevel:       "medium",
  destructive:     true,
  requiresConsent: true,
  supportsDryRun:  false,
  affectedScope:   ["user"],
  auditRequired:   true,
  schema: {
    name: z
      .string()
      .optional()
      .describe("Process name to restart (e.g. 'Finder', 'CrowdStrikeFalconSensor')"),
    pid: z
      .number()
      .optional()
      .describe("Process ID to restart. Use instead of name for precision"),
    launchPath: z
      .string()
      .optional()
      .describe("Full path to re-launch after killing. Required if process doesn't relaunch itself."),
  },
} as const;
 
// -- Types --------------------------------------------------------------------
 
interface RestartResult {
  killed:     boolean;
  relaunched: boolean;
  newPid:     number | null;
  message:    string;
}
 
// -- Helpers ------------------------------------------------------------------
 
function sleep(ms: number): Promise<void> {
  return new Promise((resolve) => setTimeout(resolve, ms));
}
 
/** Validate a process name — only allow simple identifiers to prevent injection. */
function validateName(name: string): boolean {
  return /^[a-zA-Z0-9_\-. ]+$/.test(name);
}
 
/** Validate a launch path — must be absolute and not contain shell metacharacters. */
function validateLaunchPath(p: string): boolean {
  return nodePath.isAbsolute(p) && /^[a-zA-Z0-9_\-./: ]+$/.test(p);
}
 
import * as nodePath from "path";
 
// -- PowerShell helper --------------------------------------------------------
 
async function runPS(script: string): Promise<string> {
  const encoded = Buffer.from(script, "utf16le").toString("base64");
  const { stdout } = await execAsync(
    `powershell.exe -NoProfile -NonInteractive -EncodedCommand ${encoded}`,
    { maxBuffer: 10 * 1024 * 1024 },
  );
  return stdout.trim();
}
 
// -- darwin implementation ----------------------------------------------------
 
async function getFirstPidByName(name: string): Promise<number | null> {
  try {
    const { stdout } = await execAsync(
      `pgrep -n -x '${name.replace(/'/g, "'\\''")}' 2>/dev/null`,
    );
    const pid = parseInt(stdout.trim(), 10);
    return isNaN(pid) ? null : pid;
  } catch {
    return null;
  }
}
 
async function restartProcessDarwin(
  name:       string | undefined,
  pid:        number | undefined,
  launchPath: string | undefined,
): Promise<RestartResult> {
  // Resolve PID
  let targetPid = pid ?? null;
  if (!targetPid && name) {
    if (!validateName(name)) {
      throw new Error(`[restart_process] Invalid process name: ${name}`);
    }
    targetPid = await getFirstPidByName(name);
  }
 
  if (!targetPid) {
    return {
      killed:     false,
      relaunched: false,
      newPid:     null,
      message:    name ? `No running process found with name: ${name}` : "No PID or name provided",
    };
  }
 
  // Kill the process
  let killed = false;
  try {
    await execAsync(`kill -TERM ${targetPid} 2>&1`);
    killed = true;
  } catch (err) {
    return {
      killed:     false,
      relaunched: false,
      newPid:     null,
      message:    `Failed to terminate PID ${targetPid}: ${(err as Error).message}`,
    };
  }
 
  // Brief pause to allow process to clean up
  await sleep(1500);
 
  // Relaunch
  if (launchPath) {
    if (!validateLaunchPath(launchPath)) {
      return { killed, relaunched: false, newPid: null, message: `Invalid launch path: ${launchPath}` };
    }
    try {
      const { stdout } = await execAsync(`'${launchPath.replace(/'/g, "'\\''")}' &`);
      const newPid = parseInt(stdout.trim(), 10);
      return {
        killed,
        relaunched: true,
        newPid:     isNaN(newPid) ? null : newPid,
        message:    `Process killed and re-launched via ${launchPath}`,
      };
    } catch (err) {
      return { killed, relaunched: false, newPid: null, message: `Killed but relaunch failed: ${(err as Error).message}` };
    }
  }
 
  if (name) {
    if (!validateName(name)) {
      return { killed, relaunched: false, newPid: null, message: `Process killed. Invalid name for relaunch.` };
    }
    try {
      await execAsync(`open -a '${name.replace(/'/g, "'\\''")}' 2>&1`);
      await sleep(1000);
      const newPid = await getFirstPidByName(name);
      return {
        killed,
        relaunched: true,
        newPid:     newPid ?? null,
        message:    `Process killed and re-launched via 'open -a ${name}'`,
      };
    } catch (err) {
      return {
        killed,
        relaunched: false,
        newPid:     null,
        message:    `Killed PID ${targetPid} but relaunch via open -a failed: ${(err as Error).message}`,
      };
    }
  }
 
  return {
    killed,
    relaunched: false,
    newPid:     null,
    message:    `Killed PID ${targetPid}. No launchPath or name provided for relaunch.`,
  };
}
 
// -- win32 implementation -----------------------------------------------------
 
async function restartProcessWin32(
  name:       string | undefined,
  pid:        number | undefined,
  launchPath: string | undefined,
): Promise<RestartResult> {
  if (!name && !pid) {
    return { killed: false, relaunched: false, newPid: null, message: "No PID or name provided" };
  }
  if (name && !validateName(name)) {
    throw new Error(`[restart_process] Invalid process name: ${name}`);
  }
 
  const stopTarget = pid ? `-Id ${pid}` : `-Name '${name!.replace(/'/g, "''")}'`;
 
  const stopPs = `
$ErrorActionPreference = 'Stop'
Stop-Process ${stopTarget} -Force
Write-Output 'killed'`.trim();
 
  let killed = false;
  try {
    const out = await runPS(stopPs);
    killed    = out.includes("killed");
  } catch (err) {
    return {
      killed:     false,
      relaunched: false,
      newPid:     null,
      message:    `Failed to stop process: ${(err as Error).message}`,
    };
  }
 
  await sleep(1500);
 
  if (!launchPath && !name) {
    return { killed, relaunched: false, newPid: null, message: `Process killed. No launch path for relaunch.` };
  }
 
  const startCmd = launchPath
    ? `Start-Process -FilePath '${launchPath.replace(/'/g, "''")}' -PassThru`
    : `Start-Process -FilePath '${name!.replace(/'/g, "''")}' -PassThru`;
 
  const launchPs = `
$ErrorActionPreference = 'Stop'
$p = ${startCmd}
$p.Id`.trim();
 
  try {
    const out    = await runPS(launchPs);
    const newPid = parseInt(out.trim(), 10);
    return {
      killed,
      relaunched: true,
      newPid:     isNaN(newPid) ? null : newPid,
      message:    `Process killed and re-launched`,
    };
  } catch (err) {
    return {
      killed,
      relaunched: false,
      newPid:     null,
      message:    `Killed but relaunch failed: ${(err as Error).message}`,
    };
  }
}
 
// -- Exported run function ----------------------------------------------------
 
export async function run({
  name,
  pid,
  launchPath,
}: {
  name?:       string;
  pid?:        number;
  launchPath?: string;
} = {}) {
  if (!name && !pid) {
    throw new Error("[restart_process] Either name or pid must be provided");
  }
 
  const platform = os.platform();
  return platform === "win32"
    ? restartProcessWin32(name, pid, launchPath)
    : restartProcessDarwin(name, pid, launchPath);
}
 
// -- CLI smoke test -----------------------------------------------------------
 
if (false) {
  run({})
    .then((r) => console.log(JSON.stringify(r, null, 2)))
    .catch((err: Error) => { console.error(err.message); process.exit(1); });
}