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/__init__.py
ADDED
macbot/cli.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Main CLI entry point for macbot."""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
from . import __version__
|
|
6
|
+
from .context import Context
|
|
7
|
+
from .commands import (
|
|
8
|
+
apps,
|
|
9
|
+
audio,
|
|
10
|
+
bluetooth,
|
|
11
|
+
brightness,
|
|
12
|
+
clipboard,
|
|
13
|
+
notify,
|
|
14
|
+
screenshot,
|
|
15
|
+
say,
|
|
16
|
+
wifi,
|
|
17
|
+
window,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@click.group()
|
|
22
|
+
@click.option("--json", "json_output", is_flag=True, help="Output in JSON format")
|
|
23
|
+
@click.version_option(version=__version__, prog_name="macbot")
|
|
24
|
+
@click.pass_context
|
|
25
|
+
def main(ctx, json_output: bool):
|
|
26
|
+
"""macOS automation CLI for AI agents and developers.
|
|
27
|
+
|
|
28
|
+
Control notifications, clipboard, windows, audio, display, and more
|
|
29
|
+
from the command line with optional JSON output for automation.
|
|
30
|
+
"""
|
|
31
|
+
ctx.obj = Context(json_output=json_output)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# Register command groups
|
|
35
|
+
main.add_command(notify.notify)
|
|
36
|
+
main.add_command(clipboard.clipboard)
|
|
37
|
+
main.add_command(window.window)
|
|
38
|
+
main.add_command(say.say)
|
|
39
|
+
main.add_command(screenshot.screenshot)
|
|
40
|
+
main.add_command(apps.apps)
|
|
41
|
+
main.add_command(audio.audio)
|
|
42
|
+
main.add_command(brightness.brightness)
|
|
43
|
+
main.add_command(wifi.wifi)
|
|
44
|
+
main.add_command(bluetooth.bluetooth)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
if __name__ == "__main__":
|
|
48
|
+
main()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""macbot commands."""
|
macbot/commands/apps.py
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"""Application 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 apps():
|
|
13
|
+
"""Application list/launch/quit operations."""
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@apps.command("list")
|
|
18
|
+
@click.option("--running", "-r", is_flag=True, help="Only show running apps")
|
|
19
|
+
@pass_context
|
|
20
|
+
def apps_list(ctx, running: bool):
|
|
21
|
+
"""List applications.
|
|
22
|
+
|
|
23
|
+
Example:
|
|
24
|
+
macbot apps list
|
|
25
|
+
macbot apps list --running
|
|
26
|
+
"""
|
|
27
|
+
if running:
|
|
28
|
+
script = '''
|
|
29
|
+
set appList to {}
|
|
30
|
+
tell application "System Events"
|
|
31
|
+
set allProcesses to every process whose background only is false
|
|
32
|
+
repeat with proc in allProcesses
|
|
33
|
+
set end of appList to name of proc
|
|
34
|
+
end repeat
|
|
35
|
+
end tell
|
|
36
|
+
set AppleScript's text item delimiters to ":::"
|
|
37
|
+
return appList as text
|
|
38
|
+
'''
|
|
39
|
+
try:
|
|
40
|
+
result = subprocess.run(
|
|
41
|
+
["osascript", "-e", script],
|
|
42
|
+
capture_output=True,
|
|
43
|
+
text=True,
|
|
44
|
+
check=True,
|
|
45
|
+
)
|
|
46
|
+
apps_raw = result.stdout.strip().split(":::")
|
|
47
|
+
apps_list = [{"name": app, "running": True} for app in apps_raw if app.strip()]
|
|
48
|
+
ctx.output({"apps": apps_list, "count": len(apps_list)})
|
|
49
|
+
except subprocess.CalledProcessError as e:
|
|
50
|
+
ctx.error(f"Failed to list apps: {e.stderr}")
|
|
51
|
+
else:
|
|
52
|
+
# List installed applications
|
|
53
|
+
try:
|
|
54
|
+
result = subprocess.run(
|
|
55
|
+
["mdfind", "kMDItemContentType == 'com.apple.application-bundle'"],
|
|
56
|
+
capture_output=True,
|
|
57
|
+
text=True,
|
|
58
|
+
check=True,
|
|
59
|
+
)
|
|
60
|
+
apps_list = []
|
|
61
|
+
for path in result.stdout.strip().split("\n"):
|
|
62
|
+
if path.strip():
|
|
63
|
+
name = path.split("/")[-1].replace(".app", "")
|
|
64
|
+
apps_list.append({"name": name, "path": path})
|
|
65
|
+
|
|
66
|
+
# Sort by name
|
|
67
|
+
apps_list.sort(key=lambda x: x["name"].lower())
|
|
68
|
+
ctx.output({"apps": apps_list, "count": len(apps_list)})
|
|
69
|
+
except subprocess.CalledProcessError as e:
|
|
70
|
+
ctx.error(f"Failed to list apps: {e.stderr}")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@apps.command("launch")
|
|
74
|
+
@click.argument("app_name")
|
|
75
|
+
@click.option("--background", "-b", is_flag=True, help="Launch in background")
|
|
76
|
+
@pass_context
|
|
77
|
+
def apps_launch(ctx, app_name: str, background: bool):
|
|
78
|
+
"""Launch an application.
|
|
79
|
+
|
|
80
|
+
Example:
|
|
81
|
+
macbot apps launch Safari
|
|
82
|
+
macbot apps launch "Visual Studio Code" --background
|
|
83
|
+
"""
|
|
84
|
+
cmd = ["open", "-a", app_name]
|
|
85
|
+
if background:
|
|
86
|
+
cmd.append("-g")
|
|
87
|
+
|
|
88
|
+
try:
|
|
89
|
+
subprocess.run(cmd, check=True, capture_output=True, text=True)
|
|
90
|
+
ctx.output({"app": app_name, "launched": True, "background": background})
|
|
91
|
+
except subprocess.CalledProcessError as e:
|
|
92
|
+
ctx.error(f"Failed to launch app: {e.stderr}")
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@apps.command("quit")
|
|
96
|
+
@click.argument("app_name")
|
|
97
|
+
@click.option("--force", "-f", is_flag=True, help="Force quit")
|
|
98
|
+
@pass_context
|
|
99
|
+
def apps_quit(ctx, app_name: str, force: bool):
|
|
100
|
+
"""Quit an application.
|
|
101
|
+
|
|
102
|
+
Example:
|
|
103
|
+
macbot apps quit Safari
|
|
104
|
+
macbot apps quit "Frozen App" --force
|
|
105
|
+
"""
|
|
106
|
+
if force:
|
|
107
|
+
try:
|
|
108
|
+
subprocess.run(
|
|
109
|
+
["pkill", "-9", "-f", app_name],
|
|
110
|
+
check=True,
|
|
111
|
+
capture_output=True,
|
|
112
|
+
text=True,
|
|
113
|
+
)
|
|
114
|
+
ctx.output({"app": app_name, "quit": True, "force": True})
|
|
115
|
+
except subprocess.CalledProcessError:
|
|
116
|
+
ctx.error(f"App not running or failed to force quit: {app_name}")
|
|
117
|
+
else:
|
|
118
|
+
script = f'''
|
|
119
|
+
tell application "{app_name}"
|
|
120
|
+
quit
|
|
121
|
+
end tell
|
|
122
|
+
'''
|
|
123
|
+
try:
|
|
124
|
+
subprocess.run(
|
|
125
|
+
["osascript", "-e", script],
|
|
126
|
+
check=True,
|
|
127
|
+
capture_output=True,
|
|
128
|
+
text=True,
|
|
129
|
+
)
|
|
130
|
+
ctx.output({"app": app_name, "quit": True, "force": False})
|
|
131
|
+
except subprocess.CalledProcessError as e:
|
|
132
|
+
ctx.error(f"Failed to quit app: {e.stderr}")
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@apps.command("hide")
|
|
136
|
+
@click.argument("app_name")
|
|
137
|
+
@pass_context
|
|
138
|
+
def apps_hide(ctx, app_name: str):
|
|
139
|
+
"""Hide an application.
|
|
140
|
+
|
|
141
|
+
Example:
|
|
142
|
+
macbot apps hide Safari
|
|
143
|
+
"""
|
|
144
|
+
script = f'''
|
|
145
|
+
tell application "System Events"
|
|
146
|
+
set visible of process "{app_name}" to false
|
|
147
|
+
end tell
|
|
148
|
+
'''
|
|
149
|
+
try:
|
|
150
|
+
subprocess.run(
|
|
151
|
+
["osascript", "-e", script],
|
|
152
|
+
check=True,
|
|
153
|
+
capture_output=True,
|
|
154
|
+
text=True,
|
|
155
|
+
)
|
|
156
|
+
ctx.output({"app": app_name, "hidden": True})
|
|
157
|
+
except subprocess.CalledProcessError as e:
|
|
158
|
+
ctx.error(f"Failed to hide app: {e.stderr}")
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
@apps.command("show")
|
|
162
|
+
@click.argument("app_name")
|
|
163
|
+
@pass_context
|
|
164
|
+
def apps_show(ctx, app_name: str):
|
|
165
|
+
"""Show/unhide an application.
|
|
166
|
+
|
|
167
|
+
Example:
|
|
168
|
+
macbot apps show Safari
|
|
169
|
+
"""
|
|
170
|
+
script = f'''
|
|
171
|
+
tell application "System Events"
|
|
172
|
+
set visible of process "{app_name}" to true
|
|
173
|
+
end tell
|
|
174
|
+
tell application "{app_name}"
|
|
175
|
+
activate
|
|
176
|
+
end tell
|
|
177
|
+
'''
|
|
178
|
+
try:
|
|
179
|
+
subprocess.run(
|
|
180
|
+
["osascript", "-e", script],
|
|
181
|
+
check=True,
|
|
182
|
+
capture_output=True,
|
|
183
|
+
text=True,
|
|
184
|
+
)
|
|
185
|
+
ctx.output({"app": app_name, "shown": True})
|
|
186
|
+
except subprocess.CalledProcessError as e:
|
|
187
|
+
ctx.error(f"Failed to show app: {e.stderr}")
|
macbot/commands/audio.py
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
"""Audio/volume control commands."""
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
import re
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
|
|
8
|
+
from ..context import pass_context
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@click.group()
|
|
12
|
+
def audio():
|
|
13
|
+
"""Volume and audio device control."""
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@audio.command("volume")
|
|
18
|
+
@click.argument("level", required=False, type=int)
|
|
19
|
+
@pass_context
|
|
20
|
+
def audio_volume(ctx, level: int | None):
|
|
21
|
+
"""Get or set system volume (0-100).
|
|
22
|
+
|
|
23
|
+
Example:
|
|
24
|
+
macbot audio volume # Get current volume
|
|
25
|
+
macbot audio volume 50 # Set to 50%
|
|
26
|
+
"""
|
|
27
|
+
if level is not None:
|
|
28
|
+
# Set volume
|
|
29
|
+
if level < 0 or level > 100:
|
|
30
|
+
ctx.error("Volume must be between 0 and 100")
|
|
31
|
+
|
|
32
|
+
# macOS volume is 0-7 in AppleScript, convert from 0-100
|
|
33
|
+
as_volume = int(level * 7 / 100)
|
|
34
|
+
script = f'set volume output volume {level}'
|
|
35
|
+
|
|
36
|
+
try:
|
|
37
|
+
subprocess.run(
|
|
38
|
+
["osascript", "-e", script],
|
|
39
|
+
check=True,
|
|
40
|
+
capture_output=True,
|
|
41
|
+
text=True,
|
|
42
|
+
)
|
|
43
|
+
ctx.output({"volume": level, "set": True})
|
|
44
|
+
except subprocess.CalledProcessError as e:
|
|
45
|
+
ctx.error(f"Failed to set volume: {e.stderr}")
|
|
46
|
+
else:
|
|
47
|
+
# Get volume
|
|
48
|
+
script = 'output volume of (get volume settings)'
|
|
49
|
+
try:
|
|
50
|
+
result = subprocess.run(
|
|
51
|
+
["osascript", "-e", script],
|
|
52
|
+
capture_output=True,
|
|
53
|
+
text=True,
|
|
54
|
+
check=True,
|
|
55
|
+
)
|
|
56
|
+
volume = int(result.stdout.strip())
|
|
57
|
+
ctx.output({"volume": volume})
|
|
58
|
+
except subprocess.CalledProcessError as e:
|
|
59
|
+
ctx.error(f"Failed to get volume: {e.stderr}")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@audio.command("mute")
|
|
63
|
+
@click.option("--toggle", "-t", is_flag=True, help="Toggle mute state")
|
|
64
|
+
@pass_context
|
|
65
|
+
def audio_mute(ctx, toggle: bool):
|
|
66
|
+
"""Mute system audio.
|
|
67
|
+
|
|
68
|
+
Example:
|
|
69
|
+
macbot audio mute
|
|
70
|
+
macbot audio mute --toggle
|
|
71
|
+
"""
|
|
72
|
+
if toggle:
|
|
73
|
+
script = '''
|
|
74
|
+
set currentMute to output muted of (get volume settings)
|
|
75
|
+
set volume output muted (not currentMute)
|
|
76
|
+
return not currentMute
|
|
77
|
+
'''
|
|
78
|
+
else:
|
|
79
|
+
script = '''
|
|
80
|
+
set volume output muted true
|
|
81
|
+
return true
|
|
82
|
+
'''
|
|
83
|
+
|
|
84
|
+
try:
|
|
85
|
+
result = subprocess.run(
|
|
86
|
+
["osascript", "-e", script],
|
|
87
|
+
capture_output=True,
|
|
88
|
+
text=True,
|
|
89
|
+
check=True,
|
|
90
|
+
)
|
|
91
|
+
muted = result.stdout.strip().lower() == "true"
|
|
92
|
+
ctx.output({"muted": muted})
|
|
93
|
+
except subprocess.CalledProcessError as e:
|
|
94
|
+
ctx.error(f"Failed to mute: {e.stderr}")
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@audio.command("unmute")
|
|
98
|
+
@pass_context
|
|
99
|
+
def audio_unmute(ctx):
|
|
100
|
+
"""Unmute system audio.
|
|
101
|
+
|
|
102
|
+
Example:
|
|
103
|
+
macbot audio unmute
|
|
104
|
+
"""
|
|
105
|
+
script = 'set volume output muted false'
|
|
106
|
+
try:
|
|
107
|
+
subprocess.run(
|
|
108
|
+
["osascript", "-e", script],
|
|
109
|
+
check=True,
|
|
110
|
+
capture_output=True,
|
|
111
|
+
text=True,
|
|
112
|
+
)
|
|
113
|
+
ctx.output({"muted": False})
|
|
114
|
+
except subprocess.CalledProcessError as e:
|
|
115
|
+
ctx.error(f"Failed to unmute: {e.stderr}")
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@audio.command("devices")
|
|
119
|
+
@pass_context
|
|
120
|
+
def audio_devices(ctx):
|
|
121
|
+
"""List audio devices.
|
|
122
|
+
|
|
123
|
+
Example:
|
|
124
|
+
macbot audio devices
|
|
125
|
+
"""
|
|
126
|
+
# Check if SwitchAudioSource is available
|
|
127
|
+
try:
|
|
128
|
+
result = subprocess.run(
|
|
129
|
+
["which", "SwitchAudioSource"],
|
|
130
|
+
capture_output=True,
|
|
131
|
+
text=True,
|
|
132
|
+
)
|
|
133
|
+
if result.returncode != 0:
|
|
134
|
+
ctx.error("SwitchAudioSource not installed. Run: brew install switchaudio-osx")
|
|
135
|
+
|
|
136
|
+
# Get output devices
|
|
137
|
+
output_result = subprocess.run(
|
|
138
|
+
["SwitchAudioSource", "-a", "-t", "output"],
|
|
139
|
+
capture_output=True,
|
|
140
|
+
text=True,
|
|
141
|
+
check=True,
|
|
142
|
+
)
|
|
143
|
+
output_devices = [d.strip() for d in output_result.stdout.strip().split("\n") if d.strip()]
|
|
144
|
+
|
|
145
|
+
# Get input devices
|
|
146
|
+
input_result = subprocess.run(
|
|
147
|
+
["SwitchAudioSource", "-a", "-t", "input"],
|
|
148
|
+
capture_output=True,
|
|
149
|
+
text=True,
|
|
150
|
+
check=True,
|
|
151
|
+
)
|
|
152
|
+
input_devices = [d.strip() for d in input_result.stdout.strip().split("\n") if d.strip()]
|
|
153
|
+
|
|
154
|
+
# Get current devices
|
|
155
|
+
current_output = subprocess.run(
|
|
156
|
+
["SwitchAudioSource", "-c", "-t", "output"],
|
|
157
|
+
capture_output=True,
|
|
158
|
+
text=True,
|
|
159
|
+
).stdout.strip()
|
|
160
|
+
|
|
161
|
+
current_input = subprocess.run(
|
|
162
|
+
["SwitchAudioSource", "-c", "-t", "input"],
|
|
163
|
+
capture_output=True,
|
|
164
|
+
text=True,
|
|
165
|
+
).stdout.strip()
|
|
166
|
+
|
|
167
|
+
ctx.output({
|
|
168
|
+
"output_devices": output_devices,
|
|
169
|
+
"input_devices": input_devices,
|
|
170
|
+
"current_output": current_output,
|
|
171
|
+
"current_input": current_input,
|
|
172
|
+
})
|
|
173
|
+
except subprocess.CalledProcessError as e:
|
|
174
|
+
ctx.error(f"Failed to list devices: {e.stderr}")
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
@audio.command("output")
|
|
178
|
+
@click.argument("device", required=False)
|
|
179
|
+
@pass_context
|
|
180
|
+
def audio_output(ctx, device: str | None):
|
|
181
|
+
"""Get or set audio output device.
|
|
182
|
+
|
|
183
|
+
Example:
|
|
184
|
+
macbot audio output # Get current
|
|
185
|
+
macbot audio output "MacBook Pro Speakers" # Set device
|
|
186
|
+
"""
|
|
187
|
+
try:
|
|
188
|
+
subprocess.run(["which", "SwitchAudioSource"], check=True, capture_output=True)
|
|
189
|
+
except subprocess.CalledProcessError:
|
|
190
|
+
ctx.error("SwitchAudioSource not installed. Run: brew install switchaudio-osx")
|
|
191
|
+
|
|
192
|
+
if device:
|
|
193
|
+
try:
|
|
194
|
+
subprocess.run(
|
|
195
|
+
["SwitchAudioSource", "-s", device, "-t", "output"],
|
|
196
|
+
check=True,
|
|
197
|
+
capture_output=True,
|
|
198
|
+
text=True,
|
|
199
|
+
)
|
|
200
|
+
ctx.output({"output_device": device, "set": True})
|
|
201
|
+
except subprocess.CalledProcessError as e:
|
|
202
|
+
ctx.error(f"Failed to set output device: {e.stderr}")
|
|
203
|
+
else:
|
|
204
|
+
try:
|
|
205
|
+
result = subprocess.run(
|
|
206
|
+
["SwitchAudioSource", "-c", "-t", "output"],
|
|
207
|
+
capture_output=True,
|
|
208
|
+
text=True,
|
|
209
|
+
check=True,
|
|
210
|
+
)
|
|
211
|
+
ctx.output({"output_device": result.stdout.strip()})
|
|
212
|
+
except subprocess.CalledProcessError as e:
|
|
213
|
+
ctx.error(f"Failed to get output device: {e.stderr}")
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
@audio.command("input")
|
|
217
|
+
@click.argument("device", required=False)
|
|
218
|
+
@pass_context
|
|
219
|
+
def audio_input(ctx, device: str | None):
|
|
220
|
+
"""Get or set audio input device.
|
|
221
|
+
|
|
222
|
+
Example:
|
|
223
|
+
macbot audio input # Get current
|
|
224
|
+
macbot audio input "MacBook Pro Microphone" # Set device
|
|
225
|
+
"""
|
|
226
|
+
try:
|
|
227
|
+
subprocess.run(["which", "SwitchAudioSource"], check=True, capture_output=True)
|
|
228
|
+
except subprocess.CalledProcessError:
|
|
229
|
+
ctx.error("SwitchAudioSource not installed. Run: brew install switchaudio-osx")
|
|
230
|
+
|
|
231
|
+
if device:
|
|
232
|
+
try:
|
|
233
|
+
subprocess.run(
|
|
234
|
+
["SwitchAudioSource", "-s", device, "-t", "input"],
|
|
235
|
+
check=True,
|
|
236
|
+
capture_output=True,
|
|
237
|
+
text=True,
|
|
238
|
+
)
|
|
239
|
+
ctx.output({"input_device": device, "set": True})
|
|
240
|
+
except subprocess.CalledProcessError as e:
|
|
241
|
+
ctx.error(f"Failed to set input device: {e.stderr}")
|
|
242
|
+
else:
|
|
243
|
+
try:
|
|
244
|
+
result = subprocess.run(
|
|
245
|
+
["SwitchAudioSource", "-c", "-t", "input"],
|
|
246
|
+
capture_output=True,
|
|
247
|
+
text=True,
|
|
248
|
+
check=True,
|
|
249
|
+
)
|
|
250
|
+
ctx.output({"input_device": result.stdout.strip()})
|
|
251
|
+
except subprocess.CalledProcessError as e:
|
|
252
|
+
ctx.error(f"Failed to get input device: {e.stderr}")
|