Code

/**
 * mcp/skills/getAccountInfo.ts — get_account_info skill
 *
 * Returns current user account details including username, full name, home
 * directory, shell, account type (admin/standard), and password policy
 * settings. Use at the start of any password reset or account repair workflow.
 *
 * Platform strategy
 * -----------------
 * darwin  `id`, `dscl . -read /Users/$(whoami)`, `pwpolicy -getaccountpolicies`
 * win32   PowerShell [System.Security.Principal.WindowsIdentity]::GetCurrent()
 *         and Get-LocalUser
 *
 * Smoke test
 *   npx tsx -r dotenv/config mcp/skills/getAccountInfo.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: "get_account_info",
  description:
    "Returns current user account details including username, full name, home " +
    "directory, shell, account type (admin/standard), and password policy settings. " +
    "Use at the start of any password reset or account repair workflow.",
  riskLevel:       "low",
  destructive:     false,
  requiresConsent: false,
  supportsDryRun:  false,
  affectedScope:   ["user"],
  auditRequired:   false,
  schema: {},
} as const;
 
// -- Types --------------------------------------------------------------------
 
interface AccountInfo {
  username:          string;
  fullName:          string | null;
  homeDir:           string;
  shell:             string | null;
  isAdmin:           boolean;
  accountType:       "admin" | "standard" | "unknown";
  passwordLastSet:   string | null;
  passwordExpiresIn: number | null;
}
 
// -- 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 implementation ----------------------------------------------------
 
async function getAccountInfoDarwin(): Promise<AccountInfo> {
  const username = os.userInfo().username;
  const homeDir  = os.homedir();
 
  // Get admin group membership
  let isAdmin = false;
  try {
    const { stdout: idOut } = await execAsync("id", { maxBuffer: 1 * 1024 * 1024 });
    isAdmin = idOut.includes("admin") || idOut.includes("(wheel)");
  } catch {
    // ignore
  }
 
  // Get full name and shell from dscl
  let fullName: string | null = null;
  let shell:    string | null = null;
  try {
    const safeName = username.replace(/'/g, `'\\''`);
    const { stdout: dsclOut } = await execAsync(
      `dscl . -read /Users/'${safeName}' RealName UserShell 2>/dev/null`,
      { maxBuffer: 2 * 1024 * 1024, shell: "/bin/bash" },
    );
    const realNameMatch = dsclOut.match(/RealName:\s*\n?\s*(.+)/);
    if (realNameMatch) fullName = realNameMatch[1].trim();
    const shellMatch = dsclOut.match(/UserShell:\s*(.+)/);
    if (shellMatch) shell = shellMatch[1].trim();
  } catch {
    // ignore dscl errors
  }
 
  // Get password policy info
  let passwordLastSet:   string | null = null;
  let passwordExpiresIn: number | null = null;
  try {
    const safeName = username.replace(/'/g, `'\\''`);
    const { stdout: pwOut } = await execAsync(
      `pwpolicy -u '${safeName}' -getpolicy 2>/dev/null`,
      { maxBuffer: 2 * 1024 * 1024, shell: "/bin/bash" },
    );
    const maxAgeMatch = pwOut.match(/maxMinutesUntilChangePassword=(\d+)/);
    if (maxAgeMatch) {
      const maxMinutes = parseInt(maxAgeMatch[1], 10);
      if (maxMinutes > 0) {
        passwordExpiresIn = Math.floor(maxMinutes / 1440); // convert to days
      }
    }
  } catch {
    // pwpolicy may not be available or may require elevated privileges
  }
 
  // Try to get password last set time from dscl
  try {
    const safeName = username.replace(/'/g, `'\\''`);
    const { stdout: dsclPwOut } = await execAsync(
      `dscl . -read /Users/'${safeName}' passwordLastSetTime 2>/dev/null`,
      { maxBuffer: 1 * 1024 * 1024, shell: "/bin/bash" },
    );
    const tsMatch = dsclPwOut.match(/passwordLastSetTime:\s*(.+)/);
    if (tsMatch) {
      const tsVal = parseFloat(tsMatch[1].trim());
      if (!isNaN(tsVal) && tsVal > 0) {
        // macOS stores as CFAbsoluteTime (seconds since 2001-01-01)
        const epochOffset = 978307200; // seconds between 1970 and 2001
        passwordLastSet = new Date((tsVal + epochOffset) * 1000).toISOString();
      }
    }
  } catch {
    // ignore
  }
 
  return {
    username,
    fullName:         fullName ?? username,
    homeDir,
    shell:            shell ?? process.env["SHELL"] ?? null,
    isAdmin,
    accountType:      isAdmin ? "admin" : "standard",
    passwordLastSet,
    passwordExpiresIn,
  };
}
 
// -- win32 implementation -----------------------------------------------------
 
async function getAccountInfoWin32(): Promise<AccountInfo> {
  const ps = `
$ErrorActionPreference = 'SilentlyContinue'
$id = [System.Security.Principal.WindowsIdentity]::GetCurrent()
$principal = New-Object System.Security.Principal.WindowsPrincipal($id)
$isAdmin = $principal.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator)
$username = $env:USERNAME
$user = Get-LocalUser -Name $username
$obj = [PSCustomObject]@{
  username          = $username
  fullName          = $user.FullName
  homeDir           = $env:USERPROFILE
  shell             = $env:ComSpec
  isAdmin           = $isAdmin
  accountType       = if ($isAdmin) { 'admin' } else { 'standard' }
  passwordLastSet   = if ($user.PasswordLastSet) { $user.PasswordLastSet.ToString('o') } else { $null }
  passwordExpiresIn = if ($user.PasswordExpires) {
    [int]($user.PasswordExpires - (Get-Date)).TotalDays
  } else { $null }
}
$obj | ConvertTo-Json -Compress`.trim();
 
  const raw = await runPS(ps);
  const parsed = JSON.parse(raw) as AccountInfo;
  return parsed;
}
 
// -- Exported run function ----------------------------------------------------
 
export async function run(_args: Record<string, never> = {}) {
  const platform = os.platform();
 
  const info = platform === "win32"
    ? await getAccountInfoWin32()
    : await getAccountInfoDarwin();
 
  return { platform, ...info };
}
 
// -- CLI smoke test -----------------------------------------------------------
 
if (false) {
  run({})
    .then(r => console.log(JSON.stringify(r, null, 2)))
    .catch((err: Error) => { console.error(err.message); process.exit(1); });
}