@wirecat/cli-core 0.457.0 → 0.459.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,66 @@ shape of this file.
7
7
 
8
8
  ## Unreleased
9
9
 
10
+ ## 0.459.0 — 27.10.2026
11
+
12
+ ### Changed — may break callers
13
+
14
+ - **A calendar event id is Google's base32hex: 5 to 1024 of `a-v` and `0-9`.** `events create --id` and
15
+ `event_create`'s `id` refused only characters outside `a-z0-9`, so an id with `w`-`z` reached Google and was refused
16
+ there ([Google's rule](https://developers.google.com/workspace/calendar/api/v3/reference/events#id)). The ids
17
+ cli-core mints were already valid. `calendarWriterContract` creates with `contract0sample0id0001` instead of an id
18
+ with `x`, so a writer that checks the rule itself passes it.
19
+ - **A command tool has one boolean for a `--x` / `--no-x` pair.** `toolsFromCommands` offered both `x` and `no_x`;
20
+ `no_x` is gone, and `x: false` passes `--no-x`, as it already did. A host that sends `no_notify` (or any other
21
+ `no_*` with its `--x` beside it) gets `no argument "no_notify"`; send `notify: false`.
22
+
23
+ ### Fixed
24
+
25
+ - **An agent's `events create` and `events update` tools take what the command line takes.** `attendee` is a list
26
+ (it was one string, so one invitee per create); `location` and `description` take `""`, which clears them as
27
+ `--location ""` does (they needed one character); `notify` is one boolean, required by the command when the event
28
+ has attendees other than the owner. Generally: a single-value option whose help says "empty clears" takes `""`
29
+ (except `-`, stdin), so `replies edit --words` now does too; the comma-separated lists still need a value.
30
+ - **A write's answer names the event's title.** `entry` (and each of `events show`'s `entries`) carries `title`; it
31
+ is written by whoever sent the invitation, so `callCalendarTool`'s `untrusted` names `entry.title` and
32
+ `entries.title`.
33
+
34
+ ## 0.458.0 — 27.10.2026
35
+
36
+ ### Added
37
+
38
+ - **Calendar writes (`@wirecat/cli-core/calendar`).** An optional `CalendarWriter` beside `CalendarSource`:
39
+ `create(calendar, draft, { id, notify })`, `update(calendar, externalId, patch, { scope, etag, notify })`,
40
+ `remove(calendar, externalId, { scope, etag, notify })` and `respond(calendar, externalId, response, { etag,
41
+ notify })`, in neutral words (`EventDraft`, `EventPatch`, `WriteScope` `this | series`, `EventResponse`). The id of a
42
+ create is chosen before the call and journaled as its `sendId`, so a retry after `outcome_unknown` with the same id
43
+ finds the event instead of making a second one. Every change sends the stored `etag`; an adapter turns a mismatch
44
+ into `calendarConflict()` (`validation_error`, reason `event.conflict`: sync and try again), never an overwrite. What
45
+ a write returns is saved at once; a deletion cancels the entry as a sync would. Repeating events are not created
46
+ here, and "this and following" is not offered.
47
+ - **`events create | update | delete | respond`** in `addCalendarCommands`, only when the CLI passes
48
+ `writes: CalendarWrites` (the writer, `app`, the profile's `GuardSettings`, and `extraKeys(write)` for keys the
49
+ CLI adds, such as gcal's `api.calendar.events.insert`). Each goes through `guardFor` and the send journal as a
50
+ messenger's writes do (WC-PERM-13, WC-PERM-17), and `--dry-run` previews it. Built-in levels
51
+ (`CALENDAR_WRITE_DEFAULTS`, WC-PERM-6/7): `events.create`, `events.update`, `events.respond` and `events.delete`
52
+ `ask`; `events.delete.series` `deny`; `events.notify` `deny`. Times are `--start-time`/`--end-time`, an all-day
53
+ event `--start-date`/`--end-date` (the last day). `--notify` or `--no-notify` is required when the event has
54
+ attendees other than the owner (reason `event.notify-required`), and `--notify` must pass `events.notify`.
55
+ `CALENDAR_WRITE_KEYS` lists the keys that are not command paths, for `commandPermissions({ extra })`.
56
+ `addCalendarCommands` now honours a global `--dry-run` in any host program, as cli-core's other shared commands do.
57
+ - **`calendarWriteTools`** (`event_create`, `event_update`, `event_delete`, `event_respond`) for `callCalendarTool`
58
+ with `writes: (accountId) => CalendarWrites`. Over MCP `ask` goes ahead (WC-MCP-7); `deny`, `readonly`,
59
+ `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
60
+ calendar or event; two make it ask for `account_id`.
61
+ - **Contract kit:** `fakeCalendarWriter` and `calendarWriterContract` (a stale etag is a conflict for update, remove
62
+ and respond; a create retried with its id finds the event; `notify` reaches the provider as given; with
63
+ `events.notify` at its default a create that would mail attendees never reaches the provider).
64
+
65
+ ### Changed — may break callers
66
+
67
+ - **`EventStore` has `entry(calendarId, externalId)`, and `StoredEntry` has `etag`.** A store of its own must add
68
+ both; the column already exists, so there is no migration.
69
+
10
70
  ## 0.457.0 — 27.10.2026
11
71
 
12
72
  ### 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,71 @@ 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; this or --no-notify is required " +
195
+ "when there are other attendees")
196
+ .option("--no-notify", "the provider mails nobody");
197
+ const fields = (command, replacing) => command
198
+ .option("--title <title>", "the event's title")
199
+ .option("--start-time <time>", "when it starts: a time in your zone, or one with its own")
200
+ .option("--end-time <time>", "when it ends")
201
+ .option("--start-date <date>", "the first day of an all-day event, YYYY-MM-DD")
202
+ .option("--end-date <date>", "the last day of an all-day event")
203
+ .option("--location <text>", "where; empty clears it")
204
+ .option("--description <text>", "the details; empty clears it")
205
+ // The empty default marks the flag as repeatable, so an agent's tool takes a list; empty is "not given".
206
+ .option("--attendee <email>", `an attendee; repeat for each${replacing ? ", replacing the list" : ""}`, collect, []);
207
+ const fieldsOf = ({ attendee, ...given }) => ({
208
+ ...Object.fromEntries(Object.entries(given).filter(([, value]) => value !== undefined)),
209
+ ...(attendee === undefined || attendee.length === 0 ? {} : { attendees: attendee }),
210
+ });
211
+ const choicesOf = ({ calendar, series, notify }) => ({
212
+ ...(calendar === undefined ? {} : { calendar }),
213
+ ...(series ? { series: true } : {}),
214
+ ...(notify === undefined ? {} : { notify }),
215
+ });
216
+ const write = (name) => annotate(events.command(name), { effects: REMOTE_WRITE, offline: false });
217
+ const target = (command) => command
218
+ .argument("<event>", "the event's id, or event:<id>")
219
+ .option("--calendar <id>", "the calendar, when the event is on more than one of the account's");
220
+ output(example(notify(fields(write("create"), false)
221
+ .description("add an event to a calendar; one that repeats is made in the calendar itself")
222
+ .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 " +
223
+ "--attendee ana@example.com --no-notify")).action(async (calendar, opts, command) => {
224
+ const { notify: choice, id, ...rest } = opts;
225
+ print(await createEvent(context(command), calendar, fieldsOf(rest), {
226
+ ...(choice === undefined ? {} : { notify: choice }),
227
+ ...(id === undefined ? {} : { id }),
228
+ }), command);
229
+ });
230
+ output(example(notify(target(fields(write("update"), true))
231
+ .description("change an event; with --series every occurrence of the repeating event")
232
+ .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) => {
233
+ const { calendar: _calendar, series: _series, notify: _notify, ...rest } = opts;
234
+ print(await updateEvent(context(command), reference, fieldsOf(rest), choicesOf(opts)), command);
235
+ });
236
+ output(example(notify(target(write("delete"))
237
+ .description("delete an event; a whole repeating event needs events.delete.series allowed first")
238
+ .option("--series", "delete the whole repeating event, not only this occurrence")), "events delete event:42 --no-notify")).action(async (reference, opts, command) => {
239
+ print(await deleteEvent(context(command), reference, choicesOf(opts)), command);
240
+ });
241
+ output(example(notify(target(write("respond"))
242
+ .description("answer an invitation; the organiser is told only with --notify")
243
+ .addArgument(new Argument("<response>", "the answer").choices(EVENT_RESPONSES))), "events respond event:42 accepted --notify")).action(async (reference, response, opts, command) => {
244
+ print(await respondToEvent(context(command), reference, response, choicesOf(opts)), command);
245
+ });
246
+ };
247
+ // honoursDryRun: a host program other than `run` would otherwise let a write through under --dry-run.
248
+ 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";