Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,9 @@ config.toml

# Local migration notes, not part of the project.
LAPTOP-MIGRATION.local.md

# SwiftPM build artifacts (the Swift rewrite).
swift/.build/

# Generated app bundle (rebuilt by swift/Packaging/build-app.sh).
swift/Packaging/Hark.app/
591 changes: 591 additions & 0 deletions docs/superpowers/specs/2026-07-31-native-client-design.md

Large diffs are not rendered by default.

18 changes: 18 additions & 0 deletions swift/Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// swift-tools-version: 5.9
import PackageDescription

let package = Package(
name: "Hark",
platforms: [.macOS(.v13)],
products: [
.executable(name: "hark", targets: ["hark"])
],
targets: [
// Pure / server-logic core: config, key, sanitize, wav, server, whisper.
.target(name: "HarkCore"),
// macOS agent: hotkey, recorder, dictate client, controller. Kept out
// of HarkCore so tests run headless in CI (no TCC, no hardware).
.executableTarget(name: "hark", dependencies: ["HarkCore"]),
.testTarget(name: "HarkCoreTests", dependencies: ["HarkCore"]),
]
)
31 changes: 31 additions & 0 deletions swift/Packaging/Info.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?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">
<dict>
<key>CFBundleIdentifier</key>
<string>com.drycodeworks.hark-agent</string>
<key>CFBundleName</key>
<string>Hark</string>
<key>CFBundleExecutable</key>
<string>hark</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>0.1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>LSMinimumSystemVersion</key>
<string>13.0</string>
<key>LSUIElement</key>
<true/>
<key>NSMicrophoneUsageDescription</key>
<string>Hark records your voice while you hold the dictate hotkey, to transcribe it.</string>
<key>NSLocalNetworkUsageDescription</key>
<string>Hark talks to the transcription server on your local network.</string>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsLocalNetworking</key>
<true/>
</dict>
</dict>
</plist>
34 changes: 34 additions & 0 deletions swift/Packaging/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Makefile for Hark — build, package, sign, verify, clean, test.
# Rules use tabs (this file is written with tab-indented recipe lines).

PKG_DIR := Packaging
APP := $(PKG_DIR)/Hark.app

.PHONY: build app sign verify clean test

## build: compile the release binary
build:
swift build -c release

## app: run the packaging script (build release + assemble + sign Hark.app)
app:
./$(PKG_DIR)/build-app.sh

## sign: re-sign the assembled bundle (ad-hoc unless HARK_SIGN_IDENTITY is set)
sign:
codesign --force --deep --sign "$${HARK_SIGN_IDENTITY:--}" \
--options runtime \
--entitlements $(PKG_DIR)/entitlements.plist \
$(APP)

## verify: codesign verification of the bundle
verify:
codesign --verify --deep --strict --verbose=2 $(APP)

## clean: remove build products and the assembled bundle
clean:
rm -rf .build $(APP)

## test: run the test suite
test:
swift test
48 changes: 48 additions & 0 deletions swift/Packaging/build-app.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
#!/bin/bash
# build-app.sh — build the release binary and assemble a signed Hark.app bundle.
#
# ./Packaging/build-app.sh
#
# Signing identity comes from $HARK_SIGN_IDENTITY if set (e.g. a Developer ID),
# otherwise ad-hoc ("-") is used. Default is ad-hoc — fine for local dev.
set -euo pipefail

PKG_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(cd "${PKG_DIR}/.." && pwd)"
APP="${PKG_DIR}/Hark.app"

SIGN_IDENTITY="${HARK_SIGN_IDENTITY:-}"

echo "==> Building release binary"
cd "${ROOT_DIR}"
swift build -c release

echo "==> Assembling ${APP}"
rm -rf "${APP}"
mkdir -p "${APP}/Contents/MacOS" "${APP}/Contents/Resources"

cp ".build/release/hark" "${APP}/Contents/MacOS/hark"
cp "${PKG_DIR}/Info.plist" "${APP}/Contents/Info.plist"
cp "${PKG_DIR}/entitlements.plist" "${APP}/Contents/Resources/"
chmod +x "${APP}/Contents/MacOS/hark"

echo "==> Signing"
if [[ -n "${SIGN_IDENTITY}" ]]; then
echo " using identity: ${SIGN_IDENTITY}"
codesign --force --deep --sign "${SIGN_IDENTITY}" \
--options runtime \
--entitlements "${PKG_DIR}/entitlements.plist" \
"${APP}"
else
echo " using ad-hoc signature (-)"
codesign --force --deep --sign - \
--options runtime \
--entitlements "${PKG_DIR}/entitlements.plist" \
"${APP}"
fi

echo "==> Verifying"
codesign --verify --deep --strict --verbose=2 "${APP}"
echo "==> Entitlements embedded in binary:"
codesign -d --entitlements :- "${APP}" 2>/dev/null || true
echo "==> done. Bundle at: ${APP}"
12 changes: 12 additions & 0 deletions swift/Packaging/entitlements.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?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">
<dict>
<key>com.apple.security.device.audio-input</key>
<true/>
<key>com.apple.security.automation.apple-events</key>
<false/>
<key>com.apple.security.cs.allow-jit</key>
<false/>
</dict>
</plist>
131 changes: 131 additions & 0 deletions swift/Sources/HarkCore/Config.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import Foundation

/// Server deployment configuration — a port of `src/hark/config.py`.
///
/// Defaults describe the single-machine setup: bind to loopback, expose
/// nothing. `~/.config/hark/config.toml` (outside the repo) overrides, exactly
/// as it does for the Python server. A missing file is the ordinary case; a
/// malformed one raises rather than silently falling back to defaults (that
/// could bind the service somewhere the user did not ask for).
public struct HarkConfig {
public var bindHost: String // server.bind, default 127.0.0.1
public var harkPort: Int // server.port, default 8911
public var whisperHost: String // always 127.0.0.1 (not configurable)
public var whisperPort: Int // whisper.port, default 8910
public var modelPath: String // whisper.model
public var vocabPrompt: String // whisper.prompt
public var silenceRMSThreshold: Double // audio.silence_rms_threshold, default 150.0
public var transcribeTimeout: Double // 60
public var connectTimeout: Double // 5

public init(bindHost: String = "127.0.0.1",
harkPort: Int = 8911,
whisperPort: Int = 8910,
modelPath: String = "~/.local/share/whisper-cpp/ggml-large-v3-turbo.bin",
vocabPrompt: String = "",
silenceRMSThreshold: Double = 150.0,
transcribeTimeout: Double = 60.0,
connectTimeout: Double = 5.0) {
self.bindHost = bindHost
self.harkPort = harkPort
self.whisperHost = "127.0.0.1"
self.whisperPort = whisperPort
self.modelPath = modelPath
self.vocabPrompt = vocabPrompt
self.silenceRMSThreshold = silenceRMSThreshold
self.transcribeTimeout = transcribeTimeout
self.connectTimeout = connectTimeout
}

public static var configFile: URL {
if let env = ProcessInfo.processInfo.environment["HARK_CONFIG"] {
return URL(fileURLWithPath: env)
}
return FileManager.default.homeDirectoryForCurrentUser
.appendingPathComponent(".config/hark/config.toml")
}

public static func load() -> HarkConfig {
var cfg = HarkConfig()
guard let text = try? String(contentsOf: configFile, encoding: .utf8) else {
return cfg
}
do {
let parsed = try MiniTOML.parse(text)
if let s = parsed.section("server") {
cfg.bindHost = s.string("bind") ?? cfg.bindHost
cfg.harkPort = s.int("port") ?? cfg.harkPort
}
if let w = parsed.section("whisper") {
cfg.whisperPort = w.int("port") ?? cfg.whisperPort
cfg.modelPath = w.string("model") ?? cfg.modelPath
cfg.vocabPrompt = w.string("prompt") ?? cfg.vocabPrompt
}
if let a = parsed.section("audio") {
cfg.silenceRMSThreshold = a.double("silence_rms_threshold") ?? cfg.silenceRMSThreshold
}
} catch {
fatalError("malformed hark config at \(configFile.path): \(error)")
}
return cfg
}

public var whisperURL: String { "http://\(whisperHost):\(whisperPort)" }
}

// MARK: - Minimal TOML subset parser

/// Parses just the shape `config.example.toml` uses: `[section]` headers,
/// `key = value` pairs (string / integer / float / boolean), `#` comments.
/// Unknown keys and sections are ignored so a config written for the Python
/// server keeps working. Values that do not parse raise, never silently drop.
struct MiniTOML {
struct Section {
let name: String
var values: [String: String] = [:]
func string(_ key: String) -> String? { values[key] }
func int(_ key: String) -> Int? {
guard let v = values[key], let i = Int(v) else { return nil }
return i
}
func double(_ key: String) -> Double? {
guard let v = values[key] else { return nil }
if let i = Int(v) { return Double(i) }
return Double(v)
}
}
var sections: [String: Section] = [:]

func section(_ name: String) -> Section? { sections[name] }

static func parse(_ text: String) throws -> MiniTOML {
var result = MiniTOML()
var current: String = ""
for rawLine in text.split(separator: "\n", omittingEmptySubsequences: false) {
var line = String(rawLine)
if let hash = line.firstIndex(of: "#") { line = String(line[..<hash]) }
line = line.trimmingCharacters(in: .whitespaces)
if line.isEmpty { continue }
if line.hasPrefix("[") && line.hasSuffix("]") {
current = String(line.dropFirst().dropLast()).trimmingCharacters(in: .whitespaces)
result.sections[current] = Section(name: current)
continue
}
guard let eq = line.firstIndex(of: "=") else {
throw TOMLError.malformed(line)
}
let key = String(line[..<eq]).trimmingCharacters(in: .whitespaces)
var value = String(line[line.index(after: eq)...]).trimmingCharacters(in: .whitespaces)
if value.hasPrefix("\""), value.hasSuffix("\""), value.count >= 2 {
value = String(value.dropFirst().dropLast())
}
result.sections[current, default: Section(name: current)].values[key] = value
}
return result
}
}

enum TOMLError: Error, CustomStringConvertible {
case malformed(String)
var description: String { "malformed TOML line: \(self)" }
}
Loading
Loading