Code

/**
 * mcp/skills/getStartupItems.ts — get_startup_items skill
 *
 * Lists all applications and agents that launch automatically at login.
 * Includes login items, launch agents, and launch daemons.
 *
 * Platform strategy
 * -----------------
 * darwin  osascript for login items; scan LaunchAgents / LaunchDaemons dirs
 * win32   PowerShell Win32_StartupCommand + registry Run keys
 *
 * Smoke test
 *   npx tsx -r dotenv/config mcp/skills/getStartupItems.ts
 */
 
import * as os       from "os";
import * as nodePath from "path";
import { exec }      from "child_process";
import { promisify } from "util";
import { z }         from "zod";
import * as fs       from "fs/promises";
 
const execAsync = promisify(exec);
 
// -- Meta ---------------------------------------------------------------------
 
export const meta = {
  name: "get_startup_items",
  description:
    "Lists all applications and agents that launch automatically at login. " +
    "Includes login items, launch agents, and launch daemons. Use when " +
    "diagnosing slow boot times or identifying unwanted startup programs.",
  riskLevel:       "low",
  destructive:     false,
  requiresConsent: false,
  supportsDryRun:  false,
  affectedScope:   ["user"],
  auditRequired:   false,
  schema: {
    includeSystem: z
      .boolean()
      .optional()
      .describe("Include Apple system agents. Default: false"),
  },
} as const;
 
// -- Types --------------------------------------------------------------------
 
interface StartupItem {
  name: string;
  path: string;
  type: "login-item" | "launch-agent" | "launch-daemon" | "registry" | "startup-command";
}
 
// -- 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 helpers -----------------------------------------------------------
 
const APPLE_PREFIXES = ["com.apple.", "com.osquery.", "com.openssh."];
 
function isAppleItem(name: string): boolean {
  const lower = name.toLowerCase();
  return APPLE_PREFIXES.some(p => lower.startsWith(p));
}
 
async function scanPlistDir(
  dir:           string,
  type:          StartupItem["type"],
  includeSystem: boolean,
): Promise<StartupItem[]> {
  try {
    const entries = await fs.readdir(dir);
    return entries
      .filter(e => e.endsWith(".plist"))
      .filter(e => includeSystem || !isAppleItem(e.replace(/\.plist$/, "")))
      .map(e => ({
        name: e.replace(/\.plist$/, ""),
        path: nodePath.join(dir, e),
        type,
      }));
  } catch {
    return [];
  }
}
 
async function getStartupItemsDarwin(includeSystem: boolean): Promise<StartupItem[]> {
  const items: StartupItem[] = [];
 
  // Login items via osascript
  try {
    const { stdout } = await execAsync(
      `osascript -e 'tell application "System Events" to get the name of every login item' 2>/dev/null`,
      { maxBuffer: 1024 * 1024 },
    );
    const names = stdout.trim();
    if (names) {
      for (const n of names.split(", ").map(s => s.trim()).filter(Boolean)) {
        if (includeSystem || !isAppleItem(n)) {
          items.push({ name: n, path: "", type: "login-item" });
        }
      }
    }
  } catch {
    // osascript may be unavailable or restricted
  }
 
  const home = os.homedir();
 
  // User LaunchAgents
  const userAgents = await scanPlistDir(
    nodePath.join(home, "Library", "LaunchAgents"),
    "launch-agent",
    includeSystem,
  );
  items.push(...userAgents);
 
  // System LaunchAgents
  const sysAgents = await scanPlistDir(
    "/Library/LaunchAgents",
    "launch-agent",
    includeSystem,
  );
  items.push(...sysAgents);
 
  // System LaunchDaemons
  const daemons = await scanPlistDir(
    "/Library/LaunchDaemons",
    "launch-daemon",
    includeSystem,
  );
  items.push(...daemons);
 
  return items;
}
 
// -- win32 implementation -----------------------------------------------------
 
async function getStartupItemsWin32(): Promise<StartupItem[]> {
  const ps = `
$ErrorActionPreference = 'SilentlyContinue'
$results = @()
$wmi = Get-CimInstance Win32_StartupCommand | ForEach-Object {
  @{ name=$_.Name; path=$_.Command; type='startup-command' }
}
if ($wmi) { $results += $wmi }
$regPaths = @(
  'HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\Run',
  'HKLM:\\Software\\Microsoft\\Windows\\CurrentVersion\\Run'
)
foreach ($rp in $regPaths) {
  $key = Get-ItemProperty -Path $rp -ErrorAction SilentlyContinue
  if ($key) {
    $key.PSObject.Properties | Where-Object { $_.Name -notlike 'PS*' } | ForEach-Object {
      $results += @{ name=$_.Name; path=[string]$_.Value; type='registry' }
    }
  }
}
$results | ConvertTo-Json -Depth 2 -Compress`.trim();
 
  const raw = await runPS(ps);
  if (!raw) return [];
  const parsed = JSON.parse(raw) as StartupItem | StartupItem[];
  return Array.isArray(parsed) ? parsed : [parsed];
}
 
// -- Exported run function ----------------------------------------------------
 
export async function run({
  includeSystem = false,
}: {
  includeSystem?: boolean;
} = {}) {
  const platform  = os.platform();
  const loginItems = platform === "win32"
    ? await getStartupItemsWin32()
    : await getStartupItemsDarwin(includeSystem);
 
  return {
    platform,
    includeSystem,
    loginItems,
    total: loginItems.length,
  };
}
 
// -- Smoke test ---------------------------------------------------------------
 
if (false) {
  run({})
    .then(r => console.log(JSON.stringify(r, null, 2)))
    .catch((err: Error) => { console.error(err.message); process.exit(1); });
}