apple-tools-mcp 2.0.0 → 2.0.2
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 +78 -20
- package/index.js +30 -7
- package/indexer.js +4 -1
- package/lib/appleScript.js +209 -21
- package/lib/calendarWrite.js +835 -23
- package/lib/contactsWrite.js +192 -5
- package/lib/eventKitSession.js +369 -0
- package/lib/mailWrite.js +332 -26
- package/lib/messagesWrite.js +39 -3
- package/lib/permissions.js +360 -0
- package/lib/processMode.js +47 -0
- package/lib/shell.js +50 -5
- package/lib/writeGuards.js +8 -0
- package/lib/writeRouting.js +76 -6
- package/lib/writeTools.js +26 -8
- package/package.json +4 -1
- package/scripts/postinstall.js +32 -0
- package/scripts/smoke-writes.js +188 -17
package/lib/appleScript.js
CHANGED
|
@@ -24,6 +24,7 @@ export const DEFAULT_SCRIPT_TIMEOUT_MS = 30000;
|
|
|
24
24
|
export const NOT_FOUND_SENTINELS = [
|
|
25
25
|
"MESSAGE_NOT_FOUND",
|
|
26
26
|
"EVENT_NOT_FOUND",
|
|
27
|
+
"EVENTKIT_NOT_FOUND",
|
|
27
28
|
"CONTACT_NOT_FOUND",
|
|
28
29
|
"CALENDAR_NOT_FOUND",
|
|
29
30
|
"CHAT_NOT_FOUND",
|
|
@@ -51,29 +52,48 @@ const TCC_SIGNATURES = [
|
|
|
51
52
|
];
|
|
52
53
|
|
|
53
54
|
/**
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
* failures, not as a missing app.
|
|
55
|
+
* Contacts (and sometimes Mail) report these when the app is installed
|
|
56
|
+
* but not launched. That is a cold-start, not an Automation deny.
|
|
57
|
+
* "-1728" / "can't get application" stays on APP_MISSING_SIGNATURES —
|
|
58
|
+
* when the bundle is on disk that is still attribution.
|
|
59
59
|
*/
|
|
60
|
-
const
|
|
60
|
+
const APP_NOT_RUNNING_SIGNATURES = [
|
|
61
61
|
"application isn't running",
|
|
62
|
-
"can't get application",
|
|
63
|
-
"can't get every application",
|
|
64
62
|
"application is not running",
|
|
65
63
|
"-600",
|
|
64
|
+
"contacts_not_running"
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
const APP_MISSING_SIGNATURES = [
|
|
68
|
+
"can't get application",
|
|
69
|
+
"can't get every application",
|
|
66
70
|
"-1728",
|
|
67
71
|
"-10810"
|
|
68
72
|
];
|
|
69
73
|
|
|
70
74
|
// osascript itself is missing: not macOS, or a stripped PATH. Never an
|
|
71
75
|
// attribution problem, because nothing ran.
|
|
76
|
+
//
|
|
77
|
+
// Do **not** match a bare "spawnSync osascript" here. A TCC-denied Mail
|
|
78
|
+
// compose hangs until spawnSync times out (`spawnSync osascript ETIMEDOUT`);
|
|
79
|
+
// that string also contains "spawnSync osascript" and must not be reported
|
|
80
|
+
// as "Mail.app could not be reached".
|
|
72
81
|
const OSASCRIPT_MISSING_SIGNATURES = [
|
|
73
|
-
"spawnsync osascript",
|
|
82
|
+
"spawnsync osascript enoent",
|
|
74
83
|
"enoent"
|
|
75
84
|
];
|
|
76
85
|
|
|
86
|
+
// Hung Apple events. Mini diagnosis: `tell Mail to get name` returns, but
|
|
87
|
+
// `make new outgoing message` blocks until timeout when node → Mail
|
|
88
|
+
// Automation is denied. That hang is a TCC deny, not a missing app.
|
|
89
|
+
const TIMEOUT_SIGNATURES = [
|
|
90
|
+
"etimedout",
|
|
91
|
+
"timed out after",
|
|
92
|
+
"-1712",
|
|
93
|
+
"appleevent timed out",
|
|
94
|
+
"apple event timed out"
|
|
95
|
+
];
|
|
96
|
+
|
|
77
97
|
/**
|
|
78
98
|
* Standard install locations for the apps this package automates.
|
|
79
99
|
* Ventura and later keep the first-party apps in /System/Applications.
|
|
@@ -98,7 +118,7 @@ export function appBundleInstalled(appName, existsFn = fs.existsSync) {
|
|
|
98
118
|
* @param {string} message
|
|
99
119
|
* @param {object} [context]
|
|
100
120
|
* @param {boolean|null} [context.appInstalled] - whether the target app exists
|
|
101
|
-
* @returns {"tcc"|"not_found"|"attribution"|"app_unavailable"|"unknown"}
|
|
121
|
+
* @returns {"tcc"|"timeout"|"not_found"|"attribution"|"app_not_running"|"app_unavailable"|"unknown"}
|
|
102
122
|
*/
|
|
103
123
|
export function classifyAppleScriptError(message, context = {}) {
|
|
104
124
|
const text = String(message || "").toLowerCase();
|
|
@@ -106,7 +126,25 @@ export function classifyAppleScriptError(message, context = {}) {
|
|
|
106
126
|
if (text.includes(sentinel.toLowerCase())) return "not_found";
|
|
107
127
|
}
|
|
108
128
|
if (OSASCRIPT_MISSING_SIGNATURES.some((sig) => text.includes(sig))) return "app_unavailable";
|
|
129
|
+
// ETIMEDOUT / -1712 is a hang, not a TCC deny. Mail maps this kind to
|
|
130
|
+
// MAIL_SEND_TIMEOUT_GUIDANCE (find/reply/open/send), never MAIL_TCC_GUIDANCE.
|
|
131
|
+
// calendar_remove must never print Calendar-denied copy for ETIMEDOUT alone.
|
|
132
|
+
if (TIMEOUT_SIGNATURES.some((sig) => text.includes(sig))) return "timeout";
|
|
109
133
|
if (TCC_SIGNATURES.some((sig) => text.includes(sig))) return "tcc";
|
|
134
|
+
// Calendar delete of a detached event specifier often returns
|
|
135
|
+
// "Can't get event … (-1728)". That is a missing object, not a missing app.
|
|
136
|
+
// Check before the generic -1728 / "can't get application" path.
|
|
137
|
+
if (
|
|
138
|
+
text.includes("can't get event") ||
|
|
139
|
+
text.includes("can't get calendar") ||
|
|
140
|
+
text.includes("can't get theevent")
|
|
141
|
+
) {
|
|
142
|
+
return "not_found";
|
|
143
|
+
}
|
|
144
|
+
// Installed + not running (-600) is a cold launch, not Automation.
|
|
145
|
+
if (APP_NOT_RUNNING_SIGNATURES.some((sig) => text.includes(sig))) {
|
|
146
|
+
return context.appInstalled === true ? "app_not_running" : "app_unavailable";
|
|
147
|
+
}
|
|
110
148
|
if (APP_MISSING_SIGNATURES.some((sig) => text.includes(sig))) {
|
|
111
149
|
// The app is on disk, so "can't get application" means macOS refused to
|
|
112
150
|
// let the responsible process drive it - an Automation / attribution
|
|
@@ -120,14 +158,111 @@ export function isTccDenial(message) {
|
|
|
120
158
|
return classifyAppleScriptError(message) === "tcc";
|
|
121
159
|
}
|
|
122
160
|
|
|
161
|
+
/**
|
|
162
|
+
* Whether dispatchWriteTool should append MacBook/Mini host recovery copy.
|
|
163
|
+
*
|
|
164
|
+
* Handlers rewrite raw Apple events as CONTACTS_/CALENDAR_/MESSAGES_/MAIL
|
|
165
|
+
* TCC guidance or ATTRIBUTION_GUIDANCE, which do not contain the -1743 /
|
|
166
|
+
* -10004 signatures `isTccDenial` looks for (except MAIL_TCC_GUIDANCE).
|
|
167
|
+
* MAIL_SEND_TIMEOUT_GUIDANCE must never match — that hang is not a deny.
|
|
168
|
+
*/
|
|
169
|
+
export function needsHostTccAdvice(message) {
|
|
170
|
+
const text = String(message || "");
|
|
171
|
+
if (!text) return false;
|
|
172
|
+
if (text.includes(MAIL_SEND_TIMEOUT_GUIDANCE)) return false;
|
|
173
|
+
if (text.includes(CONTACTS_APP_NOT_RUNNING_GUIDANCE)) return false;
|
|
174
|
+
if (text.includes(MAIL_APP_NOT_RUNNING_GUIDANCE)) return false;
|
|
175
|
+
if (text.includes(MESSAGES_APP_NOT_RUNNING_GUIDANCE)) return false;
|
|
176
|
+
if (text.includes(CALENDAR_APP_NOT_RUNNING_GUIDANCE)) return false;
|
|
177
|
+
if (
|
|
178
|
+
text.includes(CONTACTS_TCC_GUIDANCE) ||
|
|
179
|
+
text.includes(CALENDAR_TCC_GUIDANCE) ||
|
|
180
|
+
text.includes(MESSAGES_TCC_GUIDANCE) ||
|
|
181
|
+
text.includes(MAIL_TCC_GUIDANCE) ||
|
|
182
|
+
text.includes(ATTRIBUTION_GUIDANCE) ||
|
|
183
|
+
text.includes(TCC_GUIDANCE)
|
|
184
|
+
) {
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
187
|
+
return isTccDenial(text);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Real Automation / privacy deny codes — not a hung Calendar.app delete.
|
|
192
|
+
*
|
|
193
|
+
* Mini calendar_remove: add/edit succeed (Automation granted), then delete
|
|
194
|
+
* hits spawnSync ETIMEDOUT / AppleEvent -1712. That is an iCloud/CalDAV
|
|
195
|
+
* hang or a confirmation dialog, not -1743 / -10004. Callers must not
|
|
196
|
+
* rewrite that as CALENDAR_TCC_GUIDANCE.
|
|
197
|
+
*/
|
|
198
|
+
const HARD_TCC_SIGNATURES = [
|
|
199
|
+
"-1743",
|
|
200
|
+
"-10004",
|
|
201
|
+
"-25211",
|
|
202
|
+
"not authorized to send apple events",
|
|
203
|
+
"not allowed to send apple events",
|
|
204
|
+
"is not allowed assistive access",
|
|
205
|
+
"operation not permitted",
|
|
206
|
+
"not permitted to access",
|
|
207
|
+
"access to contacts",
|
|
208
|
+
"access to calendars",
|
|
209
|
+
"privacy settings",
|
|
210
|
+
"errae eventnotpermitted",
|
|
211
|
+
"errAEEventNotPermitted".toLowerCase()
|
|
212
|
+
];
|
|
213
|
+
|
|
214
|
+
export function isHardTccDenial(message) {
|
|
215
|
+
const text = String(message || "").toLowerCase();
|
|
216
|
+
return HARD_TCC_SIGNATURES.some((sig) => text.includes(sig));
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const APPLEEVENT_CODES = [
|
|
220
|
+
"-1743",
|
|
221
|
+
"-10004",
|
|
222
|
+
"-25211",
|
|
223
|
+
"-1712",
|
|
224
|
+
"-1728",
|
|
225
|
+
"-600",
|
|
226
|
+
"-10810",
|
|
227
|
+
"-1708",
|
|
228
|
+
"-1719",
|
|
229
|
+
"-10025",
|
|
230
|
+
"-2700"
|
|
231
|
+
];
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Pull known AppleEvent / spawn codes out of osascript stderr so smoke
|
|
235
|
+
* can print them instead of guessing TCC.
|
|
236
|
+
*/
|
|
237
|
+
export function extractAppleEventCodes(message) {
|
|
238
|
+
const text = String(message || "");
|
|
239
|
+
const found = [];
|
|
240
|
+
for (const code of APPLEEVENT_CODES) {
|
|
241
|
+
if (text.includes(code) && !found.includes(code)) found.push(code);
|
|
242
|
+
}
|
|
243
|
+
if (/etimedout/i.test(text) && !found.includes("ETIMEDOUT")) found.push("ETIMEDOUT");
|
|
244
|
+
return found;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* One-line osascript diagnostic. Always include this on calendar_remove
|
|
249
|
+
* failure so Mini --apply stops looking like a TCC deny.
|
|
250
|
+
*/
|
|
251
|
+
export function formatOsascriptDiagnostic(result, source = "osascript") {
|
|
252
|
+
const raw = String(result && result.error ? result.error : "").replace(/\s+/g, " ").trim();
|
|
253
|
+
const clipped = raw.length > 360 ? `${raw.slice(0, 360)}...` : raw;
|
|
254
|
+
const kind = (result && result.kind) || "unknown";
|
|
255
|
+
const codes = extractAppleEventCodes(raw);
|
|
256
|
+
return `${source} kind=${kind} error=${clipped || "(empty)"}${codes.length ? ` codes=${codes.join(",")}` : ""}`;
|
|
257
|
+
}
|
|
258
|
+
|
|
123
259
|
/**
|
|
124
260
|
* Guidance attached to TCC denials. Names the host constraint without
|
|
125
261
|
* claiming the package can grant another app's entitlements.
|
|
126
262
|
*/
|
|
127
263
|
export const TCC_GUIDANCE =
|
|
128
264
|
"macOS denied this automation. TCC attributes Apple events to the process responsible for the MCP server, " +
|
|
129
|
-
"so a host app without the matching automation grant blocks the write even when node has Full Disk Access.
|
|
130
|
-
"Run the indexer daemon (apple-tools-indexer / the LaunchAgent) so writes execute under node, or run this server from a host that can be granted Automation access.";
|
|
265
|
+
"so a host app without the matching automation grant blocks the write even when node has Full Disk Access.";
|
|
131
266
|
|
|
132
267
|
/**
|
|
133
268
|
* Contacts writes are the sharpest case, and worth separating from contacts
|
|
@@ -141,8 +276,28 @@ export const CONTACTS_TCC_GUIDANCE =
|
|
|
141
276
|
"macOS denied Contacts access for this write. Contacts writes go through Contacts.app (CNContactStore), which is gated by the AddressBook privacy class " +
|
|
142
277
|
"and attributed to the process responsible for this MCP server - not to node. A host app that was built without the AddressBook entitlement " +
|
|
143
278
|
"(com.apple.security.personal-information.addressbook) is denied with no prompt, and that cannot be fixed with Full Disk Access or tccutil. " +
|
|
144
|
-
"Contacts *reads* are unaffected: they query the AddressBook database directly and only need Full Disk Access.
|
|
145
|
-
|
|
279
|
+
"Contacts *reads* are unaffected: they query the AddressBook database directly and only need Full Disk Access.";
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Contacts.app was installed but not running. Cold `tell application
|
|
283
|
+
* "Contacts"` under launchd often returns -600 instead of auto-launching.
|
|
284
|
+
* That is not -1743 / -10004 and not attribution.
|
|
285
|
+
*/
|
|
286
|
+
export const CONTACTS_APP_NOT_RUNNING_GUIDANCE =
|
|
287
|
+
"Contacts.app was not running and did not become ready after launch. This is not a TCC / Automation deny (-1743 / -10004) " +
|
|
288
|
+
"and not an attribution / responsible-process failure. The write path launches Contacts.app; if this persists, open Contacts.app and retry.";
|
|
289
|
+
|
|
290
|
+
export const MAIL_APP_NOT_RUNNING_GUIDANCE =
|
|
291
|
+
"Mail.app was not running. Keep Mail, Messages, and Contacts running for write reliability. " +
|
|
292
|
+
"This is not a TCC / Automation deny (-1743 / -10004) and not an attribution / responsible-process failure.";
|
|
293
|
+
|
|
294
|
+
export const MESSAGES_APP_NOT_RUNNING_GUIDANCE =
|
|
295
|
+
"Messages.app was not running. Keep Mail, Messages, and Contacts running for write reliability. " +
|
|
296
|
+
"This is not a TCC / Automation deny (-1743 / -10004) and not an attribution / responsible-process failure.";
|
|
297
|
+
|
|
298
|
+
export const CALENDAR_APP_NOT_RUNNING_GUIDANCE =
|
|
299
|
+
"Calendar.app was not running. Calendar writes use EventKit and do not require Calendar.app to stay open. " +
|
|
300
|
+
"This is not a TCC / Automation deny (-1743 / -10004).";
|
|
146
301
|
|
|
147
302
|
/**
|
|
148
303
|
* Calendar has the same split as Contacts: reads in this package are sqlite
|
|
@@ -153,8 +308,7 @@ export const CALENDAR_TCC_GUIDANCE =
|
|
|
153
308
|
"macOS denied Calendar access for this write. Calendar writes go through Calendar.app (EventKit), which is gated by the calendars privacy class " +
|
|
154
309
|
"(com.apple.security.personal-information.calendars) and attributed to the process responsible for this MCP server - not to node. " +
|
|
155
310
|
"A host app built without that entitlement is denied with no prompt, and Full Disk Access or tccutil cannot change it. " +
|
|
156
|
-
"Calendar *reads* are unaffected: they query Calendar.sqlitedb directly and only need Full Disk Access.
|
|
157
|
-
"Run the indexer daemon (apple-tools-indexer / the LaunchAgent) so Calendar writes execute under node, which macOS can grant Calendars access to.";
|
|
311
|
+
"Calendar *reads* are unaffected: they query Calendar.sqlitedb directly and only need Full Disk Access.";
|
|
158
312
|
|
|
159
313
|
/**
|
|
160
314
|
* Shown when the target app is installed but macOS still refused the Apple
|
|
@@ -164,15 +318,49 @@ export const CALENDAR_TCC_GUIDANCE =
|
|
|
164
318
|
*/
|
|
165
319
|
export const ATTRIBUTION_GUIDANCE =
|
|
166
320
|
"The app is installed, so this is an Automation / responsible-process problem rather than a missing app: macOS refused to let the process " +
|
|
167
|
-
"responsible for this server drive it.
|
|
168
|
-
|
|
321
|
+
"responsible for this server drive it.";
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Mail writes are gated by Automation → Mail for the responsible process.
|
|
325
|
+
* Shown only for a hard deny (-1743 / -10004 / "not authorized…").
|
|
326
|
+
* ETIMEDOUT / -1712 is `MAIL_SEND_TIMEOUT_GUIDANCE`, including find/reply/open
|
|
327
|
+
* before send. dry_run never sends Apple events to Mail.
|
|
328
|
+
*/
|
|
329
|
+
export const MAIL_TCC_GUIDANCE =
|
|
330
|
+
"macOS denied Mail automation (Apple Events to Mail.app). Real denials report -1743, -10004, or \"not authorized to send Apple events\" — a TCC / Automation deny for node → Mail, " +
|
|
331
|
+
"not Mail.app missing or unavailable. dry_run never talks to Mail, so it cannot detect this grant. " +
|
|
332
|
+
"Allow node in System Settings → Privacy & Security → Automation for Mail (same Allow-via-prompt as Contacts and Calendar — do not add node via + in a privacy list). " +
|
|
333
|
+
"A Contacts or Calendar grant does not include Mail.";
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* AppleScript hung (ETIMEDOUT / -1712) while finding, opening, replying,
|
|
337
|
+
* or sending. That is not -1743 / -10004, including before send.
|
|
338
|
+
* Clients must Sent-check before retrying a send — a retry of a delivered
|
|
339
|
+
* send creates a second copy. This is not a silent TCC grant.
|
|
340
|
+
*/
|
|
341
|
+
export const MAIL_SEND_TIMEOUT_GUIDANCE =
|
|
342
|
+
"Mail AppleScript timed out (ETIMEDOUT / -1712 / AppleEvent timed out) — this is a find/reply/send hang (including find/reply/open before send), not a TCC / Automation deny. " +
|
|
343
|
+
"Real Mail Automation denials report -1743, -10004, or \"not authorized to send Apple events\". " +
|
|
344
|
+
"A timeout can happen after Mail already delivered the message. Check Sent (and Outbox) for this send before retrying; a retry after a successful delivery sends a second copy. " +
|
|
345
|
+
"This tool verifies Sent when a send hangs; if nothing is there yet, wait and look again rather than immediately resending.";
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Messages writes are a separate Automation target from Mail / Contacts / Calendar.
|
|
349
|
+
*/
|
|
350
|
+
export const MESSAGES_TCC_GUIDANCE =
|
|
351
|
+
"macOS denied Messages automation (Apple Events to Messages.app). A hang or timeout on send is a TCC / Automation deny for node → Messages, " +
|
|
352
|
+
"not Messages.app missing or unavailable. dry_run never talks to Messages, so it cannot detect this grant. " +
|
|
353
|
+
"Allow node in System Settings → Privacy & Security → Automation for Messages (same Allow-via-prompt as Mail, Contacts, and Calendar — do not add node via + in a privacy list). " +
|
|
354
|
+
"A Contacts or Calendar grant does not include Messages.";
|
|
169
355
|
|
|
170
356
|
/**
|
|
171
|
-
* @param {"contacts"|"calendar"|string} source
|
|
357
|
+
* @param {"contacts"|"calendar"|"mail"|"messages"|string} source
|
|
172
358
|
*/
|
|
173
359
|
export function tccGuidanceFor(source) {
|
|
174
360
|
if (source === "contacts") return CONTACTS_TCC_GUIDANCE;
|
|
175
361
|
if (source === "calendar") return CALENDAR_TCC_GUIDANCE;
|
|
362
|
+
if (source === "mail") return MAIL_TCC_GUIDANCE;
|
|
363
|
+
if (source === "messages") return MESSAGES_TCC_GUIDANCE;
|
|
176
364
|
return TCC_GUIDANCE;
|
|
177
365
|
}
|
|
178
366
|
|
|
@@ -184,9 +372,9 @@ export function tccGuidanceFor(source) {
|
|
|
184
372
|
* @returns {{ ok: boolean, output: string, error: string|null, kind: string|null }}
|
|
185
373
|
*/
|
|
186
374
|
export function runAppleScript(script, options = {}) {
|
|
187
|
-
const { timeout = DEFAULT_SCRIPT_TIMEOUT_MS, appName = null } = options;
|
|
375
|
+
const { timeout = DEFAULT_SCRIPT_TIMEOUT_MS, appName = null, language = null } = options;
|
|
188
376
|
try {
|
|
189
|
-
const output = safeOsascript(script, { timeout });
|
|
377
|
+
const output = safeOsascript(script, { timeout, language });
|
|
190
378
|
return { ok: true, output: (output || "").trim(), error: null, kind: null };
|
|
191
379
|
} catch (e) {
|
|
192
380
|
const message = e && e.message ? e.message : String(e);
|