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.
- macbot/__init__.py +3 -0
- macbot/cli.py +48 -0
- macbot/commands/__init__.py +1 -0
- macbot/commands/apps.py +187 -0
- macbot/commands/audio.py +252 -0
- macbot/commands/bluetooth.py +261 -0
- macbot/commands/brightness.py +104 -0
- macbot/commands/clipboard.py +75 -0
- macbot/commands/notify.py +50 -0
- macbot/commands/say.py +94 -0
- macbot/commands/screenshot.py +109 -0
- macbot/commands/wifi.py +222 -0
- macbot/commands/window.py +234 -0
- macbot/context.py +47 -0
- macbot_cli-0.1.0.dist-info/METADATA +210 -0
- macbot_cli-0.1.0.dist-info/RECORD +19 -0
- macbot_cli-0.1.0.dist-info/WHEEL +4 -0
- macbot_cli-0.1.0.dist-info/entry_points.txt +2 -0
- macbot_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
macbot/commands/wifi.py
ADDED
|
@@ -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
|