/**
* mcp/skills/clearPrintQueue.ts — clear_print_queue skill
*
* Cancels all pending and stuck print jobs. Use when the print queue is
* jammed and jobs cannot be removed through normal means.
*
* Platform strategy
* -----------------
* darwin `cancel -a [printerName]` — cancels all jobs for a printer or all
* win32 PowerShell Get-PrintJob | Remove-PrintJob
*
* Smoke test
* npx tsx -r dotenv/config mcp/skills/clearPrintQueue.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: "clear_print_queue",
description:
"Cancels all pending and stuck print jobs. " +
"Use when the print queue is jammed and jobs cannot be removed through normal means.",
riskLevel: "medium",
destructive: false,
requiresConsent: true,
supportsDryRun: true,
affectedScope: ["system"],
auditRequired: true,
escalationHint: {
darwin: "sudo cancel -a # clears all queues for all users",
win32: "Stop-Service Spooler -Force; Remove-Item C:\\Windows\\System32\\spool\\PRINTERS\\* -Force; Start-Service Spooler # run from elevated PowerShell",
},
schema: {
printerName: z
.string()
.optional()
.describe("Printer name to clear. Omit to clear all queues"),
dryRun: z
.boolean()
.optional()
.describe(
"If true, show jobs that would be cancelled without cancelling. Default: true",
),
},
} as const;
// -- Types --------------------------------------------------------------------
interface ClearPrintQueueResult {
cancelledCount: number;
printers: string[];
jobs: string[];
dryRun: boolean;
message: string;
}
// -- 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 clearPrintQueueDarwin(
dryRun: boolean,
printerName?: string,
): Promise<ClearPrintQueueResult> {
// First enumerate jobs
const listCmd = printerName
? `lpstat -o '${printerName.replace(/'/g, "'\\''")}'`
: "lpstat -o";
let lpstatOut = "";
try {
const { stdout } = await execAsync(listCmd);
lpstatOut = stdout;
} catch (err) {
lpstatOut = (err as { stdout?: string }).stdout ?? "";
}
const jobLines = lpstatOut.trim().split("\n").filter(Boolean);
const jobs = jobLines.map((l) => l.trim().split(/\s+/)[0] ?? "").filter(Boolean);
const printers = [...new Set(
jobs.map((j) => j.replace(/-\d+$/, "")).filter(Boolean),
)];
if (!dryRun) {
try {
const cancelCmd = printerName
? `cancel -a '${printerName.replace(/'/g, "'\\''")}'`
: "cancel -a";
await execAsync(cancelCmd);
} catch {
// Non-fatal — queue may already be empty
}
}
const message = dryRun
? `Found ${jobs.length} job(s) in queue. Run with dryRun=false to cancel them.`
: `Cancelled ${jobs.length} job(s) from the print queue.`;
return {
cancelledCount: dryRun ? 0 : jobs.length,
printers,
jobs,
dryRun,
message,
};
}
// -- win32 implementation -----------------------------------------------------
async function clearPrintQueueWin32(
dryRun: boolean,
printerName?: string,
): Promise<ClearPrintQueueResult> {
// Enumerate jobs
const listPs = printerName
? `
$ErrorActionPreference = 'SilentlyContinue'
Get-PrintJob -PrinterName '${printerName.replace(/'/g, "''")}' |
Select-Object Id,DocumentName |
ConvertTo-Json -Depth 2 -Compress`.trim()
: `
$ErrorActionPreference = 'SilentlyContinue'
$jobs = Get-Printer | ForEach-Object {
$n = $_.Name
Get-PrintJob -PrinterName $n -ErrorAction SilentlyContinue |
Select-Object Id,DocumentName,@{Name='PrinterName';Expression={$n}}
}
$jobs | ConvertTo-Json -Depth 2 -Compress`.trim();
const raw = await runPS(listPs);
let jobItems: any[] = [];
if (raw) {
try {
const parsed = JSON.parse(raw);
jobItems = Array.isArray(parsed) ? parsed : [parsed];
} catch { /* ignore parse errors */ }
}
const jobs = jobItems.map((j) => String(j.DocumentName ?? j.Id ?? "")).filter(Boolean);
const printers = [...new Set(
jobItems.map((j) => String(j.PrinterName ?? printerName ?? "")).filter(Boolean),
)];
if (!dryRun && jobItems.length > 0) {
try {
const cancelPs = printerName
? `
$ErrorActionPreference = 'SilentlyContinue'
Get-PrintJob -PrinterName '${printerName.replace(/'/g, "''")}' | Remove-PrintJob`.trim()
: `
$ErrorActionPreference = 'SilentlyContinue'
Get-Printer | ForEach-Object {
Get-PrintJob -PrinterName $_.Name -ErrorAction SilentlyContinue | Remove-PrintJob
}`.trim();
await runPS(cancelPs);
} catch { /* Non-fatal */ }
}
const message = dryRun
? `Found ${jobs.length} job(s) in queue. Run with dryRun=false to cancel them.`
: `Cancelled ${jobs.length} job(s) from the print queue.`;
return {
cancelledCount: dryRun ? 0 : jobs.length,
printers,
jobs,
dryRun,
message,
};
}
// -- Exported run function ----------------------------------------------------
export async function run({
printerName,
dryRun = true,
}: {
printerName?: string;
dryRun?: boolean;
} = {}) {
const platform = os.platform();
return platform === "win32"
? clearPrintQueueWin32(dryRun, printerName)
: clearPrintQueueDarwin(dryRun, printerName);
}
// -- Smoke test ---------------------------------------------------------------
if (false) {
run({})
.then(r => console.log(JSON.stringify(r, null, 2)))
.catch((err: Error) => { console.error(err.message); process.exit(1); });
}