@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/src/index.ts ADDED
@@ -0,0 +1,844 @@
1
+ /**
2
+ * @fileoverview osascript MCP - macOS automation via AppleScript/JXA
3
+ *
4
+ * Provides programmatic access to macOS automation through osascript.
5
+ * Supports AppleScript and JavaScript for Automation (JXA).
6
+ */
7
+
8
+ import { spawn } from "child_process";
9
+
10
+ import type {
11
+ OsascriptResult,
12
+ ExecuteOptions,
13
+ DialogOptions,
14
+ DialogResult,
15
+ NotificationOptions,
16
+ WindowInfo,
17
+ ApplicationInfo,
18
+ KeystrokeOptions,
19
+ KeyCodeOptions,
20
+ VolumeInfo,
21
+ DisplayInfo,
22
+ FinderItem,
23
+ ScriptLanguage,
24
+ } from "./types.js";
25
+
26
+ export type {
27
+ OsascriptResult,
28
+ ExecuteOptions,
29
+ DialogOptions,
30
+ DialogResult,
31
+ NotificationOptions,
32
+ WindowInfo,
33
+ ApplicationInfo,
34
+ KeystrokeOptions,
35
+ KeyCodeOptions,
36
+ VolumeInfo,
37
+ DisplayInfo,
38
+ FinderItem,
39
+ ScriptLanguage,
40
+ };
41
+
42
+ // ==============
43
+ // Core Executor
44
+ // ==============
45
+
46
+ /**
47
+ * Execute osascript command
48
+ */
49
+ export async function executeOsascript(
50
+ script: string,
51
+ options: ExecuteOptions = {}
52
+ ): Promise<OsascriptResult> {
53
+ const { language = "applescript", timeout = 30000 } = options;
54
+
55
+ const args: string[] = [];
56
+ if (language === "javascript") {
57
+ args.push("-l", "JavaScript");
58
+ }
59
+ args.push("-e", script);
60
+
61
+ return new Promise((resolve) => {
62
+ const proc = spawn("osascript", args, { timeout });
63
+ let stdout = "";
64
+ let stderr = "";
65
+
66
+ proc.stdout.on("data", (data) => {
67
+ stdout += data.toString();
68
+ });
69
+
70
+ proc.stderr.on("data", (data) => {
71
+ stderr += data.toString();
72
+ });
73
+
74
+ const timer = setTimeout(() => {
75
+ proc.kill();
76
+ resolve({
77
+ success: false,
78
+ stdout: stdout.trim(),
79
+ stderr: "Timeout exceeded",
80
+ exitCode: 1,
81
+ });
82
+ }, timeout);
83
+
84
+ proc.on("close", (code) => {
85
+ clearTimeout(timer);
86
+ resolve({
87
+ success: code === 0,
88
+ stdout: stdout.trim(),
89
+ stderr: stderr.trim(),
90
+ exitCode: code ?? 1,
91
+ });
92
+ });
93
+
94
+ proc.on("error", (error) => {
95
+ clearTimeout(timer);
96
+ resolve({
97
+ success: false,
98
+ stdout: "",
99
+ stderr: error.message,
100
+ exitCode: 1,
101
+ });
102
+ });
103
+ });
104
+ }
105
+
106
+ /**
107
+ * Execute AppleScript
108
+ */
109
+ export async function executeAppleScript(script: string, timeout = 30000): Promise<OsascriptResult> {
110
+ return executeOsascript(script, { language: "applescript", timeout });
111
+ }
112
+
113
+ /**
114
+ * Execute JavaScript for Automation (JXA)
115
+ */
116
+ export async function executeJXA(script: string, timeout = 30000): Promise<OsascriptResult> {
117
+ return executeOsascript(script, { language: "javascript", timeout });
118
+ }
119
+
120
+ // ==============
121
+ // System Operations
122
+ // ==============
123
+
124
+ /**
125
+ * Display a macOS notification
126
+ */
127
+ export async function displayNotification(options: NotificationOptions): Promise<OsascriptResult> {
128
+ const { title = "Notification", subtitle, message, soundName } = options;
129
+
130
+ let script = `display notification "${escapeString(message)}"`;
131
+ if (title) script += ` with title "${escapeString(title)}"`;
132
+ if (subtitle) script += ` subtitle "${escapeString(subtitle)}"`;
133
+ if (soundName) script += ` sound name "${escapeString(soundName)}"`;
134
+
135
+ return executeAppleScript(script);
136
+ }
137
+
138
+ /**
139
+ * Display a dialog
140
+ */
141
+ export async function displayDialog(options: DialogOptions): Promise<DialogResult> {
142
+ const { title, message, buttons = ["OK"], defaultButton = 1, icon, givingUpAfter } = options;
143
+
144
+ let script = `display dialog "${escapeString(message)}"`;
145
+ script += ` buttons ${JSON.stringify(buttons)}`;
146
+ script += ` default button ${defaultButton}`;
147
+ if (icon) script += ` with icon ${icon}`;
148
+ if (givingUpAfter) script += ` giving up after ${givingUpAfter}`;
149
+
150
+ const result = await executeAppleScript(script);
151
+
152
+ if (!result.success) {
153
+ throw new Error(`Dialog failed: ${result.stderr}`);
154
+ }
155
+
156
+ // Parse result: "button returned:OK" or "button returned:OK, gave up:true"
157
+ const buttonMatch = result.stdout.match(/button returned:(\w+)/);
158
+ const gaveUpMatch = result.stdout.match(/gave up:(\w+)/);
159
+
160
+ return {
161
+ buttonReturned: buttonMatch ? buttonMatch[1] : buttons[0],
162
+ gaveUp: gaveUpMatch ? gaveUpMatch[1] === "true" : false,
163
+ };
164
+ }
165
+
166
+ /**
167
+ * Get system volume
168
+ */
169
+ export async function getVolume(): Promise<VolumeInfo> {
170
+ const script = `
171
+ output volume of (get volume settings) as integer
172
+ `;
173
+
174
+ const volumeResult = await executeAppleScript(script);
175
+ const mutedScript = `
176
+ output muted of (get volume settings) as boolean
177
+ `;
178
+ const mutedResult = await executeAppleScript(mutedScript);
179
+
180
+ return {
181
+ volume: parseInt(volumeResult.stdout) || 0,
182
+ muted: mutedResult.stdout.toLowerCase() === "true",
183
+ };
184
+ }
185
+
186
+ /**
187
+ * Set system volume (0-100)
188
+ */
189
+ export async function setVolume(volume: number, muted?: boolean): Promise<OsascriptResult> {
190
+ const safeVolume = Math.max(0, Math.min(100, Math.round(volume)));
191
+ let script = `set volume output volume ${safeVolume}`;
192
+ if (muted !== undefined) {
193
+ script += muted ? " with output muted" : " without output muted";
194
+ }
195
+ return executeAppleScript(script);
196
+ }
197
+
198
+ /**
199
+ * Get clipboard contents
200
+ */
201
+ export async function getClipboard(): Promise<string> {
202
+ const result = await executeAppleScript("the clipboard");
203
+ if (!result.success) {
204
+ throw new Error(`Failed to get clipboard: ${result.stderr}`);
205
+ }
206
+ return result.stdout;
207
+ }
208
+
209
+ /**
210
+ * Set clipboard contents
211
+ */
212
+ export async function setClipboard(text: string): Promise<OsascriptResult> {
213
+ const script = `set the clipboard to "${escapeString(text)}"`;
214
+ return executeAppleScript(script);
215
+ }
216
+
217
+ /**
218
+ * Clear clipboard
219
+ */
220
+ export async function clearClipboard(): Promise<OsascriptResult> {
221
+ return executeAppleScript("set the clipboard to \"\"");
222
+ }
223
+
224
+ // ==============
225
+ // Application Control
226
+ // ==============
227
+
228
+ /**
229
+ * List running applications
230
+ */
231
+ export async function listRunningApplications(): Promise<ApplicationInfo[]> {
232
+ const script = `
233
+ tell application "System Events"
234
+ set appList to name of every process whose background only is false
235
+ set output to ""
236
+ repeat with appName in appList
237
+ set output to output & appName & linefeed
238
+ end repeat
239
+ return output
240
+ end tell
241
+ `;
242
+
243
+ const result = await executeAppleScript(script);
244
+ if (!result.success) {
245
+ throw new Error(`Failed to list applications: ${result.stderr}`);
246
+ }
247
+
248
+ const apps = result.stdout
249
+ .split("\n")
250
+ .filter((name) => name.trim())
251
+ .map((name) => ({
252
+ name: name.trim(),
253
+ running: true,
254
+ }));
255
+
256
+ return apps;
257
+ }
258
+
259
+ /**
260
+ * Get detailed application info
261
+ */
262
+ export async function getApplicationInfo(appName: string): Promise<ApplicationInfo> {
263
+ const script = `
264
+ tell application "System Events"
265
+ if exists process "${escapeString(appName)}" then
266
+ set theProcess to process "${escapeString(appName)}"
267
+ set theName to name of theProcess
268
+ set thePath to POSIX path of (application file of theProcess as alias)
269
+ set thePID to unix id of theProcess
270
+ set windowCount to count of windows of theProcess
271
+ return theName & "|" & thePath & "|" & thePID & "|" & windowCount
272
+ else
273
+ return "NOT_RUNNING"
274
+ end if
275
+ end tell
276
+ `;
277
+
278
+ const result = await executeAppleScript(script);
279
+ if (!result.success || result.stdout === "NOT_RUNNING") {
280
+ return {
281
+ name: appName,
282
+ running: false,
283
+ };
284
+ }
285
+
286
+ const [name, path, pid, windowCount] = result.stdout.split("|");
287
+
288
+ return {
289
+ name,
290
+ path,
291
+ processId: parseInt(pid),
292
+ running: true,
293
+ windowCount: parseInt(windowCount),
294
+ };
295
+ }
296
+
297
+ /**
298
+ * Activate (bring to front) an application
299
+ */
300
+ export async function activateApplication(appName: string): Promise<OsascriptResult> {
301
+ const script = `
302
+ tell application "${escapeString(appName)}"
303
+ activate
304
+ end tell
305
+ `;
306
+ return executeAppleScript(script);
307
+ }
308
+
309
+ /**
310
+ * Quit an application
311
+ */
312
+ export async function quitApplication(appName: string, force = false): Promise<OsascriptResult> {
313
+ let script: string;
314
+ if (force) {
315
+ script = `
316
+ tell application "System Events"
317
+ set theProcess to process "${escapeString(appName)}"
318
+ set thePID to unix id of theProcess
319
+ do shell script "kill -9 " & thePID
320
+ end tell
321
+ `;
322
+ } else {
323
+ script = `
324
+ tell application "${escapeString(appName)}"
325
+ quit
326
+ end tell
327
+ `;
328
+ }
329
+ return executeAppleScript(script);
330
+ }
331
+
332
+ /**
333
+ * Launch an application
334
+ */
335
+ export async function launchApplication(appName: string): Promise<OsascriptResult> {
336
+ const script = `
337
+ tell application "${escapeString(appName)}"
338
+ launch
339
+ end tell
340
+ `;
341
+ return executeAppleScript(script);
342
+ }
343
+
344
+ /**
345
+ * Get frontmost application name
346
+ */
347
+ export async function getFrontmostApplication(): Promise<string> {
348
+ const script = `
349
+ tell application "System Events"
350
+ name of first process whose frontmost is true
351
+ end tell
352
+ `;
353
+
354
+ const result = await executeAppleScript(script);
355
+ if (!result.success) {
356
+ throw new Error(`Failed to get frontmost app: ${result.stderr}`);
357
+ }
358
+ return result.stdout;
359
+ }
360
+
361
+ // ==============
362
+ // Window Management
363
+ // ==============
364
+
365
+ /**
366
+ * Get window list for an application
367
+ */
368
+ export async function getApplicationWindows(appName: string): Promise<WindowInfo[]> {
369
+ const script = `
370
+ tell application "System Events"
371
+ tell process "${escapeString(appName)}"
372
+ set windowList to {}
373
+ repeat with theWindow in windows
374
+ set windowName to name of theWindow
375
+ set windowId to id of theWindow
376
+ set windowIndex to index of theWindow
377
+ set end of windowList to windowName & "|" & windowId & "|" & windowIndex
378
+ end repeat
379
+ return windowList as text
380
+ end tell
381
+ end tell
382
+ `;
383
+
384
+ const result = await executeAppleScript(script);
385
+ if (!result.success) {
386
+ return [];
387
+ }
388
+
389
+ if (!result.stdout.trim()) {
390
+ return [];
391
+ }
392
+
393
+ const windows = result.stdout
394
+ .split(", ")
395
+ .filter((w) => w.trim())
396
+ .map((w) => {
397
+ const [name, id, index] = w.split("|");
398
+ return {
399
+ name: name || null,
400
+ id: parseInt(id) || 0,
401
+ index: parseInt(index) || 0,
402
+ };
403
+ });
404
+
405
+ return windows;
406
+ }
407
+
408
+ /**
409
+ * Set window bounds (position and size)
410
+ */
411
+ export async function setWindowBounds(
412
+ appName: string,
413
+ windowIndex: number,
414
+ bounds: { x: number; y: number; width: number; height: number }
415
+ ): Promise<OsascriptResult> {
416
+ const script = `
417
+ tell application "System Events"
418
+ tell process "${escapeString(appName)}"
419
+ set position of window ${windowIndex} to {${bounds.x}, ${bounds.y}}
420
+ set size of window ${windowIndex} to {${bounds.width}, ${bounds.height}}
421
+ end tell
422
+ end tell
423
+ `;
424
+ return executeAppleScript(script);
425
+ }
426
+
427
+ /**
428
+ * Minimize window
429
+ */
430
+ export async function minimizeWindow(
431
+ appName: string,
432
+ windowIndex: number
433
+ ): Promise<OsascriptResult> {
434
+ const script = `
435
+ tell application "System Events"
436
+ tell process "${escapeString(appName)}"
437
+ set value of attribute "AXMinimized" of window ${windowIndex} to true
438
+ end tell
439
+ end tell
440
+ `;
441
+ return executeAppleScript(script);
442
+ }
443
+
444
+ // ==============
445
+ // Keyboard Input
446
+ // ==============
447
+
448
+ /**
449
+ * Send keystroke (text input)
450
+ */
451
+ export async function sendKeystroke(options: KeystrokeOptions): Promise<OsascriptResult> {
452
+ const { key, modifiers = [], delay = 0 } = options;
453
+
454
+ const modifierStr = modifiers.map((m) => m + " down").join(", ");
455
+ const usingStr = modifierStr ? ` using {${modifierStr}}` : "";
456
+
457
+ const script = `
458
+ tell application "System Events"
459
+ keystroke "${escapeString(key)}"${usingStr}
460
+ ${delay > 0 ? `delay ${delay}` : ""}
461
+ end tell
462
+ `;
463
+
464
+ return executeAppleScript(script);
465
+ }
466
+
467
+ /**
468
+ * Send key code (for special keys)
469
+ */
470
+ export async function sendKeyCode(options: KeyCodeOptions): Promise<OsascriptResult> {
471
+ const { keyCode, modifiers = [] } = options;
472
+
473
+ const modifierStr = modifiers.map((m) => m + " down").join(", ");
474
+ const usingStr = modifierStr ? ` using {${modifierStr}}` : "";
475
+
476
+ const script = `
477
+ tell application "System Events"
478
+ key code ${keyCode}${usingStr}
479
+ end tell
480
+ `;
481
+
482
+ return executeAppleScript(script);
483
+ }
484
+
485
+ /**
486
+ * Type text (simulate typing)
487
+ */
488
+ export async function typeText(text: string, delayBetweenChars = 0.01): Promise<OsascriptResult> {
489
+ const script = `
490
+ tell application "System Events"
491
+ keystroke "${escapeString(text)}"
492
+ ${delayBetweenChars > 0 ? `delay ${delayBetweenChars}` : ""}
493
+ end tell
494
+ `;
495
+
496
+ return executeAppleScript(script);
497
+ }
498
+
499
+ /**
500
+ * Press key combination (e.g., Command+C)
501
+ */
502
+ export async function pressKeyCombination(
503
+ key: string,
504
+ modifiers: ("command" | "shift" | "option" | "control")[]
505
+ ): Promise<OsascriptResult> {
506
+ return sendKeystroke({ key, modifiers });
507
+ }
508
+
509
+ // Common key codes
510
+ export const KeyCodes = {
511
+ RETURN: 36,
512
+ TAB: 48,
513
+ SPACE: 49,
514
+ DELETE: 51,
515
+ ESCAPE: 53,
516
+ LEFT_ARROW: 123,
517
+ RIGHT_ARROW: 124,
518
+ DOWN_ARROW: 125,
519
+ UP_ARROW: 126,
520
+ F1: 122,
521
+ F2: 120,
522
+ F3: 99,
523
+ F4: 118,
524
+ F5: 96,
525
+ F6: 97,
526
+ F7: 98,
527
+ F8: 100,
528
+ F9: 101,
529
+ F10: 109,
530
+ F11: 103,
531
+ F12: 111,
532
+ HOME: 115,
533
+ END: 119,
534
+ PAGE_UP: 116,
535
+ PAGE_DOWN: 121,
536
+ } as const;
537
+
538
+ // ==============
539
+ // Safari
540
+ // ==============
541
+
542
+ /**
543
+ * Get current Safari URL
544
+ */
545
+ export async function getSafariURL(): Promise<string> {
546
+ const script = `
547
+ tell application "Safari"
548
+ return URL of current tab of front window
549
+ end tell
550
+ `;
551
+
552
+ const result = await executeAppleScript(script);
553
+ if (!result.success) {
554
+ throw new Error(`Failed to get Safari URL: ${result.stderr}`);
555
+ }
556
+ return result.stdout;
557
+ }
558
+
559
+ /**
560
+ * Set Safari URL (navigate)
561
+ */
562
+ export async function setSafariURL(url: string): Promise<OsascriptResult> {
563
+ const script = `
564
+ tell application "Safari"
565
+ set URL of current tab of front window to "${escapeString(url)}"
566
+ end tell
567
+ `;
568
+ return executeAppleScript(script);
569
+ }
570
+
571
+ /**
572
+ * Get Safari tab titles
573
+ */
574
+ export async function getSafariTabTitles(): Promise<string[]> {
575
+ const script = `
576
+ tell application "Safari"
577
+ set tabTitles to name of every tab of front window
578
+ return tabTitles as text
579
+ end tell
580
+ `;
581
+
582
+ const result = await executeAppleScript(script);
583
+ if (!result.success) {
584
+ return [];
585
+ }
586
+
587
+ return result.stdout.split(", ").filter((t) => t.trim());
588
+ }
589
+
590
+ /**
591
+ * Execute JavaScript in Safari
592
+ */
593
+ export async function executeSafariJS(jsCode: string): Promise<string> {
594
+ const script = `
595
+ tell application "Safari"
596
+ do JavaScript "${escapeString(jsCode)}" in current tab of front window
597
+ end tell
598
+ `;
599
+
600
+ const result = await executeAppleScript(script);
601
+ if (!result.success) {
602
+ throw new Error(`Failed to execute Safari JS: ${result.stderr}`);
603
+ }
604
+ return result.stdout;
605
+ }
606
+
607
+ // ==============
608
+ // Finder
609
+ // ==============
610
+
611
+ /**
612
+ * Get Finder selection
613
+ */
614
+ export async function getFinderSelection(): Promise<FinderItem[]> {
615
+ const script = `
616
+ tell application "Finder"
617
+ set selectedItems to selection
618
+ set output to ""
619
+ repeat with theItem in selectedItems
620
+ set itemName to name of theItem
621
+ set itemPath to POSIX path of (theItem as alias)
622
+ set itemKind to kind of theItem
623
+ set output to output & itemName & "|" & itemPath & "|" & itemKind & linefeed
624
+ end repeat
625
+ return output
626
+ end tell
627
+ `;
628
+
629
+ const result = await executeAppleScript(script);
630
+ if (!result.success || !result.stdout.trim()) {
631
+ return [];
632
+ }
633
+
634
+ return result.stdout
635
+ .split("\n")
636
+ .filter((line) => line.trim())
637
+ .map((line) => {
638
+ const [name, path, kind] = line.split("|");
639
+ const type = kind?.toLowerCase().includes("folder")
640
+ ? "folder"
641
+ : kind?.toLowerCase().includes("application")
642
+ ? "application"
643
+ : "file";
644
+
645
+ return {
646
+ name,
647
+ path,
648
+ type: type as FinderItem["type"],
649
+ exists: true,
650
+ };
651
+ });
652
+ }
653
+
654
+ /**
655
+ * Open Finder window at path
656
+ */
657
+ export async function openFinderWindow(path: string): Promise<OsascriptResult> {
658
+ const script = `
659
+ tell application "Finder"
660
+ activate
661
+ open POSIX file "${escapeString(path)}"
662
+ end tell
663
+ `;
664
+ return executeAppleScript(script);
665
+ }
666
+
667
+ /**
668
+ * Create new folder in Finder
669
+ */
670
+ export async function createFolder(
671
+ parentPath: string,
672
+ folderName: string
673
+ ): Promise<OsascriptResult> {
674
+ const script = `
675
+ tell application "Finder"
676
+ make new folder at POSIX file "${escapeString(parentPath)}" with properties {name:"${escapeString(folderName)}"}
677
+ end tell
678
+ `;
679
+ return executeAppleScript(script);
680
+ }
681
+
682
+ /**
683
+ * Move file/folder to trash
684
+ */
685
+ export async function moveToTrash(path: string): Promise<OsascriptResult> {
686
+ const script = `
687
+ tell application "Finder"
688
+ delete POSIX file "${escapeString(path)}"
689
+ end tell
690
+ `;
691
+ return executeAppleScript(script);
692
+ }
693
+
694
+ // ==============
695
+ // Display
696
+ // ==============
697
+
698
+ /**
699
+ * Get display information
700
+ */
701
+ export async function getDisplayInfo(): Promise<DisplayInfo[]> {
702
+ // Using JXA for better display info
703
+ const script = `
704
+ ObjC.import('Cocoa');
705
+ var screens = $.NSScreen.screens;
706
+ var count = screens.count;
707
+ var result = [];
708
+ for (var i = 0; i < count; i++) {
709
+ var screen = screens.objectAtIndex(i);
710
+ var frame = screen.frame;
711
+ result.push({
712
+ id: i,
713
+ width: frame.size.width,
714
+ height: frame.size.height,
715
+ main: i === 0
716
+ });
717
+ }
718
+ JSON.stringify(result);
719
+ `;
720
+
721
+ const result = await executeJXA(script);
722
+ if (!result.success) {
723
+ // Fallback to AppleScript
724
+ const fallbackScript = `
725
+ tell application "System Events"
726
+ set screenWidth to word 3 of (do shell script "system_profiler SPDisplaysDataType | grep Resolution")
727
+ set screenHeight to word 5 of (do shell script "system_profiler SPDisplaysDataType | grep Resolution")
728
+ return screenWidth & "x" & screenHeight
729
+ end tell
730
+ `;
731
+ const fallback = await executeAppleScript(fallbackScript);
732
+ const [width, height] = fallback.stdout.split("x").map((n) => parseInt(n));
733
+
734
+ return [
735
+ {
736
+ id: 0,
737
+ width: width || 1920,
738
+ height: height || 1080,
739
+ main: true,
740
+ },
741
+ ];
742
+ }
743
+
744
+ try {
745
+ return JSON.parse(result.stdout);
746
+ } catch {
747
+ return [
748
+ {
749
+ id: 0,
750
+ width: 1920,
751
+ height: 1080,
752
+ main: true,
753
+ },
754
+ ];
755
+ }
756
+ }
757
+
758
+ /**
759
+ * Get screen resolution
760
+ */
761
+ export async function getScreenResolution(): Promise<{ width: number; height: number }> {
762
+ const displays = await getDisplayInfo();
763
+ const mainDisplay = displays.find((d) => d.main) || displays[0];
764
+ return {
765
+ width: mainDisplay?.width || 1920,
766
+ height: mainDisplay?.height || 1080,
767
+ };
768
+ }
769
+
770
+ // ==============
771
+ // Utilities
772
+ // ==============
773
+
774
+ /**
775
+ * Check if running on macOS
776
+ */
777
+ export function isMacOS(): boolean {
778
+ return process.platform === "darwin";
779
+ }
780
+
781
+ /**
782
+ * Ensure running on macOS
783
+ */
784
+ export function ensureMacOS(): void {
785
+ if (!isMacOS()) {
786
+ throw new Error("osascript MCP only works on macOS");
787
+ }
788
+ }
789
+
790
+ /**
791
+ * Escape string for AppleScript
792
+ */
793
+ export function escapeString(str: string): string {
794
+ return str
795
+ .replace(/\\/g, "\\\\")
796
+ .replace(/"/g, '\\"')
797
+ .replace(/\n/g, "\\n")
798
+ .replace(/\r/g, "\\r")
799
+ .replace(/\t/g, "\\t");
800
+ }
801
+
802
+ /**
803
+ * Delay execution
804
+ */
805
+ export async function delay(seconds: number): Promise<void> {
806
+ await executeAppleScript(`delay ${seconds}`);
807
+ }
808
+
809
+ /**
810
+ * Say text using system voice
811
+ */
812
+ export async function sayText(
813
+ text: string,
814
+ voice?: string,
815
+ rate?: number
816
+ ): Promise<OsascriptResult> {
817
+ let script = `say "${escapeString(text)}"`;
818
+ if (voice) script += ` using "${voice}"`;
819
+ if (rate) script += ` speaking rate ${rate}`;
820
+
821
+ return executeAppleScript(script);
822
+ }
823
+
824
+ /**
825
+ * Beep
826
+ */
827
+ export async function beep(count = 1): Promise<OsascriptResult> {
828
+ return executeAppleScript(`beep ${count}`);
829
+ }
830
+
831
+ /**
832
+ * Get current date/time from system
833
+ */
834
+ export async function getSystemDateTime(): Promise<string> {
835
+ const script = `
836
+ get current date
837
+ `;
838
+
839
+ const result = await executeAppleScript(script);
840
+ if (!result.success) {
841
+ throw new Error(`Failed to get system date: ${result.stderr}`);
842
+ }
843
+ return result.stdout;
844
+ }