@wirecat/cli-core 0.457.0 → 0.458.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/CHANGELOG.md CHANGED
@@ -7,6 +7,42 @@ shape of this file.
7
7
 
8
8
  ## Unreleased
9
9
 
10
+ ## 0.458.0 — 27.10.2026
11
+
12
+ ### Added
13
+
14
+ - **Calendar writes (`@wirecat/cli-core/calendar`).** An optional `CalendarWriter` beside `CalendarSource`:
15
+ `create(calendar, draft, { id, notify })`, `update(calendar, externalId, patch, { scope, etag, notify })`,
16
+ `remove(calendar, externalId, { scope, etag, notify })` and `respond(calendar, externalId, response, { etag,
17
+ notify })`, in neutral words (`EventDraft`, `EventPatch`, `WriteScope` `this | series`, `EventResponse`). The id of a
18
+ create is chosen before the call and journaled as its `sendId`, so a retry after `outcome_unknown` with the same id
19
+ finds the event instead of making a second one. Every change sends the stored `etag`; an adapter turns a mismatch
20
+ into `calendarConflict()` (`validation_error`, reason `event.conflict`: sync and try again), never an overwrite. What
21
+ a write returns is saved at once; a deletion cancels the entry as a sync would. Repeating events are not created
22
+ here, and "this and following" is not offered.
23
+ - **`events create | update | delete | respond`** in `addCalendarCommands`, only when the CLI passes
24
+ `writes: CalendarWrites` (the writer, `app`, the profile's `GuardSettings`, and `extraKeys(write)` for keys the
25
+ CLI adds, such as gcal's `api.calendar.events.insert`). Each goes through `guardFor` and the send journal as a
26
+ messenger's writes do (WC-PERM-13, WC-PERM-17), and `--dry-run` previews it. Built-in levels
27
+ (`CALENDAR_WRITE_DEFAULTS`, WC-PERM-6/7): `events.create`, `events.update`, `events.respond` and `events.delete`
28
+ `ask`; `events.delete.series` `deny`; `events.notify` `deny`. Times are `--start-time`/`--end-time`, an all-day
29
+ event `--start-date`/`--end-date` (the last day). `--notify` or `--no-notify` is required when the event has
30
+ attendees other than the owner (reason `event.notify-required`), and `--notify` must pass `events.notify`.
31
+ `CALENDAR_WRITE_KEYS` lists the keys that are not command paths, for `commandPermissions({ extra })`.
32
+ `addCalendarCommands` now honours a global `--dry-run` in any host program, as cli-core's other shared commands do.
33
+ - **`calendarWriteTools`** (`event_create`, `event_update`, `event_delete`, `event_respond`) for `callCalendarTool`
34
+ with `writes: (accountId) => CalendarWrites`. Over MCP `ask` goes ahead (WC-MCP-7); `deny`, `readonly`,
35
+ `events.notify` and the extra keys still refuse; `notify` is `true` or `false`. A write acts in the account named, or the only one that holds the
36
+ calendar or event; two make it ask for `account_id`.
37
+ - **Contract kit:** `fakeCalendarWriter` and `calendarWriterContract` (a stale etag is a conflict for update, remove
38
+ and respond; a create retried with its id finds the event; `notify` reaches the provider as given; with
39
+ `events.notify` at its default a create that would mail attendees never reaches the provider).
40
+
41
+ ### Changed — may break callers
42
+
43
+ - **`EventStore` has `entry(calendarId, externalId)`, and `StoredEntry` has `etag`.** A store of its own must add
44
+ both; the column already exists, so there is no migration.
45
+
10
46
  ## 0.457.0 — 27.10.2026
11
47
 
12
48
  ### Fixed
@@ -2,6 +2,7 @@ import { type Command } from "commander";
2
2
  import type { EventStore } from "../../meetings/index.js";
3
3
  import type { AccountKey, MessageStore } from "../../store/store.js";
4
4
  import type { CalendarExpander, CalendarSource } from "../source.js";
5
+ import { type CalendarWrites } from "../writes.js";
5
6
  export interface CalendarCommandDeps {
6
7
  store: EventStore;
7
8
  /** The selected profile's calendar account: every command reads and syncs only it. */
@@ -20,5 +21,7 @@ export interface CalendarCommandDeps {
20
21
  now?: () => number;
21
22
  /** The zone `today`, `week` and a bare date are read in; the computer's when absent. */
22
23
  timezone?: string;
24
+ /** The provider's writer and the profile's guard: `events create|update|delete|respond` exist only with it. */
25
+ writes?: CalendarWrites;
23
26
  }
24
27
  export declare const addCalendarCommands: (program: Command, deps: CalendarCommandDeps) => void;
@@ -1,14 +1,18 @@
1
1
  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
- import { InvalidArgumentError } from "commander";
4
+ import { Argument, InvalidArgumentError } from "commander";
5
+ import { honoursDryRun } from "../../cli/dry-run-guard.js";
6
+ import { terminalAsker } from "../../cli/messenger/ask.js";
5
7
  import { searchEvents } from "../../services/events-search.js";
6
8
  import { linkPeople } from "../../services/people.js";
7
9
  import { calendarIn, calendarItem, EVENTS_QUERY_HELP, eventAnswer, eventIdOf, eventOf, eventsAnswer, eventsFilter, peopleAnswer, resolveAnswer, reviewsAnswer, } from "../reads.js";
8
10
  import { syncCalendars } from "../sync.js";
11
+ import { createEvent, deleteEvent, EVENT_RESPONSES, respondToEvent, updateEvent, } from "../writes.js";
9
12
  const LOCAL_READ = { remote: "none", local: "read" };
10
13
  const LOCAL_WRITE = { remote: "none", local: "write" };
11
14
  const FETCH = { remote: "read", local: "write" };
15
+ const REMOTE_WRITE = { remote: "write", local: "write" };
12
16
  const GROUP = { remote: "none", local: "none" };
13
17
  /** How many pieces one `sync --embed` embeds, as drive's. */
14
18
  const EMBED_MAX = 600;
@@ -132,6 +136,8 @@ const addCalendarCommandsDefinition = (program, deps) => {
132
136
  ...(opts.newest ? { newest: true } : {}),
133
137
  }), command);
134
138
  });
139
+ if (deps.writes)
140
+ addEventWrites(events, deps.writes, { deps, now, zone, output, print, example });
135
141
  const people = annotate(program.command("people").description("the people in the account's events"), {
136
142
  effects: GROUP,
137
143
  });
@@ -172,4 +178,69 @@ const addCalendarCommandsDefinition = (program, deps) => {
172
178
  print(await resolveAnswer(deps.store, review, choice, now()), command);
173
179
  });
174
180
  };
175
- export const addCalendarCommands = sharedCommand(addCalendarCommandsDefinition);
181
+ const collect = (value, previous = []) => [...previous, value];
182
+ const addEventWrites = (events, writes, { deps, now, zone, output, print, example }) => {
183
+ const context = (command) => ({
184
+ store: deps.store,
185
+ accountId: deps.accountId,
186
+ writes,
187
+ ask: terminalAsker(command),
188
+ now: now(),
189
+ zone: zone(),
190
+ names: { notify: "--notify or --no-notify", id: "--id", calendar: "--calendar", series: "--series" },
191
+ });
192
+ // Left out on purpose: with other attendees the owner must choose, so neither switch is the default.
193
+ const notify = (command) => command
194
+ .option("--notify", "the provider mails the other attendees; needs events.notify allowed")
195
+ .option("--no-notify", "the provider mails nobody; one of the two is required when there are other attendees");
196
+ const fields = (command, replacing) => command
197
+ .option("--title <title>", "the event's title")
198
+ .option("--start-time <time>", "when it starts: a time in your zone, or one with its own")
199
+ .option("--end-time <time>", "when it ends")
200
+ .option("--start-date <date>", "the first day of an all-day event, YYYY-MM-DD")
201
+ .option("--end-date <date>", "the last day of an all-day event")
202
+ .option("--location <text>", "where; empty clears it")
203
+ .option("--description <text>", "the details; empty clears it")
204
+ .option("--attendee <email>", `an attendee; repeat for each${replacing ? ", replacing the list" : ""}`, collect);
205
+ const fieldsOf = ({ attendee, ...given }) => ({
206
+ ...Object.fromEntries(Object.entries(given).filter(([, value]) => value !== undefined)),
207
+ ...(attendee === undefined ? {} : { attendees: attendee }),
208
+ });
209
+ const choicesOf = ({ calendar, series, notify }) => ({
210
+ ...(calendar === undefined ? {} : { calendar }),
211
+ ...(series ? { series: true } : {}),
212
+ ...(notify === undefined ? {} : { notify }),
213
+ });
214
+ const write = (name) => annotate(events.command(name), { effects: REMOTE_WRITE, offline: false });
215
+ const target = (command) => command
216
+ .argument("<event>", "the event's id, or event:<id>")
217
+ .option("--calendar <id>", "the calendar, when the event is on more than one of the account's");
218
+ output(example(notify(fields(write("create"), false)
219
+ .description("add an event to a calendar; one that repeats is made in the calendar itself")
220
+ .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 " +
221
+ "--attendee ana@example.com --no-notify")).action(async (calendar, opts, command) => {
222
+ const { notify: choice, id, ...rest } = opts;
223
+ print(await createEvent(context(command), calendar, fieldsOf(rest), {
224
+ ...(choice === undefined ? {} : { notify: choice }),
225
+ ...(id === undefined ? {} : { id }),
226
+ }), command);
227
+ });
228
+ output(example(notify(target(fields(write("update"), true))
229
+ .description("change an event; with --series every occurrence of the repeating event")
230
+ .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) => {
231
+ const { calendar: _calendar, series: _series, notify: _notify, ...rest } = opts;
232
+ print(await updateEvent(context(command), reference, fieldsOf(rest), choicesOf(opts)), command);
233
+ });
234
+ output(example(notify(target(write("delete"))
235
+ .description("delete an event; a whole repeating event needs events.delete.series allowed first")
236
+ .option("--series", "delete the whole repeating event, not only this occurrence")), "events delete event:42 --no-notify")).action(async (reference, opts, command) => {
237
+ print(await deleteEvent(context(command), reference, choicesOf(opts)), command);
238
+ });
239
+ output(example(notify(target(write("respond"))
240
+ .description("answer an invitation; the organiser is told only with --notify")
241
+ .addArgument(new Argument("<response>", "the answer").choices(EVENT_RESPONSES))), "events respond event:42 accepted --notify")).action(async (reference, response, opts, command) => {
242
+ print(await respondToEvent(context(command), reference, response, choicesOf(opts)), command);
243
+ });
244
+ };
245
+ // honoursDryRun: a host program other than `run` would otherwise let a write through under --dry-run.
246
+ export const addCalendarCommands = honoursDryRun(sharedCommand(addCalendarCommandsDefinition));
@@ -1,3 +1,5 @@
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
2
  export type { CalendarExpander, CalendarSource, CalendarSourceTraits, ChangePage, ChangeResult, SyncWindow, } from "./source.js";
3
3
  export { type CalendarCursor, type CalendarSyncOptions, type CalendarSyncReport, type CalendarSyncResult, calendarSyncWindow, parseCalendarCursor, syncCalendars, } from "./sync.js";
4
+ 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
+ export type { CalendarWrites } from "./writes.js";
@@ -1 +1,2 @@
1
1
  export { calendarSyncWindow, parseCalendarCursor, syncCalendars, } from "./sync.js";
2
+ export { CALENDAR_WRITE_DEFAULTS, CALENDAR_WRITE_KEYS, calendarConflict, EVENTS_DELETE_SERIES_KEY, EVENTS_NOTIFY_KEY, } from "./writer.js";