@wirecat/cli-core 0.538.0 → 0.540.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 +49 -0
- package/dist/calendar/busy.js +1 -1
- package/dist/calendar/cli/answers.d.ts +36 -0
- package/dist/calendar/cli/answers.js +17 -0
- package/dist/calendar/cli/index.d.ts +4 -0
- package/dist/calendar/cli/index.js +77 -12
- package/dist/calendar/ics.d.ts +55 -0
- package/dist/calendar/ics.js +314 -0
- package/dist/calendar/index.d.ts +1 -0
- package/dist/calendar/index.js +1 -0
- package/dist/calendar/mcp/index.js +2 -1
- package/dist/calendar/prepare.d.ts +2 -0
- package/dist/calendar/reads.d.ts +17 -4
- package/dist/calendar/reads.js +116 -27
- package/dist/calendar/testing/memory.js +5 -0
- package/dist/calendar/testing/writer.d.ts +1 -1
- package/dist/calendar/testing/writer.js +40 -0
- package/dist/calendar/writer.d.ts +15 -1
- package/dist/calendar/writer.js +2 -0
- package/dist/calendar/writes.d.ts +46 -0
- package/dist/calendar/writes.js +98 -0
- package/dist/cli/bot/mcp.js +2 -2
- package/dist/cli/effects.d.ts +14 -0
- package/dist/cli/effects.js +33 -2
- package/dist/cli/envelope.d.ts +4 -0
- package/dist/cli/index.d.ts +2 -2
- package/dist/cli/index.js +2 -2
- package/dist/cli/list-query.d.ts +8 -3
- package/dist/cli/list-query.js +21 -15
- package/dist/cli/messenger/store-maintenance-command.d.ts +1 -1
- package/dist/errors/reasons.js +7 -0
- package/dist/mcp/agent.js +12 -3
- package/dist/mcp/app-server.js +2 -2
- package/dist/mcp/server.js +2 -2
- package/dist/meetings/event-store.d.ts +6 -0
- package/dist/meetings/testing/event-store-contract.js +15 -0
- package/dist/store/sqlite/calendar-events.js +15 -0
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,55 @@ shape of this file.
|
|
|
7
7
|
|
|
8
8
|
## Unreleased
|
|
9
9
|
|
|
10
|
+
## 0.540.0 — 02.11.2026
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **`events export` without `--output` is a read over MCP and under the read-only lock.** It only prints the `.ics`
|
|
15
|
+
text, and MCP tools never offer `--output`. `writesFileOnlyWith(command, option)` marks a command whose only local
|
|
16
|
+
write is the file an owner-only option names: its effects stay a write (WC-CMD-17), `readsOverMcp` / `readsOverMcpAt`
|
|
17
|
+
count it as a read, and `<PREFIX>_READ_ONLY` refuses only a run that gives the option. Every MCP server now judges
|
|
18
|
+
`read` with `readsOverMcpAt`, which is `fetchesOnly` plus these; a CLI test that snapshots the catalogue with
|
|
19
|
+
`fetchesOnlyAt` should switch to it.
|
|
20
|
+
- **An MCP list's `query` input names its fields.** It said "the fields are below", and no Fields section is sent
|
|
21
|
+
over MCP. It now reads cli-base's `queryDescription`: the fields, enum values, and which fields `-field:value`
|
|
22
|
+
leaves out. `events list`'s query declares `event_type` as `negatable` (cli-base), and `negatableOf(registry)`
|
|
23
|
+
gives `nativeTermLists` the same list, so the description and the filter cannot drift. `queryArgument` uses the
|
|
24
|
+
same sentence.
|
|
25
|
+
- **`events list` and `events prepare` give `meta.now`, `meta.now_local` and `meta.timezone` (WC-TIME-7)**, from the
|
|
26
|
+
host's clock and zone, so an agent plans "tomorrow" from the real date.
|
|
27
|
+
|
|
28
|
+
### Changed — may break callers
|
|
29
|
+
|
|
30
|
+
- **cli-base 0.72.0.** `metaSchema` accepts `now` and `now_local`; a host that checks meta against an older cli-base's
|
|
31
|
+
strict schema drops the whole meta.
|
|
32
|
+
|
|
33
|
+
## 0.539.0 — 01.11.2026
|
|
34
|
+
|
|
35
|
+
### Added
|
|
36
|
+
|
|
37
|
+
- **`events create --text <line>`**: an event the provider reads from one line, such as 'Lunch with Ana tomorrow
|
|
38
|
+
1pm', when the writer has the new optional `CalendarWriter.quickAdd`. Checked as `events.create` and
|
|
39
|
+
`extraKeys("quick-add")`; takes no `--operation-id`, since the provider picks the id, and mails nobody without
|
|
40
|
+
`--notify`.
|
|
41
|
+
- **`events export [query…] [--output <file>]`**: the events `events list`'s query finds, as iCalendar (RFC 5545).
|
|
42
|
+
`--output` exists when the host passes `deps.writeFile`, and is left out of MCP tools: an agent gets the text.
|
|
43
|
+
`writeIcs` and `readIcs` are in `./calendar`.
|
|
44
|
+
- **`events import <calendar> <file>`**: an iCalendar file's events into a calendar, each under its UID, when the
|
|
45
|
+
writer has the new optional `CalendarWriter.importEvent` and the host passes `deps.readFile`. Its key `events.import`
|
|
46
|
+
asks by default (`CALENDAR_WRITE_DEFAULTS`).
|
|
47
|
+
- **`events list` takes `id:<id>` and `event_type:<kind>`**, and `-event_type:<kind>` or `NOT event_type:<kind>` to
|
|
48
|
+
leave a kind out, such as birthdays and working-location blocks; filtered in the store
|
|
49
|
+
(`EventFilter.id`, `eventType`, `notEventTypes`). `nativeTermLists` takes the fields a list lets the query exclude.
|
|
50
|
+
- **A calendar by its title**: `calendar:`, `<calendar>`, `--calendar` and `--to-calendar` take a calendar's title,
|
|
51
|
+
case aside, after its id and the provider's id. A title two calendars share is refused (`calendar.ambiguous`) with
|
|
52
|
+
their ids.
|
|
53
|
+
|
|
54
|
+
### Changed — may break callers
|
|
55
|
+
|
|
56
|
+
- `CalendarWrite` has `quick-add` and `import`: a `Record<CalendarWrite, …>`, such as a CLI's map of write to its API
|
|
57
|
+
keys, needs both.
|
|
58
|
+
|
|
10
59
|
## 0.538.0 — 01.11.2026
|
|
11
60
|
|
|
12
61
|
### Added
|
package/dist/calendar/busy.js
CHANGED
|
@@ -29,7 +29,7 @@ export const BUSY_QUERY = {
|
|
|
29
29
|
calendar: {
|
|
30
30
|
type: "kw",
|
|
31
31
|
example: "calendar:3",
|
|
32
|
-
relation: "one of the account's calendars, by its id
|
|
32
|
+
relation: "one of the account's calendars, by its id, the provider's id or its title; repeat for more",
|
|
33
33
|
},
|
|
34
34
|
person: { type: "kw", example: "person:ana@example.com", relation: "someone else, by address; repeat for more" },
|
|
35
35
|
},
|
|
@@ -236,6 +236,42 @@ export declare const calendarEventWrite: v.StrictObjectSchema<{
|
|
|
236
236
|
readonly hint: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
|
|
237
237
|
readonly untrusted: v.OptionalSchema<v.ArraySchema<v.StringSchema<undefined>, undefined>, undefined>;
|
|
238
238
|
}, undefined>;
|
|
239
|
+
export declare const calendarExport: v.StrictObjectSchema<{
|
|
240
|
+
readonly format: v.LiteralSchema<"ics", undefined>;
|
|
241
|
+
readonly events: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
|
|
242
|
+
/** The iCalendar text, when no `--output` took it. */
|
|
243
|
+
readonly content: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
|
|
244
|
+
readonly output: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
|
|
245
|
+
readonly untrusted: v.OptionalSchema<v.ArraySchema<v.StringSchema<undefined>, undefined>, undefined>;
|
|
246
|
+
}, undefined>;
|
|
247
|
+
export declare const calendarImport: v.StrictObjectSchema<{
|
|
248
|
+
readonly operation_id: v.StringSchema<undefined>;
|
|
249
|
+
readonly calendar: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>;
|
|
250
|
+
readonly imported: v.ArraySchema<v.StrictObjectSchema<{
|
|
251
|
+
readonly uid: v.StringSchema<undefined>;
|
|
252
|
+
readonly event: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
253
|
+
readonly entry: v.NullableSchema<v.StrictObjectSchema<{
|
|
254
|
+
readonly id: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>;
|
|
255
|
+
readonly calendar_id: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>;
|
|
256
|
+
readonly external_id: v.StringSchema<undefined>;
|
|
257
|
+
readonly kind: v.StringSchema<undefined>;
|
|
258
|
+
readonly title: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
259
|
+
readonly status: v.StringSchema<undefined>;
|
|
260
|
+
readonly event_type: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
261
|
+
readonly own_response: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
262
|
+
readonly starts_at: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, "an ISO 8601 time in UTC">, v.EndsWithAction<string, "Z", "an ISO 8601 time in UTC, ending in `Z`">]>, undefined>;
|
|
263
|
+
readonly ends_at: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, "an ISO 8601 time in UTC">, v.EndsWithAction<string, "Z", "an ISO 8601 time in UTC, ending in `Z`">]>, undefined>;
|
|
264
|
+
readonly join_url: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
265
|
+
readonly url: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
266
|
+
readonly deleted_at: v.NullableSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, "an ISO 8601 time in UTC">, v.EndsWithAction<string, "Z", "an ISO 8601 time in UTC, ending in `Z`">]>, undefined>;
|
|
267
|
+
}, undefined>, undefined>;
|
|
268
|
+
}, undefined>, undefined>;
|
|
269
|
+
readonly skipped: v.ArraySchema<v.StrictObjectSchema<{
|
|
270
|
+
readonly uid: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
271
|
+
readonly reason: v.StringSchema<undefined>;
|
|
272
|
+
}, undefined>, undefined>;
|
|
273
|
+
readonly untrusted: v.OptionalSchema<v.ArraySchema<v.StringSchema<undefined>, undefined>, undefined>;
|
|
274
|
+
}, undefined>;
|
|
239
275
|
export declare const calendarAttendee: v.StrictObjectSchema<{
|
|
240
276
|
readonly email: v.StringSchema<undefined>;
|
|
241
277
|
readonly name: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
@@ -175,6 +175,21 @@ export const calendarEventWrite = v.strictObject({
|
|
|
175
175
|
hint: v.optional(text),
|
|
176
176
|
untrusted,
|
|
177
177
|
});
|
|
178
|
+
export const calendarExport = v.strictObject({
|
|
179
|
+
format: v.literal("ics"),
|
|
180
|
+
events: count,
|
|
181
|
+
/** The iCalendar text, when no `--output` took it. */
|
|
182
|
+
content: v.optional(text),
|
|
183
|
+
output: v.optional(text),
|
|
184
|
+
untrusted: v.optional(v.array(text)),
|
|
185
|
+
});
|
|
186
|
+
export const calendarImport = v.strictObject({
|
|
187
|
+
operation_id: text,
|
|
188
|
+
calendar: id,
|
|
189
|
+
imported: v.array(v.strictObject({ uid: text, event: nullableText, entry: v.nullable(entry) })),
|
|
190
|
+
skipped: v.array(v.strictObject({ uid: nullableText, reason: text })),
|
|
191
|
+
untrusted,
|
|
192
|
+
});
|
|
178
193
|
export const calendarAttendee = v.strictObject({
|
|
179
194
|
...attendee,
|
|
180
195
|
/** How many of the listed events the person attends; only on the week's list. */
|
|
@@ -225,6 +240,8 @@ const ANSWERS = {
|
|
|
225
240
|
"events list": { name: "event" },
|
|
226
241
|
"events show": { name: "calendar_event", schema: calendarEvent },
|
|
227
242
|
"events prepare": { name: "calendar_event_preparation", schema: calendarEventPreparation },
|
|
243
|
+
"events export": { name: "calendar_export", schema: calendarExport },
|
|
244
|
+
"events import": { name: "calendar_import", schema: calendarImport },
|
|
228
245
|
"events create": { name: "calendar_event_write", schema: calendarEventWrite },
|
|
229
246
|
"events update": { name: "calendar_event_write", schema: calendarEventWrite },
|
|
230
247
|
"events delete": { name: "calendar_event_write", schema: calendarEventWrite },
|
|
@@ -39,6 +39,10 @@ export interface CalendarCommandDeps {
|
|
|
39
39
|
timezone?: string;
|
|
40
40
|
/** The provider's writer and the profile's guard: `events create|update|delete|respond` exist only with it. */
|
|
41
41
|
writes?: CalendarWrites;
|
|
42
|
+
/** Writes `events export --output`'s file; without it the export is printed only. */
|
|
43
|
+
writeFile?: (path: string, content: string) => Promise<void>;
|
|
44
|
+
/** Reads `events import`'s file; without it, and the writer's `import`, there is no `events import`. */
|
|
45
|
+
readFile?: (path: string) => Promise<string>;
|
|
42
46
|
/** A profile lock pins the process (WC-ACCT-5): `events list --source` may then read only the profile's account. */
|
|
43
47
|
locked?: boolean;
|
|
44
48
|
}
|
|
@@ -1,9 +1,11 @@
|
|
|
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 { clockMeta } from "@wirecat/cli-base/standards";
|
|
4
5
|
import { Argument, InvalidArgumentError, Option } from "commander";
|
|
5
6
|
import { environmentOf, runBudgetOf } from "../../cli/context.js";
|
|
6
7
|
import { honoursDryRun } from "../../cli/dry-run-guard.js";
|
|
8
|
+
import { ownerOnlyOption, writesFileOnlyWith } from "../../cli/effects.js";
|
|
7
9
|
import { queryTerms, withQuery } from "../../cli/list-query.js";
|
|
8
10
|
import { terminalAsker } from "../../cli/messenger/ask.js";
|
|
9
11
|
import { lineOf } from "../../cli/steps.js";
|
|
@@ -12,11 +14,11 @@ import { dateWindow } from "../../services/moment.js";
|
|
|
12
14
|
import { linkPeople } from "../../services/people.js";
|
|
13
15
|
import { BUSY_QUERY, BUSY_QUERY_NOTE, busyAnswer, busyQuery, workingHoursOf } from "../busy.js";
|
|
14
16
|
import { eventPreparation, nextMeeting, PREPARE_MESSAGES } from "../prepare.js";
|
|
15
|
-
import { accountsMeta, CALENDAR_UNTRUSTED_FIELDS, calendarIn, calendarItem, EVENTS_QUERY, EVENTS_QUERY_NOTE, eventAnswer, eventIdOf, eventOf, eventsAnswer, eventsFilter, peopleAnswer, resolveAnswer, reviewsAnswer, sourceAccounts, weekFilter, } from "../reads.js";
|
|
17
|
+
import { accountsMeta, CALENDAR_UNTRUSTED_FIELDS, calendarIn, calendarItem, EVENTS_QUERY, EVENTS_QUERY_NOTE, EXPORT_MAX, eventAnswer, eventIdOf, eventOf, eventsAnswer, eventsFilter, eventsIcs, peopleAnswer, resolveAnswer, reviewsAnswer, sourceAccounts, weekFilter, } from "../reads.js";
|
|
16
18
|
import { REPEAT_HELP } from "../recurrence.js";
|
|
17
19
|
import { bothSearchAnswer, serverSearchAnswer } from "../server-search.js";
|
|
18
20
|
import { calendarStatus, syncCalendars } from "../sync.js";
|
|
19
|
-
import { createEvent, deleteEvent, EVENT_RESPONSES, moveEvent, respondToEvent, updateEvent, } from "../writes.js";
|
|
21
|
+
import { createEvent, createEventFromText, deleteEvent, EVENT_RESPONSES, IMPORT_MAX, importEvents, moveEvent, respondToEvent, updateEvent, } from "../writes.js";
|
|
20
22
|
import { CALENDARS_QUERY, declareCalendarAnswers, PEOPLE_QUERY } from "./answers.js";
|
|
21
23
|
const LOCAL_READ = { remote: "none", local: "read" };
|
|
22
24
|
const LOCAL_WRITE = { remote: "none", local: "write" };
|
|
@@ -33,6 +35,9 @@ const ATTENDEE_UNTRUSTED = ["name"];
|
|
|
33
35
|
const REVIEW_UNTRUSTED = ["candidates.title"];
|
|
34
36
|
const SEARCH_UNTRUSTED = ["hits.title", "hits.location", "hits.excerpt"];
|
|
35
37
|
const SERVER_SEARCH_UNTRUSTED = ["hits.title", "hits.location"];
|
|
38
|
+
const IMPORT_UNTRUSTED = ["imported.uid", "imported.entry.title", "skipped.uid", "skipped.reason"];
|
|
39
|
+
/** The largest .ics file `events import` reads. */
|
|
40
|
+
const ICS_MAX_BYTES = 10 * 1024 * 1024;
|
|
36
41
|
const eachMarked = (answer, untrusted) => ({
|
|
37
42
|
...answer,
|
|
38
43
|
items: answer.items.map((item) => ({ ...item, untrusted })),
|
|
@@ -101,7 +106,7 @@ const addCalendarCommandsDefinition = (program, deps) => {
|
|
|
101
106
|
])
|
|
102
107
|
example(annotate(calendars.command(name), { effects: LOCAL_WRITE })
|
|
103
108
|
.description(synced ? "sync this calendar from now on" : "stop syncing this calendar; what is stored stays")
|
|
104
|
-
.argument("<calendar>", "the calendar's id,
|
|
109
|
+
.argument("<calendar>", "the calendar's id, the provider's id, or its title"), `calendars ${name} 3`).action(async (reference, _opts, command) => {
|
|
105
110
|
const calendar = await calendarIn(deps.store, deps.accountId, reference);
|
|
106
111
|
await deps.store.setCalendarSynced(calendar.id, synced, now());
|
|
107
112
|
print(calendarItem({ ...calendar, synced }), command);
|
|
@@ -231,9 +236,37 @@ const addCalendarCommandsDefinition = (program, deps) => {
|
|
|
231
236
|
});
|
|
232
237
|
print({
|
|
233
238
|
...eachMarked(await eventsAnswer(deps.store, filter && { ...filter, limit: opts.limit }, opts.page), EVENT_ITEM_UNTRUSTED),
|
|
234
|
-
meta: await accountsMeta(deps.store, accounts, skipped, now()),
|
|
239
|
+
meta: { ...(await accountsMeta(deps.store, accounts, skipped, now())), ...clockMeta(now(), zone()) },
|
|
235
240
|
}, command);
|
|
236
241
|
});
|
|
242
|
+
const exporter = example(withQuery(annotate(events.command("export"), { effects: deps.writeFile ? LOCAL_WRITE : LOCAL_READ }).description(`write stored events as iCalendar (.ics), at most ${EXPORT_MAX}: one with id:<id>, or those the query ` +
|
|
243
|
+
"finds; each occurrence of a repeating event as an event of its own"), EVENTS_QUERY).addHelpText("after", EVENTS_QUERY_NOTE), ...(deps.writeFile
|
|
244
|
+
? [
|
|
245
|
+
"events export id:42 --output planning.ics",
|
|
246
|
+
"events export starts_at:[2026-11-02 TO 2026-11-09} NOT event_type:birthday --output week.ics",
|
|
247
|
+
]
|
|
248
|
+
: ["events export id:42", "events export starts_at:[2026-11-02 TO 2026-11-09} NOT event_type:birthday"]));
|
|
249
|
+
// Not over MCP: an agent would choose where on the owner's disk to write. It gets the text in the answer.
|
|
250
|
+
if (deps.writeFile)
|
|
251
|
+
writesFileOnlyWith(exporter.addOption(ownerOnlyOption("--output <file>", "the file to write; printed in the answer when absent")), "output");
|
|
252
|
+
exporter.action(async (words, opts, command) => {
|
|
253
|
+
const filter = await eventsFilter(words, {
|
|
254
|
+
store: deps.store,
|
|
255
|
+
accountId: deps.accountId,
|
|
256
|
+
zone: zone(),
|
|
257
|
+
now: now(),
|
|
258
|
+
personOf: personId,
|
|
259
|
+
});
|
|
260
|
+
const exported = await eventsIcs(deps.store, deps.accountId, filter, { now: now(), product: program.name() });
|
|
261
|
+
if (opts.output === undefined) {
|
|
262
|
+
print({ ...exported, untrusted: ["content"] }, command);
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
if (!opts.output.trim())
|
|
266
|
+
throw new CliError("validation_error", "--output names no file");
|
|
267
|
+
await deps.writeFile?.(opts.output, exported.content);
|
|
268
|
+
print({ format: exported.format, events: exported.events, output: opts.output }, command);
|
|
269
|
+
});
|
|
237
270
|
example(annotate(events.command("show"), { effects: LOCAL_READ })
|
|
238
271
|
.description("one event: its calendar copies, attendees with their person, Drive files and history")
|
|
239
272
|
.argument("<event>", "the event's id, or event:<id>"), "events show event:42 --json").action(async (reference, _opts, command) => {
|
|
@@ -265,7 +298,7 @@ const addCalendarCommandsDefinition = (program, deps) => {
|
|
|
265
298
|
...(opts.limit === undefined ? {} : { limit: opts.limit }),
|
|
266
299
|
...(window.since === undefined ? {} : { since: window.since }),
|
|
267
300
|
});
|
|
268
|
-
print({ ...prepared, meta: { ...meta, ...window.meta } }, command);
|
|
301
|
+
print({ ...prepared, meta: { ...meta, ...window.meta, ...clockMeta(now(), zone()) } }, command);
|
|
269
302
|
});
|
|
270
303
|
const searches = annotate(program.command("search").description("find stored things by text: events"), {
|
|
271
304
|
effects: GROUP,
|
|
@@ -404,6 +437,7 @@ const addEventWrites = (events, writes, { deps, now, zone, print, example }) =>
|
|
|
404
437
|
// The empty default marks the flag as repeatable, so an agent's tool takes a list; empty is "not given".
|
|
405
438
|
.option("--attendee <email>", `an attendee; repeat for each${replacing ? ", replacing the list" : ""}`, collect, [])
|
|
406
439
|
.option("--repeat <rule>", `how it repeats${replacing ? ", with --series" : ""}: ${REPEAT_HELP}`);
|
|
440
|
+
const flagOf = (field) => field === "attendees" ? "--attendee" : `--${field.replace(/[A-Z]/gu, (letter) => `-${letter.toLowerCase()}`)}`;
|
|
407
441
|
const fieldsOf = ({ attendee, ...given }) => ({
|
|
408
442
|
...Object.fromEntries(Object.entries(given).filter(([, value]) => value !== undefined)),
|
|
409
443
|
...(attendee === undefined || attendee.length === 0 ? {} : { attendees: attendee }),
|
|
@@ -417,13 +451,30 @@ const addEventWrites = (events, writes, { deps, now, zone, print, example }) =>
|
|
|
417
451
|
const write = (name) => annotate(events.command(name), { effects: REMOTE_WRITE, offline: false });
|
|
418
452
|
const target = (command) => command
|
|
419
453
|
.argument("<event>", "the event's id, or event:<id>")
|
|
420
|
-
.option("--calendar <id>", "the calendar, when the event is on more than one of the account's");
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
.
|
|
454
|
+
.option("--calendar <id>", "the calendar, by id or title, when the event is on more than one of the account's");
|
|
455
|
+
const quick = writes.writer.quickAdd !== undefined;
|
|
456
|
+
const create = example(notify(fields(write("create"), false)
|
|
457
|
+
.description(quick
|
|
458
|
+
? "add an event to a calendar; with --repeat, one that repeats; with --text, one the provider reads " +
|
|
459
|
+
"from a line such as 'Lunch with Ana tomorrow 1pm'"
|
|
460
|
+
: "add an event to a calendar; with --repeat, one that repeats")
|
|
461
|
+
.argument("<calendar>", "the calendar's id, the provider's id, or its title")).option("--operation-id <id>", "the operation_id a create that ended in outcome_unknown named: a retry finds that event"), quick
|
|
462
|
+
? "events create Work --text 'Example lunch with Ana tomorrow 1pm at Example Cafe'"
|
|
463
|
+
: "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 " +
|
|
424
464
|
"--attendee ana@example.com --no-notify", "events create 3 --title 'Example standup' --start-time 2026-11-02T09:30 --end-time 2026-11-02T09:45 " +
|
|
425
|
-
"--repeat 'FREQ=WEEKLY;BYDAY=MO,WE,FR;COUNT=12'")
|
|
426
|
-
|
|
465
|
+
"--repeat 'FREQ=WEEKLY;BYDAY=MO,WE,FR;COUNT=12'");
|
|
466
|
+
if (quick)
|
|
467
|
+
create.option("--text <text>", "one line the provider reads the title, date, time and place from, in place of the other fields; nobody is " +
|
|
468
|
+
"mailed without --notify, and a retry after outcome_unknown may add the event twice");
|
|
469
|
+
create.action(async (calendar, opts, command) => {
|
|
470
|
+
const { notify: choice, operationId: id, text, ...rest } = opts;
|
|
471
|
+
if (text !== undefined) {
|
|
472
|
+
const given = Object.keys(fieldsOf(rest));
|
|
473
|
+
if (given.length || id !== undefined)
|
|
474
|
+
throw new CliError("validation_error", `--text takes the whole event from its line; leave out ${[...given.map(flagOf), ...(id === undefined ? [] : ["--operation-id"])].join(", ")}`, { reason: "event.text-with-fields", param: "--text" });
|
|
475
|
+
printWritten(await createEventFromText(context(command), calendar, text, choice === undefined ? {} : { notify: choice }), command);
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
427
478
|
printWritten(await createEvent(context(command), calendar, fieldsOf(rest), {
|
|
428
479
|
...(choice === undefined ? {} : { notify: choice }),
|
|
429
480
|
...(id === undefined ? {} : { id }),
|
|
@@ -449,10 +500,24 @@ const addEventWrites = (events, writes, { deps, now, zone, print, example }) =>
|
|
|
449
500
|
.option("--following", "delete this occurrence and every later one: the repeating event ends before it")), "events delete event:42 --no-notify", "events delete event:42 --following --no-notify").action(async (reference, opts, command) => {
|
|
450
501
|
printWritten(await deleteEvent(context(command), reference, choicesOf(opts)), command);
|
|
451
502
|
});
|
|
503
|
+
const readFile = deps.readFile;
|
|
504
|
+
if (writes.writer.importEvent && readFile)
|
|
505
|
+
example(write("import")
|
|
506
|
+
.description(`add the events of an iCalendar (.ics) file to a calendar, at most ${IMPORT_MAX}, each under its own UID; ` +
|
|
507
|
+
"nobody is mailed; a changed occurrence of a repeating event (RECURRENCE-ID) is skipped")
|
|
508
|
+
.argument("<calendar>", "the calendar's id, the provider's id, or its title")
|
|
509
|
+
.argument("<file>", "the .ics file to read"), "events import Work planning.ics").action(async (calendar, file, _opts, command) => {
|
|
510
|
+
const text = await readFile(file);
|
|
511
|
+
if (Buffer.byteLength(text, "utf8") > ICS_MAX_BYTES)
|
|
512
|
+
throw new CliError("validation_error", `${file} is larger than ${ICS_MAX_BYTES / 1024 / 1024} MB`, {
|
|
513
|
+
reason: "ics.too-large",
|
|
514
|
+
});
|
|
515
|
+
print({ ...(await importEvents(context(command), calendar, text)), untrusted: IMPORT_UNTRUSTED }, command);
|
|
516
|
+
});
|
|
452
517
|
if (writes.writer.move)
|
|
453
518
|
example(notify(target(write("move"))
|
|
454
519
|
.description("move an event, with its id, to another calendar of the account; a repeating one moves whole")
|
|
455
|
-
.requiredOption("--to-calendar <id>", "the calendar to move it to: its id,
|
|
520
|
+
.requiredOption("--to-calendar <id>", "the calendar to move it to: its id, the provider's id, or its title")
|
|
456
521
|
.option("--series", "move the whole repeating event an occurrence belongs to")), "events move event:42 --to-calendar 4 --no-notify").action(async (reference, opts, command) => {
|
|
457
522
|
const { series, calendar, notify } = opts;
|
|
458
523
|
printWritten(await moveEvent(context(command), reference, opts.toCalendar, choicesOf({ series, calendar, notify })), command);
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { EntryStatus, EntryTransparency, ParticipantResponse } from "../meetings/index.js";
|
|
2
|
+
/**
|
|
3
|
+
* The part of iCalendar (RFC 5545, https://www.rfc-editor.org/rfc/rfc5545) the calendar commands read and write: one
|
|
4
|
+
* VCALENDAR of VEVENTs, with no VTIMEZONE. Times are written in UTC; a time read with a TZID keeps that zone's name,
|
|
5
|
+
* which the provider takes as it is.
|
|
6
|
+
*/
|
|
7
|
+
export interface IcsEvent {
|
|
8
|
+
uid: string;
|
|
9
|
+
title: string | null;
|
|
10
|
+
description?: string | null;
|
|
11
|
+
location?: string | null;
|
|
12
|
+
/** A timed event, epoch ms, end exclusive. */
|
|
13
|
+
startsAt?: number;
|
|
14
|
+
endsAt?: number;
|
|
15
|
+
/** An all-day event, `YYYY-MM-DD`, end exclusive. */
|
|
16
|
+
startDate?: string;
|
|
17
|
+
endDate?: string;
|
|
18
|
+
/** The zone a read time named (`TZID`), or the reader's zone for a floating one. */
|
|
19
|
+
timezone?: string | null;
|
|
20
|
+
/** RRULE, RDATE and EXDATE lines, as written. */
|
|
21
|
+
recurrence?: string[];
|
|
22
|
+
status?: EntryStatus | null;
|
|
23
|
+
transparency?: EntryTransparency | null;
|
|
24
|
+
organizer?: {
|
|
25
|
+
email: string;
|
|
26
|
+
name?: string | null;
|
|
27
|
+
} | null;
|
|
28
|
+
attendees?: {
|
|
29
|
+
email: string;
|
|
30
|
+
name?: string | null;
|
|
31
|
+
response?: ParticipantResponse | null;
|
|
32
|
+
optional?: boolean;
|
|
33
|
+
}[];
|
|
34
|
+
url?: string | null;
|
|
35
|
+
}
|
|
36
|
+
/** Lines longer than 75 octets go on under a leading space, never splitting a character's UTF-8 bytes. */
|
|
37
|
+
export declare const foldLine: (line: string) => string;
|
|
38
|
+
/** One VCALENDAR holding the events, CRLF line ends and folded lines, as RFC 5545 §3.1 asks. */
|
|
39
|
+
export declare const writeIcs: (events: readonly IcsEvent[], options: {
|
|
40
|
+
now: number;
|
|
41
|
+
product: string;
|
|
42
|
+
}) => string;
|
|
43
|
+
/** An event the reader could not take, by its UID when it had one. */
|
|
44
|
+
export interface IcsSkip {
|
|
45
|
+
uid: string | null;
|
|
46
|
+
reason: string;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* The VEVENTs of an iCalendar text; times with neither `Z` nor a TZID are read in `zone`. An event that cannot be
|
|
50
|
+
* imported alone is in `skipped` with the reason; a text with no VCALENDAR is refused.
|
|
51
|
+
*/
|
|
52
|
+
export declare const readIcs: (text: string, zone: string) => {
|
|
53
|
+
events: IcsEvent[];
|
|
54
|
+
skipped: IcsSkip[];
|
|
55
|
+
};
|