Skip to main content

hermes-swift-mac-app

Build and configure the native macOS desktop app wrapper for Hermes Web UI with Swift and WKWebView

Zur Installation springen

Quellinformationen

Repository
reason-machines/hermes-skills
Letzte Quellaktivität
17. Mai 2026 um 23:20
Erkannte Sprache von SKILL.md
Englisch
Sterne
5
Forks
0

Installationsoptionen

Standardmäßig ist der Prompt ausgewählt, der zuerst die Quelle prüft. Sie können zu einem direkten Befehl wechseln oder eine lokale Kopie herunterladen.

Quelldateien prüfen

Lesen Sie SKILL.md und alle von SkillsMP angezeigten Begleitdateien, bevor Sie sich für eine Installation entscheiden.

SKILL.md wird angezeigt

SKILL.md
Quellanweisungen · Schreibgeschützte Vorschau
name
hermes-swift-mac-app
description
Build and configure the native macOS desktop app wrapper for Hermes Web UI with Swift and WKWebView
triggers
["how do I install the Hermes macOS app","set up Hermes Agent on my Mac","connect Hermes Swift app to remote server","configure SSH tunnel for Hermes macOS","build Hermes Agent from source","troubleshoot Hermes Mac app connection","customize Hermes Swift wrapper settings","release new version of Hermes macOS app"]
# Hermes Swift Mac App Skill > Skill by [ara.so](https://ara.so) — Hermes Skills collection ## Overview Hermes Agent is a native macOS desktop wrapper for [Hermes Web UI](https://github.com/nesquena/hermes-webui). Built with Swift and WKWebView, it provides a standalone Mac app experience with no Electron or heavy dependencies. The app supports both local Hermes instances and remote servers via SSH tunneling. **Key features:** - Native macOS app using WKWebView (requires macOS 12+) - Direct connection mode for local Hermes Web UI - SSH tunnel mode for remote servers - Clipboard integration (text and images) - System notifications for AI responses - Auto-update via Sparkle framework - Signed and notarized for Gatekeeper ## Installation ### For End Users Download the latest DMG from releases: ```bash # Visit https://github.com/hermes-webui/hermes-swift-mac/releases # Download Hermes-Agent-vX.X.X.dmg # Open DMG and drag to Applications folder ``` ### For Developers - Build from Source ```bash # Install Xcode Command Line Tools if needed xcode-select --install # Clone and build git clone https://github.com/hermes-webui/hermes-swift-mac.git cd hermes-swift-mac ./build.sh ``` The `build.sh` script: 1. Compiles the Swift code via `swift build -c release` 2. Creates the `.app` bundle structure 3. Converts `icon.png` to `AppIcon.icns` 4. Copies binaries and resources 5. Installs to `/Applications/Hermes Agent.app` ## Project Structure ``` Sources/HermesAgent/ ├── main.swift # Entry point, signal handling ├── AppDelegate.swift # App lifecycle, menu bar, Sparkle updater ├── BrowserWindowController.swift # WKWebView window, clipboard, notifications ├── TunnelManager.swift # SSH tunnel management ├── PreferencesWindowController.swift # Settings UI └── SplashWindowController.swift # Launch splash screen Package.swift # SPM manifest build.sh # Build and install script scripts/release.sh # Release automation ``` ## Configuration Modes ### Direct Mode (Local Hermes) For Hermes Web UI running on the same Mac: **Default settings:** - Target URL: `http://localhost:8787` - Connection Mode: Direct ```swift // In PreferencesWindowController.swift // Default URL stored in UserDefaults UserDefaults.standard.string(forKey: "targetURL") ?? "http://localhost:8787" ``` ### SSH Tunnel Mode (Remote Hermes) For Hermes Web UI on a remote server: **Required settings:** - Connection Mode: SSH Tunnel - Username: SSH user on remote server - Host: Server hostname or IP - Local Port: Port on Mac (default 8787) - Remote Port: Port where Hermes runs on server (default 8787) **SSH requirements:** - Key-based authentication must work (`ssh user@host` without password) - `~/.ssh/known_hosts` file accessible - No password authentication support ```swift // TunnelManager.swift constructs SSH command: ssh -o StrictHostKeyChecking=accept-new \ -o ExitOnForwardFailure=yes \ -N -L \(localPort):localhost:\(remotePort) \ \(username)@\(host) ``` ## Code Examples ### Reading User Preferences ```swift // From PreferencesWindowController.swift let defaults = UserDefaults.standard // Get connection mode let mode = defaults.string(forKey: "connectionMode") ?? "direct" // Get target URL let targetURL = defaults.string(forKey: "targetURL") ?? "http://localhost:8787" // Get SSH settings (tunnel mode) let sshUsername = defaults.string(forKey: "sshUsername") ?? "" let sshHost = defaults.string(forKey: "sshHost") ?? "" let localPort = defaults.integer(forKey: "localPort") let remotePort = defaults.integer(forKey: "remotePort") ``` ### Starting SSH Tunnel ```swift // From TunnelManager.swift class TunnelManager { private var sshProcess: Process? func startTunnel(username: String, host: String, localPort: Int, remotePort: Int) { let process = Process() process.executableURL = URL(fileURLWithPath: "/usr/bin/ssh") process.arguments = [ "-o", "StrictHostKeyChecking=accept-new", "-o", "ExitOnForwardFailure=yes", "-N", "-L", "\(localPort):localhost:\(remotePort)", "\(username)@\(host)" ] try? process.run() self.sshProcess = process // Monitor tunnel health monitorTunnel() } func stopTunnel() { sshProcess?.terminate() sshProcess = nil } } ``` ### Testing Connection ```swift // From PreferencesWindowController.swift func testConnection(url: String, completion: @escaping (Bool) -> Void) { guard let testURL = URL(string: url) else { completion(false) return } var request = URLRequest(url: testURL) request.timeoutInterval = 5.0 URLSession.shared.dataTask(with: request) { data, response, error in DispatchQueue.main.async { if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 { completion(true) } else { completion(false) } } }.resume() } ``` ### Clipboard Integration ```swift // From BrowserWindowController.swift // Paste handler for text and images @objc func handlePaste(_ sender: Any?) { let pasteboard = NSPasteboard.general if let image = pasteboard.readObjects(forClasses: [NSImage.self])?.first as? NSImage, let data = image.tiffRepresentation, let bitmap = NSBitmapImageRep(data: data), let pngData = bitmap.representation(using: .png, properties: [:]) { let base64 = pngData.base64EncodedString() let js = "window.pasteImage('data:image/png;base64,\(base64)');" webView.evaluateJavaScript(js) } else if let string = pasteboard.string(forType: .string) { let js = "window.pasteText(\(jsonEscape(string)));" webView.evaluateJavaScript(js) } } ``` ### System Notifications ```swift // From BrowserWindowController.swift // Show notification when AI response completes func showNotification(title: String, body: String) { let content = UNMutableNotificationContent() content.title = title content.body = body content.sound = .default let request = UNNotificationRequest( identifier: UUID().uuidString, content: content, trigger: nil ) UNUserNotificationCenter.current().add(request) } ``` ### Auto-Update with Sparkle ```swift // From AppDelegate.swift import Sparkle class AppDelegate: NSObject, NSApplicationDelegate { private var updaterController: SPUStandardUpdaterController? func applicationDidFinishLaunching(_ notification: Notification) { // Initialize Sparkle updater updaterController = SPUStandardUpdaterController( startingUpdater: true, updaterDelegate: nil, userDriverDelegate: nil ) // Check for updates on launch updaterController?.updater.checkForUpdatesInBackground() } @objc func checkForUpdates(_ sender: Any?) { updaterController?.updater.checkForUpdates() } } ``` ## Building and Running ### Development Build ```bash # Build in debug mode swift build # Run directly swift run # Run tests swift test ``` ### Release Build ```bash # Build release binary swift build -c release # Or use build script (compiles + installs) ./build.sh ``` The build script handles: - Release compilation - App bundle creation at `HermesAgent.app/` - Icon conversion (PNG → ICNS) - Binary signing (if configured) - Installation to `/Applications/` ### Icon Generation ```bash # Convert PNG to ICNS (done by build.sh) sips -s format icns icon.png --out AppIcon.icns # Or manually with iconutil mkdir icon.iconset sips -z 16 16 icon.png --out icon.iconset/icon_16x16.png sips -z 32 32 icon.png --out icon.iconset/icon_16x16@2x.png sips -z 32 32 icon.png --out icon.iconset/icon_32x32.png sips -z 64 64 icon.png --out icon.iconset/icon_32x32@2x.png sips -z 128 128 icon.png --out icon.iconset/icon_128x128.png sips -z 256 256 icon.png --out icon.iconset/icon_128x128@2x.png sips -z 256 256 icon.png --out icon.iconset/icon_256x256.png sips -z 512 512 icon.png --out icon.iconset/icon_256x256@2x.png sips -z 512 512 icon.png --out icon.iconset/icon_512x512.png sips -z 1024 1024 icon.png --out icon.iconset/icon_512x512@2x.png iconutil -c icns icon.iconset ``` ## Releasing ### Create a Release ```bash # Use release script (pushes main, then tag separately) scripts/release.sh v1.0.9 ``` **Why separate pushes?** GitHub sometimes drops workflow triggers when commit and tag are pushed together. The script: 1. Pushes `main` branch 2. Waits briefly 3. Pushes the tag separately This ensures the GitHub Actions workflow fires reliably. ### Manual Release Process ```bash # Tag the release git tag v1.0.9 git push origin main git push origin v1.0.9 # If workflow doesn't trigger, run manually: # Actions → Build and Release macOS App → Run workflow → enter tag ``` ### GitHub Actions Workflow The `.github/workflows/build-release.yml` (not shown in README but typical) would: 1. Build the app on `macos-latest` 2. Sign with Developer ID certificate (from secrets) 3. Notarize with Apple (requires Apple ID credentials) 4. Create DMG with `create-dmg` or `hdiutil` 5. Upload DMG as release asset ## Common Patterns ### Checking Tunnel Status ```swift // From TunnelManager.swift func isTunnelRunning() -> Bool { guard let process = sshProcess else { return false } return process.isRunning } func monitorTunnel() { // Check if local port is accessible let socket = CFSocketCreate(kCFAllocatorDefault, PF_INET, SOCK_STREAM, IPPROTO_TCP, 0, nil, nil) // ... probe localhost:localPort } ``` ### Handling WebView Navigation ```swift // From BrowserWindowController.swift extension BrowserWindowController: WKNavigationDelegate { func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { // Open external links in Safari if let url = navigationAction.request.url, navigationAction.navigationType == .linkActivated { NSWorkspace.shared.open(url) decisionHandler(.cancel) return } decisionHandler(.allow) } } ``` ### Global Keyboard Shortcut ```swift // From AppDelegate.swift func applicationDidFinishLaunching(_ notification: Notification) { // Register ⌘⇧H to bring app forward NSEvent.addGlobalMonitorForEvents(matching: .keyDown) { event in if event.modifierFlags.contains([.command, .shift]), event.charactersIgnoringModifiers == "h" { NSApp.activate(ignoringOtherApps: true) } } } ``` ## Troubleshooting ### Connection Error on Launch **Symptom:** Blank page or "Unable to connect" error **Solutions:** 1. Ensure Hermes Web UI is running: ```bash cd ~/hermes-webui-public bash start.sh ``` 2. Check Target URL in Preferences (⌘,) 3. Use Test Connection button to verify 4. Check Console.app for errors from "Hermes Agent" ### SSH Tunnel Fails Immediately **Symptom:** Status shows "Disconnected" right after connecting **Solutions:** 1. Test SSH key auth works: ```bash ssh user@your-server # Should connect without password ``` 2. Set up SSH keys if needed: ```bash ssh-keygen -t ed25519 ssh-copy-id user@your-server ``` 3. Verify remote port is correct (where Hermes actually runs) 4. Check `~/.ssh/known_hosts` isn't corrupted ### Voice Input Not Working **Symptom:** Microphone button doesn't respond **Solutions:** 1. Grant microphone permission: - System Settings → Privacy & Security → Microphone - Enable "Hermes Agent" 2. Restart app after granting permission 3. Check Info.plist includes `NSMicrophoneUsageDescription` ### Gatekeeper Blocks App **Symptom:** "App can't be opened because it is from an unidentified developer" **Solutions:** 1. Download latest release (v1.0.4+) — these are signed and notarized 2. If building from source without signing: ```bash xattr -cr /Applications/Hermes\ Agent.app ``` 3. Right-click app → Open (first time only) ### Blurry App Icon **Symptom:** Icon looks pixelated in Dock after building **Solution:** ```bash # Refresh icon cache killall Dock ``` ### Port Already in Use **Symptom:** "Port forwarding failed" when starting tunnel **Solutions:** ```bash # Find what's using the port lsof -i :8787 # Kill the process or change Local Port in Preferences ``` ## Environment Variables The app doesn't use environment variables directly, but for development: ```bash # Enable WebKit debug logging export WEBKIT_DEBUG=1 # Sparkle update feed (set in Info.plist normally) export SPARKLE_APPCAST_URL=https://example.com/appcast.xml ``` ## Testing ```bash # Run all tests swift test # Run specific test swift test --filter TunnelManagerTests # Run with verbose output swift test --verbose ``` ### Example Test ```swift // From Tests/HermesAgentTests/TunnelManagerTests.swift import XCTest @testable import HermesAgent final class TunnelManagerTests: XCTestCase { func testTunnelCreation() { let manager = TunnelManager() XCTAssertFalse(manager.isTunnelRunning()) manager.startTunnel( username: "test", host: "localhost", localPort: 8787, remotePort: 8787 ) // Wait briefly for process to start sleep(1) XCTAssertTrue(manager.isTunnelRunning()) manager.stopTunnel() XCTAssertFalse(manager.isTunnelRunning()) } } ``` ## Dependencies
Auf GitHub ansehen
Diese SKILL.md ist sehr gross, daher zeigt SkillsMP hier nur den ersten Abschnitt. Auf GitHub ansehen