Code

/**
 * mcp/skills/getMemoryPressure.ts — get_memory_pressure skill
 *
 * Reports current memory pressure level, RAM usage breakdown, and swap usage.
 * Use when diagnosing system slowness caused by memory exhaustion.
 *
 * Platform strategy
 * -----------------
 * darwin  memory_pressure + vm_stat + sysctl hw.memsize
 * win32   PowerShell Win32_OperatingSystem + Get-Counter Pages/sec
 *
 * Smoke test
 *   npx tsx -r dotenv/config mcp/skills/getMemoryPressure.ts
 */
 
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: "get_memory_pressure",
  description:
    "Reports current memory pressure level, RAM usage breakdown, and swap " +
    "usage. Use when diagnosing system slowness caused by memory exhaustion.",
  riskLevel:       "low",
  destructive:     false,
  requiresConsent: false,
  supportsDryRun:  false,
  affectedScope:   ["user"],
  auditRequired:   false,
  schema: {} as Record<string, ReturnType<typeof z.string>>,
} as const;
 
// -- Types --------------------------------------------------------------------
 
type PressureLevel = "normal" | "warn" | "critical";
 
interface MemoryPressureResult {
  totalRamMb:    number;
  usedRamMb:     number;
  freeRamMb:     number;
  swapUsedMb:    number;
  pressureLevel: PressureLevel;
  pageIns:       number;
  pageOuts:      number;
}
 
// -- 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 getMemoryPressureDarwin(): Promise<MemoryPressureResult> {
  // Total RAM
  const { stdout: memSizeOut } = await execAsync("sysctl -n hw.memsize 2>/dev/null");
  const totalBytes  = parseInt(memSizeOut.trim(), 10);
  const totalRamMb  = Math.round(totalBytes / (1024 * 1024));
 
  // Pressure level
  let pressureLevel: PressureLevel = "normal";
  try {
    const { stdout: pressureOut } = await execAsync("memory_pressure 2>/dev/null");
    const text = pressureOut.toLowerCase();
    if (text.includes("critical")) {
      pressureLevel = "critical";
    } else if (text.includes("warn")) {
      pressureLevel = "warn";
    }
  } catch {
    // memory_pressure may not be available in all contexts
  }
 
  // vm_stat for page stats and free memory
  let freeRamMb  = 0;
  let swapUsedMb = 0;
  let pageIns    = 0;
  let pageOuts   = 0;
 
  try {
    const { stdout: vmOut } = await execAsync("vm_stat 2>/dev/null");
    const pageSize = 4096; // macOS default page size in bytes
 
    const parseVmStat = (label: string): number => {
      const match = vmOut.match(new RegExp(`${label}[^\\d]*(\\d+)`));
      return match ? parseInt(match[1], 10) : 0;
    };
 
    const freePages      = parseVmStat("Pages free");
    const speculativeP   = parseVmStat("Pages speculative");
    pageIns              = parseVmStat("Pageins");
    pageOuts             = parseVmStat("Pageouts");
 
    freeRamMb  = Math.round(((freePages + speculativeP) * pageSize) / (1024 * 1024));
 
    // Swap usage from sysctl
    try {
      const { stdout: swapOut } = await execAsync("sysctl -n vm.swapusage 2>/dev/null");
      const swapMatch = swapOut.match(/used\s*=\s*([\d.]+)M/i);
      if (swapMatch) swapUsedMb = Math.round(parseFloat(swapMatch[1]));
    } catch {
      // swap info unavailable
    }
  } catch {
    // vm_stat unavailable
    freeRamMb = Math.round(os.freemem() / (1024 * 1024));
  }
 
  const usedRamMb = Math.max(0, totalRamMb - freeRamMb);
 
  return { totalRamMb, usedRamMb, freeRamMb, swapUsedMb, pressureLevel, pageIns, pageOuts };
}
 
// -- win32 implementation -----------------------------------------------------
 
async function getMemoryPressureWin32(): Promise<MemoryPressureResult> {
  const ps = `
$ErrorActionPreference = 'SilentlyContinue'
$os = Get-CimInstance Win32_OperatingSystem | Select-Object TotalVisibleMemorySize,FreePhysicalMemory,SizeStoredInPagingFiles,FreeSpaceInPagingFiles
$pagesSec = 0
try {
  $counter = Get-Counter '\\Memory\\Pages/sec' -SampleInterval 1 -MaxSamples 1
  $pagesSec = [int]$counter.CounterSamples[0].CookedValue
} catch {}
[PSCustomObject]@{
  totalKb      = [long]$os.TotalVisibleMemorySize
  freeKb       = [long]$os.FreePhysicalMemory
  pagingTotalKb= [long]$os.SizeStoredInPagingFiles
  pagingFreeKb = [long]$os.FreeSpaceInPagingFiles
  pagesSec     = $pagesSec
} | ConvertTo-Json -Compress`.trim();
 
  const raw  = await runPS(ps);
  const data = JSON.parse(raw) as {
    totalKb:       number;
    freeKb:        number;
    pagingTotalKb: number;
    pagingFreeKb:  number;
    pagesSec:      number;
  };
 
  const totalRamMb  = Math.round(data.totalKb / 1024);
  const freeRamMb   = Math.round(data.freeKb  / 1024);
  const usedRamMb   = Math.max(0, totalRamMb - freeRamMb);
  const swapUsedMb  = Math.round((data.pagingTotalKb - data.pagingFreeKb) / 1024);
 
  const usedRatio   = totalRamMb > 0 ? usedRamMb / totalRamMb : 0;
  const pressureLevel: PressureLevel =
    usedRatio > 0.90 ? "critical" :
    usedRatio > 0.75 ? "warn"     : "normal";
 
  return {
    totalRamMb,
    usedRamMb,
    freeRamMb,
    swapUsedMb,
    pressureLevel,
    pageIns:  data.pagesSec,
    pageOuts: 0,
  };
}
 
// -- Exported run function ----------------------------------------------------
 
export async function run(_args: Record<string, never> = {}): Promise<MemoryPressureResult> {
  const platform = os.platform();
  if (platform === "win32") {
    return getMemoryPressureWin32();
  }
  return getMemoryPressureDarwin();
}
 
// -- Smoke test ---------------------------------------------------------------
 
if (false) {
  run({})
    .then(r => console.log(JSON.stringify(r, null, 2)))
    .catch((err: Error) => { console.error(err.message); process.exit(1); });
}