macbot-cli 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,222 @@
1
+ """WiFi control commands."""
2
+
3
+ import subprocess
4
+ import re
5
+
6
+ import click
7
+
8
+ from ..context import pass_context
9
+
10
+ # System utilities paths
11
+ NETWORKSETUP = "/usr/sbin/networksetup"
12
+ IPCONFIG = "/usr/sbin/ipconfig"
13
+ AIRPORT = "/System/Library/PrivateFrameworks/Apple80211.framework/Versions/Current/Resources/airport"
14
+
15
+
16
+ def get_wifi_interface() -> str:
17
+ """Get the WiFi interface name (usually en0)."""
18
+ try:
19
+ result = subprocess.run(
20
+ [NETWORKSETUP, "-listallhardwareports"],
21
+ capture_output=True,
22
+ text=True,
23
+ check=True,
24
+ )
25
+ lines = result.stdout.split("\n")
26
+ for i, line in enumerate(lines):
27
+ if "Wi-Fi" in line or "AirPort" in line:
28
+ for j in range(i, min(i + 3, len(lines))):
29
+ if "Device:" in lines[j]:
30
+ return lines[j].split(":")[1].strip()
31
+ except Exception:
32
+ pass
33
+ return "en0" # Default
34
+
35
+
36
+ @click.group()
37
+ def wifi():
38
+ """WiFi status and control."""
39
+ pass
40
+
41
+
42
+ @wifi.command("status")
43
+ @pass_context
44
+ def wifi_status(ctx):
45
+ """Get WiFi status and connected network.
46
+
47
+ Example:
48
+ macbot wifi status
49
+ """
50
+ interface = get_wifi_interface()
51
+
52
+ try:
53
+ # Get power state
54
+ power_result = subprocess.run(
55
+ [NETWORKSETUP, "-getairportpower", interface],
56
+ capture_output=True,
57
+ text=True,
58
+ )
59
+ power_on = "On" in power_result.stdout
60
+
61
+ # Get current network
62
+ network_result = subprocess.run(
63
+ [NETWORKSETUP, "-getairportnetwork", interface],
64
+ capture_output=True,
65
+ text=True,
66
+ )
67
+ network = None
68
+ if "Current Wi-Fi Network:" in network_result.stdout:
69
+ network = network_result.stdout.split(":")[1].strip()
70
+ elif "You are not associated" in network_result.stdout:
71
+ network = None
72
+
73
+ # Get IP address
74
+ ip_result = subprocess.run(
75
+ [IPCONFIG, "getifaddr", interface],
76
+ capture_output=True,
77
+ text=True,
78
+ )
79
+ ip_address = ip_result.stdout.strip() if ip_result.returncode == 0 else None
80
+
81
+ ctx.output({
82
+ "interface": interface,
83
+ "power": "on" if power_on else "off",
84
+ "network": network,
85
+ "ip_address": ip_address,
86
+ "connected": network is not None,
87
+ })
88
+ except subprocess.CalledProcessError as e:
89
+ ctx.error(f"Failed to get WiFi status: {e.stderr}")
90
+
91
+
92
+ @wifi.command("on")
93
+ @pass_context
94
+ def wifi_on(ctx):
95
+ """Turn WiFi on.
96
+
97
+ Example:
98
+ macbot wifi on
99
+ """
100
+ interface = get_wifi_interface()
101
+ try:
102
+ subprocess.run(
103
+ [NETWORKSETUP, "-setairportpower", interface, "on"],
104
+ check=True,
105
+ capture_output=True,
106
+ text=True,
107
+ )
108
+ ctx.output({"wifi": "on", "interface": interface})
109
+ except subprocess.CalledProcessError as e:
110
+ ctx.error(f"Failed to turn WiFi on: {e.stderr}")
111
+
112
+
113
+ @wifi.command("off")
114
+ @pass_context
115
+ def wifi_off(ctx):
116
+ """Turn WiFi off.
117
+
118
+ Example:
119
+ macbot wifi off
120
+ """
121
+ interface = get_wifi_interface()
122
+ try:
123
+ subprocess.run(
124
+ [NETWORKSETUP, "-setairportpower", interface, "off"],
125
+ check=True,
126
+ capture_output=True,
127
+ text=True,
128
+ )
129
+ ctx.output({"wifi": "off", "interface": interface})
130
+ except subprocess.CalledProcessError as e:
131
+ ctx.error(f"Failed to turn WiFi off: {e.stderr}")
132
+
133
+
134
+ @wifi.command("networks")
135
+ @pass_context
136
+ def wifi_networks(ctx):
137
+ """List available WiFi networks.
138
+
139
+ Example:
140
+ macbot wifi networks
141
+ """
142
+ interface = get_wifi_interface()
143
+
144
+ try:
145
+ result = subprocess.run(
146
+ [AIRPORT, "-s"],
147
+ capture_output=True,
148
+ text=True,
149
+ check=True,
150
+ )
151
+
152
+ networks = []
153
+ lines = result.stdout.strip().split("\n")
154
+ if len(lines) > 1: # Skip header
155
+ for line in lines[1:]:
156
+ parts = line.split()
157
+ if len(parts) >= 2:
158
+ # SSID may have spaces, RSSI is usually negative number
159
+ # Find the RSSI (negative number)
160
+ for i, part in enumerate(parts):
161
+ if part.startswith("-") and part[1:].isdigit():
162
+ ssid = " ".join(parts[:i])
163
+ rssi = int(part)
164
+ networks.append({
165
+ "ssid": ssid.strip(),
166
+ "rssi": rssi,
167
+ "signal": "excellent" if rssi > -50 else "good" if rssi > -70 else "fair" if rssi > -80 else "weak"
168
+ })
169
+ break
170
+
171
+ ctx.output({"networks": networks, "count": len(networks)})
172
+ except subprocess.CalledProcessError as e:
173
+ ctx.error(f"Failed to scan networks: {e.stderr}")
174
+ except FileNotFoundError:
175
+ ctx.error("airport command not found")
176
+
177
+
178
+ @wifi.command("connect")
179
+ @click.argument("ssid")
180
+ @click.option("--password", "-p", help="WiFi password")
181
+ @pass_context
182
+ def wifi_connect(ctx, ssid: str, password: str | None):
183
+ """Connect to a WiFi network.
184
+
185
+ Example:
186
+ macbot wifi connect "MyNetwork" --password "secret123"
187
+ """
188
+ interface = get_wifi_interface()
189
+
190
+ cmd = [NETWORKSETUP, "-setairportnetwork", interface, ssid]
191
+ if password:
192
+ cmd.append(password)
193
+
194
+ try:
195
+ subprocess.run(cmd, check=True, capture_output=True, text=True)
196
+ ctx.output({"ssid": ssid, "connected": True})
197
+ except subprocess.CalledProcessError as e:
198
+ ctx.error(f"Failed to connect: {e.stderr}")
199
+
200
+
201
+ @wifi.command("disconnect")
202
+ @pass_context
203
+ def wifi_disconnect(ctx):
204
+ """Disconnect from current WiFi network.
205
+
206
+ Example:
207
+ macbot wifi disconnect
208
+ """
209
+ interface = get_wifi_interface()
210
+
211
+ try:
212
+ subprocess.run(
213
+ [AIRPORT, "-z"],
214
+ check=True,
215
+ capture_output=True,
216
+ text=True,
217
+ )
218
+ ctx.output({"disconnected": True, "interface": interface})
219
+ except subprocess.CalledProcessError as e:
220
+ ctx.error(f"Failed to disconnect: {e.stderr}")
221
+ except FileNotFoundError:
222
+ ctx.error("airport command not found")
@@ -0,0 +1,234 @@
1
+ """Window management commands."""
2
+
3
+ import subprocess
4
+ import json as json_module
5
+
6
+ import click
7
+
8
+ from ..context import pass_context
9
+
10
+
11
+ @click.group()
12
+ def window():
13
+ """Window list/focus/move operations."""
14
+ pass
15
+
16
+
17
+ @window.command("list")
18
+ @click.option("--app", "-a", help="Filter by application name")
19
+ @pass_context
20
+ def window_list(ctx, app: str | None):
21
+ """List all windows.
22
+
23
+ Example:
24
+ macbot window list
25
+ macbot window list --app Safari
26
+ """
27
+ script = '''
28
+ set windowList to {}
29
+ tell application "System Events"
30
+ set allProcesses to every process whose visible is true
31
+ repeat with proc in allProcesses
32
+ set procName to name of proc
33
+ try
34
+ set procWindows to every window of proc
35
+ repeat with w in procWindows
36
+ set windowName to name of w
37
+ set windowPos to position of w
38
+ set windowSize to size of w
39
+ set end of windowList to procName & "|||" & windowName & "|||" & (item 1 of windowPos as text) & "," & (item 2 of windowPos as text) & "|||" & (item 1 of windowSize as text) & "," & (item 2 of windowSize as text)
40
+ end repeat
41
+ end try
42
+ end repeat
43
+ end tell
44
+ set AppleScript's text item delimiters to ":::"
45
+ return windowList as text
46
+ '''
47
+
48
+ try:
49
+ result = subprocess.run(
50
+ ["osascript", "-e", script],
51
+ capture_output=True,
52
+ text=True,
53
+ check=True,
54
+ )
55
+
56
+ windows = []
57
+ if result.stdout.strip():
58
+ for item in result.stdout.strip().split(":::"):
59
+ if "|||" in item:
60
+ parts = item.split("|||")
61
+ if len(parts) >= 4:
62
+ pos = parts[2].split(",")
63
+ size = parts[3].split(",")
64
+ window_info = {
65
+ "app": parts[0],
66
+ "title": parts[1],
67
+ "x": int(pos[0]) if pos[0].strip().lstrip("-").isdigit() else 0,
68
+ "y": int(pos[1]) if pos[1].strip().lstrip("-").isdigit() else 0,
69
+ "width": int(size[0]) if size[0].strip().isdigit() else 0,
70
+ "height": int(size[1]) if size[1].strip().isdigit() else 0,
71
+ }
72
+ if app is None or app.lower() in window_info["app"].lower():
73
+ windows.append(window_info)
74
+
75
+ ctx.output({"windows": windows, "count": len(windows)})
76
+ except subprocess.CalledProcessError as e:
77
+ ctx.error(f"Failed to list windows: {e.stderr}")
78
+
79
+
80
+ @window.command("focus")
81
+ @click.argument("app_name")
82
+ @click.option("--title", "-t", help="Window title to focus (partial match)")
83
+ @pass_context
84
+ def window_focus(ctx, app_name: str, title: str | None):
85
+ """Focus an application window.
86
+
87
+ Example:
88
+ macbot window focus Safari
89
+ macbot window focus "Visual Studio Code" --title "project"
90
+ """
91
+ if title:
92
+ script = f'''
93
+ tell application "System Events"
94
+ tell process "{app_name}"
95
+ set frontmost to true
96
+ repeat with w in windows
97
+ if name of w contains "{title}" then
98
+ perform action "AXRaise" of w
99
+ return "focused"
100
+ end if
101
+ end repeat
102
+ end tell
103
+ end tell
104
+ return "not found"
105
+ '''
106
+ else:
107
+ script = f'''
108
+ tell application "{app_name}"
109
+ activate
110
+ end tell
111
+ return "focused"
112
+ '''
113
+
114
+ try:
115
+ result = subprocess.run(
116
+ ["osascript", "-e", script],
117
+ capture_output=True,
118
+ text=True,
119
+ check=True,
120
+ )
121
+ status = result.stdout.strip()
122
+ if status == "not found":
123
+ ctx.error(f"Window not found: {title}")
124
+ ctx.output({"app": app_name, "title": title, "focused": True})
125
+ except subprocess.CalledProcessError as e:
126
+ ctx.error(f"Failed to focus window: {e.stderr}")
127
+
128
+
129
+ @window.command("move")
130
+ @click.argument("app_name")
131
+ @click.option("--x", type=int, help="X position")
132
+ @click.option("--y", type=int, help="Y position")
133
+ @click.option("--width", "-w", type=int, help="Window width")
134
+ @click.option("--height", "-h", type=int, help="Window height")
135
+ @click.option("--title", "-t", help="Window title (partial match)")
136
+ @pass_context
137
+ def window_move(
138
+ ctx,
139
+ app_name: str,
140
+ x: int | None,
141
+ y: int | None,
142
+ width: int | None,
143
+ height: int | None,
144
+ title: str | None,
145
+ ):
146
+ """Move/resize an application window.
147
+
148
+ Example:
149
+ macbot window move Safari --x 0 --y 0 --width 1200 --height 800
150
+ """
151
+ script_parts = [f'tell application "System Events"', f'tell process "{app_name}"']
152
+
153
+ if title:
154
+ script_parts.append(f'''
155
+ repeat with w in windows
156
+ if name of w contains "{title}" then
157
+ set targetWindow to w
158
+ exit repeat
159
+ end if
160
+ end repeat
161
+ ''')
162
+ window_ref = "targetWindow"
163
+ else:
164
+ window_ref = "front window"
165
+
166
+ if x is not None and y is not None:
167
+ script_parts.append(f"set position of {window_ref} to {{{x}, {y}}}")
168
+
169
+ if width is not None and height is not None:
170
+ script_parts.append(f"set size of {window_ref} to {{{width}, {height}}}")
171
+
172
+ script_parts.extend(["end tell", "end tell"])
173
+ script = "\n".join(script_parts)
174
+
175
+ try:
176
+ subprocess.run(
177
+ ["osascript", "-e", script],
178
+ capture_output=True,
179
+ text=True,
180
+ check=True,
181
+ )
182
+ ctx.output({
183
+ "app": app_name,
184
+ "title": title,
185
+ "x": x,
186
+ "y": y,
187
+ "width": width,
188
+ "height": height,
189
+ "moved": True,
190
+ })
191
+ except subprocess.CalledProcessError as e:
192
+ ctx.error(f"Failed to move window: {e.stderr}")
193
+
194
+
195
+ @window.command("minimize")
196
+ @click.argument("app_name")
197
+ @click.option("--all", "minimize_all", is_flag=True, help="Minimize all windows")
198
+ @pass_context
199
+ def window_minimize(ctx, app_name: str, minimize_all: bool):
200
+ """Minimize application window(s).
201
+
202
+ Example:
203
+ macbot window minimize Safari
204
+ macbot window minimize Safari --all
205
+ """
206
+ if minimize_all:
207
+ script = f'''
208
+ tell application "System Events"
209
+ tell process "{app_name}"
210
+ repeat with w in windows
211
+ set value of attribute "AXMinimized" of w to true
212
+ end repeat
213
+ end tell
214
+ end tell
215
+ '''
216
+ else:
217
+ script = f'''
218
+ tell application "System Events"
219
+ tell process "{app_name}"
220
+ set value of attribute "AXMinimized" of front window to true
221
+ end tell
222
+ end tell
223
+ '''
224
+
225
+ try:
226
+ subprocess.run(
227
+ ["osascript", "-e", script],
228
+ capture_output=True,
229
+ text=True,
230
+ check=True,
231
+ )
232
+ ctx.output({"app": app_name, "all": minimize_all, "minimized": True})
233
+ except subprocess.CalledProcessError as e:
234
+ ctx.error(f"Failed to minimize window: {e.stderr}")
macbot/context.py ADDED
@@ -0,0 +1,47 @@
1
+ """Shared context for macbot commands."""
2
+
3
+ import json
4
+ import sys
5
+
6
+ import click
7
+
8
+
9
+ class Context:
10
+ """Shared context for all commands."""
11
+
12
+ def __init__(self, json_output: bool = False):
13
+ self.json_output = json_output
14
+
15
+ def output(self, data: dict | list | str, success: bool = True):
16
+ """Output data in the appropriate format."""
17
+ if self.json_output:
18
+ if isinstance(data, str):
19
+ data = {"message": data, "success": success}
20
+ elif isinstance(data, (dict, list)):
21
+ if isinstance(data, dict) and "success" not in data:
22
+ data = {**data, "success": success}
23
+ click.echo(json.dumps(data, indent=2, default=str))
24
+ else:
25
+ if isinstance(data, dict):
26
+ for key, value in data.items():
27
+ if key != "success":
28
+ click.echo(f"{key}: {value}")
29
+ elif isinstance(data, list):
30
+ for item in data:
31
+ if isinstance(item, dict):
32
+ click.echo(" | ".join(f"{k}: {v}" for k, v in item.items()))
33
+ else:
34
+ click.echo(item)
35
+ else:
36
+ click.echo(data)
37
+
38
+ def error(self, message: str, exit_code: int = 1):
39
+ """Output an error message and exit."""
40
+ if self.json_output:
41
+ click.echo(json.dumps({"error": message, "success": False}))
42
+ else:
43
+ click.echo(f"Error: {message}", err=True)
44
+ sys.exit(exit_code)
45
+
46
+
47
+ pass_context = click.make_pass_decorator(Context, ensure=True)
@@ -0,0 +1,210 @@
1
+ Metadata-Version: 2.4
2
+ Name: macbot-cli
3
+ Version: 0.1.0
4
+ Summary: macOS automation CLI for AI agents and developers
5
+ Project-URL: Homepage, https://github.com/marcusbuildsthings-droid/macbot
6
+ Project-URL: Repository, https://github.com/marcusbuildsthings-droid/macbot
7
+ Project-URL: Issues, https://github.com/marcusbuildsthings-droid/macbot/issues
8
+ Author-email: Marcus <marcus.builds.things@gmail.com>
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: ai-agent,applescript,automation,cli,macos
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Environment :: Console
14
+ Classifier: Environment :: MacOS X
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Operating System :: MacOS
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3.9
20
+ Classifier: Programming Language :: Python :: 3.10
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Topic :: System :: Systems Administration
24
+ Classifier: Topic :: Utilities
25
+ Requires-Python: >=3.9
26
+ Requires-Dist: click>=8.0
27
+ Requires-Dist: rich>=13.0
28
+ Provides-Extra: dev
29
+ Requires-Dist: black>=23.0; extra == 'dev'
30
+ Requires-Dist: pytest-cov>=4.0; extra == 'dev'
31
+ Requires-Dist: pytest>=7.0; extra == 'dev'
32
+ Requires-Dist: ruff>=0.1.0; extra == 'dev'
33
+ Description-Content-Type: text/markdown
34
+
35
+ # macbot
36
+
37
+ macOS automation CLI for AI agents and developers. Control notifications, clipboard, windows, audio, display, and more from the command line with optional JSON output.
38
+
39
+ ## Installation
40
+
41
+ ```bash
42
+ pip install macbot-cli
43
+ ```
44
+
45
+ ## Quick Start
46
+
47
+ ```bash
48
+ # Send a notification
49
+ macbot notify "Build Complete" "All tests passed"
50
+
51
+ # Get/set clipboard
52
+ macbot clipboard get
53
+ macbot clipboard set "Hello, World!"
54
+
55
+ # Text-to-speech
56
+ macbot say "Hello from macbot" --voice Daniel
57
+
58
+ # Take a screenshot
59
+ macbot screenshot ~/Desktop/screen.png
60
+
61
+ # Volume control
62
+ macbot audio volume 50
63
+ macbot audio mute
64
+
65
+ # WiFi control
66
+ macbot wifi status
67
+ macbot wifi networks
68
+ ```
69
+
70
+ ## Commands
71
+
72
+ ### Notifications
73
+ ```bash
74
+ macbot notify "Title" "Message"
75
+ macbot notify "Alert" --subtitle "Warning" --sound Ping
76
+ ```
77
+
78
+ ### Clipboard
79
+ ```bash
80
+ macbot clipboard get # Get contents
81
+ macbot clipboard set "text" # Set contents
82
+ macbot clipboard clear # Clear clipboard
83
+ ```
84
+
85
+ ### Text-to-Speech
86
+ ```bash
87
+ macbot say "Hello" # Default voice
88
+ macbot say "British" --voice Daniel # Specific voice
89
+ macbot say "Fast" --rate 300 # Faster speech
90
+ macbot say "Save" --output speech.aiff # Save to file
91
+ macbot say "" --list-voices # List voices
92
+ ```
93
+
94
+ ### Screenshots
95
+ ```bash
96
+ macbot screenshot # Desktop with timestamp
97
+ macbot screenshot screen.png # Specific file
98
+ macbot screenshot --clipboard # To clipboard
99
+ macbot screenshot --window # Front window
100
+ macbot screenshot --interactive # Select region
101
+ macbot screenshot --delay 3 # 3 second delay
102
+ ```
103
+
104
+ ### Window Management
105
+ ```bash
106
+ macbot window list # List all windows
107
+ macbot window list --app Safari # Filter by app
108
+ macbot window focus Safari # Focus app
109
+ macbot window focus "VS Code" -t project # Focus specific window
110
+ macbot window move Safari -x 0 -y 0 -w 1200 -h 800
111
+ macbot window minimize Safari
112
+ ```
113
+
114
+ ### Applications
115
+ ```bash
116
+ macbot apps list # All installed apps
117
+ macbot apps list --running # Running apps only
118
+ macbot apps launch Safari # Launch app
119
+ macbot apps launch Safari -b # Launch in background
120
+ macbot apps quit Safari # Quit app
121
+ macbot apps quit "Frozen" --force # Force quit
122
+ macbot apps hide Safari # Hide app
123
+ macbot apps show Safari # Show/unhide app
124
+ ```
125
+
126
+ ### Audio
127
+ ```bash
128
+ macbot audio volume # Get volume
129
+ macbot audio volume 50 # Set to 50%
130
+ macbot audio mute # Mute
131
+ macbot audio mute --toggle # Toggle mute
132
+ macbot audio unmute # Unmute
133
+ macbot audio devices # List devices
134
+ macbot audio output "Speakers" # Set output device
135
+ macbot audio input "Microphone" # Set input device
136
+ ```
137
+
138
+ ### Display Brightness
139
+ ```bash
140
+ macbot brightness # Get brightness
141
+ macbot brightness 0.7 # Set to 70%
142
+ macbot brightness 0.5 -d 1 # Set external display
143
+ ```
144
+
145
+ Requires: `brew install brightness`
146
+
147
+ ### WiFi
148
+ ```bash
149
+ macbot wifi status # Status and IP
150
+ macbot wifi on # Turn on
151
+ macbot wifi off # Turn off
152
+ macbot wifi networks # List available
153
+ macbot wifi connect "SSID" --password "pass"
154
+ macbot wifi disconnect
155
+ ```
156
+
157
+ ### Bluetooth
158
+ ```bash
159
+ macbot bluetooth status # Power state
160
+ macbot bluetooth on # Turn on
161
+ macbot bluetooth off # Turn off
162
+ macbot bluetooth devices # List devices
163
+ macbot bluetooth devices --connected # Connected only
164
+ macbot bluetooth connect XX-XX-XX-XX-XX-XX
165
+ macbot bluetooth disconnect XX-XX-XX-XX-XX-XX
166
+ ```
167
+
168
+ Requires: `brew install blueutil`
169
+
170
+ ## JSON Output
171
+
172
+ All commands support `--json` for machine-readable output:
173
+
174
+ ```bash
175
+ macbot --json wifi status
176
+ ```
177
+
178
+ ```json
179
+ {
180
+ "interface": "en0",
181
+ "power": "on",
182
+ "network": "MyNetwork",
183
+ "ip_address": "192.168.1.100",
184
+ "connected": true,
185
+ "success": true
186
+ }
187
+ ```
188
+
189
+ ## Optional Dependencies
190
+
191
+ Some commands require additional tools:
192
+
193
+ ```bash
194
+ # Display brightness
195
+ brew install brightness
196
+
197
+ # Bluetooth control
198
+ brew install blueutil
199
+
200
+ # Audio device switching
201
+ brew install switchaudio-osx
202
+ ```
203
+
204
+ ## For AI Agents
205
+
206
+ See [SKILL.md](SKILL.md) for agent-optimized documentation.
207
+
208
+ ## License
209
+
210
+ MIT