Restarts the system Bluetooth daemon (macOS bluetoothd / Windows bthserv) so the OS re-enumerates paired devices and re-establishes connections. Last-resort fix for paired-but-disconnected devices when toggling the device itself has failed. Requires admin privilege; will briefly drop all Bluetooth connections including any audio call in progress.
/** * mcp/skills/resetBluetoothModule.ts — reset_bluetooth_module skill * * Restarts the Bluetooth daemon / service so the OS re-enumerates paired * devices and re-establishes connections. Used as a last-resort fix * when devices are paired-but-disconnected, after toggling the device * itself failed. * * Platform strategy * ----------------- * darwin `sudo -n /bin/launchctl kickstart -k system/com.apple.bluetoothd` * restarts the system bluetoothd. Requires admin — the G4 * scope-boundary check (`affectedScope: ["system"]`) blocks * non-admin runs by aborting the step. * win32 `Restart-Service -Name bthserv -Force` via elevated * PowerShell. Same admin requirement. * * Both restarts are ASYNCHRONOUS — the command returns the moment the * request is accepted, while the radio stays down for several more * seconds. `succeeded` therefore derives from a bounded poll of the * real radio state, never from the command's exit code. * * Dry-run: returns the exact command(s) that would run. Does NOT touch * the system in dry-run mode. * * Notes on side effects * --------------------- * - Active connections drop for 2–5 s. A user on a Bluetooth audio * call will hear the audio interrupt. * - Bluetooth-input devices (keyboard, trackpad) reconnect within the * same window; the user may temporarily lose input. This is why the * skill always surfaces the dry-run preview + consent gate. */import * as os from "os";import { z } from "zod";import { execAsync, runPS, isDarwin, isWin32,} from "./_shared/platform";// -- Meta ---------------------------------------------------------------------export const meta = { name: "reset_bluetooth_module", description: "Restarts the system Bluetooth daemon (macOS bluetoothd / Windows bthserv) " + "so the OS re-enumerates paired devices and re-establishes connections. " + "Last-resort fix for paired-but-disconnected devices when toggling the " + "device itself has failed. Requires admin privilege; will briefly drop " + "all Bluetooth connections including any audio call in progress.", riskLevel: "medium", // Restarting the Bluetooth daemon drops every active connection — including // the user's keyboard / trackpad — so destructive:true. It also makes G4 // auto-fire the dry-run preview + consent flow (autoTriggerDryRun = // supportsDryRun && (riskLevel >= high || destructive)); without it the // consent gate fires with rationale text only and the user never sees the // command or the connection-drop warning before approving. destructive: true, requiresConsent: true, supportsDryRun: true, affectedScope: ["system"], auditRequired: true, // Covers the kickstart plus the bounded settle poll below; must clear G4's // TOOL_TIMEOUT_MS so the poll isn't killed mid-restart. timeoutMs: 90_000, escalationHint: { darwin: "sudo /bin/launchctl kickstart -k system/com.apple.bluetoothd", win32: "Restart-Service -Name bthserv -Force # run from elevated PowerShell", }, outputKeys: ["platform","dryRun","command","succeeded","settled","message","durationMs"], schema: { dryRun: z .boolean() .nullable().optional() .describe("If true, report what would be restarted without touching the system."), },} as const;// -- Types --------------------------------------------------------------------export interface ResetBluetoothModuleResult { platform: NodeJS.Platform; dryRun: boolean; command: string; /** * True only when the daemon was restarted AND the radio came back up * within the settle budget. Never set from "the restart command exited * 0" alone — the restart is asynchronous and the radio is down for * several seconds afterwards. */ succeeded: boolean; /** * Whether the radio reached its terminal (powered-on) state before the * settle deadline. `false` with `succeeded: false` after a clean restart * means "still coming back up", which is not the same as a failed reset. */ settled: boolean; /** Diagnostic message — usually a stderr line on failure, or a confirmation on success. */ message: string; durationMs?: number;}// -- Platform helpers ---------------------------------------------------------// `-n` is load-bearing: without it sudo prompts for a password on a TTY the// Electron child process does not have, and the call hangs until the timeout.// The privileged helper daemon is the real path for this tool// (affectedScope: ["system"]); this local command is the fallback for a// machine where the agent already holds admin.const DARWIN_CMD = "sudo -n /bin/launchctl kickstart -k system/com.apple.bluetoothd";const WIN_CMD = "Restart-Service -Name bthserv -Force";/** Bounded settle poll — the radio typically returns in 2–5 s. */const SETTLE_DEADLINE_MS = 20_000;const SETTLE_INTERVAL_MS = 1_500;function isPermissionFailure(stderr: string): boolean { const s = stderr.toLowerCase(); return ( s.includes("permission denied") || s.includes("not permitted") || s.includes("access is denied") || s.includes("requires administrative") || s.includes("must be run as administrator") || s.includes("needed root privileges") || s.includes("a password is required") );}/** True when the OS reports the Bluetooth radio powered on, right now. */async function probeRadioOn(): Promise<boolean> { try { if (isDarwin()) { const { stdout } = await execAsync( "system_profiler SPBluetoothDataType -json 2>/dev/null", { timeout: 10_000, maxBuffer: 10 * 1024 * 1024 }, ); // macOS 13+ reports "attrib_on"; older builds reported "On". return /"controller_state"\s*:\s*"(attrib_on|On)"/i.test(stdout); } const stdout = await runPS( `$ErrorActionPreference='SilentlyContinue'$svc = Get-Service -Name bthserv -ErrorAction SilentlyContinueif ($svc -and $svc.Status -eq 'Running') { 'on' } else { 'off' }`, { timeoutMs: 10_000 }, ); return stdout.trim().toLowerCase() === "on"; } catch { return false; }}/** * Both platform restarts are fire-and-forget: the command returns as soon * as the request is accepted, while the radio stays down for several more * seconds. Poll until it is actually back before reporting success (see * SKILL-AUDIT-CHECKLIST § 10i). */async function waitForRadio(deadlineAt: number): Promise<boolean> { for (;;) { if (await probeRadioOn()) return true; if (Date.now() >= deadlineAt) return false; await new Promise((resolve) => setTimeout(resolve, SETTLE_INTERVAL_MS)); }}interface ExecOutcome { /** The restart command itself was accepted (says nothing about the radio). */ issued: boolean; message: string; durationMs: number;}async function executeDarwin(): Promise<ExecOutcome> { const start = Date.now(); try { const { stdout, stderr } = await execAsync(DARWIN_CMD, { timeout: 15_000 }); const elapsed = Date.now() - start; if (stderr && isPermissionFailure(stderr)) { return { issued: false, message: stderr.trim(), durationMs: elapsed }; } return { issued: true, message: stdout.trim() || "bluetoothd restarted via launchctl kickstart", durationMs: elapsed, }; } catch (err) { const elapsed = Date.now() - start; const msg = (err as Error).message; return { issued: false, message: msg, durationMs: elapsed }; }}async function executeWin32(): Promise<ExecOutcome> { const start = Date.now(); try { const stdout = await runPS(`$ErrorActionPreference='Stop'; ${WIN_CMD}; 'OK'`, { timeoutMs: 15_000 }); const elapsed = Date.now() - start; if (stdout.trim().toLowerCase().endsWith("ok")) { return { issued: true, message: "bthserv restarted via Restart-Service", durationMs: elapsed }; } return { issued: false, message: stdout.trim() || "Restart-Service returned no output", durationMs: elapsed }; } catch (err) { const elapsed = Date.now() - start; const msg = (err as Error).message; return { issued: false, message: msg, durationMs: elapsed }; }}// -- Exported run function ----------------------------------------------------export async function run({ dryRun = false,}: { dryRun?: boolean } = {}): Promise<ResetBluetoothModuleResult> { const platform = os.platform(); if (!isDarwin() && !isWin32()) { throw new Error(`reset_bluetooth_module: unsupported platform "${platform}"`); } const command = isDarwin() ? DARWIN_CMD : WIN_CMD; if (dryRun) { return { platform, dryRun: true, command, succeeded: true, settled: true, message: `Would restart the Bluetooth daemon. ` + `Active Bluetooth connections (audio, keyboards, mice) will drop ` + `for 2–5 seconds while the daemon restarts. Requires admin privilege.`, }; } const start = Date.now(); const outcome = isDarwin() ? await executeDarwin() : await executeWin32(); if (!outcome.issued) { return { platform, dryRun: false, command, succeeded: false, settled: false, message: outcome.message, durationMs: outcome.durationMs, }; } const settled = await waitForRadio(Date.now() + SETTLE_DEADLINE_MS); return { platform, dryRun: false, command, succeeded: settled, settled, message: settled ? `${outcome.message}. Radio is back up — paired devices reconnect on their own over the next few seconds.` : `The restart command was accepted but the Bluetooth radio had not come ` + `back up after ${Math.round(SETTLE_DEADLINE_MS / 1000)} seconds. Wait a moment and ` + `re-check; if it stays down, toggle Bluetooth off and on from the menu bar ` + `(macOS) or Quick Settings (Windows), or restart the machine.`, durationMs: Date.now() - start, };}// -- Test helpers -------------------------------------------------------------/** Exported for unit tests only — do not use from production code. */export const __testing = { isPermissionFailure, probeRadioOn, waitForRadio, DARWIN_CMD, WIN_CMD, SETTLE_DEADLINE_MS, SETTLE_INTERVAL_MS,};