diff --git a/src/integrations/system.js b/src/integrations/system.js new file mode 100644 index 0000000..bf2dfb4 --- /dev/null +++ b/src/integrations/system.js @@ -0,0 +1,127 @@ +import { execFile, execFile as execFileCb } from "node:child_process"; +import { promisify } from "node:util"; + +const execFileAsync = promisify(execFile); + +export async function getSystemInfo() { + const results = {}; + // sw_vers for macOS version + try { + const { stdout } = await execFileAsync("/usr/bin/sw_vers", { timeout: 5000 }); + const info = {}; + for (const line of stdout.splitlines()) { + const [k, ...rest] = line.split(":"); + if (!k) continue; + info[k.trim()] = rest.join(":").trim(); + } + results.sw_vers = info; + results.macos_version = info.ProductVersion || info.productVersion || ""; + results.build = info.BuildVersion || info.buildVersion || ""; + } catch (e) { + results.sw_vers_error = e.message; + } + + // uname -a + try { + const { stdout } = await execFileAsync("/usr/bin/uname", ["-a"], { timeout: 3000 }); + results.uname = stdout.trim(); + } catch (e) { + results.uname_error = e.message; + } + + // Check for SpeechAnalyzer / SpeechTranscriber availability (macOS 26+) + // These frameworks exist only on macOS 26+. We probe via swift/python check. + try { + const { stdout } = await execFileAsync("/usr/bin/python3", ["-c", ` +import os, glob, sys +# Check if Speech framework contains new symbols (macOS 26) +frameworks = glob.glob('/System/Library/Frameworks/Speech.framework/*') + glob.glob('/System/Library/PrivateFrameworks/*Speech*') +# Simplest: check macOS version parse +import platform +print(platform.mac_ver()[0]) +`], { timeout: 5000 }); + results.python_mac_ver = stdout.trim(); + } catch (e) { + results.python_mac_ver_error = e.message; + } + + // Try to detect SpeechAnalyzer via file existence / Swift availability + try { + // On macOS 26, Speech.framework/Versions should have newer build + const { stdout } = await execFileAsync("/bin/ls", ["-la", "/System/Library/Frameworks/Speech.framework/"], { timeout: 3000 }); + results.speech_framework_ls = stdout.trim().slice(0, 2000); + } catch (e) { + results.speech_framework_error = e.message; + } + + // Check hardware model + try { + const { stdout } = await execFileAsync("/usr/sbin/sysctl", ["-n", "hw.model"], { timeout: 2000 }); + results.hw_model = stdout.trim(); + } catch {} + try { + const { stdout } = await execFileAsync("/usr/sbin/sysctl", ["-n", "machdep.cpu.brand_string"], { timeout: 2000 }); + results.cpu_brand = stdout.trim(); + } catch {} + + // Is this macOS 26+ ? + if (results.macos_version) { + const major = parseInt(results.macos_version.split(".")[0], 10); + results.is_macos_26_plus = major >= 26; + results.speech_analyzer_expected = major >= 26 ? "likely available (macOS 26+)" : "not available - requires macOS 26+"; + } + + return results; +} + +export async function getSpeechApiStatus() { + const sysInfo = await getSystemInfo(); + // Try to run a tiny Swift snippet that imports Speech and checks for SpeechAnalyzer + // Fallback to reporting version info if swiftc not available + let swiftCheck = null; + try { + // Write temp swift file that checks API availability + const { stdout: swiftPath } = await execFileAsync("/usr/bin/which", ["swift"], { timeout: 2000 }); + const swiftBin = swiftPath.trim(); + if (swiftBin) { + // Create a small swift program to test SpeechAnalyzer availability + const swiftCode = ` +import Speech +import Foundation +#if canImport(Speech) +if #available(macOS 26.0, *) { + print("SpeechAnalyzer: available") + // Try to reference the type + let _ = SpeechTranscriber.self + print("SpeechTranscriber: available") +} else { + print("SpeechAnalyzer: requires macOS 26") +} +#else +print("Speech framework not importable") +#endif +`; + const tmpFile = `/tmp/speech_check_${Date.now()}.swift`; + const { writeFile, unlink } = await import("node:fs/promises"); + await writeFile(tmpFile, swiftCode, "utf8"); + try { + const { stdout, stderr } = await execFileAsync(swiftBin, [tmpFile], { timeout: 15000, maxBuffer: 2 * 1024 * 1024 }); + swiftCheck = { stdout: stdout.trim(), stderr: stderr.trim(), ok: true }; + } catch (e) { + swiftCheck = { stdout: e.stdout?.toString().trim() || "", stderr: (e.stderr?.toString() || e.message).trim().slice(0, 2000), ok: false }; + } finally { + try { await unlink(tmpFile); } catch {} + } + } + } catch (e) { + swiftCheck = { error: e.message }; + } + + return { + system: sysInfo, + swift_availability: swiftCheck, + conclusion: sysInfo.is_macos_26_plus + ? "macOS 26+ detected — SpeechAnalyzer/SpeechTranscriber should be available per Apple docs." + : `macOS ${sysInfo.macos_version || "unknown"} detected — SpeechAnalyzer requires macOS 26+. ${sysInfo.macos_version ? `You are on ${sysInfo.macos_version}, need to upgrade to 26.` : ""}`, + }; +} diff --git a/src/server.js b/src/server.js index 9aa9ae4..6b34d4c 100644 --- a/src/server.js +++ b/src/server.js @@ -18,6 +18,7 @@ import { listReminderLists, listReminders, } from "./integrations/reminders.js"; +import { getSystemInfo, getSpeechApiStatus } from "./integrations/system.js"; const jsonResult = (value) => ({ content: [{ type: "text", text: JSON.stringify(value, null, 2) }], @@ -226,5 +227,19 @@ export function createMacMiniMcpServer() { handled(() => getDecoStats("firmware")), ); + server.tool( + "system_get_info", + "Get Mac mini system info: macOS version (sw_vers), uname, hardware model, and whether macOS 26+ SpeechAnalyzer is available.", + {}, + handled(() => getSystemInfo()), + ); + + server.tool( + "system_speech_api_status", + "Check if Apple's new SpeechAnalyzer / SpeechTranscriber (macOS 26+) is available — returns macOS version, build, and Swift availability probe.", + {}, + handled(() => getSpeechApiStatus()), + ); + return server; }