@wirecat/cli-core 0.545.0 → 0.546.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 (88) hide show
  1. package/CHANGELOG.md +76 -0
  2. package/dist/calendar/busy.js +4 -1
  3. package/dist/calendar/cli/answers.d.ts +31 -0
  4. package/dist/calendar/cli/answers.js +8 -0
  5. package/dist/calendar/cli/index.d.ts +6 -0
  6. package/dist/calendar/cli/index.js +43 -4
  7. package/dist/calendar/cli/remind.js +2 -1
  8. package/dist/calendar/expand.d.ts +17 -0
  9. package/dist/calendar/expand.js +340 -0
  10. package/dist/calendar/feeds.d.ts +96 -0
  11. package/dist/calendar/feeds.js +324 -0
  12. package/dist/calendar/ics.d.ts +15 -4
  13. package/dist/calendar/ics.js +18 -7
  14. package/dist/calendar/index.d.ts +3 -1
  15. package/dist/calendar/index.js +2 -0
  16. package/dist/calendar/server-search.js +3 -1
  17. package/dist/calendar/writes.js +15 -4
  18. package/dist/cli/after-dashes.d.ts +16 -0
  19. package/dist/cli/after-dashes.js +41 -0
  20. package/dist/cli/bot/context.d.ts +1 -1
  21. package/dist/cli/bot/context.js +9 -1
  22. package/dist/cli/bot/messages.js +2 -0
  23. package/dist/cli/bot/setup.js +1 -0
  24. package/dist/cli/contract-templates.js +2 -2
  25. package/dist/cli/entry.d.ts +2 -0
  26. package/dist/cli/entry.js +3 -2
  27. package/dist/cli/index.d.ts +2 -1
  28. package/dist/cli/index.js +2 -1
  29. package/dist/cli/messenger/admin-commands.js +5 -1
  30. package/dist/cli/messenger/admin-contacts-command.js +5 -1
  31. package/dist/cli/messenger/admin-folders-command.js +5 -1
  32. package/dist/cli/messenger/complete-command.js +1 -0
  33. package/dist/cli/messenger/messages-command.js +2 -19
  34. package/dist/cli/messenger/messages-edit-command.js +1 -0
  35. package/dist/cli/messenger/messages-press-command.js +1 -0
  36. package/dist/cli/messenger/messages-send-command.js +1 -0
  37. package/dist/cli/messenger/observed.js +2 -2
  38. package/dist/cli/messenger/polls-command.js +1 -0
  39. package/dist/cli/messenger/private-people-command.js +5 -1
  40. package/dist/cli/messenger/stored.js +56 -3
  41. package/dist/cli/messenger/topics-command.js +1 -1
  42. package/dist/cli/program.js +17 -6
  43. package/dist/cli/schedule/schedule-command.js +1 -0
  44. package/dist/domain/message-link.d.ts +3 -0
  45. package/dist/domain/message-link.js +9 -14
  46. package/dist/domain/money-words.d.ts +11 -0
  47. package/dist/domain/money-words.js +38 -0
  48. package/dist/domain/providers.d.ts +2 -0
  49. package/dist/domain/providers.js +15 -3
  50. package/dist/domain/ref-arguments.d.ts +31 -0
  51. package/dist/domain/ref-arguments.js +79 -0
  52. package/dist/domain/references.d.ts +20 -2
  53. package/dist/domain/references.js +132 -47
  54. package/dist/errors/reasons.js +18 -0
  55. package/dist/index.d.ts +1 -1
  56. package/dist/index.js +1 -1
  57. package/dist/meetings/mcp/index.d.ts +15 -15
  58. package/dist/oauth/client-file.d.ts +14 -0
  59. package/dist/oauth/client-file.js +28 -0
  60. package/dist/oauth/index.d.ts +1 -0
  61. package/dist/oauth/index.js +1 -0
  62. package/dist/ports/messenger.d.ts +3 -2
  63. package/dist/resolve.d.ts +3 -1
  64. package/dist/resolve.js +8 -0
  65. package/dist/search/task-signature.d.ts +5 -1
  66. package/dist/search/task-signature.js +27 -9
  67. package/dist/services/agenda.d.ts +5 -0
  68. package/dist/services/agenda.js +3 -1
  69. package/dist/services/index.d.ts +5 -1
  70. package/dist/services/index.js +5 -1
  71. package/dist/services/message-resolution.d.ts +4 -1
  72. package/dist/services/message-resolution.js +19 -3
  73. package/dist/services/messages.js +15 -13
  74. package/dist/services/money-words.d.ts +3 -0
  75. package/dist/services/money-words.js +5 -0
  76. package/dist/services/questions.d.ts +7 -2
  77. package/dist/services/questions.js +13 -4
  78. package/dist/services/stored-project-gathering.d.ts +9 -0
  79. package/dist/services/stored-project-gathering.js +9 -4
  80. package/dist/services/task-categories.d.ts +2 -0
  81. package/dist/services/task-categories.js +19 -8
  82. package/dist/store/sqlite/evidence-sources.js +2 -2
  83. package/dist/store/sqlite/identities.js +1 -1
  84. package/dist/store/sqlite/knowledge.js +7 -2
  85. package/dist/store/sqlite/notes.js +4 -4
  86. package/dist/store/sqlite/things.d.ts +0 -1
  87. package/dist/store/sqlite/things.js +25 -24
  88. package/package.json +1 -1
@@ -0,0 +1,340 @@
1
+ const DAY = 86_400_000;
2
+ const WEEKDAYS = ["SU", "MO", "TU", "WE", "TH", "FR", "SA"];
3
+ const FREQUENCIES = new Set(["DAILY", "WEEKLY", "MONTHLY", "YEARLY"]);
4
+ const PARTS = new Set(["FREQ", "INTERVAL", "COUNT", "UNTIL", "BYDAY", "BYMONTHDAY", "BYMONTH", "BYSETPOS", "WKST"]);
5
+ const BY_DAY = /^([+-]?\d{1,2})?(MO|TU|WE|TH|FR|SA|SU)$/u;
6
+ const STAMP = /^(\d{4})(\d{2})(\d{2})(?:T(\d{2})(\d{2})(\d{2})(Z?))?$/u;
7
+ /** A runaway rule (FREQ=DAILY from 1900) still ends: the window's end stops it long before this. */
8
+ const MOST_PERIODS = 200_000;
9
+ const numbers = (text, low, high) => {
10
+ const values = text.split(",").map(Number);
11
+ return values.every((one) => Number.isInteger(one) && one !== 0 && Math.abs(one) >= low && Math.abs(one) <= high)
12
+ ? values
13
+ : undefined;
14
+ };
15
+ /** RFC 5545 §3.3.10 as far as calendar feeds use it; anything else is a reason, never a guess. */
16
+ const ruleOf = (line) => {
17
+ const parts = new Map();
18
+ for (const part of line.replace(/^RRULE:/iu, "").split(";")) {
19
+ const [key = "", value = ""] = part.split("=");
20
+ parts.set(key.trim().toUpperCase(), value.trim().toUpperCase());
21
+ }
22
+ for (const key of parts.keys())
23
+ if (!PARTS.has(key))
24
+ return `RRULE part ${key} is not expanded`;
25
+ const freq = parts.get("FREQ") ?? "";
26
+ if (!FREQUENCIES.has(freq))
27
+ return `RRULE FREQ=${freq} is not expanded`;
28
+ const rule = { freq, interval: Number(parts.get("INTERVAL") ?? 1), wkst: 1 };
29
+ if (!Number.isInteger(rule.interval) || rule.interval < 1)
30
+ return "RRULE INTERVAL is not a whole number from 1";
31
+ if (parts.has("COUNT")) {
32
+ rule.count = Number(parts.get("COUNT"));
33
+ if (!Number.isInteger(rule.count) || rule.count < 1)
34
+ return "RRULE COUNT is not a whole number from 1";
35
+ }
36
+ if (parts.has("UNTIL"))
37
+ rule.until = parts.get("UNTIL");
38
+ const days = parts.get("BYDAY");
39
+ if (days !== undefined) {
40
+ rule.byDay = [];
41
+ for (const day of days.split(",")) {
42
+ const found = BY_DAY.exec(day);
43
+ if (!found)
44
+ return `RRULE BYDAY ${day} is not a day`;
45
+ rule.byDay.push({ nth: found[1] ? Number(found[1]) : null, day: WEEKDAYS.indexOf(found[2]) });
46
+ }
47
+ }
48
+ for (const [key, field, high] of [
49
+ ["BYMONTHDAY", "byMonthDay", 31],
50
+ ["BYMONTH", "byMonth", 12],
51
+ ["BYSETPOS", "bySetPos", 366],
52
+ ]) {
53
+ const text = parts.get(key);
54
+ if (text === undefined)
55
+ continue;
56
+ const values = numbers(text, 1, high);
57
+ if (!values || (key === "BYMONTH" && values.some((one) => one < 0)))
58
+ return `RRULE ${key}=${text} is not expanded`;
59
+ rule[field] = values;
60
+ }
61
+ const wkst = parts.get("WKST");
62
+ if (wkst !== undefined) {
63
+ if (!WEEKDAYS.includes(wkst))
64
+ return `RRULE WKST=${wkst} is not a day`;
65
+ rule.wkst = WEEKDAYS.indexOf(wkst);
66
+ }
67
+ return rule;
68
+ };
69
+ const formatters = new Map();
70
+ const formatterOf = (zone) => {
71
+ let formatter = formatters.get(zone);
72
+ if (!formatter) {
73
+ formatter = new Intl.DateTimeFormat("en-CA", {
74
+ timeZone: zone,
75
+ year: "numeric",
76
+ month: "2-digit",
77
+ day: "2-digit",
78
+ hour: "2-digit",
79
+ minute: "2-digit",
80
+ second: "2-digit",
81
+ hourCycle: "h23",
82
+ });
83
+ formatters.set(zone, formatter);
84
+ }
85
+ return formatter;
86
+ };
87
+ /** The wall clock in `zone` at `at`, as `YYYY-MM-DD` and `HH:MM:SS`. */
88
+ const wallClock = (at, zone) => {
89
+ const parts = Object.fromEntries(formatterOf(zone)
90
+ .formatToParts(at)
91
+ .map((part) => [part.type, part.value]));
92
+ return { date: `${parts.year}-${parts.month}-${parts.day}`, time: `${parts.hour}:${parts.minute}:${parts.second}` };
93
+ };
94
+ const offsetAt = (at, zone) => {
95
+ const { date, time } = wallClock(at, zone);
96
+ return Date.parse(`${date}T${time}Z`) - Math.floor(at / 1000) * 1000;
97
+ };
98
+ /**
99
+ * A wall-clock time in `zone` as epoch ms. A time a clock change skips is read with the offset before the change, so it
100
+ * lands after it, and a repeated one takes its first moment (RFC 5545 §3.3.5): a slot is moved, never dropped.
101
+ */
102
+ export const localMoment = (date, time, zone) => {
103
+ const guess = Date.parse(`${date}T${time}Z`);
104
+ const before = guess - offsetAt(guess - DAY / 2, zone);
105
+ const after = guess - offsetAt(guess + DAY / 2, zone);
106
+ const exact = [before, after].filter((at) => {
107
+ const shown = wallClock(at, zone);
108
+ return shown.date === date && shown.time === time;
109
+ });
110
+ return exact.length === 2 ? Math.min(before, after) : (exact[0] ?? before);
111
+ };
112
+ const plusDays = (date, days) => {
113
+ const [year, month, day] = date.split("-").map(Number);
114
+ return new Date(Date.UTC(year, month - 1, day + days)).toISOString().slice(0, 10);
115
+ };
116
+ const dayOf = (date) => new Date(`${date}T00:00:00Z`).getUTCDay();
117
+ const pad = (value) => String(value).padStart(2, "0");
118
+ const dateOf = (year, month, day) => `${year}-${pad(month)}-${pad(day)}`;
119
+ const daysIn = (year, month) => new Date(Date.UTC(year, month, 0)).getUTCDate();
120
+ const daysBetween = (from, to) => Math.round((Date.parse(to) - Date.parse(from)) / DAY);
121
+ /** The days of a set that BYDAY names: every such weekday, or with a number the nth of them, from the end if negative. */
122
+ const pickDays = (all, byDay) => byDay.flatMap(({ nth, day }) => {
123
+ const matching = all.filter((date) => dayOf(date) === day);
124
+ if (nth === null)
125
+ return matching;
126
+ const picked = nth > 0 ? matching[nth - 1] : matching[matching.length + nth];
127
+ return picked ? [picked] : [];
128
+ });
129
+ const monthDays = (year, month) => Array.from({ length: daysIn(year, month) }, (_, n) => dateOf(year, month, n + 1));
130
+ const byMonthDay = (year, month, days) => days.flatMap((day) => {
131
+ const size = daysIn(year, month);
132
+ const at = day > 0 ? day : size + day + 1;
133
+ return at >= 1 && at <= size ? [dateOf(year, month, at)] : [];
134
+ });
135
+ /** One month's days under BYMONTHDAY and BYDAY, or the start's day of the month without either. */
136
+ const inMonth = (year, month, rule, startDay) => {
137
+ const byDays = rule.byDay ? pickDays(monthDays(year, month), rule.byDay) : undefined;
138
+ const byDates = rule.byMonthDay ? byMonthDay(year, month, rule.byMonthDay) : undefined;
139
+ if (byDays && byDates)
140
+ return byDates.filter((date) => byDays.includes(date));
141
+ if (byDays)
142
+ return byDays;
143
+ if (byDates)
144
+ return byDates;
145
+ return startDay <= daysIn(year, month) ? [dateOf(year, month, startDay)] : [];
146
+ };
147
+ const setPos = (dates, positions) => {
148
+ if (!positions)
149
+ return dates;
150
+ return positions.flatMap((at) => {
151
+ const picked = at > 0 ? dates[at - 1] : dates[dates.length + at];
152
+ return picked ? [picked] : [];
153
+ });
154
+ };
155
+ const sorted = (dates) => [...new Set(dates)].sort();
156
+ /** The candidate dates of the nth period of the rule, in order. */
157
+ const periodDates = (rule, start, n) => {
158
+ const [year, month, day] = start.split("-").map(Number);
159
+ const step = n * rule.interval;
160
+ const inMonths = (dates) => rule.byMonth ? dates.filter((date) => rule.byMonth?.includes(Number(date.slice(5, 7)))) : dates;
161
+ if (rule.freq === "DAILY") {
162
+ const date = plusDays(start, step);
163
+ const [y, m] = [Number(date.slice(0, 4)), Number(date.slice(5, 7))];
164
+ if (rule.byMonthDay && !byMonthDay(y, m, rule.byMonthDay).includes(date))
165
+ return [];
166
+ if (rule.byDay && !rule.byDay.some(({ day: weekday }) => weekday === dayOf(date)))
167
+ return [];
168
+ return inMonths([date]);
169
+ }
170
+ if (rule.freq === "WEEKLY") {
171
+ const weekStart = plusDays(plusDays(start, -((dayOf(start) - rule.wkst + 7) % 7)), 7 * step);
172
+ const days = rule.byDay ? rule.byDay.map(({ day: weekday }) => weekday) : [dayOf(start)];
173
+ const dates = days.map((weekday) => plusDays(weekStart, (weekday - rule.wkst + 7) % 7));
174
+ return setPos(sorted(inMonths(dates)), rule.bySetPos);
175
+ }
176
+ if (rule.freq === "MONTHLY") {
177
+ const total = month - 1 + step;
178
+ const [y, m] = [year + Math.floor(total / 12), (total % 12) + 1];
179
+ if (rule.byMonth && !rule.byMonth.includes(m))
180
+ return [];
181
+ return setPos(sorted(inMonth(y, m, rule, day)), rule.bySetPos);
182
+ }
183
+ const y = year + step;
184
+ if (!rule.byMonth && rule.byDay && !rule.byMonthDay) {
185
+ const all = Array.from({ length: 12 }, (_, at) => monthDays(y, at + 1)).flat();
186
+ return setPos(sorted(pickDays(all, rule.byDay)), rule.bySetPos);
187
+ }
188
+ const months = rule.byMonth ?? (rule.byMonthDay ? Array.from({ length: 12 }, (_, at) => at + 1) : [month]);
189
+ return setPos(sorted(months.flatMap((m) => inMonth(y, m, rule, day))), rule.bySetPos);
190
+ };
191
+ /** The first day of the nth period, so a rule that matches no day still stops at the window's end. */
192
+ const periodStart = (rule, start, n) => {
193
+ const step = n * rule.interval;
194
+ if (rule.freq === "DAILY")
195
+ return plusDays(start, step);
196
+ if (rule.freq === "WEEKLY")
197
+ return plusDays(start, 7 * step - ((dayOf(start) - rule.wkst + 7) % 7));
198
+ const [year, month] = start.split("-").map(Number);
199
+ if (rule.freq === "YEARLY")
200
+ return dateOf(year + step, 1, 1);
201
+ const total = month - 1 + step;
202
+ return dateOf(year + Math.floor(total / 12), (total % 12) + 1, 1);
203
+ };
204
+ /** A DATE or DATE-TIME of UNTIL, EXDATE or RDATE: `date` always, `at` for a time; floating times are read in `zone`. */
205
+ const stampOf = (text, zone, tzid) => {
206
+ const found = STAMP.exec(text.trim());
207
+ if (!found)
208
+ return undefined;
209
+ const [, year, month, day, hour, minute, second, utc] = found;
210
+ const date = `${year}-${month}-${day}`;
211
+ if (hour === undefined)
212
+ return { date };
213
+ const time = `${hour}:${minute}:${second}`;
214
+ return { date, at: utc ? Date.parse(`${date}T${time}Z`) : localMoment(date, time, tzid ?? zone) };
215
+ };
216
+ /** The values of every `EXDATE` or `RDATE` line, with its TZID; a PERIOD's start stands for it. */
217
+ const listed = (lines, name, zone) => lines
218
+ .filter((line) => line.toUpperCase().startsWith(name))
219
+ .flatMap((line) => {
220
+ const colon = line.indexOf(":");
221
+ const tzid = /;TZID=([^;:]+)/iu.exec(line.slice(0, colon))?.[1]?.replace(/^"|"$/gu, "");
222
+ return line
223
+ .slice(colon + 1)
224
+ .split(",")
225
+ .map((value) => stampOf(value.split("/")[0] ?? "", zone, tzid))
226
+ .filter((one) => one !== undefined);
227
+ });
228
+ /** Why a series cannot be expanded here, or `null` when it can. */
229
+ export const unexpandable = (recurrence) => {
230
+ const rules = (recurrence ?? []).filter((line) => /^RRULE[:;]/iu.test(line));
231
+ if ((recurrence ?? []).some((line) => /^EXRULE[:;]/iu.test(line)))
232
+ return "EXRULE is not expanded";
233
+ if (rules.length > 1)
234
+ return "more than one RRULE is not expanded";
235
+ const [line] = rules;
236
+ if (line === undefined)
237
+ return null;
238
+ const rule = ruleOf(line);
239
+ return typeof rule === "string" ? rule : null;
240
+ };
241
+ const utcStamp = (at) => new Date(at)
242
+ .toISOString()
243
+ .replace(/[-:]/gu, "")
244
+ .replace(/\.\d{3}/u, "");
245
+ /**
246
+ * The slots of a series inside the window, as occurrences: its RRULE, plus RDATE, less EXDATE, which come back
247
+ * `cancelled` so a stored slot is cancelled. Times repeat in the series' own zone, so a weekly 10:00 stays 10:00
248
+ * across a clock change. An override (an iCalendar RECURRENCE-ID) takes the place of the slot it names.
249
+ */
250
+ export const expandSeries = (series, window, overrides = []) => {
251
+ const recurrence = series.recurrence ?? [];
252
+ if (unexpandable(recurrence) !== null || series.startsAt == null)
253
+ return [];
254
+ const zone = series.timezone ?? "UTC";
255
+ const allDay = series.allDay === true && typeof series.startDate === "string";
256
+ const start = allDay ? series.startDate : wallClock(series.startsAt, zone).date;
257
+ const time = allDay ? "00:00:00" : wallClock(series.startsAt, zone).time;
258
+ const length = (series.endsAt ?? series.startsAt) - series.startsAt;
259
+ const days = allDay && series.endDate ? Math.max(1, daysBetween(start, series.endDate)) : 1;
260
+ const line = recurrence.find((one) => /^RRULE[:;]/iu.test(one));
261
+ const rule = line === undefined ? undefined : ruleOf(line);
262
+ const until = rule?.until === undefined ? undefined : stampOf(rule.until, zone);
263
+ const momentOf = (date) => (allDay ? localMoment(date, "00:00:00", zone) : localMoment(date, time, zone));
264
+ const past = (date, at) => until !== undefined && (allDay || until.at === undefined ? date > until.date : at > until.at);
265
+ const dates = [{ date: start, at: series.startsAt }];
266
+ if (rule) {
267
+ let count = 1;
268
+ outer: for (let n = 0; n < MOST_PERIODS; n++) {
269
+ if (Date.parse(periodStart(rule, start, n)) > window.to + DAY)
270
+ break;
271
+ for (const date of periodDates(rule, start, n)) {
272
+ if (date <= start)
273
+ continue;
274
+ const at = momentOf(date);
275
+ if (past(date, at) || (rule.count !== undefined && count >= rule.count) || at >= window.to)
276
+ break outer;
277
+ dates.push({ date, at });
278
+ count++;
279
+ }
280
+ }
281
+ }
282
+ for (const extra of listed(recurrence, "RDATE", zone)) {
283
+ const at = allDay ? momentOf(extra.date) : (extra.at ?? momentOf(extra.date));
284
+ if (!dates.some((one) => one.at === at))
285
+ dates.push({ date: extra.date, at });
286
+ }
287
+ const excluded = listed(recurrence, "EXDATE", zone);
288
+ const isExcluded = ({ date, at }) => excluded.some((one) => (allDay || one.at === undefined ? one.date === date : one.at === at));
289
+ const originalOf = ({ date, at }) => (allDay ? Date.parse(`${date}T00:00:00Z`) : at);
290
+ const replaced = new Map(overrides.map((one) => [one.originalStartsAt, one]));
291
+ const inWindow = (entry) => entry.startsAt != null &&
292
+ entry.startsAt < window.to &&
293
+ Math.max(entry.endsAt ?? entry.startsAt, entry.startsAt + 1) > window.from;
294
+ const slotId = (original) => `${series.externalId}_${allDay ? new Date(original).toISOString().slice(0, 10).replaceAll("-", "") : utcStamp(original)}`;
295
+ const slots = [];
296
+ for (const one of dates.sort((a, b) => a.at - b.at)) {
297
+ const original = originalOf(one);
298
+ const override = replaced.get(original);
299
+ replaced.delete(original);
300
+ const cancelled = isExcluded(one);
301
+ const base = override && !cancelled ? override : series;
302
+ const endDate = allDay ? plusDays(one.date, days) : null;
303
+ const slot = {
304
+ ...base,
305
+ externalId: slotId(original),
306
+ kind: "occurrence",
307
+ seriesExternalId: series.externalId,
308
+ originalStartsAt: original,
309
+ exception: cancelled || override !== undefined,
310
+ status: cancelled ? "cancelled" : (base.status ?? "confirmed"),
311
+ recurrence: null,
312
+ etag: null,
313
+ ...(override && !cancelled
314
+ ? {}
315
+ : {
316
+ startsAt: one.at,
317
+ endsAt: allDay ? momentOf(endDate) : one.at + length,
318
+ startDate: allDay ? one.date : null,
319
+ endDate,
320
+ }),
321
+ };
322
+ if (inWindow(slot))
323
+ slots.push(slot);
324
+ }
325
+ for (const [original, override] of replaced) {
326
+ const slot = {
327
+ ...override,
328
+ externalId: slotId(original),
329
+ kind: "occurrence",
330
+ seriesExternalId: series.externalId,
331
+ originalStartsAt: original,
332
+ exception: true,
333
+ recurrence: null,
334
+ etag: null,
335
+ };
336
+ if (inWindow(slot))
337
+ slots.push(slot);
338
+ }
339
+ return slots;
340
+ };
@@ -0,0 +1,96 @@
1
+ import type { CalendarInput, EntryInput, EventStore, StoredCalendar } from "../meetings/index.js";
2
+ import { type IcsEvent } from "./ics.js";
3
+ import type { CalendarExpander, CalendarSource, SyncWindow } from "./source.js";
4
+ import { type CalendarSyncOptions, type CalendarSyncReport } from "./sync.js";
5
+ /** A feed's calendar id: `ics:` and a random id, never the address or anything made from it. */
6
+ export declare const FEED_PREFIX = "ics:";
7
+ export declare const isFeedCalendar: (calendar: Pick<StoredCalendar, "externalId">) => boolean;
8
+ /** One feed the profile follows. `url` may hold a private token: it is a secret, kept as one and never printed. */
9
+ export interface FeedRecord {
10
+ id: string;
11
+ url: string;
12
+ /** The owner's `--title`, or the feed's own name when he gave none. */
13
+ title: string | null;
14
+ }
15
+ /** Where the profile's feeds are kept: the keyring, or a 0600 file, as the CLI keeps its other secrets. */
16
+ export interface FeedRegistry {
17
+ read(): Promise<FeedRecord[]>;
18
+ write(feeds: FeedRecord[]): Promise<void>;
19
+ }
20
+ export interface FeedLimits {
21
+ /** Decoded bytes, so a compressed answer cannot grow past it. */
22
+ maxBytes: number;
23
+ timeoutMs: number;
24
+ redirects: number;
25
+ }
26
+ export declare const FEED_LIMITS: FeedLimits;
27
+ export interface FeedSyncStats {
28
+ calendarId: number;
29
+ events: number;
30
+ skipped: number;
31
+ }
32
+ /** The profile's iCalendar feeds: a source the engine expands locally, and how one is added and forgotten. */
33
+ export interface CalendarFeeds {
34
+ readonly source: CalendarSource;
35
+ readonly expand: CalendarExpander;
36
+ /** Reads the feed once to check it, then keeps its address. */
37
+ add(url: string, options?: {
38
+ title?: string;
39
+ }): Promise<{
40
+ calendar: CalendarInput;
41
+ events: number;
42
+ }>;
43
+ /** Forgets the feed's address; its stored events are the caller's to drop. */
44
+ remove(externalId: string): Promise<void>;
45
+ /** What the last sync read from each feed, and how many of its events it could not take. */
46
+ stats(): FeedSyncStats[];
47
+ }
48
+ /** `webcal://` is `https://`: calendar apps write it so a click opens them, and the feed itself is served over https. */
49
+ export declare const feedAddress: (text: string) => URL;
50
+ export interface FetchFeedOptions {
51
+ /** Names the feed in errors; the address never appears in one. */
52
+ name: string;
53
+ fetch?: typeof fetch;
54
+ signal?: AbortSignal;
55
+ limits?: FeedLimits;
56
+ }
57
+ /**
58
+ * The feed's text: http or https, each redirect followed by hand and only to https, the body cut off at its limit and
59
+ * the request at its time. Errors name the feed and the status, never the address, which may hold a token.
60
+ */
61
+ export declare const fetchFeed: (address: string, options: FetchFeedOptions) => Promise<string>;
62
+ /** One feed event in the neutral words, its text cleaned as an invitation's is: feed text is somebody else's. */
63
+ export declare const feedEntry: (event: IcsEvent, zone: string) => EntryInput;
64
+ export interface FeedRead {
65
+ entries: EntryInput[];
66
+ overrides: Map<string, (EntryInput & {
67
+ originalStartsAt: number;
68
+ })[]>;
69
+ name: string | null;
70
+ timezone: string | null;
71
+ events: number;
72
+ skipped: number;
73
+ }
74
+ /** A feed's events inside the window, its series with their overrides, and what it could not take. */
75
+ export declare const readFeed: (text: string, zone: string, window?: SyncWindow) => FeedRead;
76
+ export interface IcsFeedsOptions {
77
+ registry: FeedRegistry;
78
+ /** The zone a floating time is read in when the feed names none. */
79
+ zone: string;
80
+ fetch?: typeof fetch;
81
+ signal?: AbortSignal;
82
+ limits?: FeedLimits;
83
+ }
84
+ /**
85
+ * The profile's feeds as one calendar source with `expands: "local"` and a full list on every sync: a feed has no
86
+ * cursor. The expander a sync is given reads the overrides of the feed `changes()` just read.
87
+ */
88
+ export declare const icsFeeds: (options: IcsFeedsOptions) => CalendarFeeds;
89
+ export type FeedsSyncReport = CalendarSyncReport & {
90
+ feeds?: FeedSyncStats[];
91
+ };
92
+ /**
93
+ * The provider's calendars, then the profile's feeds, under one budget and in one report: the feeds are calendars of
94
+ * the same account, synced by their own source because they expand their series here.
95
+ */
96
+ export declare const syncWithFeeds: (source: CalendarSource, feeds: CalendarFeeds | undefined, store: EventStore, options: CalendarSyncOptions) => Promise<FeedsSyncReport>;