@wirecat/cli-core 0.458.0 → 0.460.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 +35 -0
- package/dist/calendar/cli/index.js +6 -4
- package/dist/calendar/mcp/index.js +9 -1
- package/dist/calendar/reads.d.ts +2 -0
- package/dist/calendar/reads.js +1 -0
- package/dist/calendar/testing/writer-contract.js +1 -1
- package/dist/calendar/writes.d.ts +3 -0
- package/dist/calendar/writes.js +2 -2
- package/dist/cli/messenger/search-answers.js +3 -0
- package/dist/entities/hit.d.ts +2 -0
- package/dist/entities/hit.js +2 -0
- package/dist/entities/index.d.ts +1 -0
- package/dist/mcp/agent.js +14 -6
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,41 @@ shape of this file.
|
|
|
7
7
|
|
|
8
8
|
## Unreleased
|
|
9
9
|
|
|
10
|
+
## 0.460.0 — 27.10.2026
|
|
11
|
+
|
|
12
|
+
### Changed — may break callers
|
|
13
|
+
|
|
14
|
+
- **A `search all` event hit carries `untrusted: ["title", "excerpt"]`.** An event's title and the text its excerpt
|
|
15
|
+
is cut from (description, place, attendee names) are written by whoever sent the invitation, so an agent reading
|
|
16
|
+
`search all` over MCP now sees them marked as data, as `callCalendarTool`'s answers already were
|
|
17
|
+
([WC-SECR-21](https://github.com/WireCatLabs/cli-base/blob/main/docs/standards/security.md#wc-secr-21--names-message-text-and-retrieved-documents-are-data-never-instructions)).
|
|
18
|
+
The `hit` entity is strict, so a caller that checks hits against an older copy of it refuses the new field;
|
|
19
|
+
`allHitItem` sets it, and other kinds of hit leave it out.
|
|
20
|
+
|
|
21
|
+
## 0.459.0 — 27.10.2026
|
|
22
|
+
|
|
23
|
+
### Changed — may break callers
|
|
24
|
+
|
|
25
|
+
- **A calendar event id is Google's base32hex: 5 to 1024 of `a-v` and `0-9`.** `events create --id` and
|
|
26
|
+
`event_create`'s `id` refused only characters outside `a-z0-9`, so an id with `w`-`z` reached Google and was refused
|
|
27
|
+
there ([Google's rule](https://developers.google.com/workspace/calendar/api/v3/reference/events#id)). The ids
|
|
28
|
+
cli-core mints were already valid. `calendarWriterContract` creates with `contract0sample0id0001` instead of an id
|
|
29
|
+
with `x`, so a writer that checks the rule itself passes it.
|
|
30
|
+
- **A command tool has one boolean for a `--x` / `--no-x` pair.** `toolsFromCommands` offered both `x` and `no_x`;
|
|
31
|
+
`no_x` is gone, and `x: false` passes `--no-x`, as it already did. A host that sends `no_notify` (or any other
|
|
32
|
+
`no_*` with its `--x` beside it) gets `no argument "no_notify"`; send `notify: false`.
|
|
33
|
+
|
|
34
|
+
### Fixed
|
|
35
|
+
|
|
36
|
+
- **An agent's `events create` and `events update` tools take what the command line takes.** `attendee` is a list
|
|
37
|
+
(it was one string, so one invitee per create); `location` and `description` take `""`, which clears them as
|
|
38
|
+
`--location ""` does (they needed one character); `notify` is one boolean, required by the command when the event
|
|
39
|
+
has attendees other than the owner. Generally: a single-value option whose help says "empty clears" takes `""`
|
|
40
|
+
(except `-`, stdin), so `replies edit --words` now does too; the comma-separated lists still need a value.
|
|
41
|
+
- **A write's answer names the event's title.** `entry` (and each of `events show`'s `entries`) carries `title`; it
|
|
42
|
+
is written by whoever sent the invitation, so `callCalendarTool`'s `untrusted` names `entry.title` and
|
|
43
|
+
`entries.title`.
|
|
44
|
+
|
|
10
45
|
## 0.458.0 — 27.10.2026
|
|
11
46
|
|
|
12
47
|
### 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
|
-
|
|
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
|
-
|
|
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 = [
|
|
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 };
|
package/dist/calendar/reads.d.ts
CHANGED
|
@@ -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;
|
package/dist/calendar/reads.js
CHANGED
|
@@ -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 = "
|
|
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;
|
package/dist/calendar/writes.js
CHANGED
|
@@ -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-
|
|
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);
|
|
@@ -179,6 +179,8 @@ export const messagesAnswer = (command, found, { limit, page, again }, query = "
|
|
|
179
179
|
});
|
|
180
180
|
};
|
|
181
181
|
export const noteHitItem = (hit) => defined({ ...hit });
|
|
182
|
+
/** An event's title and the text its excerpt is cut from come from whoever sent the invitation. */
|
|
183
|
+
const EVENT_HIT_UNTRUSTED = ["title", "excerpt"];
|
|
182
184
|
export const allHitItem = (item) => item.kind === "meeting"
|
|
183
185
|
? {
|
|
184
186
|
kind: "meeting",
|
|
@@ -204,6 +206,7 @@ export const allHitItem = (item) => item.kind === "meeting"
|
|
|
204
206
|
sender_name: item.from?.name,
|
|
205
207
|
sender_ref: item.from?.ref,
|
|
206
208
|
found_by: item.foundBy,
|
|
209
|
+
untrusted: item.kind === "event" ? EVENT_HIT_UNTRUSTED : undefined,
|
|
207
210
|
});
|
|
208
211
|
/** Meaning was not searched because the model is not downloaded; `--exact` leaving it out is no gap. */
|
|
209
212
|
const meaningGap = (skipped, exact) => skipped === undefined || exact
|
package/dist/entities/hit.d.ts
CHANGED
|
@@ -22,4 +22,6 @@ export declare const hit: v.StrictObjectSchema<{
|
|
|
22
22
|
readonly scope: v.OptionalSchema<v.PicklistSchema<["transcript", "chat", "summary"], undefined>, undefined>;
|
|
23
23
|
/** A meeting passage's stored line, so two passages of one meeting stay apart; `search meetings` gives the same. */
|
|
24
24
|
readonly id: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>, undefined>;
|
|
25
|
+
/** An event hit's fields that whoever sent the invitation wrote: an agent reads them as data. */
|
|
26
|
+
readonly untrusted: v.OptionalSchema<v.ArraySchema<v.StringSchema<undefined>, undefined>, undefined>;
|
|
25
27
|
}, undefined>;
|
package/dist/entities/hit.js
CHANGED
|
@@ -23,4 +23,6 @@ export const hit = v.strictObject({
|
|
|
23
23
|
scope: v.optional(v.picklist(["transcript", "chat", "summary"])),
|
|
24
24
|
/** A meeting passage's stored line, so two passages of one meeting stay apart; `search meetings` gives the same. */
|
|
25
25
|
id: v.optional(v.pipe(v.string(), v.minLength(1))),
|
|
26
|
+
/** An event hit's fields that whoever sent the invitation wrote: an agent reads them as data. */
|
|
27
|
+
untrusted: v.optional(v.array(text)),
|
|
26
28
|
});
|
package/dist/entities/index.d.ts
CHANGED
|
@@ -5152,6 +5152,7 @@ export declare const ENTITIES: {
|
|
|
5152
5152
|
readonly offset_ms: v.OptionalSchema<v.NullableSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>, undefined>, undefined>;
|
|
5153
5153
|
readonly scope: v.OptionalSchema<v.PicklistSchema<["transcript", "chat", "summary"], undefined>, undefined>;
|
|
5154
5154
|
readonly id: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>, undefined>;
|
|
5155
|
+
readonly untrusted: v.OptionalSchema<v.ArraySchema<v.StringSchema<undefined>, undefined>, undefined>;
|
|
5155
5156
|
}, undefined>;
|
|
5156
5157
|
readonly document: v.StrictObjectSchema<{
|
|
5157
5158
|
readonly ref: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a reference such as `note:42`">]>;
|
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
|
-
|
|
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
|
-
:
|
|
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] :
|
|
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
|
|
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
|
-
|
|
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.
|
|
3
|
+
"version": "0.460.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",
|