check_timemachine_status
check_timemachine_status
Reports the current Time Machine backup state on macOS — last-backup timestamp, configured destination, current status, and a stale flag set when the most recent backup is older than the configured threshold (default 72h). Used both by the user-facing Cloud Sync & Backup repair skill and as the telemetry source for the proactive timemachine-stale trigger. Returns status: 'not-supported' on Windows.
Metadata
Name
check_timemachine_status
Updated
2 months ago
Source
Risk
Low
Requires consent
false
Affected scope
user
Code
/**
* mcp/skills/checkTimemachineStatus.ts — check_timemachine_status skill
*
* macOS-only. Reports Time Machine backup state via:
* - `tmutil latestbackup` → path of the most recent backup snapshot
* - `tmutil status` → current state (Running / Idle / FailedBackup)
* - `tmutil destinationinfo` → configured destination(s)
*
* Telemetry contract (Track B Phase 4)
* ------------------------------------
* `meta.outputKeys` includes: platform, lastBackupMs, destination, status,
* stale. These are at the top level of the result so the proactive
* evaluator's restricted DSL can reference them. Trigger 5 fires when
* `stale == true` — i.e. the most recent backup is older than the
* configured threshold (default 72 h).
*
* Read-only.
*
* Windows behaviour
* -----------------
* Time Machine is a macOS-only feature. On win32 the tool returns:
* { platform: "win32", status: "not-supported", stale: false, ... }
* Trigger 5 in Wave 2 declares no Windows variant; this is intentional —
* Windows backup landscape is fragmented (File History, Windows Backup,
* Microsoft Backup app, third-party tools). A future tool can probe
* those individually.
*/
import * as os from "os";
import { z } from "zod";
import {
execAsync,
isDarwin,
isWin32,
} from "./_shared/platform";
// -- Meta ---------------------------------------------------------------------
export const meta = {
name: "check_timemachine_status",
description:
"Reports the current Time Machine backup state on macOS — last-backup " +
"timestamp, configured destination, current status, and a `stale` flag " +
"set when the most recent backup is older than the configured " +
"threshold (default 72h). Used both by the user-facing Cloud Sync & " +
"Backup repair skill and as the telemetry source for the proactive " +
"timemachine-stale trigger. Returns status: 'not-supported' on Windows.",
riskLevel: "low",
destructive: false,
requiresConsent: false,
supportsDryRun: false,
affectedScope: ["user"],
auditRequired: false,
outputKeys: [
"platform",
"lastBackupMs",
"destination",
"status",
"stale",
],
schema: {
staleThresholdHours: z
.number()
.int()
.min(1)
.max(720)
.optional()
.describe("Hours since last successful backup that count as stale. Default 72 (3 days)."),
},
} as const;
// -- Types --------------------------------------------------------------------
export type TimemachineStatus =
| "running"
| "idle"
| "failed"
| "no-destination"
| "stale"
| "not-supported"
| "unknown";
export interface CheckTimemachineStatusResult {
platform: NodeJS.Platform;
lastBackupMs: number | null;
/** Most recent backup snapshot path. */
lastBackupPath: string | null;
/** Configured destination volume name (or path). null when none configured. */
destination: string | null;
status: TimemachineStatus;
stale: boolean;
/** Diagnostic message for the user — short. */
message: string;
}
const DEFAULT_STALE_HOURS = 72;
// -- darwin parsing helpers ---------------------------------------------------
/**
* `tmutil latestbackup` outputs lines like:
* /Volumes/Backup/Backups.backupdb/Mac/2026-04-23-101500
* The trailing component is the snapshot folder named YYYY-MM-DD-HHMMSS.
*/
function parseLatestBackupPath(stdout: string): { path: string | null; mtimeMs: number | null } {
const trimmed = stdout.trim();
if (!trimmed) return { path: null, mtimeMs: null };
// Take the last line in case multiple are returned.
const line = trimmed.split("\n").pop()!;
const match = line.match(/(\d{4}-\d{2}-\d{2}-\d{6})/);
if (!match) return { path: line, mtimeMs: null };
const ts = match[1];
const yyyy = parseInt(ts.slice(0, 4), 10);
const mm = parseInt(ts.slice(5, 7), 10);
const dd = parseInt(ts.slice(8, 10), 10);
const hh = parseInt(ts.slice(11,13), 10);
const mi = parseInt(ts.slice(13,15), 10);
const ss = parseInt(ts.slice(15,17), 10);
const date = new Date(yyyy, mm - 1, dd, hh, mi, ss);
const mtimeMs = date.getTime();
return { path: line, mtimeMs: Number.isNaN(mtimeMs) ? null : mtimeMs };
}
/**
* `tmutil status` output is plist-formatted, with key fields:
* Running 1 | 0
* ClientID "com.apple.backupd"
* BackupPhase "Copying" | "FindingChanges" | "ThinningPostBackup" | …
* Percent 0.0 - 1.0
* Stopping 0
* We only need the Running flag and the BackupPhase for a coarse
* classification.
*/
function parseTmutilStatus(stdout: string): { running: boolean; phase: string | null; failed: boolean } {
const lines = stdout.split("\n").map((l) => l.trim());
const runningRe = /Running\s*=\s*(\d)/;
const phaseRe = /BackupPhase\s*=\s*"?([^"+?])"?\s*;?$/;
const failedHint = /(failedBackup|FailedSetup|InvalidDest|Error)/i;
let running = false;
let phase: string | null = null;
let failed = false;
for (const line of lines) {
const r = line.match(runningRe);
if (r) running = r[1] === "1";
const p = line.match(phaseRe);
if (p) phase = p[1];
if (failedHint.test(line)) failed = true;
}
return { running, phase, failed };
}
/**
* `tmutil destinationinfo` lists configured destinations. We only need
* the first one's "Name" field (or "URL" / "Mount Point" as a fallback).
*/
function parseDestinationInfo(stdout: string): string | null {
const lines = stdout.split("\n").map((l) => l.trim());
for (const line of lines) {
const nameMatch = line.match(/^Name\s*:.*$/);
if (nameMatch) return nameMatch[1].trim();
}
for (const line of lines) {
const mountMatch = line.match(/^Mount Point\s*:.*$/);
if (mountMatch) return mountMatch[1].trim();
}
return null;
}
// -- Status computation -------------------------------------------------------
interface StatusInputs {
running: boolean;
phase: string | null;
failed: boolean;
destination: string | null;
lastBackupMs: number | null;
staleMs: number;
}
function computeStatus(inputs: StatusInputs): { status: TimemachineStatus; stale: boolean; message: string } {
if (!inputs.destination) {
return { status: "no-destination", stale: false, message: "No Time Machine destination is configured." };
}
if (inputs.failed) {
return { status: "failed", stale: true, message: "Time Machine reported a failed backup." };
}
if (inputs.running) {
return {
status: "running",
stale: false,
message: inputs.phase ? `Backup running — phase: ${inputs.phase}` : "Backup currently running.",
};
}
if (inputs.lastBackupMs === null) {
return {
status: "unknown",
stale: false,
message: "Unable to determine last backup time. tmutil returned no usable output.",
};
}
const ageMs = Date.now() - inputs.lastBackupMs;
if (ageMs >= inputs.staleMs) {
const ageHours = Math.round(ageMs / (60 * 60 * 1000));
return {
status: "stale",
stale: true,
message: `Last backup is ${ageHours}h old, beyond the ${Math.round(inputs.staleMs / (60 * 60 * 1000))}h threshold.`,
};
}
return { status: "idle", stale: false, message: "Time Machine is up to date." };
}
// -- darwin implementation ----------------------------------------------------
async function checkTimemachineDarwin(staleMs: number): Promise<CheckTimemachineStatusResult> {
const [latestRes, statusRes, destRes] = await Promise.allSettled([
execAsync("tmutil latestbackup 2>/dev/null", { timeout: 10_000 }),
execAsync("tmutil status 2>/dev/null", { timeout: 10_000 }),
execAsync("tmutil destinationinfo 2>/dev/null", { timeout: 10_000 }),
]);
const latestStdout = latestRes.status === "fulfilled" ? latestRes.value.stdout : "";
const statusStdout = statusRes.status === "fulfilled" ? statusRes.value.stdout : "";
const destStdout = destRes.status === "fulfilled" ? destRes.value.stdout : "";
const { path: lastBackupPath, mtimeMs: lastBackupMs } = parseLatestBackupPath(latestStdout);
const { running, phase, failed } = parseTmutilStatus(statusStdout);
const destination = parseDestinationInfo(destStdout);
const { status, stale, message } = computeStatus({
running, phase, failed, destination, lastBackupMs, staleMs,
});
return {
platform: "darwin",
lastBackupMs,
lastBackupPath,
destination,
status,
stale,
message,
};
}
// -- win32 implementation -----------------------------------------------------
function checkTimemachineWin32(): CheckTimemachineStatusResult {
return {
platform: "win32",
lastBackupMs: null,
lastBackupPath: null,
destination: null,
status: "not-supported",
stale: false,
message: "Time Machine is a macOS-only feature. Use Windows Backup or File History on this platform.",
};
}
// -- Exported run function ----------------------------------------------------
export async function run({
staleThresholdHours = DEFAULT_STALE_HOURS,
}: {
staleThresholdHours?: number;
} = {}): Promise<CheckTimemachineStatusResult> {
const platform = os.platform();
if (isDarwin()) {
const staleMs = staleThresholdHours * 60 * 60 * 1000;
return checkTimemachineDarwin(staleMs);
}
if (isWin32()) {
return checkTimemachineWin32();
}
throw new Error(`check_timemachine_status: unsupported platform "${platform}"`);
}
// -- Test helpers -------------------------------------------------------------
/** Exported for unit tests only — do not use from production code. */
export const __testing = {
parseLatestBackupPath,
parseTmutilStatus,
parseDestinationInfo,
computeStatus,
checkTimemachineDarwin,
checkTimemachineWin32,
};