@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
@@ -4,8 +4,8 @@ const require_dist = require("../../dist-Gj7ygaW2.cjs");
4
4
  const require_root = require("../../root-BvwBUcol.cjs");
5
5
  const require_itemId = require("../../itemId-CGT2J7YK.cjs");
6
6
  const require_project = require("../../project-C1ep9pvo.cjs");
7
- const require_discovery = require("../../discovery-DCMW05Bp.cjs");
8
- const require_server = require("../../server-rA9FSkl2.cjs");
7
+ const require_discovery = require("../../discovery-CDXYswtw.cjs");
8
+ const require_server = require("../../server-DfYwSX8b.cjs");
9
9
  const require_feeds_paths = require("../paths.cjs");
10
10
  const require_ingestTypes = require("../../ingestTypes-DsVH9BYy.cjs");
11
11
  const require_notifier = require("../../notifier-B65Mvngb.cjs");
@@ -2,8 +2,8 @@ import { a as isErrorWithCode, l as isUnknownArray, s as isRecord$1 } from "../.
2
2
  import { t as canonicalRoot } from "../../root-BcS4-Kge.js";
3
3
  import { G as INGEST_KINDS, W as FEED_SCHEDULES } from "../../itemId-4P9S_ehz.js";
4
4
  import { t as mergeIntoExisting } from "../../project-B0j_Vf_Q.js";
5
- import { W as resolveDataDir, _t as createForwardingLogger, d as storeFor, k as listItems, n as discoverCollections, q as safeSlugName, vt as createHostSlot } from "../../discovery-CBjI1lx9.js";
6
- import { F as buildCollectionActionSeedPrompt, I as promptPathsFor, z as readSkillTemplate } from "../../server-BMZ_BcPH.js";
5
+ import { W as resolveDataDir, _t as createForwardingLogger, d as storeFor, k as listItems, n as discoverCollections, q as safeSlugName, vt as createHostSlot } from "../../discovery-B_64PBZU.js";
6
+ import { F as buildCollectionActionSeedPrompt, I as promptPathsFor, z as readSkillTemplate } from "../../server-7POrhHKv.js";
7
7
  import { FEEDS_DIR, feedDir, feedStatePath, feedsRoot, ingestStateDir, ingestStatePath } from "../paths.js";
8
8
  import { n as DEFAULT_FEED_MAX_ITEMS, r as isFeedSchedule, t as AGENT_INGEST_KIND } from "../../ingestTypes-GkG7bJdB.js";
9
9
  import { d as publish, n as clear } from "../../notifier-vAp5t6wL.js";
@@ -1,3 +1,8 @@
1
+ /** A GET or DELETE on an event that is not there. Exported because the push's
2
+ * delete sweep has to read "already gone" as success, not as a failure to
3
+ * retry — a retained baseline would announce the same phantom deletion on
4
+ * every run. */
5
+ export declare const EVENT_ABSENT_STATUSES: readonly number[];
1
6
  /** An `If-Match` write whose etag no longer matches — someone else changed the
2
7
  * event since it was read. */
3
8
  export declare const HTTP_PRECONDITION_FAILED = 412;
@@ -74,6 +79,12 @@ export interface UpdateCalendarEventInput {
74
79
  export interface DeleteCalendarEventInput {
75
80
  eventId: string;
76
81
  calendarId?: string | undefined;
82
+ /** Etag of the version this decision was made against. Sent as `If-Match`, so
83
+ * Google answers 412 rather than removing an event that changed after it was
84
+ * read — the same guard `updateCalendarEvent` takes, and it matters more
85
+ * here: a delete cannot be undone from this side. Omit for an unconditional
86
+ * delete. */
87
+ ifMatch?: string | undefined;
77
88
  }
78
89
  export interface ListEventsInput {
79
90
  timeMin?: string | undefined;
@@ -117,6 +128,14 @@ export interface CalendarEventSummary {
117
128
  eventType: string;
118
129
  /** Google Meet URL, "" when nothing is attached. */
119
130
  hangoutLink: string;
131
+ /** The signed-in user's own `responseStatus`, folded out of `attendees`.
132
+ * `""` when Google reported none — which is every event that HAS no
133
+ * attendees, so it reads as "nothing said", never as "not going". */
134
+ selfResponseStatus: string;
135
+ /** The URL that joins this event's meeting, folded out of
136
+ * `conferenceData.entryPoints`. Covers the conferences `hangoutLink` does
137
+ * not: Zoom, Teams, anything attached as conference data. */
138
+ conferenceVideoUri: string;
120
139
  }
121
140
  export interface CalendarSummary {
122
141
  id: string;
@@ -166,6 +185,11 @@ export interface FetchedCalendarEvent {
166
185
  /** Google's `etag` for this version, `""` when absent. Sent back as
167
186
  * `If-Match` so a PATCH cannot overwrite a version we never read. */
168
187
  etag: string;
188
+ /** How many people the event lists, `0` for a solo one. Carried here rather
189
+ * than on the summary because only the delete guard reads it, and it is the
190
+ * one thing that guard cannot get from the record — by then the record is
191
+ * gone (`deletePlan.ts`). */
192
+ attendeeCount: number;
169
193
  }
170
194
  /** Read ONE event, or null when it is gone (404 / 410).
171
195
  *
@@ -1,6 +1,6 @@
1
1
  import { LoadedCollection } from '../collection/server/discoveredCollection.js';
2
2
  import { CollectionFieldSpec, CollectionItem } from '../collection/core/schema.js';
3
- import { CalendarEventSummary } from './calendar.js';
3
+ import { deleteCalendarEvent, getCalendarEvent, CalendarEventSummary } from './calendar.js';
4
4
  import { ShadowEvent } from './calendarPushState.js';
5
5
  import { PushableSourceField } from './pushPlan.js';
6
6
  /** What the up-front writability gate knows about the target calendar. */
@@ -31,8 +31,12 @@ export interface CalendarCollectionPushResult {
31
31
  updated: number;
32
32
  /** Edited on both sides; skipped so neither version is destroyed. */
33
33
  conflicts: number;
34
- /** Records deleted locally. Reported only — v1 never deletes in Google. */
34
+ /** Records deleted locally. Reported whether or not the deletion carried, so
35
+ * the count still answers "how many rows went away here". */
35
36
  localDeletes: number;
37
+ /** Of those, how many were deleted in Google too. Always `0` unless the
38
+ * collection opted in with `propagateDeletes` (#3234). */
39
+ deletedInGoogle: number;
36
40
  /** Records that cannot be pushed as they stand, each with the reason. */
37
41
  skipped: string[];
38
42
  errors: string[];
@@ -72,6 +76,15 @@ export interface CalendarPushDeps {
72
76
  isLinked: () => Promise<boolean>;
73
77
  accessToken: () => Promise<string>;
74
78
  calendarMeta: (accessToken: string, calendarId: string | undefined) => Promise<CalendarWriteTarget>;
79
+ /** Read and write for the delete guard. Injected like the rest so a test can
80
+ * exercise "this event has attendees" without a grant (#3234).
81
+ *
82
+ * OPTIONAL, unlike the rest: `CalendarPushDeps` is an exported type, and a
83
+ * deps object written before these existed must keep satisfying it — the
84
+ * more so because a collection that never opted into `propagateDeletes`
85
+ * cannot reach either of them. Absent means the live implementation. */
86
+ fetchEvent?: typeof getCalendarEvent;
87
+ deleteEvent?: typeof deleteCalendarEvent;
75
88
  }
76
89
  type PushOutcome = {
77
90
  kind: "created";
@@ -114,6 +127,36 @@ export declare function locallyEditedIds(records: readonly CollectionItem[], sha
114
127
  * from the stored baseline alone, so it works precisely when Google is the
115
128
  * thing that is unreachable. */
116
129
  export declare function unsentLocalEdits(collection: LoadedCollection, workspaceRoot: string): Promise<string[]>;
130
+ /** What one run's deletions did. `seen` counts every record that went away
131
+ * locally, whether or not the deletion carried — so the count means the same
132
+ * thing with the feature off as it always did. */
133
+ export interface DeleteSweep {
134
+ seen: number;
135
+ deleted: string[];
136
+ skipped: string[];
137
+ errors: string[];
138
+ }
139
+ /** What `sweepDeletes` needs, narrowed to the three effects it has. Taken as an
140
+ * argument rather than reached for, so the guard can be exercised against
141
+ * "this event has attendees" with no grant and no workspace. */
142
+ export interface DeleteSweepDeps {
143
+ /** What Google currently holds for the event, or null when it is gone. The
144
+ * `etag` is the version the decision was made against. */
145
+ fetchEvent: (eventId: string) => Promise<{
146
+ attendeeCount: number;
147
+ etag: string;
148
+ } | null>;
149
+ /** Delete it, conditional on that version. */
150
+ deleteEvent: (eventId: string, ifMatch: string) => Promise<void>;
151
+ /** Drop the event's baseline entry. */
152
+ forget: (eventId: string) => Promise<void>;
153
+ }
154
+ /** Carry every local deletion, when the collection opted in.
155
+ *
156
+ * Sequential rather than concurrent: each one writes the shared baseline file,
157
+ * and an interrupted run has to leave a state the next run resumes from rather
158
+ * than a partially applied batch. */
159
+ export declare function sweepDeletes(eventIds: readonly string[], propagate: boolean, deps: DeleteSweepDeps): Promise<DeleteSweep>;
117
160
  /** Push one collection WITHOUT taking the calendar lock.
118
161
  *
119
162
  * The caller must already hold it. The scheduled push→pull cycle
@@ -0,0 +1,32 @@
1
+ /** Why an event was left in Google although its record is gone. */
2
+ export type DeleteRefusal = {
3
+ kind: "has-attendees";
4
+ } | {
5
+ kind: "already-gone";
6
+ };
7
+ /** An approval carries the version it was made against, so the delete can be
8
+ * conditional on it. Keeping the two together is what stops a caller deleting
9
+ * against a version the guard never saw. */
10
+ export type DeleteDecision = {
11
+ ok: true;
12
+ etag: string;
13
+ } | ({
14
+ ok: false;
15
+ } & DeleteRefusal);
16
+ /** The message the push reports for a refusal, with the event named. */
17
+ export declare const deleteRefusalMessage: (eventId: string, refusal: DeleteRefusal) => string;
18
+ /** Decide one deletion against what Google currently holds.
19
+ *
20
+ * `null` means the event is already absent — a 404/410 on the read. Nothing to
21
+ * delete, and reported apart from a refusal so "we chose not to" never reads
22
+ * as "we could not".
23
+ *
24
+ * ANY attendee refuses, including the entry Google adds for the organiser. A
25
+ * rule that tried to exclude "only me" would have to work out which entry is
26
+ * the user from a payload that may not say, and being wrong there withdraws a
27
+ * real invitation. Solo events — the ones this feature exists for — carry no
28
+ * attendees at all, so the blunt rule costs them nothing. */
29
+ export declare function planDelete(fetched: {
30
+ attendeeCount: number;
31
+ etag: string;
32
+ } | null): DeleteDecision;
@@ -0,0 +1,19 @@
1
+ /** The signed-in user's own `responseStatus` on this event, `""` when Google
2
+ * reported none.
3
+ *
4
+ * `""` is the common case, not an edge one: an event with no attendees has no
5
+ * entry to mark `self`, which is most of a personal calendar. So it means
6
+ * "Google said nothing", never "not going" — filter with `!= "declined"`, not
7
+ * with `== "accepted"`, or every solo event disappears too.
8
+ *
9
+ * Google's values: `needsAction`, `declined`, `tentative`, `accepted`. */
10
+ export declare function selfResponseStatus(attendees: unknown): string;
11
+ /** The URL that joins this event's meeting, `""` when it has none.
12
+ *
13
+ * Only the `video` entry point. The others are a phone number and a dial-in
14
+ * PIN page, and a column named for joining that sometimes holds `tel:` would
15
+ * be worse than one that is empty — the caller can see empty and fall back.
16
+ *
17
+ * `hangoutLink` (#3229) already carries this for Google Meet; this is what
18
+ * reaches the events whose conference is Zoom or Teams. */
19
+ export declare function conferenceVideoUri(conferenceData: unknown): string;
@@ -0,0 +1,58 @@
1
+ import { CalendarEventTime } from './calendar.js';
2
+ /** Whether `value` is a bare calendar day — the all-day spelling of an event
3
+ * time, and the one shape `isIsoDateTimeWithOffset` is built to refuse.
4
+ *
5
+ * Realness comes from the parser the record lint, the calendar grid and the
6
+ * push already share, so every surface agrees that `2026-02-30` is not a day.
7
+ * The regex runs first because that parser trims, and a value with whitespace
8
+ * around it would validate here and then reach Google verbatim. */
9
+ export declare const isCalendarDateOnly: (value: string) => boolean;
10
+ /** One end of a span, or null when it is neither shape Calendar accepts. */
11
+ export declare const toEventTimeInput: (value: string) => CalendarEventTime | null;
12
+ /** The rule every surface states for a value that is neither shape. Shared so
13
+ * the tool, the remote host and their tests cannot drift apart. */
14
+ export declare const 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)";
15
+ /** The same rule with the offending key named, for a surface whose error text
16
+ * carries no path of its own. */
17
+ export declare const eventTimeHint: (key: string) => string;
18
+ /** All-day `end` is exclusive, so a one-day event ends on the NEXT day. Spelled
19
+ * out because the natural reading of "all day on the 17th" is `end` = the
20
+ * 17th, which Calendar refuses. */
21
+ export declare const ALL_DAY_END_HINT = "an all-day `end` is EXCLUSIVE \u2014 it is the day AFTER the last day, so a single day on 2026-07-17 needs end 2026-07-18";
22
+ export declare const MIXED_SPAN_HINT = "start and end must both be date-times with an offset, or both be dates for an all-day event \u2014 not one of each";
23
+ /** A lone all-day end cannot be patched: the stored event's kind is unknown
24
+ * here, and giving one end of a timed event a `date` makes an event Calendar
25
+ * rejects — with a message that names neither end. Both together are
26
+ * unambiguous, so that is what the caller is asked for. */
27
+ export declare const LONE_ALL_DAY_HINT = "pass BOTH start and end to move an all-day event \u2014 an all-day `end` is EXCLUSIVE \u2014 it is the day AFTER the last day, so a single day on 2026-07-17 needs end 2026-07-18";
28
+ export interface SpanTimes {
29
+ start: CalendarEventTime;
30
+ end: CalendarEventTime;
31
+ }
32
+ export type SpanResult = {
33
+ ok: true;
34
+ span: SpanTimes;
35
+ } | {
36
+ ok: false;
37
+ reason: string;
38
+ };
39
+ /** Both ends of a create, validated together.
40
+ *
41
+ * Only the all-day ordering is checked: a timed span's ordering depends on the
42
+ * two offsets, which Calendar resolves and reports on clearly, while an all-day
43
+ * `end <= start` comes back as an opaque 400. Lexicographic comparison is exact
44
+ * for `YYYY-MM-DD` — fixed-width, most significant part first. */
45
+ export declare function resolveSpanInput(start: string, end: string): SpanResult;
46
+ export interface PartialSpanTimes {
47
+ start?: CalendarEventTime;
48
+ end?: CalendarEventTime;
49
+ }
50
+ export type PartialSpanResult = {
51
+ ok: true;
52
+ times: PartialSpanTimes;
53
+ } | {
54
+ ok: false;
55
+ reason: string;
56
+ };
57
+ /** The span of an EDIT, where either end may be absent. */
58
+ export declare function resolvePartialSpanInput(start: string | undefined, end: string | undefined): PartialSpanResult;