@ebowwa/osascript 1.1.0

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.
package/README.md ADDED
@@ -0,0 +1,123 @@
1
+ # @ebowwa/osascript
2
+
3
+ macOS automation via osascript (AppleScript/JXA) with MCP interface.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ bun add @ebowwa/osascript
9
+ ```
10
+
11
+ ## MCP Server
12
+
13
+ Add to your Claude Code config:
14
+
15
+ ```json
16
+ {
17
+ "mcpServers": {
18
+ "osascript": {
19
+ "command": "bunx",
20
+ "args": ["@ebowwa/osascript/mcp"]
21
+ }
22
+ }
23
+ }
24
+ ```
25
+
26
+ ## Available Tools
27
+
28
+ | Tool | Description |
29
+ |------|-------------|
30
+ | `osascript_execute` | Execute raw AppleScript or JXA code |
31
+ | `osascript_display_notification` | Display a macOS notification |
32
+ | `osascript_display_dialog` | Display a dialog with buttons |
33
+ | `osascript_get_volume` | Get system volume level |
34
+ | `osascript_set_volume` | Set system volume (0-100) |
35
+ | `osascript_get_clipboard` | Get clipboard contents |
36
+ | `osascript_set_clipboard` | Set clipboard contents |
37
+ | `osascript_list_apps` | List all running applications |
38
+ | `osascript_get_app_info` | Get detailed app information |
39
+ | `osascript_activate_app` | Activate (bring to front) an app |
40
+ | `osascript_quit_app` | Quit an application |
41
+ | `osascript_launch_app` | Launch an application |
42
+ | `osascript_get_frontmost_app` | Get the active application name |
43
+ | `osascript_get_windows` | Get window list for an app |
44
+ | `osascript_set_window_bounds` | Set window position and size |
45
+ | `osascript_keystroke` | Send keystroke with modifiers |
46
+ | `osascript_key_code` | Send key code (special keys) |
47
+ | `osascript_type_text` | Type text (simulate keyboard) |
48
+ | `osascript_safari_get_url` | Get current Safari URL |
49
+ | `osascript_safari_set_url` | Navigate Safari to URL |
50
+ | `osascript_safari_get_tabs` | Get Safari tab titles |
51
+ | `osascript_safari_exec_js` | Execute JavaScript in Safari |
52
+ | `osascript_finder_get_selection` | Get Finder selected files |
53
+ | `osascript_finder_open` | Open Finder window at path |
54
+ | `osascript_finder_create_folder` | Create new folder |
55
+ | `osascript_finder_trash` | Move file to trash |
56
+ | `osascript_get_displays` | Get display information |
57
+ | `osascript_get_resolution` | Get screen resolution |
58
+ | `osascript_say` | Say text using system voice |
59
+ | `osascript_beep` | Play system beep |
60
+
61
+ ## Key Codes
62
+
63
+ Common key codes for `osascript_key_code`:
64
+
65
+ | Key | Code |
66
+ |-----|------|
67
+ | Return | 36 |
68
+ | Tab | 48 |
69
+ | Space | 49 |
70
+ | Delete | 51 |
71
+ | Escape | 53 |
72
+ | Arrow Left | 123 |
73
+ | Arrow Right | 124 |
74
+ | Arrow Down | 125 |
75
+ | Arrow Up | 126 |
76
+ | Home | 115 |
77
+ | End | 119 |
78
+ | Page Up | 116 |
79
+ | Page Down | 121 |
80
+
81
+ ## Usage Examples
82
+
83
+ ### Notification
84
+ ```typescript
85
+ import { displayNotification } from "@ebowwa/osascript";
86
+
87
+ await displayNotification({
88
+ title: "Task Complete",
89
+ message: "Your file has been processed",
90
+ soundName: "Glass"
91
+ });
92
+ ```
93
+
94
+ ### Clipboard
95
+ ```typescript
96
+ import { getClipboard, setClipboard } from "@ebowwa/osascript";
97
+
98
+ await setClipboard("Hello, World!");
99
+ const content = await getClipboard(); // "Hello, World!"
100
+ ```
101
+
102
+ ### Safari
103
+ ```typescript
104
+ import { getSafariURL, setSafariURL } from "@ebowwa/osascript";
105
+
106
+ const url = await getSafariURL();
107
+ await setSafariURL("https://github.com");
108
+ ```
109
+
110
+ ### Keyboard
111
+ ```typescript
112
+ import { sendKeystroke, KeyCodes, sendKeyCode } from "@ebowwa/osascript";
113
+
114
+ // Send Command+C
115
+ await sendKeystroke({ key: "c", modifiers: ["command"] });
116
+
117
+ // Press Return
118
+ await sendKeyCode({ keyCode: KeyCodes.RETURN });
119
+ ```
120
+
121
+ ## License
122
+
123
+ MIT
package/dist/index.js ADDED
@@ -0,0 +1,574 @@
1
+ // @bun
2
+ // src/index.ts
3
+ import { spawn } from "child_process";
4
+ async function executeOsascript(script, options = {}) {
5
+ const { language = "applescript", timeout = 30000 } = options;
6
+ const args = [];
7
+ if (language === "javascript") {
8
+ args.push("-l", "JavaScript");
9
+ }
10
+ args.push("-e", script);
11
+ return new Promise((resolve) => {
12
+ const proc = spawn("osascript", args, { timeout });
13
+ let stdout = "";
14
+ let stderr = "";
15
+ proc.stdout.on("data", (data) => {
16
+ stdout += data.toString();
17
+ });
18
+ proc.stderr.on("data", (data) => {
19
+ stderr += data.toString();
20
+ });
21
+ const timer = setTimeout(() => {
22
+ proc.kill();
23
+ resolve({
24
+ success: false,
25
+ stdout: stdout.trim(),
26
+ stderr: "Timeout exceeded",
27
+ exitCode: 1
28
+ });
29
+ }, timeout);
30
+ proc.on("close", (code) => {
31
+ clearTimeout(timer);
32
+ resolve({
33
+ success: code === 0,
34
+ stdout: stdout.trim(),
35
+ stderr: stderr.trim(),
36
+ exitCode: code ?? 1
37
+ });
38
+ });
39
+ proc.on("error", (error) => {
40
+ clearTimeout(timer);
41
+ resolve({
42
+ success: false,
43
+ stdout: "",
44
+ stderr: error.message,
45
+ exitCode: 1
46
+ });
47
+ });
48
+ });
49
+ }
50
+ async function executeAppleScript(script, timeout = 30000) {
51
+ return executeOsascript(script, { language: "applescript", timeout });
52
+ }
53
+ async function executeJXA(script, timeout = 30000) {
54
+ return executeOsascript(script, { language: "javascript", timeout });
55
+ }
56
+ async function displayNotification(options) {
57
+ const { title = "Notification", subtitle, message, soundName } = options;
58
+ let script = `display notification "${escapeString(message)}"`;
59
+ if (title)
60
+ script += ` with title "${escapeString(title)}"`;
61
+ if (subtitle)
62
+ script += ` subtitle "${escapeString(subtitle)}"`;
63
+ if (soundName)
64
+ script += ` sound name "${escapeString(soundName)}"`;
65
+ return executeAppleScript(script);
66
+ }
67
+ async function displayDialog(options) {
68
+ const { title, message, buttons = ["OK"], defaultButton = 1, icon, givingUpAfter } = options;
69
+ let script = `display dialog "${escapeString(message)}"`;
70
+ script += ` buttons ${JSON.stringify(buttons)}`;
71
+ script += ` default button ${defaultButton}`;
72
+ if (icon)
73
+ script += ` with icon ${icon}`;
74
+ if (givingUpAfter)
75
+ script += ` giving up after ${givingUpAfter}`;
76
+ const result = await executeAppleScript(script);
77
+ if (!result.success) {
78
+ throw new Error(`Dialog failed: ${result.stderr}`);
79
+ }
80
+ const buttonMatch = result.stdout.match(/button returned:(\w+)/);
81
+ const gaveUpMatch = result.stdout.match(/gave up:(\w+)/);
82
+ return {
83
+ buttonReturned: buttonMatch ? buttonMatch[1] : buttons[0],
84
+ gaveUp: gaveUpMatch ? gaveUpMatch[1] === "true" : false
85
+ };
86
+ }
87
+ async function getVolume() {
88
+ const script = `
89
+ output volume of (get volume settings) as integer
90
+ `;
91
+ const volumeResult = await executeAppleScript(script);
92
+ const mutedScript = `
93
+ output muted of (get volume settings) as boolean
94
+ `;
95
+ const mutedResult = await executeAppleScript(mutedScript);
96
+ return {
97
+ volume: parseInt(volumeResult.stdout) || 0,
98
+ muted: mutedResult.stdout.toLowerCase() === "true"
99
+ };
100
+ }
101
+ async function setVolume(volume, muted) {
102
+ const safeVolume = Math.max(0, Math.min(100, Math.round(volume)));
103
+ let script = `set volume output volume ${safeVolume}`;
104
+ if (muted !== undefined) {
105
+ script += muted ? " with output muted" : " without output muted";
106
+ }
107
+ return executeAppleScript(script);
108
+ }
109
+ async function getClipboard() {
110
+ const result = await executeAppleScript("the clipboard");
111
+ if (!result.success) {
112
+ throw new Error(`Failed to get clipboard: ${result.stderr}`);
113
+ }
114
+ return result.stdout;
115
+ }
116
+ async function setClipboard(text) {
117
+ const script = `set the clipboard to "${escapeString(text)}"`;
118
+ return executeAppleScript(script);
119
+ }
120
+ async function clearClipboard() {
121
+ return executeAppleScript('set the clipboard to ""');
122
+ }
123
+ async function listRunningApplications() {
124
+ const script = `
125
+ tell application "System Events"
126
+ set appList to name of every process whose background only is false
127
+ set output to ""
128
+ repeat with appName in appList
129
+ set output to output & appName & linefeed
130
+ end repeat
131
+ return output
132
+ end tell
133
+ `;
134
+ const result = await executeAppleScript(script);
135
+ if (!result.success) {
136
+ throw new Error(`Failed to list applications: ${result.stderr}`);
137
+ }
138
+ const apps = result.stdout.split(`
139
+ `).filter((name) => name.trim()).map((name) => ({
140
+ name: name.trim(),
141
+ running: true
142
+ }));
143
+ return apps;
144
+ }
145
+ async function getApplicationInfo(appName) {
146
+ const script = `
147
+ tell application "System Events"
148
+ if exists process "${escapeString(appName)}" then
149
+ set theProcess to process "${escapeString(appName)}"
150
+ set theName to name of theProcess
151
+ set thePath to POSIX path of (application file of theProcess as alias)
152
+ set thePID to unix id of theProcess
153
+ set windowCount to count of windows of theProcess
154
+ return theName & "|" & thePath & "|" & thePID & "|" & windowCount
155
+ else
156
+ return "NOT_RUNNING"
157
+ end if
158
+ end tell
159
+ `;
160
+ const result = await executeAppleScript(script);
161
+ if (!result.success || result.stdout === "NOT_RUNNING") {
162
+ return {
163
+ name: appName,
164
+ running: false
165
+ };
166
+ }
167
+ const [name, path, pid, windowCount] = result.stdout.split("|");
168
+ return {
169
+ name,
170
+ path,
171
+ processId: parseInt(pid),
172
+ running: true,
173
+ windowCount: parseInt(windowCount)
174
+ };
175
+ }
176
+ async function activateApplication(appName) {
177
+ const script = `
178
+ tell application "${escapeString(appName)}"
179
+ activate
180
+ end tell
181
+ `;
182
+ return executeAppleScript(script);
183
+ }
184
+ async function quitApplication(appName, force = false) {
185
+ let script;
186
+ if (force) {
187
+ script = `
188
+ tell application "System Events"
189
+ set theProcess to process "${escapeString(appName)}"
190
+ set thePID to unix id of theProcess
191
+ do shell script "kill -9 " & thePID
192
+ end tell
193
+ `;
194
+ } else {
195
+ script = `
196
+ tell application "${escapeString(appName)}"
197
+ quit
198
+ end tell
199
+ `;
200
+ }
201
+ return executeAppleScript(script);
202
+ }
203
+ async function launchApplication(appName) {
204
+ const script = `
205
+ tell application "${escapeString(appName)}"
206
+ launch
207
+ end tell
208
+ `;
209
+ return executeAppleScript(script);
210
+ }
211
+ async function getFrontmostApplication() {
212
+ const script = `
213
+ tell application "System Events"
214
+ name of first process whose frontmost is true
215
+ end tell
216
+ `;
217
+ const result = await executeAppleScript(script);
218
+ if (!result.success) {
219
+ throw new Error(`Failed to get frontmost app: ${result.stderr}`);
220
+ }
221
+ return result.stdout;
222
+ }
223
+ async function getApplicationWindows(appName) {
224
+ const script = `
225
+ tell application "System Events"
226
+ tell process "${escapeString(appName)}"
227
+ set windowList to {}
228
+ repeat with theWindow in windows
229
+ set windowName to name of theWindow
230
+ set windowId to id of theWindow
231
+ set windowIndex to index of theWindow
232
+ set end of windowList to windowName & "|" & windowId & "|" & windowIndex
233
+ end repeat
234
+ return windowList as text
235
+ end tell
236
+ end tell
237
+ `;
238
+ const result = await executeAppleScript(script);
239
+ if (!result.success) {
240
+ return [];
241
+ }
242
+ if (!result.stdout.trim()) {
243
+ return [];
244
+ }
245
+ const windows = result.stdout.split(", ").filter((w) => w.trim()).map((w) => {
246
+ const [name, id, index] = w.split("|");
247
+ return {
248
+ name: name || null,
249
+ id: parseInt(id) || 0,
250
+ index: parseInt(index) || 0
251
+ };
252
+ });
253
+ return windows;
254
+ }
255
+ async function setWindowBounds(appName, windowIndex, bounds) {
256
+ const script = `
257
+ tell application "System Events"
258
+ tell process "${escapeString(appName)}"
259
+ set position of window ${windowIndex} to {${bounds.x}, ${bounds.y}}
260
+ set size of window ${windowIndex} to {${bounds.width}, ${bounds.height}}
261
+ end tell
262
+ end tell
263
+ `;
264
+ return executeAppleScript(script);
265
+ }
266
+ async function minimizeWindow(appName, windowIndex) {
267
+ const script = `
268
+ tell application "System Events"
269
+ tell process "${escapeString(appName)}"
270
+ set value of attribute "AXMinimized" of window ${windowIndex} to true
271
+ end tell
272
+ end tell
273
+ `;
274
+ return executeAppleScript(script);
275
+ }
276
+ async function sendKeystroke(options) {
277
+ const { key, modifiers = [], delay = 0 } = options;
278
+ const modifierStr = modifiers.map((m) => m + " down").join(", ");
279
+ const usingStr = modifierStr ? ` using {${modifierStr}}` : "";
280
+ const script = `
281
+ tell application "System Events"
282
+ keystroke "${escapeString(key)}"${usingStr}
283
+ ${delay > 0 ? `delay ${delay}` : ""}
284
+ end tell
285
+ `;
286
+ return executeAppleScript(script);
287
+ }
288
+ async function sendKeyCode(options) {
289
+ const { keyCode, modifiers = [] } = options;
290
+ const modifierStr = modifiers.map((m) => m + " down").join(", ");
291
+ const usingStr = modifierStr ? ` using {${modifierStr}}` : "";
292
+ const script = `
293
+ tell application "System Events"
294
+ key code ${keyCode}${usingStr}
295
+ end tell
296
+ `;
297
+ return executeAppleScript(script);
298
+ }
299
+ async function typeText(text, delayBetweenChars = 0.01) {
300
+ const script = `
301
+ tell application "System Events"
302
+ keystroke "${escapeString(text)}"
303
+ ${delayBetweenChars > 0 ? `delay ${delayBetweenChars}` : ""}
304
+ end tell
305
+ `;
306
+ return executeAppleScript(script);
307
+ }
308
+ async function pressKeyCombination(key, modifiers) {
309
+ return sendKeystroke({ key, modifiers });
310
+ }
311
+ var KeyCodes = {
312
+ RETURN: 36,
313
+ TAB: 48,
314
+ SPACE: 49,
315
+ DELETE: 51,
316
+ ESCAPE: 53,
317
+ LEFT_ARROW: 123,
318
+ RIGHT_ARROW: 124,
319
+ DOWN_ARROW: 125,
320
+ UP_ARROW: 126,
321
+ F1: 122,
322
+ F2: 120,
323
+ F3: 99,
324
+ F4: 118,
325
+ F5: 96,
326
+ F6: 97,
327
+ F7: 98,
328
+ F8: 100,
329
+ F9: 101,
330
+ F10: 109,
331
+ F11: 103,
332
+ F12: 111,
333
+ HOME: 115,
334
+ END: 119,
335
+ PAGE_UP: 116,
336
+ PAGE_DOWN: 121
337
+ };
338
+ async function getSafariURL() {
339
+ const script = `
340
+ tell application "Safari"
341
+ return URL of current tab of front window
342
+ end tell
343
+ `;
344
+ const result = await executeAppleScript(script);
345
+ if (!result.success) {
346
+ throw new Error(`Failed to get Safari URL: ${result.stderr}`);
347
+ }
348
+ return result.stdout;
349
+ }
350
+ async function setSafariURL(url) {
351
+ const script = `
352
+ tell application "Safari"
353
+ set URL of current tab of front window to "${escapeString(url)}"
354
+ end tell
355
+ `;
356
+ return executeAppleScript(script);
357
+ }
358
+ async function getSafariTabTitles() {
359
+ const script = `
360
+ tell application "Safari"
361
+ set tabTitles to name of every tab of front window
362
+ return tabTitles as text
363
+ end tell
364
+ `;
365
+ const result = await executeAppleScript(script);
366
+ if (!result.success) {
367
+ return [];
368
+ }
369
+ return result.stdout.split(", ").filter((t) => t.trim());
370
+ }
371
+ async function executeSafariJS(jsCode) {
372
+ const script = `
373
+ tell application "Safari"
374
+ do JavaScript "${escapeString(jsCode)}" in current tab of front window
375
+ end tell
376
+ `;
377
+ const result = await executeAppleScript(script);
378
+ if (!result.success) {
379
+ throw new Error(`Failed to execute Safari JS: ${result.stderr}`);
380
+ }
381
+ return result.stdout;
382
+ }
383
+ async function getFinderSelection() {
384
+ const script = `
385
+ tell application "Finder"
386
+ set selectedItems to selection
387
+ set output to ""
388
+ repeat with theItem in selectedItems
389
+ set itemName to name of theItem
390
+ set itemPath to POSIX path of (theItem as alias)
391
+ set itemKind to kind of theItem
392
+ set output to output & itemName & "|" & itemPath & "|" & itemKind & linefeed
393
+ end repeat
394
+ return output
395
+ end tell
396
+ `;
397
+ const result = await executeAppleScript(script);
398
+ if (!result.success || !result.stdout.trim()) {
399
+ return [];
400
+ }
401
+ return result.stdout.split(`
402
+ `).filter((line) => line.trim()).map((line) => {
403
+ const [name, path, kind] = line.split("|");
404
+ const type = kind?.toLowerCase().includes("folder") ? "folder" : kind?.toLowerCase().includes("application") ? "application" : "file";
405
+ return {
406
+ name,
407
+ path,
408
+ type,
409
+ exists: true
410
+ };
411
+ });
412
+ }
413
+ async function openFinderWindow(path) {
414
+ const script = `
415
+ tell application "Finder"
416
+ activate
417
+ open POSIX file "${escapeString(path)}"
418
+ end tell
419
+ `;
420
+ return executeAppleScript(script);
421
+ }
422
+ async function createFolder(parentPath, folderName) {
423
+ const script = `
424
+ tell application "Finder"
425
+ make new folder at POSIX file "${escapeString(parentPath)}" with properties {name:"${escapeString(folderName)}"}
426
+ end tell
427
+ `;
428
+ return executeAppleScript(script);
429
+ }
430
+ async function moveToTrash(path) {
431
+ const script = `
432
+ tell application "Finder"
433
+ delete POSIX file "${escapeString(path)}"
434
+ end tell
435
+ `;
436
+ return executeAppleScript(script);
437
+ }
438
+ async function getDisplayInfo() {
439
+ const script = `
440
+ ObjC.import('Cocoa');
441
+ var screens = $.NSScreen.screens;
442
+ var count = screens.count;
443
+ var result = [];
444
+ for (var i = 0; i < count; i++) {
445
+ var screen = screens.objectAtIndex(i);
446
+ var frame = screen.frame;
447
+ result.push({
448
+ id: i,
449
+ width: frame.size.width,
450
+ height: frame.size.height,
451
+ main: i === 0
452
+ });
453
+ }
454
+ JSON.stringify(result);
455
+ `;
456
+ const result = await executeJXA(script);
457
+ if (!result.success) {
458
+ const fallbackScript = `
459
+ tell application "System Events"
460
+ set screenWidth to word 3 of (do shell script "system_profiler SPDisplaysDataType | grep Resolution")
461
+ set screenHeight to word 5 of (do shell script "system_profiler SPDisplaysDataType | grep Resolution")
462
+ return screenWidth & "x" & screenHeight
463
+ end tell
464
+ `;
465
+ const fallback = await executeAppleScript(fallbackScript);
466
+ const [width, height] = fallback.stdout.split("x").map((n) => parseInt(n));
467
+ return [
468
+ {
469
+ id: 0,
470
+ width: width || 1920,
471
+ height: height || 1080,
472
+ main: true
473
+ }
474
+ ];
475
+ }
476
+ try {
477
+ return JSON.parse(result.stdout);
478
+ } catch {
479
+ return [
480
+ {
481
+ id: 0,
482
+ width: 1920,
483
+ height: 1080,
484
+ main: true
485
+ }
486
+ ];
487
+ }
488
+ }
489
+ async function getScreenResolution() {
490
+ const displays = await getDisplayInfo();
491
+ const mainDisplay = displays.find((d) => d.main) || displays[0];
492
+ return {
493
+ width: mainDisplay?.width || 1920,
494
+ height: mainDisplay?.height || 1080
495
+ };
496
+ }
497
+ function isMacOS() {
498
+ return process.platform === "darwin";
499
+ }
500
+ function ensureMacOS() {
501
+ if (!isMacOS()) {
502
+ throw new Error("osascript MCP only works on macOS");
503
+ }
504
+ }
505
+ function escapeString(str) {
506
+ return str.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t");
507
+ }
508
+ async function delay(seconds) {
509
+ await executeAppleScript(`delay ${seconds}`);
510
+ }
511
+ async function sayText(text, voice, rate) {
512
+ let script = `say "${escapeString(text)}"`;
513
+ if (voice)
514
+ script += ` using "${voice}"`;
515
+ if (rate)
516
+ script += ` speaking rate ${rate}`;
517
+ return executeAppleScript(script);
518
+ }
519
+ async function beep(count = 1) {
520
+ return executeAppleScript(`beep ${count}`);
521
+ }
522
+ async function getSystemDateTime() {
523
+ const script = `
524
+ get current date
525
+ `;
526
+ const result = await executeAppleScript(script);
527
+ if (!result.success) {
528
+ throw new Error(`Failed to get system date: ${result.stderr}`);
529
+ }
530
+ return result.stdout;
531
+ }
532
+ export {
533
+ typeText,
534
+ setWindowBounds,
535
+ setVolume,
536
+ setSafariURL,
537
+ setClipboard,
538
+ sendKeystroke,
539
+ sendKeyCode,
540
+ sayText,
541
+ quitApplication,
542
+ pressKeyCombination,
543
+ openFinderWindow,
544
+ moveToTrash,
545
+ minimizeWindow,
546
+ listRunningApplications,
547
+ launchApplication,
548
+ isMacOS,
549
+ getVolume,
550
+ getSystemDateTime,
551
+ getScreenResolution,
552
+ getSafariURL,
553
+ getSafariTabTitles,
554
+ getFrontmostApplication,
555
+ getFinderSelection,
556
+ getDisplayInfo,
557
+ getClipboard,
558
+ getApplicationWindows,
559
+ getApplicationInfo,
560
+ executeSafariJS,
561
+ executeOsascript,
562
+ executeJXA,
563
+ executeAppleScript,
564
+ escapeString,
565
+ ensureMacOS,
566
+ displayNotification,
567
+ displayDialog,
568
+ delay,
569
+ createFolder,
570
+ clearClipboard,
571
+ beep,
572
+ activateApplication,
573
+ KeyCodes
574
+ };