@wirecat/cli-core 0.456.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,50 @@ 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
+
46
+ ## 0.457.0 — 27.10.2026
47
+
48
+ ### Fixed
49
+
50
+ - **`sync --full` lists the whole window again.** Under the shared program its global `--full` took the flag before
51
+ the calendar `sync` read it, so `sync --full` quietly read only the changes. `sync` now reads the flag with the
52
+ program's globals.
53
+
10
54
  ## 0.456.0 — 27.10.2026
11
55
 
12
56
  ### Added
@@ -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;
@@ -76,10 +80,12 @@ const addCalendarCommandsDefinition = (program, deps) => {
76
80
  .option("--full", "list every synced calendar's window again instead of reading changes")
77
81
  .option("--embed", "also index new events for meaning search, with a model already downloaded")
78
82
  .action(async (opts, command) => {
83
+ // The shared program has a global --full of its own, which claims the flag before this command sees it.
84
+ const full = command.optsWithGlobals().full === true;
79
85
  const report = await syncCalendars(source(), deps.store, {
80
86
  accountId: deps.accountId,
81
87
  now: now(),
82
- ...(opts.full ? { full: true } : {}),
88
+ ...(full ? { full: true } : {}),
83
89
  ...(deps.expand ? { expand: deps.expand } : {}),
84
90
  });
85
91
  if (opts.embed && !deps.embed)
@@ -130,6 +136,8 @@ const addCalendarCommandsDefinition = (program, deps) => {
130
136
  ...(opts.newest ? { newest: true } : {}),
131
137
  }), command);
132
138
  });
139
+ if (deps.writes)
140
+ addEventWrites(events, deps.writes, { deps, now, zone, output, print, example });
133
141
  const people = annotate(program.command("people").description("the people in the account's events"), {
134
142
  effects: GROUP,
135
143
  });
@@ -170,4 +178,69 @@ const addCalendarCommandsDefinition = (program, deps) => {
170
178
  print(await resolveAnswer(deps.store, review, choice, now()), command);
171
179
  });
172
180
  };
173
- 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";