Skip to main content

hermespet-macos-ai-companion

Build, configure, and extend HermesPet — a native macOS AI companion living in the Dynamic Island with multi-engine support, desktop pets, and advanced system integration

Jump to install

Source facts

Repository
reason-machines/hermes-skills
Last source activity
May 25, 2026 at 05:03
Detected SKILL.md language
English
Stars
5
Forks
0

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
hermespet-macos-ai-companion
description
Build, configure, and extend HermesPet — a native macOS AI companion living in the Dynamic Island with multi-engine support, desktop pets, and advanced system integration
triggers
["how do I build HermesPet from source","configure HermesPet AI engines","add a new AI provider to HermesPet","customize HermesPet desktop pet animations","integrate voice recognition in HermesPet","troubleshoot HermesPet Dynamic Island display","extend HermesPet with new AI capabilities","debug HermesPet memory and context system"]
# HermesPet macOS AI Companion Skill > Skill by [ara.so](https://ara.so) — Hermes Skills collection. ## What is HermesPet? HermesPet is a native macOS application (macOS 14+) built with Swift 6 and SwiftUI that places an AI companion in your MacBook's Dynamic Island. It supports **5 parallel AI engines** (DeepSeek, Kimi, MiniMax, OpenAI, Claude Code, Codex, OpenClaw, custom gateways), **5 pixel desktop pets**, voice input, file drag-and-drop, multi-conversation context sharing, and local memory tracking. **Key Architecture:** - Pure native Swift 6 (no Electron/WebView) - SwiftUI-based UI with Dynamic Island integration - Local SQLite for conversation/memory storage - SFSpeechRecognizer for offline voice recognition - Multi-threaded AI engine orchestration (up to 8 simultaneous conversations) - Embedded opencode runtime for cloud AI (zero external dependencies) - Apache 2.0 licensed ## Project Structure ``` HermesPet/ ├── HermesPet/ # Main app target │ ├── Models/ # Data models (Conversation, Message, AIEngine) │ ├── Views/ # SwiftUI views │ │ ├── DynamicIslandView.swift │ │ ├── ChatWindowView.swift │ │ ├── DesktopPetView.swift │ │ └── SettingsView.swift │ ├── Managers/ # Core business logic │ │ ├── AIEngineManager.swift │ │ ├── ConversationManager.swift │ │ ├── VoiceManager.swift │ │ ├── MemoryManager.swift │ │ └── UpdateManager.swift │ ├── Services/ # AI provider integrations │ │ ├── CloudAIService.swift │ │ ├── ClaudeCodeService.swift │ │ ├── CodexService.swift │ │ ├── OpenClawService.swift │ │ └── HermesGatewayService.swift │ └── Utils/ # Helpers (FileDropHandler, MarkdownRenderer) └── docs/ # Assets and documentation ``` ## Building from Source ### Prerequisites ```bash # Requires Xcode 15.0+ (for Swift 6) xcode-select --install # Optional CLI tools (auto-detected by app) # Claude Code brew install anthropic-cli # Codex (if available) npm install -g @openai/codex-cli # OpenClaw npm install -g openclaw ``` ### Build Steps ```bash # Clone repository git clone https://github.com/basionwang-bot/HermesPet.git cd HermesPet # Open in Xcode open HermesPet.xcodeproj # Or build from command line xcodebuild -scheme HermesPet -configuration Release build # Create DMG for distribution # (Uses create-dmg script in scripts/) ./scripts/create-dmg.sh ``` **Code Signing Configuration:** ```swift // HermesPet.xcodeproj settings PRODUCT_BUNDLE_IDENTIFIER = "cc.hermespet.HermesPet" DEVELOPMENT_TEAM = "R34KL4X4D9" // Official Team ID CODE_SIGN_IDENTITY = "Apple Development" ENABLE_HARDENED_RUNTIME = YES MACOS_DEPLOYMENT_TARGET = 14.0 ``` ## Key Components & Extension Points ### 1. Adding a New AI Engine Create a new service conforming to `AIServiceProtocol`: ```swift // Services/CustomAIService.swift import Foundation protocol AIServiceProtocol { var engineType: AIEngineType { get } func sendMessage(_ message: String, context: [Message]) async throws -> String func streamMessage(_ message: String, context: [Message]) async throws -> AsyncThrowingStream<String, Error> func isAvailable() -> Bool } class CustomAIService: AIServiceProtocol { var engineType: AIEngineType { .custom } private let baseURL: String private let apiKey: String init(baseURL: String, apiKey: String) { self.baseURL = baseURL self.apiKey = apiKey } func isAvailable() -> Bool { // Check if API key is configured and endpoint is reachable return !apiKey.isEmpty && checkEndpointHealth() } func sendMessage(_ message: String, context: [Message]) async throws -> String { let request = buildRequest(message: message, context: context) let (data, response) = try await URLSession.shared.data(for: request) guard let httpResponse = response as? HTTPURLResponse, (200...299).contains(httpResponse.statusCode) else { throw AIServiceError.invalidResponse } let result = try JSONDecoder().decode(CustomAIResponse.self, from: data) return result.content } func streamMessage(_ message: String, context: [Message]) async throws -> AsyncThrowingStream<String, Error> { AsyncThrowingStream { continuation in Task { let request = buildStreamRequest(message: message, context: context) let (bytes, _) = try await URLSession.shared.bytes(for: request) for try await line in bytes.lines { if line.hasPrefix("data: ") { let json = String(line.dropFirst(6)) if let chunk = parseChunk(json) { continuation.yield(chunk) } } } continuation.finish() } } } private func buildRequest(message: String, context: [Message]) -> URLRequest { var request = URLRequest(url: URL(string: "\(baseURL)/v1/chat/completions")!) request.httpMethod = "POST" request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization") request.setValue("application/json", forHTTPHeaderField: "Content-Type") let messages = context.map { ["role": $0.role, "content": $0.content] } let body: [String: Any] = [ "model": "custom-model", "messages": messages + [["role": "user", "content": message]], "temperature": 0.7 ] request.httpBody = try? JSONSerialization.data(withJSONObject: body) return request } private func checkEndpointHealth() -> Bool { // Implement health check return true } } ``` Register in `AIEngineManager`: ```swift // Managers/AIEngineManager.swift class AIEngineManager: ObservableObject { @Published var availableEngines: [AIEngineType] = [] private var services: [AIEngineType: AIServiceProtocol] = [:] func detectAvailableEngines() { services = [:] // Cloud AI (always available with embedded runtime) if let cloudService = CloudAIService() { services[.cloudAI] = cloudService availableEngines.append(.cloudAI) } // Custom gateway if let customAPIKey = ProcessInfo.processInfo.environment["CUSTOM_AI_API_KEY"], let baseURL = ProcessInfo.processInfo.environment["CUSTOM_AI_BASE_URL"] { let customService = CustomAIService(baseURL: baseURL, apiKey: customAPIKey) if customService.isAvailable() { services[.custom] = customService availableEngines.append(.custom) } } // Claude Code (check CLI) if checkCLIAvailable("claude") { services[.claudeCode] = ClaudeCodeService() availableEngines.append(.claudeCode) } // ... other engines } private func checkCLIAvailable(_ command: String) -> Bool { let process = Process() process.launchPath = "/usr/bin/which" process.arguments = [command] process.launch() process.waitUntilExit() return process.terminationStatus == 0 } } ``` ### 2. Creating Custom Desktop Pet Animations Desktop pets are SwiftUI views with state-driven animations: ```swift // Views/Pets/CustomPetView.swift import SwiftUI struct CustomPetView: View { @State private var position: CGPoint @State private var animationPhase: PetAnimationPhase = .idle @State private var isSniffing = false enum PetAnimationPhase { case idle, walking, sniffing, eating, celebrating } var body: some View { ZStack { // Base sprite (16x16 pixel art) Image(spriteName) .interpolation(.none) .resizable() .frame(width: 48, height: 48) // Overlay effects (sniff particles, etc.) if isSniffing { sniffParticles } } .position(position) .onAppear { startIdleAnimation() } } private var spriteName: String { switch animationPhase { case .idle: return "custom-pet-idle-\(idleFrame)" case .walking: return "custom-pet-walk-\(walkFrame)" case .sniffing: return "custom-pet-sniff" case .eating: return "custom-pet-eat-\(eatFrame)" case .celebrating: return "custom-pet-celebrate" } } private var sniffParticles: some View { ForEach(0..<3, id: \.self) { i in Circle() .fill(Color.white.opacity(0.6)) .frame(width: 4, height: 4) .offset(x: CGFloat(i * 8) - 8, y: -10) .animation( .easeInOut(duration: 0.8) .repeatForever() .delay(Double(i) * 0.2), value: isSniffing ) } } func sniff(at location: CGPoint) { withAnimation(.spring(response: 0.3)) { position = location animationPhase = .sniffing isSniffing = true } DispatchQueue.main.asyncAfter(deadline: .now() + 2) { withAnimation { isSniffing = false animationPhase = .idle } } } func eat(file: URL) { animationPhase = .eating // Trigger eating animation cycle DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { animationPhase = .idle } } private func startIdleAnimation() { Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { _ in if animationPhase == .idle { idleFrame = (idleFrame + 1) % 4 } } } @State private var idleFrame = 0 @State private var walkFrame = 0 @State private var eatFrame = 0 } ``` Register pet in `DesktopPetManager`: ```swift // Managers/DesktopPetManager.swift class DesktopPetManager: ObservableObject { @Published var currentPet: AnyView? func setPet(for engine: AIEngineType) { switch engine { case .cloudAI: currentPet = AnyView(CloudPetView()) case .claudeCode: currentPet = AnyView(ClawdPetView()) case .custom: currentPet = AnyView(CustomPetView(position: initialPosition)) // ... other pets default: currentPet = nil } } } ``` ### 3. Extending Memory System The memory system tracks user interactions locally: ```swift // Managers/MemoryManager.swift import Foundation import SQLite3 class MemoryManager { private var db: OpaquePointer? private let sensitiveKeywords = ["password", "salary", "contract", ".env", "secret"] init() { openDatabase() createTables() } private func openDatabase() { let fileURL = try! FileManager.default .url(for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: true) .appendingPathComponent("HermesPet/memory.sqlite") if sqlite3_open(fileURL.path, &db) != SQLITE_OK { print("Failed to open database") } } private func createTables() { let createIntentTable = """ CREATE TABLE IF NOT EXISTS user_intents ( id INTEGER PRIMARY KEY AUTOINCREMENT, timestamp INTEGER NOT NULL, app_name TEXT, file_path TEXT, query TEXT, ai_engine TEXT, response_summary TEXT ) """ executeSQL(createIntentTable) } func recordIntent(app: String?, filePath: String?, query: String, engine: AIEngineType, response: String) { // Blacklist check if let path = filePath, containsSensitiveKeyword(path) { return } if containsSensitiveKeyword(query) { return } let summary = summarizeResponse(response) // First 200 chars let sql = """ INSERT INTO user_intents (timestamp, app_name, file_path, query, ai_engine, response_summary) VALUES (?, ?, ?, ?, ?, ?) """ var stmt: OpaquePointer? if sqlite3_prepare_v2(db, sql, -1, &stmt, nil) == SQLITE_OK { sqlite3_bind_int64(stmt, 1, Int64(Date().timeIntervalSince1970)) sqlite3_bind_text(stmt, 2, app, -1, nil) sqlite3_bind_text(stmt, 3, filePath, -1, nil) sqlite3_bind_text(stmt, 4, query, -1, nil) sqlite3_bind_text(stmt, 5, engine.rawValue, -1, nil) sqlite3_bind_text(stmt, 6, summary, -1, nil) if sqlite3_step(stmt) != SQLITE_DONE { print("Failed to insert intent") } } sqlite3_finalize(stmt) } func getDailySummary(for date: Date) -> String { let startOfDay = Calendar.current.startOfDay(for: date) let endOfDay = startOfDay.addingTimeInterval(86400) let sql = """ SELECT app_name, file_path, query, response_summary FROM user_intents WHERE timestamp >= ? AND timestamp < ? ORDER BY timestamp ASC """ var intents: [(app: String?, file: String?, query: String, response: String)] = [] var stmt: OpaquePointer? if sqlite3_prepare_v2(db, sql, -1, &stmt, nil) == SQLITE_OK { sqlite3_bind_int64(stmt, 1, Int64(startOfDay.timeIntervalSince1970)) sqlite3_bind_int64(stmt, 2, Int64(endOfDay.timeIntervalSince1970)) while sqlite3_step(stmt) == SQLITE_ROW { let app = sqlite3_column_text(stmt, 0).map { String(cString: $0) } let file = sqlite3_column_text(stmt, 1).map { String(cString: $0) } let query = String(cString: sqlite3_column_text(stmt, 2)) let response = String(cString: sqlite3_column_text(stmt, 3)) intents.append((app, file, query, response)) } } sqlite3_finalize(stmt) return generateSummaryPrompt(from: intents) } private func containsSensitiveKeyword(_ text: String) -> Bool { let lowercase = text.lowercased() return sensitiveKeywords.contains { lowercase.contains($0) } } private func generateSummaryPrompt(from intents: [(app: String?, file: String?, query: String, response: String)]) -> String { var prompt = "Based on yesterday's activity:\n\n" for intent in intents { if let app = intent.app { prompt += "- Used \(app)\n" } if let file = intent.file { prompt += "- Worked on \(file)\n" } prompt += "- Asked: \(intent.query)\n" } prompt += "\nGenerate a brief daily summary in Markdown and suggest 1-2 follow-up actions." return prompt }
View on GitHub
This SKILL.md is very large, so SkillsMP previews the first section here. View on GitHub