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

الانتقال إلى التثبيت

معلومات المصدر

المستودع
reason-machines/hermes-skills
آخر نشاط في المصدر
٢٥ مايو ٢٠٢٦ في ٠٥:٠٣
لغة SKILL.md المكتشفة
الإنجليزية
النجوم
٥
التفرعات
٠

خيارات التثبيت

يُحدَّد Prompt الذي يراجع المصدر أولًا بشكل افتراضي. يمكنك التبديل إلى أمر مباشر أو تنزيل نسخة محلية.

مراجعة ملفات المصدر

اقرأ SKILL.md وأي ملفات مرافقة يعرضها SkillsMP قبل أن تقرر التثبيت.

عرض SKILL.md

SKILL.md
تعليمات المصدر · معاينة للقراءة فقط
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 }
عرض على GitHub
ملف SKILL.md هذا كبير جدا، لذلك يعرض SkillsMP القسم الاول فقط هنا. عرض على GitHub