用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
直接命令不会经过审查 Prompt;运行前请先检查来源。
npx skills add https://github.com/Aradotso/hermes-skills --skill hermes-swift-mac-app命令会保持在同一行。复制前请横向滚动并检查完整内容。
想先保存到本地?可下载 SkillsMP 当前能够提供的文件。
AI second brain system with MCP tools for goals, schedules, notes, decisions, and long-term memory management
Multi-tenant SaaS platform built on OpenClaw with auth, billing, workspace isolation, and AI agent execution gateway
Add Opik LLM observability and tracing to OpenClaw AI agents for monitoring prompts, responses, tool calls, and agent behavior.
基于 SOC 职业分类
正在显示 SKILL.md
| 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"] |
Skill by ara.so — Hermes Skills collection
Hermes Agent is a native macOS desktop wrapper for Hermes Web UI. 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:
Download the latest DMG from releases:
# Visit https://github.com/hermes-webui/hermes-swift-mac/releases
# Download Hermes-Agent-vX.X.X.dmg
# Open DMG and drag to Applications folder
# 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:
swift build -c release.app bundle structureicon.png to AppIcon.icns/Applications/Hermes Agent.appSources/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
For Hermes Web UI running on the same Mac:
Default settings:
http://localhost:8787// In PreferencesWindowController.swift
// Default URL stored in UserDefaults
UserDefaults.standard.string(forKey: "targetURL") ?? "http://localhost:8787"
For Hermes Web UI on a remote server:
Required settings:
SSH requirements:
ssh user@host without password)~/.ssh/known_hosts file accessible// TunnelManager.swift constructs SSH command:
ssh -o StrictHostKeyChecking=accept-new \
-o ExitOnForwardFailure=yes \
-N -L \(localPort):localhost:\(remotePort) \
\(username)@\(host)
// 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")
// 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
}
}
// 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()
}
// 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)
}
}
// 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)
}
// 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()
}
}
# Build in debug mode
swift build
# Run directly
swift run
# Run tests
swift test
# Build release binary
swift build -c release
# Or use build script (compiles + installs)
./build.sh
The build script handles:
HermesAgent.app//Applications/# 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
# 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:
main branchThis ensures the GitHub Actions workflow fires reliably.
# 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
The .github/workflows/build-release.yml (not shown in README but typical) would:
macos-latestcreate-dmg or hdiutil// 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
}
// 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)
}
}
// 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)
}
}
}
Symptom: Blank page or "Unable to connect" error
Solutions:
cd ~/hermes-webui-public
bash start.sh
Symptom: Status shows "Disconnected" right after connecting
Solutions:
ssh user@your-server
# Should connect without password
ssh-keygen -t ed25519
ssh-copy-id user@your-server
~/.ssh/known_hosts isn't corruptedSymptom: Microphone button doesn't respond
Solutions:
NSMicrophoneUsageDescriptionSymptom: "App can't be opened because it is from an unidentified developer"
Solutions:
xattr -cr /Applications/Hermes\ Agent.app
Symptom: Icon looks pixelated in Dock after building
Solution:
# Refresh icon cache
killall Dock
Symptom: "Port forwarding failed" when starting tunnel
Solutions:
# Find what's using the port
lsof -i :8787
# Kill the process or change Local Port in Preferences
The app doesn't use environment variables directly, but for development:
# 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
# Run all tests
swift test
# Run specific test
swift test --filter TunnelManagerTests
# Run with verbose output
swift test --verbose
// 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())
}
}
Managed via Swift Package Manager in Package.swift:
// Package.swift
dependencies: [
.package(url: "https://github.com/sparkle-project/Sparkle", from: "2.3.0")
],
targets: [
.executableTarget(
name: "HermesAgent",
dependencies: [
.product(name: "Sparkle", package: "Sparkle")
]
)
]
Runtime dependencies:
No external dependencies for users — fully self-contained app.