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.
@@ -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
- const calendars = result.output
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) => `• ${c.name}${c.writable ? "" : " (read-only)"}`);
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
- export function buildRemoveEventScript(eventId) {
466
- return `${findEventHandler()}
467
-
468
- set theEvent to atmFindEvent(${asString(eventId)})
469
- tell application "Calendar"
470
- set removedTitle to summary of theEvent
471
- delete theEvent
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
- return removedTitle`;
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 summary = `delete calendar event ${eventId}`;
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
- const result = runAppleScript(buildRemoveEventScript(eventId), { timeout: 60000, appName: "Calendar" });
495
- if (!result.ok) return { ok: false, message: failure(action, summary, result) };
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
- return {
498
- ok: true,
499
- message: writeSuccessMessage(action, "event deleted", {
500
- event_id: eventId,
501
- title: truncate(result.output, 150) || undefined
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 }) {