apple-tools-mcp 2.0.0 → 2.0.1
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 +31 -14
- package/index.js +4 -0
- package/lib/appleScript.js +129 -5
- package/lib/calendarWrite.js +796 -23
- package/lib/contactsWrite.js +1 -1
- package/lib/eventKitSession.js +369 -0
- package/lib/mailWrite.js +39 -3
- package/lib/messagesWrite.js +35 -3
- package/lib/shell.js +23 -5
- package/lib/writeGuards.js +8 -0
- package/lib/writeRouting.js +2 -2
- package/lib/writeTools.js +18 -3
- package/package.json +1 -1
- package/scripts/smoke-writes.js +184 -16
package/lib/calendarWrite.js
CHANGED
|
@@ -15,11 +15,14 @@ import {
|
|
|
15
15
|
parseWriteDateTime,
|
|
16
16
|
DATE_HANDLER,
|
|
17
17
|
CALENDAR_TCC_GUIDANCE,
|
|
18
|
-
ATTRIBUTION_GUIDANCE
|
|
18
|
+
ATTRIBUTION_GUIDANCE,
|
|
19
|
+
formatOsascriptDiagnostic,
|
|
20
|
+
isHardTccDenial
|
|
19
21
|
} from "./appleScript.js";
|
|
20
22
|
import {
|
|
21
23
|
planWrite,
|
|
22
24
|
validateEventId,
|
|
25
|
+
validateEventKitId,
|
|
23
26
|
validateCalendarName,
|
|
24
27
|
validateBody,
|
|
25
28
|
validateSubject,
|
|
@@ -29,6 +32,11 @@ import {
|
|
|
29
32
|
normalizeList,
|
|
30
33
|
truncate
|
|
31
34
|
} from "./writeGuards.js";
|
|
35
|
+
import {
|
|
36
|
+
ensureEventKitSession,
|
|
37
|
+
getEventKitSession,
|
|
38
|
+
setEventKitSession
|
|
39
|
+
} from "./eventKitSession.js";
|
|
32
40
|
|
|
33
41
|
export const RECURRENCE_FREQUENCIES = ["daily", "weekly", "monthly", "yearly"];
|
|
34
42
|
export const RECURRENCE_DAYS = ["MO", "TU", "WE", "TH", "FR", "SA", "SU"];
|
|
@@ -176,14 +184,14 @@ function alarmLines(minutes, eventVar) {
|
|
|
176
184
|
}
|
|
177
185
|
|
|
178
186
|
function failure(action, summary, result, secrets = []) {
|
|
179
|
-
if (result.kind === "tcc") {
|
|
187
|
+
if (result.kind === "tcc" || result.kind === "timeout") {
|
|
180
188
|
return `${action} failed — attempted to ${summary}. ${CALENDAR_TCC_GUIDANCE}`;
|
|
181
189
|
}
|
|
182
190
|
const raw = String(result.error || "");
|
|
183
191
|
if (raw.includes("CALENDAR_NOT_FOUND")) {
|
|
184
192
|
return `${action} failed — attempted to ${summary}. That calendar does not exist; call calendar_list_calendars first.`;
|
|
185
193
|
}
|
|
186
|
-
if (raw.includes("EVENT_NOT_FOUND")) {
|
|
194
|
+
if (result.kind === "not_found" || raw.includes("EVENT_NOT_FOUND")) {
|
|
187
195
|
return `${action} failed — attempted to ${summary}. No event with that id was found; use the Event ID from calendar_date.`;
|
|
188
196
|
}
|
|
189
197
|
if (result.kind === "attribution") {
|
|
@@ -195,6 +203,433 @@ function failure(action, summary, result, secrets = []) {
|
|
|
195
203
|
return writeErrorMessage(action, summary, new Error(raw || "unknown error"), secrets);
|
|
196
204
|
}
|
|
197
205
|
|
|
206
|
+
/**
|
|
207
|
+
* calendar_remove must never hide the AppleEvent code behind TCC copy.
|
|
208
|
+
* Mini --apply: add/edit PASS then remove FAIL looked like a TCC deny
|
|
209
|
+
* because failure() dropped stderr when classify mapped ETIMEDOUT/-1712
|
|
210
|
+
* to tcc. Same write-bridge RPC as add/edit; not a different calendar
|
|
211
|
+
* account. Calendar.app has no `remove` / `move to trash` — only `delete`.
|
|
212
|
+
*/
|
|
213
|
+
export function describeCalendarRemoveFailure(action, summary, appleResult, eventKitResult = null) {
|
|
214
|
+
const parts = [`AppleScript ${formatOsascriptDiagnostic(appleResult, "osascript")}`];
|
|
215
|
+
if (eventKitResult) {
|
|
216
|
+
parts.push(`EventKit ${formatOsascriptDiagnostic(eventKitResult, "osascript")}`);
|
|
217
|
+
}
|
|
218
|
+
const diagnostics = parts.join(" ");
|
|
219
|
+
const raw = String(appleResult.error || "");
|
|
220
|
+
const hardTcc = isHardTccDenial(raw);
|
|
221
|
+
const timedOut =
|
|
222
|
+
appleResult.kind === "timeout" ||
|
|
223
|
+
/delete_timeout|etimedout|-1712|appleevent timed out|timed out after/i.test(raw);
|
|
224
|
+
|
|
225
|
+
let head;
|
|
226
|
+
if (raw.includes("CALENDAR_NOT_FOUND")) {
|
|
227
|
+
head = `${action} failed — attempted to ${summary}. That calendar does not exist; call calendar_list_calendars first.`;
|
|
228
|
+
} else if (hardTcc) {
|
|
229
|
+
head = `${action} failed — attempted to ${summary}. ${CALENDAR_TCC_GUIDANCE}`;
|
|
230
|
+
} else if (timedOut) {
|
|
231
|
+
head = `${action} failed — attempted to ${summary}. Calendar.app delete timed out. That is not a TCC / Automation deny when calendar_add/calendar_edit succeed on this host — Calendar.app has no remove or move-to-trash; iCloud/CalDAV delete can hang or wait on a confirmation dialog.`;
|
|
232
|
+
} else if (appleResult.kind === "not_found" || raw.includes("EVENT_NOT_FOUND")) {
|
|
233
|
+
head = `${action} failed — attempted to ${summary}. No event with that id was found; use the Event ID from calendar_date.`;
|
|
234
|
+
} else if (appleResult.kind === "attribution") {
|
|
235
|
+
head = `${action} failed — attempted to ${summary}. ${ATTRIBUTION_GUIDANCE}`;
|
|
236
|
+
} else if (appleResult.kind === "app_unavailable") {
|
|
237
|
+
head = `${action} failed — attempted to ${summary}. Calendar.app could not be reached on this host.`;
|
|
238
|
+
} else {
|
|
239
|
+
head = writeErrorMessage(action, summary, new Error(raw || "unknown error"));
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return {
|
|
243
|
+
ok: false,
|
|
244
|
+
message: `${head} ${diagnostics}`,
|
|
245
|
+
suppressTccAdvice: !hardTcc,
|
|
246
|
+
diagnostics
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Non-recurring add must not fall through to Calendar.app: Mini writeOnly
|
|
252
|
+
* EventKit cannot delete AppleScript-created events, and Calendar.app
|
|
253
|
+
* delete hangs (ETIMEDOUT). Fail closed with the EventKit code.
|
|
254
|
+
*/
|
|
255
|
+
export function describeCalendarAddFailure(action, summary, eventKitResult) {
|
|
256
|
+
const diagnostics = `EventKit ${formatOsascriptDiagnostic(eventKitResult)}`;
|
|
257
|
+
const raw = String(eventKitResult && eventKitResult.error ? eventKitResult.error : "");
|
|
258
|
+
const hardTcc = isHardTccDenial(raw) || /eventkit_denied/i.test(raw);
|
|
259
|
+
let head;
|
|
260
|
+
if (raw.includes("CALENDAR_NOT_FOUND")) {
|
|
261
|
+
head = `${action} failed — attempted to ${summary}. EventKit could not target that calendar (writeOnly often cannot list calendars; we use defaultCalendarForNewEvents). Call calendar_list_calendars first.`;
|
|
262
|
+
} else if (hardTcc) {
|
|
263
|
+
head = `${action} failed — attempted to ${summary}. ${CALENDAR_TCC_GUIDANCE}`;
|
|
264
|
+
} else {
|
|
265
|
+
head = `${action} failed — attempted to ${summary}. EventKit create failed; non-recurring add does not fall back to Calendar.app because writeOnly EventKit cannot see AppleScript-created events and Calendar.app delete hangs.`;
|
|
266
|
+
}
|
|
267
|
+
return {
|
|
268
|
+
ok: false,
|
|
269
|
+
message: `${head} ${diagnostics}`,
|
|
270
|
+
suppressTccAdvice: !hardTcc,
|
|
271
|
+
diagnostics
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export function describeEventKitWriteFailure(action, summary, eventKitResult) {
|
|
276
|
+
const diagnostics = `EventKit ${formatOsascriptDiagnostic(eventKitResult)}`;
|
|
277
|
+
const raw = String(eventKitResult && eventKitResult.error ? eventKitResult.error : "");
|
|
278
|
+
const hardTcc = isHardTccDenial(raw) || /eventkit_denied/i.test(raw);
|
|
279
|
+
const head = raw.includes("EVENTKIT_NOT_FOUND")
|
|
280
|
+
? `${action} failed — attempted to ${summary}. writeOnly EventKit cannot re-query by id; edit/remove need the in-memory EKEvent from the same EventKit session that created it. Calendar.app uid lookup was skipped to avoid an iCloud hang.`
|
|
281
|
+
: `${action} failed — attempted to ${summary}. EventKit write failed; Calendar.app was not used because eventkit_id was provided.`;
|
|
282
|
+
return {
|
|
283
|
+
ok: false,
|
|
284
|
+
message: `${head} ${diagnostics}`,
|
|
285
|
+
suppressTccAdvice: !hardTcc,
|
|
286
|
+
diagnostics
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Mini 74d304e: EventKit status=4 is writeOnly. That grant can create and
|
|
292
|
+
* delete events *this process* saved, but calendarItemsWithExternalIdentifier
|
|
293
|
+
* cannot see an event Calendar.app created via AppleScript. Align add→remove
|
|
294
|
+
* by creating through EventKit and deleting with the same identifiers
|
|
295
|
+
* (calendarItemExternalIdentifier and eventIdentifier).
|
|
296
|
+
*/
|
|
297
|
+
export function localUnixSeconds(parts) {
|
|
298
|
+
return Math.floor(new Date(parts.year, parts.month - 1, parts.day, parts.hour, parts.minute, 0, 0).getTime() / 1000);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export function parseEventKitAddOutput(output) {
|
|
302
|
+
const text = String(output || "").trim();
|
|
303
|
+
if (!text) return { eventId: null, eventKitId: null, calendar: null };
|
|
304
|
+
const [externalId, eventKitId, calendar, calendarItemId] = text.split("<<>>");
|
|
305
|
+
const ext = String(externalId || "").trim();
|
|
306
|
+
const local = String(eventKitId || "").trim();
|
|
307
|
+
const item = String(calendarItemId || "").trim();
|
|
308
|
+
return {
|
|
309
|
+
eventId: ext || local || item || null,
|
|
310
|
+
eventKitId: local || item || null,
|
|
311
|
+
calendar: String(calendar || "").trim() || null
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
export function parseEventKitCalendarList(output) {
|
|
316
|
+
return String(output || "")
|
|
317
|
+
.split("|||")
|
|
318
|
+
.map((entry) => entry.trim())
|
|
319
|
+
.filter((entry) => entry.length > 0)
|
|
320
|
+
.map((entry) => {
|
|
321
|
+
const parts = entry.split("<<>>");
|
|
322
|
+
if (parts.length < 3) return null;
|
|
323
|
+
const [name, writable, local, sourceType, source] = parts;
|
|
324
|
+
return {
|
|
325
|
+
name: name || "",
|
|
326
|
+
writable: writable !== "no",
|
|
327
|
+
local: local === "yes",
|
|
328
|
+
sourceType: sourceType === undefined || sourceType === "" ? null : Number(sourceType),
|
|
329
|
+
source: source || ""
|
|
330
|
+
};
|
|
331
|
+
})
|
|
332
|
+
.filter(Boolean);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* JXA prints NSString as `[id NSTaggedPointerString]` if you `String(title)`.
|
|
337
|
+
* Mini cd74071: default=[id NSTaggedPointerString] so title match failed
|
|
338
|
+
* even though eventKitCalendars=1. Always ObjC.unwrap / .js.
|
|
339
|
+
*/
|
|
340
|
+
export const EVENTKIT_JXA_HELPERS = `function jsString(value) {
|
|
341
|
+
if (value === undefined || value === null) return "";
|
|
342
|
+
if (typeof value === "string") {
|
|
343
|
+
if (value.indexOf("[id ") === 0 || value === "[object Object]") return "";
|
|
344
|
+
return value;
|
|
345
|
+
}
|
|
346
|
+
try {
|
|
347
|
+
if (typeof value.js === "string") return value.js;
|
|
348
|
+
} catch (e) {}
|
|
349
|
+
try {
|
|
350
|
+
var unwrapped = ObjC.unwrap(value);
|
|
351
|
+
if (typeof unwrapped === "string") return unwrapped;
|
|
352
|
+
} catch (e) {}
|
|
353
|
+
return "";
|
|
354
|
+
}
|
|
355
|
+
function titleOf(c) {
|
|
356
|
+
try { return c ? jsString(c.title) : ""; } catch (e) { return ""; }
|
|
357
|
+
}
|
|
358
|
+
function identifierOf(c) {
|
|
359
|
+
try { return c ? jsString(c.calendarIdentifier) : ""; } catch (e) { return ""; }
|
|
360
|
+
}
|
|
361
|
+
function asEvent(item) {
|
|
362
|
+
if (item === undefined || item === null) return false;
|
|
363
|
+
try { if (item.isKindOfClass && item.isKindOfClass($.EKEvent)) return true; } catch (e) {}
|
|
364
|
+
try { if (jsString(item.eventIdentifier)) return true; } catch (e) {}
|
|
365
|
+
try { if (jsString(item.calendarItemExternalIdentifier)) return true; } catch (e) {}
|
|
366
|
+
try { if (jsString(item.calendarItemIdentifier)) return true; } catch (e) {}
|
|
367
|
+
return false;
|
|
368
|
+
}
|
|
369
|
+
function nsId(id) {
|
|
370
|
+
return $.NSString.stringWithString(String(id));
|
|
371
|
+
}
|
|
372
|
+
function firstMatching(items) {
|
|
373
|
+
if (!items) return null;
|
|
374
|
+
var n = 0;
|
|
375
|
+
try { n = Number(items.count); } catch (e) { n = 0; }
|
|
376
|
+
for (var j = 0; j < n; j++) {
|
|
377
|
+
var hit = null;
|
|
378
|
+
try { hit = items.objectAtIndex(j); } catch (e) {}
|
|
379
|
+
if (asEvent(hit)) return hit;
|
|
380
|
+
}
|
|
381
|
+
return null;
|
|
382
|
+
}
|
|
383
|
+
function lookupOne(store, raw) {
|
|
384
|
+
if (!raw) return null;
|
|
385
|
+
var bridged = nsId(raw);
|
|
386
|
+
var candidates = [bridged, raw];
|
|
387
|
+
for (var k = 0; k < candidates.length; k++) {
|
|
388
|
+
var id = candidates[k];
|
|
389
|
+
var ev = null;
|
|
390
|
+
try { ev = store.eventWithIdentifier(id); } catch (e) {}
|
|
391
|
+
if (!asEvent(ev)) {
|
|
392
|
+
try { ev = store.eventWithIdentifier_(id); } catch (e) {}
|
|
393
|
+
}
|
|
394
|
+
if (asEvent(ev)) return ev;
|
|
395
|
+
try {
|
|
396
|
+
var item = store.calendarItemWithIdentifier(id);
|
|
397
|
+
if (asEvent(item)) return item;
|
|
398
|
+
} catch (e) {}
|
|
399
|
+
try {
|
|
400
|
+
var item2 = store.calendarItemWithIdentifier_(id);
|
|
401
|
+
if (asEvent(item2)) return item2;
|
|
402
|
+
} catch (e) {}
|
|
403
|
+
try {
|
|
404
|
+
var found = firstMatching(store.calendarItemsWithExternalIdentifier(id));
|
|
405
|
+
if (found) return found;
|
|
406
|
+
} catch (e) {}
|
|
407
|
+
try {
|
|
408
|
+
var found2 = firstMatching(store.calendarItemsWithExternalIdentifier_(id));
|
|
409
|
+
if (found2) return found2;
|
|
410
|
+
} catch (e) {}
|
|
411
|
+
}
|
|
412
|
+
return null;
|
|
413
|
+
}
|
|
414
|
+
function findEventByIds(store, ids) {
|
|
415
|
+
try { store.refreshSourcesIfNecessary(); } catch (e) {}
|
|
416
|
+
for (var i = 0; i < ids.length; i++) {
|
|
417
|
+
var found = lookupOne(store, ids[i]);
|
|
418
|
+
if (found) return found;
|
|
419
|
+
}
|
|
420
|
+
return null;
|
|
421
|
+
}
|
|
422
|
+
`;
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Mini ed6d834: eventkit_id is `calendarUUID:eventUUID`. Look up the
|
|
426
|
+
* compound id and each half so eventWithIdentifier / externalIdentifier
|
|
427
|
+
* both get a usable string.
|
|
428
|
+
*/
|
|
429
|
+
export function eventKitLookupIds(eventId, eventKitId) {
|
|
430
|
+
const out = [];
|
|
431
|
+
const seen = new Set();
|
|
432
|
+
const add = (value) => {
|
|
433
|
+
if (typeof value !== "string") return;
|
|
434
|
+
const trimmed = value.trim();
|
|
435
|
+
if (!trimmed || seen.has(trimmed)) return;
|
|
436
|
+
seen.add(trimmed);
|
|
437
|
+
out.push(trimmed);
|
|
438
|
+
};
|
|
439
|
+
add(eventKitId);
|
|
440
|
+
add(eventId);
|
|
441
|
+
for (const value of [...out]) {
|
|
442
|
+
if (value.includes(":")) {
|
|
443
|
+
for (const part of value.split(":")) add(part);
|
|
444
|
+
}
|
|
445
|
+
if (value.includes("/")) add(value.split("/")[0]);
|
|
446
|
+
}
|
|
447
|
+
return out;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function eventKitSessionRequest(cmd) {
|
|
451
|
+
const session = getEventKitSession() || ensureEventKitSession({ helpers: EVENTKIT_JXA_HELPERS });
|
|
452
|
+
if (!session) return null;
|
|
453
|
+
try {
|
|
454
|
+
return session.request(cmd);
|
|
455
|
+
} catch (e) {
|
|
456
|
+
try { session.close(); } catch { /* ignore */ }
|
|
457
|
+
setEventKitSession(null);
|
|
458
|
+
return { ok: false, error: e && e.message ? e.message : String(e), sessionDead: true };
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
export function mergeCalendarSources(appleCalendars, eventKitCalendars) {
|
|
463
|
+
if (!eventKitCalendars || eventKitCalendars.length === 0) return appleCalendars || [];
|
|
464
|
+
const byName = new Map(eventKitCalendars.map((c) => [c.name, c]));
|
|
465
|
+
return (appleCalendars || []).map((c) => {
|
|
466
|
+
const ek = byName.get(c.name);
|
|
467
|
+
return {
|
|
468
|
+
...c,
|
|
469
|
+
local: ek ? ek.local : false,
|
|
470
|
+
sourceType: ek ? ek.sourceType : null,
|
|
471
|
+
source: ek ? ek.source : null
|
|
472
|
+
};
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
export function buildEventKitListCalendarsScript() {
|
|
477
|
+
return `ObjC.import("EventKit");
|
|
478
|
+
ObjC.import("Foundation");
|
|
479
|
+
${EVENTKIT_JXA_HELPERS}
|
|
480
|
+
var store = $.EKEventStore.alloc.init;
|
|
481
|
+
var rows = [];
|
|
482
|
+
var seen = {};
|
|
483
|
+
function addRow(c) {
|
|
484
|
+
if (!c) return;
|
|
485
|
+
var name = titleOf(c);
|
|
486
|
+
if (!name || seen[name]) return;
|
|
487
|
+
seen[name] = true;
|
|
488
|
+
var src = null;
|
|
489
|
+
try { src = c.source; } catch (e) {}
|
|
490
|
+
var type = -1;
|
|
491
|
+
try { if (src) type = Number(src.sourceType); } catch (e) {}
|
|
492
|
+
var srcName = "";
|
|
493
|
+
try { if (src) srcName = jsString(src.title); } catch (e) {}
|
|
494
|
+
var writable = "yes";
|
|
495
|
+
try { if (c.allowsContentModifications === false) writable = "no"; } catch (e) {}
|
|
496
|
+
var local = type === 0 ? "yes" : "no";
|
|
497
|
+
rows.push(name + "<<>>" + writable + "<<>>" + local + "<<>>" + type + "<<>>" + srcName);
|
|
498
|
+
}
|
|
499
|
+
try {
|
|
500
|
+
var cals = store.calendarsForEntityType($.EKEntityTypeEvent);
|
|
501
|
+
for (var i = 0; i < cals.count; i++) addRow(cals.objectAtIndex(i));
|
|
502
|
+
} catch (e) {}
|
|
503
|
+
try { addRow(store.defaultCalendarForNewEvents); } catch (e) {}
|
|
504
|
+
rows.join("|||");`;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
export function buildEventKitAddScript({ calendarName, title, start, end, allDay, location, notes, alerts }) {
|
|
508
|
+
const startSec = localUnixSeconds(start);
|
|
509
|
+
const endSec = localUnixSeconds(end);
|
|
510
|
+
return `ObjC.import("EventKit");
|
|
511
|
+
ObjC.import("Foundation");
|
|
512
|
+
${EVENTKIT_JXA_HELPERS}
|
|
513
|
+
var calendarName = ${JSON.stringify(calendarName)};
|
|
514
|
+
var title = ${JSON.stringify(title)};
|
|
515
|
+
var startSec = ${asInteger(startSec, { field: "start" })};
|
|
516
|
+
var endSec = ${asInteger(endSec, { field: "end" })};
|
|
517
|
+
var allDay = ${allDay ? "true" : "false"};
|
|
518
|
+
var location = ${JSON.stringify(location || "")};
|
|
519
|
+
var notes = ${JSON.stringify(notes || "")};
|
|
520
|
+
var alerts = ${JSON.stringify(alerts || [])};
|
|
521
|
+
var status = $.EKEventStore.authorizationStatusForEntityType($.EKEntityTypeEvent);
|
|
522
|
+
if (status === 1 || status === 2) {
|
|
523
|
+
throw new Error("EVENTKIT_DENIED status=" + status);
|
|
524
|
+
}
|
|
525
|
+
var store = $.EKEventStore.alloc.init;
|
|
526
|
+
var defaultCal = null;
|
|
527
|
+
try { defaultCal = store.defaultCalendarForNewEvents; } catch (e) {}
|
|
528
|
+
var cals = null;
|
|
529
|
+
var count = 0;
|
|
530
|
+
try {
|
|
531
|
+
cals = store.calendarsForEntityType($.EKEntityTypeEvent);
|
|
532
|
+
count = cals ? Number(cals.count) : 0;
|
|
533
|
+
} catch (e) {}
|
|
534
|
+
var target = null;
|
|
535
|
+
var writables = [];
|
|
536
|
+
var wanted = String(calendarName || "").toLowerCase();
|
|
537
|
+
function matchesWanted(c) {
|
|
538
|
+
var t = titleOf(c).toLowerCase();
|
|
539
|
+
var id = identifierOf(c);
|
|
540
|
+
return (t && t === wanted) || (id && (id === calendarName || id.toLowerCase() === wanted));
|
|
541
|
+
}
|
|
542
|
+
for (var i = 0; i < count; i++) {
|
|
543
|
+
var c = cals.objectAtIndex(i);
|
|
544
|
+
var writable = true;
|
|
545
|
+
try { writable = !!c.allowsContentModifications; } catch (e) {}
|
|
546
|
+
if (!writable) continue;
|
|
547
|
+
writables.push(c);
|
|
548
|
+
if (matchesWanted(c)) {
|
|
549
|
+
target = c;
|
|
550
|
+
break;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
if (!target && defaultCal && matchesWanted(defaultCal)) {
|
|
554
|
+
target = defaultCal;
|
|
555
|
+
}
|
|
556
|
+
if (!target && writables.length === 1) {
|
|
557
|
+
target = writables[0];
|
|
558
|
+
}
|
|
559
|
+
if (!target && defaultCal) {
|
|
560
|
+
target = defaultCal;
|
|
561
|
+
}
|
|
562
|
+
if (!target) {
|
|
563
|
+
throw new Error("CALENDAR_NOT_FOUND status=" + status + " eventKitCalendars=" + count + " default=" + titleOf(defaultCal) + " defaultId=" + identifierOf(defaultCal));
|
|
564
|
+
}
|
|
565
|
+
var event = null;
|
|
566
|
+
try { event = $.EKEvent.eventWithEventStore(store); } catch (e) {}
|
|
567
|
+
if (!event) {
|
|
568
|
+
try { event = $.EKEvent.alloc.initWithEventStore(store); } catch (e2) {}
|
|
569
|
+
}
|
|
570
|
+
if (!event) throw new Error("EVENTKIT_NO_EVENT status=" + status);
|
|
571
|
+
event.title = title;
|
|
572
|
+
event.startDate = $.NSDate.dateWithTimeIntervalSince1970(startSec);
|
|
573
|
+
event.endDate = $.NSDate.dateWithTimeIntervalSince1970(endSec);
|
|
574
|
+
event.allDay = allDay;
|
|
575
|
+
try { event.calendar = target; } catch (e) {}
|
|
576
|
+
try { if (event.setCalendar) event.setCalendar(target); } catch (e) {}
|
|
577
|
+
if (location) event.location = location;
|
|
578
|
+
if (notes) event.notes = notes;
|
|
579
|
+
if (alerts && alerts.length) {
|
|
580
|
+
var alarms = $.NSMutableArray.array;
|
|
581
|
+
for (var a = 0; a < alerts.length; a++) {
|
|
582
|
+
alarms.addObject($.EKAlarm.alarmWithRelativeOffset(-Number(alerts[a]) * 60));
|
|
583
|
+
}
|
|
584
|
+
event.alarms = alarms;
|
|
585
|
+
}
|
|
586
|
+
var err = Ref();
|
|
587
|
+
var ok = false;
|
|
588
|
+
try {
|
|
589
|
+
ok = store.saveEventSpanCommitError(event, $.EKSpanThisEvent, true, err);
|
|
590
|
+
} catch (e) {
|
|
591
|
+
throw new Error("EVENTKIT_SAVE_FAILED: " + String(e));
|
|
592
|
+
}
|
|
593
|
+
if (!ok) {
|
|
594
|
+
var desc = "unknown";
|
|
595
|
+
try { desc = String(err[0]); } catch (e) {}
|
|
596
|
+
throw new Error("EVENTKIT_SAVE_FAILED: " + desc + " status=" + status);
|
|
597
|
+
}
|
|
598
|
+
var externalId = "";
|
|
599
|
+
var localId = "";
|
|
600
|
+
var itemId = "";
|
|
601
|
+
try { externalId = jsString(event.calendarItemExternalIdentifier); } catch (e) {}
|
|
602
|
+
try { localId = jsString(event.eventIdentifier); } catch (e) {}
|
|
603
|
+
try { itemId = jsString(event.calendarItemIdentifier); } catch (e) {}
|
|
604
|
+
if (!externalId && !localId && !itemId) throw new Error("EVENTKIT_NO_ID status=" + status);
|
|
605
|
+
externalId + "<<>>" + localId + "<<>>" + titleOf(target) + "<<>>" + itemId;`;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
export function buildEventKitRemoveScript(eventId, { eventKitId = null } = {}) {
|
|
609
|
+
const ids = eventKitLookupIds(eventId, eventKitId);
|
|
610
|
+
return `ObjC.import("EventKit");
|
|
611
|
+
ObjC.import("Foundation");
|
|
612
|
+
${EVENTKIT_JXA_HELPERS}
|
|
613
|
+
var ids = ${JSON.stringify(ids)};
|
|
614
|
+
var status = $.EKEventStore.authorizationStatusForEntityType($.EKEntityTypeEvent);
|
|
615
|
+
if (status === 1 || status === 2) {
|
|
616
|
+
throw new Error("EVENTKIT_DENIED status=" + status);
|
|
617
|
+
}
|
|
618
|
+
var store = $.EKEventStore.alloc.init;
|
|
619
|
+
var found = findEventByIds(store, ids);
|
|
620
|
+
if (!found) {
|
|
621
|
+
throw new Error("EVENTKIT_NOT_FOUND status=" + status + (status === 4 ? " writeOnly" : "") + " tried=" + ids.join(","));
|
|
622
|
+
}
|
|
623
|
+
var err = Ref();
|
|
624
|
+
var ok = store.removeEventSpanCommitError(found, $.EKSpanThisEvent, true, err);
|
|
625
|
+
if (!ok) {
|
|
626
|
+
var desc = "unknown";
|
|
627
|
+
try { desc = String(err[0]); } catch (e) {}
|
|
628
|
+
throw new Error("EVENTKIT_REMOVE_FAILED: " + desc);
|
|
629
|
+
}
|
|
630
|
+
1;`;
|
|
631
|
+
}
|
|
632
|
+
|
|
198
633
|
/**
|
|
199
634
|
* List calendars so a caller can pick a target instead of defaulting.
|
|
200
635
|
*/
|
|
@@ -221,7 +656,7 @@ return outputList as string`;
|
|
|
221
656
|
return { ok: false, message: failure(action, "list calendars", result) };
|
|
222
657
|
}
|
|
223
658
|
|
|
224
|
-
|
|
659
|
+
let calendars = result.output
|
|
225
660
|
.split("|||")
|
|
226
661
|
.map((entry) => entry.trim())
|
|
227
662
|
.filter((entry) => entry.length > 0)
|
|
@@ -230,11 +665,26 @@ return outputList as string`;
|
|
|
230
665
|
return { name: name || "", writable: writable !== "no" };
|
|
231
666
|
});
|
|
232
667
|
|
|
668
|
+
const ekList = runAppleScript(buildEventKitListCalendarsScript(), {
|
|
669
|
+
timeout: 15000,
|
|
670
|
+
appName: "Calendar",
|
|
671
|
+
language: "JavaScript"
|
|
672
|
+
});
|
|
673
|
+
if (ekList.ok) {
|
|
674
|
+
calendars = mergeCalendarSources(calendars, parseEventKitCalendarList(ekList.output));
|
|
675
|
+
}
|
|
676
|
+
|
|
233
677
|
if (calendars.length === 0) {
|
|
234
678
|
return { ok: true, message: "No calendars found in Calendar.app." };
|
|
235
679
|
}
|
|
236
680
|
|
|
237
|
-
const lines = calendars.map((c) =>
|
|
681
|
+
const lines = calendars.map((c) => {
|
|
682
|
+
const tags = [];
|
|
683
|
+
if (!c.writable) tags.push("read-only");
|
|
684
|
+
if (c.local) tags.push("On My Mac");
|
|
685
|
+
else if (c.source) tags.push(c.source);
|
|
686
|
+
return `• ${c.name}${tags.length ? ` (${tags.join(", ")})` : ""}`;
|
|
687
|
+
});
|
|
238
688
|
return {
|
|
239
689
|
ok: true,
|
|
240
690
|
message: `Calendars (${calendars.length}):\n${lines.join("\n")}\n\nPass one of these names as calendar_name when creating events.`,
|
|
@@ -318,6 +768,92 @@ export function calendarAdd(args = {}) {
|
|
|
318
768
|
const plan = planWrite({ action, summary, dryRun: isFlagTrue(args.dry_run), confirm: isFlagTrue(args.confirm) });
|
|
319
769
|
if (!plan.proceed) return { ok: true, message: plan.message, planned: true };
|
|
320
770
|
|
|
771
|
+
// Non-recurring creates MUST go through EventKit. Mini 8be6cd5: EventKit
|
|
772
|
+
// add failed (likely writeOnly cannot list calendars) and we silently
|
|
773
|
+
// created via Calendar.app — remove then EVENTKIT_NOT_FOUND status=4 and
|
|
774
|
+
// Calendar.app delete ETIMEDOUT. No AppleScript fallback here.
|
|
775
|
+
if (!recurrence.rule) {
|
|
776
|
+
const sessionReply = eventKitSessionRequest({
|
|
777
|
+
op: "create",
|
|
778
|
+
calendarName,
|
|
779
|
+
title: title.text,
|
|
780
|
+
startSec: localUnixSeconds(start.parts),
|
|
781
|
+
endSec: localUnixSeconds(end.parts),
|
|
782
|
+
allDay,
|
|
783
|
+
location: location.text || "",
|
|
784
|
+
notes: notes.text || "",
|
|
785
|
+
alerts: alerts.minutes
|
|
786
|
+
});
|
|
787
|
+
if (sessionReply && sessionReply.ok) {
|
|
788
|
+
const ids = parseEventKitAddOutput(sessionReply.output);
|
|
789
|
+
if (ids.eventId) {
|
|
790
|
+
return {
|
|
791
|
+
ok: true,
|
|
792
|
+
message: writeSuccessMessage(action, "event created", {
|
|
793
|
+
event_id: ids.eventId,
|
|
794
|
+
eventkit_id: ids.eventKitId || ids.eventId,
|
|
795
|
+
calendar: ids.calendar || calendarName,
|
|
796
|
+
via: "EventKit",
|
|
797
|
+
title: truncate(title.text, 150),
|
|
798
|
+
start: args.start,
|
|
799
|
+
alerts: alerts.minutes.length ? alerts.minutes.join(", ") : undefined
|
|
800
|
+
})
|
|
801
|
+
};
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
if (sessionReply && !sessionReply.ok && !sessionReply.sessionDead) {
|
|
805
|
+
return describeCalendarAddFailure(action, summary, {
|
|
806
|
+
ok: false,
|
|
807
|
+
error: sessionReply.error || "EVENTKIT_SESSION_CREATE_FAILED",
|
|
808
|
+
kind: "unknown"
|
|
809
|
+
});
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
// One-shot: return identifiers from the saved EKEvent. Do not re-query —
|
|
813
|
+
// writeOnly status=4 cannot eventWithIdentifier after save (Mini 2cdf44d).
|
|
814
|
+
let ek;
|
|
815
|
+
try {
|
|
816
|
+
ek = runAppleScript(
|
|
817
|
+
buildEventKitAddScript({
|
|
818
|
+
calendarName,
|
|
819
|
+
title: title.text,
|
|
820
|
+
start: start.parts,
|
|
821
|
+
end: end.parts,
|
|
822
|
+
allDay,
|
|
823
|
+
location: location.text,
|
|
824
|
+
notes: notes.text,
|
|
825
|
+
alerts: alerts.minutes
|
|
826
|
+
}),
|
|
827
|
+
{ timeout: 30000, appName: "Calendar", language: "JavaScript" }
|
|
828
|
+
);
|
|
829
|
+
} catch (e) {
|
|
830
|
+
ek = { ok: false, error: e && e.message ? e.message : String(e), kind: "unknown" };
|
|
831
|
+
}
|
|
832
|
+
if (ek.ok) {
|
|
833
|
+
const ids = parseEventKitAddOutput(ek.output);
|
|
834
|
+
if (ids.eventId) {
|
|
835
|
+
return {
|
|
836
|
+
ok: true,
|
|
837
|
+
message: writeSuccessMessage(action, "event created", {
|
|
838
|
+
event_id: ids.eventId,
|
|
839
|
+
eventkit_id: ids.eventKitId || ids.eventId,
|
|
840
|
+
calendar: ids.calendar || calendarName,
|
|
841
|
+
via: "EventKit",
|
|
842
|
+
title: truncate(title.text, 150),
|
|
843
|
+
start: args.start,
|
|
844
|
+
alerts: alerts.minutes.length ? alerts.minutes.join(", ") : undefined
|
|
845
|
+
})
|
|
846
|
+
};
|
|
847
|
+
}
|
|
848
|
+
return describeCalendarAddFailure(
|
|
849
|
+
action,
|
|
850
|
+
summary,
|
|
851
|
+
{ ok: false, error: `EVENTKIT_NO_ID output=${truncate(ek.output, 120)}`, kind: "unknown" }
|
|
852
|
+
);
|
|
853
|
+
}
|
|
854
|
+
return describeCalendarAddFailure(action, summary, ek);
|
|
855
|
+
}
|
|
856
|
+
|
|
321
857
|
let script;
|
|
322
858
|
try {
|
|
323
859
|
script = buildAddEventScript({
|
|
@@ -351,6 +887,58 @@ export function calendarAdd(args = {}) {
|
|
|
351
887
|
};
|
|
352
888
|
}
|
|
353
889
|
|
|
890
|
+
export function buildEventKitEditScript({
|
|
891
|
+
eventId,
|
|
892
|
+
eventKitId = null,
|
|
893
|
+
updates = {},
|
|
894
|
+
start = null,
|
|
895
|
+
end = null,
|
|
896
|
+
alerts = [],
|
|
897
|
+
clearAlerts = false
|
|
898
|
+
}) {
|
|
899
|
+
const ids = eventKitLookupIds(eventId, eventKitId);
|
|
900
|
+
const title = updates.summary ? JSON.stringify(updates.summary) : "null";
|
|
901
|
+
const location = updates.location !== undefined ? JSON.stringify(updates.location) : "null";
|
|
902
|
+
const notes = updates.description !== undefined ? JSON.stringify(updates.description) : "null";
|
|
903
|
+
const startSec = start ? localUnixSeconds(start) : null;
|
|
904
|
+
const endSec = end ? localUnixSeconds(end) : null;
|
|
905
|
+
return `ObjC.import("EventKit");
|
|
906
|
+
ObjC.import("Foundation");
|
|
907
|
+
${EVENTKIT_JXA_HELPERS}
|
|
908
|
+
var ids = ${JSON.stringify(ids)};
|
|
909
|
+
var status = $.EKEventStore.authorizationStatusForEntityType($.EKEntityTypeEvent);
|
|
910
|
+
if (status === 1 || status === 2) {
|
|
911
|
+
throw new Error("EVENTKIT_DENIED status=" + status);
|
|
912
|
+
}
|
|
913
|
+
var store = $.EKEventStore.alloc.init;
|
|
914
|
+
var event = findEventByIds(store, ids);
|
|
915
|
+
if (!event) {
|
|
916
|
+
throw new Error("EVENTKIT_NOT_FOUND status=" + status + (status === 4 ? " writeOnly" : "") + " tried=" + ids.join(","));
|
|
917
|
+
}
|
|
918
|
+
var newTitle = ${title};
|
|
919
|
+
var newLocation = ${location};
|
|
920
|
+
var newNotes = ${notes};
|
|
921
|
+
if (newTitle !== null) event.title = newTitle;
|
|
922
|
+
if (newLocation !== null) event.location = newLocation;
|
|
923
|
+
if (newNotes !== null) event.notes = newNotes;
|
|
924
|
+
${startSec !== null ? `event.startDate = $.NSDate.dateWithTimeIntervalSince1970(${asInteger(startSec, { field: "start" })});` : ""}
|
|
925
|
+
${endSec !== null ? `event.endDate = $.NSDate.dateWithTimeIntervalSince1970(${asInteger(endSec, { field: "end" })});` : ""}
|
|
926
|
+
${clearAlerts ? `try { event.alarms = $.NSMutableArray.array; } catch (e) {}` : ""}
|
|
927
|
+
${alerts.length ? `var alarms = $.NSMutableArray.array;
|
|
928
|
+
for (var a = 0; a < ${JSON.stringify(alerts)}.length; a++) {
|
|
929
|
+
alarms.addObject($.EKAlarm.alarmWithRelativeOffset(-Number(${JSON.stringify(alerts)}[a]) * 60));
|
|
930
|
+
}
|
|
931
|
+
event.alarms = alarms;` : ""}
|
|
932
|
+
var err = Ref();
|
|
933
|
+
var ok = store.saveEventSpanCommitError(event, $.EKSpanThisEvent, true, err);
|
|
934
|
+
if (!ok) {
|
|
935
|
+
var desc = "unknown";
|
|
936
|
+
try { desc = String(err[0]); } catch (e) {}
|
|
937
|
+
throw new Error("EVENTKIT_SAVE_FAILED: " + desc);
|
|
938
|
+
}
|
|
939
|
+
jsString(event.eventIdentifier) || jsString(event.calendarItemExternalIdentifier) || "1";`;
|
|
940
|
+
}
|
|
941
|
+
|
|
354
942
|
export function buildEditEventScript({ eventId, updates, start, end, rule, alerts, clearAlerts }) {
|
|
355
943
|
const lines = [];
|
|
356
944
|
for (const [prop, value] of Object.entries(updates)) {
|
|
@@ -384,6 +972,13 @@ export function calendarEdit(args = {}) {
|
|
|
384
972
|
return { ok: false, message: `${action} refused: event_id is required (the "Event ID" from calendar_date or calendar_add). This tool will not guess which event you meant.` };
|
|
385
973
|
}
|
|
386
974
|
|
|
975
|
+
const eventKitId = args.eventkit_id === undefined || args.eventkit_id === null || args.eventkit_id === ""
|
|
976
|
+
? null
|
|
977
|
+
: validateEventKitId(args.eventkit_id);
|
|
978
|
+
if (args.eventkit_id && !eventKitId) {
|
|
979
|
+
return { ok: false, message: `${action} refused: eventkit_id is not a valid EventKit eventIdentifier.` };
|
|
980
|
+
}
|
|
981
|
+
|
|
387
982
|
const updates = {};
|
|
388
983
|
const changed = [];
|
|
389
984
|
|
|
@@ -438,6 +1033,66 @@ export function calendarEdit(args = {}) {
|
|
|
438
1033
|
const plan = planWrite({ action, summary, dryRun: isFlagTrue(args.dry_run), confirm: isFlagTrue(args.confirm) });
|
|
439
1034
|
if (!plan.proceed) return { ok: true, message: plan.message, planned: true };
|
|
440
1035
|
|
|
1036
|
+
const preferEventKit = Boolean(eventKitId) || eventId.includes(":");
|
|
1037
|
+
if (preferEventKit && !recurrence.rule) {
|
|
1038
|
+
const sessionReply = eventKitSessionRequest({
|
|
1039
|
+
op: "update",
|
|
1040
|
+
ids: eventKitLookupIds(eventId, eventKitId),
|
|
1041
|
+
title: updates.summary,
|
|
1042
|
+
location: updates.location,
|
|
1043
|
+
notes: updates.description,
|
|
1044
|
+
startSec: start ? localUnixSeconds(start) : null,
|
|
1045
|
+
endSec: end ? localUnixSeconds(end) : null,
|
|
1046
|
+
alerts: alerts.minutes,
|
|
1047
|
+
clearAlerts
|
|
1048
|
+
});
|
|
1049
|
+
if (sessionReply && sessionReply.ok) {
|
|
1050
|
+
return {
|
|
1051
|
+
ok: true,
|
|
1052
|
+
message: writeSuccessMessage(action, "event updated", {
|
|
1053
|
+
event_id: eventId,
|
|
1054
|
+
eventkit_id: eventKitId || undefined,
|
|
1055
|
+
via: "EventKit",
|
|
1056
|
+
changed: changed.join(", ")
|
|
1057
|
+
})
|
|
1058
|
+
};
|
|
1059
|
+
}
|
|
1060
|
+
if (sessionReply && !sessionReply.ok && !sessionReply.sessionDead && eventKitId) {
|
|
1061
|
+
return describeEventKitWriteFailure(action, summary, {
|
|
1062
|
+
ok: false,
|
|
1063
|
+
error: sessionReply.error || "EVENTKIT_SESSION_UPDATE_FAILED",
|
|
1064
|
+
kind: "not_found"
|
|
1065
|
+
});
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
const ek = runAppleScript(
|
|
1069
|
+
buildEventKitEditScript({
|
|
1070
|
+
eventId,
|
|
1071
|
+
eventKitId,
|
|
1072
|
+
updates,
|
|
1073
|
+
start,
|
|
1074
|
+
end,
|
|
1075
|
+
alerts: alerts.minutes,
|
|
1076
|
+
clearAlerts
|
|
1077
|
+
}),
|
|
1078
|
+
{ timeout: 30000, appName: "Calendar", language: "JavaScript" }
|
|
1079
|
+
);
|
|
1080
|
+
if (ek.ok) {
|
|
1081
|
+
return {
|
|
1082
|
+
ok: true,
|
|
1083
|
+
message: writeSuccessMessage(action, "event updated", {
|
|
1084
|
+
event_id: eventId,
|
|
1085
|
+
eventkit_id: eventKitId || undefined,
|
|
1086
|
+
via: "EventKit",
|
|
1087
|
+
changed: changed.join(", ")
|
|
1088
|
+
})
|
|
1089
|
+
};
|
|
1090
|
+
}
|
|
1091
|
+
if (eventKitId) {
|
|
1092
|
+
return describeEventKitWriteFailure(action, summary, ek);
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
|
|
441
1096
|
let script;
|
|
442
1097
|
try {
|
|
443
1098
|
script = buildEditEventScript({
|
|
@@ -462,15 +1117,68 @@ export function calendarEdit(args = {}) {
|
|
|
462
1117
|
};
|
|
463
1118
|
}
|
|
464
1119
|
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
1120
|
+
/**
|
|
1121
|
+
* Delete via Calendar.app `delete` (the dictionary has no `remove` or
|
|
1122
|
+
* `move to trash`) using the AppleScript event `id` inside the owning
|
|
1123
|
+
* calendar. Do not `delete (every event whose uid …)` — that specifier
|
|
1124
|
+
* hangs on iCloud/CalDAV and Mini's 60s spawnSync timeout was then
|
|
1125
|
+
* classified as TCC. A detached `delete theEvent` after atmFindEvent
|
|
1126
|
+
* is also wrong (-1728). Lookup errors are not swallowed: a failed
|
|
1127
|
+
* delete must surface, then EventKit can run.
|
|
1128
|
+
*/
|
|
1129
|
+
export function buildRemoveEventScript(eventId, { calendarName = null } = {}) {
|
|
1130
|
+
const uidLit = asString(eventId);
|
|
1131
|
+
const scoped = calendarName
|
|
1132
|
+
? ` set targetCals to {}
|
|
1133
|
+
repeat with calRef in calendars
|
|
1134
|
+
try
|
|
1135
|
+
set cal to contents of calRef
|
|
1136
|
+
if (name of cal) is ${asString(calendarName)} then
|
|
1137
|
+
set end of targetCals to cal
|
|
1138
|
+
exit repeat
|
|
1139
|
+
end if
|
|
1140
|
+
end try
|
|
1141
|
+
end repeat
|
|
1142
|
+
if (count of targetCals) is 0 then error "CALENDAR_NOT_FOUND"`
|
|
1143
|
+
: ` set targetCals to calendars`;
|
|
1144
|
+
|
|
1145
|
+
return `tell application "Calendar"
|
|
1146
|
+
${scoped}
|
|
1147
|
+
repeat with calRef in targetCals
|
|
1148
|
+
set cal to contents of calRef
|
|
1149
|
+
set hits to {}
|
|
1150
|
+
try
|
|
1151
|
+
set hits to (every event of cal whose uid is ${uidLit})
|
|
1152
|
+
end try
|
|
1153
|
+
if (count of hits) > 0 then
|
|
1154
|
+
set theEvent to item 1 of hits
|
|
1155
|
+
set removedTitle to summary of theEvent
|
|
1156
|
+
set evId to id of theEvent
|
|
1157
|
+
try
|
|
1158
|
+
with timeout of 20 seconds
|
|
1159
|
+
delete (event id evId of cal)
|
|
1160
|
+
end timeout
|
|
1161
|
+
on error err1 number n1
|
|
1162
|
+
if n1 is -1712 then error "DELETE_TIMEOUT: " & err1
|
|
1163
|
+
try
|
|
1164
|
+
with timeout of 20 seconds
|
|
1165
|
+
tell cal
|
|
1166
|
+
delete theEvent
|
|
1167
|
+
end tell
|
|
1168
|
+
end timeout
|
|
1169
|
+
on error err2 number n2
|
|
1170
|
+
if n2 is -1712 then error "DELETE_TIMEOUT: " & err2
|
|
1171
|
+
error "DELETE_FAILED: " & err2 number n2
|
|
1172
|
+
end try
|
|
1173
|
+
end try
|
|
1174
|
+
try
|
|
1175
|
+
reload calendars
|
|
1176
|
+
end try
|
|
1177
|
+
return removedTitle
|
|
1178
|
+
end if
|
|
1179
|
+
end repeat
|
|
472
1180
|
end tell
|
|
473
|
-
|
|
1181
|
+
error "EVENT_NOT_FOUND"`;
|
|
474
1182
|
}
|
|
475
1183
|
|
|
476
1184
|
export function calendarRemove(args = {}) {
|
|
@@ -481,7 +1189,23 @@ export function calendarRemove(args = {}) {
|
|
|
481
1189
|
return { ok: false, message: `${action} refused: event_id is required (the "Event ID" from calendar_date). Deletes never run on a guessed id.` };
|
|
482
1190
|
}
|
|
483
1191
|
|
|
484
|
-
const
|
|
1192
|
+
const calendarName = args.calendar_name === undefined || args.calendar_name === null || args.calendar_name === ""
|
|
1193
|
+
? null
|
|
1194
|
+
: validateCalendarName(args.calendar_name);
|
|
1195
|
+
if (args.calendar_name && !calendarName) {
|
|
1196
|
+
return { ok: false, message: `${action} refused: calendar_name must match a calendar from calendar_list_calendars.` };
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
const eventKitId = args.eventkit_id === undefined || args.eventkit_id === null || args.eventkit_id === ""
|
|
1200
|
+
? null
|
|
1201
|
+
: validateEventKitId(args.eventkit_id);
|
|
1202
|
+
if (args.eventkit_id && !eventKitId) {
|
|
1203
|
+
return { ok: false, message: `${action} refused: eventkit_id is not a valid EventKit eventIdentifier.` };
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
const summary = calendarName
|
|
1207
|
+
? `delete calendar event ${eventId} from "${calendarName}"`
|
|
1208
|
+
: `delete calendar event ${eventId}`;
|
|
485
1209
|
const plan = planWrite({
|
|
486
1210
|
action,
|
|
487
1211
|
summary,
|
|
@@ -491,16 +1215,65 @@ export function calendarRemove(args = {}) {
|
|
|
491
1215
|
});
|
|
492
1216
|
if (!plan.proceed) return { ok: true, message: plan.message, planned: true };
|
|
493
1217
|
|
|
494
|
-
|
|
495
|
-
|
|
1218
|
+
// writeOnly cannot re-query; prefer the EventKit session that cached
|
|
1219
|
+
// the EKEvent from create. One-shot lookup is a full-access fallback.
|
|
1220
|
+
const sessionReply = eventKitSessionRequest({
|
|
1221
|
+
op: "remove",
|
|
1222
|
+
ids: eventKitLookupIds(eventId, eventKitId)
|
|
1223
|
+
});
|
|
1224
|
+
if (sessionReply && sessionReply.ok) {
|
|
1225
|
+
return {
|
|
1226
|
+
ok: true,
|
|
1227
|
+
message: writeSuccessMessage(action, "event deleted", {
|
|
1228
|
+
event_id: eventId,
|
|
1229
|
+
via: "EventKit",
|
|
1230
|
+
eventkit_id: eventKitId || undefined
|
|
1231
|
+
})
|
|
1232
|
+
};
|
|
1233
|
+
}
|
|
1234
|
+
if (sessionReply && !sessionReply.ok && !sessionReply.sessionDead && eventKitId) {
|
|
1235
|
+
return describeEventKitWriteFailure(action, summary, {
|
|
1236
|
+
ok: false,
|
|
1237
|
+
error: sessionReply.error || "EVENTKIT_SESSION_REMOVE_FAILED",
|
|
1238
|
+
kind: "not_found"
|
|
1239
|
+
});
|
|
1240
|
+
}
|
|
496
1241
|
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
1242
|
+
const eventKitResult = runAppleScript(buildEventKitRemoveScript(eventId, { eventKitId }), {
|
|
1243
|
+
timeout: 20000,
|
|
1244
|
+
appName: "Calendar",
|
|
1245
|
+
language: "JavaScript"
|
|
1246
|
+
});
|
|
1247
|
+
if (eventKitResult.ok) {
|
|
1248
|
+
return {
|
|
1249
|
+
ok: true,
|
|
1250
|
+
message: writeSuccessMessage(action, "event deleted", {
|
|
1251
|
+
event_id: eventId,
|
|
1252
|
+
via: "EventKit",
|
|
1253
|
+
eventkit_id: eventKitId || undefined
|
|
1254
|
+
})
|
|
1255
|
+
};
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
if (eventKitId) {
|
|
1259
|
+
return describeEventKitWriteFailure(action, summary, eventKitResult);
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
const result = runAppleScript(buildRemoveEventScript(eventId, { calendarName }), {
|
|
1263
|
+
timeout: 25000,
|
|
1264
|
+
appName: "Calendar"
|
|
1265
|
+
});
|
|
1266
|
+
if (result.ok) {
|
|
1267
|
+
return {
|
|
1268
|
+
ok: true,
|
|
1269
|
+
message: writeSuccessMessage(action, "event deleted", {
|
|
1270
|
+
event_id: eventId,
|
|
1271
|
+
title: truncate(result.output, 150) || undefined
|
|
1272
|
+
})
|
|
1273
|
+
};
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
return describeCalendarRemoveFailure(action, summary, result, eventKitResult);
|
|
504
1277
|
}
|
|
505
1278
|
|
|
506
1279
|
export function buildRsvpScript({ eventId, status, attendeeEmail }) {
|