@wirecat/cli-core 0.564.0 → 0.566.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 +76 -0
- package/constants.json +13 -0
- package/dist/calendar/busy.d.ts +10 -10
- package/dist/calendar/busy.js +9 -9
- package/dist/calendar/cli/answers.d.ts +7 -8
- package/dist/calendar/cli/answers.js +4 -5
- package/dist/calendar/cli/bodies.d.ts +33 -0
- package/dist/calendar/cli/bodies.js +52 -0
- package/dist/calendar/cli/files.d.ts +8 -0
- package/dist/calendar/cli/files.js +41 -0
- package/dist/calendar/cli/index.d.ts +58 -51
- package/dist/calendar/cli/index.js +311 -284
- package/dist/calendar/cli/remind.d.ts +2 -11
- package/dist/calendar/cli/remind.js +81 -81
- package/dist/calendar/cli/search-all.d.ts +3 -8
- package/dist/calendar/cli/search-all.js +47 -42
- package/dist/calendar/mcp/index.d.ts +13 -13
- package/dist/calendar/prepare.d.ts +1 -1
- package/dist/calendar/testing/answers.d.ts +27 -5
- package/dist/calendar/testing/answers.js +88 -17
- package/dist/calendar/testing/writer.d.ts +1 -0
- package/dist/calendar/testing/writer.js +6 -0
- package/dist/calendar/writer.d.ts +6 -1
- package/dist/calendar/writes.d.ts +3 -1
- package/dist/calendar/writes.js +42 -20
- package/dist/cli/app-doctor-command.d.ts +7 -13
- package/dist/cli/app-doctor-command.js +1 -1
- package/dist/cli/config-command.js +1 -1
- package/dist/cli/config-issues.d.ts +3 -1
- package/dist/cli/config-issues.js +2 -2
- package/dist/cli/contract-evidence.d.ts +1 -1
- package/dist/cli/early-refusal.d.ts +2 -1
- package/dist/cli/early-refusal.js +4 -2
- package/dist/cli/entry.d.ts +6 -2
- package/dist/cli/entry.js +4 -1
- package/dist/cli/envelope.d.ts +2 -2
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +1 -0
- package/dist/cli/messenger/store-maintenance-command.d.ts +1 -1
- package/dist/cli/permission-keys.d.ts +3 -1
- package/dist/cli/permission-keys.js +2 -1
- package/dist/cli/preview.d.ts +12 -0
- package/dist/cli/preview.js +8 -1
- package/dist/cli/settings.d.ts +2 -0
- package/dist/cli/settings.js +8 -1
- package/dist/constants/index.d.ts +83 -0
- package/dist/constants/index.js +77 -0
- package/dist/constants/obscure.d.ts +1 -0
- package/dist/constants/obscure.js +9 -0
- package/dist/errors/reasons.js +15 -1
- package/dist/mcp/meeting-preparation.d.ts +1 -1
- package/dist/mcp/meeting-project-candidates.d.ts +1 -1
- package/dist/mcp/meeting-series-inventory.d.ts +1 -1
- package/dist/meetings/mcp/passage.d.ts +1 -1
- package/dist/meetings/mcp/project-evidence.d.ts +1 -1
- package/dist/meetings/mcp/search.d.ts +1 -1
- package/dist/oauth/index.d.ts +1 -1
- package/dist/oauth/provider.d.ts +5 -0
- package/dist/oauth/storage.js +12 -2
- package/dist/services/metadata.d.ts +1 -1
- package/package.json +8 -3
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
import { CliError } from "@wirecat/cli-base";
|
|
2
|
-
import { annotate, sharedCommand } from "@wirecat/cli-base/commands";
|
|
2
|
+
import { annotate, metaOf, sharedCommand } from "@wirecat/cli-base/commands";
|
|
3
3
|
import { timezoneOf } from "@wirecat/cli-base/query";
|
|
4
4
|
import { clockMeta } from "@wirecat/cli-base/standards";
|
|
5
5
|
import { Argument, InvalidArgumentError, Option } from "commander";
|
|
6
6
|
import { environmentOf, outputFor, runBudgetOf } from "../../cli/context.js";
|
|
7
7
|
import { honoursDryRun } from "../../cli/dry-run-guard.js";
|
|
8
8
|
import { changesStore, ownerOnlyOption, writesFileOnlyWith } from "../../cli/effects.js";
|
|
9
|
+
import { renderAnswer } from "../../cli/envelope.js";
|
|
9
10
|
import { queryTerms, withQuery } from "../../cli/list-query.js";
|
|
10
11
|
import { terminalAsker } from "../../cli/messenger/ask.js";
|
|
11
12
|
import { lineOf } from "../../cli/steps.js";
|
|
12
13
|
import { secretInput } from "../../runs/run-context.js";
|
|
13
14
|
import { searchEvents } from "../../services/events-search.js";
|
|
14
15
|
import { dateWindow } from "../../services/moment.js";
|
|
16
|
+
import { embedEvents } from "../../services/notes-search.js";
|
|
15
17
|
import { linkPeople } from "../../services/people.js";
|
|
16
18
|
import { BUSY_QUERY, BUSY_QUERY_NOTE, busyAnswer, busyQuery, workingHoursOf } from "../busy.js";
|
|
17
19
|
import { isFeedCalendar, isPlainHttp, syncWithFeeds } from "../feeds.js";
|
|
@@ -22,6 +24,8 @@ import { bothSearchAnswer, serverSearchAnswer } from "../server-search.js";
|
|
|
22
24
|
import { calendarStatus } from "../sync.js";
|
|
23
25
|
import { createEvent, createEventFromText, deleteEvent, EVENT_RESPONSES, IMPORT_MAX, importEvents, moveEvent, respondToEvent, updateEvent, } from "../writes.js";
|
|
24
26
|
import { CALENDARS_QUERY, declareCalendarAnswers, PEOPLE_QUERY } from "./answers.js";
|
|
27
|
+
import { hitsBody, itemBody, listBody, messagesOf, pathOf } from "./bodies.js";
|
|
28
|
+
import { ICS_MAX_BYTES, readIcsFile, writeIcsFile } from "./files.js";
|
|
25
29
|
import { localTimesView } from "./local-times.js";
|
|
26
30
|
import { addRemind } from "./remind.js";
|
|
27
31
|
import { addSearchAll } from "./search-all.js";
|
|
@@ -41,8 +45,6 @@ const REVIEW_UNTRUSTED = ["candidates.title"];
|
|
|
41
45
|
const SEARCH_UNTRUSTED = ["hits.title", "hits.location", "hits.excerpt"];
|
|
42
46
|
const SERVER_SEARCH_UNTRUSTED = ["hits.title", "hits.location"];
|
|
43
47
|
const IMPORT_UNTRUSTED = ["imported.uid", "imported.entry.title", "skipped.uid", "skipped.reason"];
|
|
44
|
-
/** The largest .ics file `events import` reads. */
|
|
45
|
-
const ICS_MAX_BYTES = 10 * 1024 * 1024;
|
|
46
48
|
const eachMarked = (answer, untrusted) => ({
|
|
47
49
|
...answer,
|
|
48
50
|
items: answer.items.map((item) => ({ ...item, untrusted })),
|
|
@@ -64,47 +66,42 @@ const minutes = (text) => {
|
|
|
64
66
|
throw new InvalidArgumentError("expected 5 to 1440 minutes");
|
|
65
67
|
return value;
|
|
66
68
|
};
|
|
67
|
-
|
|
69
|
+
/** A search tops up its pending word index before it answers, which is a write; every other read only reads. */
|
|
70
|
+
const readsOnly = (command) => metaOf(command).effects?.local !== "write" && pathOf(command)[0] !== "search";
|
|
71
|
+
const addCalendarCommandsDefinition = (program, options) => {
|
|
68
72
|
const mounted = new Set(program.commands);
|
|
69
|
-
const now = () =>
|
|
70
|
-
const zone = () => timezoneOf(
|
|
71
|
-
const
|
|
72
|
-
const
|
|
73
|
-
|
|
73
|
+
const now = () => options.now?.() ?? Date.now();
|
|
74
|
+
const zone = () => timezoneOf(options.timezone);
|
|
75
|
+
const answer = (command, body, view) => {
|
|
76
|
+
const output = outputFor(command);
|
|
77
|
+
renderAnswer(command, output, body, view ?? ((shown) => localTimesView(shown, zone(), output.color)));
|
|
74
78
|
};
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
? async (email) => {
|
|
82
|
-
const person = await messages("a person").people.personOf({ provider: "email", id: email });
|
|
83
|
-
return person ? `person:${person.uid}` : null;
|
|
79
|
+
/** Opens the profile for `work` and closes it before the answer prints, also when `work` fails. */
|
|
80
|
+
const within = async (command, work, view) => {
|
|
81
|
+
const run = await options.open(command, { readOnly: readsOnly(command) });
|
|
82
|
+
let body;
|
|
83
|
+
try {
|
|
84
|
+
body = await work(run);
|
|
84
85
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
return Number(text);
|
|
90
|
-
const person = await messages("person:").people.personOf({ provider: "email", id: text.trim().toLowerCase() });
|
|
91
|
-
return person ? Number(person.uid) : null;
|
|
92
|
-
};
|
|
93
|
-
const example = (command, ...lines) => annotate(command, { examples: lines.map((line) => `${program.name()} personal ${line}`) });
|
|
94
|
-
const source = () => {
|
|
95
|
-
if (!deps.source)
|
|
96
|
-
throw new CliError("configuration_error", "the calendar provider is unavailable");
|
|
97
|
-
return deps.source;
|
|
86
|
+
finally {
|
|
87
|
+
await run.close?.();
|
|
88
|
+
}
|
|
89
|
+
answer(command, body, view);
|
|
98
90
|
};
|
|
91
|
+
const example = (command, ...lines) => annotate(command, { examples: lines.map((line) => `${options.app.command} personal ${line}`) });
|
|
99
92
|
const calendars = annotate(program.command("calendars").description("the account's calendars"), { effects: GROUP });
|
|
100
93
|
withQuery(example(annotate(calendars.command("list"), { effects: FETCH, offline: false }).description("list the account's calendars from the provider, and which of them sync"), "calendars list --json", "calendars list synced:true"), CALENDARS_QUERY).action(async (words, _opts, command) => {
|
|
101
94
|
const terms = [...queryTerms(words, CALENDARS_QUERY, "calendars list")];
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
.
|
|
106
|
-
|
|
107
|
-
|
|
95
|
+
await within(command, async (run) => {
|
|
96
|
+
// WC-CMD-20: the provider's list has no filter for these terms (Google's calendarList.list takes only a minimum
|
|
97
|
+
// access role, and synced is this store's own), and every calendar it answers is saved, so they narrow here.
|
|
98
|
+
const found = [...(await run.source().calendars()), ...((await run.feeds?.().source.calendars()) ?? [])];
|
|
99
|
+
const listed = await run.store.saveCalendars(run.accountId, found, now());
|
|
100
|
+
const items = listed
|
|
101
|
+
.map(calendarItem)
|
|
102
|
+
.filter((item) => terms.every(([field, value]) => String(item[field]) === value));
|
|
103
|
+
return listBody(command, { items, page: 1 });
|
|
104
|
+
});
|
|
108
105
|
});
|
|
109
106
|
for (const [name, synced] of [
|
|
110
107
|
["sync", true],
|
|
@@ -113,50 +110,58 @@ const addCalendarCommandsDefinition = (program, deps) => {
|
|
|
113
110
|
example(annotate(calendars.command(name), { effects: LOCAL_WRITE })
|
|
114
111
|
.description(synced ? "sync this calendar from now on" : "stop syncing this calendar; what is stored stays")
|
|
115
112
|
.argument("<calendar>", "the calendar's id, the provider's id, or its title"), `calendars ${name} 3`).action(async (reference, _opts, command) => {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
113
|
+
await within(command, async (run) => {
|
|
114
|
+
const calendar = await calendarIn(run.store, run.accountId, reference);
|
|
115
|
+
await run.store.setCalendarSynced(calendar.id, synced, now());
|
|
116
|
+
return itemBody(calendarItem({ ...calendar, synced }));
|
|
117
|
+
});
|
|
119
118
|
});
|
|
120
|
-
if (
|
|
121
|
-
const
|
|
119
|
+
if (options.feeds) {
|
|
120
|
+
const feedsOf = (run) => {
|
|
121
|
+
if (!run.feeds)
|
|
122
|
+
throw new CliError("configuration_error", "this profile follows no feeds");
|
|
123
|
+
return run.feeds();
|
|
124
|
+
};
|
|
122
125
|
example(
|
|
123
126
|
// A fetch, but also a new secret and a new address every later sync reads: the read-only lock refuses it.
|
|
124
127
|
changesStore(annotate(calendars.command("add"), { effects: FETCH, offline: false, mcp: false }))
|
|
125
128
|
.description("follow an iCalendar feed, such as a holiday calendar or a published .ics address, as a read-only calendar " +
|
|
126
|
-
"of this profile
|
|
129
|
+
"of this profile that sync reads again each time; the address is kept as a secret and never printed")
|
|
127
130
|
.summary("follow an iCalendar feed as a read-only calendar")
|
|
128
131
|
.addArgument(secretInput(new Argument("<url>", "the feed's https://, http:// or webcal:// address")))
|
|
129
132
|
.option("--title <title>", "the calendar's title; the feed's own name if not given"), "calendars add webcal://example.com/holidays.ics --title Holidays").action(async (url, opts, command) => {
|
|
130
133
|
if (command.optsWithGlobals().offline)
|
|
131
134
|
throw new CliError("permission_error", "calendars add reads the feed; leave out --offline");
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
"
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
135
|
+
await within(command, async (run) => {
|
|
136
|
+
const { calendar, events } = await feedsOf(run).add(url, opts.title === undefined ? {} : { title: opts.title });
|
|
137
|
+
if (isPlainHttp(url))
|
|
138
|
+
outputFor(command).renderer.warn("this feed's address starts with http://, so the feed and any token in its address travel unencrypted; " +
|
|
139
|
+
"use https:// if the server offers it");
|
|
140
|
+
const [saved] = await run.store.saveCalendars(run.accountId, [calendar], now());
|
|
141
|
+
return itemBody({
|
|
142
|
+
...calendarItem(saved),
|
|
143
|
+
events,
|
|
144
|
+
hint: `run ${lineOf(command, "sync")} to store its events`,
|
|
145
|
+
});
|
|
146
|
+
});
|
|
142
147
|
});
|
|
143
148
|
example(annotate(calendars.command("remove"), { effects: LOCAL_WRITE, mcp: false })
|
|
144
149
|
.description("stop following a feed calendars add added: its address is forgotten and its stored events are deleted, " +
|
|
145
|
-
"not cancelled
|
|
150
|
+
"not cancelled; only a feed, as calendars unsync stops a provider's calendar")
|
|
146
151
|
.argument("<calendar>", "the feed calendar's id, its ics: id, or its title"), "calendars remove 7").action(async (reference, _opts, command) => {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
reason: "feed.not-a-feed"
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
152
|
+
await within(command, async (run) => {
|
|
153
|
+
const calendar = await calendarIn(run.store, run.accountId, reference);
|
|
154
|
+
if (!isFeedCalendar(calendar))
|
|
155
|
+
throw new CliError("validation_error", `calendar ${calendar.id} is not a feed; calendars remove takes only feeds`, { reason: "feed.not-a-feed" });
|
|
156
|
+
await feedsOf(run).remove(calendar.externalId);
|
|
157
|
+
const { entries, events } = await run.store.forgetCalendar(calendar.id, now());
|
|
158
|
+
return itemBody({ ...calendarItem({ ...calendar, synced: false }), removed: entries, events });
|
|
159
|
+
});
|
|
155
160
|
});
|
|
156
161
|
}
|
|
157
|
-
if (
|
|
162
|
+
if (options.freeBusy)
|
|
158
163
|
example(withQuery(annotate(calendars.command("busy"), { effects: REMOTE_READ, offline: false, partial: true }).description("when calendars and people are taken, asked of the provider and not stored; with --duration-minutes, " +
|
|
159
|
-
"the free stretches they share"), BUSY_QUERY).addHelpText("after", BUSY_QUERY_NOTE), "calendars busy person:ana@example.com --json", "calendars busy
|
|
164
|
+
"the free stretches they share"), BUSY_QUERY).addHelpText("after", BUSY_QUERY_NOTE), "calendars busy person:ana@example.com --json", "calendars busy starts_at>=2026-11-02 ends_at<=2026-11-06 person:ana@example.com --duration-minutes 30 " +
|
|
160
165
|
"--hours 10:00-16:00")
|
|
161
166
|
.option("--duration-minutes <minutes>", "find free stretches at least this long, 5–1440", minutes)
|
|
162
167
|
.option("--hours <from-to>", "working hours in your time zone, Monday to Friday, with --duration-minutes", "09:00-17:00")
|
|
@@ -166,12 +171,15 @@ const addCalendarCommandsDefinition = (program, deps) => {
|
|
|
166
171
|
reason: "busy.offline",
|
|
167
172
|
});
|
|
168
173
|
const hours = workingHoursOf(opts.hours);
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
174
|
+
await within(command, async (run) => {
|
|
175
|
+
const query = await busyQuery(words, { store: run.store, accountId: run.accountId, zone: zone(), now: now() });
|
|
176
|
+
const { meta, ...busy } = await busyAnswer(run.source(), query, {
|
|
177
|
+
zone: zone(),
|
|
178
|
+
now: now(),
|
|
179
|
+
...(opts.durationMinutes === undefined ? {} : { slot: { minutes: opts.durationMinutes, hours } }),
|
|
180
|
+
});
|
|
181
|
+
return itemBody(busy, { meta });
|
|
182
|
+
});
|
|
175
183
|
});
|
|
176
184
|
example(annotate(program.command("sync"), {
|
|
177
185
|
effects: FETCH,
|
|
@@ -188,133 +196,149 @@ const addCalendarCommandsDefinition = (program, deps) => {
|
|
|
188
196
|
if (command.optsWithGlobals().full)
|
|
189
197
|
throw new CliError("validation_error", "sync starts over with --rescan; --full only widens output");
|
|
190
198
|
const { signal } = environmentOf(command);
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
199
|
+
await within(command, async (run) => {
|
|
200
|
+
if (opts.embed && !run.messages)
|
|
201
|
+
throw new CliError("configuration_error", "this tool cannot embed events");
|
|
202
|
+
const report = await syncWithFeeds(run.source(), run.feeds?.(), run.store, {
|
|
203
|
+
accountId: run.accountId,
|
|
204
|
+
now: now(),
|
|
205
|
+
budget: runBudgetOf(command, opts.maxPages),
|
|
206
|
+
...(signal ? { signal } : {}),
|
|
207
|
+
...(opts.rescan ? { full: true } : {}),
|
|
208
|
+
...(run.expand ? { expand: run.expand } : {}),
|
|
209
|
+
});
|
|
210
|
+
// A run its budget stopped has no time left for the model; the run that finishes the round embeds.
|
|
211
|
+
const embeddings = opts.embed && !report.stopped && run.messages
|
|
212
|
+
? await embedEvents(run.messages, {
|
|
213
|
+
maxChunks: EMBED_MAX,
|
|
214
|
+
...(run.env ? { env: run.env } : {}),
|
|
215
|
+
command: options.app.command,
|
|
216
|
+
})
|
|
217
|
+
: undefined;
|
|
218
|
+
const failed = report.synced.filter((one) => one.error !== undefined);
|
|
219
|
+
const timeout = command.optsWithGlobals().timeout;
|
|
220
|
+
// The same command again, never --rescan: that would drop the round this run leaves in progress. A run that
|
|
221
|
+
// moved nothing on would only repeat itself, so it gets no step; a sync sends the provider no write, so
|
|
222
|
+
// running it again is safe.
|
|
223
|
+
const stuck = report.state === "partial" && report.progress.steps === 0 && failed.length === 0;
|
|
224
|
+
const why = failed.length
|
|
225
|
+
? "a calendar failed to sync; running sync again retries it"
|
|
226
|
+
: "the sync stopped before its end to answer in time; running it again goes on from there";
|
|
227
|
+
const next = failed.length || (report.state === "partial" && !stuck)
|
|
228
|
+
? [
|
|
229
|
+
{
|
|
230
|
+
kind: "command",
|
|
231
|
+
step: "calendar.sync",
|
|
232
|
+
command: "sync",
|
|
233
|
+
args: Object.fromEntries(Object.entries({ max_pages: opts.maxPages, timeout, embed: opts.embed }).filter(([, value]) => value !== undefined)),
|
|
234
|
+
why,
|
|
235
|
+
safe_to_auto_execute: true,
|
|
236
|
+
},
|
|
237
|
+
]
|
|
238
|
+
: undefined;
|
|
239
|
+
const progressAt = lineOf(command, "store status");
|
|
240
|
+
report.hint = failed.length
|
|
241
|
+
? `${failed.map((one) => `calendar ${one.calendarId} (${one.title ?? one.externalId}): ${one.error}`).join("; ")}` +
|
|
242
|
+
" — run sync again to retry"
|
|
243
|
+
: stuck
|
|
244
|
+
? `this run moved nothing on: raise --timeout or --max-pages, then run sync again; see progress: ${progressAt}`
|
|
245
|
+
: report.state === "partial"
|
|
246
|
+
? `run sync again to continue; see progress: ${progressAt}`
|
|
247
|
+
: report.hint;
|
|
248
|
+
if (report.hint === undefined)
|
|
249
|
+
delete report.hint;
|
|
250
|
+
// What the healthy calendars brought is saved and answered; each failed one is an issue (WC-OUT-7, WC-OUT-8).
|
|
251
|
+
const issues = failed.map((one) => ({
|
|
252
|
+
code: "provider_error",
|
|
232
253
|
reason: "sync.partial",
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
saved: one.saved,
|
|
240
|
-
removed: one.removed,
|
|
241
|
-
occurrences: one.occurrences,
|
|
242
|
-
...(one.error === undefined ? {} : { error: one.error }),
|
|
243
|
-
})),
|
|
244
|
-
retryable: true,
|
|
245
|
-
// A sync sends the provider no write, so running it again changes nothing there.
|
|
246
|
-
sent: false,
|
|
254
|
+
source: `calendar:${one.calendarId}`,
|
|
255
|
+
}));
|
|
256
|
+
const partial = report.state === "partial" || issues.length > 0;
|
|
257
|
+
return itemBody({ ...report, ...(embeddings === undefined ? {} : { embeddings }) }, {
|
|
258
|
+
...(partial ? { meta: { complete: false, ...(issues.length ? { issues } : {}) } } : {}),
|
|
259
|
+
...(next ? { next } : {}),
|
|
247
260
|
});
|
|
248
|
-
|
|
261
|
+
});
|
|
249
262
|
});
|
|
250
263
|
const store = annotate(program.command("store").description("the local store of calendar events"), {
|
|
251
264
|
effects: GROUP,
|
|
252
265
|
});
|
|
253
266
|
example(annotate(store.command("status"), { effects: LOCAL_READ }).description("how far each synced calendar's sync is: entries done, partial or complete, and when it last moved"), "store status --json").action(async (_opts, command) => {
|
|
254
|
-
|
|
255
|
-
profile: deps.profile ?? `account ${deps.accountId}`,
|
|
256
|
-
now: now(),
|
|
257
|
-
}), command);
|
|
267
|
+
await within(command, async (run) => listBody(command, await calendarStatus(run.store, run.accountId, { profile: run.profile, now: now() })));
|
|
258
268
|
});
|
|
269
|
+
const personOfEmail = (run) => {
|
|
270
|
+
const shared = run.messages;
|
|
271
|
+
return shared
|
|
272
|
+
? async (email) => {
|
|
273
|
+
const person = await shared.people.personOf({ provider: "email", id: email });
|
|
274
|
+
return person ? `person:${person.uid}` : null;
|
|
275
|
+
}
|
|
276
|
+
: undefined;
|
|
277
|
+
};
|
|
278
|
+
const personId = (run) => async (reference) => {
|
|
279
|
+
const text = reference.replace(/^person:/u, "");
|
|
280
|
+
if (/^[1-9]\d*$/u.test(text))
|
|
281
|
+
return Number(text);
|
|
282
|
+
const person = await messagesOf(run, "person:").people.personOf({
|
|
283
|
+
provider: "email",
|
|
284
|
+
id: text.trim().toLowerCase(),
|
|
285
|
+
});
|
|
286
|
+
return person ? Number(person.uid) : null;
|
|
287
|
+
};
|
|
259
288
|
const events = annotate(program.command("events").description("the account's stored events"), { effects: GROUP });
|
|
260
289
|
example(withQuery(annotate(events.command("list"), { effects: LOCAL_READ, partial: true }).description("list stored events, earliest first; --source reads several accounts as one list"), EVENTS_QUERY).addHelpText("after", EVENTS_QUERY_NOTE), "events list 'starts_at:[2026-11-02 TO 2026-11-07}' --json", "events list ends_at>=today person:ana@example.com", "events list overlapping:true --source all")
|
|
261
290
|
.option("--limit <n>", "events per page, 1–1000", integer, 100)
|
|
262
291
|
.option("--page <n>", "which page, starting at 1", integer, 1)
|
|
263
292
|
.option("--source <accounts>", "calendar accounts held in the store to read as one list: all, a provider, or a list such as " +
|
|
264
|
-
`${
|
|
293
|
+
`${options.app.command}:work,${options.app.command}:personal or group:<name>; the profile's account when absent`)
|
|
265
294
|
.action(async (words, opts, command) => {
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
295
|
+
await within(command, async (run) => {
|
|
296
|
+
const own = { id: run.accountId, key: run.account };
|
|
297
|
+
const { accounts, skipped } = opts.source === undefined
|
|
298
|
+
? { accounts: [own], skipped: [] }
|
|
299
|
+
: await sourceAccounts(messagesOf(run, "--source"), run.account, opts.source, options.app.command);
|
|
300
|
+
if (run.locked && accounts.some(({ id }) => id !== own.id))
|
|
301
|
+
throw new CliError("permission_error", "this process is locked to one profile; --source may name only its own account", { reason: "profile.locked" });
|
|
302
|
+
const filter = await eventsFilter(words, {
|
|
303
|
+
store: run.store,
|
|
304
|
+
accountId: run.accountId,
|
|
305
|
+
accountIds: accounts.map(({ id }) => id),
|
|
306
|
+
zone: zone(),
|
|
307
|
+
now: now(),
|
|
308
|
+
personOf: personId(run),
|
|
309
|
+
});
|
|
310
|
+
return listBody(command, eachMarked(await eventsAnswer(run.store, filter && { ...filter, limit: opts.limit }, opts.page), EVENT_ITEM_UNTRUSTED), { ...(await accountsMeta(run.store, accounts, skipped, now())), ...clockMeta(now(), zone()) });
|
|
279
311
|
});
|
|
280
|
-
print({
|
|
281
|
-
...eachMarked(await eventsAnswer(deps.store, filter && { ...filter, limit: opts.limit }, opts.page), EVENT_ITEM_UNTRUSTED),
|
|
282
|
-
meta: { ...(await accountsMeta(deps.store, accounts, skipped, now())), ...clockMeta(now(), zone()) },
|
|
283
|
-
}, command);
|
|
284
312
|
});
|
|
285
|
-
const exporter = example(withQuery(annotate(events.command("export"), { effects:
|
|
286
|
-
"finds; each occurrence of a repeating event as an event of its own"), EVENTS_QUERY).addHelpText("after", EVENTS_QUERY_NOTE),
|
|
287
|
-
? [
|
|
288
|
-
"events export id:42 --output planning.ics",
|
|
289
|
-
"events export 'starts_at:[2026-11-02 TO 2026-11-09}' NOT event_type:birthday --output week.ics",
|
|
290
|
-
]
|
|
291
|
-
: ["events export id:42", "events export 'starts_at:[2026-11-02 TO 2026-11-09}' NOT event_type:birthday"]));
|
|
313
|
+
const exporter = example(withQuery(annotate(events.command("export"), { effects: LOCAL_WRITE }).description(`write stored events as iCalendar (.ics), at most ${EXPORT_MAX}: one with id:<id>, or those the query ` +
|
|
314
|
+
"finds; each occurrence of a repeating event as an event of its own"), EVENTS_QUERY).addHelpText("after", EVENTS_QUERY_NOTE), "events export id:42 --output planning.ics", "events export 'starts_at:[2026-11-02 TO 2026-11-09}' NOT event_type:birthday --output week.ics");
|
|
292
315
|
// Not over MCP: an agent would choose where on the owner's disk to write. It gets the text in the answer.
|
|
293
|
-
|
|
294
|
-
writesFileOnlyWith(exporter.addOption(ownerOnlyOption("--output <file>", "the file to write; printed in the answer when absent")), "output");
|
|
316
|
+
writesFileOnlyWith(exporter.addOption(ownerOnlyOption("--output <file>", "the file to write; printed in the answer when absent")), "output");
|
|
295
317
|
exporter.action(async (words, opts, command) => {
|
|
296
|
-
|
|
297
|
-
store: deps.store,
|
|
298
|
-
accountId: deps.accountId,
|
|
299
|
-
zone: zone(),
|
|
300
|
-
now: now(),
|
|
301
|
-
personOf: personId,
|
|
302
|
-
});
|
|
303
|
-
const exported = await eventsIcs(deps.store, deps.accountId, filter, { now: now(), product: program.name() });
|
|
304
|
-
if (opts.output === undefined) {
|
|
305
|
-
print({ ...exported, untrusted: ["content"] }, command);
|
|
306
|
-
return;
|
|
307
|
-
}
|
|
308
|
-
if (!opts.output.trim())
|
|
318
|
+
if (opts.output !== undefined && !opts.output.trim())
|
|
309
319
|
throw new CliError("validation_error", "--output names no file");
|
|
310
|
-
await
|
|
311
|
-
|
|
320
|
+
await within(command, async (run) => {
|
|
321
|
+
const filter = await eventsFilter(words, {
|
|
322
|
+
store: run.store,
|
|
323
|
+
accountId: run.accountId,
|
|
324
|
+
zone: zone(),
|
|
325
|
+
now: now(),
|
|
326
|
+
personOf: personId(run),
|
|
327
|
+
});
|
|
328
|
+
const exported = await eventsIcs(run.store, run.accountId, filter, { now: now(), product: options.app.command });
|
|
329
|
+
if (opts.output === undefined)
|
|
330
|
+
return itemBody({ ...exported, untrusted: ["content"] });
|
|
331
|
+
await (options.writeFile ?? writeIcsFile)(opts.output, exported.content);
|
|
332
|
+
return itemBody({ format: exported.format, events: exported.events, output: opts.output });
|
|
333
|
+
});
|
|
312
334
|
});
|
|
313
335
|
example(annotate(events.command("show"), { effects: LOCAL_READ })
|
|
314
336
|
.description("one event: its calendar copies, attendees with their person, Drive files and history")
|
|
315
337
|
.argument("<event>", "the event's id, or event:<id>"), "events show event:42 --json").action(async (reference, _opts, command) => {
|
|
316
|
-
|
|
317
|
-
|
|
338
|
+
await within(command, async (run) => {
|
|
339
|
+
const shown = await eventAnswer(await eventOf(run.store, run.accountId, eventIdOf(reference)), personOfEmail(run));
|
|
340
|
+
return itemBody({ ...shown, untrusted: CALENDAR_UNTRUSTED_FIELDS });
|
|
341
|
+
});
|
|
318
342
|
});
|
|
319
343
|
example(annotate(events.command("prepare"), { effects: LOCAL_READ, partial: true })
|
|
320
344
|
.description("before a meeting: the event, its attendees with their answer, and each one's latest messages, mail and " +
|
|
@@ -329,60 +353,48 @@ const addCalendarCommandsDefinition = (program, deps) => {
|
|
|
329
353
|
zone: zone(),
|
|
330
354
|
now: now(),
|
|
331
355
|
});
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
356
|
+
await within(command, async (run) => {
|
|
357
|
+
const details = reference === undefined
|
|
358
|
+
? await nextMeeting(run.store, run.accountId, now())
|
|
359
|
+
: await eventOf(run.store, run.accountId, eventIdOf(reference));
|
|
360
|
+
const { meta, ...prepared } = await eventPreparation(details, {
|
|
361
|
+
store: run.store,
|
|
362
|
+
messages: messagesOf(run, "events prepare"),
|
|
363
|
+
account: { id: run.accountId, key: run.account },
|
|
364
|
+
personOf: personOfEmail(run) ?? (async () => null),
|
|
365
|
+
now: now(),
|
|
366
|
+
...(opts.limit === undefined ? {} : { limit: opts.limit }),
|
|
367
|
+
...(window.since === undefined ? {} : { since: window.since }),
|
|
368
|
+
});
|
|
369
|
+
return itemBody(prepared, { meta: { ...meta, ...window.meta, ...clockMeta(now(), zone()) } });
|
|
343
370
|
});
|
|
344
|
-
print({ ...prepared, meta: { ...meta, ...window.meta, ...clockMeta(now(), zone()) } }, command);
|
|
345
371
|
});
|
|
346
|
-
if (
|
|
347
|
-
addRemind(events,
|
|
348
|
-
const
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
.description(
|
|
352
|
-
? "find stored things by text: events, or all of messages, notes and events"
|
|
353
|
-
: "find stored things by text: events"), { effects: GROUP });
|
|
354
|
-
if (app)
|
|
355
|
-
addSearchAll(searches, { deps, app, zone, messages, example });
|
|
356
|
-
const search = example(annotate(searches.command("events"), { effects: deps.serverSearch ? LOCAL_OR_REMOTE_READ : LOCAL_READ })
|
|
357
|
-
.description(deps.serverSearch
|
|
372
|
+
if (options.writes)
|
|
373
|
+
addRemind(events, { within, now, zone, example });
|
|
374
|
+
const searches = annotate(program.command("search").description("find stored things by text: events, or all of messages, notes and events"), { effects: GROUP });
|
|
375
|
+
addSearchAll(searches, { within, app: options.app, zone, example });
|
|
376
|
+
const search = example(annotate(searches.command("events"), { effects: options.serverSearch ? LOCAL_OR_REMOTE_READ : LOCAL_READ })
|
|
377
|
+
.description(options.serverSearch
|
|
358
378
|
? "find stored events by words, and by meaning when its model is downloaded; with --backend server, ask " +
|
|
359
379
|
"the provider instead, or with --backend both, ask both"
|
|
360
380
|
: "find stored events by words, and by meaning when its model is downloaded")
|
|
361
|
-
.argument("<query...>", "what to find in titles, descriptions, places and attendees"), "search events roadmap planning --json", ...(
|
|
381
|
+
.argument("<query...>", "what to find in titles, descriptions, places and attendees"), "search events roadmap planning --json", ...(options.serverSearch
|
|
362
382
|
? ["search events --backend server quarterly review", "search events --backend both quarterly review"]
|
|
363
383
|
: []))
|
|
364
384
|
.option("--limit <n>", "events per page", integer, 20)
|
|
365
385
|
.option("--cursor <cursor>", "go on from where the last page stopped; page.next gives it", (text) => (text === "0" ? 0 : integer(text)), 0)
|
|
366
386
|
.option("--exact", "every word as written, no word stems")
|
|
367
387
|
.option("--newest", "latest start first instead of best first");
|
|
368
|
-
if (
|
|
388
|
+
if (options.serverSearch)
|
|
369
389
|
search.addOption(new Option("--backend <archive|server|both>", "where to search: archive, the local store, or server, the provider's own search over the synced calendars, " +
|
|
370
390
|
"earliest first; both gives the store's hits, each the provider also found marked server, then the " +
|
|
371
391
|
"provider's other hits, one page with no cursor")
|
|
372
392
|
.choices(["archive", "server", "both"])
|
|
373
393
|
.default("archive"));
|
|
374
394
|
search.action(async (words, opts, command) => {
|
|
375
|
-
const
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
offset: opts.cursor,
|
|
379
|
-
timezone: zone(),
|
|
380
|
-
command: program.name(),
|
|
381
|
-
...(opts.exact ? { exact: true } : {}),
|
|
382
|
-
...(opts.newest ? { newest: true } : {}),
|
|
383
|
-
});
|
|
384
|
-
if (opts.backend === "server" || opts.backend === "both") {
|
|
385
|
-
const flag = `search events --backend ${opts.backend}`;
|
|
395
|
+
const flag = `search events --backend ${opts.backend}`;
|
|
396
|
+
const server = opts.backend === "server" || opts.backend === "both";
|
|
397
|
+
if (server) {
|
|
386
398
|
if (command.optsWithGlobals().offline)
|
|
387
399
|
throw new CliError("permission_error", `${flag} asks the provider; leave out --offline`, {
|
|
388
400
|
reason: "search.offline",
|
|
@@ -393,90 +405,111 @@ const addCalendarCommandsDefinition = (program, deps) => {
|
|
|
393
405
|
});
|
|
394
406
|
if (opts.cursor || opts.newest)
|
|
395
407
|
throw new CliError("validation_error", `${flag} takes neither --cursor nor --newest: the provider ranks and pages its own hits`, { reason: "search.server-options" });
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
408
|
+
}
|
|
409
|
+
await within(command, async (run) => {
|
|
410
|
+
const archived = () => searchEvents(messagesOf(run, "search"), words.join(" "), {
|
|
411
|
+
accounts: [run.account],
|
|
412
|
+
limit: opts.limit,
|
|
413
|
+
offset: opts.cursor,
|
|
414
|
+
timezone: zone(),
|
|
415
|
+
command: options.app.command,
|
|
416
|
+
...(opts.exact ? { exact: true } : {}),
|
|
417
|
+
...(opts.newest ? { newest: true } : {}),
|
|
418
|
+
});
|
|
419
|
+
if (!server)
|
|
420
|
+
return hitsBody(command, { ...(await archived()), untrusted: SEARCH_UNTRUSTED });
|
|
421
|
+
const served = await serverSearchAnswer(run.source(), run.store, {
|
|
422
|
+
accountId: run.accountId,
|
|
423
|
+
account: run.account,
|
|
399
424
|
query: words.join(" "),
|
|
400
425
|
limit: opts.limit,
|
|
401
426
|
});
|
|
402
427
|
if (opts.backend === "server")
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
return;
|
|
410
|
-
}
|
|
411
|
-
print({ ...(await archived()), untrusted: SEARCH_UNTRUSTED }, command);
|
|
428
|
+
return hitsBody(command, { ...served, untrusted: SERVER_SEARCH_UNTRUSTED });
|
|
429
|
+
return hitsBody(command, {
|
|
430
|
+
...(await bothSearchAnswer(run.store, run.accountId, await archived(), served, opts.limit)),
|
|
431
|
+
untrusted: SEARCH_UNTRUSTED,
|
|
432
|
+
});
|
|
433
|
+
});
|
|
412
434
|
});
|
|
413
|
-
if (
|
|
414
|
-
addEventWrites(events,
|
|
435
|
+
if (options.writes)
|
|
436
|
+
addEventWrites(events, options.writes, { within, now, zone, example, readFile: options.readFile });
|
|
415
437
|
const people = annotate(program.command("people").description("the people in the account's events"), {
|
|
416
438
|
effects: GROUP,
|
|
417
439
|
});
|
|
418
440
|
withQuery(example(annotate(people.command("list"), { effects: LOCAL_READ }).description("one event's attendees, or everyone attending this week's events"), "people list --json", "people list event:42"), PEOPLE_QUERY).action(async (words, _opts, command) => {
|
|
419
441
|
const reference = queryTerms(words, PEOPLE_QUERY, "people list").get("event");
|
|
420
442
|
const event = reference === undefined ? undefined : eventIdOf(reference);
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
443
|
+
await within(command, async (run) => {
|
|
444
|
+
const filter = event === undefined ? weekFilter(run.accountId, zone(), now()) : undefined;
|
|
445
|
+
const personOf = personOfEmail(run);
|
|
446
|
+
const found = await peopleAnswer(run.store, run.accountId, {
|
|
447
|
+
...(event === undefined ? {} : { event }),
|
|
448
|
+
...(filter ? { filter } : {}),
|
|
449
|
+
...(personOf ? { personOf } : {}),
|
|
450
|
+
});
|
|
451
|
+
return listBody(command, eachMarked(found, ATTENDEE_UNTRUSTED));
|
|
426
452
|
});
|
|
427
|
-
print(eachMarked(people, ATTENDEE_UNTRUSTED), command);
|
|
428
453
|
});
|
|
429
454
|
example(annotate(people.command("link"), { effects: LOCAL_WRITE })
|
|
430
455
|
.description("join an attendee to a person the owner knows elsewhere")
|
|
431
456
|
.argument("<person>", "an attendee's address as email:<address>, or a person:<id>")
|
|
432
457
|
.argument("<other>", "the same person in another tool, as <provider>:<person>, or me"), "people link email:ana@example.com telegram:12345").action(async (person, other, _opts, command) => {
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
458
|
+
await within(command, async (run) => {
|
|
459
|
+
const linked = await linkPeople(messagesOf(run, "people link"), run.account, person, other);
|
|
460
|
+
return itemBody({
|
|
461
|
+
person: `person:${linked.uid}`,
|
|
462
|
+
name: linked.name,
|
|
463
|
+
identities: linked.identities.map(({ provider, id }) => `${provider}:${id}`),
|
|
464
|
+
});
|
|
465
|
+
});
|
|
439
466
|
});
|
|
440
467
|
const links = annotate(program.command("links").description("calendar copies the rules could not join alone"), {
|
|
441
468
|
effects: GROUP,
|
|
442
469
|
});
|
|
443
470
|
example(annotate(links.command("review"), { effects: LOCAL_READ }).description("copies that fit two or more events equally well, waiting for the owner's choice"), "links review --json").action(async (_opts, command) => {
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
471
|
+
await within(command, async (run) => {
|
|
472
|
+
// Locked to a profile (WC-ACCT-5), or with no shared store to name the others by, only the profile's own account.
|
|
473
|
+
const own = run.locked || !run.messages;
|
|
474
|
+
const found = await reviewsAnswer(run.store, own ? [run.accountId] : undefined);
|
|
475
|
+
const accounts = own
|
|
476
|
+
? [`${run.account.provider}:${run.account.account}`]
|
|
477
|
+
: await calendarAccountNames(run.store, messagesOf(run, "links review"), run.account);
|
|
478
|
+
return listBody(command, eachMarked(found, REVIEW_UNTRUSTED), { accounts, complete: true });
|
|
479
|
+
});
|
|
451
480
|
});
|
|
452
481
|
example(annotate(links.command("resolve"), { effects: LOCAL_WRITE })
|
|
453
482
|
.description("join a copy to the event you choose, or keep it apart")
|
|
454
483
|
.argument("<review>", "the review's id from links review")
|
|
455
484
|
.argument("<choice>", "one of its candidates, as event:<id>, or apart"), "links resolve 7 event:42").action(async (review, choice, _opts, command) => {
|
|
456
|
-
|
|
485
|
+
await within(command, async (run) => itemBody(await resolveAnswer(run.store, run.accountId, review, choice, now())));
|
|
457
486
|
});
|
|
458
487
|
const leaves = (command) => command.commands.length ? command.commands.flatMap(leaves) : [command];
|
|
459
488
|
declareCalendarAnswers(program, program.commands.filter((one) => !mounted.has(one)).flatMap(leaves));
|
|
460
489
|
};
|
|
461
490
|
const collect = (value, previous = []) => [...previous, value];
|
|
462
|
-
const addEventWrites = (events,
|
|
463
|
-
const
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
491
|
+
const addEventWrites = (events, traits, { within, now, zone, example, readFile }) => {
|
|
492
|
+
const contextOf = (run, command) => {
|
|
493
|
+
if (!run.writes)
|
|
494
|
+
throw new CliError("configuration_error", "this profile cannot write to its calendars");
|
|
495
|
+
return {
|
|
496
|
+
store: run.store,
|
|
497
|
+
accountId: run.accountId,
|
|
498
|
+
writes: run.writes(),
|
|
499
|
+
ask: terminalAsker(command),
|
|
500
|
+
now: now(),
|
|
501
|
+
zone: zone(),
|
|
502
|
+
names: {
|
|
503
|
+
notify: "--notify or --no-notify",
|
|
504
|
+
id: "--operation-id",
|
|
505
|
+
calendar: "--calendar",
|
|
506
|
+
series: "--series",
|
|
507
|
+
following: "--following",
|
|
508
|
+
repeat: "--repeat",
|
|
509
|
+
},
|
|
510
|
+
};
|
|
511
|
+
};
|
|
512
|
+
const written = (command, write) => within(command, async (run) => itemBody({ ...(await write(contextOf(run, command))), untrusted: CALENDAR_UNTRUSTED_FIELDS }));
|
|
480
513
|
// Left out on purpose: with other attendees the owner must choose, so neither switch is the default.
|
|
481
514
|
const notify = (command) => command
|
|
482
515
|
.option("--notify", "the provider mails the other attendees; needs events.notify allowed; this or --no-notify is required " +
|
|
@@ -508,7 +541,7 @@ const addEventWrites = (events, writes, { deps, now, zone, print, example }) =>
|
|
|
508
541
|
const target = (command) => command
|
|
509
542
|
.argument("<event>", "the event's id, or event:<id>")
|
|
510
543
|
.option("--calendar <id>", "the calendar, by id or title, when the event is on more than one of the account's");
|
|
511
|
-
const quick =
|
|
544
|
+
const quick = traits.quickAdd === true;
|
|
512
545
|
const create = example(notify(fields(write("create"), false)
|
|
513
546
|
.description(quick
|
|
514
547
|
? "add an event to a calendar; with --repeat, one that repeats; with --text, one the provider reads " +
|
|
@@ -521,20 +554,18 @@ const addEventWrites = (events, writes, { deps, now, zone, print, example }) =>
|
|
|
521
554
|
"--repeat 'FREQ=WEEKLY;BYDAY=MO,WE,FR;COUNT=12'");
|
|
522
555
|
if (quick)
|
|
523
556
|
create.option("--text <text>", "one line the provider reads the title, date, time and place from, in place of the other fields; nobody is " +
|
|
524
|
-
"mailed without --notify
|
|
557
|
+
"mailed without --notify");
|
|
525
558
|
create.action(async (calendar, opts, command) => {
|
|
526
559
|
const { notify: choice, operationId: id, text, ...rest } = opts;
|
|
560
|
+
const options = { ...(choice === undefined ? {} : { notify: choice }), ...(id === undefined ? {} : { id }) };
|
|
527
561
|
if (text !== undefined) {
|
|
528
562
|
const given = Object.keys(fieldsOf(rest));
|
|
529
|
-
if (given.length
|
|
530
|
-
throw new CliError("validation_error", `--text takes the whole event from its line; leave out ${
|
|
531
|
-
|
|
563
|
+
if (given.length)
|
|
564
|
+
throw new CliError("validation_error", `--text takes the whole event from its line; leave out ${given.map(flagOf).join(", ")}`, { reason: "event.text-with-fields", param: "--text" });
|
|
565
|
+
await written(command, (context) => createEventFromText(context, calendar, text, options));
|
|
532
566
|
return;
|
|
533
567
|
}
|
|
534
|
-
|
|
535
|
-
...(choice === undefined ? {} : { notify: choice }),
|
|
536
|
-
...(id === undefined ? {} : { id }),
|
|
537
|
-
}), command);
|
|
568
|
+
await written(command, (context) => createEvent(context, calendar, fieldsOf(rest), options));
|
|
538
569
|
});
|
|
539
570
|
example(notify(target(fields(write("update"), true))
|
|
540
571
|
.description("change an event; with --series every occurrence of the repeating event, with --following this " +
|
|
@@ -544,44 +575,40 @@ const addEventWrites = (events, writes, { deps, now, zone, print, example }) =>
|
|
|
544
575
|
"with the change")
|
|
545
576
|
.option("--operation-id <id>", "with --following, the operation_id an outcome_unknown named: a retry finds that new series")), "events update event:42 --start-time 2026-11-02T14:00 --end-time 2026-11-02T15:00 --notify", "events update event:42 --series --repeat 'FREQ=WEEKLY;BYDAY=TU;UNTIL=20261231T230000Z' --no-notify", "events update event:42 --following --start-time 2026-11-09T10:00 --end-time 2026-11-09T10:30 --no-notify").action(async (reference, opts, command) => {
|
|
546
577
|
const { calendar: _calendar, series: _series, following: _following, notify: _notify, operationId: id, ...rest } = opts;
|
|
547
|
-
|
|
548
|
-
...choicesOf(opts),
|
|
549
|
-
...(id === undefined ? {} : { id }),
|
|
550
|
-
}), command);
|
|
578
|
+
await written(command, (context) => updateEvent(context, reference, fieldsOf(rest), { ...choicesOf(opts), ...(id === undefined ? {} : { id }) }));
|
|
551
579
|
});
|
|
552
580
|
example(notify(target(write("delete"))
|
|
553
581
|
.description("delete an event; a whole repeating event, or every later occurrence, needs events.delete.series " +
|
|
554
582
|
"allowed first")
|
|
555
583
|
.option("--series", "delete the whole repeating event, not only this occurrence")
|
|
556
584
|
.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) => {
|
|
557
|
-
|
|
585
|
+
await written(command, (context) => deleteEvent(context, reference, choicesOf(opts)));
|
|
558
586
|
});
|
|
559
|
-
|
|
560
|
-
if (writes.writer.importEvent && readFile)
|
|
587
|
+
if (traits.importEvent)
|
|
561
588
|
example(write("import")
|
|
562
589
|
.description(`add the events of an iCalendar (.ics) file to a calendar, at most ${IMPORT_MAX}, each under its own UID; ` +
|
|
563
590
|
"nobody is mailed; a changed occurrence of a repeating event (RECURRENCE-ID) is skipped")
|
|
564
591
|
.argument("<calendar>", "the calendar's id, the provider's id, or its title")
|
|
565
592
|
.argument("<file>", "the .ics file to read"), "events import Work planning.ics").action(async (calendar, file, _opts, command) => {
|
|
566
|
-
const text = await readFile(file);
|
|
593
|
+
const text = await (readFile ?? readIcsFile)(file);
|
|
567
594
|
if (Buffer.byteLength(text, "utf8") > ICS_MAX_BYTES)
|
|
568
595
|
throw new CliError("validation_error", `${file} is larger than ${ICS_MAX_BYTES / 1024 / 1024} MB`, {
|
|
569
596
|
reason: "ics.too-large",
|
|
570
597
|
});
|
|
571
|
-
|
|
598
|
+
await within(command, async (run) => itemBody({ ...(await importEvents(contextOf(run, command), calendar, text)), untrusted: IMPORT_UNTRUSTED }));
|
|
572
599
|
});
|
|
573
|
-
if (
|
|
600
|
+
if (traits.move)
|
|
574
601
|
example(notify(target(write("move"))
|
|
575
602
|
.description("move an event, with its id, to another calendar of the account; a repeating one moves whole")
|
|
576
603
|
.requiredOption("--to-calendar <id>", "the calendar to move it to: its id, the provider's id, or its title")
|
|
577
604
|
.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) => {
|
|
578
605
|
const { series, calendar, notify } = opts;
|
|
579
|
-
|
|
606
|
+
await written(command, (context) => moveEvent(context, reference, opts.toCalendar, choicesOf({ series, calendar, notify })));
|
|
580
607
|
});
|
|
581
608
|
example(notify(target(write("respond"))
|
|
582
609
|
.description("answer an invitation; the organiser is told only with --notify")
|
|
583
610
|
.addArgument(new Argument("<response>", "the answer").choices(EVENT_RESPONSES))), "events respond event:42 accepted --notify").action(async (reference, response, opts, command) => {
|
|
584
|
-
|
|
611
|
+
await written(command, (context) => respondToEvent(context, reference, response, choicesOf(opts)));
|
|
585
612
|
});
|
|
586
613
|
};
|
|
587
614
|
// honoursDryRun: a host program other than `run` would otherwise let a write through under --dry-run.
|