/**
* 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 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.
*
* Dry-run: returns the exact command(s) that would run + reports
* whether the agent has admin already (best-effort detection via a
* harmless probe). Does NOT touch the system in dry-run mode.
*
* Notes on side effects
* ---------------------
* - Active connections drop briefly (1–3 s). A user on a Bluetooth
* audio call will hear the audio interrupt.
* - Bluetooth-input devices (keyboard, trackpad) reconnect within
* 2–5 s; 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" ,
destructive: false ,
requiresConsent: true ,
supportsDryRun: true ,
affectedScope: [ "system" ],
auditRequired: true ,
escalationHint: {
darwin: "sudo /bin/launchctl kickstart -k system/com.apple.bluetoothd" ,
win32: "Restart-Service -Name bthserv -Force # run from elevated PowerShell" ,
},
schema: {
dryRun: z
. boolean ()
. 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 ;
/** Best-effort probe — true when the call succeeded. False when stderr indicated permission denial. */
succeeded : boolean ;
/** Diagnostic message — usually a stderr line on failure, or a confirmation on success. */
message : string ;
durationMs ?: number ;
}
// -- Platform helpers ---------------------------------------------------------
const DARWIN_CMD = "sudo /bin/launchctl kickstart -k system/com.apple.bluetoothd" ;
const WIN_CMD = "Restart-Service -Name bthserv -Force" ;
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" )
);
}
async function executeDarwin () : Promise <{ succeeded : boolean ; message : string ; durationMs : number }> {
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 { succeeded: false , message: stderr. trim (), durationMs: elapsed };
}
return {
succeeded: 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 { succeeded: false , message: msg, durationMs: elapsed };
}
}
async function executeWin32 () : Promise <{ succeeded : boolean ; message : string ; durationMs : number }> {
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 { succeeded: true , message: "bthserv restarted via Restart-Service" , durationMs: elapsed };
}
return { succeeded: 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 { succeeded: 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 ,
message:
`Would restart the Bluetooth daemon. ` +
`Active Bluetooth connections (audio, keyboards, mice) will drop ` +
`for 1–3 seconds while the daemon restarts. Requires admin privilege.` ,
};
}
const result = isDarwin () ? await executeDarwin () : await executeWin32 ();
return {
platform,
dryRun: false ,
command,
succeeded: result.succeeded,
message: result.message,
durationMs: result.durationMs,
};
}
// -- Test helpers -------------------------------------------------------------
/** Exported for unit tests only — do not use from production code. */
export const __testing = {
isPermissionFailure,
DARWIN_CMD,
WIN_CMD,
};