Skip to content

dewanshrawat15/jack

Jack

Control your Mac from your iPhone. Jack is a monorepo project consisting of a macOS daemon and a React Native mobile app that communicate over WebSocket. It lets you control volume, brightness, media playback, and display settings from your phone over your local network.


How It Works

  1. The daemon runs on your Mac as a background LaunchAgent. On startup it generates a secret token, stores it in ~/.jack/config.json, and advertises itself on your local network via mDNS (_jack._tcp.local).
  2. The mobile app scans for the daemon using mDNS, or you can enter the IP and port manually. You paste the token once and your credentials are saved to the iOS Keychain.
  3. Once connected, the app sends commands over a WebSocket and receives real-time system status (volume, brightness, battery) every 3 seconds.
iPhone ──── mDNS discovery ────► Mac Daemon (port 9521)
       ◄─── WebSocket (WS) ─────
       ──── Commands ──────────►
       ◄─── Status / ACK ───────

Repository Structure

jack/
├── apps/
│   ├── daemon/          # Node.js macOS daemon
│   └── mobile/          # Expo React Native iOS app
└── packages/
    └── shared/          # Shared TypeScript types

This is a Yarn workspace monorepo. All packages are TypeScript.


Packages

@jack/sharedpackages/shared/

Shared type definitions used by both the daemon and mobile app.

Module Contents
commands.ts Discriminated union of all 12 command types
events.ts Socket event name constants (command, ack, status, error)
responses.ts Success/error response types
status.ts SystemStatus type (volume, brightness, battery, etc.)

@jack/daemonapps/daemon/

A Node.js server that runs as a macOS LaunchAgent.

Module Responsibility
src/index.ts Entry point — loads config, starts server, advertises mDNS
src/server.ts Socket.IO server, auth middleware, status polling
src/auth.ts Token generation and constant-time validation
src/mdns.ts Bonjour/mDNS advertisement
src/status.ts System status collection (osascript, pmset, Swift helpers)
src/handlers/ Per-feature command handlers
bin/brightness_helper Compiled Swift binary using DisplayServices
bin/media_helper Compiled Swift binary using MediaRemote.framework

Tech stack: Node.js 20, Socket.IO 4.7, bonjour-service, Zod, tsx

@jack/mobileapps/mobile/

A React Native app built with Expo.

Module Responsibility
app/connect.tsx mDNS discovery, manual entry, pairing
app/dashboard.tsx Main control interface
src/context/SocketContext.tsx WebSocket connection lifecycle
src/hooks/useDiscovery.ts mDNS scanner (react-native-zeroconf)
src/hooks/useSystemStatus.ts Real-time status from daemon
src/services/storage.ts Credential persistence (expo-secure-store)
src/components/controls/ Volume, brightness, media, display control components

Tech stack: Expo 55, React Native 0.83, Expo Router, Socket.IO client 4.7, react-native-zeroconf


Setup

Prerequisites

  • macOS (Apple Silicon or Intel)
  • Node.js 20+ (brew install node)
  • Yarn (npm install -g yarn)
  • Homebrew (for the brightness CLI)

Daemon (Mac)

Run the setup script from the daemon directory:

cd apps/daemon
bash install/setup.sh

The script will:

  1. Install the brightness CLI via Homebrew
  2. Install dependencies and build the project
  3. Install a LaunchAgent that auto-starts the daemon on login
  4. Print your token, port, and local IP address

After setup, the daemon starts automatically. You can check its logs:

tail -f /tmp/jack.daemon.log
tail -f /tmp/jack.daemon.error.log

Your token and port are stored in ~/.jack/config.json:

{
  "token": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "port": 9521
}

Accessibility permission (required for media controls and display lock):

Go to System Settings → Privacy & Security → Accessibility and add Terminal (or whichever app runs Node.js).

Mobile App

Install dependencies:

cd apps/mobile
yarn install

Run on iOS simulator:

yarn ios

Run on a physical device:

yarn ios:device

Run with Expo Go (easiest for testing):

yarn start
# Scan the QR code with the Expo Go app

Production build (requires EAS account):

yarn build

Development

Run everything from the repo root

# Build the shared types package (required by daemon and mobile)
yarn build:shared

# Start the daemon in watch mode (hot reload)
yarn daemon

# Start the Expo dev server
yarn mobile

# Type-check all packages
yarn typecheck

Package-level commands

# Shared
cd packages/shared
yarn build       # Compile TypeScript
yarn dev         # Watch mode

# Daemon
cd apps/daemon
yarn dev         # Start with tsx watch (hot reload)
yarn build       # Compile to dist/
yarn start       # Run compiled dist/index.js

# Mobile
cd apps/mobile
yarn start       # Expo dev server
yarn typecheck   # tsc --noEmit

Commands

All commands are sent over WebSocket on the command event. The daemon validates them with Zod before dispatch.

Command Payload Description
volume:set { value: 0–100 } Set output volume
volume:mute Mute audio
volume:unmute Unmute audio
volume:get Get current volume
brightness:set { value: 0–100 } Set display brightness
brightness:get Get current brightness
display:lock Lock the screen
display:wake Wake the display
media:playpause Toggle play/pause
media:next Next track
media:previous Previous track
status:get Request current status

Events

Event Direction Description
command Client → Daemon Send a command
ack Daemon → Client Command response (success or error)
status Daemon → Client System status, broadcast every 3 seconds
error Daemon → Client Unhandled error

Status payload

{
  volume: number;     // 0–100
  muted: boolean;
  brightness: number; // 0–100
  battery: number;    // 0–100
  charging: boolean;
  timestamp: number;  // ms since epoch
}

Authentication

The daemon generates a random UUID token on first run and saves it to ~/.jack/config.json. The mobile app must supply this token in the Socket.IO handshake:

io(url, { auth: { token } })

Token comparison uses constant-time XOR to prevent timing attacks. Invalid tokens are rejected before the WebSocket connection is established.

Credentials (host, port, token) are stored on the device using expo-secure-store, which uses the iOS Keychain on iOS and Android KeyStore on Android.


Architecture Notes

  • Transport: WebSocket only (no HTTP long-polling). The daemon uses socket.io 4.7 with transports: ['websocket'].
  • Discovery: mDNS service type _jack._tcp.local, advertised on the daemon's port with a version=2.0.0 TXT record.
  • Media controls: The media_helper Swift binary talks directly to mediaserverd via MediaRemote.framework (private API), so no Accessibility permission is needed for play/pause/next/prev.
  • Brightness: The brightness_helper Swift binary uses the DisplayServices private framework. It accepts a float 0.0–1.0 and returns the current level.
  • Display lock: Uses osascript to send Control+Command+Q, with a fallback to pmset displaysleepnow.
  • LaunchAgent: The daemon is managed by launchd via ~/Library/LaunchAgents/com.jack.daemon.plist. It starts on login and restarts automatically on crash.

Configuration Reference

Setting Location Default
Token ~/.jack/config.json Auto-generated UUID
Port ~/.jack/config.json 9521
mDNS service name src/mdns.ts Jack Mac Daemon
mDNS service type src/mdns.ts _jack._tcp.local
Status poll interval src/server.ts 3000ms
Socket timeout (mobile) src/context/SocketContext.tsx 8000ms
Reconnect attempts (mobile) src/context/SocketContext.tsx 5
Daemon stdout log LaunchAgent plist /tmp/jack.daemon.log
Daemon stderr log LaunchAgent plist /tmp/jack.daemon.error.log

License

MIT

About

A utility tool to help control my Mac

Resources

License

Code of conduct

Contributing

Stars

Watchers

Forks

Releases

No releases published

Sponsor this project

Packages

 
 
 

Contributors