apple-tools-mcp 3.0.3 → 3.0.4

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/lib/mailWrite.js CHANGED
@@ -31,7 +31,7 @@ import {
31
31
  isHardTccDenial
32
32
  } from "./appleScript.js";
33
33
  import { isIndexerMode } from "./processMode.js";
34
- import { safeSpawn } from "./shell.js";
34
+ import { safeSpawnDetached } from "./shell.js";
35
35
  import {
36
36
  planWrite,
37
37
  plannedWriteResult,
@@ -161,16 +161,25 @@ export const BODY_PASTE_MISDIRECTED_SENTINEL = "BODY_PASTE_MISDIRECTED";
161
161
  export const BODY_FOCUS_FAILED_SENTINEL = "BODY_FOCUS_FAILED";
162
162
  /** Focus request ran, but the re-read AXFocusedUIElement was not the chosen body. */
163
163
  export const BODY_FOCUS_UNPROVEN_SENTINEL = "BODY_FOCUS_UNPROVEN";
164
- /** System Events sees 0 Mail windows (display asleep) while Mail AppleScript still lists the compose. */
164
+ /**
165
+ * System Events sees 0 Mail windows while Mail AppleScript still lists the compose:
166
+ * the screen saver is up (loginwindow owns the screen) or the display is asleep.
167
+ */
165
168
  export const DISPLAY_ASLEEP_GUI_UNAVAILABLE_SENTINEL = "DISPLAY_ASLEEP_GUI_UNAVAILABLE";
166
- export const DISPLAY_WAKE_SECONDS = 15;
169
+ /** Outlasts the whole compose osascript (MAIL_COMPOSE_TIMEOUT_MS) so the assertion is live while it runs. */
170
+ export const DISPLAY_WAKE_SECONDS = 30;
167
171
  export const DISPLAY_WAKE_POLL_ATTEMPTS = 20;
168
172
 
169
- /** Declare user activity so a sleeping display wakes before GUI scripting. Best-effort; never throws. */
170
- export function wakeDisplay() {
171
- if (process.platform !== "darwin" || process.env.VITEST) return false;
173
+ /**
174
+ * Declare user activity so a sleeping display wakes before GUI scripting. caffeinate runs
175
+ * detached for its own -t: a blocking spawn would either SIGKILL it early (3.0.3's 5s timeout
176
+ * cut the 15s assertion at ~4s) or release it before compose starts. This does not dismiss a
177
+ * running screen saver; the compose script stops that itself. Best-effort; never throws.
178
+ */
179
+ export function wakeDisplay({ spawnDetached = safeSpawnDetached, platform = process.platform } = {}) {
180
+ if (platform !== "darwin" || (process.env.VITEST && spawnDetached === safeSpawnDetached)) return false;
172
181
  try {
173
- return safeSpawn("/usr/bin/caffeinate", ["-u", "-t", String(DISPLAY_WAKE_SECONDS)], { timeout: 5000 }).status === 0;
182
+ return spawnDetached("/usr/bin/caffeinate", ["-u", "-t", String(DISPLAY_WAKE_SECONDS)]) != null;
174
183
  } catch {
175
184
  return false;
176
185
  }
@@ -634,6 +643,16 @@ on atmAxValueIsRecipientToken(atmVal)
634
643
  return false
635
644
  end atmAxValueIsRecipientToken
636
645
 
646
+ -- A running screen saver keeps loginwindow in front and hides every Mail window from
647
+ -- System Events; caffeinate's user-activity assertion does not dismiss it.
648
+ on atmStopScreenSaver()
649
+ try
650
+ tell application "System Events"
651
+ if running of screen saver preferences then stop current screen saver
652
+ end tell
653
+ end try
654
+ end atmStopScreenSaver
655
+
637
656
  on atmFillMailBody(bodyText, needle, titleMarker, msg)
638
657
  try
639
658
  tell application "Mail" to activate
@@ -644,10 +663,12 @@ on atmFillMailBody(bodyText, needle, titleMarker, msg)
644
663
  set atmSaveBh to -1
645
664
  with timeout of ${seSecs} seconds
646
665
  tell application "System Events"
666
+ my atmStopScreenSaver()
647
667
  tell process "Mail"
648
668
  set frontmost to true
649
669
  repeat ${DISPLAY_WAKE_POLL_ATTEMPTS} times
650
670
  if (count of (every window)) > 0 then exit repeat
671
+ my atmStopScreenSaver()
651
672
  delay 0.5
652
673
  end repeat
653
674
  set atmSeWindowCount to count of (every window)
@@ -1316,7 +1337,7 @@ function failure(action, summary, result, secrets) {
1316
1337
  return `${action} failed — attempted to ${summary}. ${MAIL_GUI_SCRIPTING_GUIDANCE}`;
1317
1338
  }
1318
1339
  if (err.includes("display_asleep_gui_unavailable")) {
1319
- return `${action} failed — attempted to ${summary}. The display is asleep, so System Events cannot see Mail's compose window (GUI unavailable even after a wake attempt); nothing was sent. Wake the display and retry.`;
1340
+ return `${action} failed — attempted to ${summary}. The display is asleep or the screen is locked, so System Events cannot see Mail's compose window (GUI unavailable even after waking the display and stopping the screen saver); nothing was sent. Wake or unlock the Mac and retry.`;
1320
1341
  }
1321
1342
  if (err.includes("body_focus_unproven")) {
1322
1343
  return `${action} failed — attempted to ${summary}. Keyboard focus was not on the message body (focus proof failed); the draft was discarded and nothing was sent.`;
package/lib/shell.js CHANGED
@@ -8,7 +8,7 @@
8
8
  * - Validating inputs before execution
9
9
  */
10
10
 
11
- import { spawnSync } from "child_process";
11
+ import { spawn, spawnSync } from "child_process";
12
12
  import path from "path";
13
13
  import { escapeSQL } from "./validators.js";
14
14
 
@@ -340,6 +340,28 @@ export function safeOpenApp(appName, options = {}) {
340
340
  return String(result.stdout || "").trim();
341
341
  }
342
342
 
343
+ /**
344
+ * Start a command in the background and return immediately (no shell, no
345
+ * timeout, stdio ignored). The child is detached and unref'd, so it is never
346
+ * killed mid-run by a spawnSync timeout and never holds the caller open — for
347
+ * self-terminating helpers like `caffeinate -t N`. Returns the pid, or null if
348
+ * the spawn failed.
349
+ */
350
+ export function safeSpawnDetached(command, args = []) {
351
+ if (!command || typeof command !== 'string') {
352
+ throw new Error('Command is required');
353
+ }
354
+
355
+ if (!Array.isArray(args)) {
356
+ throw new Error('Args must be an array');
357
+ }
358
+
359
+ const child = spawn(command, args, { detached: true, stdio: 'ignore', shell: false });
360
+ child.on('error', () => {});
361
+ child.unref();
362
+ return child.pid ?? null;
363
+ }
364
+
343
365
  export function safeSpawn(command, args = [], options = {}) {
344
366
  const {
345
367
  timeout = 30000,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apple-tools-mcp",
3
- "version": "3.0.3",
3
+ "version": "3.0.4",
4
4
  "description": "MCP server for semantic search and write actions across Apple Mail, Messages, Calendar, and Contacts. Speaks stdio (local) or bearer-token-authenticated Streamable HTTP (LAN / Tailscale).",
5
5
  "type": "module",
6
6
  "main": "index.js",