@mulmoclaude/core 5.0.1 → 5.1.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.
- package/assets/helps/google-calendar-collection.md +38 -3
- package/assets/helps/google.md +14 -4
- package/dist/google/eventSpanInput.d.ts +58 -0
- package/dist/google/index.cjs +107 -0
- package/dist/google/index.cjs.map +1 -1
- package/dist/google/index.d.ts +1 -0
- package/dist/google/index.js +99 -1
- package/dist/google/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -106,6 +106,39 @@ Declaring it in `map` is a schema error.
|
|
|
106
106
|
Use `datetime` (not `date`) for start/end when events have real clock times —
|
|
107
107
|
the calendar day view then draws each record as a proportional time block.
|
|
108
108
|
|
|
109
|
+
## All-day events
|
|
110
|
+
|
|
111
|
+
A `datetime` column stores an all-day event as `2026-07-17T00:00`, because that
|
|
112
|
+
is where the day view places it. That is fine for mirroring, but it is also
|
|
113
|
+
exactly how a real midnight appointment is stored — so a record CREATED locally
|
|
114
|
+
in a `datetime` column is pushed as a midnight event, never as an all-day one.
|
|
115
|
+
|
|
116
|
+
For a calendar whose events are all-day, give start/end a **`date`** column
|
|
117
|
+
instead. Google's bare date is then kept verbatim, and a record the user creates
|
|
118
|
+
by typing two dates is pushed as a real all-day event.
|
|
119
|
+
|
|
120
|
+
```jsonc
|
|
121
|
+
"fields": {
|
|
122
|
+
"on": { "type": "date", "label": "From" },
|
|
123
|
+
"until": { "type": "date", "label": "To" }
|
|
124
|
+
},
|
|
125
|
+
"googleCalendar": { "map": { "on": "start", "until": "end" } }
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Google's all-day `end` is **exclusive** — it is the day AFTER the last day, so a
|
|
129
|
+
single day on the 17th is `on: 2026-07-17`, `until: 2026-07-18`. Records
|
|
130
|
+
mirrored from Google already carry it that way. Say so when the user asks why
|
|
131
|
+
the end date "looks a day late"; do not offset it, because the push sends the
|
|
132
|
+
stored value straight back.
|
|
133
|
+
|
|
134
|
+
An existing all-day event stays all-day when its dates are edited, whatever the
|
|
135
|
+
column type: the push reads what Google last reported for that event and keeps
|
|
136
|
+
its kind. Only a record with no such history — one created locally — depends on
|
|
137
|
+
the column type.
|
|
138
|
+
|
|
139
|
+
To create a one-off all-day event without a collection, use the `google` tool's
|
|
140
|
+
`calendarCreateEvent` with a bare date on both ends.
|
|
141
|
+
|
|
109
142
|
## When sync runs
|
|
110
143
|
|
|
111
144
|
- **On creation** — the first sync starts as soon as the schema lands, so the
|
|
@@ -174,9 +207,11 @@ What the button does and deliberately does not do:
|
|
|
174
207
|
Reasons a record can be reported as skipped:
|
|
175
208
|
|
|
176
209
|
- **Its record id cannot be a Google event id.** Google requires 5-1024
|
|
177
|
-
characters from `0-9a-v
|
|
178
|
-
|
|
179
|
-
|
|
210
|
+
characters from `0-9a-v` (lower-case base32hex: digits plus `a`-`v`, so no
|
|
211
|
+
`w`-`z`, no upper case, no hyphen). Records created through the UI get a
|
|
212
|
+
valid generated id; a semantic id you authored (`team-standup`) cannot be
|
|
213
|
+
used. Fix by recreating the record without setting the primary field, or by
|
|
214
|
+
choosing an id that satisfies the rule.
|
|
180
215
|
- **No `start` / `end` is mapped, on a record being CREATED.** An event cannot
|
|
181
216
|
be created without a span. Editing an existing event is unaffected — a changed
|
|
182
217
|
title or colour is patched on its own.
|
package/assets/helps/google.md
CHANGED
|
@@ -32,13 +32,23 @@ from `calendarListCalendars` — to target another.
|
|
|
32
32
|
| `calendarColors` | Palettes mapping a `colorId` to hex, for events and for calendars |
|
|
33
33
|
| `calendarListEvents` | Upcoming events. Optional `calendarId`, `timeMin`, `maxResults` (1-50, default 10) |
|
|
34
34
|
| `calendarSync` | Only what CHANGED since the last sync, via a stored token. Returns counts plus a capped sample |
|
|
35
|
-
| `calendarCreateEvent` | Create. Requires `summary`, `start`, `end`; optional `description`, `calendarId`, `colorId` |
|
|
35
|
+
| `calendarCreateEvent` | Create, timed or all-day. Requires `summary`, `start`, `end`; optional `description`, `calendarId`, `colorId` |
|
|
36
36
|
| `calendarUpdateEvent` | Edit in place. Requires `eventId` + at least one of `summary`, `start`, `end`, `description`, `colorId` |
|
|
37
37
|
| `calendarDeleteEvent` | Delete. Requires `eventId` |
|
|
38
38
|
|
|
39
|
-
**
|
|
40
|
-
`2026-07-
|
|
41
|
-
opaque 400.
|
|
39
|
+
**A timed event's ends must carry a timezone offset** —
|
|
40
|
+
`2026-07-17T09:00:00+09:00`, not `2026-07-17T09:00:00`. Calendar rejects an
|
|
41
|
+
offset-less value with an opaque 400. The same goes for `timeMin`.
|
|
42
|
+
|
|
43
|
+
**An all-day event takes a bare date on BOTH ends** — `start: "2026-07-17"`,
|
|
44
|
+
`end: "2026-07-18"`. Its `end` is **exclusive**: it is the day AFTER the last
|
|
45
|
+
day, so a single day on the 17th ends on the 18th, and a three-day event
|
|
46
|
+
starting the 17th ends on the 20th. This is Google's own convention and the
|
|
47
|
+
values it reports back, so do not "correct" it.
|
|
48
|
+
|
|
49
|
+
One end of each kind is rejected. Editing an all-day event needs **both** ends —
|
|
50
|
+
a lone date is refused, because whether the stored event is all-day cannot be
|
|
51
|
+
told from the argument.
|
|
42
52
|
|
|
43
53
|
**Editing is a patch.** Fields you omit keep their current value, so changing a
|
|
44
54
|
title needs `eventId` + `summary` and nothing else. `description: ""` clears the
|
|
@@ -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;
|
package/dist/google/index.cjs
CHANGED
|
@@ -45,6 +45,104 @@ var isIsoDateTimeWithOffset = (value) => {
|
|
|
45
45
|
return isRealCalendarDate(year, month, day) && timeInRange && offsetInRange;
|
|
46
46
|
};
|
|
47
47
|
//#endregion
|
|
48
|
+
//#region src/google/eventSpanInput.ts
|
|
49
|
+
var DATE_ONLY_RE$1 = /^\d{4}-\d{2}-\d{2}$/;
|
|
50
|
+
/** Whether `value` is a bare calendar day — the all-day spelling of an event
|
|
51
|
+
* time, and the one shape `isIsoDateTimeWithOffset` is built to refuse.
|
|
52
|
+
*
|
|
53
|
+
* Realness comes from the parser the record lint, the calendar grid and the
|
|
54
|
+
* push already share, so every surface agrees that `2026-02-30` is not a day.
|
|
55
|
+
* The regex runs first because that parser trims, and a value with whitespace
|
|
56
|
+
* around it would validate here and then reach Google verbatim. */
|
|
57
|
+
var isCalendarDateOnly = (value) => DATE_ONLY_RE$1.test(value) && require_itemId.parseIsoDate(value) !== null;
|
|
58
|
+
/** One end of a span, or null when it is neither shape Calendar accepts. */
|
|
59
|
+
var toEventTimeInput = (value) => {
|
|
60
|
+
if (isCalendarDateOnly(value)) return { date: value };
|
|
61
|
+
return isIsoDateTimeWithOffset(value) ? { dateTime: value } : null;
|
|
62
|
+
};
|
|
63
|
+
/** The rule every surface states for a value that is neither shape. Shared so
|
|
64
|
+
* the tool, the remote host and their tests cannot drift apart. */
|
|
65
|
+
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)";
|
|
66
|
+
/** The same rule with the offending key named, for a surface whose error text
|
|
67
|
+
* carries no path of its own. */
|
|
68
|
+
var eventTimeHint = (key) => `${key} ${EVENT_TIME_HINT}`;
|
|
69
|
+
/** All-day `end` is exclusive, so a one-day event ends on the NEXT day. Spelled
|
|
70
|
+
* out because the natural reading of "all day on the 17th" is `end` = the
|
|
71
|
+
* 17th, which Calendar refuses. */
|
|
72
|
+
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";
|
|
73
|
+
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";
|
|
74
|
+
/** A lone all-day end cannot be patched: the stored event's kind is unknown
|
|
75
|
+
* here, and giving one end of a timed event a `date` makes an event Calendar
|
|
76
|
+
* rejects — with a message that names neither end. Both together are
|
|
77
|
+
* unambiguous, so that is what the caller is asked for. */
|
|
78
|
+
var LONE_ALL_DAY_HINT = `pass BOTH start and end to move an all-day event — ${ALL_DAY_END_HINT}`;
|
|
79
|
+
var isAllDay = (time) => "date" in time;
|
|
80
|
+
/** Both ends of a create, validated together.
|
|
81
|
+
*
|
|
82
|
+
* Only the all-day ordering is checked: a timed span's ordering depends on the
|
|
83
|
+
* two offsets, which Calendar resolves and reports on clearly, while an all-day
|
|
84
|
+
* `end <= start` comes back as an opaque 400. Lexicographic comparison is exact
|
|
85
|
+
* for `YYYY-MM-DD` — fixed-width, most significant part first. */
|
|
86
|
+
function resolveSpanInput(start, end) {
|
|
87
|
+
const startTime = toEventTimeInput(start);
|
|
88
|
+
if (startTime === null) return {
|
|
89
|
+
ok: false,
|
|
90
|
+
reason: eventTimeHint("start")
|
|
91
|
+
};
|
|
92
|
+
const endTime = toEventTimeInput(end);
|
|
93
|
+
if (endTime === null) return {
|
|
94
|
+
ok: false,
|
|
95
|
+
reason: eventTimeHint("end")
|
|
96
|
+
};
|
|
97
|
+
if (isAllDay(startTime) !== isAllDay(endTime)) return {
|
|
98
|
+
ok: false,
|
|
99
|
+
reason: MIXED_SPAN_HINT
|
|
100
|
+
};
|
|
101
|
+
if (isAllDay(startTime) && isAllDay(endTime) && endTime.date <= startTime.date) return {
|
|
102
|
+
ok: false,
|
|
103
|
+
reason: ALL_DAY_END_HINT
|
|
104
|
+
};
|
|
105
|
+
return {
|
|
106
|
+
ok: true,
|
|
107
|
+
span: {
|
|
108
|
+
start: startTime,
|
|
109
|
+
end: endTime
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
/** One end of an EDIT, where the other end is absent. */
|
|
114
|
+
function resolveLoneEnd(key, value) {
|
|
115
|
+
const time = toEventTimeInput(value);
|
|
116
|
+
if (time === null) return {
|
|
117
|
+
ok: false,
|
|
118
|
+
reason: eventTimeHint(key)
|
|
119
|
+
};
|
|
120
|
+
if (isAllDay(time)) return {
|
|
121
|
+
ok: false,
|
|
122
|
+
reason: LONE_ALL_DAY_HINT
|
|
123
|
+
};
|
|
124
|
+
return {
|
|
125
|
+
ok: true,
|
|
126
|
+
times: { [key]: time }
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
/** The span of an EDIT, where either end may be absent. */
|
|
130
|
+
function resolvePartialSpanInput(start, end) {
|
|
131
|
+
if (start !== void 0 && end !== void 0) {
|
|
132
|
+
const resolved = resolveSpanInput(start, end);
|
|
133
|
+
return resolved.ok ? {
|
|
134
|
+
ok: true,
|
|
135
|
+
times: resolved.span
|
|
136
|
+
} : resolved;
|
|
137
|
+
}
|
|
138
|
+
if (start !== void 0) return resolveLoneEnd("start", start);
|
|
139
|
+
if (end !== void 0) return resolveLoneEnd("end", end);
|
|
140
|
+
return {
|
|
141
|
+
ok: true,
|
|
142
|
+
times: {}
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
//#endregion
|
|
48
146
|
//#region src/google/paths.ts
|
|
49
147
|
/** Same directory as every other cross-app per-user file — see
|
|
50
148
|
* `../global-config/paths.ts`, which owns the spelling. */
|
|
@@ -2891,8 +2989,10 @@ async function deleteDriveFile(accessToken, input) {
|
|
|
2891
2989
|
await googleRequest(DRIVE_API_LABEL, accessToken, `${DRIVE_FILES_URL}/${encodeURIComponent(input.fileId)}`, { method: "DELETE" });
|
|
2892
2990
|
}
|
|
2893
2991
|
//#endregion
|
|
2992
|
+
exports.ALL_DAY_END_HINT = ALL_DAY_END_HINT;
|
|
2894
2993
|
exports.CANCELLED_EVENT_STATUS = CANCELLED_EVENT_STATUS;
|
|
2895
2994
|
exports.DEFAULT_LIST_MAX_RESULTS = DEFAULT_LIST_MAX_RESULTS;
|
|
2995
|
+
exports.EVENT_TIME_HINT = EVENT_TIME_HINT;
|
|
2896
2996
|
exports.GOOGLE_AUTH_CANCELLED = GOOGLE_AUTH_CANCELLED;
|
|
2897
2997
|
exports.GOOGLE_CALENDARLIST_SCOPE = GOOGLE_CALENDARLIST_SCOPE;
|
|
2898
2998
|
exports.GOOGLE_CALENDAR_SCOPE = GOOGLE_CALENDAR_SCOPE;
|
|
@@ -2904,7 +3004,9 @@ exports.GoogleApiError = GoogleApiError;
|
|
|
2904
3004
|
exports.HTTP_CONFLICT = HTTP_CONFLICT;
|
|
2905
3005
|
exports.HTTP_FORBIDDEN = HTTP_FORBIDDEN;
|
|
2906
3006
|
exports.HTTP_PRECONDITION_FAILED = HTTP_PRECONDITION_FAILED;
|
|
3007
|
+
exports.LONE_ALL_DAY_HINT = LONE_ALL_DAY_HINT;
|
|
2907
3008
|
exports.MAX_LIST_RESULTS = MAX_LIST_RESULTS;
|
|
3009
|
+
exports.MIXED_SPAN_HINT = MIXED_SPAN_HINT;
|
|
2908
3010
|
exports.PARTIAL_CALENDAR_WINDOW = PARTIAL_CALENDAR_WINDOW;
|
|
2909
3011
|
exports.PROTECTION_UNKNOWN = PROTECTION_UNKNOWN;
|
|
2910
3012
|
exports.PUSHABLE_SOURCE_FIELDS = PUSHABLE_SOURCE_FIELDS;
|
|
@@ -2950,6 +3052,7 @@ exports.deleteCalendarEvent = deleteCalendarEvent;
|
|
|
2950
3052
|
exports.deleteDriveFile = deleteDriveFile;
|
|
2951
3053
|
exports.deleteGoogleTokens = deleteGoogleTokens;
|
|
2952
3054
|
exports.deleteTask = deleteTask;
|
|
3055
|
+
exports.eventTimeHint = eventTimeHint;
|
|
2953
3056
|
exports.fieldText = fieldText;
|
|
2954
3057
|
exports.findClientSecretPath = findClientSecretPath;
|
|
2955
3058
|
exports.getCalendarColors = getCalendarColors;
|
|
@@ -2965,6 +3068,7 @@ exports.googleTokenPath = googleTokenPath;
|
|
|
2965
3068
|
exports.groupByCalendar = groupByCalendar;
|
|
2966
3069
|
exports.groupsNeedingBackfill = groupsNeedingBackfill;
|
|
2967
3070
|
exports.heldBack = heldBack;
|
|
3071
|
+
exports.isCalendarDateOnly = isCalendarDateOnly;
|
|
2968
3072
|
exports.isCalendarSyncDue = isCalendarSyncDue;
|
|
2969
3073
|
exports.isClientSettableEventId = isClientSettableEventId;
|
|
2970
3074
|
exports.isDeniedAccessRole = isDeniedAccessRole;
|
|
@@ -3006,6 +3110,8 @@ exports.readDriveFile = readDriveFile;
|
|
|
3006
3110
|
exports.releaseOrphanedCalendarToken = releaseOrphanedCalendarToken;
|
|
3007
3111
|
exports.reportedAccessRole = reportedAccessRole;
|
|
3008
3112
|
exports.resolveEventSpan = resolveEventSpan;
|
|
3113
|
+
exports.resolvePartialSpanInput = resolvePartialSpanInput;
|
|
3114
|
+
exports.resolveSpanInput = resolveSpanInput;
|
|
3009
3115
|
exports.resumableToken = resumableToken;
|
|
3010
3116
|
exports.saveCalendarShadow = saveCalendarShadow;
|
|
3011
3117
|
exports.saveCalendarSyncToken = saveCalendarSyncToken;
|
|
@@ -3024,6 +3130,7 @@ exports.toCollectionDateTime = toCollectionDateTime;
|
|
|
3024
3130
|
exports.toCollectionRecord = toCollectionRecord;
|
|
3025
3131
|
exports.toDriveFileSummary = toDriveFileSummary;
|
|
3026
3132
|
exports.toEventSummary = toEventSummary;
|
|
3133
|
+
exports.toEventTimeInput = toEventTimeInput;
|
|
3027
3134
|
exports.toGoogleEventTime = toGoogleEventTime;
|
|
3028
3135
|
exports.toShadowEvent = toShadowEvent;
|
|
3029
3136
|
exports.toTaskListSummary = toTaskListSummary;
|