Code

/**
 * mcp/skills/listInstalledApps.ts — list_installed_apps skill
 *
 * Lists all installed applications with their names, versions, and install
 * locations. Useful for checking if software is installed, finding outdated
 * apps, or before reinstalling an application.
 *
 * Platform strategy
 * -----------------
 * darwin  `system_profiler SPApplicationsDataType -json` — comprehensive app list
 * win32   PowerShell Get-Package | ConvertTo-Json
 *
 * Smoke test
 *   npx tsx -r dotenv/config mcp/skills/listInstalledApps.ts [filter]
 */
 
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: "list_installed_apps",
  description:
    "Lists all installed applications with their names, versions, and install " +
    "locations. Use when checking if software is installed, finding outdated " +
    "apps, or before reinstalling an application.",
  riskLevel:       "low",
  destructive:     false,
  requiresConsent: false,
  supportsDryRun:  false,
  affectedScope:   ["user"],
  auditRequired:   false,
  schema: {
    filter: z
      .string()
      .optional()
      .describe("Case-insensitive name filter to narrow results"),
    includeSystemApps: z
      .boolean()
      .optional()
      .describe("Include Apple/Microsoft system apps. Default: false"),
  },
} as const;
 
// -- Types --------------------------------------------------------------------
 
interface AppEntry {
  name:    string;
  version: string;
  path:    string;
  vendor:  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: 50 * 1024 * 1024 },
  );
  return stdout.trim();
}
 
// -- darwin implementation ----------------------------------------------------
 
interface SPApplicationEntry {
  _name:            string;
  version?:         string;
  path?:            string;
  obtained_from?:   string;
  // system_profiler uses different key names depending on macOS version
  info?:            string;
}
 
interface SPApplicationsData {
  SPApplicationsDataType: SPApplicationEntry[];
}
 
const APPLE_SYSTEM_VENDORS = [
  "apple",
  "com.apple",
];
 
function isSystemApp(entry: SPApplicationEntry): boolean {
  const vendor = (entry.obtained_from ?? "").toLowerCase();
  const name   = (entry._name ?? "").toLowerCase();
  const path   = (entry.path ?? "").toLowerCase();
  return (
    APPLE_SYSTEM_VENDORS.some((v) => vendor.includes(v)) ||
    path.startsWith("/system/") ||
    path.startsWith("/library/apple/")
  ) && !name.includes("xcode"); // keep Xcode as it's developer-relevant
}
 
async function listInstalledAppsDarwin(
  filter:            string | undefined,
  includeSystemApps: boolean,
): Promise<AppEntry[]> {
  const { stdout } = await execAsync(
    "system_profiler SPApplicationsDataType -json 2>/dev/null",
    { maxBuffer: 50 * 1024 * 1024 },
  );
 
  const data = JSON.parse(stdout) as SPApplicationsData;
  const apps = data.SPApplicationsDataType ?? [];
 
  return apps
    .filter((entry) => {
      if (!includeSystemApps && isSystemApp(entry)) return false;
      if (filter) {
        return entry._name.toLowerCase().includes(filter.toLowerCase());
      }
      return true;
    })
    .map((entry) => ({
      name:    entry._name,
      version: entry.version ?? "unknown",
      path:    entry.path    ?? "unknown",
      vendor:  entry.obtained_from ?? "unknown",
    }))
    .sort((a, b) => a.name.localeCompare(b.name));
}
 
// -- win32 implementation -----------------------------------------------------
 
interface WinPackage {
  Name:    string;
  Version: string;
  Source:  string | null;
}
 
const MICROSOFT_SYSTEM_PREFIXES = [
  "microsoft windows",
  "windows ",
  "microsoft visual c++",
  "microsoft .net",
];
 
function isMicrosoftSystemApp(name: string): boolean {
  const lower = name.toLowerCase();
  return MICROSOFT_SYSTEM_PREFIXES.some((p) => lower.startsWith(p));
}
 
async function listInstalledAppsWin32(
  filter:            string | undefined,
  includeSystemApps: boolean,
): Promise<AppEntry[]> {
  const ps = `
$ErrorActionPreference = 'SilentlyContinue'
Get-Package | Select-Object Name, Version, Source | ConvertTo-Json -Depth 2 -Compress`.trim();
 
  const raw    = await runPS(ps);
  const parsed = JSON.parse(raw) as WinPackage | WinPackage[];
  const pkgs   = Array.isArray(parsed) ? parsed : [parsed];
 
  return pkgs
    .filter((pkg) => {
      if (!pkg.Name) return false;
      if (!includeSystemApps && isMicrosoftSystemApp(pkg.Name)) return false;
      if (filter) {
        return pkg.Name.toLowerCase().includes(filter.toLowerCase());
      }
      return true;
    })
    .map((pkg) => ({
      name:    pkg.Name,
      version: pkg.Version ?? "unknown",
      path:    pkg.Source  ?? "unknown",
      vendor:  "unknown",
    }))
    .sort((a, b) => a.name.localeCompare(b.name));
}
 
// -- Exported run function ----------------------------------------------------
 
export async function run({
  filter,
  includeSystemApps = false,
}: {
  filter?:            string;
  includeSystemApps?: boolean;
} = {}) {
  const platform = os.platform();
  const apps     = platform === "win32"
    ? await listInstalledAppsWin32(filter, includeSystemApps)
    : await listInstalledAppsDarwin(filter, includeSystemApps);
 
  return {
    platform,
    filter:           filter ?? null,
    includeSystemApps,
    total:            apps.length,
    apps,
  };
}
 
// -- CLI smoke test -----------------------------------------------------------
 
if (false) {
  run({ filter: process.argv[2] })
    .then((r) => console.log(JSON.stringify(r, null, 2)))
    .catch((err: Error) => { console.error(err.message); process.exit(1); });
}