diff --git a/.gitignore b/.gitignore
index 6fe90bf..0bd092e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,3 +6,6 @@ swift/speech-helper
swift/llm-helper
swift/test_foundation
swift/test_foundation.swift
+dist/
+.DS_Store
+
diff --git a/MACOS_APP_PACKAGING.md b/MACOS_APP_PACKAGING.md
new file mode 100644
index 0000000..5f8f2aa
--- /dev/null
+++ b/MACOS_APP_PACKAGING.md
@@ -0,0 +1,184 @@
+# Packaging Python Applications into Native macOS Apps (`.app`) with Custom Privacy Entitlements
+
+This guide details how to transform a CLI Python application into a native macOS `.app` bundle that requests macOS Transparency, Consent, and Control (TCC) privacy permissions (Microphone, Speech Recognition, Camera, Input Monitoring) under its **own custom app identity** instead of `Terminal.app` or `python3.11`.
+
+---
+
+## 1. Problem Overview
+
+When launching a Python script via Terminal or a shell launcher (`#!/bin/bash`), macOS TCC attributes privacy permissions to either:
+1. `Terminal.app` (if executed inside a terminal window), or
+2. `python3.11` / `python3.1` (if `exec /path/to/python` is called from a script).
+
+To ensure macOS prompts display your custom application name (e.g., *"VoiceAgent would like to access the microphone"*) and show up under **System Settings → Privacy & Security → Microphone**, you must follow Apple's native bundle layout and executable architecture.
+
+---
+
+## 2. Key Architectural Requirements
+
+```
+VoiceAgent.app/
+└── Contents/
+ ├── Info.plist # App Identity & Custom Privacy Descriptions
+ ├── MacOS/
+ │ └── VoiceAgent # Compiled Mach-O Swift Launcher Binary
+ └── Resources/
+ ├── src/ # Packaged Python Source Code
+ └── swift/ # Native Helper Binaries (speech-helper, llm-helper)
+```
+
+1. **Standard macOS `.app` Directory Layout**:
+ The bundle must strictly use the `Contents/` directory layout.
+
+2. **Compiled Native Mach-O Executable (Swift/C)**:
+ - macOS TCC identifies process ownership through the executable binary image (`CFBundleExecutable`).
+ - Shell scripts (`/bin/bash`) cause macOS to inspect the underlying `python` image and attribute permissions to `python3.1`.
+ - **Solution**: Compile a native Swift executable (`swift/VoiceAgentLauncher.swift`) into a Mach-O 64-bit arm64 binary (`Contents/MacOS/VoiceAgent`). When Finder launches this binary, macOS registers the process under `com.voiceagent.mac`. Subprocesses spawned by this binary inherit the bundle's TCC identity.
+
+3. **Privacy Entitlements in `Info.plist` & Code Signing**:
+ - `Info.plist` defines `CFBundleExecutable`, `CFBundleIdentifier`, and the `NS*UsageDescription` keys.
+ - The app bundle must be deeply code-signed (`codesign --deep`).
+
+---
+
+## 3. Implementation Workflow
+
+### Step 1: Native Swift Launcher (`swift/VoiceAgentLauncher.swift`)
+
+The Swift launcher sets environment variables, redirects logs, and spawns the Python process:
+
+```swift
+import Foundation
+
+@main
+struct VoiceAgentLauncher {
+ static func main() {
+ let fileManager = FileManager.default
+ let projDir = "/Users/adolforeyna/Projects/VoiceAgent1"
+
+ let bundleResPath = Bundle.main.resourcePath ?? ""
+ let bundledSrcPath = "\(bundleResPath)/src"
+
+ let workDir = fileManager.fileExists(atPath: projDir) ? projDir : bundledSrcPath
+ let pythonBin = "\(projDir)/.venv/bin/python"
+ let fallbackPython = "/usr/bin/python3"
+ let targetPython = fileManager.fileExists(atPath: pythonBin) ? pythonBin : fallbackPython
+ let targetScript = "\(workDir)/app_main.py"
+
+ setenv("SSL_CERT_FILE", "/etc/ssl/cert.pem", 1)
+ setenv("REQUESTS_CA_BUNDLE", "/etc/ssl/cert.pem", 1)
+
+ let logDir = fileManager.homeDirectoryForCurrentUser.appendingPathComponent("Library/Logs/VoiceAgent")
+ try? fileManager.createDirectory(at: logDir, withIntermediateDirectories: true)
+ let logFile = logDir.appendingPathComponent("voiceagent.log")
+
+ if !fileManager.fileExists(atPath: logFile.path) {
+ fileManager.createFile(atPath: logFile.path, contents: nil)
+ }
+
+ let process = Process()
+ process.executableURL = URL(fileURLWithPath: targetPython)
+ process.arguments = [targetScript]
+ process.currentDirectoryURL = URL(fileURLWithPath: workDir)
+
+ if let logHandle = try? FileHandle(forWritingTo: logFile) {
+ logHandle.seekToEndOfFile()
+ process.standardOutput = logHandle
+ process.standardError = logHandle
+ }
+
+ do {
+ try process.run()
+ process.waitUntilExit()
+ } catch {
+ print("Failed to run VoiceAgent: \(error)")
+ }
+ }
+}
+```
+
+---
+
+### Step 2: Build & Package Script (`build_app.sh`)
+
+Automate Swift binary compilation, bundle structure creation, `Info.plist` injection, installation to `/Applications`, and code signing:
+
+```bash
+#!/bin/bash
+set -euo pipefail
+
+HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+cd "$HERE"
+
+# 1. Build Swift Helpers
+bash swift/build.sh
+
+APP_NAME="VoiceAgent"
+DIST_DIR="$HERE/dist"
+APP_BUNDLE="$DIST_DIR/$APP_NAME.app"
+CONTENTS_DIR="$APP_BUNDLE/Contents"
+MACOS_DIR="$CONTENTS_DIR/MacOS"
+RESOURCES_DIR="$CONTENTS_DIR/Resources"
+SRC_DIR="$RESOURCES_DIR/src"
+
+# 2. Setup Bundle Directory Structure
+rm -rf "$APP_BUNDLE"
+mkdir -p "$MACOS_DIR" "$RESOURCES_DIR/swift" "$SRC_DIR"
+
+# 3. Compile Native Mach-O Launcher
+swiftc -O -parse-as-library "$HERE/swift/VoiceAgentLauncher.swift" -o "$MACOS_DIR/VoiceAgent"
+chmod +x "$MACOS_DIR/VoiceAgent"
+
+# 4. Copy Swift Helper Binaries & Python Source Files
+cp "$HERE/swift/speech-helper" "$RESOURCES_DIR/swift/"
+cp "$HERE/swift/llm-helper" "$RESOURCES_DIR/swift/"
+cp "$HERE"/*.py "$SRC_DIR/" 2>/dev/null || true
+
+# 5. Generate Info.plist
+cat << 'EOF' > "$CONTENTS_DIR/Info.plist"
+
+
+
+
+ CFBundleExecutable
+ VoiceAgent
+ CFBundleIdentifier
+ com.voiceagent.mac
+ CFBundleName
+ VoiceAgent
+ CFBundleDisplayName
+ VoiceAgent
+ CFBundlePackageType
+ APPL
+ CFBundleShortVersionString
+ 1.0.0
+ CFBundleVersion
+ 1.0.0
+ LSMinimumSystemVersion
+ 14.0
+ NSMicrophoneUsageDescription
+ VoiceAgent requires access to your microphone to listen to your voice commands.
+ NSSpeechRecognitionUsageDescription
+ VoiceAgent uses on-device speech recognition to process your spoken input.
+ NSHighResolutionCapable
+
+
+
+EOF
+
+# 6. Sign & Install to /Applications
+codesign -s - --deep --force "$APP_BUNDLE"
+rm -rf /Applications/VoiceAgent.app
+cp -R "$APP_BUNDLE" /Applications/
+codesign -s - --deep --force /Applications/VoiceAgent.app
+```
+
+---
+
+## 4. Verification Matrix
+
+| Launch Mechanism | Process Ownership | Permission Prompt Identity | System Settings Category |
+| --- | --- | --- | --- |
+| Terminal Script (`python bot.py`) | `Terminal.app` | *"Terminal would like to access..."* | Listed under Terminal |
+| Shell Script (`#!/bin/bash exec`) | `python3.11` | *"python3.1 would like to access..."* | Listed under `python3.1` |
+| **Native Swift Launcher (`VoiceAgent`)** | **`com.voiceagent.mac`** | ***"VoiceAgent would like to access..."*** | **Listed under `VoiceAgent`** |
diff --git a/app_main.py b/app_main.py
new file mode 100644
index 0000000..832423f
--- /dev/null
+++ b/app_main.py
@@ -0,0 +1,18 @@
+#!/usr/bin/env python3
+"""Main entrypoint for VoiceAgent macOS App bundle."""
+
+import asyncio
+import os
+import sys
+
+# Ensure CA certificates are configured for macOS SSL
+os.environ["SSL_CERT_FILE"] = "/etc/ssl/cert.pem"
+os.environ["REQUESTS_CA_BUNDLE"] = "/etc/ssl/cert.pem"
+
+import bot
+
+if __name__ == "__main__":
+ try:
+ sys.exit(asyncio.run(bot.main()))
+ except KeyboardInterrupt:
+ sys.exit(0)
diff --git a/apple_llm.py b/apple_llm.py
index a790d96..a9b3fb1 100644
--- a/apple_llm.py
+++ b/apple_llm.py
@@ -13,6 +13,7 @@ import json
import os
import shutil
import subprocess
+import sys
from pathlib import Path
from loguru import logger
@@ -41,8 +42,25 @@ _NOISE_TRANSCRIPTS = {
"[silence]",
}
-HERE = Path(__file__).parent
-LLM_HELPER_PATH = HERE / "swift" / "llm-helper"
+def get_helper_path(binary_name: str) -> Path:
+ if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"):
+ p1 = Path(sys._MEIPASS) / binary_name
+ if p1.exists():
+ return p1
+ p2 = Path(sys._MEIPASS) / "swift" / binary_name
+ if p2.exists():
+ return p2
+ if getattr(sys, "frozen", False):
+ res_dir = Path(sys.executable).parent.parent / "Resources"
+ p1 = res_dir / binary_name
+ if p1.exists():
+ return p1
+ p2 = res_dir / "swift" / binary_name
+ if p2.exists():
+ return p2
+ return Path(__file__).parent / "swift" / binary_name
+
+LLM_HELPER_PATH = get_helper_path("llm-helper")
def probe_apple_llm() -> tuple[bool, str]:
diff --git a/build_app.sh b/build_app.sh
new file mode 100644
index 0000000..7543069
--- /dev/null
+++ b/build_app.sh
@@ -0,0 +1,87 @@
+#!/bin/bash
+# build_app.sh - Build standalone macOS VoiceAgent.app bundle
+set -euo pipefail
+
+HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+cd "$HERE"
+
+echo "=== 1. Building Swift Helper & Launcher Binaries ==="
+bash swift/build.sh
+
+APP_NAME="VoiceAgent"
+DIST_DIR="$HERE/dist"
+APP_BUNDLE="$DIST_DIR/$APP_NAME.app"
+CONTENTS_DIR="$APP_BUNDLE/Contents"
+MACOS_DIR="$CONTENTS_DIR/MacOS"
+RESOURCES_DIR="$CONTENTS_DIR/Resources"
+SRC_DIR="$RESOURCES_DIR/src"
+
+echo "=== 2. Creating macOS App Bundle Structure ==="
+rm -rf "$APP_BUNDLE"
+mkdir -p "$MACOS_DIR"
+mkdir -p "$RESOURCES_DIR/swift"
+mkdir -p "$SRC_DIR"
+
+echo "=== 3. Compiling Native App Bundle Launcher ==="
+swiftc -O -parse-as-library "$HERE/swift/VoiceAgentLauncher.swift" -o "$MACOS_DIR/VoiceAgent"
+chmod +x "$MACOS_DIR/VoiceAgent"
+
+echo "=== 4. Copying Source Files, Swift Helpers, and Assets ==="
+cp "$HERE/swift/speech-helper" "$RESOURCES_DIR/swift/"
+cp "$HERE/swift/llm-helper" "$RESOURCES_DIR/swift/"
+chmod +x "$RESOURCES_DIR/swift/speech-helper" "$RESOURCES_DIR/swift/llm-helper"
+
+# Copy python files to bundle resources
+cp "$HERE"/*.py "$SRC_DIR/" 2>/dev/null || true
+if [ -f "$HERE/vocabulary.example.txt" ]; then
+ cp "$HERE/vocabulary.example.txt" "$RESOURCES_DIR/"
+fi
+if [ -f "$HERE/corrections.example.txt" ]; then
+ cp "$HERE/corrections.example.txt" "$RESOURCES_DIR/"
+fi
+
+echo "=== 5. Generating Info.plist with Entitlements ==="
+cat << 'EOF' > "$CONTENTS_DIR/Info.plist"
+
+
+
+
+ CFBundleExecutable
+ VoiceAgent
+ CFBundleIdentifier
+ com.voiceagent.mac
+ CFBundleName
+ VoiceAgent
+ CFBundleDisplayName
+ VoiceAgent
+ CFBundlePackageType
+ APPL
+ CFBundleShortVersionString
+ 1.0.0
+ CFBundleVersion
+ 1.0.0
+ LSMinimumSystemVersion
+ 14.0
+ NSMicrophoneUsageDescription
+ VoiceAgent requires access to your microphone to listen to your voice commands.
+ NSSpeechRecognitionUsageDescription
+ VoiceAgent uses on-device speech recognition to process your spoken input.
+ NSHighResolutionCapable
+
+
+
+EOF
+
+echo "=== 6. Code-signing App Bundle ==="
+codesign -s - --deep --force "$APP_BUNDLE"
+
+echo "=== 7. Installing to /Applications ==="
+rm -rf /Applications/VoiceAgent.app
+cp -R "$APP_BUNDLE" /Applications/
+codesign -s - --deep --force /Applications/VoiceAgent.app
+
+echo "=========================================================="
+echo "Successfully built and installed VoiceAgent.app to:"
+echo "1. /Applications/VoiceAgent.app"
+echo "2. $APP_BUNDLE"
+echo "=========================================================="
diff --git a/setup_app.py b/setup_app.py
new file mode 100644
index 0000000..a7bc6ea
--- /dev/null
+++ b/setup_app.py
@@ -0,0 +1,52 @@
+"""py2app setup configuration for building VoiceAgent.app."""
+
+from setuptools import setup
+
+APP = ["app_main.py"]
+DATA_FILES = [
+ ("swift", ["swift/speech-helper", "swift/llm-helper"]),
+]
+
+OPTIONS = {
+ "argv_emulation": False,
+ "iconfile": None,
+ "plist": {
+ "CFBundleName": "VoiceAgent",
+ "CFBundleDisplayName": "VoiceAgent",
+ "CFBundleIdentifier": "com.voiceagent.mac",
+ "CFBundleVersion": "1.0.0",
+ "CFBundleShortVersionString": "1.0.0",
+ "NSMicrophoneUsageDescription": "VoiceAgent requires access to your microphone for voice interaction.",
+ "NSSpeechRecognitionUsageDescription": "VoiceAgent uses on-device speech recognition to process your spoken input.",
+ "NSHumanReadableCopyright": "Copyright © 2026 Adolfo Reyna. All rights reserved.",
+ "LSMinimumSystemVersion": "14.0",
+ "NSHighResolutionCapable": True,
+ },
+ "includes": [
+ "bot",
+ "brain",
+ "voice_manager",
+ "claude_llm",
+ "apple_llm",
+ "apple_stt",
+ "apple_tts",
+ "speech_analyzer_stt",
+ "echo_guard",
+ "global_hotkey",
+ "push_to_talk",
+ "journal",
+ "memory_tools",
+ "transcript_repair",
+ "vocabulary",
+ "spoken_text",
+ "sounddevice_transport",
+ ],
+}
+
+setup(
+ app=APP,
+ name="VoiceAgent",
+ data_files=DATA_FILES,
+ options={"py2app": OPTIONS},
+ setup_requires=["py2app"],
+)
diff --git a/speech_analyzer_stt.py b/speech_analyzer_stt.py
index cc7b5f8..901576e 100644
--- a/speech_analyzer_stt.py
+++ b/speech_analyzer_stt.py
@@ -33,6 +33,7 @@ import asyncio
import json
import os
import subprocess
+import sys
import tempfile
from collections.abc import AsyncGenerator
from pathlib import Path
@@ -45,7 +46,25 @@ from pipecat.services.stt_service import SegmentedSTTService
from pipecat.transcriptions.language import Language
from pipecat.utils.time import time_now_iso8601
-HELPER = Path(__file__).parent / "swift" / "speech-helper"
+def get_helper_path(binary_name: str) -> Path:
+ if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"):
+ p1 = Path(sys._MEIPASS) / binary_name
+ if p1.exists():
+ return p1
+ p2 = Path(sys._MEIPASS) / "swift" / binary_name
+ if p2.exists():
+ return p2
+ if getattr(sys, "frozen", False):
+ res_dir = Path(sys.executable).parent.parent / "Resources"
+ p1 = res_dir / binary_name
+ if p1.exists():
+ return p1
+ p2 = res_dir / "swift" / binary_name
+ if p2.exists():
+ return p2
+ return Path(__file__).parent / "swift" / binary_name
+
+HELPER = get_helper_path("speech-helper")
# Generous: the helper may be downloading the on-device model on first use.
_FIRST_RUN_TIMEOUT = 300.0
diff --git a/swift/VoiceAgentLauncher.swift b/swift/VoiceAgentLauncher.swift
new file mode 100644
index 0000000..91e39ce
--- /dev/null
+++ b/swift/VoiceAgentLauncher.swift
@@ -0,0 +1,47 @@
+import Foundation
+
+@main
+struct VoiceAgentLauncher {
+ static func main() {
+ let fileManager = FileManager.default
+ let projDir = "/Users/adolforeyna/Projects/VoiceAgent1"
+
+ let bundleResPath = Bundle.main.resourcePath ?? ""
+ let bundledSrcPath = "\(bundleResPath)/src"
+
+ let workDir = fileManager.fileExists(atPath: projDir) ? projDir : bundledSrcPath
+ let pythonBin = "\(projDir)/.venv/bin/python"
+ let fallbackPython = "/usr/bin/python3"
+ let targetPython = fileManager.fileExists(atPath: pythonBin) ? pythonBin : fallbackPython
+ let targetScript = "\(workDir)/app_main.py"
+
+ setenv("SSL_CERT_FILE", "/etc/ssl/cert.pem", 1)
+ setenv("REQUESTS_CA_BUNDLE", "/etc/ssl/cert.pem", 1)
+
+ let logDir = fileManager.homeDirectoryForCurrentUser.appendingPathComponent("Library/Logs/VoiceAgent")
+ try? fileManager.createDirectory(at: logDir, withIntermediateDirectories: true)
+ let logFile = logDir.appendingPathComponent("voiceagent.log")
+
+ if !fileManager.fileExists(atPath: logFile.path) {
+ fileManager.createFile(atPath: logFile.path, contents: nil)
+ }
+
+ let process = Process()
+ process.executableURL = URL(fileURLWithPath: targetPython)
+ process.arguments = [targetScript]
+ process.currentDirectoryURL = URL(fileURLWithPath: workDir)
+
+ if let logHandle = try? FileHandle(forWritingTo: logFile) {
+ logHandle.seekToEndOfFile()
+ process.standardOutput = logHandle
+ process.standardError = logHandle
+ }
+
+ do {
+ try process.run()
+ process.waitUntilExit()
+ } catch {
+ print("Failed to run VoiceAgent: \(error)")
+ }
+ }
+}