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 ADDED
@@ -0,0 +1,3 @@
1
+ """macbot - macOS automation CLI for AI agents and developers."""
2
+
3
+ __version__ = "0.1.0"
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."""
@@ -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}")
@@ -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}")