diff --git a/imessage-macos/.swiftlint.yml b/imessage-macos/.swiftlint.yml
new file mode 100755
index 0000000..7083245
--- /dev/null
+++ b/imessage-macos/.swiftlint.yml
@@ -0,0 +1,106 @@
+# iMessage macOS Agent - SwiftLint Configuration
+# Extends shared tooling from @packages/@swift
+
+# Note: parent_config with ~ paths doesn't work in CI Docker containers
+# This file contains the full configuration for CI compatibility
+
+# Opt-in rules for code quality
+opt_in_rules:
+ - force_cast
+ - force_try
+ - force_unwrapping
+ - sorted_imports
+ - closure_spacing
+ - empty_count
+ - first_where
+ - flatmap_over_map_reduce
+ - last_where
+ - multiline_parameters
+ - operator_usage_whitespace
+ - redundant_nil_coalescing
+ - sorted_first_last
+ - toggle_bool
+ - vertical_parameter_alignment_on_call
+ - explicit_init
+ - fallthrough
+ - fatal_error_message
+ - function_default_parameter_at_end
+ - implicit_return
+ - joined_default_parameter
+ - prefer_self_type_over_type_of_self
+ - redundant_type_annotation
+ - unowned_variable_capture
+ - accessibility_label_for_image
+ - private_outlet
+ - discouraged_optional_collection
+ - discouraged_optional_boolean
+
+# Disabled rules
+disabled_rules:
+ - todo # Allow TODOs during development
+ - line_length # 120 char limit can be too restrictive
+ - file_length # Some service files are large
+ - type_name # Sometimes short names are appropriate
+
+# Rule configurations
+function_parameter_count:
+ warning: 6
+ error: 8
+
+cyclomatic_complexity:
+ warning: 20
+ error: 30
+
+type_body_length:
+ warning: 400
+ error: 600
+
+function_body_length:
+ warning: 110
+ error: 175
+
+identifier_name:
+ min_length:
+ warning: 2
+ max_length:
+ warning: 50
+ excluded:
+ - id
+ - x
+ - y
+ - z
+ - db
+ - ip
+ - ok
+ - i
+ - j
+ - k
+ - n
+
+nesting:
+ type_level:
+ warning: 2
+ error: 3
+ function_level:
+ warning: 3
+ error: 4
+
+# Custom rules for Lilith Platform conventions
+custom_rules:
+ no_print_statements:
+ name: "No Print Statements"
+ regex: '^\s*print\s*\('
+ match_kinds:
+ - identifier
+ message: "Use NSLog() or os.Logger instead of print() for production code"
+ severity: warning
+
+# Excluded paths
+excluded:
+ - .build
+ - DerivedData
+ - "*.generated.swift"
+ - Package.swift
+
+# Reporter type
+reporter: "xcode"
diff --git a/imessage-macos/INSTALL.md b/imessage-macos/INSTALL.md
new file mode 100755
index 0000000..8625cab
--- /dev/null
+++ b/imessage-macos/INSTALL.md
@@ -0,0 +1,224 @@
+# iMessage macOS - Agent Installation
+
+This directory contains the macOS menu bar agent that syncs iMessages to the iMessage Sync server.
+
+## Prerequisites
+
+- macOS 13.0 (Ventura) or later
+- Swift 5.9+ (included with Xcode or Command Line Tools)
+- Full Disk Access permission (for reading iMessage database)
+
+## Installation
+
+### Automatic Installation (Recommended)
+
+The installer script handles the complete installation process:
+
+```bash
+./install.sh [server_url]
+```
+
+**Examples:**
+
+```bash
+# Interactive mode (prompts for server URL)
+./install.sh
+
+# With server URL
+./install.sh http://localhost:3100
+
+# Production server
+./install.sh https://assistant.lilith.is
+```
+
+### What the Installer Does
+
+1. **Builds the application** - Compiles Swift code with release optimizations
+2. **Installs the binary** - Creates app bundle in `~/Applications/iMessageMacos.app`
+3. **Configures auto-start** - Sets up LaunchAgent to run on login
+4. **Stores configuration** - Saves server URL in macOS preferences
+5. **Guides permissions** - Shows how to grant Full Disk Access
+
+### Post-Installation Steps
+
+1. **Grant Full Disk Access** (required for iMessage database access):
+ - Open System Settings → Privacy & Security → Full Disk Access
+ - Click the '+' button
+ - Navigate to `~/Applications/iMessageMacos.app`
+ - Enable the toggle
+
+2. **Complete device registration**:
+ - Click the menu bar icon (💬) in the top-right
+ - Click "Settings"
+ - Follow the registration flow
+ - Enter the verification code shown on the server
+
+3. **Verify sync is working**:
+ - Check the menu bar icon for sync status
+ - View logs: `tail -f ~/Library/Application\ Support/iMessageMacos/stderr.log`
+
+## Manual Installation
+
+If you prefer to install manually:
+
+```bash
+# 1. Build
+swift build -c release
+
+# 2. Create app bundle
+mkdir -p ~/Applications/iMessageMacos.app/Contents/MacOS
+cp .build/release/iMessageMacos ~/Applications/iMessageMacos.app/Contents/MacOS/
+
+# 3. Set server URL
+defaults write com.lilith.imessage-macos apiBaseURL "http://localhost:3100"
+
+# 4. Create LaunchAgent (see install.sh for plist template)
+
+# 5. Load LaunchAgent
+launchctl load ~/Library/LaunchAgents/com.lilith.imessage-macos.plist
+```
+
+## Configuration
+
+The agent stores configuration in macOS UserDefaults:
+
+```bash
+# View current configuration
+defaults read com.lilith.imessage-macos
+
+# Change server URL
+defaults write com.lilith.imessage-macos apiBaseURL "https://new-server.example.com"
+
+# Restart agent to apply changes
+launchctl unload ~/Library/LaunchAgents/com.lilith.imessage-macos.plist
+launchctl load ~/Library/LaunchAgents/com.lilith.imessage-macos.plist
+```
+
+Authentication tokens are stored securely in the macOS Keychain.
+
+## Troubleshooting
+
+### Agent not starting
+
+```bash
+# Check LaunchAgent status
+launchctl list | grep imessage-macos
+
+# View error logs
+tail -f ~/Library/Application\ Support/iMessageMacos/stderr.log
+
+# Manually start to see errors
+~/Applications/iMessageMacos.app/Contents/MacOS/iMessageMacos
+```
+
+### Database access errors
+
+The agent needs Full Disk Access to read the iMessage database at:
+`~/Library/Messages/chat.db`
+
+If you see errors like "unable to open database file", grant Full Disk Access:
+
+1. System Settings → Privacy & Security → Full Disk Access
+2. Add iMessageMacos.app
+3. Restart the agent
+
+### Build failures
+
+```bash
+# Clean and rebuild
+rm -rf .build
+swift build -c release
+
+# Check Swift version (requires 5.9+)
+swift --version
+
+# Update dependencies
+swift package update
+```
+
+### Network errors
+
+```bash
+# Test server connectivity
+curl http://localhost:3100/health
+
+# Check server URL configuration
+defaults read com.lilith.imessage-macos apiBaseURL
+```
+
+## Uninstallation
+
+The uninstaller removes all components:
+
+```bash
+./uninstall.sh
+```
+
+This removes:
+- Application binary
+- LaunchAgent configuration
+- Application data and logs
+- Preferences
+- Keychain entries
+
+You may need to manually remove Full Disk Access permission in System Settings.
+
+## File Locations
+
+| Component | Path |
+|-----------|------|
+| **Application** | `~/Applications/iMessageMacos.app` |
+| **LaunchAgent** | `~/Library/LaunchAgents/com.lilith.imessage-macos.plist` |
+| **Logs** | `~/Library/Application Support/iMessageMacos/*.log` |
+| **Preferences** | `~/Library/Preferences/com.lilith.imessage-macos.plist` |
+| **Keychain** | Keychain Access → "authToken" |
+
+## Development
+
+For development and testing:
+
+```bash
+# Run without installing
+swift run
+
+# Build debug version
+swift build
+
+# Run tests (when available)
+swift test
+
+# Clean build artifacts
+swift package clean
+```
+
+## Architecture
+
+The agent consists of:
+
+- **Menu Bar UI** - SwiftUI-based menu bar icon and settings
+- **iMessage Reader** - GRDB-based SQLite reader for Messages database
+- **Sync Manager** - Periodic sync orchestrator (every 30 seconds)
+- **API Client** - HTTP client for server communication (Alamofire)
+
+See the source code in `Sources/` for implementation details.
+
+## Security
+
+- Auth tokens stored in macOS Keychain (secure storage)
+- iMessage database accessed read-only
+- No message content stored locally by the agent
+- All sync traffic uses HTTPS in production
+
+## Support
+
+For issues or questions:
+- Check logs in `~/Library/Application Support/iMessageMacos/`
+- Review server logs for API errors
+- Verify Full Disk Access is granted
+- Ensure server URL is correct
+
+---
+
+**Last Updated**: 2026-03-03
+**Platforms**: macOS 13.0+
+**License**: Proprietary - Lilith Platform
diff --git a/imessage-macos/Package.swift b/imessage-macos/Package.swift
new file mode 100755
index 0000000..5a24960
--- /dev/null
+++ b/imessage-macos/Package.swift
@@ -0,0 +1,30 @@
+// swift-tools-version: 5.9
+import PackageDescription
+
+let package = Package(
+ name: "iMessageMacos",
+ platforms: [
+ .macOS(.v13)
+ ],
+ dependencies: [
+ .package(url: "https://github.com/groue/GRDB.swift.git", from: "6.24.0"),
+ .package(url: "https://github.com/Alamofire/Alamofire.git", from: "5.8.0"),
+ .package(url: "https://github.com/SwiftyJSON/SwiftyJSON.git", from: "5.0.0"),
+ .package(url: "https://github.com/SwiftGen/SwiftGenPlugin", from: "6.6.0"),
+ .package(url: "https://github.com/httpswift/swifter.git", from: "1.5.0"),
+ .package(path: "../../../@packages/@swift/@macos/menu-bar"),
+ ],
+ targets: [
+ .executableTarget(
+ name: "iMessageMacos",
+ dependencies: [
+ .product(name: "GRDB", package: "GRDB.swift"),
+ .product(name: "Alamofire", package: "Alamofire"),
+ .product(name: "SwiftyJSON", package: "SwiftyJSON"),
+ .product(name: "Swifter", package: "swifter"),
+ .product(name: "LilithMenuBar", package: "menu-bar"),
+ ],
+ path: "Sources"
+ ),
+ ]
+)
diff --git a/imessage-macos/deploy-remote.sh b/imessage-macos/deploy-remote.sh
new file mode 100755
index 0000000..110a9d9
--- /dev/null
+++ b/imessage-macos/deploy-remote.sh
@@ -0,0 +1,246 @@
+#!/bin/bash
+set -euo pipefail
+
+# Deploy iMessage macOS agent to plum (MacBook) via rsync + SSH.
+#
+# What this does:
+# 1. Rsyncs the entire imessage-macos/ source to plum staging dir
+# 2. SSHs into plum and runs install.sh (builds Swift + installs)
+# 3. Verifies the agent is running
+#
+# Usage:
+# ./deploy-remote.sh # deploy with default URL
+# ./deploy-remote.sh https://conversations.nasty.sh # deploy with custom URL
+# ./deploy-remote.sh uninstall # uninstall on plum
+
+# Colors
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+BLUE='\033[0;34m'
+NC='\033[0m'
+
+# Configuration
+PLUM_HOST="${PLUM_HOST:-plum}"
+PLUM_STAGING="imessage-macos-staging"
+# Parse args: skip flags, use positional arg as server URL
+NO_BUMP=false
+SERVER_URL="http://10.0.0.11:3100"
+for arg in "$@"; do
+ case "$arg" in
+ --no-bump) NO_BUMP=true ;;
+ uninstall|frontend) ;; # handled by case below
+ *) SERVER_URL="$arg" ;;
+ esac
+done
+SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
+
+print_step() { echo -e "${GREEN}▸${NC} $1"; }
+print_info() { echo -e "${BLUE}ℹ${NC} $1"; }
+print_error() { echo -e "${RED}✗${NC} $1"; }
+print_success() { echo -e "${GREEN}✓${NC} $1"; }
+
+echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
+echo -e "${BLUE} iMessage macOS - Remote Deployment${NC}"
+echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
+echo ""
+
+# ---------------------------------------------------------------------------
+# Connectivity check
+# ---------------------------------------------------------------------------
+
+check_plum() {
+ print_step "Checking SSH connection to $PLUM_HOST..."
+ if ! ssh -o ConnectTimeout=5 "$PLUM_HOST" 'echo ok' >/dev/null 2>&1; then
+ print_error "Cannot connect to $PLUM_HOST"
+ echo "Make sure:"
+ echo " - The MacBook is on and connected to the network"
+ echo " - SSH config exists for host '$PLUM_HOST'"
+ exit 1
+ fi
+ print_success "Connected to $PLUM_HOST"
+}
+
+# ---------------------------------------------------------------------------
+# Install
+# ---------------------------------------------------------------------------
+
+cmd_install() {
+ check_plum
+
+ # Rsync source to plum (excludes build artifacts)
+ # Mirror the local directory structure so relative Package.swift paths resolve correctly
+ local plum_base="$PLUM_STAGING"
+ local plum_app_dir="$plum_base/@applications/@messenger/imessage-macos"
+
+ print_step "Syncing source to $PLUM_HOST:~/$plum_app_dir/"
+ ssh "$PLUM_HOST" "mkdir -p ~/$plum_app_dir"
+ rsync -az --delete \
+ --exclude '.build/' \
+ --exclude 'DerivedData/' \
+ --exclude '.swiftpm/' \
+ "$SCRIPT_DIR/" \
+ "$PLUM_HOST:~/$plum_app_dir/"
+
+ # Sync local package dependencies referenced by relative paths in Package.swift
+ # ../../../@packages/@swift/@macos/menu-bar -> @packages/@swift/@macos/menu-bar
+ local local_packages_root
+ local_packages_root="$(cd "$SCRIPT_DIR/../../../@packages/@swift/@macos" && pwd)"
+ local plum_packages_dir="$plum_base/@packages/@swift/@macos"
+
+ if [[ -d "$local_packages_root/menu-bar" ]]; then
+ print_step "Syncing local Swift package dependencies..."
+ ssh "$PLUM_HOST" "mkdir -p ~/$plum_packages_dir"
+ rsync -az --delete \
+ --exclude '.build/' \
+ "$local_packages_root/menu-bar/" \
+ "$PLUM_HOST:~/$plum_packages_dir/menu-bar/"
+ print_success "Local packages synced"
+ fi
+
+ # Sync frontend-macos-client (webapp served by the local web server)
+ local frontend_src="$SCRIPT_DIR/../frontend-macos-client"
+ local plum_frontend_dir="$plum_base/@applications/@messenger/frontend-macos-client"
+
+ if [[ -d "$frontend_src" ]]; then
+ print_step "Syncing webapp frontend..."
+ ssh "$PLUM_HOST" "mkdir -p ~/$plum_frontend_dir"
+ rsync -az --delete \
+ "$frontend_src/" \
+ "$PLUM_HOST:~/$plum_frontend_dir/"
+ print_success "Frontend synced"
+ fi
+
+ print_success "Source synced"
+
+ # Bump build number in shared VERSION.json (unless --no-bump passed by ./run)
+ local version_file="$SCRIPT_DIR/../VERSION.json"
+ if [[ "$NO_BUMP" != "true" ]] && [[ -f "$version_file" ]] && command -v jq &>/dev/null; then
+ print_step "Bumping build version..."
+ local builds=$(jq '.builds' "$version_file")
+ local new_builds=$((builds + 1))
+ local major=$(jq -r '.major' "$version_file")
+ local minor=$(jq -r '.minor' "$version_file")
+ local new_version="${major}.${minor}.${new_builds}"
+ local today=$(date +%Y-%m-%d)
+
+ jq --arg v "$new_version" --argjson b "$new_builds" --arg d "$today" \
+ '.builds = $b | .version = $v | .lastBuild = $d' \
+ "$version_file" > "${version_file}.tmp" && mv "${version_file}.tmp" "$version_file"
+ print_success "Version: $new_version"
+ fi
+
+ # Sync VERSION.json to plum for generate-version.sh
+ if [[ -f "$version_file" ]]; then
+ rsync -az "$version_file" "$PLUM_HOST:~/$plum_app_dir/VERSION.json"
+ rsync -az "$version_file" "$PLUM_HOST:~/$plum_base/@applications/@messenger/VERSION.json"
+ fi
+
+ # Run installer on plum
+ print_step "Building and installing on $PLUM_HOST..."
+ ssh -t "$PLUM_HOST" "cd ~/$plum_app_dir && chmod +x install.sh && ./install.sh '$SERVER_URL'"
+
+ # Verify agent is running
+ print_step "Checking agent status..."
+ ssh "$PLUM_HOST" bash << 'CHECK_SCRIPT'
+sleep 2
+if pgrep -x iMessageMacos >/dev/null; then
+ echo "✓ Agent is running"
+ echo ""
+ echo "Recent logs:"
+ tail -10 ~/Library/Application\ Support/iMessageMacos/stderr.log 2>/dev/null || echo "(no logs yet)"
+else
+ echo "⚠ Agent is not running (may need Full Disk Access)"
+ echo ""
+ echo "Grant Full Disk Access:"
+ echo " System Settings → Privacy & Security → Full Disk Access"
+ echo " Add ~/Applications/iMessageMacos.app"
+fi
+CHECK_SCRIPT
+
+ # Clean up staging
+ print_step "Cleaning up staging directory..."
+ ssh "$PLUM_HOST" "rm -rf ~/$plum_base"
+ print_success "Staging cleaned"
+
+ echo ""
+ print_success "Deployment complete!"
+ echo ""
+ print_info "View logs: ssh $PLUM_HOST 'tail -f ~/Library/Application\\ Support/iMessageMacos/stderr.log'"
+}
+
+# ---------------------------------------------------------------------------
+# Uninstall
+# ---------------------------------------------------------------------------
+
+cmd_uninstall() {
+ check_plum
+
+ local plum_base="$PLUM_STAGING"
+
+ # Rsync just the uninstall script
+ print_step "Syncing uninstaller to $PLUM_HOST..."
+ ssh "$PLUM_HOST" "mkdir -p ~/$plum_base"
+ rsync -az "$SCRIPT_DIR/uninstall.sh" "$PLUM_HOST:~/$plum_base/"
+
+ print_step "Running uninstaller on $PLUM_HOST..."
+ ssh -t "$PLUM_HOST" "cd ~/$plum_base && chmod +x uninstall.sh && ./uninstall.sh"
+
+ ssh "$PLUM_HOST" "rm -rf ~/$plum_base"
+ print_success "Staging cleaned"
+
+ echo ""
+ print_success "iMessage macOS uninstalled from $PLUM_HOST"
+}
+
+# ---------------------------------------------------------------------------
+# Frontend-only deploy (no rebuild, no re-sign, preserves TCC permissions)
+# ---------------------------------------------------------------------------
+
+cmd_frontend() {
+ check_plum
+
+ local frontend_src="$SCRIPT_DIR/../frontend-macos-client"
+ local app_webapp="Applications/iMessageMacos.app/Contents/Resources/webapp"
+
+ if [[ ! -d "$frontend_src" ]]; then
+ print_error "Frontend source not found at $frontend_src"
+ exit 1
+ fi
+
+ print_step "Syncing webapp to $PLUM_HOST:~/$app_webapp/"
+ rsync -az \
+ "$frontend_src/" \
+ "$PLUM_HOST:~/$app_webapp/"
+ print_success "Frontend synced"
+
+ # Restart agent to pick up new files (kill → LaunchAgent auto-restarts)
+ print_step "Restarting agent..."
+ ssh "$PLUM_HOST" "pkill -x iMessageMacos 2>/dev/null || true"
+ sleep 2
+
+ if ssh "$PLUM_HOST" "pgrep -x iMessageMacos >/dev/null"; then
+ print_success "Agent restarted"
+ else
+ print_error "Agent did not restart — check LaunchAgent"
+ fi
+
+ echo ""
+ print_success "Frontend deploy complete (no re-sign, TCC preserved)"
+}
+
+# ---------------------------------------------------------------------------
+# Entry
+# ---------------------------------------------------------------------------
+
+case "${1:-}" in
+ uninstall)
+ cmd_uninstall
+ ;;
+ frontend)
+ cmd_frontend
+ ;;
+ *)
+ cmd_install
+ ;;
+esac
diff --git a/imessage-macos/generate-version.sh b/imessage-macos/generate-version.sh
new file mode 100755
index 0000000..22fdb31
--- /dev/null
+++ b/imessage-macos/generate-version.sh
@@ -0,0 +1,92 @@
+#!/bin/bash
+#
+# Regenerate AppVersion.swift from VERSION.json
+#
+# Run this before building the macOS app:
+# ./generate-version.sh && swift build
+#
+# This script:
+# 1. Reads VERSION.json (bumped by CI, not this script)
+# 2. Generates Sources/AppVersion.swift with current values
+#
+# NOTE: CI bumps VERSION.json on each build. This script is READ-ONLY.
+# To manually bump versions, use: ./workflow/version build
+#
+# Reads: VERSION.json (searched up from script dir, or falls back to defaults)
+# Writes: Sources/AppVersion.swift
+#
+
+set -e
+
+SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
+OUTPUT_FILE="$SCRIPT_DIR/Sources/AppVersion.swift"
+
+# Search for VERSION.json upward from script directory
+VERSION_FILE=""
+search_dir="$SCRIPT_DIR"
+while [[ "$search_dir" != "/" ]]; do
+ if [[ -f "$search_dir/VERSION.json" ]]; then
+ VERSION_FILE="$search_dir/VERSION.json"
+ break
+ fi
+ search_dir="$(dirname "$search_dir")"
+done
+
+# If VERSION.json found, parse it; otherwise use defaults
+if [[ -n "$VERSION_FILE" ]]; then
+ MAJOR=$(jq -r '.major' "$VERSION_FILE")
+ MERGES=$(jq -r '.merges // 0' "$VERSION_FILE")
+ BUILDS=$(jq -r '.builds' "$VERSION_FILE")
+ VERSION=$(jq -r '.version' "$VERSION_FILE")
+ LAST_BUILD=$(jq -r '.lastBuild' "$VERSION_FILE")
+else
+ echo "Warning: VERSION.json not found, using defaults" >&2
+ MAJOR=0
+ MERGES=0
+ BUILDS=0
+ VERSION="0.0.0"
+ LAST_BUILD="unknown"
+fi
+
+# Get git info
+GIT_COMMIT=$(git rev-parse --short HEAD 2>/dev/null || echo "unknown")
+GIT_BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "unknown")
+
+# Generate Swift file
+cat > "$OUTPUT_FILE" << EOF
+//
+// AppVersion.swift
+// Generated from VERSION.json - DO NOT EDIT MANUALLY
+//
+// Regenerate with: ./generate-version.sh
+//
+
+import Foundation
+
+enum AppVersion {
+ static let version = "$VERSION"
+ static let major = $MAJOR
+ static let merges = $MERGES
+ static let builds = $BUILDS
+
+ static let gitCommit = "$GIT_COMMIT"
+ static let gitBranch = "$GIT_BRANCH"
+ static let buildTime = "$LAST_BUILD"
+
+ static var displayVersion: String {
+ "v\(version)"
+ }
+
+ static var fullVersion: String {
+ "Version \(version) (\(gitCommit))"
+ }
+
+ static var detailedVersion: String {
+ "v\(version) (\(gitCommit) on \(gitBranch))"
+ }
+}
+EOF
+
+echo "Generated: $OUTPUT_FILE"
+echo " Version: $VERSION"
+echo " Commit: $GIT_COMMIT"
diff --git a/imessage-macos/iMessageMacos.entitlements b/imessage-macos/iMessageMacos.entitlements
new file mode 100755
index 0000000..bf4f61c
--- /dev/null
+++ b/imessage-macos/iMessageMacos.entitlements
@@ -0,0 +1,31 @@
+
+
+
+
+
+ com.apple.security.personal-information.addressbook
+
+
+
+ com.apple.security.files.user-selected.read-only
+
+
+
+ com.apple.security.network.client
+
+
+
+ com.apple.security.network.server
+
+
+
+ com.apple.security.automation.apple-events
+
+
+
+ com.apple.security.keychain-access-groups
+
+ $(AppIdentifierPrefix)com.lilith.imessage-macos
+
+
+
diff --git a/imessage-macos/install.sh b/imessage-macos/install.sh
new file mode 100755
index 0000000..dbb6315
--- /dev/null
+++ b/imessage-macos/install.sh
@@ -0,0 +1,537 @@
+#!/bin/bash
+set -euo pipefail
+
+# iMessage macOS - Agent Installer
+# This script installs and configures the iMessage macOS menu bar agent
+
+# Colors for output
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+BLUE='\033[0;34m'
+NC='\033[0m' # No Color
+
+# Configuration
+APP_NAME="iMessageMacos"
+BUNDLE_ID="com.lilith.imessage-macos"
+INSTALL_DIR="$HOME/Applications"
+APP_SUPPORT_DIR="$HOME/Library/Application Support/$APP_NAME"
+LAUNCH_AGENTS_DIR="$HOME/Library/LaunchAgents"
+PLIST_FILE="$LAUNCH_AGENTS_DIR/$BUNDLE_ID.plist"
+
+# Script directory (where this script lives)
+SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
+MESSENGER_ROOT="$( cd "$SCRIPT_DIR/.." && pwd )"
+
+# Functions
+print_header() {
+ echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
+ echo -e "${BLUE} iMessage macOS - Agent Installer${NC}"
+ echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
+ echo ""
+}
+
+print_step() {
+ echo -e "${GREEN}▸${NC} $1"
+}
+
+print_info() {
+ echo -e "${BLUE}ℹ${NC} $1"
+}
+
+print_warning() {
+ echo -e "${YELLOW}⚠${NC} $1"
+}
+
+print_error() {
+ echo -e "${RED}✗${NC} $1"
+}
+
+print_success() {
+ echo -e "${GREEN}✓${NC} $1"
+}
+
+get_default_port() {
+ local default="$1"
+ echo "$default"
+}
+
+check_macos() {
+ if [[ "$OSTYPE" != "darwin"* ]]; then
+ print_error "This script is designed for macOS only"
+ exit 1
+ fi
+}
+
+check_swift() {
+ if ! command -v swift &> /dev/null; then
+ print_error "Swift is not installed. Please install Xcode or Command Line Tools."
+ echo ""
+ echo "To install Command Line Tools:"
+ echo " xcode-select --install"
+ exit 1
+ fi
+
+ local swift_version=$(swift --version | head -n1)
+ print_info "Found: $swift_version"
+}
+
+stop_existing_agent() {
+ print_step "Stopping existing agent (if running)..."
+
+ # Unload LaunchAgent if it exists
+ if [[ -f "$PLIST_FILE" ]]; then
+ launchctl unload "$PLIST_FILE" 2>/dev/null || true
+ print_info "Stopped LaunchAgent"
+ fi
+
+ # Kill any running instances
+ pkill -x "$APP_NAME" 2>/dev/null || true
+ sleep 1
+}
+
+build_application() {
+ print_step "Building Swift application..."
+
+ cd "$SCRIPT_DIR"
+
+ # Generate version file from VERSION.json
+ print_info "Generating version info..."
+ if [[ -x "./generate-version.sh" ]]; then
+ ./generate-version.sh
+ else
+ print_warning "generate-version.sh not found or not executable, skipping version generation"
+ fi
+
+ # Clean previous builds
+ if [[ -d ".build" ]]; then
+ rm -rf .build
+ fi
+
+ # Build release version
+ print_info "Compiling with optimizations (this may take a minute)..."
+ if swift build -c release; then
+ print_success "Build successful"
+ else
+ print_error "Build failed"
+ exit 1
+ fi
+}
+
+install_binary() {
+ print_step "Installing application..."
+
+ # Create installation directory
+ mkdir -p "$INSTALL_DIR"
+
+ # Copy binary
+ local binary_path="$SCRIPT_DIR/.build/release/$APP_NAME"
+ local install_path="$INSTALL_DIR/$APP_NAME.app/Contents/MacOS"
+
+ if [[ ! -f "$binary_path" ]]; then
+ print_error "Binary not found at $binary_path"
+ exit 1
+ fi
+
+ # Create app bundle structure
+ mkdir -p "$install_path"
+ mkdir -p "$INSTALL_DIR/$APP_NAME.app/Contents/Resources"
+ mkdir -p "$INSTALL_DIR/$APP_NAME.app/Contents/Resources/webapp"
+
+ # Copy binary
+ cp "$binary_path" "$install_path/$APP_NAME"
+ chmod +x "$install_path/$APP_NAME"
+
+ # Copy webapp files
+ local webapp_src="$SCRIPT_DIR/../frontend-macos-client"
+ local webapp_dest="$INSTALL_DIR/$APP_NAME.app/Contents/Resources/webapp"
+
+ if [[ -d "$webapp_src" ]]; then
+ print_info "Copying webapp files..."
+ cp "$webapp_src/index.html" "$webapp_dest/"
+ cp "$webapp_src/styles.css" "$webapp_dest/"
+ cp "$webapp_src/app.js" "$webapp_dest/"
+ print_success "Webapp bundled into app"
+ else
+ print_warning "Webapp source not found at $webapp_src"
+ fi
+
+ # Create Info.plist
+ cat > "$INSTALL_DIR/$APP_NAME.app/Contents/Info.plist" <
+
+
+
+ CFBundleExecutable
+ $APP_NAME
+ CFBundleIdentifier
+ $BUNDLE_ID
+ CFBundleName
+ iMessage macOS
+ CFBundlePackageType
+ APPL
+ CFBundleVersion
+ 1.0.0
+ LSUIElement
+
+ NSHighResolutionCapable
+
+ NSContactsUsageDescription
+ iMessage macOS needs access to your Contacts to display names for your iMessage conversations.
+ NSAppleEventsUsageDescription
+ iMessage macOS needs to control the Messages app to send iMessages on your behalf.
+ NSAppTransportSecurity
+
+ NSAllowsLocalNetworking
+
+
+
+
+EOF
+
+ # Copy entitlements file
+ local entitlements_src="$SCRIPT_DIR/iMessageMacos.entitlements"
+ if [[ -f "$entitlements_src" ]]; then
+ cp "$entitlements_src" "$INSTALL_DIR/$APP_NAME.app/Contents/Resources/"
+ print_info "Copied entitlements file"
+ fi
+
+ # Code sign the app with entitlements
+ print_step "Code signing application..."
+ if [[ -f "$entitlements_src" ]]; then
+ # Sign with entitlements for TCC permissions to work
+ codesign --force --deep --sign - \
+ --entitlements "$entitlements_src" \
+ "$INSTALL_DIR/$APP_NAME.app"
+ if [[ $? -eq 0 ]]; then
+ print_success "Code signed with entitlements"
+ else
+ print_warning "Code signing failed - TCC permissions may not work"
+ fi
+ else
+ # Fallback to basic signing
+ codesign --force --deep --sign - "$INSTALL_DIR/$APP_NAME.app"
+ print_warning "Code signed without entitlements (entitlements file not found)"
+ fi
+
+ print_success "Installed to $INSTALL_DIR/$APP_NAME.app"
+}
+
+discover_service_url() {
+ # Try to discover imessage-sync service from service-registry
+ local registry_url="${SERVICE_REGISTRY_URL:-http://localhost:30000}"
+
+ # Try to discover the service
+ local discovery_response
+ discovery_response=$(curl -s --connect-timeout 2 "$registry_url/registry/discover?serviceName=imessage-sync&healthy=true" 2>/dev/null || echo "")
+
+ if [[ -n "$discovery_response" && "$discovery_response" != *"error"* ]]; then
+ # Parse the first instance's URL from JSON response
+ local service_url
+ service_url=$(echo "$discovery_response" | python3 -c "
+import sys, json
+try:
+ data = json.load(sys.stdin)
+ if data.get('instances') and len(data['instances']) > 0:
+ inst = data['instances'][0]
+ ip = inst.get('ipAddress', 'localhost')
+ port = inst.get('port', 3105)
+ print(f'http://{ip}:{port}')
+except:
+ pass
+" 2>/dev/null)
+
+ if [[ -n "$service_url" ]]; then
+ echo "$service_url"
+ return 0
+ fi
+ fi
+
+ return 1
+}
+
+configure_app() {
+ print_step "Configuring application..."
+
+ # Create application support directory
+ mkdir -p "$APP_SUPPORT_DIR"
+
+ # Get server URL from: 1) argument, 2) service-registry, 3) user input, 4) default
+ local server_url="${1:-}"
+
+ if [[ -z "$server_url" ]]; then
+ print_info "Checking service registry for imessage-sync..."
+ server_url=$(discover_service_url) || true
+
+ if [[ -n "$server_url" ]]; then
+ print_success "Discovered service at: $server_url"
+ else
+ echo ""
+ print_info "Enter the iMessage Sync server URL"
+ print_info "(Production: https://conversations.nasty.sh)"
+ read -p "Server URL [https://conversations.nasty.sh]: " server_url
+ server_url=${server_url:-https://conversations.nasty.sh}
+ fi
+ fi
+
+ # Validate URL format
+ if [[ ! "$server_url" =~ ^https?:// ]]; then
+ print_error "Invalid URL format. Must start with http:// or https://"
+ exit 1
+ fi
+
+ # Store in UserDefaults via defaults command
+ defaults write "$BUNDLE_ID" apiBaseURL "$server_url"
+
+ # Configure web server port from ports.yaml (or default)
+ local webserver_port
+ webserver_port=$(get_default_port "8765")
+ defaults write "$BUNDLE_ID" webServerPort -int "$webserver_port"
+
+ print_success "Server URL configured: $server_url"
+ print_success "Web server port configured: $webserver_port"
+}
+
+create_launch_agent() {
+ print_step "Creating LaunchAgent for auto-start..."
+
+ mkdir -p "$LAUNCH_AGENTS_DIR"
+
+ local binary_path="$INSTALL_DIR/$APP_NAME.app/Contents/MacOS/$APP_NAME"
+
+ cat > "$PLIST_FILE" <
+
+
+
+ Label
+ $BUNDLE_ID
+ ProgramArguments
+
+ $binary_path
+
+ RunAtLoad
+
+ KeepAlive
+
+ SuccessfulExit
+
+ Crashed
+
+
+ StandardOutPath
+ $APP_SUPPORT_DIR/stdout.log
+ StandardErrorPath
+ $APP_SUPPORT_DIR/stderr.log
+ EnvironmentVariables
+
+ PATH
+ /usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin
+
+
+
+EOF
+
+ # Load the LaunchAgent
+ launchctl load "$PLIST_FILE"
+
+ print_success "LaunchAgent created and loaded"
+ print_info "The agent will start automatically on login"
+}
+
+show_permissions_guide() {
+ echo ""
+ echo -e "${YELLOW}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
+ echo -e "${YELLOW} IMPORTANT: Grant Full Disk Access${NC}"
+ echo -e "${YELLOW}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
+ echo ""
+ echo "iMessage macOS requires Full Disk Access to read the iMessage database."
+ echo ""
+ echo "To grant access:"
+ echo " 1. System Settings will open to Full Disk Access"
+ echo " 2. Click the '+' button"
+ echo " 3. Navigate to: $INSTALL_DIR/$APP_NAME.app"
+ echo " 4. Select the app and click 'Open'"
+ echo " 5. Enable the toggle next to $APP_NAME"
+ echo ""
+
+ # Check if running interactively (has a TTY)
+ if [[ -t 0 ]]; then
+ read -p "Press Enter to open System Settings, or 'n' to skip: " open_settings
+ if [[ "$open_settings" != "n" && "$open_settings" != "N" ]]; then
+ open_fda_settings
+ fi
+ else
+ # Non-interactive mode (SSH batch) - auto-open settings
+ print_info "Opening System Settings → Full Disk Access..."
+ open_fda_settings
+ fi
+}
+
+open_fda_settings() {
+ # Open System Settings to Full Disk Access pane (macOS Ventura+)
+ # Use osascript to ensure it opens in the user's GUI session (works via SSH)
+ osascript -e 'tell application "System Settings"
+ activate
+ delay 0.5
+ tell application "System Events"
+ keystroke "Full Disk Access"
+ delay 0.3
+ key code 36
+ end tell
+ end tell' 2>/dev/null || \
+ open "x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles" 2>/dev/null || \
+ open "/System/Applications/System Settings.app" 2>/dev/null
+}
+
+request_contacts_permission() {
+ echo ""
+ echo -e "${YELLOW}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
+ echo -e "${YELLOW} IMPORTANT: Grant Contacts Access${NC}"
+ echo -e "${YELLOW}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
+ echo ""
+ echo "iMessage macOS needs Contacts access to display names for conversations."
+ echo ""
+ print_info "Launching app to request Contacts permission..."
+ echo ""
+
+ # Launch the app via 'open' which gives it proper GUI context for permission dialogs
+ # Run briefly to trigger the contacts permission request
+ open "$INSTALL_DIR/$APP_NAME.app"
+
+ # Wait a moment for the permission dialog to appear
+ sleep 3
+
+ echo "A permission dialog should appear asking for Contacts access."
+ echo "Please click 'OK' or 'Allow' to grant access."
+ echo ""
+
+ if [[ -t 0 ]]; then
+ read -p "Press Enter once you've granted Contacts access (or 'n' to skip): " contacts_response
+ if [[ "$contacts_response" == "n" || "$contacts_response" == "N" ]]; then
+ print_warning "Skipped Contacts permission. You can grant it later in System Settings."
+ else
+ print_success "Contacts permission requested"
+ fi
+ else
+ # Non-interactive - just wait and continue
+ sleep 5
+ print_info "If a permission dialog appeared, please grant access."
+ fi
+
+ # Stop the app so LaunchAgent can manage it
+ pkill -x "$APP_NAME" 2>/dev/null || true
+ sleep 1
+}
+
+register_device() {
+ print_step "Starting the agent for device registration..."
+
+ echo ""
+ print_info "The iMessage macOS agent is now running in your menu bar."
+ print_info "Look for the message bubble icon (💬) in the top-right of your screen."
+ echo ""
+ print_info "To complete setup:"
+ echo " 1. Click the menu bar icon to open the web interface"
+ echo " 2. A browser will open showing the device registration page"
+ echo " 3. Enter the verification code shown in your admin panel"
+ echo ""
+}
+
+check_installation() {
+ print_step "Verifying installation..."
+
+ local errors=0
+
+ # Check binary exists
+ if [[ -f "$INSTALL_DIR/$APP_NAME.app/Contents/MacOS/$APP_NAME" ]]; then
+ print_success "Binary installed"
+ else
+ print_error "Binary not found"
+ errors=$((errors + 1))
+ fi
+
+ # Check LaunchAgent exists
+ if [[ -f "$PLIST_FILE" ]]; then
+ print_success "LaunchAgent configured"
+ else
+ print_error "LaunchAgent not found"
+ errors=$((errors + 1))
+ fi
+
+ # Check if agent is running
+ if pgrep -x "$APP_NAME" > /dev/null; then
+ print_success "Agent is running"
+ else
+ print_warning "Agent is not running yet (may need permissions)"
+ fi
+
+ return $errors
+}
+
+print_completion() {
+ echo ""
+ echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
+ echo -e "${GREEN} Installation Complete${NC}"
+ echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
+ echo ""
+ echo "iMessage macOS has been successfully installed."
+ echo ""
+ echo "Next steps:"
+ echo " 1. Grant Full Disk Access (see instructions above)"
+ echo " 2. Click the menu bar icon and complete device registration"
+ echo " 3. Your iMessages will automatically sync to the server"
+ echo ""
+ echo "Useful commands:"
+ echo " View logs: tail -f $APP_SUPPORT_DIR/stderr.log"
+ echo " Restart agent: launchctl unload $PLIST_FILE && launchctl load $PLIST_FILE"
+ echo " Uninstall: ./uninstall.sh"
+ echo ""
+}
+
+# Main installation flow
+main() {
+ print_header
+
+ # Parse arguments
+ local server_url=""
+ if [[ $# -gt 0 ]]; then
+ server_url="$1"
+ fi
+
+ # Pre-flight checks
+ check_macos
+ check_swift
+
+ # Detect fresh install vs update
+ local IS_FRESH_INSTALL="true"
+ if [[ -f "$PLIST_FILE" ]]; then
+ IS_FRESH_INSTALL="false"
+ print_info "Existing installation detected — updating"
+ fi
+
+ # Installation steps
+ stop_existing_agent
+ build_application
+ install_binary
+ configure_app "$server_url"
+ create_launch_agent
+
+ # Post-installation
+ if check_installation; then
+ # Only show permission prompts on fresh install (no existing LaunchAgent)
+ if [[ "$IS_FRESH_INSTALL" == "true" ]]; then
+ show_permissions_guide
+ request_contacts_permission
+ register_device
+ fi
+ print_completion
+ else
+ echo ""
+ print_error "Installation completed with errors. Please review the output above."
+ exit 1
+ fi
+}
+
+# Run main function with all arguments
+main "$@"
diff --git a/imessage-macos/uninstall.sh b/imessage-macos/uninstall.sh
new file mode 100755
index 0000000..d335bda
--- /dev/null
+++ b/imessage-macos/uninstall.sh
@@ -0,0 +1,217 @@
+#!/bin/bash
+set -euo pipefail
+
+# iMessage macOS - Agent Uninstaller
+# This script removes the iMessage macOS menu bar agent
+
+# Colors for output
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+BLUE='\033[0;34m'
+NC='\033[0m' # No Color
+
+# Configuration
+APP_NAME="iMessageMacos"
+BUNDLE_ID="com.lilith.imessage-macos"
+INSTALL_DIR="$HOME/Applications"
+APP_SUPPORT_DIR="$HOME/Library/Application Support/$APP_NAME"
+LAUNCH_AGENTS_DIR="$HOME/Library/LaunchAgents"
+PLIST_FILE="$LAUNCH_AGENTS_DIR/$BUNDLE_ID.plist"
+PREFS_FILE="$HOME/Library/Preferences/$BUNDLE_ID.plist"
+
+# Functions
+print_header() {
+ echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
+ echo -e "${BLUE} iMessage macOS - Uninstaller${NC}"
+ echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
+ echo ""
+}
+
+print_step() {
+ echo -e "${GREEN}▸${NC} $1"
+}
+
+print_info() {
+ echo -e "${BLUE}ℹ${NC} $1"
+}
+
+print_warning() {
+ echo -e "${YELLOW}⚠${NC} $1"
+}
+
+print_error() {
+ echo -e "${RED}✗${NC} $1"
+}
+
+print_success() {
+ echo -e "${GREEN}✓${NC} $1"
+}
+
+confirm_uninstall() {
+ echo -e "${YELLOW}This will remove:${NC}"
+ echo " • Application binary: $INSTALL_DIR/$APP_NAME.app"
+ echo " • LaunchAgent: $PLIST_FILE"
+ echo " • Application data: $APP_SUPPORT_DIR"
+ echo " • Preferences: $PREFS_FILE"
+ echo " • Keychain entries (auth tokens)"
+ echo ""
+
+ read -p "Are you sure you want to uninstall? [y/N]: " confirm
+
+ if [[ "$confirm" != "y" && "$confirm" != "Y" ]]; then
+ echo ""
+ print_info "Uninstall cancelled"
+ exit 0
+ fi
+}
+
+stop_agent() {
+ print_step "Stopping agent..."
+
+ # Unload LaunchAgent
+ if [[ -f "$PLIST_FILE" ]]; then
+ launchctl unload "$PLIST_FILE" 2>/dev/null || true
+ print_success "Unloaded LaunchAgent"
+ fi
+
+ # Kill any running instances
+ if pgrep -x "$APP_NAME" > /dev/null; then
+ pkill -x "$APP_NAME" 2>/dev/null || true
+ sleep 1
+ print_success "Stopped running processes"
+ fi
+}
+
+remove_files() {
+ print_step "Removing files..."
+
+ local removed_count=0
+
+ # Remove application
+ if [[ -d "$INSTALL_DIR/$APP_NAME.app" ]]; then
+ rm -rf "$INSTALL_DIR/$APP_NAME.app"
+ print_success "Removed application"
+ removed_count=$((removed_count + 1))
+ fi
+
+ # Remove LaunchAgent plist
+ if [[ -f "$PLIST_FILE" ]]; then
+ rm -f "$PLIST_FILE"
+ print_success "Removed LaunchAgent"
+ removed_count=$((removed_count + 1))
+ fi
+
+ # Remove application support directory
+ if [[ -d "$APP_SUPPORT_DIR" ]]; then
+ rm -rf "$APP_SUPPORT_DIR"
+ print_success "Removed application data"
+ removed_count=$((removed_count + 1))
+ fi
+
+ # Remove preferences
+ if [[ -f "$PREFS_FILE" ]]; then
+ rm -f "$PREFS_FILE"
+ print_success "Removed preferences"
+ removed_count=$((removed_count + 1))
+ fi
+
+ # Also remove defaults domain
+ defaults delete "$BUNDLE_ID" 2>/dev/null || true
+
+ if [[ $removed_count -eq 0 ]]; then
+ print_warning "No files found to remove"
+ fi
+}
+
+remove_keychain_entries() {
+ print_step "Removing keychain entries..."
+
+ # The app stores auth token in keychain with account "authToken"
+ security delete-generic-password -a "authToken" -s "$BUNDLE_ID" 2>/dev/null || true
+
+ print_success "Cleared keychain entries"
+}
+
+verify_removal() {
+ print_step "Verifying removal..."
+
+ local remaining=0
+
+ # Check if any files remain
+ if [[ -d "$INSTALL_DIR/$APP_NAME.app" ]]; then
+ print_warning "Application still exists"
+ remaining=$((remaining + 1))
+ fi
+
+ if [[ -f "$PLIST_FILE" ]]; then
+ print_warning "LaunchAgent still exists"
+ remaining=$((remaining + 1))
+ fi
+
+ if [[ -d "$APP_SUPPORT_DIR" ]]; then
+ print_warning "Application data still exists"
+ remaining=$((remaining + 1))
+ fi
+
+ if pgrep -x "$APP_NAME" > /dev/null; then
+ print_warning "Process still running"
+ remaining=$((remaining + 1))
+ fi
+
+ if [[ $remaining -eq 0 ]]; then
+ print_success "All components removed successfully"
+ return 0
+ else
+ return 1
+ fi
+}
+
+print_completion() {
+ echo ""
+ echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
+ echo -e "${GREEN} Uninstall Complete${NC}"
+ echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
+ echo ""
+ echo "iMessage macOS has been removed from this system."
+ echo ""
+ echo "Note: You may need to manually remove Full Disk Access permission:"
+ echo " System Settings → Privacy & Security → Full Disk Access"
+ echo " Find '$APP_NAME' and remove it from the list"
+ echo ""
+ echo "To reinstall, run: ./install.sh"
+ echo ""
+}
+
+# Main uninstall flow
+main() {
+ print_header
+
+ # Check if running on macOS
+ if [[ "$OSTYPE" != "darwin"* ]]; then
+ print_error "This script is designed for macOS only"
+ exit 1
+ fi
+
+ # Confirm with user
+ confirm_uninstall
+
+ echo ""
+
+ # Uninstall steps
+ stop_agent
+ remove_files
+ remove_keychain_entries
+
+ # Verification
+ if verify_removal; then
+ print_completion
+ else
+ echo ""
+ print_warning "Uninstall completed with warnings. Some components may remain."
+ exit 1
+ fi
+}
+
+# Run main function
+main "$@"