get_top_consumers
get_top_consumers
Returns processes ranked by combined CPU and memory consumption. Provides a quick snapshot of what is most impacting system performance. Use when diagnosing slowness without a specific process in mind.
Metadata
Name
get_top_consumers
Updated
2 weeks ago
Risk
Low
Requires consent
false
Affected scope
user
Code
/**
* mcp/skills/getTopConsumers.ts — get_top_consumers skill
*
* Returns processes ranked by combined CPU and memory consumption. Provides a
* quick snapshot of what is most impacting system performance.
*
* Platform strategy
* -----------------
* darwin `ps -eo pid,pcpu,rss,comm` — parse and rank by combined score
* win32 PowerShell Get-Process | Sort-Object CPU -Descending
*
* Smoke test
* npx tsx -r dotenv/config mcp/skills/getTopConsumers.ts
*/
import * as os from "os";
import { exec } from "child_process";
import { promisify } from "util";
import { z } from "zod";
import { formatBytesBinary } from "./_shared/formatBytes";
import { isAgentSelf, isSystemProcess } from "./_shared/processIdentity";
const execAsync = promisify(exec);
// -- Meta ---------------------------------------------------------------------
export const meta = {
name: "get_top_consumers",
description:
"Returns processes ranked by combined CPU and memory consumption. Provides " +
"a quick snapshot of what is most impacting system performance. Use when " +
"diagnosing slowness without a specific process in mind.",
riskLevel: "low",
destructive: false,
requiresConsent: false,
supportsDryRun: false,
affectedScope: ["user"],
auditRequired: false,
schema: {
limit: z
.number()
.optional()
.describe("Number of top processes to return. Default: 10"),
metric: z
.enum(["cpu", "memory", "combined"])
.optional()
.describe("Ranking metric. Default: combined"),
},
} as const;
// -- Types --------------------------------------------------------------------
interface ConsumerEntry {
pid: number;
name: string;
cpuPercent: number;
memoryMb: number;
/** Pre-formatted memory string (binary units — matches Activity Monitor / Task Manager). */
memoryHuman: string;
combinedScore: number;
/** True for critical OS processes that must NOT be killed (surface as a note, never an action). */
isSystem: boolean;
}
// -- 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: 20 * 1024 * 1024 },
);
return stdout.trim();
}
// -- darwin implementation ----------------------------------------------------
async function getTopConsumersDarwin(
limit: number,
metric: "cpu" | "memory" | "combined",
): Promise<ConsumerEntry[]> {
const { stdout } = await execAsync(
"ps -eo pid,pcpu,rss,comm 2>/dev/null",
{ maxBuffer: 10 * 1024 * 1024 },
);
const rows = stdout
.trim()
.split("\n")
.slice(1)
.flatMap((line) => {
const parts = line.trim().split(/\s+/);
if (parts.length < 4) return [];
const pid = parseInt(parts[0], 10);
const cpu = parseFloat(parts[1]);
const rssKb = parseInt(parts[2], 10);
const fullComm = parts.slice(3).join(" ");
const name = fullComm.split("/").at(-1) ?? fullComm;
if (isNaN(pid)) return [];
// Never surface the agent's own process(es) — it must not be offered for
// kill/restart (would terminate the agent mid-run).
if (isAgentSelf(name, pid, fullComm)) return [];
const memoryMb = Math.round((rssKb / 1024) * 10) / 10;
const memoryHuman = formatBytesBinary(rssKb * 1024);
return [{ pid, name, cpuPercent: cpu, memoryMb, memoryHuman, combinedScore: 0,
isSystem: isSystemProcess(name, fullComm) }];
});
// Normalise and compute combined score
const maxCpu = Math.max(...rows.map(r => r.cpuPercent), 1);
const maxMem = Math.max(...rows.map(r => r.memoryMb), 1);
for (const r of rows) {
r.combinedScore = Math.round(
((r.cpuPercent / maxCpu) * 50 + (r.memoryMb / maxMem) * 50) * 100,
) / 100;
}
const sortKey: keyof ConsumerEntry =
metric === "cpu" ? "cpuPercent" :
metric === "memory" ? "memoryMb" : "combinedScore";
return rows
.sort((a, b) => (b[sortKey] as number) - (a[sortKey] as number))
.slice(0, limit);
}
// -- win32 implementation -----------------------------------------------------
async function getTopConsumersWin32(
limit: number,
metric: "cpu" | "memory" | "combined",
): Promise<ConsumerEntry[]> {
// Two-snapshot sampling (1 s apart) so cpuPercent is a live per-interval %.
// $_.CPU on Get-Process is TotalProcessorTime.TotalSeconds — cumulative, NOT a
// live percentage. Comparing it against a 20% threshold in the classifier gives
// completely wrong results (old processes appear "pegged"; idle processes appear 0).
// Delta / elapsed / logical-CPU-count gives the same metric Task Manager shows.
const ps = `
$ErrorActionPreference = 'SilentlyContinue'
$cpuCount = [Math]::Max([Environment]::ProcessorCount, 1)
$snap = @{}
Get-Process | ForEach-Object { $snap[[int]$_.Id] = [double]($_.CPU -as [double]) }
$sw = [System.Diagnostics.Stopwatch]::StartNew()
Start-Sleep -Milliseconds 1000
$sw.Stop()
$elapsed = [Math]::Max($sw.Elapsed.TotalSeconds, 0.1)
$result = @(Get-Process | ForEach-Object {
$prev = if ($snap.ContainsKey([int]$_.Id)) { $snap[[int]$_.Id] } else { [double]($_.CPU -as [double]) }
$delta = [Math]::Max([double]($_.CPU -as [double]) - $prev, 0)
[PSCustomObject]@{
pid = [int]$_.Id
name = $_.ProcessName
cpuPercent = [Math]::Round(($delta / $elapsed / $cpuCount) * 100, 2)
memoryMb = [Math]::Round($_.WorkingSet64 / 1MB, 1)
}
})
$result | ConvertTo-Json -Depth 2 -Compress`.trim();
const raw = await runPS(ps);
if (!raw) return [];
let rawArr: Omit<ConsumerEntry, "combinedScore" | "isSystem" | "memoryHuman">[];
try {
const parsed = JSON.parse(raw) as Omit<ConsumerEntry, "combinedScore" | "isSystem" | "memoryHuman">[] | Omit<ConsumerEntry, "combinedScore" | "isSystem" | "memoryHuman">;
rawArr = Array.isArray(parsed) ? parsed : [parsed];
} catch {
return [];
}
// Never surface the agent's own process(es) (no exec path on win32 — match by name/pid).
const arr = rawArr.filter(r => !isAgentSelf(r.name, r.pid));
const maxCpu = Math.max(...arr.map(r => r.cpuPercent), 1);
const maxMem = Math.max(...arr.map(r => r.memoryMb), 1);
const entries = arr.map(r => ({
...r,
memoryHuman: formatBytesBinary(r.memoryMb * 1024 * 1024),
combinedScore: Math.round(
((r.cpuPercent / maxCpu) * 50 + (r.memoryMb / maxMem) * 50) * 100,
) / 100,
isSystem: isSystemProcess(r.name),
}));
const sortKey: keyof ConsumerEntry =
metric === "cpu" ? "cpuPercent" :
metric === "memory" ? "memoryMb" : "combinedScore";
return entries
.sort((a, b) => (b[sortKey] as number) - (a[sortKey] as number))
.slice(0, limit);
}
// -- Exported run function ----------------------------------------------------
export async function run({
limit = 10,
metric = "combined",
}: {
limit?: number;
metric?: "cpu" | "memory" | "combined";
} = {}) {
const platform = os.platform();
const processes = platform === "win32"
? await getTopConsumersWin32(limit, metric)
: await getTopConsumersDarwin(limit, metric);
return {
platform,
metric,
processes,
sampledAt: new Date().toISOString(),
};
}
// -- Smoke test ---------------------------------------------------------------
if (false) {
run({})
.then(r => console.log(JSON.stringify(r, null, 2)))
.catch((err: Error) => { console.error(err.message); process.exit(1); });
}