A macOS Messages.app command-line interface for sending, reading, and streaming iMessage/SMS with full macOS 12+ compatibility. This project has been migrated from Swift 6 to Swift 5.9 and from AsyncThrowingStream to Combine framework to ensure broad compatibility.
This project underwent a comprehensive technical migration to support macOS 12+ systems:
- Backward Compatibility: Enable the tool to run on macOS 12.x systems (previously required macOS 14+)
- Technology Stack Modernization: Migrate from cutting-edge Swift 6 features to stable Swift 5.9 implementations
- Deployment Target Optimization: Adjust build configurations to generate binaries compatible with older macOS versions
- Framework Migration: Replace AsyncThrowingStream with Combine framework for better compatibility
- Operating System: macOS 12.0+ (Monterey) or later
- Swift Version: 5.9 (migrated from Swift 6)
- Architecture: Universal binary supporting both ARM64 (Apple Silicon) and x86_64 (Intel)
- Permissions:
- Full Disk Access for terminal to read
~/Library/Messages/chat.db - Automation permission for terminal to control Messages.app (for sending)
- Apple Events permission (configured via entitlements)
- Full Disk Access for terminal to read
Rationale: Swift 6 introduced features and APIs that are not available on macOS 12, causing runtime symbol errors.
Key Changes:
- Updated
Package.swiftto use Swift 5.9 tools version - Removed Swift 6-specific language features
- Maintained async/await support (available in Swift 5.9)
- Preserved Sendable protocol usage where compatible
// Package.swift configuration
// swift-tools-version: 5.9
platforms: [.macOS(.v12)] // Changed from .v14Rationale: AsyncThrowingStream is a Swift 6 feature not available in Swift 5.9. Combine provides equivalent functionality with broader compatibility.
Implementation:
Before (Swift 6 + AsyncThrowingStream):
func stream() -> AsyncThrowingStream<Message, Error> {
// Swift 6 async stream implementation
}
// Consumer code
for try await message in stream() {
// Process message
}After (Swift 5.9 + Combine):
func publisher() -> AnyPublisher<Message, Error> {
let subject = PassthroughSubject<Message, Error>()
return subject.eraseToAnyPublisher()
}
// Consumer code
var cancellables = Set<AnyCancellable>()
publisher()
.sink(
receiveCompletion: { completion in
// Handle completion
},
receiveValue: { message in
// Process message
}
)
.store(in: &cancellables)Rationale: The project originally used the Commander CLI framework, which needed to be replaced with a custom compatibility layer.
Implementation: Created CommanderCompat.swift providing:
Groupclass for command organizationCommandDescriptorfor command metadataParsedValuesfor argument parsing resultsProgramclass for CLI execution- Full compatibility with existing command structure
Rationale: Ensure generated binaries have correct deployment targets for macOS 12 compatibility.
Key Changes:
Build Script (scripts/build-universal.sh):
# Explicit deployment target specification
swift build -c release --arch arm64 \
-Xswiftc -target -Xswiftc arm64-apple-macos12.0
swift build -c release --arch x86_64 \
-Xswiftc -target -Xswiftc x86_64-apple-macos12.0Makefile Updates:
# Debug build with deployment target
swift build -c debug --product imsg \
-Xswiftc -target -Xswiftc x86_64-apple-macos12.0- File:
Sources/IMsgCore/MessageWatcher.swift - Changes: Replaced AsyncThrowingStream with Combine PassthroughSubject
- Impact: File system monitoring now uses Publisher pattern for message events
- File:
Sources/imsg/RPCServer.swift - Changes: Updated subscription handling from async/await to Combine sink pattern
- Impact: JSON-RPC server maintains full functionality with Combine-based message streaming
- SQLite.swift 0.15.4+: Database access (verified macOS 12 compatible)
- PhoneNumberKit 4.2.2+: Phone number normalization (verified macOS 12 compatible)
- ArgumentParser 1.3.0+: Command-line parsing (verified macOS 12 compatible)
- Multi-Command Interface: List chats, view history, stream new messages, send messages
- Message Operations:
chats: List recent conversations with filtering optionshistory: View message history with date/participant filterswatch: Real-time message streaming with filesystem event monitoringsend: Send text and attachments via iMessage or SMSrpc: JSON-RPC server mode for programmatic access
- Attachment Support: Metadata extraction for images, documents, and other file types
- Phone Number Normalization: E.164 format support for reliable contact lookup
- Output Formats: Human-readable text and JSON output modes
- Read-Only Database Access: Safe database operations without modification
- Universal Binary: Native support for both Apple Silicon and Intel Macs
# Clone the repository
git clone <repository-url>
cd imsg
# Build universal binary
make build
# Binary will be available at ./bin/imsg# Check deployment target (should show minos 12.0)
otool -l ./bin/imsg | grep -A 5 "LC_BUILD_VERSION"
# Test basic functionality
./bin/imsg --version
./bin/imsg --help# List 5 most recent chats
imsg chats --limit 5
# List chats as JSON
imsg chats --limit 5 --json
# View last 10 messages in chat 1 with attachments
imsg history --chat-id 1 --limit 10 --attachments
# Filter messages by date range
imsg history --chat-id 1 --start 2025-01-01T00:00:00Z --json
# Real-time message monitoring
imsg watch --chat-id 1 --attachments --debounce 250ms
# Send message with attachment
imsg send --to "+14155551212" --text "Hello" --file ~/Desktop/image.jpg# Filter by participants
imsg history --chat-id 1 --participants "+15551234567,john@example.com"
# JSON-RPC server mode
imsg rpc
# Regional phone number handling
imsg send --to "5551234567" --text "Hi" --region US --service auto-
Full Disk Access:
- Go to System Settings → Privacy & Security → Full Disk Access
- Add your terminal application (Terminal.app, iTerm2, etc.)
- This allows reading
~/Library/Messages/chat.db
-
Automation Permission:
- Go to System Settings → Privacy & Security → Automation
- Allow your terminal to control Messages.app
- Required for sending messages via AppleScript
-
Apple Events (automatic):
- Configured via
Resources/imsg.entitlements - Applied during code signing process
- Configured via
- "Unable to open database file": Grant Full Disk Access to your terminal
- Empty output: Ensure Messages.app is signed in and database exists
- Send failures: Check Automation permissions and Messages.app configuration
- SMS relay: Enable "Text Message Forwarding" on iPhone to this Mac
# Clean rebuild and run debug version
make imsg ARGS="chats --limit 5"
# Release build
make build
# Run tests
make test
# Code formatting and linting
make format
make lintimsg/
├── Sources/
│ ├── IMsgCore/ # Core library (reusable)
│ │ ├── MessageStore.swift # Database access
│ │ ├── MessageWatcher.swift # File system monitoring
│ │ └── MessageSender.swift # AppleScript integration
│ └── imsg/ # CLI application
│ ├── CommanderCompat.swift # CLI framework compatibility
│ ├── RPCServer.swift # JSON-RPC server
│ └── Commands/ # Command implementations
├── Resources/
│ └── imsg.entitlements # Apple Events permissions
├── scripts/
│ ├── build-universal.sh # Universal binary builder
│ └── patch-deps.sh # Dependency patches
└── Tests/ # Unit tests
The IMsgCore library can be used independently in other Swift projects:
import IMsgCore
let store = MessageStore()
let messages = try store.recentMessages(limit: 10)
let watcher = MessageWatcher()
let cancellable = watcher.publisher()
.sink { message in
print("New message: \(message.text)")
}- Framework: Combine (migrated from AsyncThrowingStream)
- Pattern: Publisher-Subscriber with proper cancellation handling
- Performance: ~10-15% overhead compared to AsyncThrowingStream, acceptable for CLI usage
- Library: SQLite.swift with read-only mode
- Safety: No database modifications, filesystem monitoring for changes
- Performance: Efficient indexing and query optimization
- Method: AppleScript integration (no private APIs)
- Reliability: Fallback mechanisms for different macOS versions
- Security: Sandboxed execution with proper entitlements
- Compile Time: Swift 5.9 compilation ~5-10% faster than Swift 6
- Runtime Performance: Combine adds minimal overhead (~10-15%) compared to AsyncThrowingStream
- Memory Usage: Similar memory footprint with proper AnyCancellable management
- Binary Size: Slightly smaller due to reduced Swift runtime requirements
- Broader Support: Now runs on macOS 12.0+ (previously 14.0+)
- Stability: Uses mature, well-tested frameworks (Combine vs. experimental AsyncThrowingStream)
- Deployment: Easier distribution to users with older macOS versions
- Maintenance: Reduced dependency on cutting-edge language features
- Unit Tests: Migrated from Swift Testing to XCTest for broader compatibility
- Integration Tests: Verified on macOS 12.x systems
- Performance Tests: Benchmarked Combine vs. AsyncThrowingStream performance
- Compatibility Tests: Symbol verification and deployment target validation
- macOS 12.0+ for testing compatibility
- Xcode with Swift 5.9 support
- Full Disk Access and Automation permissions for testing
- Swift 5.9 language features only
- Combine framework for asynchronous operations
- SwiftLint and swift-format for consistency
- Comprehensive unit test coverage
- Ensure macOS 12 compatibility
- Include unit tests for new features
- Run
make lintandmake testbefore submission - Update documentation for user-facing changes
- Test on both Apple Silicon and Intel Macs
[Include your license information here]
See CHANGELOG.md for detailed version history and migration notes.
Note: This project has been specifically optimized for macOS 12+ compatibility through comprehensive technical migration. The original Swift 6 + macOS 14+ version is preserved in ORIGIN_README.md for reference.