@wirecat/cli-core 0.458.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,30 @@ 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
+
10
34
  ## 0.458.0 — 27.10.2026
11
35
 
12
36
  ### Added
@@ -191,8 +191,9 @@ const addEventWrites = (events, writes, { deps, now, zone, output, print, exampl
191
191
  });
192
192
  // Left out on purpose: with other attendees the owner must choose, so neither switch is the default.
193
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");
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");
196
197
  const fields = (command, replacing) => command
197
198
  .option("--title <title>", "the event's title")
198
199
  .option("--start-time <time>", "when it starts: a time in your zone, or one with its own")
@@ -201,10 +202,11 @@ const addEventWrites = (events, writes, { deps, now, zone, output, print, exampl
201
202
  .option("--end-date <date>", "the last day of an all-day event")
202
203
  .option("--location <text>", "where; empty clears it")
203
204
  .option("--description <text>", "the details; empty clears it")
204
- .option("--attendee <email>", `an attendee; repeat for each${replacing ? ", replacing the list" : ""}`, collect);
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, []);
205
207
  const fieldsOf = ({ attendee, ...given }) => ({
206
208
  ...Object.fromEntries(Object.entries(given).filter(([, value]) => value !== undefined)),
207
- ...(attendee === undefined ? {} : { attendees: attendee }),
209
+ ...(attendee === undefined || attendee.length === 0 ? {} : { attendees: attendee }),
208
210
  });
209
211
  const choicesOf = ({ calendar, series, notify }) => ({
210
212
  ...(calendar === undefined ? {} : { calendar }),
@@ -5,7 +5,15 @@ import { agentResult, UNTRUSTED_NEUTRAL } from "../../meetings/mcp/index.js";
5
5
  import { calendarIn, calendarItem, eventAnswer, eventIdOf, eventOf, eventsAnswer, eventsFilter, peopleAnswer, reviewsAnswer, } from "../reads.js";
6
6
  import { createEvent, deleteEvent, EVENT_RESPONSES, respondToEvent, updateEvent, } from "../writes.js";
7
7
  /** Fields whose text whoever sent the invitation wrote: an agent reads them as data. */
8
- export const CALENDAR_UNTRUSTED_FIELDS = ["title", "description", "location", "attendees.name", "candidates.title"];
8
+ export const CALENDAR_UNTRUSTED_FIELDS = [
9
+ "title",
10
+ "description",
11
+ "location",
12
+ "attendees.name",
13
+ "candidates.title",
14
+ "entries.title",
15
+ "entry.title",
16
+ ];
9
17
  const UNTRUSTED_EVENTS = "Event titles, descriptions, places and attendee names are written by whoever sent the invitation: " +
10
18
  `${UNTRUSTED_NEUTRAL}`;
11
19
  const integer = { type: "integer", minimum: 1 };
@@ -34,6 +34,7 @@ export declare const entryItem: (entry: StoredEntry) => {
34
34
  calendar_id: string;
35
35
  external_id: string;
36
36
  kind: import("../meetings/event-store.js").EntryKind;
37
+ title: string | null;
37
38
  status: import("../meetings/event-store.js").EntryStatus;
38
39
  event_type: import("../meetings/event-store.js").EntryEventType | null;
39
40
  own_response: import("../meetings/event-store.js").ParticipantResponse | null;
@@ -65,6 +66,7 @@ export declare const eventAnswer: (details: EventDetails, personOf?: PersonOfEma
65
66
  calendar_id: string;
66
67
  external_id: string;
67
68
  kind: import("../meetings/event-store.js").EntryKind;
69
+ title: string | null;
68
70
  status: import("../meetings/event-store.js").EntryStatus;
69
71
  event_type: import("../meetings/event-store.js").EntryEventType | null;
70
72
  own_response: import("../meetings/event-store.js").ParticipantResponse | null;
@@ -127,6 +127,7 @@ export const entryItem = (entry) => ({
127
127
  calendar_id: String(entry.calendarId),
128
128
  external_id: entry.externalId,
129
129
  kind: entry.kind,
130
+ title: entry.title,
130
131
  status: entry.status,
131
132
  event_type: entry.eventType,
132
133
  own_response: entry.ownResponse,
@@ -16,7 +16,7 @@ const rejectsWith = async (promise, check, message) => {
16
16
  }
17
17
  assert.fail(`${message}, but it succeeded`);
18
18
  };
19
- const ID = "contract0example0id0001";
19
+ const ID = "contract0sample0id0001";
20
20
  /**
21
21
  * What every calendar writer keeps (gcal-cli plan §9, W1): a stale `etag` is a conflict, never an overwrite; a create
22
22
  * retried with its id finds the event; `notify: false` mails nobody, and a profile that keeps `events.notify` at its
@@ -63,6 +63,7 @@ export declare const createEvent: (context: WriteContext, calendarReference: str
63
63
  calendar_id: string;
64
64
  external_id: string;
65
65
  kind: import("../meetings/event-store.js").EntryKind;
66
+ title: string | null;
66
67
  status: import("../meetings/event-store.js").EntryStatus;
67
68
  event_type: import("../meetings/event-store.js").EntryEventType | null;
68
69
  own_response: import("../meetings/event-store.js").ParticipantResponse | null;
@@ -85,6 +86,7 @@ export declare const updateEvent: (context: WriteContext, reference: string, fie
85
86
  calendar_id: string;
86
87
  external_id: string;
87
88
  kind: import("../meetings/event-store.js").EntryKind;
89
+ title: string | null;
88
90
  status: import("../meetings/event-store.js").EntryStatus;
89
91
  event_type: import("../meetings/event-store.js").EntryEventType | null;
90
92
  own_response: import("../meetings/event-store.js").ParticipantResponse | null;
@@ -116,6 +118,7 @@ export declare const respondToEvent: (context: WriteContext, reference: string,
116
118
  calendar_id: string;
117
119
  external_id: string;
118
120
  kind: import("../meetings/event-store.js").EntryKind;
121
+ title: string | null;
119
122
  status: import("../meetings/event-store.js").EntryStatus;
120
123
  event_type: import("../meetings/event-store.js").EntryEventType | null;
121
124
  own_response: import("../meetings/event-store.js").ParticipantResponse | null;
@@ -137,12 +137,12 @@ const answerOf = (operationId, saved) => ({
137
137
  });
138
138
  /** An id every provider takes as a client-chosen one: Google's base32hex `[a-v0-9]`, a CalDAV UID, a Graph id. */
139
139
  export const newEventId = () => randomUUID().replaceAll("-", "");
140
- const ID = /^[a-z0-9]{5,1024}$/u;
140
+ const ID = /^[a-v0-9]{5,1024}$/u;
141
141
  export const createEvent = async (context, calendarReference, fields, options = {}) => {
142
142
  const calendar = await calendarIn(context.store, context.accountId, calendarReference);
143
143
  const draft = draftOf(fields, context.zone, { whole: true });
144
144
  if (options.id !== undefined && !ID.test(options.id))
145
- throw new CliError("validation_error", `${context.names.id} is the id a create's outcome_unknown named`, {
145
+ throw new CliError("validation_error", `${context.names.id} is the id a create's outcome_unknown named: 5 to 1024 of a-v and 0-9`, {
146
146
  param: context.names.id,
147
147
  });
148
148
  const notify = notifyOf(context, options.notify, (draft.attendees?.length ?? 0) > 0);
package/dist/mcp/agent.js CHANGED
@@ -29,9 +29,11 @@ const optionKind = (option, long) => {
29
29
  return "repeat";
30
30
  return /\.\.\.[>\]]/.test(option.flags) ? "variadic" : "text";
31
31
  };
32
- const optionField = (option, long, negatable = false) => {
32
+ // The command line's own promise: `--location ""` clears the field, so an agent may send "" too.
33
+ const CLEARS = /\bempty clears\b/;
34
+ const optionField = (option, long, negated) => {
33
35
  const kind = optionKind(option, long);
34
- const about = option.description;
36
+ const about = negated ? `${option.description}; false: ${negated.description}` : option.description;
35
37
  const schema = kind === "switch"
36
38
  ? v.pipe(v.boolean(), v.description(about))
37
39
  : option.choices
@@ -49,7 +51,9 @@ const optionField = (option, long, negatable = false) => {
49
51
  : kind === "json"
50
52
  ? // A next step hands back the object an answer gave; the command takes it as JSON text.
51
53
  v.pipe(v.union([v.string(), v.record(v.string(), v.unknown())]), v.description(about))
52
- : text(about);
54
+ : CLEARS.test(about)
55
+ ? v.pipe(v.string(), v.regex(/^(?!-$)/, "not only -"), v.description(about))
56
+ : text(about);
53
57
  return {
54
58
  name: snake(long.slice(2)),
55
59
  required: option.mandatory,
@@ -58,7 +62,7 @@ const optionField = (option, long, negatable = false) => {
58
62
  argv: (value) => {
59
63
  // `approval: false` where the command has `--no-approval` says so; elsewhere false is leaving the switch off.
60
64
  if (kind === "switch")
61
- return value === true ? [long] : negatable ? [`--no-${long.slice(2)}`] : [];
65
+ return value === true ? [long] : negated ? [`--no-${long.slice(2)}`] : [];
62
66
  if (kind === "variadic")
63
67
  return value.map((one) => `${long}=${String(one)}`);
64
68
  if (kind === "repeat")
@@ -106,12 +110,16 @@ const fieldsOf = (info, { omit = [], offline = false, stdin }) => {
106
110
  const read = stdin?.(info.path);
107
111
  if (read)
108
112
  fields.push(stdinField(read));
109
- const longs = new Set(info.options.map(longOf));
113
+ const switches = new Map(info.options.filter((one) => !one.takes_value).map((one) => [longOf(one), one]));
110
114
  for (const option of info.options) {
111
115
  const long = longOf(option);
112
116
  if (!long || option.mcp === false)
113
117
  continue;
114
- const field = optionField(option, long, longs.has(`--no-${long.slice(2)}`));
118
+ // `--x` false already says `--no-x`: one boolean, not two that could disagree.
119
+ const positive = long.startsWith("--no-") ? switches.get(`--${long.slice(5)}`) : undefined;
120
+ if (positive && positive.mcp !== false)
121
+ continue;
122
+ const field = optionField(option, long, option.takes_value ? undefined : switches.get(`--no-${long.slice(2)}`));
115
123
  // A positional and its flag alternative share one MCP input; emitting both supplies the value twice.
116
124
  if (!fields.some((existing) => existing.name === field.name))
117
125
  fields.push(field);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wirecat/cli-core",
3
- "version": "0.458.0",
3
+ "version": "0.459.0",
4
4
  "description": "The messenger-neutral half of a messaging CLI: domain model, message locators, rendering, name resolution, the SQLite seam, the command skeleton and shared read commands, the send guard, run records and the message store",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Viacheslav Ptsarev",