UNPKG

@tgomareli/macos-tools-mcp

Version:

MCP server for advanced macOS system monitoring and file search capabilities

214 lines 7.68 kB
import { exec } from "child_process"; import { promisify } from "util"; import { stat } from "fs/promises"; import { basename } from "path"; const execAsync = promisify(exec); export async function searchWithSpotlight(query, path, fileTypes) { try { let mdfindQuery = query; const args = []; if (path) { args.push(`-onlyin "${path}"`); } if (fileTypes && fileTypes.length > 0) { const typeQuery = fileTypes .map(ext => `kMDItemFSName == "*.${ext}"`) .join(" || "); mdfindQuery = `(${typeQuery}) && (${query})`; } const { stdout } = await execAsync(`mdfind ${args.join(" ")} '${mdfindQuery}'`); const files = stdout.trim().split("\n").filter(Boolean); const results = []; for (const file of files) { try { const stats = await stat(file); results.push({ path: file, filename: basename(file), size: stats.size, modifiedDate: stats.mtime, score: 1.0, }); } catch { // Skip files we can't access } } return results; } catch (error) { throw new Error(`Spotlight search failed: ${error}`); } } export async function searchFileContent(pattern, path, fileTypes, isRegex = false) { try { const includes = fileTypes?.map(ext => `--include="*.${ext}"`).join(" ") || ""; const regexFlag = isRegex ? "-E" : "-F"; const { stdout } = await execAsync(`grep -r ${regexFlag} -n ${includes} "${pattern}" "${path}" 2>/dev/null | head -1000`, { maxBuffer: 10 * 1024 * 1024 }); const results = []; const lines = stdout.trim().split("\n").filter(Boolean); for (const line of lines) { const match = line.match(/^(.+?):(\d+):(.*)$/); if (match) { const filePath = match[1]; const lineNumber = parseInt(match[2]); const content = match[3]; try { const stats = await stat(filePath); results.push({ path: filePath, filename: basename(filePath), size: stats.size, modifiedDate: stats.mtime, matchedContent: content.trim().substring(0, 200), lineNumber, score: 1.0, }); } catch { // Skip inaccessible files } } } return results; } catch (error) { if (error.code === 1) { // No matches found return []; } throw new Error(`Content search failed: ${error}`); } } export async function getFileTags(filePath) { try { const { stdout } = await execAsync(`xattr -p com.apple.metadata:_kMDItemUserTags "${filePath}" 2>/dev/null | xxd -r -p | plutil -convert json -o - -`); const tags = JSON.parse(stdout); return tags.map((tag) => { if (typeof tag === "string") return tag; if (tag && typeof tag === "object" && tag.name) return tag.name; return null; }).filter(Boolean); } catch { return []; } } export async function setFileTags(filePath, tags) { try { if (tags.length === 0) { // Remove tags await execAsync(`xattr -d com.apple.metadata:_kMDItemUserTags "${filePath}" 2>/dev/null`); return; } // Create plist format for tags const plistContent = `<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <array> ${tags.map(tag => ` <string>${tag}</string>`).join("\n")} </array> </plist>`; // Convert to binary plist and set as xattr const { stdout } = await execAsync(`echo '${plistContent}' | plutil -convert binary1 -o - - | xxd -p | tr -d '\n'`); await execAsync(`xattr -wx com.apple.metadata:_kMDItemUserTags "${stdout}" "${filePath}"`); } catch (error) { throw new Error(`Failed to set tags: ${error}`); } } export async function searchByTags(tags, path) { try { const tagQuery = tags .map(tag => `kMDItemUserTags == "${tag}"`) .join(" || "); let command = `mdfind '${tagQuery}'`; if (path) { command = `mdfind -onlyin "${path}" '${tagQuery}'`; } const { stdout } = await execAsync(command); const files = stdout.trim().split("\n").filter(Boolean); const results = []; for (const file of files) { try { const stats = await stat(file); const fileTags = await getFileTags(file); results.push({ path: file, filename: basename(file), size: stats.size, modifiedDate: stats.mtime, tags: fileTags, score: tags.filter(t => fileTags.includes(t)).length / tags.length, }); } catch { // Skip inaccessible files } } return results.sort((a, b) => b.score - a.score); } catch (error) { throw new Error(`Tag search failed: ${error}`); } } export function fuzzyMatch(query, text) { const queryLower = query.toLowerCase(); const textLower = text.toLowerCase(); if (textLower.includes(queryLower)) { return 1.0; } let score = 0; let queryIndex = 0; let textIndex = 0; while (queryIndex < queryLower.length && textIndex < textLower.length) { if (queryLower[queryIndex] === textLower[textIndex]) { score++; queryIndex++; } textIndex++; } return queryIndex === queryLower.length ? score / queryLower.length : 0; } export async function searchFilenames(query, path, fileTypes, maxResults = 100) { try { const findArgs = [path]; if (fileTypes && fileTypes.length > 0) { const typeArgs = fileTypes .map((ext, i) => i === 0 ? `-name "*.${ext}"` : `-o -name "*.${ext}"`) .join(" "); findArgs.push(`\\( ${typeArgs} \\)`); } const { stdout } = await execAsync(`find ${findArgs.join(" ")} -type f 2>/dev/null | head -${maxResults * 2}`); const files = stdout.trim().split("\n").filter(Boolean); const results = []; for (const file of files) { const filename = basename(file); const score = fuzzyMatch(query, filename); if (score > 0.5) { try { const stats = await stat(file); results.push({ path: file, filename, size: stats.size, modifiedDate: stats.mtime, score, }); } catch { // Skip inaccessible files } } } return results .sort((a, b) => b.score - a.score) .slice(0, maxResults); } catch (error) { throw new Error(`Filename search failed: ${error}`); } } //# sourceMappingURL=file-search.js.map