Code

/**
 * mcp/skills/listVideoDevices.ts — list_video_devices skill
 *
 * Enumerates cameras + the system default on macOS and Windows.  Shared
 * between the P0-c Collab App Repair skill and the P0-d A/V & Peripheral
 * Repair skill.
 *
 * Platform strategy
 * -----------------
 * darwin  `system_profiler SPCameraDataType -json` reports each camera
 *         with a unique_id + model_id.  macOS does not have a single
 *         "system default camera" — collab apps pick per-app — but the
 *         built-in FaceTime camera is the conventional default if
 *         present.
 * win32   PowerShell `Get-PnpDevice -Class Camera`.  As on macOS, Windows
 *         does not expose a single default camera across the OS — each
 *         app picks its own.  `defaultCamera` is a best-effort heuristic
 *         (first built-in / integrated camera).
 *
 * Read-only — device mutation lives in `reset_av_device_selection`.
 */
 
import * as os from "os";
import { z }   from "zod";
 
import {
  execAsync,
  runPS,
}                from "./_shared/platform";
 
// -- Meta ---------------------------------------------------------------------
 
export const meta = {
  name: "list_video_devices",
  description:
    "Enumerates cameras connected to the system (built-in, USB, Continuity " +
    "Camera on macOS) and reports a best-guess default. Use during collab-app " +
    "troubleshooting (Teams/Slack/Zoom/Webex camera problems) or when the " +
    "user reports the wrong camera is being selected. Read-only.",
  riskLevel:       "low",
  destructive:     false,
  requiresConsent: false,
  supportsDryRun:  false,
  affectedScope:   ["user"],
  auditRequired:   false,
  schema: {} as Record<string, z.ZodTypeAny>,
} as const;
 
// -- Types --------------------------------------------------------------------
 
export interface VideoDevice {
  name:        string;
  connection:  "built-in" | "usb" | "continuity" | "unknown";
  vendorId?:   string;
  productId?:  string;
  isDefault:   boolean;
}
 
export interface ListVideoDevicesResult {
  platform:       NodeJS.Platform;
  cameras:        VideoDevice[];
  defaultCamera:  string | null;
}
 
// -- darwin implementation ----------------------------------------------------
 
interface SPCameraEntry {
  _name:                   string;
  spcamera_model_id?:      string;      // e.g. "Model Id: FaceTime HD Camera"
  spcamera_unique_id?:     string;
}
 
interface SPCameraData {
  SPCameraDataType?: SPCameraEntry[];
}
 
function classifyDarwinCamera(entry: SPCameraEntry): VideoDevice["connection"] {
  const model = (entry.spcamera_model_id ?? "").toLowerCase();
  const name  = entry._name.toLowerCase();
  if (name.includes("facetime") || model.includes("apple"))      return "built-in";
  if (name.includes("continuity") || name.includes("iphone"))    return "continuity";
  if (model.includes("usb") || name.includes("logitech") || name.includes("razer")) {
    return "usb";
  }
  return "unknown";
}
 
function parseDarwinOutput(stdout: string): ListVideoDevicesResult {
  const data     = JSON.parse(stdout) as SPCameraData;
  const items    = data.SPCameraDataType ?? [];
  const cameras: VideoDevice[] = [];
  let defaultCamera: string | null = null;
 
  for (const entry of items) {
    const connection = classifyDarwinCamera(entry);
    const isDefault  = connection === "built-in" && !defaultCamera;
    const cam: VideoDevice = {
      name: entry._name,
      connection,
      isDefault,
    };
    if (isDefault) defaultCamera = entry._name;
    cameras.push(cam);
  }
 
  // If no built-in was found, the first camera is the conventional default.
  if (!defaultCamera && cameras.length > 0) {
    cameras[0].isDefault = true;
    defaultCamera = cameras[0].name;
  }
 
  return {
    platform:      "darwin",
    cameras,
    defaultCamera,
  };
}
 
async function listVideoDarwin(): Promise<ListVideoDevicesResult> {
  const { stdout } = await execAsync(
    "system_profiler SPCameraDataType -json 2>/dev/null",
    { maxBuffer: 10 * 1024 * 1024 },
  );
  return parseDarwinOutput(stdout);
}
 
// -- win32 implementation -----------------------------------------------------
 
interface WinCameraDevice {
  Name:         string;
  Class:        string;
  Status:       string;
  InstanceId?:  string;
  FriendlyName?: string;
}
 
function classifyWinCamera(d: WinCameraDevice): VideoDevice["connection"] {
  const id   = (d.InstanceId ?? "").toUpperCase();
  const name = d.Name.toLowerCase();
  if (id.includes("USB"))                                      return "usb";
  if (name.includes("integrated") || name.includes("built-in") || id.includes("MIPI")) {
    return "built-in";
  }
  return "unknown";
}
 
function parseWinVidIdPid(instanceId: string | undefined): { vendorId?: string; productId?: string } {
  if (!instanceId) return {};
  const match = instanceId.match(/VID_([0-9A-F]{4})&PID_([0-9A-F]{4})/i);
  if (!match) return {};
  return { vendorId: match[1].toUpperCase(), productId: match[2].toUpperCase() };
}
 
function parseWinOutput(stdout: string): ListVideoDevicesResult {
  const parsed = stdout.trim()
    ? (JSON.parse(stdout) as WinCameraDevice | WinCameraDevice[])
    : [];
  const devices: WinCameraDevice[] = Array.isArray(parsed) ? parsed : [parsed];
 
  const cameras: VideoDevice[] = devices
    .filter((d) => d?.Name)
    .map((d) => {
      const connection = classifyWinCamera(d);
      const { vendorId, productId } = parseWinVidIdPid(d.InstanceId);
      return {
        name: d.Name,
        connection,
        ...(vendorId  && { vendorId }),
        ...(productId && { productId }),
        isDefault: false,
      };
    });
 
  // Windows: first built-in camera (if any) is the conventional default,
  // otherwise the first camera returned.  Apps make their own selection.
  let defaultCamera: string | null = null;
  const firstBuiltin = cameras.find((c) => c.connection === "built-in");
  if (firstBuiltin) {
    firstBuiltin.isDefault = true;
    defaultCamera = firstBuiltin.name;
  } else if (cameras.length > 0) {
    cameras[0].isDefault = true;
    defaultCamera = cameras[0].name;
  }
 
  return { platform: "win32", cameras, defaultCamera };
}
 
async function listVideoWin32(): Promise<ListVideoDevicesResult> {
  const script = `
$ErrorActionPreference = 'SilentlyContinue'
Get-PnpDevice -Class Camera,Image | Where-Object { $_.Status -eq 'OK' -or $_.Status -eq 'Error' } |
  Select-Object Name, Class, Status, InstanceId, FriendlyName |
  ConvertTo-Json -Depth 2 -Compress`.trim();
  const raw = await runPS(script);
  return parseWinOutput(raw);
}
 
// -- Exported run function ----------------------------------------------------
 
export async function run(): Promise<ListVideoDevicesResult> {
  const platform = os.platform();
  if (platform === "darwin") return listVideoDarwin();
  if (platform === "win32")  return listVideoWin32();
  throw new Error(`list_video_devices: unsupported platform "${platform}"`);
}
 
// -- Test helpers -------------------------------------------------------------
 
/** Exported for unit tests only — do not use from production code. */
export const __testing = {
  parseDarwinOutput,
  parseWinOutput,
  classifyDarwinCamera,
  classifyWinCamera,
  parseWinVidIdPid,
};