@wirecat/cli-core 0.463.0 → 0.465.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 (69) hide show
  1. package/CHANGELOG.md +92 -0
  2. package/dist/background/after-upgrade.d.ts +51 -0
  3. package/dist/background/after-upgrade.js +245 -0
  4. package/dist/background/budget.d.ts +37 -0
  5. package/dist/background/budget.js +48 -0
  6. package/dist/background/index.d.ts +1 -0
  7. package/dist/background/index.js +1 -0
  8. package/dist/background/relay.js +5 -1
  9. package/dist/calendar/busy.d.ts +68 -0
  10. package/dist/calendar/busy.js +148 -0
  11. package/dist/calendar/cli/index.d.ts +7 -0
  12. package/dist/calendar/cli/index.js +98 -15
  13. package/dist/calendar/index.d.ts +3 -2
  14. package/dist/calendar/index.js +2 -1
  15. package/dist/calendar/mcp/index.d.ts +55 -13
  16. package/dist/calendar/mcp/index.js +19 -4
  17. package/dist/calendar/recurrence.d.ts +10 -0
  18. package/dist/calendar/recurrence.js +65 -0
  19. package/dist/calendar/source.d.ts +19 -0
  20. package/dist/calendar/sync.d.ts +86 -7
  21. package/dist/calendar/sync.js +271 -105
  22. package/dist/calendar/testing/source-contract.js +14 -0
  23. package/dist/calendar/testing/source.d.ts +6 -0
  24. package/dist/calendar/testing/source.js +30 -2
  25. package/dist/calendar/testing/sync-contract.d.ts +2 -1
  26. package/dist/calendar/testing/sync-contract.js +80 -5
  27. package/dist/calendar/testing/writer-contract.d.ts +2 -1
  28. package/dist/calendar/testing/writer-contract.js +46 -2
  29. package/dist/calendar/testing/writer.js +1 -1
  30. package/dist/calendar/writer.d.ts +7 -2
  31. package/dist/calendar/writes.d.ts +12 -2
  32. package/dist/calendar/writes.js +32 -6
  33. package/dist/cli/context.d.ts +8 -0
  34. package/dist/cli/context.js +15 -0
  35. package/dist/cli/execution.d.ts +7 -0
  36. package/dist/cli/execution.js +7 -0
  37. package/dist/cli/index.d.ts +1 -1
  38. package/dist/cli/index.js +1 -1
  39. package/dist/cli/mcp-server-command.js +16 -10
  40. package/dist/cli/meeting-preparation-answer.d.ts +1 -0
  41. package/dist/cli/meeting-preparation-projects.d.ts +1 -0
  42. package/dist/cli/meeting-preparation-read.d.ts +1 -0
  43. package/dist/cli/messenger/chat-answers.js +1 -0
  44. package/dist/cli/messenger/search-answers.js +29 -4
  45. package/dist/cli/messenger/store-maintenance-command.d.ts +1 -1
  46. package/dist/cli/program.js +15 -5
  47. package/dist/cli/upgrade-command.d.ts +7 -2
  48. package/dist/cli/upgrade-command.js +65 -9
  49. package/dist/entities/fields.d.ts +2 -0
  50. package/dist/entities/fields.js +2 -0
  51. package/dist/entities/hit.d.ts +1 -1
  52. package/dist/entities/hit.js +3 -3
  53. package/dist/entities/index.d.ts +10 -800
  54. package/dist/entities/index.js +37 -31
  55. package/dist/entities/meeting-preparation.d.ts +1 -0
  56. package/dist/entities/topic.d.ts +1 -0
  57. package/dist/entities/topic.js +2 -1
  58. package/dist/errors/reasons.js +4 -0
  59. package/dist/mcp/http/state.d.ts +14 -0
  60. package/dist/mcp/http/state.js +16 -0
  61. package/dist/mcp/meeting-preparation.d.ts +1 -0
  62. package/dist/meetings/answers.d.ts +2 -0
  63. package/dist/meetings/answers.js +3 -0
  64. package/dist/meetings/mcp/search.d.ts +1 -0
  65. package/dist/meetings/project-evidence-answer.d.ts +1 -0
  66. package/dist/ports/app.d.ts +5 -0
  67. package/dist/services/package-upgrade.d.ts +9 -0
  68. package/dist/services/package-upgrade.js +8 -1
  69. package/package.json +3 -3
@@ -0,0 +1,148 @@
1
+ import { CliError } from "@wirecat/cli-base";
2
+ import { dayBoundary, zonedMoment } from "@wirecat/cli-base/query";
3
+ import { calendarIn, plusDays } from "./reads.js";
4
+ export const BUSY_QUERY_HELP = `
5
+ Words, any order:
6
+ today | week today, or today and the next six days, in your time zone
7
+ from:<date> to:<date> from the start of one day through the end of another (or a time with a zone)
8
+ calendar:<id> one of the account's calendars, by its id or the provider's id; repeat for more
9
+ person:<email> someone else, by email address; repeat for more
10
+ Without a time word: this week. Without calendar:, every calendar that syncs.`;
11
+ /** Google answers for 50 calendars at most (`calendarExpansionMax`, reference/freebusy/query). */
12
+ const MOST = 50;
13
+ const EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]+$/u;
14
+ const HOURS = /^([01]\d|2[0-3]):([0-5]\d)-([01]\d|2[0-4]):([0-5]\d)$/u;
15
+ const MINUTE = 60_000;
16
+ const localDate = (at, zone) => new Intl.DateTimeFormat("en-CA", { timeZone: zone, year: "numeric", month: "2-digit", day: "2-digit" }).format(at);
17
+ const span = (text) => ({ start: 0, end: text.length });
18
+ export const busyQuery = async (words, context) => {
19
+ const today = localDate(context.now, context.zone);
20
+ const day = (date) => dayBoundary(date, context.zone, false, span(date));
21
+ let from;
22
+ let to;
23
+ const calendars = [];
24
+ const people = new Set();
25
+ for (const word of words) {
26
+ const [, field, value = ""] = /^([a-z]+):(.+)$/u.exec(word) ?? [];
27
+ if (word === "today" || word === "week") {
28
+ from = day(today);
29
+ to = day(plusDays(today, word === "today" ? 1 : 7));
30
+ }
31
+ else if (field === "from")
32
+ from = zonedMoment(value, context.zone, span(value));
33
+ else if (field === "to")
34
+ to = zonedMoment(value, context.zone, span(value), { end: true }) + 1;
35
+ else if (field === "calendar") {
36
+ const calendar = await calendarIn(context.store, context.accountId, value);
37
+ if (!calendars.some(({ id }) => id === calendar.id))
38
+ calendars.push(calendar);
39
+ }
40
+ else if (field === "person") {
41
+ const email = value.trim().toLowerCase();
42
+ if (!EMAIL.test(email))
43
+ throw new CliError("validation_error", `${value} is not an email address`);
44
+ people.add(email);
45
+ }
46
+ else
47
+ throw new CliError("validation_error", `${word} is not a calendars busy word${BUSY_QUERY_HELP}`, {
48
+ reason: "query.unknown_word",
49
+ });
50
+ }
51
+ from ??= day(today);
52
+ to ??= from === day(today) ? day(plusDays(today, 7)) : from + 7 * 86_400_000;
53
+ if (to <= from)
54
+ throw new CliError("validation_error", "the window ends before it starts");
55
+ const asked = calendars.length
56
+ ? calendars
57
+ : (await context.store.calendars(context.accountId)).filter(({ synced, deletedAt }) => synced && !deletedAt);
58
+ if (asked.length === 0 && people.size === 0)
59
+ throw new CliError("validation_error", "no calendar syncs yet; name one with calendar:<id>, or a person:<email>", {
60
+ reason: "busy.no-calendars",
61
+ });
62
+ if (asked.length + people.size > MOST)
63
+ throw new CliError("validation_error", `at most ${MOST} calendars and people at once`);
64
+ return { window: { from, to }, calendars: asked, people: [...people] };
65
+ };
66
+ export const workingHoursOf = (text) => {
67
+ const match = HOURS.exec(text.trim());
68
+ const [start, end] = match ? [Number(match[1]) * 60 + Number(match[2]), Number(match[3]) * 60 + Number(match[4])] : [];
69
+ if (start === undefined || end === undefined || end <= start || end > 24 * 60)
70
+ throw new CliError("validation_error", "working hours are HH:MM-HH:MM in your time zone, such as 09:00-17:00");
71
+ return { start, end };
72
+ };
73
+ const clock = (minutes) => `${String(Math.floor(minutes / 60)).padStart(2, "0")}:${String(minutes % 60).padStart(2, "0")}`;
74
+ /** Each weekday's working hours as moments in `zone`, so a day that changes its clock is still read in local time. */
75
+ const workingSpans = (window, hours, zone) => {
76
+ const spans = [];
77
+ const last = localDate(window.to - 1, zone);
78
+ for (let date = localDate(window.from, zone); date <= last; date = plusDays(date, 1)) {
79
+ const weekday = new Date(`${date}T12:00:00Z`).getUTCDay();
80
+ if (weekday === 0 || weekday === 6)
81
+ continue;
82
+ const at = (minutes) => minutes === 24 * 60
83
+ ? dayBoundary(plusDays(date, 1), zone, false, span(date))
84
+ : zonedMoment(`${date}T${clock(minutes)}`, zone, span(date));
85
+ spans.push({ from: at(hours.start), to: at(hours.end) });
86
+ }
87
+ return spans;
88
+ };
89
+ /**
90
+ * The stretches of at least `minutes` inside working hours on weekdays, after `now`, that no busy time touches.
91
+ * A stretch is returned whole; the caller picks where in it to put the meeting.
92
+ */
93
+ export const freeSlots = (busy, window, options) => {
94
+ const taken = [...busy].sort((a, b) => a.from - b.from);
95
+ const slots = [];
96
+ for (const day of workingSpans(window, options.hours, options.zone)) {
97
+ let cursor = Math.max(day.from, window.from, options.now);
98
+ const end = Math.min(day.to, window.to);
99
+ for (const one of taken) {
100
+ if (one.to <= cursor || one.from >= end)
101
+ continue;
102
+ if (one.from - cursor >= options.minutes * MINUTE)
103
+ slots.push({ from: cursor, to: one.from });
104
+ cursor = Math.max(cursor, one.to);
105
+ }
106
+ if (end - cursor >= options.minutes * MINUTE)
107
+ slots.push({ from: cursor, to: end });
108
+ }
109
+ return slots;
110
+ };
111
+ const iso = (at) => new Date(at).toISOString();
112
+ const times = ({ from, to }) => ({ start: iso(from), end: iso(to) });
113
+ /**
114
+ * `calendars busy`: the provider's busy times for the query, and with `slot` the free stretches every calendar and
115
+ * person shares. Nothing is stored, and a person appears only by the address the caller gave.
116
+ */
117
+ export const busyAnswer = async (source, query, options) => {
118
+ if (!source.freeBusy)
119
+ throw new CliError("configuration_error", "this calendar provider cannot tell busy times");
120
+ const answered = await source.freeBusy({
121
+ calendars: query.calendars.map(({ externalId }) => externalId),
122
+ people: query.people,
123
+ window: query.window,
124
+ });
125
+ const byId = new Map(answered.map((one) => [one.id, one]));
126
+ const missing = (id) => byId.get(id) ?? { id, busy: [], error: "notAnswered" };
127
+ const rows = [
128
+ ...query.calendars.map((calendar) => ({ calendar: String(calendar.id), ...missing(calendar.externalId) })),
129
+ ...query.people.map((person) => ({ person, ...missing(person) })),
130
+ ];
131
+ const unknown = rows.filter(({ error }) => error !== undefined).map(({ id }) => id);
132
+ return {
133
+ ...times(query.window),
134
+ timezone: options.zone,
135
+ calendars: rows.map(({ busy, error, ...row }) => ({
136
+ ...row,
137
+ busy: busy.map(times),
138
+ ...(error === undefined ? {} : { error }),
139
+ })),
140
+ ...(options.slot
141
+ ? {
142
+ slots: freeSlots(rows.flatMap(({ busy }) => busy), query.window, { ...options.slot, zone: options.zone, now: options.now }).map(times),
143
+ // Free here means free in every calendar the provider answered for: these were not.
144
+ ...(unknown.length ? { unknown } : {}),
145
+ }
146
+ : {}),
147
+ };
148
+ };
@@ -9,10 +9,17 @@ export interface CalendarCommandDeps {
9
9
  accountId: number;
10
10
  /** The same account as the shared store names it, for search and people. */
11
11
  account: AccountKey;
12
+ /** The profile's name, which `status` gives as each sync's `profile`. */
13
+ profile?: string;
12
14
  /** The shared store: `search`, `people link` and the person of an attendee read it. */
13
15
  messages?: MessageStore;
14
16
  /** The provider; `sync` and `calendars list` read it. */
15
17
  source?: CalendarSource;
18
+ /**
19
+ * The source answers `freeBusy`: mounts `calendars busy`. A flag rather than a look at `source`, which a CLI may
20
+ * build only once a profile is open.
21
+ */
22
+ freeBusy?: boolean;
16
23
  /** For a source with `expands: "local"`. */
17
24
  expand?: CalendarExpander;
18
25
  /** After `sync --embed`: embeds new events for search by meaning (`embedEvents`), as drive's `sync --embed`. */
@@ -2,16 +2,21 @@ import { CliError } from "@wirecat/cli-base";
2
2
  import { annotate, sharedCommand } from "@wirecat/cli-base/commands";
3
3
  import { timezoneOf } from "@wirecat/cli-base/query";
4
4
  import { Argument, InvalidArgumentError } from "commander";
5
+ import { environmentOf, runBudgetOf } from "../../cli/context.js";
5
6
  import { honoursDryRun } from "../../cli/dry-run-guard.js";
6
7
  import { terminalAsker } from "../../cli/messenger/ask.js";
8
+ import { lineOf } from "../../cli/steps.js";
7
9
  import { searchEvents } from "../../services/events-search.js";
8
10
  import { linkPeople } from "../../services/people.js";
11
+ import { BUSY_QUERY_HELP, busyAnswer, busyQuery, workingHoursOf } from "../busy.js";
9
12
  import { CALENDAR_UNTRUSTED_FIELDS, calendarIn, calendarItem, EVENTS_QUERY_HELP, eventAnswer, eventIdOf, eventOf, eventsAnswer, eventsFilter, peopleAnswer, resolveAnswer, reviewsAnswer, } from "../reads.js";
10
- import { syncCalendars } from "../sync.js";
13
+ import { REPEAT_HELP } from "../recurrence.js";
14
+ import { calendarStatus, syncCalendars } from "../sync.js";
11
15
  import { createEvent, deleteEvent, EVENT_RESPONSES, respondToEvent, updateEvent, } from "../writes.js";
12
16
  const LOCAL_READ = { remote: "none", local: "read" };
13
17
  const LOCAL_WRITE = { remote: "none", local: "write" };
14
18
  const FETCH = { remote: "read", local: "write" };
19
+ const REMOTE_READ = { remote: "read", local: "none" };
15
20
  const REMOTE_WRITE = { remote: "write", local: "write" };
16
21
  const GROUP = { remote: "none", local: "none" };
17
22
  /** How many pieces one `sync --embed` embeds, as drive's. */
@@ -30,6 +35,12 @@ const integer = (text) => {
30
35
  throw new InvalidArgumentError("expected a positive integer");
31
36
  return Number(text);
32
37
  };
38
+ const minutes = (text) => {
39
+ const value = integer(text);
40
+ if (value < 5 || value > 1440)
41
+ throw new InvalidArgumentError("expected 5 to 1440 minutes");
42
+ return value;
43
+ };
33
44
  const addCalendarCommandsDefinition = (program, deps) => {
34
45
  const now = () => deps.now?.() ?? Date.now();
35
46
  const zone = () => timezoneOf(deps.timezone);
@@ -85,27 +96,90 @@ const addCalendarCommandsDefinition = (program, deps) => {
85
96
  await deps.store.setCalendarSynced(calendar.id, synced, now());
86
97
  print(calendarItem({ ...calendar, synced }), command);
87
98
  });
88
- output(example(annotate(program.command("sync"), { effects: FETCH, offline: false }).description("bring the synced calendars up to date: what changed since the last sync, or everything with --full"), "sync", "sync --full --embed"))
89
- .option("--full", "list every synced calendar's window again instead of reading changes")
99
+ if (deps.freeBusy)
100
+ output(example(annotate(calendars.command("busy"), { effects: REMOTE_READ, offline: false })
101
+ .description("when calendars and people are taken, asked of the provider and not stored; with --duration-minutes, " +
102
+ "the free stretches they share")
103
+ .argument("[words...]", "today, week, from:<date>, to:<date>, calendar:<id>, person:<email>")
104
+ .addHelpText("after", BUSY_QUERY_HELP), "calendars busy week person:ana@example.com --json", "calendars busy from:2026-11-02 to:2026-11-06 person:ana@example.com --duration-minutes 30 --hours 10:00-16:00"))
105
+ .option("--duration-minutes <minutes>", "find free stretches at least this long, 5–1440", minutes)
106
+ .option("--hours <from-to>", "working hours in your time zone, Monday to Friday, with --duration-minutes", "09:00-17:00")
107
+ .action(async (words, opts, command) => {
108
+ if (command.optsWithGlobals().offline)
109
+ throw new CliError("permission_error", "calendars busy asks the provider; leave out --offline", {
110
+ reason: "busy.offline",
111
+ });
112
+ const hours = workingHoursOf(opts.hours);
113
+ const query = await busyQuery(words, { store: deps.store, accountId: deps.accountId, zone: zone(), now: now() });
114
+ print(await busyAnswer(source(), query, {
115
+ zone: zone(),
116
+ now: now(),
117
+ ...(opts.durationMinutes === undefined ? {} : { slot: { minutes: opts.durationMinutes, hours } }),
118
+ }), command);
119
+ });
120
+ output(example(annotate(program.command("sync"), {
121
+ effects: FETCH,
122
+ offline: false,
123
+ cooperative: true,
124
+ partial: true,
125
+ }).description("bring the synced calendars up to date: what changed since the last sync, or everything with --rescan; a run " +
126
+ "stops before its deadline or --max-pages and the next one goes on from there"), "sync", "sync --rescan --embed", "sync --timeout 2m"))
127
+ .option("--rescan", "list every synced calendar's window again, dropping a round in progress")
128
+ .option("--max-pages <pages>", "at most this many requests to the provider in this run; as many as fit the deadline if not given", integer)
90
129
  .option("--embed", "also index new events for meaning search, with a model already downloaded")
91
130
  .action(async (opts, command) => {
92
- // The shared program has a global --full of its own, which claims the flag before this command sees it.
93
- const full = command.optsWithGlobals().full === true;
131
+ // --full is the shared program's output flag; it used to start the round over here, and silently would not now.
132
+ if (command.optsWithGlobals().full)
133
+ throw new CliError("validation_error", "sync starts over with --rescan; --full only widens output");
134
+ const { signal } = environmentOf(command);
94
135
  const report = await syncCalendars(source(), deps.store, {
95
136
  accountId: deps.accountId,
96
137
  now: now(),
97
- ...(full ? { full: true } : {}),
138
+ budget: runBudgetOf(command, opts.maxPages),
139
+ ...(signal ? { signal } : {}),
140
+ ...(opts.rescan ? { full: true } : {}),
98
141
  ...(deps.expand ? { expand: deps.expand } : {}),
99
142
  });
100
143
  if (opts.embed && !deps.embed)
101
144
  throw new CliError("configuration_error", "this tool cannot embed events");
102
- const embeddings = opts.embed ? await deps.embed?.(EMBED_MAX) : undefined;
103
- print(embeddings === undefined ? report : { ...report, embeddings }, command);
104
- if (!report.complete)
145
+ // A run its budget stopped has no time left for the model; the run that finishes the round embeds.
146
+ const embeddings = opts.embed && !report.stopped ? await deps.embed?.(EMBED_MAX) : undefined;
147
+ const timeout = command.optsWithGlobals().timeout;
148
+ // The same command again, never --rescan: that would drop the round this run leaves in progress. A run that moved
149
+ // nothing on would only repeat itself, so it gets no step an agent may run unasked.
150
+ const stuck = report.state === "partial" && report.progress.steps === 0 && !report.synced.some((one) => one.error);
151
+ const next = report.state === "partial" && !stuck && !report.synced.some((one) => one.error !== undefined)
152
+ ? [
153
+ {
154
+ kind: "command",
155
+ step: "calendar.sync",
156
+ command: "sync",
157
+ args: Object.fromEntries(Object.entries({ max_pages: opts.maxPages, timeout, embed: opts.embed }).filter(([, value]) => value !== undefined)),
158
+ why: "the sync stopped before its end to answer in time; running it again goes on from where it stopped",
159
+ safe_to_auto_execute: true,
160
+ },
161
+ ]
162
+ : undefined;
163
+ const progressAt = lineOf(command, "status");
164
+ report.hint = stuck
165
+ ? `this run moved nothing on: raise --timeout or --max-pages, then run sync again; see progress: ${progressAt}`
166
+ : report.state === "partial"
167
+ ? `run sync again to continue; see progress: ${progressAt}`
168
+ : report.hint;
169
+ if (report.hint === undefined)
170
+ delete report.hint;
171
+ print({ ...report, ...(embeddings === undefined ? {} : { embeddings }), ...(next ? { next } : {}) }, command);
172
+ if (report.synced.some((one) => one.error !== undefined))
105
173
  throw new CliError("provider_error", "some calendars did not sync; run sync again to retry them", {
106
174
  reason: "sync.partial",
107
175
  });
108
176
  });
177
+ output(example(annotate(program.command("status"), { effects: LOCAL_READ }).description("how far each synced calendar's sync is: entries done, partial or complete, and when it last moved"), "status --json")).action(async (_opts, command) => {
178
+ print(await calendarStatus(deps.store, deps.accountId, {
179
+ profile: deps.profile ?? `account ${deps.accountId}`,
180
+ now: now(),
181
+ }), command);
182
+ });
109
183
  const events = annotate(program.command("events").description("the account's stored events"), { effects: GROUP });
110
184
  output(example(annotate(events.command("list"), { effects: LOCAL_READ, output: { entity: "event" } })
111
185
  .description("list stored events, earliest first")
@@ -199,7 +273,13 @@ const addEventWrites = (events, writes, { deps, now, zone, output, print, exampl
199
273
  ask: terminalAsker(command),
200
274
  now: now(),
201
275
  zone: zone(),
202
- names: { notify: "--notify or --no-notify", id: "--id", calendar: "--calendar", series: "--series" },
276
+ names: {
277
+ notify: "--notify or --no-notify",
278
+ id: "--id",
279
+ calendar: "--calendar",
280
+ series: "--series",
281
+ repeat: "--repeat",
282
+ },
203
283
  });
204
284
  const printWritten = (value, command) => print({ ...value, untrusted: CALENDAR_UNTRUSTED_FIELDS }, command);
205
285
  // Left out on purpose: with other attendees the owner must choose, so neither switch is the default.
@@ -216,7 +296,8 @@ const addEventWrites = (events, writes, { deps, now, zone, output, print, exampl
216
296
  .option("--location <text>", "where; empty clears it")
217
297
  .option("--description <text>", "the details; empty clears it")
218
298
  // The empty default marks the flag as repeatable, so an agent's tool takes a list; empty is "not given".
219
- .option("--attendee <email>", `an attendee; repeat for each${replacing ? ", replacing the list" : ""}`, collect, []);
299
+ .option("--attendee <email>", `an attendee; repeat for each${replacing ? ", replacing the list" : ""}`, collect, [])
300
+ .option("--repeat <rule>", `how it repeats${replacing ? ", with --series" : ""}: ${REPEAT_HELP}`);
220
301
  const fieldsOf = ({ attendee, ...given }) => ({
221
302
  ...Object.fromEntries(Object.entries(given).filter(([, value]) => value !== undefined)),
222
303
  ...(attendee === undefined || attendee.length === 0 ? {} : { attendees: attendee }),
@@ -231,9 +312,10 @@ const addEventWrites = (events, writes, { deps, now, zone, output, print, exampl
231
312
  .argument("<event>", "the event's id, or event:<id>")
232
313
  .option("--calendar <id>", "the calendar, when the event is on more than one of the account's");
233
314
  output(example(notify(fields(write("create"), false)
234
- .description("add an event to a calendar; one that repeats is made in the calendar itself")
315
+ .description("add an event to a calendar; with --repeat, one that repeats")
235
316
  .argument("<calendar>", "the calendar's id, or the provider's id")).option("--id <id>", "the id a create that ended in outcome_unknown named: a retry finds that event"), "events create 3 --title 'Example planning' --start-time 2026-11-02T10:00 --end-time 2026-11-02T11:00", "events create 3 --title 'Example review' --start-date 2026-11-03 --end-date 2026-11-03 " +
236
- "--attendee ana@example.com --no-notify")).action(async (calendar, opts, command) => {
317
+ "--attendee ana@example.com --no-notify", "events create 3 --title 'Example standup' --start-time 2026-11-02T09:30 --end-time 2026-11-02T09:45 " +
318
+ "--repeat 'FREQ=WEEKLY;BYDAY=MO,WE,FR;COUNT=12'")).action(async (calendar, opts, command) => {
237
319
  const { notify: choice, id, ...rest } = opts;
238
320
  printWritten(await createEvent(context(command), calendar, fieldsOf(rest), {
239
321
  ...(choice === undefined ? {} : { notify: choice }),
@@ -241,8 +323,9 @@ const addEventWrites = (events, writes, { deps, now, zone, output, print, exampl
241
323
  }), command);
242
324
  });
243
325
  output(example(notify(target(fields(write("update"), true))
244
- .description("change an event; with --series every occurrence of the repeating event")
245
- .option("--series", "change the whole repeating event, not only this occurrence")), "events update event:42 --start-time 2026-11-02T14:00 --end-time 2026-11-02T15:00 --notify")).action(async (reference, opts, command) => {
326
+ .description("change an event; with --series every occurrence of the repeating event; new times or dates switch it " +
327
+ "between timed and all-day")
328
+ .option("--series", "change the whole repeating event, not only this occurrence")), "events update event:42 --start-time 2026-11-02T14:00 --end-time 2026-11-02T15:00 --notify", "events update event:42 --series --repeat 'FREQ=WEEKLY;BYDAY=TU;UNTIL=20261231T230000Z' --no-notify")).action(async (reference, opts, command) => {
246
329
  const { calendar: _calendar, series: _series, notify: _notify, ...rest } = opts;
247
330
  printWritten(await updateEvent(context(command), reference, fieldsOf(rest), choicesOf(opts)), command);
248
331
  });
@@ -1,5 +1,6 @@
1
1
  export type { CalendarAccessRole, CalendarInput, EntryAttachment, EntryAttachmentInput, EntryEventType, EntryInput, EntryKind, EntryParticipantInput, EntryRevision, EntryStatus, EntryTransparency, EntryVisibility, EventDetails, EventFilter, EventStore, LinkReview, ParticipantResponse, ReviewDecision, StoredCalendar, StoredEntry, StoredParticipant, } from "../meetings/index.js";
2
- export type { CalendarExpander, CalendarSource, CalendarSourceTraits, ChangePage, ChangeResult, SyncWindow, } from "./source.js";
3
- export { type CalendarCursor, type CalendarSyncOptions, type CalendarSyncReport, type CalendarSyncResult, calendarSyncWindow, parseCalendarCursor, syncCalendars, } from "./sync.js";
2
+ export { repeatRule } from "./recurrence.js";
3
+ export type { BusyTimes, CalendarExpander, CalendarSource, CalendarSourceTraits, ChangePage, ChangeResult, FreeBusyRequest, SyncWindow, } from "./source.js";
4
+ export { type CalendarCursor, type CalendarSyncOptions, type CalendarSyncReport, type CalendarSyncResult, calendarStatus, calendarSyncWindow, type PendingRound, parseCalendarCursor, syncCalendars, } from "./sync.js";
4
5
  export { CALENDAR_WRITE_DEFAULTS, CALENDAR_WRITE_KEYS, type CalendarWrite, type CalendarWriter, calendarConflict, EVENTS_DELETE_SERIES_KEY, EVENTS_NOTIFY_KEY, type EventDraft, type EventPatch, type EventResponse, type WriteScope, } from "./writer.js";
5
6
  export type { CalendarWrites } from "./writes.js";
@@ -1,2 +1,3 @@
1
- export { calendarSyncWindow, parseCalendarCursor, syncCalendars, } from "./sync.js";
1
+ export { repeatRule } from "./recurrence.js";
2
+ export { calendarStatus, calendarSyncWindow, parseCalendarCursor, syncCalendars, } from "./sync.js";
2
3
  export { CALENDAR_WRITE_DEFAULTS, CALENDAR_WRITE_KEYS, calendarConflict, EVENTS_DELETE_SERIES_KEY, EVENTS_NOTIFY_KEY, } from "./writer.js";
@@ -10,13 +10,13 @@ export declare const calendarTools: ({
10
10
  inputSchema: {
11
11
  type: string;
12
12
  properties: {
13
+ limit?: undefined;
13
14
  account_id: {
14
15
  type: string;
15
16
  minimum: number;
16
17
  };
17
18
  words?: undefined;
18
19
  timezone?: undefined;
19
- limit?: undefined;
20
20
  event?: undefined;
21
21
  } | {
22
22
  account_id: {
@@ -38,9 +38,9 @@ export declare const calendarTools: ({
38
38
  };
39
39
  event?: undefined;
40
40
  } | {
41
+ limit?: undefined;
41
42
  words?: undefined;
42
43
  timezone?: undefined;
43
- limit?: undefined;
44
44
  event: {
45
45
  type: string;
46
46
  minLength: number;
@@ -50,9 +50,9 @@ export declare const calendarTools: ({
50
50
  minimum: number;
51
51
  };
52
52
  } | {
53
+ limit?: undefined;
53
54
  words?: undefined;
54
55
  timezone?: undefined;
55
- limit?: undefined;
56
56
  event?: undefined;
57
57
  account_id?: undefined;
58
58
  };
@@ -71,13 +71,13 @@ export declare const calendarTools: ({
71
71
  inputSchema: {
72
72
  type: string;
73
73
  properties: {
74
+ limit?: undefined;
74
75
  account_id: {
75
76
  type: string;
76
77
  minimum: number;
77
78
  };
78
79
  words?: undefined;
79
80
  timezone?: undefined;
80
- limit?: undefined;
81
81
  event?: undefined;
82
82
  } | {
83
83
  account_id: {
@@ -99,9 +99,9 @@ export declare const calendarTools: ({
99
99
  };
100
100
  event?: undefined;
101
101
  } | {
102
+ limit?: undefined;
102
103
  words?: undefined;
103
104
  timezone?: undefined;
104
- limit?: undefined;
105
105
  event: {
106
106
  type: string;
107
107
  minLength: number;
@@ -111,9 +111,9 @@ export declare const calendarTools: ({
111
111
  minimum: number;
112
112
  };
113
113
  } | {
114
+ limit?: undefined;
114
115
  words?: undefined;
115
116
  timezone?: undefined;
116
- limit?: undefined;
117
117
  event?: undefined;
118
118
  account_id?: undefined;
119
119
  };
@@ -132,13 +132,13 @@ export declare const calendarTools: ({
132
132
  inputSchema: {
133
133
  type: string;
134
134
  properties: {
135
+ limit?: undefined;
135
136
  account_id: {
136
137
  type: string;
137
138
  minimum: number;
138
139
  };
139
140
  words?: undefined;
140
141
  timezone?: undefined;
141
- limit?: undefined;
142
142
  event?: undefined;
143
143
  } | {
144
144
  account_id: {
@@ -160,9 +160,9 @@ export declare const calendarTools: ({
160
160
  };
161
161
  event?: undefined;
162
162
  } | {
163
+ limit?: undefined;
163
164
  words?: undefined;
164
165
  timezone?: undefined;
165
- limit?: undefined;
166
166
  event: {
167
167
  type: string;
168
168
  minLength: number;
@@ -172,9 +172,9 @@ export declare const calendarTools: ({
172
172
  minimum: number;
173
173
  };
174
174
  } | {
175
+ limit?: undefined;
175
176
  words?: undefined;
176
177
  timezone?: undefined;
177
- limit?: undefined;
178
178
  event?: undefined;
179
179
  account_id?: undefined;
180
180
  };
@@ -193,13 +193,13 @@ export declare const calendarTools: ({
193
193
  inputSchema: {
194
194
  type: string;
195
195
  properties: {
196
+ limit?: undefined;
196
197
  account_id: {
197
198
  type: string;
198
199
  minimum: number;
199
200
  };
200
201
  words?: undefined;
201
202
  timezone?: undefined;
202
- limit?: undefined;
203
203
  event?: undefined;
204
204
  } | {
205
205
  account_id: {
@@ -221,9 +221,9 @@ export declare const calendarTools: ({
221
221
  };
222
222
  event?: undefined;
223
223
  } | {
224
+ limit?: undefined;
224
225
  words?: undefined;
225
226
  timezone?: undefined;
226
- limit?: undefined;
227
227
  event: {
228
228
  type: string;
229
229
  minLength: number;
@@ -233,9 +233,9 @@ export declare const calendarTools: ({
233
233
  minimum: number;
234
234
  };
235
235
  } | {
236
+ limit?: undefined;
236
237
  words?: undefined;
237
238
  timezone?: undefined;
238
- limit?: undefined;
239
239
  event?: undefined;
240
240
  account_id?: undefined;
241
241
  };
@@ -303,6 +303,11 @@ export declare const calendarWriteTools: ({
303
303
  };
304
304
  description: string;
305
305
  };
306
+ repeat: {
307
+ type: string;
308
+ minLength: number;
309
+ description: string;
310
+ };
306
311
  account_id: {
307
312
  type: string;
308
313
  minimum: number;
@@ -365,6 +370,11 @@ export declare const calendarWriteTools: ({
365
370
  };
366
371
  description: string;
367
372
  };
373
+ repeat: {
374
+ type: string;
375
+ minLength: number;
376
+ description: string;
377
+ };
368
378
  account_id: {
369
379
  type: string;
370
380
  minimum: number;
@@ -488,6 +498,11 @@ export declare const calendarWriteTools: ({
488
498
  };
489
499
  description: string;
490
500
  };
501
+ repeat: {
502
+ type: string;
503
+ minLength: number;
504
+ description: string;
505
+ };
491
506
  account_id: {
492
507
  type: string;
493
508
  minimum: number;
@@ -550,6 +565,11 @@ export declare const calendarWriteTools: ({
550
565
  };
551
566
  description: string;
552
567
  };
568
+ repeat: {
569
+ type: string;
570
+ minLength: number;
571
+ description: string;
572
+ };
553
573
  account_id: {
554
574
  type: string;
555
575
  minimum: number;
@@ -673,6 +693,11 @@ export declare const calendarWriteTools: ({
673
693
  };
674
694
  description: string;
675
695
  };
696
+ repeat: {
697
+ type: string;
698
+ minLength: number;
699
+ description: string;
700
+ };
676
701
  account_id: {
677
702
  type: string;
678
703
  minimum: number;
@@ -735,6 +760,11 @@ export declare const calendarWriteTools: ({
735
760
  };
736
761
  description: string;
737
762
  };
763
+ repeat: {
764
+ type: string;
765
+ minLength: number;
766
+ description: string;
767
+ };
738
768
  account_id: {
739
769
  type: string;
740
770
  minimum: number;
@@ -858,6 +888,11 @@ export declare const calendarWriteTools: ({
858
888
  };
859
889
  description: string;
860
890
  };
891
+ repeat: {
892
+ type: string;
893
+ minLength: number;
894
+ description: string;
895
+ };
861
896
  account_id: {
862
897
  type: string;
863
898
  minimum: number;
@@ -920,6 +955,11 @@ export declare const calendarWriteTools: ({
920
955
  };
921
956
  description: string;
922
957
  };
958
+ repeat: {
959
+ type: string;
960
+ minLength: number;
961
+ description: string;
962
+ };
923
963
  account_id: {
924
964
  type: string;
925
965
  minimum: number;
@@ -998,8 +1038,10 @@ export declare const calendarWriteTools: ({
998
1038
  * Runs one calendar tool over the accounts the host serves. Without `account_id` a tool reads every one of them and
999
1039
  * names them in `accounts`; with one it must be among them.
1000
1040
  */
1001
- export declare const callCalendarTool: (store: EventStore, name: string, input: unknown, { accountIds, failure, now, writes, }: {
1041
+ export declare const callCalendarTool: (store: EventStore, name: string, input: unknown, { accountIds, failure, now, writes, profileOf, }: {
1002
1042
  accountIds: readonly number[];
1043
+ /** What `status` calls each account's sync. */
1044
+ profileOf?: (accountId: number) => string;
1003
1045
  failure?: FailureContext;
1004
1046
  now?: number;
1005
1047
  /** Each account's writer and profile; without it the write tools refuse. */