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/contactsWrite.js
CHANGED
|
@@ -9,7 +9,15 @@
|
|
|
9
9
|
* writing that database directly corrupts iCloud sync.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
runAppleScript,
|
|
14
|
+
asString,
|
|
15
|
+
formatOsascriptDiagnostic,
|
|
16
|
+
CONTACTS_TCC_GUIDANCE,
|
|
17
|
+
CONTACTS_APP_NOT_RUNNING_GUIDANCE,
|
|
18
|
+
ATTRIBUTION_GUIDANCE
|
|
19
|
+
} from "./appleScript.js";
|
|
20
|
+
import { safeOpenApp } from "./shell.js";
|
|
13
21
|
import {
|
|
14
22
|
planWrite,
|
|
15
23
|
normalizeList,
|
|
@@ -75,14 +83,128 @@ function childLines(items, kind, personVar) {
|
|
|
75
83
|
.join("\n");
|
|
76
84
|
}
|
|
77
85
|
|
|
86
|
+
export const CONTACTS_READY_ATTEMPTS = 16;
|
|
87
|
+
export const CONTACTS_READY_INTERVAL_MS = 500;
|
|
88
|
+
export const CONTACTS_OPEN_RETRY_EVERY = 4;
|
|
89
|
+
/** Wall-clock cap so ensure + CRUD stay under the 90s write-bridge timeout. */
|
|
90
|
+
export const CONTACTS_READY_BUDGET_MS = 20000;
|
|
91
|
+
export const CONTACTS_LAUNCH_TIMEOUT_MS = 4000;
|
|
92
|
+
export const CONTACTS_READY_SCRIPT_TIMEOUT_MS = 2000;
|
|
93
|
+
|
|
94
|
+
export function buildContactsLaunchScript() {
|
|
95
|
+
// launch only — activate waits until Contacts is frontmost and a slow
|
|
96
|
+
// cold start can ETIMEDOUT, which must not abort the ready poll as TCC.
|
|
97
|
+
return `tell application "Contacts" to launch`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function buildContactsReadyScript() {
|
|
101
|
+
return `tell application "Contacts" to get name`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function contactsAppIsReady(output) {
|
|
105
|
+
const text = String(output || "").trim().toLowerCase();
|
|
106
|
+
return text === "contacts" || text === "ready";
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function sleepMs(ms) {
|
|
110
|
+
if (!Number.isFinite(ms) || ms <= 0) return;
|
|
111
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, Math.min(ms, 5000));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Cold `tell application "Contacts"` under launchd often returns -600
|
|
116
|
+
* instead of auto-launching. Prefer `open -a Contacts` in *this* process
|
|
117
|
+
* (the write-bridge daemon when routed), then `launch` (not `activate`),
|
|
118
|
+
* then poll until a trivial AppleScript (`get name`) responds.
|
|
119
|
+
* A launch/ready ETIMEDOUT is a slow cold start, not Automation — keep polling.
|
|
120
|
+
*/
|
|
121
|
+
function isFatalContactsReadyKind(kind) {
|
|
122
|
+
return kind === "tcc" || kind === "attribution" || kind === "app_unavailable";
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function tryOpenContacts(openApp) {
|
|
126
|
+
try {
|
|
127
|
+
openApp("Contacts");
|
|
128
|
+
return { ok: true };
|
|
129
|
+
} catch (e) {
|
|
130
|
+
return { ok: false, error: e && e.message ? e.message : String(e) };
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function ensureContactsAppReady({
|
|
135
|
+
run = runAppleScript,
|
|
136
|
+
sleep = sleepMs,
|
|
137
|
+
openApp = safeOpenApp,
|
|
138
|
+
attempts = CONTACTS_READY_ATTEMPTS,
|
|
139
|
+
intervalMs = CONTACTS_READY_INTERVAL_MS,
|
|
140
|
+
openRetryEvery = CONTACTS_OPEN_RETRY_EVERY,
|
|
141
|
+
budgetMs = CONTACTS_READY_BUDGET_MS,
|
|
142
|
+
launchTimeoutMs = CONTACTS_LAUNCH_TIMEOUT_MS,
|
|
143
|
+
readyTimeoutMs = CONTACTS_READY_SCRIPT_TIMEOUT_MS,
|
|
144
|
+
now = Date.now
|
|
145
|
+
} = {}) {
|
|
146
|
+
const started = now();
|
|
147
|
+
const remaining = () => budgetMs - (now() - started);
|
|
148
|
+
const slice = (maxMs) => Math.max(0, Math.min(maxMs, remaining()));
|
|
149
|
+
|
|
150
|
+
tryOpenContacts(openApp);
|
|
151
|
+
|
|
152
|
+
const launchBudget = slice(launchTimeoutMs);
|
|
153
|
+
const launch = launchBudget > 0
|
|
154
|
+
? run(buildContactsLaunchScript(), { timeout: launchBudget, appName: "Contacts" })
|
|
155
|
+
: { ok: false, kind: "app_not_running", error: "Contacts ensure budget exhausted before launch", output: "" };
|
|
156
|
+
if (!launch.ok && isFatalContactsReadyKind(launch.kind)) {
|
|
157
|
+
return launch;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const tries = Math.max(1, attempts);
|
|
161
|
+
const retryEvery = Math.max(0, openRetryEvery);
|
|
162
|
+
let last = launch;
|
|
163
|
+
for (let i = 0; i < tries; i++) {
|
|
164
|
+
if (remaining() <= 0) break;
|
|
165
|
+
if (i > 0 && retryEvery > 0 && i % retryEvery === 0) {
|
|
166
|
+
tryOpenContacts(openApp);
|
|
167
|
+
}
|
|
168
|
+
const readyBudget = slice(readyTimeoutMs);
|
|
169
|
+
if (readyBudget <= 0) break;
|
|
170
|
+
const ready = run(buildContactsReadyScript(), { timeout: readyBudget, appName: "Contacts" });
|
|
171
|
+
last = ready;
|
|
172
|
+
if (ready.ok && contactsAppIsReady(ready.output)) {
|
|
173
|
+
return { ok: true, kind: null };
|
|
174
|
+
}
|
|
175
|
+
if (!ready.ok && isFatalContactsReadyKind(ready.kind)) {
|
|
176
|
+
return ready;
|
|
177
|
+
}
|
|
178
|
+
const pause = slice(intervalMs);
|
|
179
|
+
if (i < tries - 1 && pause > 0) sleep(pause);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (last && last.ok === false && isFatalContactsReadyKind(last.kind)) {
|
|
183
|
+
return last;
|
|
184
|
+
}
|
|
185
|
+
if (last && last.ok === false && last.kind === "app_not_running") {
|
|
186
|
+
return last;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return {
|
|
190
|
+
ok: false,
|
|
191
|
+
kind: "app_not_running",
|
|
192
|
+
error: (last && last.error) || (launch && launch.error) || "Contacts.app did not become ready after launch",
|
|
193
|
+
output: ""
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
78
197
|
function failure(action, summary, result, secrets = []) {
|
|
79
|
-
if (result.kind === "tcc") {
|
|
198
|
+
if (result.kind === "tcc" || result.kind === "timeout") {
|
|
80
199
|
return `${action} failed — attempted to ${summary}. ${CONTACTS_TCC_GUIDANCE}`;
|
|
81
200
|
}
|
|
82
201
|
const raw = String(result.error || "");
|
|
83
202
|
if (raw.includes("CONTACT_NOT_FOUND")) {
|
|
84
203
|
return `${action} failed — attempted to ${summary}. No contact with that id was found; use the Contact ID from contacts_search or contacts_lookup.`;
|
|
85
204
|
}
|
|
205
|
+
if (result.kind === "app_not_running") {
|
|
206
|
+
return `${action} failed — attempted to ${summary}. ${CONTACTS_APP_NOT_RUNNING_GUIDANCE} ${formatOsascriptDiagnostic(result)}`;
|
|
207
|
+
}
|
|
86
208
|
if (result.kind === "attribution") {
|
|
87
209
|
return `${action} failed — attempted to ${summary}. ${ATTRIBUTION_GUIDANCE}`;
|
|
88
210
|
}
|
|
@@ -105,7 +227,7 @@ end tell
|
|
|
105
227
|
return newId`;
|
|
106
228
|
}
|
|
107
229
|
|
|
108
|
-
export function contactsAdd(args = {}) {
|
|
230
|
+
export function contactsAdd(args = {}, deps = {}) {
|
|
109
231
|
const action = "contacts_add";
|
|
110
232
|
|
|
111
233
|
const firstName = validateSubject(args.first_name);
|
|
@@ -140,6 +262,9 @@ export function contactsAdd(args = {}) {
|
|
|
140
262
|
if (organization.text) properties.organization = organization.text;
|
|
141
263
|
if (jobTitle.text) properties["job title"] = jobTitle.text;
|
|
142
264
|
|
|
265
|
+
const ready = ensureContactsAppReady(deps);
|
|
266
|
+
if (!ready.ok) return { ok: false, message: failure(action, summary, ready) };
|
|
267
|
+
|
|
143
268
|
const result = runAppleScript(
|
|
144
269
|
buildAddContactScript({ properties, emails: emails.emails, phones: phones.phones }),
|
|
145
270
|
{ timeout: 60000, appName: "Contacts" }
|
|
@@ -181,7 +306,7 @@ end tell
|
|
|
181
306
|
return editedName`;
|
|
182
307
|
}
|
|
183
308
|
|
|
184
|
-
export function contactsEdit(args = {}) {
|
|
309
|
+
export function contactsEdit(args = {}, deps = {}) {
|
|
185
310
|
const action = "contacts_edit";
|
|
186
311
|
|
|
187
312
|
const contactId = validateContactId(args.contact_id);
|
|
@@ -234,6 +359,9 @@ export function contactsEdit(args = {}) {
|
|
|
234
359
|
});
|
|
235
360
|
if (!plan.proceed) return { ok: true, message: plan.message, planned: true };
|
|
236
361
|
|
|
362
|
+
const ready = ensureContactsAppReady(deps);
|
|
363
|
+
if (!ready.ok) return { ok: false, message: failure(action, summary, ready) };
|
|
364
|
+
|
|
237
365
|
const result = runAppleScript(
|
|
238
366
|
buildEditContactScript({
|
|
239
367
|
contactId,
|
|
@@ -269,7 +397,7 @@ end tell
|
|
|
269
397
|
return removedName`;
|
|
270
398
|
}
|
|
271
399
|
|
|
272
|
-
export function contactsRemove(args = {}) {
|
|
400
|
+
export function contactsRemove(args = {}, deps = {}) {
|
|
273
401
|
const action = "contacts_remove";
|
|
274
402
|
|
|
275
403
|
const contactId = validateContactId(args.contact_id);
|
|
@@ -287,6 +415,9 @@ export function contactsRemove(args = {}) {
|
|
|
287
415
|
});
|
|
288
416
|
if (!plan.proceed) return { ok: true, message: plan.message, planned: true };
|
|
289
417
|
|
|
418
|
+
const ready = ensureContactsAppReady(deps);
|
|
419
|
+
if (!ready.ok) return { ok: false, message: failure(action, summary, ready) };
|
|
420
|
+
|
|
290
421
|
const result = runAppleScript(buildRemoveContactScript(contactId), { timeout: 60000, appName: "Contacts" });
|
|
291
422
|
if (!result.ok) return { ok: false, message: failure(action, summary, result) };
|
|
292
423
|
|
|
@@ -298,3 +429,59 @@ export function contactsRemove(args = {}) {
|
|
|
298
429
|
})
|
|
299
430
|
};
|
|
300
431
|
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* First-run / upgrade probe: the Contacts write verb that triggers
|
|
435
|
+
* Automation → Contacts. Creates a clearly named throwaway person and
|
|
436
|
+
* deletes it in the same script so a completed run leaves no junk.
|
|
437
|
+
*/
|
|
438
|
+
export const CONTACTS_AUTOMATION_PROBE_FIRST = "ATM";
|
|
439
|
+
export const CONTACTS_AUTOMATION_PROBE_LAST = "Permissions Probe";
|
|
440
|
+
|
|
441
|
+
export function buildContactsAutomationProbeScript() {
|
|
442
|
+
return `tell application "Contacts"
|
|
443
|
+
set probe to make new person with properties {first name:${asString(CONTACTS_AUTOMATION_PROBE_FIRST)}, last name:${asString(CONTACTS_AUTOMATION_PROBE_LAST)}}
|
|
444
|
+
delete probe
|
|
445
|
+
save
|
|
446
|
+
end tell
|
|
447
|
+
return "OK"`;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
export function buildContactsProbeCleanupScript() {
|
|
451
|
+
return `tell application "Contacts"
|
|
452
|
+
set leftovers to (every person whose first name is ${asString(CONTACTS_AUTOMATION_PROBE_FIRST)} and last name is ${asString(CONTACTS_AUTOMATION_PROBE_LAST)})
|
|
453
|
+
repeat with p in leftovers
|
|
454
|
+
delete p
|
|
455
|
+
end repeat
|
|
456
|
+
save
|
|
457
|
+
end tell
|
|
458
|
+
return "OK"`;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Live Contacts Apple Events check. Not a dry_run.
|
|
463
|
+
* @returns {{ ok: boolean, message: string, kind: string|null }}
|
|
464
|
+
*/
|
|
465
|
+
export function probeContactsAutomation(deps = {}) {
|
|
466
|
+
const action = "contacts_automation_probe";
|
|
467
|
+
const summary = "create and delete a throwaway Contacts person (Automation / AddressBook check)";
|
|
468
|
+
const ready = ensureContactsAppReady(deps);
|
|
469
|
+
if (!ready.ok) {
|
|
470
|
+
runAppleScript(buildContactsProbeCleanupScript(), { timeout: 30000, appName: "Contacts" });
|
|
471
|
+
return { ok: false, message: failure(action, summary, ready), kind: ready.kind };
|
|
472
|
+
}
|
|
473
|
+
const result = runAppleScript(buildContactsAutomationProbeScript(), { timeout: 60000, appName: "Contacts" });
|
|
474
|
+
// Best-effort cleanup if the probe created the person then failed mid-script.
|
|
475
|
+
runAppleScript(buildContactsProbeCleanupScript(), { timeout: 30000, appName: "Contacts" });
|
|
476
|
+
if (!result.ok) {
|
|
477
|
+
return { ok: false, message: failure(action, summary, result), kind: result.kind };
|
|
478
|
+
}
|
|
479
|
+
return {
|
|
480
|
+
ok: true,
|
|
481
|
+
kind: null,
|
|
482
|
+
message: writeSuccessMessage(
|
|
483
|
+
action,
|
|
484
|
+
"Contacts Automation allowed (created and deleted a throwaway contact)"
|
|
485
|
+
)
|
|
486
|
+
};
|
|
487
|
+
}
|
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Long-lived EventKit JXA worker.
|
|
3
|
+
*
|
|
4
|
+
* writeOnly (EKAuthorizationStatus = 4) can create an EKEvent and read
|
|
5
|
+
* identifiers off that in-memory object, but cannot re-query via
|
|
6
|
+
* eventWithIdentifier / calendarItemsWithExternalIdentifier — Mini 2cdf44d
|
|
7
|
+
* failed add on a post-save lookup even though create returned ids.
|
|
8
|
+
*
|
|
9
|
+
* The indexer daemon therefore keeps one osascript process around and
|
|
10
|
+
* caches the EKEvent objects from create so edit/remove can call
|
|
11
|
+
* saveEvent / removeEvent without a fetch.
|
|
12
|
+
*
|
|
13
|
+
* stdin/stdout: one JSON object per line. Never interpolates commands
|
|
14
|
+
* into a shell (`spawn`, `shell: false`).
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { spawn } from "child_process";
|
|
18
|
+
import fs from "fs";
|
|
19
|
+
import os from "os";
|
|
20
|
+
import path from "path";
|
|
21
|
+
|
|
22
|
+
let singleton = null;
|
|
23
|
+
|
|
24
|
+
export function setEventKitSession(session) {
|
|
25
|
+
singleton = session || null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function getEventKitSession() {
|
|
29
|
+
return singleton;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function shouldStartEventKitSession({
|
|
33
|
+
platform = process.platform,
|
|
34
|
+
env = process.env
|
|
35
|
+
} = {}) {
|
|
36
|
+
if (platform !== "darwin") return false;
|
|
37
|
+
if (env.VITEST) return false;
|
|
38
|
+
if (env.APPLE_TOOLS_EVENTKIT_SESSION === "0") return false;
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* JXA worker: create caches the EKEvent; update/remove use the cache.
|
|
44
|
+
* create does not call findEventByIds.
|
|
45
|
+
*/
|
|
46
|
+
export function sessionPaths(home = process.env.HOME || "") {
|
|
47
|
+
const root = home ? path.join(home, ".apple-tools-mcp") : path.join(os.tmpdir(), "atm-eventkit");
|
|
48
|
+
const dir = path.join(root, "eventkit-session");
|
|
49
|
+
return {
|
|
50
|
+
root,
|
|
51
|
+
dir,
|
|
52
|
+
scriptPath: path.join(root, "eventkit-worker.jxa"),
|
|
53
|
+
cmdPath: path.join(dir, "cmd.json"),
|
|
54
|
+
rspPath: path.join(dir, "rsp.json")
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function buildEventKitWorkerScript(helpers, paths = {}) {
|
|
59
|
+
const cmdPath = JSON.stringify(paths.cmdPath || "");
|
|
60
|
+
const rspPath = JSON.stringify(paths.rspPath || "");
|
|
61
|
+
return `ObjC.import("EventKit");
|
|
62
|
+
ObjC.import("Foundation");
|
|
63
|
+
${helpers}
|
|
64
|
+
|
|
65
|
+
var store = $.EKEventStore.alloc.init;
|
|
66
|
+
var cache = {};
|
|
67
|
+
|
|
68
|
+
function remember(ev) {
|
|
69
|
+
if (!ev) return;
|
|
70
|
+
var keys = [
|
|
71
|
+
jsString(ev.eventIdentifier),
|
|
72
|
+
jsString(ev.calendarItemIdentifier),
|
|
73
|
+
jsString(ev.calendarItemExternalIdentifier)
|
|
74
|
+
];
|
|
75
|
+
for (var i = 0; i < keys.length; i++) {
|
|
76
|
+
if (keys[i]) cache[keys[i]] = ev;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function forget(ev) {
|
|
81
|
+
var keys = [];
|
|
82
|
+
for (var k in cache) keys.push(k);
|
|
83
|
+
for (var i = 0; i < keys.length; i++) {
|
|
84
|
+
if (cache[keys[i]] === ev) delete cache[keys[i]];
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function resolve(ids) {
|
|
89
|
+
if (!ids) return null;
|
|
90
|
+
for (var i = 0; i < ids.length; i++) {
|
|
91
|
+
var id = ids[i];
|
|
92
|
+
if (id && cache[id]) return cache[id];
|
|
93
|
+
}
|
|
94
|
+
return findEventByIds(store, ids);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
var cmdPath = ${cmdPath};
|
|
98
|
+
var rspPath = ${rspPath};
|
|
99
|
+
|
|
100
|
+
function writeReply(s) {
|
|
101
|
+
$.NSString.stringWithString(String(s) + "\\n").writeToFileAtomicallyEncodingError(rspPath, true, $.NSUTF8StringEncoding, null);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function fileExists(p) {
|
|
105
|
+
try { return !!$.NSFileManager.defaultManager.fileExistsAtPath(p); } catch (e) { return false; }
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function readCmd() {
|
|
109
|
+
var str = $.NSString.stringWithContentsOfFileEncodingError(cmdPath, $.NSUTF8StringEncoding, null);
|
|
110
|
+
return jsString(str);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function removeCmd() {
|
|
114
|
+
try { $.NSFileManager.defaultManager.removeItemAtPathError(cmdPath, null); } catch (e) {}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function pickCalendar(calendarName) {
|
|
118
|
+
var defaultCal = null;
|
|
119
|
+
try { defaultCal = store.defaultCalendarForNewEvents; } catch (e) {}
|
|
120
|
+
var cals = null;
|
|
121
|
+
var count = 0;
|
|
122
|
+
try {
|
|
123
|
+
cals = store.calendarsForEntityType($.EKEntityTypeEvent);
|
|
124
|
+
count = cals ? Number(cals.count) : 0;
|
|
125
|
+
} catch (e) {}
|
|
126
|
+
var target = null;
|
|
127
|
+
var writables = [];
|
|
128
|
+
var wanted = String(calendarName || "").toLowerCase();
|
|
129
|
+
function matchesWanted(c) {
|
|
130
|
+
var t = titleOf(c).toLowerCase();
|
|
131
|
+
var id = identifierOf(c);
|
|
132
|
+
return (t && t === wanted) || (id && (id === calendarName || id.toLowerCase() === wanted));
|
|
133
|
+
}
|
|
134
|
+
for (var i = 0; i < count; i++) {
|
|
135
|
+
var c = cals.objectAtIndex(i);
|
|
136
|
+
var writable = true;
|
|
137
|
+
try { writable = !!c.allowsContentModifications; } catch (e) {}
|
|
138
|
+
if (!writable) continue;
|
|
139
|
+
writables.push(c);
|
|
140
|
+
if (matchesWanted(c)) {
|
|
141
|
+
target = c;
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (!target && defaultCal && matchesWanted(defaultCal)) target = defaultCal;
|
|
146
|
+
if (!target && writables.length === 1) target = writables[0];
|
|
147
|
+
if (!target && defaultCal) target = defaultCal;
|
|
148
|
+
var status = $.EKEventStore.authorizationStatusForEntityType($.EKEntityTypeEvent);
|
|
149
|
+
if (!target) {
|
|
150
|
+
throw new Error("CALENDAR_NOT_FOUND status=" + status + " eventKitCalendars=" + count + " default=" + titleOf(defaultCal) + " defaultId=" + identifierOf(defaultCal));
|
|
151
|
+
}
|
|
152
|
+
return target;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function createEvent(cmd) {
|
|
156
|
+
var status = $.EKEventStore.authorizationStatusForEntityType($.EKEntityTypeEvent);
|
|
157
|
+
if (status === 1 || status === 2) throw new Error("EVENTKIT_DENIED status=" + status);
|
|
158
|
+
var target = pickCalendar(cmd.calendarName);
|
|
159
|
+
var event = null;
|
|
160
|
+
try { event = $.EKEvent.eventWithEventStore(store); } catch (e) {}
|
|
161
|
+
if (!event) {
|
|
162
|
+
try { event = $.EKEvent.alloc.initWithEventStore(store); } catch (e2) {}
|
|
163
|
+
}
|
|
164
|
+
if (!event) throw new Error("EVENTKIT_NO_EVENT status=" + status);
|
|
165
|
+
event.title = String(cmd.title || "");
|
|
166
|
+
event.startDate = $.NSDate.dateWithTimeIntervalSince1970(Number(cmd.startSec));
|
|
167
|
+
event.endDate = $.NSDate.dateWithTimeIntervalSince1970(Number(cmd.endSec));
|
|
168
|
+
event.allDay = !!cmd.allDay;
|
|
169
|
+
try { event.calendar = target; } catch (e) {}
|
|
170
|
+
try { if (event.setCalendar) event.setCalendar(target); } catch (e) {}
|
|
171
|
+
if (cmd.location) event.location = String(cmd.location);
|
|
172
|
+
if (cmd.notes) event.notes = String(cmd.notes);
|
|
173
|
+
var alerts = cmd.alerts || [];
|
|
174
|
+
if (alerts && alerts.length) {
|
|
175
|
+
var alarms = $.NSMutableArray.array;
|
|
176
|
+
for (var a = 0; a < alerts.length; a++) {
|
|
177
|
+
alarms.addObject($.EKAlarm.alarmWithRelativeOffset(-Number(alerts[a]) * 60));
|
|
178
|
+
}
|
|
179
|
+
event.alarms = alarms;
|
|
180
|
+
}
|
|
181
|
+
var err = Ref();
|
|
182
|
+
var ok = false;
|
|
183
|
+
try {
|
|
184
|
+
ok = store.saveEventSpanCommitError(event, $.EKSpanThisEvent, true, err);
|
|
185
|
+
} catch (e) {
|
|
186
|
+
throw new Error("EVENTKIT_SAVE_FAILED: " + String(e));
|
|
187
|
+
}
|
|
188
|
+
if (!ok) {
|
|
189
|
+
var desc = "unknown";
|
|
190
|
+
try { desc = String(err[0]); } catch (e) {}
|
|
191
|
+
throw new Error("EVENTKIT_SAVE_FAILED: " + desc + " status=" + status);
|
|
192
|
+
}
|
|
193
|
+
remember(event);
|
|
194
|
+
var externalId = "";
|
|
195
|
+
var localId = "";
|
|
196
|
+
var itemId = "";
|
|
197
|
+
try { externalId = jsString(event.calendarItemExternalIdentifier); } catch (e) {}
|
|
198
|
+
try { localId = jsString(event.eventIdentifier); } catch (e) {}
|
|
199
|
+
try { itemId = jsString(event.calendarItemIdentifier); } catch (e) {}
|
|
200
|
+
if (!externalId && !localId && !itemId) throw new Error("EVENTKIT_NO_ID status=" + status);
|
|
201
|
+
return externalId + "<<>>" + localId + "<<>>" + titleOf(target) + "<<>>" + itemId;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function updateEvent(cmd) {
|
|
205
|
+
var event = resolve(cmd.ids);
|
|
206
|
+
if (!event) {
|
|
207
|
+
var status = $.EKEventStore.authorizationStatusForEntityType($.EKEntityTypeEvent);
|
|
208
|
+
throw new Error("EVENTKIT_NOT_FOUND status=" + status + (status === 4 ? " writeOnly" : "") + " sessionMiss tried=" + (cmd.ids || []).join(","));
|
|
209
|
+
}
|
|
210
|
+
if (cmd.title !== undefined && cmd.title !== null) event.title = String(cmd.title);
|
|
211
|
+
if (cmd.location !== undefined && cmd.location !== null) event.location = String(cmd.location);
|
|
212
|
+
if (cmd.notes !== undefined && cmd.notes !== null) event.notes = String(cmd.notes);
|
|
213
|
+
if (cmd.startSec !== undefined && cmd.startSec !== null) {
|
|
214
|
+
event.startDate = $.NSDate.dateWithTimeIntervalSince1970(Number(cmd.startSec));
|
|
215
|
+
}
|
|
216
|
+
if (cmd.endSec !== undefined && cmd.endSec !== null) {
|
|
217
|
+
event.endDate = $.NSDate.dateWithTimeIntervalSince1970(Number(cmd.endSec));
|
|
218
|
+
}
|
|
219
|
+
if (cmd.clearAlerts) {
|
|
220
|
+
try { event.alarms = $.NSMutableArray.array; } catch (e) {}
|
|
221
|
+
}
|
|
222
|
+
var alerts = cmd.alerts || [];
|
|
223
|
+
if (alerts && alerts.length) {
|
|
224
|
+
var alarms = $.NSMutableArray.array;
|
|
225
|
+
for (var a = 0; a < alerts.length; a++) {
|
|
226
|
+
alarms.addObject($.EKAlarm.alarmWithRelativeOffset(-Number(alerts[a]) * 60));
|
|
227
|
+
}
|
|
228
|
+
event.alarms = alarms;
|
|
229
|
+
}
|
|
230
|
+
var err = Ref();
|
|
231
|
+
var ok = store.saveEventSpanCommitError(event, $.EKSpanThisEvent, true, err);
|
|
232
|
+
if (!ok) {
|
|
233
|
+
var desc = "unknown";
|
|
234
|
+
try { desc = String(err[0]); } catch (e) {}
|
|
235
|
+
throw new Error("EVENTKIT_SAVE_FAILED: " + desc);
|
|
236
|
+
}
|
|
237
|
+
remember(event);
|
|
238
|
+
return jsString(event.eventIdentifier) || jsString(event.calendarItemExternalIdentifier) || "1";
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function removeEvent(cmd) {
|
|
242
|
+
var event = resolve(cmd.ids);
|
|
243
|
+
if (!event) {
|
|
244
|
+
var status = $.EKEventStore.authorizationStatusForEntityType($.EKEntityTypeEvent);
|
|
245
|
+
throw new Error("EVENTKIT_NOT_FOUND status=" + status + (status === 4 ? " writeOnly" : "") + " sessionMiss tried=" + (cmd.ids || []).join(","));
|
|
246
|
+
}
|
|
247
|
+
var err = Ref();
|
|
248
|
+
var ok = store.removeEventSpanCommitError(event, $.EKSpanThisEvent, true, err);
|
|
249
|
+
if (!ok) {
|
|
250
|
+
var desc = "unknown";
|
|
251
|
+
try { desc = String(err[0]); } catch (e) {}
|
|
252
|
+
throw new Error("EVENTKIT_REMOVE_FAILED: " + desc);
|
|
253
|
+
}
|
|
254
|
+
forget(event);
|
|
255
|
+
return "1";
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function handle(cmd) {
|
|
259
|
+
if (!cmd || !cmd.op) throw new Error("EVENTKIT_BAD_CMD");
|
|
260
|
+
if (cmd.op === "ping") return "pong";
|
|
261
|
+
if (cmd.op === "create") return createEvent(cmd);
|
|
262
|
+
if (cmd.op === "update") return updateEvent(cmd);
|
|
263
|
+
if (cmd.op === "remove") return removeEvent(cmd);
|
|
264
|
+
if (cmd.op === "quit") return "bye";
|
|
265
|
+
throw new Error("EVENTKIT_BAD_OP");
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
while (true) {
|
|
269
|
+
if (!fileExists(cmdPath)) {
|
|
270
|
+
delay(0.05);
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
var line = readCmd();
|
|
274
|
+
removeCmd();
|
|
275
|
+
line = String(line || "").replace(/^\\s+|\\s+$/g, "");
|
|
276
|
+
if (!line) continue;
|
|
277
|
+
try {
|
|
278
|
+
var cmd = JSON.parse(line);
|
|
279
|
+
var output = handle(cmd);
|
|
280
|
+
writeReply(JSON.stringify({ ok: true, output: String(output == null ? "" : output) }));
|
|
281
|
+
if (cmd.op === "quit") break;
|
|
282
|
+
} catch (e) {
|
|
283
|
+
writeReply(JSON.stringify({ ok: false, error: String(e) }));
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
`;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export function workerScriptPath(home = process.env.HOME || "") {
|
|
290
|
+
return sessionPaths(home).scriptPath;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export function startEventKitSession({
|
|
294
|
+
helpers,
|
|
295
|
+
spawnImpl = spawn,
|
|
296
|
+
home = process.env.HOME || "",
|
|
297
|
+
timeoutMs = 30000,
|
|
298
|
+
sleep = (ms) => {
|
|
299
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
300
|
+
}
|
|
301
|
+
} = {}) {
|
|
302
|
+
if (!helpers) throw new Error("EventKit worker helpers are required");
|
|
303
|
+
const paths = sessionPaths(home);
|
|
304
|
+
fs.mkdirSync(paths.dir, { recursive: true, mode: 0o700 });
|
|
305
|
+
try { fs.unlinkSync(paths.cmdPath); } catch { /* ignore */ }
|
|
306
|
+
try { fs.unlinkSync(paths.rspPath); } catch { /* ignore */ }
|
|
307
|
+
fs.writeFileSync(paths.scriptPath, buildEventKitWorkerScript(helpers, paths), { mode: 0o600 });
|
|
308
|
+
|
|
309
|
+
const child = spawnImpl("osascript", ["-l", "JavaScript", paths.scriptPath], {
|
|
310
|
+
stdio: ["ignore", "ignore", "pipe"],
|
|
311
|
+
shell: false
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
let dead = false;
|
|
315
|
+
if (child.on) {
|
|
316
|
+
child.on("exit", () => {
|
|
317
|
+
dead = true;
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function request(cmd, waitMs = timeoutMs) {
|
|
322
|
+
if (dead) throw new Error("EVENTKIT_SESSION_DEAD");
|
|
323
|
+
try { fs.unlinkSync(paths.rspPath); } catch { /* ignore */ }
|
|
324
|
+
fs.writeFileSync(paths.cmdPath, JSON.stringify(cmd), { mode: 0o600 });
|
|
325
|
+
const deadline = Date.now() + waitMs;
|
|
326
|
+
while (Date.now() < deadline) {
|
|
327
|
+
if (fs.existsSync(paths.rspPath)) {
|
|
328
|
+
const text = fs.readFileSync(paths.rspPath, "utf8").trim();
|
|
329
|
+
try { fs.unlinkSync(paths.rspPath); } catch { /* ignore */ }
|
|
330
|
+
return JSON.parse(text);
|
|
331
|
+
}
|
|
332
|
+
sleep(20);
|
|
333
|
+
}
|
|
334
|
+
throw new Error("EVENTKIT_SESSION_TIMEOUT");
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const pong = request({ op: "ping" });
|
|
338
|
+
if (!pong || pong.ok !== true || pong.output !== "pong") {
|
|
339
|
+
try { child.kill(); } catch { /* ignore */ }
|
|
340
|
+
throw new Error("EVENTKIT_SESSION_HANDSHAKE");
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return {
|
|
344
|
+
request,
|
|
345
|
+
close() {
|
|
346
|
+
try { request({ op: "quit" }, 2000); } catch { /* ignore */ }
|
|
347
|
+
try { child.kill(); } catch { /* ignore */ }
|
|
348
|
+
dead = true;
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
export function ensureEventKitSession(opts = {}) {
|
|
354
|
+
if (singleton) return singleton;
|
|
355
|
+
if (!shouldStartEventKitSession(opts)) return null;
|
|
356
|
+
try {
|
|
357
|
+
singleton = startEventKitSession(opts);
|
|
358
|
+
return singleton;
|
|
359
|
+
} catch {
|
|
360
|
+
singleton = null;
|
|
361
|
+
return null;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
export function closeEventKitSession() {
|
|
366
|
+
if (!singleton) return;
|
|
367
|
+
try { singleton.close(); } catch { /* ignore */ }
|
|
368
|
+
singleton = null;
|
|
369
|
+
}
|