@wirecat/cli-core 0.495.0 → 0.497.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 +89 -1
- package/dist/calendar/cli/answers.d.ts +277 -0
- package/dist/calendar/cli/answers.js +257 -0
- package/dist/calendar/cli/index.d.ts +10 -1
- package/dist/calendar/cli/index.js +78 -47
- package/dist/calendar/index.d.ts +1 -1
- package/dist/calendar/reads.d.ts +7 -0
- package/dist/calendar/server-search.d.ts +31 -0
- package/dist/calendar/server-search.js +41 -0
- package/dist/calendar/source.d.ts +14 -0
- package/dist/calendar/testing/answers.d.ts +6 -0
- package/dist/calendar/testing/answers.js +37 -0
- package/dist/calendar/testing/source.d.ts +2 -0
- package/dist/calendar/testing/source.js +9 -0
- package/dist/cli/commands-command.js +10 -2
- package/dist/cli/config-command.js +18 -8
- package/dist/cli/messenger/conversation-answers.d.ts +1 -1
- package/dist/cli/messenger/message-answers.d.ts +1 -1
- package/dist/cli/messenger/message-writes.d.ts +2 -2
- package/dist/cli/messenger/search-answers.js +2 -2
- package/dist/cli/messenger/search-command.js +4 -2
- package/dist/cli/messenger/store-maintenance-command.d.ts +1 -1
- package/dist/cli/program.js +2 -1
- package/dist/entities/conversation-results.d.ts +2 -2
- package/dist/entities/evidence.d.ts +8 -8
- package/dist/entities/index.d.ts +26 -19
- package/dist/entities/listener.d.ts +4 -4
- package/dist/entities/message-context.d.ts +3 -3
- package/dist/entities/message-write.d.ts +4 -4
- package/dist/entities/message.d.ts +2 -2
- package/dist/entities/message.js +2 -2
- package/dist/entities/store-records.d.ts +1 -1
- package/dist/errors/reasons.js +4 -0
- package/dist/meetings/read-answers.d.ts +10 -0
- package/dist/meetings/read-answers.js +7 -0
- package/dist/services/document-source.d.ts +2 -3
- package/dist/services/document-source.js +2 -7
- package/dist/services/messages-discovery.js +7 -2
- package/dist/services/notes-search.d.ts +13 -3
- package/dist/services/notes-search.js +93 -19
- package/dist/store/sqlite/note-search.d.ts +5 -0
- package/dist/store/sqlite/note-search.js +11 -2
- package/package.json +1 -1
- package/parity.json +1 -1
|
@@ -20,10 +20,19 @@ export interface CalendarCommandDeps {
|
|
|
20
20
|
* build only once a profile is open.
|
|
21
21
|
*/
|
|
22
22
|
freeBusy?: boolean;
|
|
23
|
+
/**
|
|
24
|
+
* The source answers `search`: `search` takes `--backend server` and asks the provider. A flag, as `freeBusy`, so the
|
|
25
|
+
* source can stay unbuilt until a profile is open.
|
|
26
|
+
*/
|
|
27
|
+
serverSearch?: boolean;
|
|
23
28
|
/** For a source with `expands: "local"`. */
|
|
24
29
|
expand?: CalendarExpander;
|
|
25
30
|
/** After `sync --embed`: embeds new events for search by meaning (`embedEvents`), as drive's `sync --embed`. */
|
|
26
|
-
embed?: (max: number) => Promise<
|
|
31
|
+
embed?: (max: number) => Promise<{
|
|
32
|
+
model: string;
|
|
33
|
+
embedded: number;
|
|
34
|
+
left: boolean;
|
|
35
|
+
}>;
|
|
27
36
|
write: (value: unknown, format: "json" | "jsonl" | "text") => void;
|
|
28
37
|
now?: () => number;
|
|
29
38
|
/** The zone `today`, `week` and a bare date are read in; the computer's when absent. */
|
|
@@ -1,9 +1,10 @@
|
|
|
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 { Argument, InvalidArgumentError } from "commander";
|
|
4
|
+
import { Argument, InvalidArgumentError, Option } from "commander";
|
|
5
5
|
import { environmentOf, runBudgetOf } from "../../cli/context.js";
|
|
6
6
|
import { honoursDryRun } from "../../cli/dry-run-guard.js";
|
|
7
|
+
import { queryTerms, withQuery } from "../../cli/list-query.js";
|
|
7
8
|
import { terminalAsker } from "../../cli/messenger/ask.js";
|
|
8
9
|
import { lineOf } from "../../cli/steps.js";
|
|
9
10
|
import { searchEvents } from "../../services/events-search.js";
|
|
@@ -11,12 +12,15 @@ import { linkPeople } from "../../services/people.js";
|
|
|
11
12
|
import { BUSY_QUERY_HELP, busyAnswer, busyQuery, workingHoursOf } from "../busy.js";
|
|
12
13
|
import { CALENDAR_UNTRUSTED_FIELDS, calendarIn, calendarItem, EVENTS_QUERY_HELP, eventAnswer, eventIdOf, eventOf, eventsAnswer, eventsFilter, peopleAnswer, resolveAnswer, reviewsAnswer, } from "../reads.js";
|
|
13
14
|
import { REPEAT_HELP } from "../recurrence.js";
|
|
15
|
+
import { serverSearchAnswer } from "../server-search.js";
|
|
14
16
|
import { calendarStatus, syncCalendars } from "../sync.js";
|
|
15
17
|
import { createEvent, deleteEvent, EVENT_RESPONSES, moveEvent, respondToEvent, updateEvent, } from "../writes.js";
|
|
18
|
+
import { CALENDARS_QUERY, declareCalendarAnswers, PEOPLE_QUERY } from "./answers.js";
|
|
16
19
|
const LOCAL_READ = { remote: "none", local: "read" };
|
|
17
20
|
const LOCAL_WRITE = { remote: "none", local: "write" };
|
|
18
21
|
const FETCH = { remote: "read", local: "write" };
|
|
19
22
|
const REMOTE_READ = { remote: "read", local: "none" };
|
|
23
|
+
const LOCAL_OR_REMOTE_READ = { remote: "read", local: "read" };
|
|
20
24
|
const REMOTE_WRITE = { remote: "write", local: "write" };
|
|
21
25
|
const GROUP = { remote: "none", local: "none" };
|
|
22
26
|
/** How many pieces one `sync --embed` embeds, as drive's. */
|
|
@@ -26,6 +30,7 @@ const EVENT_ITEM_UNTRUSTED = ["title", "description", "location"];
|
|
|
26
30
|
const ATTENDEE_UNTRUSTED = ["name"];
|
|
27
31
|
const REVIEW_UNTRUSTED = ["candidates.title"];
|
|
28
32
|
const SEARCH_UNTRUSTED = ["hits.title", "hits.location", "hits.excerpt"];
|
|
33
|
+
const SERVER_SEARCH_UNTRUSTED = ["hits.title", "hits.location"];
|
|
29
34
|
const eachMarked = (answer, untrusted) => ({
|
|
30
35
|
...answer,
|
|
31
36
|
items: answer.items.map((item) => ({ ...item, untrusted })),
|
|
@@ -42,16 +47,9 @@ const minutes = (text) => {
|
|
|
42
47
|
return value;
|
|
43
48
|
};
|
|
44
49
|
const addCalendarCommandsDefinition = (program, deps) => {
|
|
50
|
+
const mounted = new Set(program.commands);
|
|
45
51
|
const now = () => deps.now?.() ?? Date.now();
|
|
46
52
|
const zone = () => timezoneOf(deps.timezone);
|
|
47
|
-
const output = (command) => command
|
|
48
|
-
.option("--json", "one JSON value")
|
|
49
|
-
.option("--jsonl", "one JSON object per item")
|
|
50
|
-
.hook("preAction", (cmd) => {
|
|
51
|
-
const opts = cmd.optsWithGlobals();
|
|
52
|
-
if (opts.json && opts.jsonl)
|
|
53
|
-
throw new CliError("validation_error", "choose --json or --jsonl");
|
|
54
|
-
});
|
|
55
53
|
const print = (value, command) => {
|
|
56
54
|
const opts = command.optsWithGlobals();
|
|
57
55
|
deps.write(value, opts.json ? "json" : opts.jsonl ? "jsonl" : "text");
|
|
@@ -81,27 +79,31 @@ const addCalendarCommandsDefinition = (program, deps) => {
|
|
|
81
79
|
return deps.source;
|
|
82
80
|
};
|
|
83
81
|
const calendars = annotate(program.command("calendars").description("the account's calendars"), { effects: GROUP });
|
|
84
|
-
|
|
82
|
+
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) => {
|
|
83
|
+
const terms = [...queryTerms(words, CALENDARS_QUERY, "calendars list")];
|
|
85
84
|
const listed = await deps.store.saveCalendars(deps.accountId, await source().calendars(), now());
|
|
86
|
-
|
|
85
|
+
const items = listed
|
|
86
|
+
.map(calendarItem)
|
|
87
|
+
.filter((item) => terms.every(([field, value]) => String(item[field]) === value));
|
|
88
|
+
print({ items, page: 1, limit: items.length, hasMore: false }, command);
|
|
87
89
|
});
|
|
88
90
|
for (const [name, synced] of [
|
|
89
91
|
["sync", true],
|
|
90
92
|
["unsync", false],
|
|
91
93
|
])
|
|
92
|
-
|
|
94
|
+
example(annotate(calendars.command(name), { effects: LOCAL_WRITE })
|
|
93
95
|
.description(synced ? "sync this calendar from now on" : "stop syncing this calendar; what is stored stays")
|
|
94
|
-
.argument("<calendar>", "the calendar's id, or the provider's id"), `calendars ${name} 3`)
|
|
96
|
+
.argument("<calendar>", "the calendar's id, or the provider's id"), `calendars ${name} 3`).action(async (reference, _opts, command) => {
|
|
95
97
|
const calendar = await calendarIn(deps.store, deps.accountId, reference);
|
|
96
98
|
await deps.store.setCalendarSynced(calendar.id, synced, now());
|
|
97
99
|
print(calendarItem({ ...calendar, synced }), command);
|
|
98
100
|
});
|
|
99
101
|
if (deps.freeBusy)
|
|
100
|
-
|
|
102
|
+
example(annotate(calendars.command("busy"), { effects: REMOTE_READ, offline: false })
|
|
101
103
|
.description("when calendars and people are taken, asked of the provider and not stored; with --duration-minutes, " +
|
|
102
104
|
"the free stretches they share")
|
|
103
105
|
.argument("[words...]", "today, week, from:<date>, to:<date>, calendar:<id>, person:<email>")
|
|
104
|
-
.addHelpText("after", BUSY_QUERY_HELP), "calendars busy week person:ana@example.com --json", "calendars busy from:2026-11-02 to:2026-11-06 person:ana@example.com --duration-minutes 30 --hours 10:00-16:00")
|
|
106
|
+
.addHelpText("after", BUSY_QUERY_HELP), "calendars busy week person:ana@example.com --json", "calendars busy from:2026-11-02 to:2026-11-06 person:ana@example.com --duration-minutes 30 --hours 10:00-16:00")
|
|
105
107
|
.option("--duration-minutes <minutes>", "find free stretches at least this long, 5–1440", minutes)
|
|
106
108
|
.option("--hours <from-to>", "working hours in your time zone, Monday to Friday, with --duration-minutes", "09:00-17:00")
|
|
107
109
|
.action(async (words, opts, command) => {
|
|
@@ -117,13 +119,13 @@ const addCalendarCommandsDefinition = (program, deps) => {
|
|
|
117
119
|
...(opts.durationMinutes === undefined ? {} : { slot: { minutes: opts.durationMinutes, hours } }),
|
|
118
120
|
}), command);
|
|
119
121
|
});
|
|
120
|
-
|
|
122
|
+
example(annotate(program.command("sync"), {
|
|
121
123
|
effects: FETCH,
|
|
122
124
|
offline: false,
|
|
123
125
|
cooperative: true,
|
|
124
126
|
partial: true,
|
|
125
127
|
}).description("bring the synced calendars up to date: what changed since the last sync, or everything with --rescan; a run " +
|
|
126
|
-
"stops before its deadline or --max-pages and the next one goes on from there"), "sync", "sync --rescan --embed", "sync --timeout 2m")
|
|
128
|
+
"stops before its deadline or --max-pages and the next one goes on from there"), "sync", "sync --rescan --embed", "sync --timeout 2m")
|
|
127
129
|
.option("--rescan", "list every synced calendar's window again, dropping a round in progress")
|
|
128
130
|
.option("--max-pages <pages>", "at most this many requests to the provider in this run; as many as fit the deadline if not given", integer)
|
|
129
131
|
.option("--embed", "also index new events for meaning search, with a model already downloaded")
|
|
@@ -191,17 +193,17 @@ const addCalendarCommandsDefinition = (program, deps) => {
|
|
|
191
193
|
});
|
|
192
194
|
print({ ...report, ...(embeddings === undefined ? {} : { embeddings }), ...(next ? { next } : {}) }, command);
|
|
193
195
|
});
|
|
194
|
-
|
|
196
|
+
example(annotate(program.command("status"), { effects: LOCAL_READ }).description("how far each synced calendar's sync is: entries done, partial or complete, and when it last moved"), "status --json").action(async (_opts, command) => {
|
|
195
197
|
print(await calendarStatus(deps.store, deps.accountId, {
|
|
196
198
|
profile: deps.profile ?? `account ${deps.accountId}`,
|
|
197
199
|
now: now(),
|
|
198
200
|
}), command);
|
|
199
201
|
});
|
|
200
202
|
const events = annotate(program.command("events").description("the account's stored events"), { effects: GROUP });
|
|
201
|
-
|
|
203
|
+
example(annotate(events.command("list"), { effects: LOCAL_READ })
|
|
202
204
|
.description("list stored events, earliest first")
|
|
203
205
|
.argument("[words...]", "today, week, from:<date>, to:<date>, person:<who>, calendar:<id>")
|
|
204
|
-
.addHelpText("after", EVENTS_QUERY_HELP), "events list week --json", "events list from:2026-11-02 to:2026-11-06 person:ana@example.com")
|
|
206
|
+
.addHelpText("after", EVENTS_QUERY_HELP), "events list week --json", "events list from:2026-11-02 to:2026-11-06 person:ana@example.com")
|
|
205
207
|
.option("--limit <n>", "events at most, 1–1000", integer, 100)
|
|
206
208
|
.action(async (words, opts, command) => {
|
|
207
209
|
const filter = await eventsFilter(words, {
|
|
@@ -213,20 +215,48 @@ const addCalendarCommandsDefinition = (program, deps) => {
|
|
|
213
215
|
});
|
|
214
216
|
print(eachMarked(await eventsAnswer(deps.store, filter && { ...filter, limit: opts.limit }), EVENT_ITEM_UNTRUSTED), command);
|
|
215
217
|
});
|
|
216
|
-
|
|
218
|
+
example(annotate(events.command("show"), { effects: LOCAL_READ })
|
|
217
219
|
.description("one event: its calendar copies, attendees with their person, Drive files and history")
|
|
218
|
-
.argument("<event>", "the event's id, or event:<id>"), "events show event:42 --json")
|
|
220
|
+
.argument("<event>", "the event's id, or event:<id>"), "events show event:42 --json").action(async (reference, _opts, command) => {
|
|
219
221
|
const shown = await eventAnswer(await eventOf(deps.store, deps.accountId, eventIdOf(reference)), personOfEmail);
|
|
220
222
|
print({ ...shown, untrusted: CALENDAR_UNTRUSTED_FIELDS }, command);
|
|
221
223
|
});
|
|
222
|
-
|
|
223
|
-
.description(
|
|
224
|
-
|
|
224
|
+
const search = example(annotate(program.command("search"), { effects: deps.serverSearch ? LOCAL_OR_REMOTE_READ : LOCAL_READ })
|
|
225
|
+
.description(deps.serverSearch
|
|
226
|
+
? "find stored events by words, and by meaning when its model is downloaded; with --backend server, ask " +
|
|
227
|
+
"the provider instead"
|
|
228
|
+
: "find stored events by words, and by meaning when its model is downloaded")
|
|
229
|
+
.argument("<words...>", "what to find in titles, descriptions, places and attendees"), "search roadmap planning --json", ...(deps.serverSearch ? ["search --backend server quarterly review"] : []))
|
|
225
230
|
.option("--limit <n>", "events per page", integer, 20)
|
|
226
231
|
.option("--cursor <cursor>", "continue from a page's next_offset", (text) => (text === "0" ? 0 : integer(text)), 0)
|
|
227
232
|
.option("--exact", "every word as written, no word stems")
|
|
228
|
-
.option("--newest", "latest start first instead of best first")
|
|
229
|
-
|
|
233
|
+
.option("--newest", "latest start first instead of best first");
|
|
234
|
+
if (deps.serverSearch)
|
|
235
|
+
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, " +
|
|
236
|
+
"earliest first")
|
|
237
|
+
.choices(["archive", "server", "both"])
|
|
238
|
+
.default("archive"));
|
|
239
|
+
search.action(async (words, opts, command) => {
|
|
240
|
+
if (opts.backend === "both")
|
|
241
|
+
throw new CliError("validation_error", "search --backend both is not offered: the store's search and the provider's rank and page events " +
|
|
242
|
+
"differently, so one merged list would have no single order or next page; run --backend archive and " +
|
|
243
|
+
"--backend server", { reason: "search.one-backend" });
|
|
244
|
+
if (opts.backend === "server") {
|
|
245
|
+
if (command.optsWithGlobals().offline)
|
|
246
|
+
throw new CliError("permission_error", "search --backend server asks the provider; leave out --offline", {
|
|
247
|
+
reason: "search.offline",
|
|
248
|
+
});
|
|
249
|
+
if (opts.exact || opts.cursor || opts.newest)
|
|
250
|
+
throw new CliError("validation_error", "search --backend server takes neither --exact, --cursor nor --newest: the provider ranks and pages it", { reason: "search.server-options" });
|
|
251
|
+
const found = await serverSearchAnswer(source(), deps.store, {
|
|
252
|
+
accountId: deps.accountId,
|
|
253
|
+
account: deps.account,
|
|
254
|
+
query: words.join(" "),
|
|
255
|
+
limit: opts.limit,
|
|
256
|
+
});
|
|
257
|
+
print({ ...found, untrusted: SERVER_SEARCH_UNTRUSTED }, command);
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
230
260
|
const found = await searchEvents(messages("search"), words.join(" "), {
|
|
231
261
|
accounts: [deps.account],
|
|
232
262
|
limit: opts.limit,
|
|
@@ -239,13 +269,12 @@ const addCalendarCommandsDefinition = (program, deps) => {
|
|
|
239
269
|
print({ ...found, untrusted: SEARCH_UNTRUSTED }, command);
|
|
240
270
|
});
|
|
241
271
|
if (deps.writes)
|
|
242
|
-
addEventWrites(events, deps.writes, { deps, now, zone,
|
|
272
|
+
addEventWrites(events, deps.writes, { deps, now, zone, print, example });
|
|
243
273
|
const people = annotate(program.command("people").description("the people in the account's events"), {
|
|
244
274
|
effects: GROUP,
|
|
245
275
|
});
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
.argument("[event]", "the event's id, or event:<id>"), "people list --json", "people list event:42")).action(async (reference, _opts, command) => {
|
|
276
|
+
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) => {
|
|
277
|
+
const reference = queryTerms(words, PEOPLE_QUERY, "people list").get("event");
|
|
249
278
|
const event = reference === undefined ? undefined : eventIdOf(reference);
|
|
250
279
|
const filter = event === undefined
|
|
251
280
|
? await eventsFilter(["week"], { store: deps.store, accountId: deps.accountId, zone: zone(), now: now() })
|
|
@@ -257,10 +286,10 @@ const addCalendarCommandsDefinition = (program, deps) => {
|
|
|
257
286
|
});
|
|
258
287
|
print(eachMarked(people, ATTENDEE_UNTRUSTED), command);
|
|
259
288
|
});
|
|
260
|
-
|
|
289
|
+
example(annotate(people.command("link"), { effects: LOCAL_WRITE })
|
|
261
290
|
.description("join an attendee to a person the owner knows elsewhere")
|
|
262
291
|
.argument("<person>", "an attendee's address as email:<address>, or a person:<id>")
|
|
263
|
-
.argument("<other>", "the same person in another tool, as <provider>:<person>, or me"), "people link email:ana@example.com telegram:12345")
|
|
292
|
+
.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) => {
|
|
264
293
|
const linked = await linkPeople(messages("people link"), deps.account, person, other);
|
|
265
294
|
print({
|
|
266
295
|
person: `person:${linked.uid}`,
|
|
@@ -271,18 +300,20 @@ const addCalendarCommandsDefinition = (program, deps) => {
|
|
|
271
300
|
const links = annotate(program.command("links").description("calendar copies the rules could not join alone"), {
|
|
272
301
|
effects: GROUP,
|
|
273
302
|
});
|
|
274
|
-
|
|
303
|
+
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) => {
|
|
275
304
|
print(eachMarked(await reviewsAnswer(deps.store), REVIEW_UNTRUSTED), command);
|
|
276
305
|
});
|
|
277
|
-
|
|
306
|
+
example(annotate(links.command("resolve"), { effects: LOCAL_WRITE })
|
|
278
307
|
.description("join a copy to the event you choose, or keep it apart")
|
|
279
308
|
.argument("<review>", "the review's id from links review")
|
|
280
|
-
.argument("<choice>", "one of its candidates, as event:<id>, or apart"), "links resolve 7 event:42")
|
|
309
|
+
.argument("<choice>", "one of its candidates, as event:<id>, or apart"), "links resolve 7 event:42").action(async (review, choice, _opts, command) => {
|
|
281
310
|
print(await resolveAnswer(deps.store, deps.accountId, review, choice, now()), command);
|
|
282
311
|
});
|
|
312
|
+
const leaves = (command) => command.commands.length ? command.commands.flatMap(leaves) : [command];
|
|
313
|
+
declareCalendarAnswers(program, program.commands.filter((one) => !mounted.has(one)).flatMap(leaves));
|
|
283
314
|
};
|
|
284
315
|
const collect = (value, previous = []) => [...previous, value];
|
|
285
|
-
const addEventWrites = (events, writes, { deps, now, zone,
|
|
316
|
+
const addEventWrites = (events, writes, { deps, now, zone, print, example }) => {
|
|
286
317
|
const context = (command) => ({
|
|
287
318
|
store: deps.store,
|
|
288
319
|
accountId: deps.accountId,
|
|
@@ -330,48 +361,48 @@ const addEventWrites = (events, writes, { deps, now, zone, output, print, exampl
|
|
|
330
361
|
const target = (command) => command
|
|
331
362
|
.argument("<event>", "the event's id, or event:<id>")
|
|
332
363
|
.option("--calendar <id>", "the calendar, when the event is on more than one of the account's");
|
|
333
|
-
|
|
364
|
+
example(notify(fields(write("create"), false)
|
|
334
365
|
.description("add an event to a calendar; with --repeat, one that repeats")
|
|
335
366
|
.argument("<calendar>", "the calendar's id, or the provider's id")).option("--id <id>", "the id a create that ended in outcome_unknown named: a retry finds that event"), "events create 3 --title 'Example planning' --start-time 2026-11-02T10:00 --end-time 2026-11-02T11:00", "events create 3 --title 'Example review' --start-date 2026-11-03 --end-date 2026-11-03 " +
|
|
336
367
|
"--attendee ana@example.com --no-notify", "events create 3 --title 'Example standup' --start-time 2026-11-02T09:30 --end-time 2026-11-02T09:45 " +
|
|
337
|
-
"--repeat 'FREQ=WEEKLY;BYDAY=MO,WE,FR;COUNT=12'")
|
|
368
|
+
"--repeat 'FREQ=WEEKLY;BYDAY=MO,WE,FR;COUNT=12'").action(async (calendar, opts, command) => {
|
|
338
369
|
const { notify: choice, id, ...rest } = opts;
|
|
339
370
|
printWritten(await createEvent(context(command), calendar, fieldsOf(rest), {
|
|
340
371
|
...(choice === undefined ? {} : { notify: choice }),
|
|
341
372
|
...(id === undefined ? {} : { id }),
|
|
342
373
|
}), command);
|
|
343
374
|
});
|
|
344
|
-
|
|
375
|
+
example(notify(target(fields(write("update"), true))
|
|
345
376
|
.description("change an event; with --series every occurrence of the repeating event, with --following this " +
|
|
346
377
|
"occurrence and every later one; new times or dates switch it between timed and all-day")
|
|
347
378
|
.option("--series", "change the whole repeating event, not only this occurrence")
|
|
348
379
|
.option("--following", "change this occurrence and every later one: the repeating event ends before it and a new one starts " +
|
|
349
380
|
"with the change")
|
|
350
|
-
.option("--id <id>", "with --following, the 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")
|
|
381
|
+
.option("--id <id>", "with --following, the 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) => {
|
|
351
382
|
const { calendar: _calendar, series: _series, following: _following, notify: _notify, id, ...rest } = opts;
|
|
352
383
|
printWritten(await updateEvent(context(command), reference, fieldsOf(rest), {
|
|
353
384
|
...choicesOf(opts),
|
|
354
385
|
...(id === undefined ? {} : { id }),
|
|
355
386
|
}), command);
|
|
356
387
|
});
|
|
357
|
-
|
|
388
|
+
example(notify(target(write("delete"))
|
|
358
389
|
.description("delete an event; a whole repeating event, or every later occurrence, needs events.delete.series " +
|
|
359
390
|
"allowed first")
|
|
360
391
|
.option("--series", "delete the whole repeating event, not only this occurrence")
|
|
361
|
-
.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")
|
|
392
|
+
.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) => {
|
|
362
393
|
printWritten(await deleteEvent(context(command), reference, choicesOf(opts)), command);
|
|
363
394
|
});
|
|
364
395
|
if (writes.writer.move)
|
|
365
|
-
|
|
396
|
+
example(notify(target(write("move"))
|
|
366
397
|
.description("move an event, with its id, to another calendar of the account; a repeating one moves whole")
|
|
367
398
|
.requiredOption("--to-calendar <id>", "the calendar to move it to: its id, or the provider's id")
|
|
368
|
-
.option("--series", "move the whole repeating event an occurrence belongs to")), "events move event:42 --to-calendar 4 --no-notify")
|
|
399
|
+
.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) => {
|
|
369
400
|
const { series, calendar, notify } = opts;
|
|
370
401
|
printWritten(await moveEvent(context(command), reference, opts.toCalendar, choicesOf({ series, calendar, notify })), command);
|
|
371
402
|
});
|
|
372
|
-
|
|
403
|
+
example(notify(target(write("respond"))
|
|
373
404
|
.description("answer an invitation; the organiser is told only with --notify")
|
|
374
|
-
.addArgument(new Argument("<response>", "the answer").choices(EVENT_RESPONSES))), "events respond event:42 accepted --notify")
|
|
405
|
+
.addArgument(new Argument("<response>", "the answer").choices(EVENT_RESPONSES))), "events respond event:42 accepted --notify").action(async (reference, response, opts, command) => {
|
|
375
406
|
printWritten(await respondToEvent(context(command), reference, response, choicesOf(opts)), command);
|
|
376
407
|
});
|
|
377
408
|
};
|
package/dist/calendar/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export type { CalendarAccessRole, CalendarInput, EntryAttachment, EntryAttachmentInput, EntryEventType, EntryInput, EntryKind, EntryParticipantInput, EntryRevision, EntryStatus, EntryTransparency, EntryVisibility, EventDetails, EventFilter, EventStore, LinkReview, ParticipantResponse, ReviewDecision, StoredCalendar, StoredEntry, StoredParticipant, } from "../meetings/index.js";
|
|
2
2
|
export { repeatRule } from "./recurrence.js";
|
|
3
|
-
export type { BusyTimes, CalendarExpander, CalendarSource, CalendarSourceTraits, ChangePage, ChangeResult, FreeBusyRequest, SyncWindow, } from "./source.js";
|
|
3
|
+
export type { BusyTimes, CalendarExpander, CalendarSource, CalendarSourceTraits, ChangePage, ChangeResult, FreeBusyRequest, SearchPage, SyncWindow, } from "./source.js";
|
|
4
4
|
export { type CalendarCursor, type CalendarSyncOptions, type CalendarSyncReport, type CalendarSyncResult, calendarStatus, calendarSyncWindow, type PendingRound, parseCalendarCursor, syncCalendars, } from "./sync.js";
|
|
5
5
|
export { CALENDAR_WRITE_DEFAULTS, CALENDAR_WRITE_KEYS, type CalendarWrite, type CalendarWriter, calendarConflict, EVENTS_DELETE_SERIES_KEY, EVENTS_NOTIFY_KEY, type EventDraft, type EventPatch, type EventResponse, type FollowingScope, type WriteScope, } from "./writer.js";
|
|
6
6
|
export type { CalendarWrites } from "./writes.js";
|
package/dist/calendar/reads.d.ts
CHANGED
|
@@ -63,6 +63,13 @@ export declare const eventAnswer: (details: EventDetails, personOf?: PersonOfEma
|
|
|
63
63
|
created_at: string;
|
|
64
64
|
updated_at: string;
|
|
65
65
|
untrusted?: string[] | undefined;
|
|
66
|
+
change?: "cancelled" | "moved" | "new" | undefined;
|
|
67
|
+
changed_at?: string | null | undefined;
|
|
68
|
+
was?: {
|
|
69
|
+
starts_at: string | null;
|
|
70
|
+
ends_at: string | null;
|
|
71
|
+
} | undefined;
|
|
72
|
+
overlaps?: string[] | undefined;
|
|
66
73
|
ref: string;
|
|
67
74
|
entries: {
|
|
68
75
|
id: string;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { EventStore } from "../meetings/index.js";
|
|
2
|
+
import type { AccountKey } from "../store/store.js";
|
|
3
|
+
import type { CalendarSource } from "./source.js";
|
|
4
|
+
/**
|
|
5
|
+
* The provider's own search over the calendars the account syncs, earliest first, stored nowhere. A calendar the owner
|
|
6
|
+
* does not sync stays out: he chose which calendars the CLI reads.
|
|
7
|
+
*/
|
|
8
|
+
export declare const serverSearchAnswer: (source: CalendarSource, store: EventStore, { accountId, account, query, limit }: {
|
|
9
|
+
accountId: number;
|
|
10
|
+
account: AccountKey;
|
|
11
|
+
query: string;
|
|
12
|
+
limit: number;
|
|
13
|
+
}) => Promise<{
|
|
14
|
+
query: string;
|
|
15
|
+
by: "server";
|
|
16
|
+
hits: {
|
|
17
|
+
provider: string;
|
|
18
|
+
account: string;
|
|
19
|
+
calendar: string;
|
|
20
|
+
external_id: string;
|
|
21
|
+
title: string | null;
|
|
22
|
+
starts_at: string | null;
|
|
23
|
+
ends_at: string | null;
|
|
24
|
+
all_day: boolean;
|
|
25
|
+
status: "confirmed" | "tentative";
|
|
26
|
+
location: string | null;
|
|
27
|
+
url?: string | undefined;
|
|
28
|
+
found_by: "server"[];
|
|
29
|
+
}[];
|
|
30
|
+
has_more: boolean;
|
|
31
|
+
}>;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { CliError } from "@wirecat/cli-base";
|
|
2
|
+
const iso = (at) => (at == null ? null : new Date(at).toISOString());
|
|
3
|
+
/**
|
|
4
|
+
* The provider's own search over the calendars the account syncs, earliest first, stored nowhere. A calendar the owner
|
|
5
|
+
* does not sync stays out: he chose which calendars the CLI reads.
|
|
6
|
+
*/
|
|
7
|
+
export const serverSearchAnswer = async (source, store, { accountId, account, query, limit }) => {
|
|
8
|
+
if (!source.search)
|
|
9
|
+
throw new CliError("configuration_error", "this calendar provider has no search of its own");
|
|
10
|
+
const calendars = (await store.calendars(accountId)).filter(({ synced, deletedAt }) => synced && deletedAt === null);
|
|
11
|
+
if (calendars.length === 0)
|
|
12
|
+
throw new CliError("validation_error", "no calendar syncs yet; run sync, or calendars sync <calendar>", {
|
|
13
|
+
reason: "search.no-calendars",
|
|
14
|
+
});
|
|
15
|
+
const hits = [];
|
|
16
|
+
let more = false;
|
|
17
|
+
for (const calendar of calendars) {
|
|
18
|
+
const page = await source.search(calendar, { query, limit });
|
|
19
|
+
more ||= page.more;
|
|
20
|
+
for (const entry of page.entries) {
|
|
21
|
+
if (entry.status === "cancelled")
|
|
22
|
+
continue;
|
|
23
|
+
hits.push({
|
|
24
|
+
provider: source.provider,
|
|
25
|
+
account: account.account,
|
|
26
|
+
calendar: calendar.externalId,
|
|
27
|
+
external_id: entry.externalId,
|
|
28
|
+
title: entry.title ?? null,
|
|
29
|
+
starts_at: iso(entry.startsAt),
|
|
30
|
+
ends_at: iso(entry.endsAt),
|
|
31
|
+
all_day: entry.allDay === true,
|
|
32
|
+
status: entry.status,
|
|
33
|
+
location: entry.location ?? null,
|
|
34
|
+
...(entry.url ? { url: entry.url } : {}),
|
|
35
|
+
found_by: ["server"],
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
hits.sort((a, b) => (a.starts_at ?? "").localeCompare(b.starts_at ?? ""));
|
|
40
|
+
return { query, by: "server", hits: hits.slice(0, limit), has_more: more || hits.length > limit };
|
|
41
|
+
};
|
|
@@ -49,6 +49,12 @@ export interface BusyTimes {
|
|
|
49
49
|
/** The provider's reason (`notFound`, `groupTooBig`): its busy times are unknown, not free. */
|
|
50
50
|
error?: string;
|
|
51
51
|
}
|
|
52
|
+
/** What the provider's own text search found on one calendar: one page, read and never stored. */
|
|
53
|
+
export interface SearchPage {
|
|
54
|
+
entries: EntryInput[];
|
|
55
|
+
/** The provider has more matches on this calendar than it returned. */
|
|
56
|
+
more: boolean;
|
|
57
|
+
}
|
|
52
58
|
/** One provider's calendars, as an adapter reads them. The engine never sees a provider word. */
|
|
53
59
|
export interface CalendarSource {
|
|
54
60
|
/** The registry name: `google-calendar`, `outlook-calendar`, `caldav`, `ics`. */
|
|
@@ -65,6 +71,14 @@ export interface CalendarSource {
|
|
|
65
71
|
occurrences?(calendar: StoredCalendar, seriesExternalId: string, window: SyncWindow): Promise<EntryInput[]>;
|
|
66
72
|
/** When calendars and people are taken, read from the provider and never stored. Absent: no `calendars busy`. */
|
|
67
73
|
freeBusy?(request: FreeBusyRequest): Promise<BusyTimes[]>;
|
|
74
|
+
/**
|
|
75
|
+
* The provider's own text search over one calendar, at most `limit` entries; it also finds events outside the sync
|
|
76
|
+
* window. Absent: no `search --backend server`.
|
|
77
|
+
*/
|
|
78
|
+
search?(calendar: StoredCalendar, request: {
|
|
79
|
+
query: string;
|
|
80
|
+
limit: number;
|
|
81
|
+
}): Promise<SearchPage>;
|
|
68
82
|
}
|
|
69
83
|
/**
|
|
70
84
|
* Core expansion for `expands: "local"`: a series' slots inside the window, as occurrences. A slot its EXDATE lines
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Command } from "commander";
|
|
2
|
+
/**
|
|
3
|
+
* Parses what a shared calendar command handed its host against the command's entity, as a host prints it: keys in
|
|
4
|
+
* snake_case, a list's items one by one, a search's hits each with the search's `untrusted`, and no `next`.
|
|
5
|
+
*/
|
|
6
|
+
export declare const checkCalendarAnswer: (root: Command, argv: readonly string[], value: unknown) => void;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import * as v from "valibot";
|
|
2
|
+
import { answerEntitySchema } from "../../cli/answer-entity.js";
|
|
3
|
+
import { snakeKeys } from "../../cli/envelope.js";
|
|
4
|
+
const commandAt = (root, argv) => {
|
|
5
|
+
let at = root;
|
|
6
|
+
for (const word of argv) {
|
|
7
|
+
const next = at.commands.find((one) => one.name() === word);
|
|
8
|
+
if (!next)
|
|
9
|
+
break;
|
|
10
|
+
at = next;
|
|
11
|
+
}
|
|
12
|
+
return at;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Parses what a shared calendar command handed its host against the command's entity, as a host prints it: keys in
|
|
16
|
+
* snake_case, a list's items one by one, a search's hits each with the search's `untrusted`, and no `next`.
|
|
17
|
+
*/
|
|
18
|
+
export const checkCalendarAnswer = (root, argv, value) => {
|
|
19
|
+
const command = commandAt(root, argv);
|
|
20
|
+
const schema = answerEntitySchema(command);
|
|
21
|
+
if (!schema)
|
|
22
|
+
throw new Error(`${argv.join(" ")} declares no answer entity`);
|
|
23
|
+
const { next: _next, ...body } = snakeKeys(value);
|
|
24
|
+
const marker = body.untrusted
|
|
25
|
+
?.filter((field) => field.startsWith("hits."))
|
|
26
|
+
.map((field) => field.slice("hits.".length));
|
|
27
|
+
const items = Array.isArray(body.items)
|
|
28
|
+
? body.items
|
|
29
|
+
: Array.isArray(body.hits)
|
|
30
|
+
? body.hits.map((hit) => ({ ...hit, untrusted: marker }))
|
|
31
|
+
: [body];
|
|
32
|
+
for (const item of items) {
|
|
33
|
+
const parsed = v.safeParse(schema, item);
|
|
34
|
+
if (!parsed.success)
|
|
35
|
+
throw new Error(`${argv.join(" ")}: ${parsed.issues.map((issue) => `${v.getDotPath(issue) ?? ""} ${issue.message}`).join("; ")}`);
|
|
36
|
+
}
|
|
37
|
+
};
|
|
@@ -6,6 +6,8 @@ export interface FakeCalendarSourceOptions {
|
|
|
6
6
|
calendars?: CalendarInput[];
|
|
7
7
|
/** Entries per page of `changes()`. */
|
|
8
8
|
pageSize?: number;
|
|
9
|
+
/** Answer `search()`: an entry whose title, description or place holds the query, case aside, in any window. */
|
|
10
|
+
search?: boolean;
|
|
9
11
|
}
|
|
10
12
|
/**
|
|
11
13
|
* A provider held in memory. Every `put` and `remove` is one change; the cursor names the last change it saw, so a
|
|
@@ -86,6 +86,15 @@ export const fakeCalendarSource = (options = {}) => {
|
|
|
86
86
|
},
|
|
87
87
|
}
|
|
88
88
|
: {}),
|
|
89
|
+
...(options.search
|
|
90
|
+
? {
|
|
91
|
+
async search(calendar, { query, limit }) {
|
|
92
|
+
const words = query.toLowerCase();
|
|
93
|
+
const found = [...of(calendar.externalId).values()].filter((entry) => [entry.title, entry.description, entry.location].some((text) => text?.toLowerCase().includes(words)));
|
|
94
|
+
return { entries: found.slice(0, limit).map((entry) => structuredClone(entry)), more: found.length > limit };
|
|
95
|
+
},
|
|
96
|
+
}
|
|
97
|
+
: {}),
|
|
89
98
|
async freeBusy({ calendars: asked, people: persons, window }) {
|
|
90
99
|
const taken = (busy) => busy
|
|
91
100
|
.filter(({ from, to }) => from < window.to && to > window.from)
|
|
@@ -96,10 +96,18 @@ const scopeOf = (root, all, path) => {
|
|
|
96
96
|
if (!child || !described) {
|
|
97
97
|
const prefix = selected?.path ?? [];
|
|
98
98
|
const choices = available.map(({ name }) => name);
|
|
99
|
+
const groups = all
|
|
100
|
+
.filter(({ name, commands }) => commands.length > 0 && name !== "commands")
|
|
101
|
+
.map(({ name }) => name);
|
|
102
|
+
// The groups the caller named come first: they are the list of groups this answer is most likely about.
|
|
103
|
+
const [one, two] = [...new Set([...path.filter((each) => groups.includes(each)), ...groups])].map((group) => `\`${root.name()} commands ${group} --json\``);
|
|
99
104
|
throw new CliError("validation_error", `unknown command path "${path.join(" ")}" — inspect \`${[root.name(), "commands", ...prefix, "--json"].join(" ")}\`; ` +
|
|
100
105
|
(choices.length ? `available here: ${choices.join(", ")}. ` : "this command has no subcommands. ") +
|
|
101
|
-
|
|
102
|
-
|
|
106
|
+
(two
|
|
107
|
+
? `Give one command path per call, not a list of groups: ${one} and ${two} are separate calls`
|
|
108
|
+
: one
|
|
109
|
+
? `Give one command path per call, such as ${one}`
|
|
110
|
+
: "Give one command path per call"), { path, at: word, available: choices });
|
|
103
111
|
}
|
|
104
112
|
if (selected && selected.options.length > 0) {
|
|
105
113
|
inheritedOptions.push({ path: selected.path, options: [...selected.options] });
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { CliError, pathsAreOverridden } from "@wirecat/cli-base";
|
|
2
2
|
import { annotate } from "@wirecat/cli-base/commands";
|
|
3
|
-
import { Command } from "commander";
|
|
3
|
+
import { Command, Option } from "commander";
|
|
4
4
|
import { envName } from "../ports/app.js";
|
|
5
5
|
import { AUTHORITY_KEY, BOT_API_KEY } from "../sends/permissions.js";
|
|
6
6
|
import { baseContext, environmentOf } from "./context.js";
|
|
@@ -16,10 +16,16 @@ import { changeStoreSetting, isStoreSetting, offersStoreSetting, storeSettings,
|
|
|
16
16
|
*/
|
|
17
17
|
export const configCommand = (app, config, { permissionKey = config.permissions } = {}) => {
|
|
18
18
|
const command = new Command("config").description("the settings in force, and where each one came from");
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
// Only a messenger has bots, and with them the personal and bot sections of the file. Elsewhere the options stay
|
|
20
|
+
// accepted, so a script that passes one still runs, but help, `commands --json` and MCP leave them out.
|
|
21
|
+
const bots = app.messenger !== false;
|
|
22
|
+
const kindOption = (flags, description) => new Option(flags, description).hideHelp(!bots);
|
|
23
|
+
const show = annotate(command.command("show"), {
|
|
24
|
+
effects: LOCAL_READ,
|
|
25
|
+
examples: [`${app.command} personal config show`],
|
|
26
|
+
}).description("the profile, the profiles that exist, and each setting with where it came from");
|
|
27
|
+
show.addOption(kindOption("--bot", "the settings a bot command on this profile gets, rather than the personal account's"));
|
|
28
|
+
show.action(async function () {
|
|
23
29
|
const bot = this.opts().bot === true;
|
|
24
30
|
const context = baseContext(this, (flags, options) => config.resolveSettings(flags, {
|
|
25
31
|
...options,
|
|
@@ -32,6 +38,9 @@ export const configCommand = (app, config, { permissionKey = config.permissions
|
|
|
32
38
|
renderAnswer(this, context, itemOf({
|
|
33
39
|
profile: settings.sources.profile === NONE_NAMED ? null : settings.profile,
|
|
34
40
|
profile_from: settings.sources.profile,
|
|
41
|
+
kind: settings.kind,
|
|
42
|
+
permissions: settings.permissions,
|
|
43
|
+
permission_sources: settings.permissionSources,
|
|
35
44
|
profiles: settings.profilesHere,
|
|
36
45
|
config_file: settings.configPath,
|
|
37
46
|
config_found: settings.configFound,
|
|
@@ -48,7 +57,8 @@ export const configCommand = (app, config, { permissionKey = config.permissions
|
|
|
48
57
|
renderer.warn(`${issue.what}, so it is left out${issue.fix ? ` — \`${issue.fix}\` removes it` : ""}`);
|
|
49
58
|
if (overridden) {
|
|
50
59
|
renderer.note(`${envName(app, "CONFIG_DIR")}, ${envName(app, "STATE_DIR")} or ${envName(app, "CACHE_DIR")} is set — ` +
|
|
51
|
-
"a
|
|
60
|
+
"a session is still read from the usual keyring entry, but a sign-in made with them is kept apart, in a " +
|
|
61
|
+
"temporary entry that session end removes");
|
|
52
62
|
}
|
|
53
63
|
});
|
|
54
64
|
for (const action of ["set", "unset"]) {
|
|
@@ -58,8 +68,8 @@ export const configCommand = (app, config, { permissionKey = config.permissions
|
|
|
58
68
|
}))
|
|
59
69
|
.argument("<setting>", `one of: ${[...config.allSettings, ...storeSettingsOf(app)].join(", ")}`)
|
|
60
70
|
.option("--defaults", "change what every profile gets, rather than this profile")
|
|
61
|
-
.
|
|
62
|
-
.
|
|
71
|
+
.addOption(kindOption("--personal", "only for personal accounts — the personal section of the file"))
|
|
72
|
+
.addOption(kindOption("--bot", "only for bots — the bot section of the file"));
|
|
63
73
|
if (action === "set")
|
|
64
74
|
sub
|
|
65
75
|
.argument("<value>", "a number, true or false; for a group tg:work,max:Alice,email:me@x")
|
|
@@ -80,7 +80,7 @@ export declare const conversationShown: (account: AccountKey, { summary, message
|
|
|
80
80
|
chat_title: string | null;
|
|
81
81
|
timestamp: string;
|
|
82
82
|
sender_name: string | null;
|
|
83
|
-
|
|
83
|
+
excerpt: string;
|
|
84
84
|
}[] | undefined;
|
|
85
85
|
outgoing: boolean | null;
|
|
86
86
|
attachments: {
|