/** * pi-sysmon — Cross-Platform System Monitor & Resource Manager * * Works on Windows, macOS, and Linux. * Real-time system health: RAM, disk, processes, cleanup. * Carmack principle: measure everything, cut the waste. * * /sysmon → quick health dashboard * /sysmon top → top processes by memory * /sysmon hogs → wasteful processes (>200MB) * /sysmon kill → kill process by name * /sysmon clean → run disk cleanup (npm, pnpm, temp) * /sysmon node → audit all node processes * /sysmon chrome → chrome tab memory breakdown * /sysmon disk → disk usage by directory * /sysmon history → resource snapshots over time * * Tools: sysmon_status, sysmon_top, sysmon_kill, sysmon_clean */ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent"; import { Type } from "@sinclair/typebox"; import { existsSync, readFileSync, writeFileSync, appendFileSync, mkdirSync } from "node:fs"; import { join } from "node:path"; import { homedir, platform, totalmem, freemem } from "node:os"; import { execSync } from "node:child_process"; const DIR = join(homedir(), ".pi", "sysmon"); const HISTORY = join(DIR, "history.jsonl"); const RST = "\x1b[0m", B = "\x1b[1m", D = "\x1b[2m"; const G = "\x1b[32m", R = "\x1b[31m", Y = "\x1b[33m", C = "\x1b[36m", M = "\x1b[35m"; const OS = platform(); const IS_WIN = OS === "win32"; const IS_MAC = OS === "darwin"; const IS_LIN = OS === "linux"; function ensureDir() { if (!existsSync(DIR)) mkdirSync(DIR, { recursive: true }); } function run(cmd: string, timeout = 12000): string { try { return execSync(cmd, { encoding: "utf-8", timeout, stdio: ["pipe", "pipe", "pipe"] }).trim(); } catch { return ""; } } function psFile(script: string, timeout = 15000): string { if (!IS_WIN) return ""; ensureDir(); const f = join(DIR, "_tmp.ps1"); writeFileSync(f, script); try { return execSync(`powershell.exe -NoProfile -File "${f}"`, { encoding: "utf-8", timeout }).trim(); } catch { return ""; } } // ─── Cross-platform primitives ───────────────────────────────────── interface AppGroup { name: string; count: number; mb: number } interface SysStatus { os: string; ramFreeMB: number; ramTotalMB: number; ramPct: number; diskFreeGB: number; diskTotalGB: number; diskPct: number; procs: number; topApps: AppGroup[]; } function getRam(): { freeMB: number; totalMB: number; pct: number } { const totalMB = Math.round(totalmem() / 1024 / 1024); const freeMB = Math.round(freemem() / 1024 / 1024); const pct = Math.round((1 - freeMB / totalMB) * 100); return { freeMB, totalMB, pct }; } function getDisk(): { freeGB: number; totalGB: number; pct: number } { if (IS_WIN) { const raw = psFile(` $d = Get-PSDrive C $free = [math]::Round($d.Free / 1GB, 1) $total = [math]::Round(($d.Free + $d.Used) / 1GB, 1) $pct = [math]::Round($d.Used / ($d.Free + $d.Used) * 100) Write-Output "$free|$total|$pct" `); const [f, t, p] = raw.split("|"); return { freeGB: parseFloat(f) || 0, totalGB: parseFloat(t) || 0, pct: parseInt(p) || 0 }; } // macOS / Linux: df on root const raw = run("df -k / | tail -1"); if (!raw) return { freeGB: 0, totalGB: 0, pct: 0 }; const cols = raw.split(/\s+/); // df -k output: Filesystem 1K-blocks Used Available Use% Mounted const totalKB = parseInt(cols[1]) || 0; const availKB = parseInt(cols[3]) || 0; const freeGB = +(availKB / 1024 / 1024).toFixed(1); const totalGB = +(totalKB / 1024 / 1024).toFixed(1); const pct = totalGB ? Math.round((1 - freeGB / totalGB) * 100) : 0; return { freeGB, totalGB, pct }; } function getTopApps(): { apps: AppGroup[]; total: number } { if (IS_WIN) { const raw = psFile(` $top = Get-Process | Group-Object Name | ForEach-Object { [PSCustomObject]@{ N = $_.Name; C = $_.Count; M = [math]::Round(($_.Group | Measure-Object WS -Sum).Sum/1MB) } } | Sort-Object M -Descending | Select-Object -First 12 $count = (Get-Process).Count $json = $top | ConvertTo-Json -Compress Write-Output "$count|$json" `); const pipeIdx = raw.indexOf("|"); const total = parseInt(raw.slice(0, pipeIdx)) || 0; let apps: AppGroup[] = []; try { const parsed = JSON.parse(raw.slice(pipeIdx + 1)); apps = (Array.isArray(parsed) ? parsed : [parsed]).map((a: any) => ({ name: a.N, count: a.C, mb: a.M })); } catch {} return { apps, total }; } if (IS_MAC) { // macOS: ps with rss (in KB) const raw = run("ps -eo comm=,rss= | sort -k2 -rn | head -40"); return parseUnixPs(raw); } // Linux: ps with rss const raw = run("ps -eo comm=,rss= --sort=-rss | head -40"); return parseUnixPs(raw); } function parseUnixPs(raw: string): { apps: AppGroup[]; total: number } { if (!raw) return { apps: [], total: 0 }; const grouped: Record = {}; for (const line of raw.split("\n")) { const match = line.trim().match(/^(\S+)\s+(\d+)$/); if (!match) continue; const name = match[1].replace(/.*\//, ""); // strip path const kb = parseInt(match[2]) || 0; if (!grouped[name]) grouped[name] = { count: 0, kb: 0 }; grouped[name].count++; grouped[name].kb += kb; } const apps = Object.entries(grouped) .map(([name, g]) => ({ name, count: g.count, mb: Math.round(g.kb / 1024) })) .sort((a, b) => b.mb - a.mb) .slice(0, 12); const total = parseInt(run("ps -e | wc -l") || "0"); return { apps, total }; } function getStatus(): SysStatus { const ram = getRam(); const disk = getDisk(); const { apps, total } = getTopApps(); return { os: IS_WIN ? "windows" : IS_MAC ? "macos" : "linux", ramFreeMB: ram.freeMB, ramTotalMB: ram.totalMB, ramPct: ram.pct, diskFreeGB: disk.freeGB, diskTotalGB: disk.totalGB, diskPct: disk.pct, procs: total, topApps: apps, }; } // ─── Node audit (cross-platform) ────────────────────────────────── function getNodeAudit(): string { if (IS_WIN) { return psFile(` Get-CimInstance Win32_Process | Where-Object { $_.Name -eq 'node.exe' } | Select-Object ProcessId, @{N='MB';E={[math]::Round($_.WorkingSetSize/1MB)}}, @{N='Cmd';E={ $c = $_.CommandLine if ($c.Length -gt 120) { $c.Substring(0, 120) + '...' } else { $c } }} | Sort-Object MB -Descending | Format-Table -AutoSize -Wrap | Out-String `) || "No node processes found."; } // macOS / Linux: ps with full command const raw = run("ps -eo pid,rss,args | grep '[n]ode' | sort -k2 -rn"); if (!raw) return "No node processes found."; let out = " PID MB Command\n ─────── ── ───────\n"; for (const line of raw.split("\n")) { const match = line.trim().match(/^(\d+)\s+(\d+)\s+(.+)$/); if (!match) continue; const pid = match[1]; const mb = Math.round(parseInt(match[2]) / 1024); const cmd = match[3].length > 120 ? match[3].slice(0, 120) + "..." : match[3]; out += ` ${pid.padStart(7)} ${String(mb).padStart(3)} ${cmd}\n`; } return out; } // ─── Chrome audit (cross-platform) ──────────────────────────────── function getChromeAudit(): string { if (IS_WIN) { return psFile(` $chrome = Get-Process chrome -ErrorAction SilentlyContinue if (-not $chrome) { Write-Output "Chrome not running."; exit } $total = [math]::Round(($chrome | Measure-Object WS -Sum).Sum/1MB) Write-Output "Chrome: $($chrome.Count) processes, $total MB total" Write-Output "" $chrome | Sort-Object WS -Descending | Select-Object -First 15 Id, @{N='MB';E={[math]::Round($_.WS/1MB)}}, @{N='Threads';E={$_.Threads.Count}}, @{N='CPU_s';E={[math]::Round($_.CPU, 1)}} | Format-Table -AutoSize | Out-String `) || "Chrome not running."; } // macOS: "Google Chrome" process name. Linux: "chrome" const procName = IS_MAC ? "Google Chrome" : "chrome"; const grepPattern = IS_MAC ? "[G]oogle Chrome" : "[c]hrome"; const raw = run(`ps -eo pid,rss,comm | grep '${grepPattern}' | sort -k2 -rn`); if (!raw) return "Chrome not running."; const lines = raw.split("\n").filter(Boolean); let totalMB = 0; const procs: { pid: string; mb: number }[] = []; for (const line of lines) { const match = line.trim().match(/^(\d+)\s+(\d+)\s+(.+)$/); if (!match) continue; const mb = Math.round(parseInt(match[2]) / 1024); totalMB += mb; procs.push({ pid: match[1], mb }); } let out = `Chrome: ${procs.length} processes, ${totalMB} MB total\n\n`; out += ` ${"PID".padStart(7)} ${"MB".padStart(5)}\n`; out += ` ${"─".repeat(7)} ${"─".repeat(5)}\n`; for (const p of procs.slice(0, 15)) { out += ` ${p.pid.padStart(7)} ${String(p.mb).padStart(5)}\n`; } return out; } // ─── Disk breakdown (cross-platform) ────────────────────────────── function getDiskBreakdown(): string { const home = homedir(); if (IS_WIN) { return psFile(` $dirs = @( "$env:LOCALAPPDATA\\npm-cache", "$env:LOCALAPPDATA\\pnpm", "$env:LOCALAPPDATA\\nvm", "$env:LOCALAPPDATA\\Temp", "$env:LOCALAPPDATA\\Docker", "$env:LOCALAPPDATA\\Programs", "$env:USERPROFILE\\Downloads", "$env:USERPROFILE\\Music", "$env:USERPROFILE\\Documents", "$env:USERPROFILE\\Projects" ) foreach ($d in $dirs) { if (Test-Path $d) { try { $size = (Get-ChildItem $d -Recurse -Force -ErrorAction SilentlyContinue | Measure-Object Length -Sum).Sum if ($size -gt 100MB) { '{0,-45} {1,8:N1} GB' -f $d.Replace($env:USERPROFILE, '~'), ($size/1GB) } } catch {} } } `) || "Could not scan."; } // macOS / Linux: du on common dirs const dirs = IS_MAC ? ["Downloads", "Documents", "Music", "Movies", "Library/Caches", "Projects", ".npm", ".pnpm-store", ".local"] : ["Downloads", "Documents", "Music", "Projects", ".npm", ".pnpm-store", ".local", ".cache", "/tmp"]; let out = ""; for (const d of dirs) { const full = d.startsWith("/") ? d : join(home, d); if (!existsSync(full)) continue; const raw = run(`du -sm "${full}" 2>/dev/null | head -1`); if (!raw) continue; const mb = parseInt(raw.split(/\s/)[0]) || 0; if (mb > 100) { const display = full.replace(home, "~"); out += ` ${display.padEnd(45)} ${(mb / 1024).toFixed(1).padStart(8)} GB\n`; } } return out || "No large directories found."; } // ─── Cleanup (cross-platform) ───────────────────────────────────── function runClean(): string { const results: string[] = []; // npm cache try { run("npm cache clean --force", 30000); results.push("npm cache: cleaned"); } catch { results.push("npm cache: failed"); } // pnpm store try { const out = run("pnpm store prune", 30000); results.push(out ? `pnpm store: ${out}` : "pnpm store: pruned"); } catch { results.push("pnpm: not installed or empty"); } // Temp files if (IS_WIN) { const r = psFile(` $before = (Get-ChildItem $env:TEMP -Recurse -Force -ErrorAction SilentlyContinue | Measure-Object Length -Sum).Sum Get-ChildItem $env:TEMP -Recurse -Force -ErrorAction SilentlyContinue | Where-Object { $_.LastWriteTime -lt (Get-Date).AddDays(-7) -and -not $_.PSIsContainer } | Remove-Item -Force -ErrorAction SilentlyContinue $after = (Get-ChildItem $env:TEMP -Recurse -Force -ErrorAction SilentlyContinue | Measure-Object Length -Sum).Sum 'Cleaned: {0:N0} MB' -f (($before - $after) / 1MB) `); results.push(`Temp: ${r}`); } else { // macOS / Linux: clean /tmp files older than 7 days owned by current user const out = run(`find /tmp -user $(whoami) -type f -mtime +7 -delete 2>/dev/null && echo "cleaned" || echo "nothing to clean"`); results.push(`Temp: ${out}`); if (IS_MAC) { // Also clean Xcode derived data, Homebrew cache const xcodeClean = run('rm -rf ~/Library/Developer/Xcode/DerivedData/* 2>/dev/null && echo "Xcode DerivedData: cleaned" || echo "Xcode: nothing"'); results.push(xcodeClean); } if (IS_LIN) { // Clean user cache const cacheClean = run('find ~/.cache -type f -mtime +30 -delete 2>/dev/null && echo "~/.cache (>30d): cleaned" || echo "cache: nothing"'); results.push(cacheClean); } } return results.join("\n"); } // ─── Kill (cross-platform) ─────────────────────────────────────── function killByName(name: string): string { if (IS_WIN) { return psFile(` $procs = Get-Process -Name '${name}' -ErrorAction SilentlyContinue if (-not $procs) { Write-Output "No process named '${name}' found."; exit } $count = $procs.Count $mb = [math]::Round(($procs | Measure-Object WS -Sum).Sum/1MB) Stop-Process -Name '${name}' -Force -ErrorAction SilentlyContinue Write-Output "Killed $count '${name}' processes (freed ~$mb MB)" `); } // macOS / Linux: pkill + report // First count and measure const countRaw = run(`pgrep -c "${name}" 2>/dev/null`); const count = parseInt(countRaw) || 0; if (count === 0) return `No process matching '${name}' found.`; const rssRaw = run(`pgrep "${name}" | xargs -I{} ps -o rss= -p {} 2>/dev/null | awk '{s+=$1} END {print int(s/1024)}'`); const mb = parseInt(rssRaw) || 0; run(`pkill -f "${name}" 2>/dev/null`); return `Killed ${count} '${name}' processes (freed ~${mb} MB)`; } // ─── UI helpers ────────────────────────────────────────────────── function ramColor(pct: number): string { return pct > 90 ? R : pct > 75 ? Y : G; } function diskColor(freeGB: number): string { return freeGB < 10 ? R : freeGB < 30 ? Y : G; } function bar(pct: number, width = 20): string { const filled = Math.round(pct / (100 / width)); return "█".repeat(Math.min(filled, width)) + "░".repeat(Math.max(width - filled, 0)); } function snapshot(s: SysStatus) { ensureDir(); appendFileSync(HISTORY, JSON.stringify({ ts: new Date().toISOString(), os: s.os, ramFreeMB: s.ramFreeMB, ramPct: s.ramPct, diskFreeGB: s.diskFreeGB, procs: s.procs, }) + "\n"); } function dashboard(s: SysStatus): string { snapshot(s); const rc = ramColor(s.ramPct); const dc = diskColor(s.diskFreeGB); const osLabel = IS_WIN ? "Windows" : IS_MAC ? "macOS" : "Linux"; let out = `${B}${C}⚡ System Monitor${RST} ${D}(${osLabel})${RST}\n\n`; out += ` ${B}RAM${RST} ${rc}${bar(s.ramPct)}${RST} ${rc}${s.ramPct}%${RST} ${s.ramFreeMB.toLocaleString()} MB free / ${s.ramTotalMB.toLocaleString()} MB\n`; out += ` ${B}Disk${RST} ${dc}${bar(s.diskPct)}${RST} ${dc}${s.diskPct}%${RST} ${s.diskFreeGB} GB free / ${s.diskTotalGB} GB\n`; out += ` ${B}Procs${RST} ${s.procs}\n\n`; out += ` ${B}Top by Memory:${RST}\n`; for (const app of s.topApps.slice(0, 8)) { const color = app.mb > 500 ? R : app.mb > 200 ? Y : D; out += ` ${color}${app.name.padEnd(25)}${RST} ${String(app.mb).padStart(6)} MB (${app.count})\n`; } // Warnings const warnings: string[] = []; if (s.ramPct > 90) warnings.push(`RAM critical: ${s.ramFreeMB} MB free`); if (s.diskFreeGB < 10) warnings.push(`Disk critical: ${s.diskFreeGB} GB free`); const chrome = s.topApps.find(a => a.name === "chrome" || a.name === "Google Chrome" || a.name === "Google Chrome H"); if (chrome && chrome.mb > 1000) warnings.push(`Chrome: ${chrome.mb} MB across ${chrome.count} procs`); if (warnings.length) { out += `\n ${R}${B}Warnings:${RST}\n`; for (const w of warnings) out += ` ${R}⚠ ${w}${RST}\n`; } return out; } function getHistory(limit = 20): string { if (!existsSync(HISTORY)) return "No history yet. Run /sysmon to start tracking."; const lines = readFileSync(HISTORY, "utf-8").trim().split("\n").filter(Boolean); const entries = lines.slice(-limit).map(l => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean); if (!entries.length) return "No history entries."; let out = `${B}${C}📈 Resource History${RST} (last ${entries.length})\n\n`; out += ` ${"Time".padEnd(22)} ${"RAM Free".padStart(10)} ${"RAM %".padStart(7)} ${"Disk Free".padStart(10)} ${"Procs".padStart(6)}\n`; out += ` ${"─".repeat(22)} ${"─".repeat(10)} ${"─".repeat(7)} ${"─".repeat(10)} ${"─".repeat(6)}\n`; for (const e of entries) { const time = (e.ts || "").replace("T", " ").slice(0, 19); const rc = e.ramPct > 90 ? R : e.ramPct > 75 ? Y : G; const dc = e.diskFreeGB < 10 ? R : e.diskFreeGB < 30 ? Y : G; out += ` ${D}${time}${RST} ${rc}${String(e.ramFreeMB + " MB").padStart(10)}${RST} ${rc}${String(e.ramPct + "%").padStart(7)}${RST} ${dc}${String(e.diskFreeGB + " GB").padStart(10)}${RST} ${String(e.procs).padStart(6)}\n`; } return out; } // ─── Extension registration ───────────────────────────────────── export default function (pi: ExtensionAPI) { ensureDir(); pi.registerCommand("sysmon", { description: "System monitor: /sysmon [top|hogs|kill|clean|node|chrome|disk|history]", handler: async (args, ctx) => { const parts = (args || "").trim().split(/\s+/); const cmd = parts[0] || ""; if (!cmd || cmd === "status") { return dashboard(getStatus()); } if (cmd === "top") { const s = getStatus(); snapshot(s); let out = `${B}${C}📊 Top Processes by Memory${RST}\n\n`; for (const app of s.topApps) { const color = app.mb > 500 ? R : app.mb > 200 ? Y : D; const pct = s.ramTotalMB ? Math.round(app.mb / s.ramTotalMB * 100) : 0; out += ` ${color}${app.name.padEnd(25)}${RST} ${String(app.mb).padStart(6)} MB ${D}(${app.count} procs, ${pct}% RAM)${RST}\n`; } return out; } if (cmd === "hogs") { const s = getStatus(); const hogs = s.topApps.filter(a => a.mb > 200); if (!hogs.length) return `${G}No processes using >200 MB.${RST}`; let out = `${B}${R}🐷 Memory Hogs (>200 MB)${RST}\n\n`; let totalHog = 0; for (const app of hogs) { totalHog += app.mb; out += ` ${R}${app.name.padEnd(25)}${RST} ${String(app.mb).padStart(6)} MB (${app.count} procs)\n`; } out += `\n ${D}Total hog memory: ${totalHog.toLocaleString()} MB${RST}`; return out; } if (cmd === "kill") { const name = parts[1]; if (!name) return `${Y}Usage:${RST} /sysmon kill \nExample: /sysmon kill Discord`; return killByName(name); } if (cmd === "clean") { return `${B}${C}🧹 Disk Cleanup${RST}\n\n${runClean()}`; } if (cmd === "node") { return `${B}${C}📦 Node Process Audit${RST}\n\n${getNodeAudit()}`; } if (cmd === "chrome") { return `${B}${C}🌐 Chrome Audit${RST}\n\n${getChromeAudit()}`; } if (cmd === "disk") { return `${B}${C}💾 Disk Usage${RST}\n\n${getDiskBreakdown()}`; } if (cmd === "history") { const limit = parseInt(parts[1]) || 20; return getHistory(limit); } return `${B}${C}⚡ System Monitor${RST}\n /sysmon — dashboard\n /sysmon top — top processes\n /sysmon hogs — memory hogs (>200MB)\n /sysmon kill — kill by name\n /sysmon clean — disk cleanup\n /sysmon node — node process audit\n /sysmon chrome — chrome tab breakdown\n /sysmon disk — disk usage\n /sysmon history — resource snapshots`; } }); pi.registerTool({ name: "sysmon_status", description: "Get system health: RAM usage, disk space, top processes, warnings. Cross-platform (Windows/macOS/Linux).", parameters: Type.Object({}), execute: async () => { const s = getStatus(); snapshot(s); return JSON.stringify(s, null, 2); } }); pi.registerTool({ name: "sysmon_top", description: "Get top processes grouped by name with total memory. Cross-platform.", parameters: Type.Object({ limit: Type.Optional(Type.Number({ description: "Number of top processes (default: 10)" })), }), execute: async (params) => { const s = getStatus(); return JSON.stringify(s.topApps.slice(0, params.limit || 10), null, 2); } }); pi.registerTool({ name: "sysmon_kill", description: "Kill all processes by name. Returns count killed and MB freed. Cross-platform.", parameters: Type.Object({ name: Type.String({ description: "Process name (e.g., Discord, Zoom, chrome)" }), }), execute: async (params) => killByName(params.name), }); pi.registerTool({ name: "sysmon_clean", description: "Run disk cleanup: npm cache, pnpm store, temp files. Platform-aware (Windows temp / macOS DerivedData / Linux ~/.cache).", parameters: Type.Object({}), execute: async () => runClean(), }); }