@l22-io/orchard-mcp 0.6.1 → 0.6.3

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/build/bridge.js CHANGED
@@ -106,14 +106,8 @@ function runBridgeProcess(bin, args, timeoutMs) {
106
106
  const MAX_BYTES = 10 * 1024 * 1024;
107
107
  let settled = false;
108
108
  let timedOut = false;
109
- const settle = (result) => {
110
- if (settled)
111
- return;
112
- settled = true;
113
- clearTimeout(timer);
114
- clearTimeout(sigkillTimer);
115
- resolvePromise(result);
116
- };
109
+ let killEscalated = false;
110
+ let sigkillTimer = null;
117
111
  const killGroup = (signal) => {
118
112
  try {
119
113
  process.kill(-pid, signal);
@@ -122,17 +116,40 @@ function runBridgeProcess(bin, args, timeoutMs) {
122
116
  // Group may already be gone; nothing to do.
123
117
  }
124
118
  };
125
- let sigkillTimer = setTimeout(() => { }, 0);
126
- clearTimeout(sigkillTimer);
127
- const timer = setTimeout(() => {
128
- timedOut = true;
119
+ // Reason: Swift's apple-bridge has no SIGTERM handler and dies on the first
120
+ // signal, which causes Node's "close" event to fire almost immediately.
121
+ // If we cleared sigkillTimer at that point, any osascript grandchild
122
+ // wedged in an Apple Event RPC to Mail.app / Notes.app would be orphaned
123
+ // (PPID=1) and continue to hold Mail.app's event queue hostage. So once
124
+ // we have committed to escalating, the SIGKILL must fire regardless of
125
+ // when the bridge process itself closes.
126
+ const escalateKill = () => {
127
+ if (killEscalated)
128
+ return;
129
+ killEscalated = true;
129
130
  killGroup("SIGTERM");
130
131
  sigkillTimer = setTimeout(() => killGroup("SIGKILL"), SIGKILL_GRACE_MS);
132
+ sigkillTimer.unref();
133
+ };
134
+ const settle = (result) => {
135
+ if (settled)
136
+ return;
137
+ settled = true;
138
+ clearTimeout(timer);
139
+ if (!killEscalated && sigkillTimer)
140
+ clearTimeout(sigkillTimer);
141
+ // When escalation is in flight, leave the SIGKILL timer alone so it
142
+ // can reap any grandchildren the bridge spawned (see escalateKill).
143
+ resolvePromise(result);
144
+ };
145
+ const timer = setTimeout(() => {
146
+ timedOut = true;
147
+ escalateKill();
131
148
  }, timeoutMs);
132
149
  child.stdout?.on("data", (d) => {
133
150
  totalBytes += d.length;
134
151
  if (totalBytes > MAX_BYTES) {
135
- killGroup("SIGTERM");
152
+ escalateKill();
136
153
  settle({
137
154
  status: "error",
138
155
  spawnError: `apple-bridge output exceeded ${MAX_BYTES} bytes`,
@@ -1 +1 @@
1
- {"version":3,"file":"bridge.js","sourceRoot":"","sources":["../src/bridge.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"bridge.js","sourceRoot":"","sources":["../src/bridge.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@l22-io/orchard-mcp",
3
- "version": "0.6.1",
3
+ "version": "0.6.3",
4
4
  "description": "MCP server for Apple Calendar, Mail, Reminders, and Files on macOS using native EventKit",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -1 +1 @@
1
- 853d524532c723fcd5c9f0c00a5895e3b384653a6d0b3e39c85e655bb4034602 apple-bridge
1
+ 8362b4b7635f775e1847b883917e1839bb8e0f3522b2b497ce471150d0ddaba4 apple-bridge
@@ -7,6 +7,11 @@ struct AppleBridge: AsyncParsableCommand {
7
7
  // This allows any subcommand to write to a file instead of stdout,
8
8
  // needed for .app bundle mode on macOS Sequoia where stdout is not capturable.
9
9
  static func main() async {
10
+ // Install before parsing so a SIGTERM during arg parsing (rare, but
11
+ // possible if node tears us down before we finish startup) still
12
+ // reaps any osascript spawned by a partial subcommand.
13
+ OsascriptRunner.installSignalHandlers()
14
+
10
15
  var args = Array(CommandLine.arguments.dropFirst())
11
16
  if let idx = args.firstIndex(of: "--output"), idx + 1 < args.count {
12
17
  JSONOutput.outputPath = args[idx + 1]
@@ -28,7 +33,7 @@ struct AppleBridge: AsyncParsableCommand {
28
33
  static let configuration = CommandConfiguration(
29
34
  commandName: "apple-bridge",
30
35
  abstract: "Native macOS bridge for Apple Calendar, Mail, Reminders, Numbers, Pages, and Keynote.",
31
- version: "0.5.0",
36
+ version: "0.6.3",
32
37
  subcommands: [
33
38
  Calendars.self,
34
39
  Events.self,
@@ -9,7 +9,7 @@ import Foundation
9
9
  enum DoctorBridge {
10
10
  static func run() async {
11
11
  var report: [String: Any] = [
12
- "version": "0.5.0",
12
+ "version": "0.6.3",
13
13
  "platform": "macOS",
14
14
  "systemVersion": ProcessInfo.processInfo.operatingSystemVersionString
15
15
  ]
@@ -147,30 +147,20 @@ enum DoctorBridge {
147
147
  }
148
148
 
149
149
  private static func checkIWorkApp(_ appName: String) -> [String: Any] {
150
- let task = Process()
151
- task.executableURL = URL(fileURLWithPath: "/usr/bin/osascript")
152
- task.arguments = ["-e", "tell application \"\(appName)\" to return name"]
153
- let outPipe = Pipe()
154
- let errPipe = Pipe()
155
- task.standardOutput = outPipe
156
- task.standardError = errPipe
157
-
158
- do {
159
- try task.run()
160
- task.waitUntilExit()
161
- if task.terminationStatus == 0 {
162
- return ["installed": true, "accessible": true]
163
- } else {
164
- let errData = errPipe.fileHandleForReading.readDataToEndOfFile()
165
- let errStr = String(data: errData, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
166
- if errStr.contains("-600") || errStr.contains("not running") {
167
- return ["installed": true, "accessible": false, "note": "\(appName) is not running."]
168
- }
169
- return ["installed": false, "accessible": false, "note": "\(appName) may not be installed."]
170
- }
171
- } catch {
172
- return ["installed": false, "accessible": false, "note": "Could not check \(appName): \(error.localizedDescription)"]
150
+ let script = "tell application \"\(appName)\" to return name"
151
+ guard let result = OsascriptRunner.runRaw(script: script, timeout: doctorAppleScriptTimeout) else {
152
+ return ["installed": false, "accessible": false, "note": "Could not spawn osascript to check \(appName)."]
173
153
  }
154
+ if result.timedOut {
155
+ return ["installed": true, "accessible": false, "note": "\(appName) did not respond within \(Int(doctorAppleScriptTimeout))s."]
156
+ }
157
+ if result.status == 0 {
158
+ return ["installed": true, "accessible": true]
159
+ }
160
+ if result.stderr.contains("-600") || result.stderr.contains("not running") {
161
+ return ["installed": true, "accessible": false, "note": "\(appName) is not running."]
162
+ }
163
+ return ["installed": false, "accessible": false, "note": "\(appName) may not be installed."]
174
164
  }
175
165
 
176
166
  private static func contactsAuthName(_ status: CNAuthorizationStatus) -> String {
@@ -185,68 +175,44 @@ enum DoctorBridge {
185
175
  }
186
176
 
187
177
  private static func checkNotesAccess() -> [String: Any] {
188
- let task = Process()
189
- task.executableURL = URL(fileURLWithPath: "/usr/bin/osascript")
190
- task.arguments = ["-e", "tell application \"Notes\" to count of accounts"]
191
- let pipe = Pipe()
192
- task.standardOutput = pipe
193
- task.standardError = Pipe()
194
-
195
- do {
196
- try task.run()
197
- task.waitUntilExit()
198
- if task.terminationStatus == 0 {
199
- let data = pipe.fileHandleForReading.readDataToEndOfFile()
200
- let output = String(data: data, encoding: .utf8)?
201
- .trimmingCharacters(in: .whitespacesAndNewlines)
202
- return [
203
- "accessible": true,
204
- "accountCount": Int(output ?? "0") ?? 0
205
- ]
206
- }
178
+ return checkAppAccess(appName: "Notes")
179
+ }
180
+
181
+ private static func checkMailAccess() -> [String: Any] {
182
+ // Reason: Try a minimal AppleScript to see if Mail.app is accessible.
183
+ // This doesn't send the permission prompt -- it just checks if we can talk to Mail.
184
+ return checkAppAccess(appName: "Mail")
185
+ }
186
+
187
+ private static func checkAppAccess(appName: String) -> [String: Any] {
188
+ let script = "tell application \"\(appName)\" to count of accounts"
189
+ guard let result = OsascriptRunner.runRaw(script: script, timeout: doctorAppleScriptTimeout) else {
207
190
  return [
208
191
  "accessible": false,
209
- "note": "Notes automation permission not yet granted or Notes.app not running."
192
+ "note": "Failed to spawn osascript to probe \(appName)."
210
193
  ]
211
- } catch {
194
+ }
195
+ if result.timedOut {
212
196
  return [
213
197
  "accessible": false,
214
- "note": "Could not run osascript: \(error.localizedDescription)"
198
+ "note": "\(appName).app did not respond within \(Int(doctorAppleScriptTimeout))s. It may be busy or unresponsive; system_doctor refuses to wait longer to avoid orphaning osascript."
215
199
  ]
216
200
  }
217
- }
218
-
219
- private static func checkMailAccess() -> [String: Any] {
220
- // Reason: Try a minimal AppleScript to see if Mail.app is accessible.
221
- // This doesn't send the permission prompt -- it just checks if we can talk to Mail.
222
- let task = Process()
223
- task.executableURL = URL(fileURLWithPath: "/usr/bin/osascript")
224
- task.arguments = ["-e", "tell application \"Mail\" to count of accounts"]
225
- let pipe = Pipe()
226
- task.standardOutput = pipe
227
- task.standardError = Pipe()
228
-
229
- do {
230
- try task.run()
231
- task.waitUntilExit()
232
- if task.terminationStatus == 0 {
233
- let data = pipe.fileHandleForReading.readDataToEndOfFile()
234
- let output = String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines)
235
- return [
236
- "accessible": true,
237
- "accountCount": Int(output ?? "0") ?? 0
238
- ]
239
- } else {
240
- return [
241
- "accessible": false,
242
- "note": "Mail automation permission not yet granted or Mail.app not running."
243
- ]
244
- }
245
- } catch {
201
+ if result.status == 0 {
246
202
  return [
247
- "accessible": false,
248
- "note": "Could not run osascript: \(error.localizedDescription)"
203
+ "accessible": true,
204
+ "accountCount": Int(result.stdout) ?? 0
249
205
  ]
250
206
  }
207
+ return [
208
+ "accessible": false,
209
+ "note": "\(appName) automation permission not yet granted or \(appName).app not running."
210
+ ]
251
211
  }
212
+
213
+ /// Hard timeout for any AppleScript invocation issued by the doctor. The
214
+ /// doctor's job is to report state quickly; if Mail.app or Notes.app
215
+ /// cannot answer "count of accounts" in this window they are by definition
216
+ /// not accessible.
217
+ private static let doctorAppleScriptTimeout: TimeInterval = 5
252
218
  }
@@ -530,41 +530,7 @@ enum KeynoteBridge {
530
530
  // MARK: - AppleScript Execution
531
531
 
532
532
  private static func runAppleScript(_ script: String) -> String? {
533
- let task = Process()
534
- task.executableURL = URL(fileURLWithPath: "/usr/bin/osascript")
535
- task.arguments = ["-e", script]
536
-
537
- let outPipe = Pipe()
538
- let errPipe = Pipe()
539
- task.standardOutput = outPipe
540
- task.standardError = errPipe
541
-
542
- do {
543
- try task.run()
544
- task.waitUntilExit()
545
-
546
- if task.terminationStatus != 0 {
547
- let errData = errPipe.fileHandleForReading.readDataToEndOfFile()
548
- let errStr = String(data: errData, encoding: .utf8)?
549
- .trimmingCharacters(in: .whitespacesAndNewlines) ?? "Unknown error"
550
-
551
- if errStr.contains("-1743") || errStr.contains("not allowed") {
552
- JSONOutput.error("Keynote automation permission denied. Grant access in System Settings > Privacy & Security > Automation.")
553
- } else if errStr.contains("-600") || errStr.contains("not running") {
554
- JSONOutput.error("Keynote is not running. It will be launched automatically on next attempt.")
555
- } else {
556
- JSONOutput.error("AppleScript error: \(errStr)")
557
- }
558
- return nil
559
- }
560
-
561
- let data = outPipe.fileHandleForReading.readDataToEndOfFile()
562
- return String(data: data, encoding: .utf8)?
563
- .trimmingCharacters(in: .whitespacesAndNewlines)
564
- } catch {
565
- JSONOutput.error("Failed to run osascript: \(error.localizedDescription)")
566
- return nil
567
- }
533
+ return OsascriptRunner.run(script: script, appName: "Keynote")
568
534
  }
569
535
 
570
536
  private static func escapeForAppleScript(_ str: String) -> String {
@@ -637,85 +637,20 @@ enum MailBridge {
637
637
 
638
638
  // MARK: - AppleScript Execution
639
639
 
640
- /// Default osascript timeout: long enough for legitimate per-account or
641
- /// per-mailbox searches on large accounts, short enough that a hung
642
- /// script returns control to the caller before Mail.app appears frozen
643
- /// from the user's perspective. The scope guard in `search()` already
644
- /// rejects the worst combinations; this is belt-and-braces.
645
- private static let defaultAppleScriptTimeout: TimeInterval = 90
646
-
647
- private static func runAppleScript(_ script: String, timeoutSeconds: TimeInterval = defaultAppleScriptTimeout) -> String? {
648
- let task = Process()
649
- task.executableURL = URL(fileURLWithPath: "/usr/bin/osascript")
650
- task.arguments = ["-e", script]
651
-
652
- let outPipe = Pipe()
653
- let errPipe = Pipe()
654
- task.standardOutput = outPipe
655
- task.standardError = errPipe
656
-
657
- do {
658
- try task.run()
659
-
660
- // Watchdog: terminate osascript if it exceeds the timeout. SIGTERM
661
- // first so the script gets a chance to clean up; SIGKILL after a
662
- // short grace period if it ignores SIGTERM (Apple Events held by
663
- // Mail.app can keep an osascript subprocess unresponsive to TERM).
664
- let pid = task.processIdentifier
665
- let didTimeOut = TimeoutFlag()
666
- let watchdog = DispatchWorkItem {
667
- guard task.isRunning else { return }
668
- didTimeOut.set()
669
- task.terminate()
670
- DispatchQueue.global().asyncAfter(deadline: .now() + 2) {
671
- if task.isRunning { kill(pid, SIGKILL) }
672
- }
673
- }
674
- DispatchQueue.global().asyncAfter(deadline: .now() + timeoutSeconds, execute: watchdog)
675
-
676
- task.waitUntilExit()
677
- watchdog.cancel()
678
-
679
- if didTimeOut.value {
680
- JSONOutput.error(
681
- "Mail AppleScript exceeded \(Int(timeoutSeconds))s timeout — killed to free Mail.app. " +
682
- "Narrow the search scope (specific --account or --mailbox) or use --search-in subject."
683
- )
684
- return nil
685
- }
686
-
687
- if task.terminationStatus != 0 {
688
- let errData = errPipe.fileHandleForReading.readDataToEndOfFile()
689
- let errStr = String(data: errData, encoding: .utf8)?
690
- .trimmingCharacters(in: .whitespacesAndNewlines) ?? "Unknown error"
691
-
692
- if errStr.contains("-1743") || errStr.contains("not allowed") {
693
- JSONOutput.error("Mail automation permission denied. Grant access in System Settings > Privacy & Security > Automation > apple-bridge > Mail.")
694
- } else if errStr.contains("-600") || errStr.contains("not running") {
695
- JSONOutput.error("Mail.app is not running. Open Mail.app and try again.")
696
- } else {
697
- JSONOutput.error("AppleScript error: \(errStr)")
698
- }
699
- return nil
700
- }
701
-
702
- let data = outPipe.fileHandleForReading.readDataToEndOfFile()
703
- return String(data: data, encoding: .utf8)?
704
- .trimmingCharacters(in: .whitespacesAndNewlines)
705
- } catch {
706
- JSONOutput.error("Failed to run osascript: \(error.localizedDescription)")
707
- return nil
708
- }
709
- }
710
-
711
- /// Tiny actor-like flag so the watchdog closure can signal the main
712
- /// thread that it fired, without racing with `task.terminationReason`
713
- /// (which is only set after the kernel reaps the child).
714
- private final class TimeoutFlag {
715
- private let lock = NSLock()
716
- private var fired = false
717
- func set() { lock.lock(); fired = true; lock.unlock() }
718
- var value: Bool { lock.lock(); defer { lock.unlock() }; return fired }
640
+ /// Mail.app's per-account/per-mailbox fallback can iterate large folders;
641
+ /// 90s is long enough for legitimate searches on big accounts but short
642
+ /// enough that node's default 30s timeout (which fires first) plus our
643
+ /// signal handler still reap osascript before it wedges Mail.app for
644
+ /// other concurrent apple-bridge instances.
645
+ private static let mailAppleScriptTimeout: TimeInterval = 90
646
+
647
+ private static func runAppleScript(_ script: String) -> String? {
648
+ return OsascriptRunner.run(
649
+ script: script,
650
+ timeout: mailAppleScriptTimeout,
651
+ appName: "Mail",
652
+ timeoutHint: "Narrow the search scope (specific --account or --mailbox) or use --search-in subject."
653
+ )
719
654
  }
720
655
 
721
656
  private static func escapeForAppleScript(_ str: String) -> String {
@@ -184,41 +184,7 @@ enum NotesBridge {
184
184
  // MARK: - AppleScript plumbing
185
185
 
186
186
  private static func runAppleScript(_ script: String) -> String? {
187
- let task = Process()
188
- task.executableURL = URL(fileURLWithPath: "/usr/bin/osascript")
189
- task.arguments = ["-e", script]
190
-
191
- let outPipe = Pipe()
192
- let errPipe = Pipe()
193
- task.standardOutput = outPipe
194
- task.standardError = errPipe
195
-
196
- do {
197
- try task.run()
198
- task.waitUntilExit()
199
-
200
- if task.terminationStatus != 0 {
201
- let errData = errPipe.fileHandleForReading.readDataToEndOfFile()
202
- let errStr = String(data: errData, encoding: .utf8)?
203
- .trimmingCharacters(in: .whitespacesAndNewlines) ?? "Unknown error"
204
-
205
- if errStr.contains("-1743") || errStr.contains("not allowed") {
206
- JSONOutput.error("Notes automation permission denied. Grant access in System Settings > Privacy & Security > Automation > apple-bridge > Notes.")
207
- } else if errStr.contains("-600") || errStr.contains("not running") {
208
- JSONOutput.error("Notes.app is not running. Open Notes.app and try again.")
209
- } else {
210
- JSONOutput.error("AppleScript error: \(errStr)")
211
- }
212
- return nil
213
- }
214
-
215
- let data = outPipe.fileHandleForReading.readDataToEndOfFile()
216
- return String(data: data, encoding: .utf8)?
217
- .trimmingCharacters(in: .whitespacesAndNewlines)
218
- } catch {
219
- JSONOutput.error("Failed to run osascript: \(error.localizedDescription)")
220
- return nil
221
- }
187
+ return OsascriptRunner.run(script: script, appName: "Notes")
222
188
  }
223
189
 
224
190
  private static func escapeForAppleScript(_ str: String) -> String {
@@ -444,74 +444,13 @@ enum NumbersBridge {
444
444
  // MARK: - AppleScript Execution
445
445
 
446
446
  private static func runAppleScript(_ script: String) -> String? {
447
- let task = Process()
448
- task.executableURL = URL(fileURLWithPath: "/usr/bin/osascript")
449
- task.arguments = ["-e", script]
450
-
451
- let outPipe = Pipe()
452
- let errPipe = Pipe()
453
- task.standardOutput = outPipe
454
- task.standardError = errPipe
455
-
456
- do {
457
- try task.run()
458
- task.waitUntilExit()
459
-
460
- if task.terminationStatus != 0 {
461
- let errData = errPipe.fileHandleForReading.readDataToEndOfFile()
462
- let errStr = String(data: errData, encoding: .utf8)?
463
- .trimmingCharacters(in: .whitespacesAndNewlines) ?? "Unknown error"
464
-
465
- if errStr.contains("-1743") || errStr.contains("not allowed") {
466
- JSONOutput.error("Numbers automation permission denied. Grant access in System Settings > Privacy & Security > Automation.")
467
- } else if errStr.contains("-600") || errStr.contains("not running") {
468
- JSONOutput.error("Numbers is not running. It will be launched automatically on next attempt.")
469
- } else {
470
- JSONOutput.error("AppleScript error: \(errStr)")
471
- }
472
- return nil
473
- }
474
-
475
- let data = outPipe.fileHandleForReading.readDataToEndOfFile()
476
- return String(data: data, encoding: .utf8)?
477
- .trimmingCharacters(in: .whitespacesAndNewlines)
478
- } catch {
479
- JSONOutput.error("Failed to run osascript: \(error.localizedDescription)")
480
- return nil
481
- }
447
+ return OsascriptRunner.run(script: script, appName: "Numbers")
482
448
  }
483
449
 
484
450
  // MARK: - JXA Execution
485
451
 
486
452
  private static func runJXA(_ script: String) -> String? {
487
- let task = Process()
488
- task.executableURL = URL(fileURLWithPath: "/usr/bin/osascript")
489
- task.arguments = ["-l", "JavaScript", "-e", script]
490
-
491
- let outPipe = Pipe()
492
- let errPipe = Pipe()
493
- task.standardOutput = outPipe
494
- task.standardError = errPipe
495
-
496
- do {
497
- try task.run()
498
- task.waitUntilExit()
499
-
500
- if task.terminationStatus != 0 {
501
- let errData = errPipe.fileHandleForReading.readDataToEndOfFile()
502
- let errStr = String(data: errData, encoding: .utf8)?
503
- .trimmingCharacters(in: .whitespacesAndNewlines) ?? "Unknown error"
504
- JSONOutput.error("JXA error: \(errStr)")
505
- return nil
506
- }
507
-
508
- let data = outPipe.fileHandleForReading.readDataToEndOfFile()
509
- return String(data: data, encoding: .utf8)?
510
- .trimmingCharacters(in: .whitespacesAndNewlines)
511
- } catch {
512
- JSONOutput.error("Failed to run osascript: \(error.localizedDescription)")
513
- return nil
514
- }
453
+ return OsascriptRunner.run(script: script, language: .javaScript, appName: "Numbers")
515
454
  }
516
455
 
517
456
  private static func escapeForAppleScript(_ str: String) -> String {
@@ -0,0 +1,171 @@
1
+ import Foundation
2
+ import Darwin
3
+
4
+ // Reason: Single PID slot for the currently-running osascript child. apple-bridge
5
+ // runs one subcommand per invocation and each runOsascript call is synchronous,
6
+ // so at most one osascript is alive at any moment. Stored as sig_atomic_t so
7
+ // the C-convention signal handler can read it without locks (async-signal-safe).
8
+ // File-scope is required: @convention(c) closures cannot capture Swift state, so
9
+ // the handler reaches it as a C global.
10
+ private var currentChildPid: sig_atomic_t = 0
11
+
12
+ /// Language flavour passed to `osascript`. AppleScript is the default; JXA is
13
+ /// used by Numbers for native JSON output via JavaScript for Automation.
14
+ enum OsascriptLanguage {
15
+ case appleScript
16
+ case javaScript
17
+ }
18
+
19
+ /// Raw result of an osascript invocation. Used by callers that need to inspect
20
+ /// status without emitting a JSON error envelope (e.g. Doctor's probes).
21
+ struct OsascriptResult {
22
+ let status: Int32
23
+ let stdout: String
24
+ let stderr: String
25
+ let timedOut: Bool
26
+ }
27
+
28
+ enum OsascriptRunner {
29
+
30
+ /// Default watchdog window. Long enough for typical iWork/Notes/Mail
31
+ /// operations under load; short enough that the Swift watchdog fires
32
+ /// before node's per-call timeout under default conditions.
33
+ static let defaultTimeout: TimeInterval = 120
34
+
35
+ /// SIGKILL grace period after the initial SIGTERM. Apple Events held by
36
+ /// host apps (Mail, Notes) can keep osascript unresponsive to SIGTERM for
37
+ /// a moment; SIGKILL is uncatchable so the second signal always wins.
38
+ private static let sigkillGrace: TimeInterval = 2
39
+
40
+ /// Install signal handlers that kill the currently-running osascript child
41
+ /// before apple-bridge dies from SIGTERM/SIGINT/SIGHUP. Required because
42
+ /// Foundation.Process on macOS spawns its child into a new process group,
43
+ /// so the node-side group-kill in src/bridge.ts only hits apple-bridge --
44
+ /// the osascript grandchild gets orphaned (PPID=1) and keeps holding
45
+ /// Mail.app's Apple Event queue hostage. The handler is async-signal-safe:
46
+ /// it only reads currentChildPid (sig_atomic_t) and calls kill/signal/raise,
47
+ /// all of which are listed as signal-safe by POSIX.
48
+ static func installSignalHandlers() {
49
+ let handler: @convention(c) (Int32) -> Void = { signo in
50
+ let pid = pid_t(currentChildPid)
51
+ if pid > 0 {
52
+ _ = kill(pid, SIGKILL)
53
+ }
54
+ // Restore default disposition and re-raise so we exit with the
55
+ // standard signal status (and any system-level cleanup runs).
56
+ signal(signo, SIG_DFL)
57
+ raise(signo)
58
+ }
59
+ signal(SIGTERM, handler)
60
+ signal(SIGINT, handler)
61
+ signal(SIGHUP, handler)
62
+ }
63
+
64
+ /// Convenience entry point that emits a `JSONOutput.error` and returns nil
65
+ /// on any failure (timeout, non-zero exit, spawn failure). On success
66
+ /// returns the trimmed stdout. `appName` is used to build the standard
67
+ /// permission-denied / not-running messages that every iWork module emits;
68
+ /// `timeoutHint` is an optional sentence appended to the timeout message
69
+ /// (used by Mail to suggest narrowing the search scope).
70
+ static func run(
71
+ script: String,
72
+ language: OsascriptLanguage = .appleScript,
73
+ timeout: TimeInterval = defaultTimeout,
74
+ appName: String,
75
+ timeoutHint: String? = nil
76
+ ) -> String? {
77
+ guard let result = runRaw(script: script, language: language, timeout: timeout) else {
78
+ JSONOutput.error("Failed to spawn osascript")
79
+ return nil
80
+ }
81
+ if result.timedOut {
82
+ var msg = "\(appName) AppleScript exceeded \(Int(timeout))s timeout - killed to free \(appName)."
83
+ if let hint = timeoutHint {
84
+ msg += " \(hint)"
85
+ }
86
+ JSONOutput.error(msg)
87
+ return nil
88
+ }
89
+ if result.status != 0 {
90
+ let errStr = result.stderr.isEmpty ? "Unknown error" : result.stderr
91
+ if errStr.contains("-1743") || errStr.contains("not allowed") {
92
+ JSONOutput.error("\(appName) automation permission denied. Grant access in System Settings > Privacy & Security > Automation > apple-bridge > \(appName).")
93
+ } else if errStr.contains("-600") || errStr.contains("not running") {
94
+ JSONOutput.error("\(appName) is not running. Open \(appName) and try again.")
95
+ } else {
96
+ JSONOutput.error("AppleScript error: \(errStr)")
97
+ }
98
+ return nil
99
+ }
100
+ return result.stdout
101
+ }
102
+
103
+ /// Raw runner that returns status + stdout + stderr without side effects on
104
+ /// the JSON envelope. Returns nil only on spawn failure. Used by callers
105
+ /// that need to inspect status (e.g. Doctor probing "is Notes accessible").
106
+ static func runRaw(
107
+ script: String,
108
+ language: OsascriptLanguage = .appleScript,
109
+ timeout: TimeInterval = defaultTimeout
110
+ ) -> OsascriptResult? {
111
+ let task = Process()
112
+ task.executableURL = URL(fileURLWithPath: "/usr/bin/osascript")
113
+ switch language {
114
+ case .appleScript:
115
+ task.arguments = ["-e", script]
116
+ case .javaScript:
117
+ task.arguments = ["-l", "JavaScript", "-e", script]
118
+ }
119
+
120
+ let outPipe = Pipe()
121
+ let errPipe = Pipe()
122
+ task.standardOutput = outPipe
123
+ task.standardError = errPipe
124
+
125
+ do {
126
+ try task.run()
127
+ } catch {
128
+ return nil
129
+ }
130
+
131
+ let pid = task.processIdentifier
132
+ currentChildPid = sig_atomic_t(pid)
133
+ defer { currentChildPid = 0 }
134
+
135
+ let timeoutLock = NSLock()
136
+ var didTimeOut = false
137
+
138
+ let watchdog = DispatchWorkItem {
139
+ guard task.isRunning else { return }
140
+ timeoutLock.lock()
141
+ didTimeOut = true
142
+ timeoutLock.unlock()
143
+ task.terminate()
144
+ DispatchQueue.global().asyncAfter(deadline: .now() + sigkillGrace) {
145
+ if task.isRunning { kill(pid, SIGKILL) }
146
+ }
147
+ }
148
+ DispatchQueue.global().asyncAfter(deadline: .now() + timeout, execute: watchdog)
149
+
150
+ task.waitUntilExit()
151
+ watchdog.cancel()
152
+
153
+ let outData = outPipe.fileHandleForReading.readDataToEndOfFile()
154
+ let errData = errPipe.fileHandleForReading.readDataToEndOfFile()
155
+ let stdout = String(data: outData, encoding: .utf8)?
156
+ .trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
157
+ let stderr = String(data: errData, encoding: .utf8)?
158
+ .trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
159
+
160
+ timeoutLock.lock()
161
+ let timedOut = didTimeOut
162
+ timeoutLock.unlock()
163
+
164
+ return OsascriptResult(
165
+ status: task.terminationStatus,
166
+ stdout: stdout,
167
+ stderr: stderr,
168
+ timedOut: timedOut
169
+ )
170
+ }
171
+ }
@@ -403,41 +403,7 @@ enum PagesBridge {
403
403
  // MARK: - AppleScript Execution
404
404
 
405
405
  private static func runAppleScript(_ script: String) -> String? {
406
- let task = Process()
407
- task.executableURL = URL(fileURLWithPath: "/usr/bin/osascript")
408
- task.arguments = ["-e", script]
409
-
410
- let outPipe = Pipe()
411
- let errPipe = Pipe()
412
- task.standardOutput = outPipe
413
- task.standardError = errPipe
414
-
415
- do {
416
- try task.run()
417
- task.waitUntilExit()
418
-
419
- if task.terminationStatus != 0 {
420
- let errData = errPipe.fileHandleForReading.readDataToEndOfFile()
421
- let errStr = String(data: errData, encoding: .utf8)?
422
- .trimmingCharacters(in: .whitespacesAndNewlines) ?? "Unknown error"
423
-
424
- if errStr.contains("-1743") || errStr.contains("not allowed") {
425
- JSONOutput.error("Pages automation permission denied. Grant access in System Settings > Privacy & Security > Automation.")
426
- } else if errStr.contains("-600") || errStr.contains("not running") {
427
- JSONOutput.error("Pages is not running. It will be launched automatically on next attempt.")
428
- } else {
429
- JSONOutput.error("AppleScript error: \(errStr)")
430
- }
431
- return nil
432
- }
433
-
434
- let data = outPipe.fileHandleForReading.readDataToEndOfFile()
435
- return String(data: data, encoding: .utf8)?
436
- .trimmingCharacters(in: .whitespacesAndNewlines)
437
- } catch {
438
- JSONOutput.error("Failed to run osascript: \(error.localizedDescription)")
439
- return nil
440
- }
406
+ return OsascriptRunner.run(script: script, appName: "Pages")
441
407
  }
442
408
 
443
409
  private static func escapeForAppleScript(_ str: String) -> String {