@mulmoclaude/core 5.0.1 → 5.3.0

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.
Files changed (41) hide show
  1. package/assets/helps/error-recovery.md +78 -0
  2. package/assets/helps/google-calendar-collection.md +130 -15
  3. package/assets/helps/google.md +14 -4
  4. package/dist/collection/core/schemaZ.d.ts +15 -2
  5. package/dist/collection/core/viewChatPolicy.d.ts +6 -3
  6. package/dist/collection/index.cjs +6 -3
  7. package/dist/collection/index.cjs.map +1 -1
  8. package/dist/collection/index.js +6 -3
  9. package/dist/collection/index.js.map +1 -1
  10. package/dist/collection/registry/server/index.cjs +2 -2
  11. package/dist/collection/registry/server/index.js +2 -2
  12. package/dist/collection/server/index.cjs +2 -2
  13. package/dist/collection/server/index.js +2 -2
  14. package/dist/collection-watchers/index.cjs +2 -2
  15. package/dist/collection-watchers/index.js +2 -2
  16. package/dist/{discovery-CBjI1lx9.js → discovery-B_64PBZU.js} +14 -3
  17. package/dist/{discovery-CBjI1lx9.js.map → discovery-B_64PBZU.js.map} +1 -1
  18. package/dist/{discovery-DCMW05Bp.cjs → discovery-CDXYswtw.cjs} +14 -3
  19. package/dist/{discovery-DCMW05Bp.cjs.map → discovery-CDXYswtw.cjs.map} +1 -1
  20. package/dist/feeds/server/index.cjs +2 -2
  21. package/dist/feeds/server/index.js +2 -2
  22. package/dist/google/calendar.d.ts +24 -0
  23. package/dist/google/collectionPush.d.ts +45 -2
  24. package/dist/google/deletePlan.d.ts +32 -0
  25. package/dist/google/eventDerived.d.ts +19 -0
  26. package/dist/google/eventSpanInput.d.ts +58 -0
  27. package/dist/google/index.cjs +286 -18
  28. package/dist/google/index.cjs.map +1 -1
  29. package/dist/google/index.d.ts +5 -2
  30. package/dist/google/index.js +272 -19
  31. package/dist/google/index.js.map +1 -1
  32. package/dist/remote-view/index.cjs +7 -7
  33. package/dist/remote-view/index.cjs.map +1 -1
  34. package/dist/remote-view/index.d.ts +3 -3
  35. package/dist/remote-view/index.js +7 -7
  36. package/dist/remote-view/index.js.map +1 -1
  37. package/dist/{server-BMZ_BcPH.js → server-7POrhHKv.js} +2 -2
  38. package/dist/{server-BMZ_BcPH.js.map → server-7POrhHKv.js.map} +1 -1
  39. package/dist/{server-rA9FSkl2.cjs → server-DfYwSX8b.cjs} +2 -2
  40. package/dist/{server-rA9FSkl2.cjs.map → server-DfYwSX8b.cjs.map} +1 -1
  41. package/package.json +1 -1
@@ -1,5 +1,8 @@
1
1
  export { configureGoogleHost, type GoogleLogger } from './host.js';
2
2
  export { isIsoDateTimeWithOffset } from './datetime.js';
3
+ export { deleteRefusalMessage, planDelete, type DeleteDecision, type DeleteRefusal } from './deletePlan.js';
4
+ export { conferenceVideoUri, selfResponseStatus } from './eventDerived.js';
5
+ export { isCalendarDateOnly, resolvePartialSpanInput, resolveSpanInput, toEventTimeInput, eventTimeHint, EVENT_TIME_HINT, ALL_DAY_END_HINT, LONE_ALL_DAY_HINT, MIXED_SPAN_HINT, type PartialSpanResult, type PartialSpanTimes, type SpanResult, type SpanTimes, } from './eventSpanInput.js';
3
6
  export { googleConfigDir, googleSecretsDir, googleTokenPath, legacyGoogleTokenPath } from './paths.js';
4
7
  export { clientSecretPresence, findClientSecretPath, loadClientSecret, type ClientSecretPresence, type InstalledClientSecret } from './clientSecret.js';
5
8
  export { deleteGoogleTokens, loadGoogleTokens, mergeGoogleTokens, saveGoogleTokens, type IssuedVia, type StoredGoogleTokens } from './tokenStore.js';
@@ -7,13 +10,13 @@ export { brokerBaseUrl, brokerExchange, brokerRefresh, brokerStart, type BrokerS
7
10
  export { authorizeGoogle, commitLinkedTokens, getGoogleAccessToken, unlinkGoogle, waitForAuthCode, GOOGLE_AUTH_CANCELLED, GOOGLE_CALENDAR_SCOPE, GOOGLE_CALENDARLIST_SCOPE, GOOGLE_TASKS_SCOPE, GOOGLE_DRIVE_FILE_SCOPE, GOOGLE_SCOPES, type AuthorizeGoogleOptions, type RevokeFetch, } from './auth.js';
8
11
  export { createGoogleAuthFlow, googleAuthFlow, type GoogleAuthFlow, type GoogleAuthFlowStatus } from './authFlow.js';
9
12
  export { googleApiError, isGoogleApiError, GoogleApiError, DEFAULT_LIST_MAX_RESULTS, HTTP_FORBIDDEN, MAX_LIST_RESULTS } from './apiClient.js';
10
- export { buildEventPatch, calendarApiError, collectCalendarPages, createCalendarEvent, deleteCalendarEvent, getCalendarColors, canonicalCalendarId, listCalendarEvents, listCalendars, syncCalendarEvents, toCalendarMeta, toCalendarSummary, toEventSummary, updateCalendarEvent, getCalendarEvent, getCalendarMeta, resolveEventSpan, CANCELLED_EVENT_STATUS, HTTP_CONFLICT, HTTP_PRECONDITION_FAILED, type CalendarColorEntry, type CalendarColors, type CalendarEventInput, type CalendarEventSpan, type CalendarEventSummary, type CalendarEventTime, type CalendarListPage, type CalendarMeta, type CalendarSummary, type CalendarSyncResult, type DeleteCalendarEventInput, type FetchedCalendarEvent, type ListEventsInput, type SyncEventsInput, type UpdateCalendarEventInput, syncPageParams, } from './calendar.js';
13
+ export { buildEventPatch, calendarApiError, collectCalendarPages, createCalendarEvent, deleteCalendarEvent, getCalendarColors, canonicalCalendarId, listCalendarEvents, listCalendars, syncCalendarEvents, toCalendarMeta, toCalendarSummary, toEventSummary, updateCalendarEvent, getCalendarEvent, getCalendarMeta, resolveEventSpan, CANCELLED_EVENT_STATUS, EVENT_ABSENT_STATUSES, HTTP_CONFLICT, HTTP_PRECONDITION_FAILED, type CalendarColorEntry, type CalendarColors, type CalendarEventInput, type CalendarEventSpan, type CalendarEventSummary, type CalendarEventTime, type CalendarListPage, type CalendarMeta, type CalendarSummary, type CalendarSyncResult, type DeleteCalendarEventInput, type FetchedCalendarEvent, type ListEventsInput, type SyncEventsInput, type UpdateCalendarEventInput, syncPageParams, } from './calendar.js';
11
14
  export { calendarSyncStatePath, claimCalendarSyncIfDue, clearCalendarLastSyncedAt, clearCalendarSyncToken, loadCalendarLastSyncedAt, loadCalendarSyncToken, saveCalendarSyncToken, toolCalendarSyncKey, } from './calendarSyncStore.js';
12
15
  export { calendarSyncDueWindowMs, isCalendarSyncDue } from './calendarSyncDue.js';
13
16
  export { calendarBackfillPath, markCalendarBackfilled, needsCalendarBackfill } from './calendarBackfillState.js';
14
17
  export { stateLockPath, withCalendarStateLock, liveLockClock, type LockClock } from './calendarStateLock.js';
15
18
  export { calendarPushStatePath, clearCalendarShadow, loadCalendarShadow, mergeShadow, saveCalendarShadow, toShadowEvent, type ShadowEvent, } from './calendarPushState.js';
16
- export { isDeniedAccessRole, isUnpushed, locallyEditedIds, pushCalendarForCollection, pushCollectionNow, reportedAccessRole, unsentLocalEdits, type PushOutcomeKind, type CalendarCollectionPushResult, type CalendarPushDeps, type CalendarPushOutcome, type CalendarWriteTarget, } from './collectionPush.js';
19
+ export { isDeniedAccessRole, isUnpushed, locallyEditedIds, pushCalendarForCollection, pushCollectionNow, reportedAccessRole, sweepDeletes, unsentLocalEdits, type DeleteSweep, type DeleteSweepDeps, type PushOutcomeKind, type CalendarCollectionPushResult, type CalendarPushDeps, type CalendarPushOutcome, type CalendarWriteTarget, } from './collectionPush.js';
17
20
  export { toGoogleEventTime, zoneSuffixOf } from './pushDateTime.js';
18
21
  export { baselineRecord, bySourceField, conflictingFields, fieldText, isClientSettableEventId, locallyChangedFields, locallyDeletedIds, mayAdoptExisting, planRecord, pushableMap, PUSHABLE_SOURCE_FIELDS, type PushableSourceField, type RecordPlan, } from './pushPlan.js';
19
22
  export { toCollectionDateTime } from './collectionDateTime.js';
@@ -5,7 +5,7 @@ import { n as writeFileAtomic } from "../root-BcS4-Kge.js";
5
5
  import { fetchWithTimeout } from "../utils/fetch.js";
6
6
  import { d as parseIsoDate, f as parseIsoDateTime } from "../itemId-4P9S_ehz.js";
7
7
  import { t as mergeIntoExisting } from "../project-B0j_Vf_Q.js";
8
- import { _t as createForwardingLogger, d as storeFor, n as discoverCollections, rt as getWorkspaceRoot, vt as createHostSlot } from "../discovery-CBjI1lx9.js";
8
+ import { _t as createForwardingLogger, d as storeFor, n as discoverCollections, rt as getWorkspaceRoot, vt as createHostSlot } from "../discovery-B_64PBZU.js";
9
9
  import { t as mulmoConfigDir } from "../paths-CkJxNZ7T.js";
10
10
  import { promises } from "node:fs";
11
11
  import path, { join } from "node:path";
@@ -41,6 +41,172 @@ var isIsoDateTimeWithOffset = (value) => {
41
41
  return isRealCalendarDate(year, month, day) && timeInRange && offsetInRange;
42
42
  };
43
43
  //#endregion
44
+ //#region src/google/deletePlan.ts
45
+ var ATTENDEES_REASON = "it carries attendees — deleting an invited event withdraws it from their calendars too";
46
+ var ALREADY_GONE_REASON = "it is no longer in Google";
47
+ /** The message the push reports for a refusal, with the event named. */
48
+ var deleteRefusalMessage = (eventId, refusal) => `${eventId}: left in Google because ${refusal.kind === "has-attendees" ? ATTENDEES_REASON : ALREADY_GONE_REASON}`;
49
+ /** Decide one deletion against what Google currently holds.
50
+ *
51
+ * `null` means the event is already absent — a 404/410 on the read. Nothing to
52
+ * delete, and reported apart from a refusal so "we chose not to" never reads
53
+ * as "we could not".
54
+ *
55
+ * ANY attendee refuses, including the entry Google adds for the organiser. A
56
+ * rule that tried to exclude "only me" would have to work out which entry is
57
+ * the user from a payload that may not say, and being wrong there withdraws a
58
+ * real invitation. Solo events — the ones this feature exists for — carry no
59
+ * attendees at all, so the blunt rule costs them nothing. */
60
+ function planDelete(fetched) {
61
+ if (fetched === null) return {
62
+ ok: false,
63
+ kind: "already-gone"
64
+ };
65
+ return fetched.attendeeCount > 0 ? {
66
+ ok: false,
67
+ kind: "has-attendees"
68
+ } : {
69
+ ok: true,
70
+ etag: fetched.etag
71
+ };
72
+ }
73
+ //#endregion
74
+ //#region src/google/util.ts
75
+ var ONE_SECOND_MS = 1e3;
76
+ var ONE_MINUTE_MS = 6e4;
77
+ function isRecord(value) {
78
+ return typeof value === "object" && value !== null && !Array.isArray(value);
79
+ }
80
+ //#endregion
81
+ //#region src/google/eventDerived.ts
82
+ var stringAt = (record, key) => typeof record[key] === "string" ? record[key] : "";
83
+ var recordsIn = (value) => Array.isArray(value) ? value.filter(isRecord) : [];
84
+ /** The signed-in user's own `responseStatus` on this event, `""` when Google
85
+ * reported none.
86
+ *
87
+ * `""` is the common case, not an edge one: an event with no attendees has no
88
+ * entry to mark `self`, which is most of a personal calendar. So it means
89
+ * "Google said nothing", never "not going" — filter with `!= "declined"`, not
90
+ * with `== "accepted"`, or every solo event disappears too.
91
+ *
92
+ * Google's values: `needsAction`, `declined`, `tentative`, `accepted`. */
93
+ function selfResponseStatus(attendees) {
94
+ const self = recordsIn(attendees).find((attendee) => attendee.self === true);
95
+ return self === void 0 ? "" : stringAt(self, "responseStatus");
96
+ }
97
+ var VIDEO_ENTRY_POINT = "video";
98
+ /** The URL that joins this event's meeting, `""` when it has none.
99
+ *
100
+ * Only the `video` entry point. The others are a phone number and a dial-in
101
+ * PIN page, and a column named for joining that sometimes holds `tel:` would
102
+ * be worse than one that is empty — the caller can see empty and fall back.
103
+ *
104
+ * `hangoutLink` (#3229) already carries this for Google Meet; this is what
105
+ * reaches the events whose conference is Zoom or Teams. */
106
+ function conferenceVideoUri(conferenceData) {
107
+ if (!isRecord(conferenceData)) return "";
108
+ const video = recordsIn(conferenceData.entryPoints).find((entry) => entry.entryPointType === VIDEO_ENTRY_POINT);
109
+ return video === void 0 ? "" : stringAt(video, "uri");
110
+ }
111
+ //#endregion
112
+ //#region src/google/eventSpanInput.ts
113
+ var DATE_ONLY_RE$1 = /^\d{4}-\d{2}-\d{2}$/;
114
+ /** Whether `value` is a bare calendar day — the all-day spelling of an event
115
+ * time, and the one shape `isIsoDateTimeWithOffset` is built to refuse.
116
+ *
117
+ * Realness comes from the parser the record lint, the calendar grid and the
118
+ * push already share, so every surface agrees that `2026-02-30` is not a day.
119
+ * The regex runs first because that parser trims, and a value with whitespace
120
+ * around it would validate here and then reach Google verbatim. */
121
+ var isCalendarDateOnly = (value) => DATE_ONLY_RE$1.test(value) && parseIsoDate(value) !== null;
122
+ /** One end of a span, or null when it is neither shape Calendar accepts. */
123
+ var toEventTimeInput = (value) => {
124
+ if (isCalendarDateOnly(value)) return { date: value };
125
+ return isIsoDateTimeWithOffset(value) ? { dateTime: value } : null;
126
+ };
127
+ /** The rule every surface states for a value that is neither shape. Shared so
128
+ * the tool, the remote host and their tests cannot drift apart. */
129
+ var EVENT_TIME_HINT = "must be an ISO 8601 date-time with a timezone offset (e.g. 2026-07-17T09:00:00+09:00), or a date for an all-day event (e.g. 2026-07-17)";
130
+ /** The same rule with the offending key named, for a surface whose error text
131
+ * carries no path of its own. */
132
+ var eventTimeHint = (key) => `${key} ${EVENT_TIME_HINT}`;
133
+ /** All-day `end` is exclusive, so a one-day event ends on the NEXT day. Spelled
134
+ * out because the natural reading of "all day on the 17th" is `end` = the
135
+ * 17th, which Calendar refuses. */
136
+ var ALL_DAY_END_HINT = "an all-day `end` is EXCLUSIVE — it is the day AFTER the last day, so a single day on 2026-07-17 needs end 2026-07-18";
137
+ var MIXED_SPAN_HINT = "start and end must both be date-times with an offset, or both be dates for an all-day event — not one of each";
138
+ /** A lone all-day end cannot be patched: the stored event's kind is unknown
139
+ * here, and giving one end of a timed event a `date` makes an event Calendar
140
+ * rejects — with a message that names neither end. Both together are
141
+ * unambiguous, so that is what the caller is asked for. */
142
+ var LONE_ALL_DAY_HINT = `pass BOTH start and end to move an all-day event — ${ALL_DAY_END_HINT}`;
143
+ var isAllDay = (time) => "date" in time;
144
+ /** Both ends of a create, validated together.
145
+ *
146
+ * Only the all-day ordering is checked: a timed span's ordering depends on the
147
+ * two offsets, which Calendar resolves and reports on clearly, while an all-day
148
+ * `end <= start` comes back as an opaque 400. Lexicographic comparison is exact
149
+ * for `YYYY-MM-DD` — fixed-width, most significant part first. */
150
+ function resolveSpanInput(start, end) {
151
+ const startTime = toEventTimeInput(start);
152
+ if (startTime === null) return {
153
+ ok: false,
154
+ reason: eventTimeHint("start")
155
+ };
156
+ const endTime = toEventTimeInput(end);
157
+ if (endTime === null) return {
158
+ ok: false,
159
+ reason: eventTimeHint("end")
160
+ };
161
+ if (isAllDay(startTime) !== isAllDay(endTime)) return {
162
+ ok: false,
163
+ reason: MIXED_SPAN_HINT
164
+ };
165
+ if (isAllDay(startTime) && isAllDay(endTime) && endTime.date <= startTime.date) return {
166
+ ok: false,
167
+ reason: ALL_DAY_END_HINT
168
+ };
169
+ return {
170
+ ok: true,
171
+ span: {
172
+ start: startTime,
173
+ end: endTime
174
+ }
175
+ };
176
+ }
177
+ /** One end of an EDIT, where the other end is absent. */
178
+ function resolveLoneEnd(key, value) {
179
+ const time = toEventTimeInput(value);
180
+ if (time === null) return {
181
+ ok: false,
182
+ reason: eventTimeHint(key)
183
+ };
184
+ if (isAllDay(time)) return {
185
+ ok: false,
186
+ reason: LONE_ALL_DAY_HINT
187
+ };
188
+ return {
189
+ ok: true,
190
+ times: { [key]: time }
191
+ };
192
+ }
193
+ /** The span of an EDIT, where either end may be absent. */
194
+ function resolvePartialSpanInput(start, end) {
195
+ if (start !== void 0 && end !== void 0) {
196
+ const resolved = resolveSpanInput(start, end);
197
+ return resolved.ok ? {
198
+ ok: true,
199
+ times: resolved.span
200
+ } : resolved;
201
+ }
202
+ if (start !== void 0) return resolveLoneEnd("start", start);
203
+ if (end !== void 0) return resolveLoneEnd("end", end);
204
+ return {
205
+ ok: true,
206
+ times: {}
207
+ };
208
+ }
209
+ //#endregion
44
210
  //#region src/google/paths.ts
45
211
  /** Same directory as every other cross-app per-user file — see
46
212
  * `../global-config/paths.ts`, which owns the spelling. */
@@ -58,13 +224,6 @@ function googleSecretsDir(home) {
58
224
  return join(home ?? homedir(), ".secrets");
59
225
  }
60
226
  //#endregion
61
- //#region src/google/util.ts
62
- var ONE_SECOND_MS = 1e3;
63
- var ONE_MINUTE_MS = 6e4;
64
- function isRecord(value) {
65
- return typeof value === "object" && value !== null && !Array.isArray(value);
66
- }
67
- //#endregion
68
227
  //#region src/google/clientSecret.ts
69
228
  var isNonEmptyString = (value) => typeof value === "string" && value !== "";
70
229
  var isInstalledClientSecret = (value) => {
@@ -595,6 +754,10 @@ var EVENT_SYNC_PAGE_SIZE = 2500;
595
754
  var MAX_EVENT_SYNC_PAGES = 200;
596
755
  var CALENDAR_META_PAGE_SIZE = 1;
597
756
  var HTTP_GONE = 410;
757
+ /** A GET or DELETE on an event that is not there. Exported because the push's
758
+ * delete sweep has to read "already gone" as success, not as a failure to
759
+ * retry — a retained baseline would announce the same phantom deletion on
760
+ * every run. */
598
761
  var EVENT_ABSENT_STATUSES = [404, HTTP_GONE];
599
762
  /** An `If-Match` write whose etag no longer matches — someone else changed the
600
763
  * event since it was read. */
@@ -612,6 +775,10 @@ var CANCELLED_EVENT_STATUS = "cancelled";
612
775
  * apart while they share a sync token silently loses events (#2184). */
613
776
  var canonicalCalendarId = (calendarId) => calendarId || DEFAULT_CALENDAR_ID;
614
777
  var eventsUrl = (calendarId) => `${CALENDAR_BASE_URL}/calendars/${encodeURIComponent(canonicalCalendarId(calendarId))}/events`;
778
+ /** People Google lists on the event, `0` when it carries none. Counts EVERY
779
+ * entry, including the one Google adds for the organiser: see `deletePlan.ts`
780
+ * for why the guard is not cleverer than that. */
781
+ var attendeeCount = (record) => Array.isArray(record.attendees) ? record.attendees.length : 0;
615
782
  var eventTime$1 = (value) => {
616
783
  if (!isRecord(value)) return "";
617
784
  if (typeof value.dateTime === "string") return value.dateTime;
@@ -635,7 +802,9 @@ var toEventSummary = (value) => {
635
802
  updated: stringField(record, "updated"),
636
803
  transparency: stringField(record, "transparency"),
637
804
  eventType: stringField(record, "eventType"),
638
- hangoutLink: stringField(record, "hangoutLink")
805
+ hangoutLink: stringField(record, "hangoutLink"),
806
+ selfResponseStatus: selfResponseStatus(record.attendees),
807
+ conferenceVideoUri: conferenceVideoUri(record.conferenceData)
639
808
  };
640
809
  };
641
810
  var toCalendarSummary = (value) => {
@@ -726,9 +895,11 @@ async function updateCalendarEvent(accessToken, input) {
726
895
  async function getCalendarEvent(accessToken, input) {
727
896
  try {
728
897
  const fetched = await googleRequest(CALENDAR_API_LABEL, accessToken, eventUrl(input.calendarId, input.eventId));
898
+ const record = asRecord(fetched);
729
899
  return {
730
900
  event: toEventSummary(fetched),
731
- etag: stringField(asRecord(fetched), "etag")
901
+ etag: stringField(record, "etag"),
902
+ attendeeCount: attendeeCount(record)
732
903
  };
733
904
  } catch (error) {
734
905
  if (isGoogleApiError(error) && EVENT_ABSENT_STATUSES.includes(error.status)) return null;
@@ -739,7 +910,10 @@ async function getCalendarEvent(accessToken, input) {
739
910
  * return; a second delete of the same id answers 410, which surfaces as a
740
911
  * GoogleApiError rather than being swallowed. */
741
912
  async function deleteCalendarEvent(accessToken, input) {
742
- await googleRequest(CALENDAR_API_LABEL, accessToken, eventUrl(input.calendarId, input.eventId), { method: "DELETE" });
913
+ await googleRequest(CALENDAR_API_LABEL, accessToken, eventUrl(input.calendarId, input.eventId), {
914
+ method: "DELETE",
915
+ ...input.ifMatch ? { extraHeaders: { "If-Match": input.ifMatch } } : {}
916
+ });
743
917
  }
744
918
  async function listCalendarEvents(accessToken, input = {}) {
745
919
  const params = new URLSearchParams({
@@ -1521,7 +1695,9 @@ var UNPUSHABLE_EVENT_FIELDS = {
1521
1695
  updated: "",
1522
1696
  transparency: "",
1523
1697
  eventType: "",
1524
- hangoutLink: ""
1698
+ hangoutLink: "",
1699
+ selfResponseStatus: "",
1700
+ conferenceVideoUri: ""
1525
1701
  };
1526
1702
  /** A baseline shaped as the event it came from, so the comparison can run
1527
1703
  * through the very projection that wrote the record. */
@@ -1642,7 +1818,9 @@ var liveDeps = {
1642
1818
  findCollection: findCalendarCollection,
1643
1819
  isLinked: async () => Boolean((await loadGoogleTokens())?.refresh_token),
1644
1820
  accessToken: getGoogleAccessToken,
1645
- calendarMeta: liveCalendarMeta
1821
+ calendarMeta: liveCalendarMeta,
1822
+ fetchEvent: getCalendarEvent,
1823
+ deleteEvent: deleteCalendarEvent
1646
1824
  };
1647
1825
  var localValue = (ctx, record, source) => {
1648
1826
  const field = ctx.bySource[source];
@@ -1852,7 +2030,7 @@ var UNPUSHED_KINDS = ["conflict", "error"];
1852
2030
  /** Whether this outcome leaves a local edit that only exists locally, so the
1853
2031
  * pull must not overwrite the record and the baseline must not advance. */
1854
2032
  var isUnpushed = (kind) => UNPUSHED_KINDS.includes(kind);
1855
- function tally(slug, attempts, localDeletes) {
2033
+ function tally(slug, attempts, deletes) {
1856
2034
  const outcomes = attempts.map((attempt) => attempt.outcome);
1857
2035
  const count = (kind) => outcomes.filter((outcome) => outcome.kind === kind).length;
1858
2036
  return {
@@ -1860,9 +2038,10 @@ function tally(slug, attempts, localDeletes) {
1860
2038
  created: count("created"),
1861
2039
  updated: count("updated"),
1862
2040
  conflicts: count("conflict"),
1863
- localDeletes,
1864
- skipped: outcomes.flatMap((outcome) => outcome.kind === "skipped" ? [outcome.message] : []),
1865
- errors: outcomes.flatMap((outcome) => outcome.kind === "error" ? [outcome.message] : []),
2041
+ localDeletes: deletes.seen,
2042
+ deletedInGoogle: deletes.deleted.length,
2043
+ skipped: [...outcomes.flatMap((outcome) => outcome.kind === "skipped" ? [outcome.message] : []), ...deletes.skipped],
2044
+ errors: [...outcomes.flatMap((outcome) => outcome.kind === "error" ? [outcome.message] : []), ...deletes.errors],
1866
2045
  unpushedIds: attempts.filter((attempt) => isUnpushed(attempt.outcome.kind)).map((attempt) => attempt.eventId)
1867
2046
  };
1868
2047
  }
@@ -1891,6 +2070,69 @@ async function unsentLocalEdits(collection, workspaceRoot) {
1891
2070
  const shadow = await loadCalendarShadow(schema.googleCalendar?.calendarId, workspaceRoot);
1892
2071
  return locallyEditedIds(await storeFor(collection, { workspaceRoot }).list(), shadow, pushableMap(schema.googleCalendar?.map ?? {}), schema.primaryKey, schema.fields);
1893
2072
  }
2073
+ var noDeletes = (seen) => ({
2074
+ seen,
2075
+ deleted: [],
2076
+ skipped: [],
2077
+ errors: []
2078
+ });
2079
+ /** A delete whose event changed in Google between the guard reading it and the
2080
+ * delete landing. Reported rather than retried: what changed may be the very
2081
+ * thing the guard refuses, and re-reading in a loop would race the same way. */
2082
+ var staleDeleteMessage = (eventId) => `${eventId}: left in Google because it changed there while this push was checking it — press Push again to re-check`;
2083
+ /** Delete ONE event and forget its baseline, or say why it was left alone.
2084
+ *
2085
+ * The baseline entry goes only after Google confirms, and it must go: the
2086
+ * baseline is what `locallyDeletedIds` reads, so a kept entry would re-report
2087
+ * the same deletion on every run forever. A refusal keeps its entry on
2088
+ * purpose — the event IS still standing in Google, and the report is the only
2089
+ * thing saying so.
2090
+ *
2091
+ * The delete carries the etag the guard read. Without it, an attendee added
2092
+ * between the two requests would be withdrawn by a decision taken before they
2093
+ * existed — the same race `updateCalendarEvent` already guards, and it costs
2094
+ * more here because a delete cannot be undone from this side. */
2095
+ async function propagateOneDelete(eventId, deps) {
2096
+ const decision = planDelete(await deps.fetchEvent(eventId));
2097
+ if (!decision.ok) {
2098
+ if (decision.kind === "already-gone") {
2099
+ await deps.forget(eventId);
2100
+ return {};
2101
+ }
2102
+ return { skipped: [deleteRefusalMessage(eventId, decision)] };
2103
+ }
2104
+ try {
2105
+ await deps.deleteEvent(eventId, decision.etag);
2106
+ } catch (error) {
2107
+ if (!isGoogleApiError(error)) throw error;
2108
+ if (EVENT_ABSENT_STATUSES.includes(error.status)) {
2109
+ await deps.forget(eventId);
2110
+ return {};
2111
+ }
2112
+ if (error.status !== 412) throw error;
2113
+ return { skipped: [staleDeleteMessage(eventId)] };
2114
+ }
2115
+ await deps.forget(eventId);
2116
+ log.info("google", "calendar event deleted by push", { id: eventId });
2117
+ return { deleted: [eventId] };
2118
+ }
2119
+ /** Carry every local deletion, when the collection opted in.
2120
+ *
2121
+ * Sequential rather than concurrent: each one writes the shared baseline file,
2122
+ * and an interrupted run has to leave a state the next run resumes from rather
2123
+ * than a partially applied batch. */
2124
+ async function sweepDeletes(eventIds, propagate, deps) {
2125
+ if (!propagate || eventIds.length === 0) return noDeletes(eventIds.length);
2126
+ const sweep = noDeletes(eventIds.length);
2127
+ for (const eventId of eventIds) try {
2128
+ const one = await propagateOneDelete(eventId, deps);
2129
+ sweep.deleted.push(...one.deleted ?? []);
2130
+ sweep.skipped.push(...one.skipped ?? []);
2131
+ } catch (error) {
2132
+ sweep.errors.push(`${eventId}: could not be deleted in Google — ${errorMessage(error)}`);
2133
+ }
2134
+ return sweep;
2135
+ }
1894
2136
  /** Push one collection WITHOUT taking the calendar lock.
1895
2137
  *
1896
2138
  * The caller must already hold it. The scheduled push→pull cycle
@@ -1929,7 +2171,18 @@ async function pushCollectionNow(collection, workspaceRoot, deps = liveDeps) {
1929
2171
  });
1930
2172
  await saveCalendarShadow(calendarId, pushedShadow([outcome]), workspaceRoot);
1931
2173
  }
1932
- const result = tally(slug, attempts, locallyDeletedIds(shadow, records.map((record) => fieldText(record[schema.primaryKey]))).length);
2174
+ const result = tally(slug, attempts, await sweepDeletes(locallyDeletedIds(shadow, records.map((record) => fieldText(record[schema.primaryKey]))), schema.googleCalendar?.propagateDeletes === true, {
2175
+ fetchEvent: (eventId) => (deps.fetchEvent ?? getCalendarEvent)(accessToken, {
2176
+ calendarId,
2177
+ eventId
2178
+ }),
2179
+ deleteEvent: (eventId, ifMatch) => (deps.deleteEvent ?? deleteCalendarEvent)(accessToken, {
2180
+ calendarId,
2181
+ eventId,
2182
+ ifMatch
2183
+ }),
2184
+ forget: (eventId) => saveCalendarShadow(calendarId, { [eventId]: null }, workspaceRoot)
2185
+ }));
1933
2186
  if (result.errors.length > 0) log.warn("google", "calendar push finished with errors", {
1934
2187
  slug,
1935
2188
  errors: result.errors.length
@@ -2887,6 +3140,6 @@ async function deleteDriveFile(accessToken, input) {
2887
3140
  await googleRequest(DRIVE_API_LABEL, accessToken, `${DRIVE_FILES_URL}/${encodeURIComponent(input.fileId)}`, { method: "DELETE" });
2888
3141
  }
2889
3142
  //#endregion
2890
- export { CANCELLED_EVENT_STATUS, DEFAULT_LIST_MAX_RESULTS, GOOGLE_AUTH_CANCELLED, GOOGLE_CALENDARLIST_SCOPE, GOOGLE_CALENDAR_SCOPE, GOOGLE_CALENDAR_SYNC_TASK_ID, GOOGLE_DRIVE_FILE_SCOPE, GOOGLE_SCOPES, GOOGLE_TASKS_SCOPE, GoogleApiError, HTTP_CONFLICT, HTTP_FORBIDDEN, HTTP_PRECONDITION_FAILED, MAX_LIST_RESULTS, PARTIAL_CALENDAR_WINDOW, PROTECTION_UNKNOWN, PUSHABLE_SOURCE_FIELDS, allUnpushed, anySyncedCollectionSurvives, applyPlanFor, assertSafeMimeType, authorizeGoogle, baselineRecord, brokerBaseUrl, brokerExchange, brokerRefresh, brokerStart, buildEventPatch, buildMultipartBody, buildTaskPatch, bySourceField, calendarApiError, calendarBackfillPath, calendarPushStatePath, calendarSyncDueWindowMs, calendarSyncStatePath, canonicalCalendarId, canonicalTaskListId, claimCalendarSyncIfDue, classifyDelete, classifyWrite, clearCalendarLastSyncedAt, clearCalendarShadow, clearCalendarSyncToken, clientSecretPresence, collectCalendarPages, collectionsNeedingBackfill, commitLinkedTokens, completeTask, configureGoogleHost, conflictingFields, createCalendarEvent, createDriveFile, createGoogleAuthFlow, createTask, deleteCalendarEvent, deleteDriveFile, deleteGoogleTokens, deleteTask, fieldText, findClientSecretPath, getCalendarColors, getCalendarEvent, getCalendarMeta, getGoogleAccessToken, googleApiError, googleAuthFlow, googleCalendarSyncTaskDef, googleConfigDir, googleSecretsDir, googleTokenPath, groupByCalendar, groupsNeedingBackfill, heldBack, isCalendarSyncDue, isClientSettableEventId, isDeniedAccessRole, isGoogleApiError, isIsoDateTimeWithOffset, isTextMimeType, isUnpushed, legacyGoogleTokenPath, listCalendarEvents, listCalendars, listDriveFiles, listTaskLists, listTasks, liveLockClock, loadCalendarLastSyncedAt, loadCalendarShadow, loadCalendarSyncToken, loadClientSecret, loadGoogleTokens, locallyChangedFields, locallyDeletedIds, locallyEditedIds, markCalendarBackfilled, mayAdoptExisting, mergeGoogleTokens, mergeIntoExisting, mergeShadow, needsCalendarBackfill, orphanedCalendarId, pickBoundary, planRecord, pullProtectionFor, pullableEvents, pushAndProtect, pushCalendarForCollection, pushCollectionNow, pushableMap, readDriveFile, releaseOrphanedCalendarToken, reportedAccessRole, resolveEventSpan, resumableToken, saveCalendarShadow, saveCalendarSyncToken, saveGoogleTokens, shadowUpdates, stateLockPath, syncCalendarEvents, syncCalendarForCollection, syncCalendarGroup, syncDueCalendarCollections, syncNewCalendarCollections, syncPageParams, toCalendarMeta, toCalendarSummary, toCollectionDateTime, toCollectionRecord, toDriveFileSummary, toEventSummary, toGoogleEventTime, toShadowEvent, toTaskListSummary, toTaskSummary, toolCalendarSyncKey, uncompleteTask, unlinkGoogle, unpushedFor, unsentEditGuard, unsentLocalEdits, updateCalendarEvent, updateTask, waitForAuthCode, windowAdvance, withCalendarLock, withCalendarStateLock, withKeyedLock, withPartialWindowError, zoneSuffixOf };
3143
+ export { ALL_DAY_END_HINT, CANCELLED_EVENT_STATUS, DEFAULT_LIST_MAX_RESULTS, EVENT_ABSENT_STATUSES, EVENT_TIME_HINT, GOOGLE_AUTH_CANCELLED, GOOGLE_CALENDARLIST_SCOPE, GOOGLE_CALENDAR_SCOPE, GOOGLE_CALENDAR_SYNC_TASK_ID, GOOGLE_DRIVE_FILE_SCOPE, GOOGLE_SCOPES, GOOGLE_TASKS_SCOPE, GoogleApiError, HTTP_CONFLICT, HTTP_FORBIDDEN, HTTP_PRECONDITION_FAILED, LONE_ALL_DAY_HINT, MAX_LIST_RESULTS, MIXED_SPAN_HINT, PARTIAL_CALENDAR_WINDOW, PROTECTION_UNKNOWN, PUSHABLE_SOURCE_FIELDS, allUnpushed, anySyncedCollectionSurvives, applyPlanFor, assertSafeMimeType, authorizeGoogle, baselineRecord, brokerBaseUrl, brokerExchange, brokerRefresh, brokerStart, buildEventPatch, buildMultipartBody, buildTaskPatch, bySourceField, calendarApiError, calendarBackfillPath, calendarPushStatePath, calendarSyncDueWindowMs, calendarSyncStatePath, canonicalCalendarId, canonicalTaskListId, claimCalendarSyncIfDue, classifyDelete, classifyWrite, clearCalendarLastSyncedAt, clearCalendarShadow, clearCalendarSyncToken, clientSecretPresence, collectCalendarPages, collectionsNeedingBackfill, commitLinkedTokens, completeTask, conferenceVideoUri, configureGoogleHost, conflictingFields, createCalendarEvent, createDriveFile, createGoogleAuthFlow, createTask, deleteCalendarEvent, deleteDriveFile, deleteGoogleTokens, deleteRefusalMessage, deleteTask, eventTimeHint, fieldText, findClientSecretPath, getCalendarColors, getCalendarEvent, getCalendarMeta, getGoogleAccessToken, googleApiError, googleAuthFlow, googleCalendarSyncTaskDef, googleConfigDir, googleSecretsDir, googleTokenPath, groupByCalendar, groupsNeedingBackfill, heldBack, isCalendarDateOnly, isCalendarSyncDue, isClientSettableEventId, isDeniedAccessRole, isGoogleApiError, isIsoDateTimeWithOffset, isTextMimeType, isUnpushed, legacyGoogleTokenPath, listCalendarEvents, listCalendars, listDriveFiles, listTaskLists, listTasks, liveLockClock, loadCalendarLastSyncedAt, loadCalendarShadow, loadCalendarSyncToken, loadClientSecret, loadGoogleTokens, locallyChangedFields, locallyDeletedIds, locallyEditedIds, markCalendarBackfilled, mayAdoptExisting, mergeGoogleTokens, mergeIntoExisting, mergeShadow, needsCalendarBackfill, orphanedCalendarId, pickBoundary, planDelete, planRecord, pullProtectionFor, pullableEvents, pushAndProtect, pushCalendarForCollection, pushCollectionNow, pushableMap, readDriveFile, releaseOrphanedCalendarToken, reportedAccessRole, resolveEventSpan, resolvePartialSpanInput, resolveSpanInput, resumableToken, saveCalendarShadow, saveCalendarSyncToken, saveGoogleTokens, selfResponseStatus, shadowUpdates, stateLockPath, sweepDeletes, syncCalendarEvents, syncCalendarForCollection, syncCalendarGroup, syncDueCalendarCollections, syncNewCalendarCollections, syncPageParams, toCalendarMeta, toCalendarSummary, toCollectionDateTime, toCollectionRecord, toDriveFileSummary, toEventSummary, toEventTimeInput, toGoogleEventTime, toShadowEvent, toTaskListSummary, toTaskSummary, toolCalendarSyncKey, uncompleteTask, unlinkGoogle, unpushedFor, unsentEditGuard, unsentLocalEdits, updateCalendarEvent, updateTask, waitForAuthCode, windowAdvance, withCalendarLock, withCalendarStateLock, withKeyedLock, withPartialWindowError, zoneSuffixOf };
2891
3144
 
2892
3145
  //# sourceMappingURL=index.js.map