@wirecat/cli-core 0.500.0 → 0.502.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.
Files changed (53) hide show
  1. package/CHANGELOG.md +53 -1
  2. package/dist/calendar/busy.d.ts +9 -2
  3. package/dist/calendar/busy.js +61 -39
  4. package/dist/calendar/cli/answers.js +2 -2
  5. package/dist/calendar/cli/index.js +20 -21
  6. package/dist/calendar/mcp/index.d.ts +28 -28
  7. package/dist/calendar/mcp/index.js +8 -9
  8. package/dist/calendar/reads.d.ts +15 -4
  9. package/dist/calendar/reads.js +93 -43
  10. package/dist/calendar/testing/memory.js +4 -0
  11. package/dist/cli/envelope.d.ts +11 -1
  12. package/dist/cli/envelope.js +15 -3
  13. package/dist/cli/index.d.ts +2 -2
  14. package/dist/cli/index.js +2 -2
  15. package/dist/cli/list-query.d.ts +5 -0
  16. package/dist/cli/list-query.js +9 -3
  17. package/dist/cli/meeting-preparation-answer.d.ts +8 -0
  18. package/dist/cli/meeting-preparation-projects.d.ts +8 -0
  19. package/dist/cli/meeting-preparation-read.d.ts +8 -0
  20. package/dist/cli/meeting-series-inventory-read.d.ts +8 -0
  21. package/dist/cli/messenger/messages-search-command.js +4 -1
  22. package/dist/cli/messenger/search-answers.d.ts +3 -0
  23. package/dist/cli/messenger/search-answers.js +6 -0
  24. package/dist/cli/messenger/search-command.js +4 -1
  25. package/dist/entities/index.d.ts +24 -0
  26. package/dist/entities/meeting-preparation.d.ts +8 -0
  27. package/dist/entities/meeting-series-inventory.d.ts +8 -0
  28. package/dist/mcp/meeting-preparation.d.ts +8 -0
  29. package/dist/mcp/meeting-series-inventory.d.ts +8 -0
  30. package/dist/meetings/event-store.d.ts +4 -0
  31. package/dist/meetings/mcp/index.d.ts +15 -15
  32. package/dist/meetings/project-evidence-answer.d.ts +8 -0
  33. package/dist/meetings/testing/event-store-contract.js +23 -0
  34. package/dist/search/total.d.ts +13 -0
  35. package/dist/search/total.js +14 -0
  36. package/dist/services/index.d.ts +2 -1
  37. package/dist/services/index.js +2 -1
  38. package/dist/services/messages-archive-search.js +13 -3
  39. package/dist/services/messages-discovery.d.ts +6 -0
  40. package/dist/services/messages-discovery.js +14 -11
  41. package/dist/services/messages-search.d.ts +5 -0
  42. package/dist/services/messages-search.js +22 -0
  43. package/dist/services/messages.d.ts +3 -0
  44. package/dist/services/notes-search.d.ts +14 -2
  45. package/dist/services/notes-search.js +101 -19
  46. package/dist/store/queries.d.ts +2 -0
  47. package/dist/store/queries.js +1 -0
  48. package/dist/store/sqlite/calendar-events.js +5 -0
  49. package/dist/store/sqlite/lucene.d.ts +5 -0
  50. package/dist/store/sqlite/lucene.js +17 -0
  51. package/dist/store/sqlite/note-search.d.ts +18 -0
  52. package/dist/store/sqlite/note-search.js +40 -4
  53. package/package.json +3 -3
package/CHANGELOG.md CHANGED
@@ -5,7 +5,59 @@ Notable changes to `@wirecat/cli-core` (`@wirecat/cli-messaging` up to 0.261.0,
5
5
  minor version may break callers, and says how under "Changed — may break callers". `pnpm docs:check` checks the
6
6
  shape of this file.
7
7
 
8
- ## Unreleased
8
+ ## 0.502.0 — 31.10.2026
9
+
10
+ ### Added
11
+
12
+ - **The event store finds events by their earliest start: `EventFilter.startsFrom`.** It keeps events that start at
13
+ or after an instant, and `startsFromDate` keeps all-day events by their local date, as `toDate` does (WC-TIME-5).
14
+ Both stores (SQLite and `memoryEventStore`) answer it, and the event store contract checks the boundary.
15
+ - **`nativeTermLists`** from `@wirecat/cli-core/cli`: as `nativeTerms`, but named fields may repeat
16
+ (`person:a person:b`), every term kept.
17
+
18
+ ### Changed — may break callers
19
+
20
+ - **The shared calendar commands take the standard query and the paths the other tools use.** A CLI that mounts
21
+ `addCalendarCommands` (gcal) gets:
22
+ - `store status` in place of `status`, and `search events <query…>` in place of `search <words…>`; the permission
23
+ keys are `store.status` and `search.events`, and the sync's hint names `store status`.
24
+ - `events list [query…]` (`EVENTS_QUERY`): `starts_at` either way, now including `starts_at>=…` and
25
+ `starts_at:[a TO b}`, `ends_at>=…`, `changed_at>=…`, `calendar:` and `person:`. The words `today`, `week`,
26
+ `from:`, `to:`, `changed:` and `overlapping` are refused as query errors.
27
+ - `calendars busy [query…]` (`BUSY_QUERY`): the window as `start>=…` and `end<=…`, and `calendar:` and `person:`,
28
+ each repeatable. `today`, `week`, `from:` and `to:` are refused.
29
+ - `eventsFilter(query, …)` takes the query text in place of words, and `busyQuery(query, …)` as well;
30
+ `EVENTS_QUERY_HELP` and `BUSY_QUERY_HELP` are gone (`EVENTS_QUERY_NOTE`, `BUSY_QUERY_NOTE`), and `weekFilter`
31
+ builds `people list`'s week.
32
+ - `calendarTools`: `status` is `store_status`, and `events_list` takes `query` in place of `words`.
33
+
34
+ ## 0.501.0 — 31.10.2026
35
+
36
+ ### Added
37
+
38
+ - **Search answers say how many match in all: `page.total`.** `search notes` and `search messages` add
39
+ `page.total: { value, relation }` to `--json`, and `NotesFound.total` / `SearchFound.total` carry it to a tool that
40
+ builds its own answer (drive prints it as `item.total`; memo's notes, messages and mail pass it on with their next
41
+ release). It is shaped as Elasticsearch's `hits.total`: `relation` is `eq` for an exact count and `gte` for a lower bound. The terminal adds a line such as
42
+ `showing 1–20 of 144` (`144+` for a lower bound). Notes count every note the words find plus every note found by
43
+ meaning; messages count the strict query's matches, or discovery's candidates with `--discover`. Counting stops at
44
+ 10,000 (`TOTAL_CAP`, Elasticsearch's default), and a query whose leaves are checked row by row (a preset, a
45
+ pattern) gets the lower bound the page proves. New: `SearchTotal`, `NotesFound.total`, `SearchFound.total`,
46
+ `NoteSearch.matchingRefs` (refs only, no note read), `NoteSearch.scoreNotes` and `QueriesStore.countMatches`;
47
+ `listOf` takes `total`, and `shownOf(shown, offset, total)` from `@wirecat/cli-core/cli` writes the terminal line.
48
+
49
+ ### Changed — may break callers
50
+
51
+ - **cli-core needs `@wirecat/cli-base` 0.67.** The peer range moves from `>=0.65.0 <0.66.0` to `>=0.67.0 <0.68.0`. A
52
+ CLI that pins cli-base moves its pin with this release. cli-base 0.67 adds `page.total` to the list envelope.
53
+
54
+ ### Fixed
55
+
56
+ - **Search by meaning inside a filter ranks the filter's own notes.** With a filter that holds at most 2,000 notes
57
+ (`RANK_WITHIN`), `search notes` scores every one of them by meaning, instead of taking the 50 nearest notes overall
58
+ and dropping those outside the filter. On the owner's store, Drive's `budget date>=30d` went from 7 hits to a full
59
+ page of 20, and 50 of the 144 documents in the date range match in all; no hit falls outside the filter, as
60
+ before. A larger filter keeps the old way, as does one the store cannot list cheaply.
9
61
 
10
62
  ## 0.500.0 — 31.10.2026
11
63
 
@@ -1,12 +1,19 @@
1
+ import { type QueryRegistry } from "@wirecat/cli-base/query";
2
+ import { type QueryWords } from "../cli/list-query.js";
1
3
  import type { EventStore, StoredCalendar } from "../meetings/index.js";
2
4
  import type { CalendarSource, SyncWindow } from "./source.js";
3
- export declare const BUSY_QUERY_HELP = "\nWords, any order:\n today | week today, or today and the next six days, in your time zone\n from:<date> to:<date> from the start of one day through the end of another (or a time with a zone)\n calendar:<id> one of the account's calendars, by its id or the provider's id; repeat for more\n person:<email> someone else, by email address; repeat for more\nWithout a time word: this week. Without calendar:, every calendar that syncs.";
4
5
  export interface BusyQuery {
5
6
  window: SyncWindow;
6
7
  calendars: StoredCalendar[];
7
8
  people: string[];
8
9
  }
9
- export declare const busyQuery: (words: readonly string[], context: {
10
+ /**
11
+ * `calendars busy`'s query: the window by the answer's own `start` and `end`, and whom to ask. `calendar` and
12
+ * `person` repeat, and every one named is asked: the free stretches are those they all share.
13
+ */
14
+ export declare const BUSY_QUERY: QueryRegistry;
15
+ export declare const BUSY_QUERY_NOTE: string;
16
+ export declare const busyQuery: (query: QueryWords, context: {
10
17
  store: EventStore;
11
18
  accountId: number;
12
19
  zone: string;
@@ -1,13 +1,8 @@
1
1
  import { CliError } from "@wirecat/cli-base";
2
- import { dayBoundary, zonedMoment } from "@wirecat/cli-base/query";
3
- import { calendarIn, plusDays } from "./reads.js";
4
- export const BUSY_QUERY_HELP = `
5
- Words, any order:
6
- today | week today, or today and the next six days, in your time zone
7
- from:<date> to:<date> from the start of one day through the end of another (or a time with a zone)
8
- calendar:<id> one of the account's calendars, by its id or the provider's id; repeat for more
9
- person:<email> someone else, by email address; repeat for more
10
- Without a time word: this week. Without calendar:, every calendar that syncs.`;
2
+ import { dayBoundary, queryError, queryFieldsOf, zonedMoment, } from "@wirecat/cli-base/query";
3
+ import { LIST_QUERY_GUIDE, listQuery, nativeTermLists, typedQuery } from "../cli/list-query.js";
4
+ import { calendarBusy } from "./cli/answers.js";
5
+ import { calendarIn, plusDays, timeWindow } from "./reads.js";
11
6
  /** Google answers for 50 calendars at most (`calendarExpansionMax`, reference/freebusy/query). */
12
7
  const MOST = 50;
13
8
  const EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]+$/u;
@@ -15,41 +10,68 @@ const HOURS = /^([01]\d|2[0-3]):([0-5]\d)-([01]\d|2[0-4]):([0-5]\d)$/u;
15
10
  const MINUTE = 60_000;
16
11
  const localDate = (at, zone) => new Intl.DateTimeFormat("en-CA", { timeZone: zone, year: "numeric", month: "2-digit", day: "2-digit" }).format(at);
17
12
  const span = (text) => ({ start: 0, end: text.length });
18
- export const busyQuery = async (words, context) => {
19
- const today = localDate(context.now, context.zone);
20
- const day = (date) => dayBoundary(date, context.zone, false, span(date));
13
+ const BUSY = "calendars busy";
14
+ const bound = (field, side, example) => (node) => {
15
+ const open = node.operator === "range" && (side === "lower" ? node.upper === "*" : node.value === "*");
16
+ if (!open)
17
+ queryError("query.unsupported-operator", node.span, `${field} takes only ${side === "lower" ? "the window's start" : "the window's end"}, such as ${example}`, { command: BUSY, guide: LIST_QUERY_GUIDE });
18
+ };
19
+ /**
20
+ * `calendars busy`'s query: the window by the answer's own `start` and `end`, and whom to ask. `calendar` and
21
+ * `person` repeat, and every one named is asked: the free stretches are those they all share.
22
+ */
23
+ export const BUSY_QUERY = {
24
+ entity: "calendar_busy",
25
+ backend: "native",
26
+ fields: queryFieldsOf(calendarBusy, {
27
+ native: ["start", "end", "calendar", "person"],
28
+ relations: {
29
+ calendar: {
30
+ type: "kw",
31
+ example: "calendar:3",
32
+ relation: "one of the account's calendars, by its id or the provider's id; repeat for more",
33
+ },
34
+ person: { type: "kw", example: "person:ana@example.com", relation: "someone else, by address; repeat for more" },
35
+ },
36
+ examples: { start: "start>=2026-11-02", end: "end<=2026-11-06" },
37
+ validate: {
38
+ start: bound("start", "lower", "start>=2026-11-02"),
39
+ end: bound("end", "upper", "end<=2026-11-06"),
40
+ },
41
+ }).map((field) => field.native
42
+ ? { ...field, operators: field.type === "time" ? ["range"] : ["term", "phrase"] }
43
+ : field),
44
+ };
45
+ export const BUSY_QUERY_NOTE = "\nWithout start: from the start of today, in your time zone. Without end: seven days after the start. " +
46
+ "Without calendar: every calendar that syncs.";
47
+ export const busyQuery = async (query, context) => {
48
+ const { zone, now } = context;
49
+ const today = localDate(now, zone);
21
50
  let from;
22
51
  let to;
23
52
  const calendars = [];
24
53
  const people = new Set();
25
- for (const word of words) {
26
- const [, field, value = ""] = /^([a-z]+):(.+)$/u.exec(word) ?? [];
27
- if (word === "today" || word === "week") {
28
- from = day(today);
29
- to = day(plusDays(today, word === "today" ? 1 : 7));
30
- }
31
- else if (field === "from")
32
- from = zonedMoment(value, context.zone, span(value));
33
- else if (field === "to")
34
- to = zonedMoment(value, context.zone, span(value), { end: true }) + 1;
35
- else if (field === "calendar") {
36
- const calendar = await calendarIn(context.store, context.accountId, value);
37
- if (!calendars.some(({ id }) => id === calendar.id))
38
- calendars.push(calendar);
39
- }
40
- else if (field === "person") {
41
- const email = value.trim().toLowerCase();
42
- if (!EMAIL.test(email))
43
- throw new CliError("validation_error", `${value} is not an email address`);
44
- people.add(email);
54
+ const terms = nativeTermLists(listQuery(typedQuery(query), BUSY_QUERY, BUSY), BUSY, ["calendar", "person"]);
55
+ for (const [field, nodes] of terms)
56
+ for (const node of nodes) {
57
+ if (field === "start")
58
+ from = timeWindow(node, zone, now).lower;
59
+ else if (field === "end")
60
+ to = timeWindow(node, zone, now).upper;
61
+ else if (field === "calendar") {
62
+ const calendar = await calendarIn(context.store, context.accountId, node.value);
63
+ if (!calendars.some(({ id }) => id === calendar.id))
64
+ calendars.push(calendar);
65
+ }
66
+ else if (field === "person") {
67
+ const email = node.value.trim().toLowerCase();
68
+ if (!EMAIL.test(email))
69
+ throw new CliError("validation_error", `${node.value} is not an email address`);
70
+ people.add(email);
71
+ }
45
72
  }
46
- else
47
- throw new CliError("validation_error", `${word} is not a calendars busy word${BUSY_QUERY_HELP}`, {
48
- reason: "query.unknown_word",
49
- });
50
- }
51
- from ??= day(today);
52
- to ??= from === day(today) ? day(plusDays(today, 7)) : from + 7 * 86_400_000;
73
+ from ??= dayBoundary(today, zone, false, span(today));
74
+ to ??= from + 7 * 86_400_000;
53
75
  if (to <= from)
54
76
  throw new CliError("validation_error", "the window ends before it starts");
55
77
  const asked = calendars.length
@@ -192,7 +192,7 @@ const ANSWERS = {
192
192
  "calendars unsync": { name: "calendar", schema: calendar },
193
193
  "calendars busy": { name: "calendar_busy", schema: calendarBusy },
194
194
  sync: { name: "calendar_sync", schema: calendarSync },
195
- status: { name: "calendar_sync_progress", schema: calendarSyncProgress },
195
+ "store status": { name: "calendar_sync_progress", schema: calendarSyncProgress },
196
196
  "events list": { name: "event" },
197
197
  "events show": { name: "calendar_event", schema: calendarEvent },
198
198
  "events create": { name: "calendar_event_write", schema: calendarEventWrite },
@@ -200,7 +200,7 @@ const ANSWERS = {
200
200
  "events delete": { name: "calendar_event_write", schema: calendarEventWrite },
201
201
  "events move": { name: "calendar_event_write", schema: calendarEventWrite },
202
202
  "events respond": { name: "calendar_event_write", schema: calendarEventWrite },
203
- search: { name: "calendar_search_hit", schema: calendarSearchHit },
203
+ "search events": { name: "calendar_search_hit", schema: calendarSearchHit },
204
204
  "people list": { name: "calendar_attendee", schema: calendarAttendee },
205
205
  "people link": { name: "calendar_linked_person", schema: calendarLinkedPerson },
206
206
  "links review": { name: "calendar_link_review", schema: calendarLinkReview },
@@ -9,8 +9,8 @@ import { terminalAsker } from "../../cli/messenger/ask.js";
9
9
  import { lineOf } from "../../cli/steps.js";
10
10
  import { searchEvents } from "../../services/events-search.js";
11
11
  import { linkPeople } from "../../services/people.js";
12
- import { BUSY_QUERY_HELP, busyAnswer, busyQuery, workingHoursOf } from "../busy.js";
13
- import { CALENDAR_UNTRUSTED_FIELDS, calendarIn, calendarItem, EVENTS_QUERY_HELP, eventAnswer, eventIdOf, eventOf, eventsAnswer, eventsFilter, peopleAnswer, resolveAnswer, reviewsAnswer, } from "../reads.js";
12
+ import { BUSY_QUERY, BUSY_QUERY_NOTE, busyAnswer, busyQuery, workingHoursOf } from "../busy.js";
13
+ import { CALENDAR_UNTRUSTED_FIELDS, calendarIn, calendarItem, EVENTS_QUERY, EVENTS_QUERY_NOTE, eventAnswer, eventIdOf, eventOf, eventsAnswer, eventsFilter, peopleAnswer, resolveAnswer, reviewsAnswer, weekFilter, } from "../reads.js";
14
14
  import { REPEAT_HELP } from "../recurrence.js";
15
15
  import { serverSearchAnswer } from "../server-search.js";
16
16
  import { calendarStatus, syncCalendars } from "../sync.js";
@@ -99,11 +99,9 @@ const addCalendarCommandsDefinition = (program, deps) => {
99
99
  print(calendarItem({ ...calendar, synced }), command);
100
100
  });
101
101
  if (deps.freeBusy)
102
- example(annotate(calendars.command("busy"), { effects: REMOTE_READ, offline: false })
103
- .description("when calendars and people are taken, asked of the provider and not stored; with --duration-minutes, " +
104
- "the free stretches they share")
105
- .argument("[words...]", "today, week, from:<date>, to:<date>, calendar:<id>, person:<email>")
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")
102
+ example(withQuery(annotate(calendars.command("busy"), { effects: REMOTE_READ, offline: false }).description("when calendars and people are taken, asked of the provider and not stored; with --duration-minutes, " +
103
+ "the free stretches they share"), BUSY_QUERY).addHelpText("after", BUSY_QUERY_NOTE), "calendars busy person:ana@example.com --json", "calendars busy start>=2026-11-02 end<=2026-11-06 person:ana@example.com --duration-minutes 30 " +
104
+ "--hours 10:00-16:00")
107
105
  .option("--duration-minutes <minutes>", "find free stretches at least this long, 5–1440", minutes)
108
106
  .option("--hours <from-to>", "working hours in your time zone, Monday to Friday, with --duration-minutes", "09:00-17:00")
109
107
  .action(async (words, opts, command) => {
@@ -162,7 +160,7 @@ const addCalendarCommandsDefinition = (program, deps) => {
162
160
  },
163
161
  ]
164
162
  : undefined;
165
- const progressAt = lineOf(command, "status");
163
+ const progressAt = lineOf(command, "store status");
166
164
  report.hint = stuck
167
165
  ? `this run moved nothing on: raise --timeout or --max-pages, then run sync again; see progress: ${progressAt}`
168
166
  : report.state === "partial"
@@ -193,17 +191,17 @@ const addCalendarCommandsDefinition = (program, deps) => {
193
191
  });
194
192
  print({ ...report, ...(embeddings === undefined ? {} : { embeddings }), ...(next ? { next } : {}) }, command);
195
193
  });
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) => {
194
+ const store = annotate(program.command("store").description("the local store of calendar events"), {
195
+ effects: GROUP,
196
+ });
197
+ 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) => {
197
198
  print(await calendarStatus(deps.store, deps.accountId, {
198
199
  profile: deps.profile ?? `account ${deps.accountId}`,
199
200
  now: now(),
200
201
  }), command);
201
202
  });
202
203
  const events = annotate(program.command("events").description("the account's stored events"), { effects: GROUP });
203
- example(annotate(events.command("list"), { effects: LOCAL_READ })
204
- .description("list stored events, earliest first")
205
- .argument("[words...]", "today, week, from:<date>, to:<date>, person:<who>, calendar:<id>")
206
- .addHelpText("after", EVENTS_QUERY_HELP), "events list week --json", "events list from:2026-11-02 to:2026-11-06 person:ana@example.com")
204
+ example(withQuery(annotate(events.command("list"), { effects: LOCAL_READ }).description("list stored events, earliest first"), EVENTS_QUERY).addHelpText("after", EVENTS_QUERY_NOTE), "events list starts_at>=2026-11-02 starts_at<2026-11-07 --json", "events list ends_at>=today person:ana@example.com")
207
205
  .option("--limit <n>", "events at most, 1–1000", integer, 100)
208
206
  .action(async (words, opts, command) => {
209
207
  const filter = await eventsFilter(words, {
@@ -221,12 +219,15 @@ const addCalendarCommandsDefinition = (program, deps) => {
221
219
  const shown = await eventAnswer(await eventOf(deps.store, deps.accountId, eventIdOf(reference)), personOfEmail);
222
220
  print({ ...shown, untrusted: CALENDAR_UNTRUSTED_FIELDS }, command);
223
221
  });
224
- const search = example(annotate(program.command("search"), { effects: deps.serverSearch ? LOCAL_OR_REMOTE_READ : LOCAL_READ })
222
+ const searches = annotate(program.command("search").description("find stored things by text: events"), {
223
+ effects: GROUP,
224
+ });
225
+ const search = example(annotate(searches.command("events"), { effects: deps.serverSearch ? LOCAL_OR_REMOTE_READ : LOCAL_READ })
225
226
  .description(deps.serverSearch
226
227
  ? "find stored events by words, and by meaning when its model is downloaded; with --backend server, ask " +
227
228
  "the provider instead"
228
229
  : "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"] : []))
230
+ .argument("<query...>", "what to find in titles, descriptions, places and attendees"), "search events roadmap planning --json", ...(deps.serverSearch ? ["search events --backend server quarterly review"] : []))
230
231
  .option("--limit <n>", "events per page", integer, 20)
231
232
  .option("--cursor <cursor>", "continue from a page's next_offset", (text) => (text === "0" ? 0 : integer(text)), 0)
232
233
  .option("--exact", "every word as written, no word stems")
@@ -238,16 +239,16 @@ const addCalendarCommandsDefinition = (program, deps) => {
238
239
  .default("archive"));
239
240
  search.action(async (words, opts, command) => {
240
241
  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
+ throw new CliError("validation_error", "search events --backend both is not offered: the store's search and the provider's rank and page events " +
242
243
  "differently, so one merged list would have no single order or next page; run --backend archive and " +
243
244
  "--backend server", { reason: "search.one-backend" });
244
245
  if (opts.backend === "server") {
245
246
  if (command.optsWithGlobals().offline)
246
- throw new CliError("permission_error", "search --backend server asks the provider; leave out --offline", {
247
+ throw new CliError("permission_error", "search events --backend server asks the provider; leave out --offline", {
247
248
  reason: "search.offline",
248
249
  });
249
250
  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
+ throw new CliError("validation_error", "search events --backend server takes neither --exact, --cursor nor --newest: the provider ranks and pages it", { reason: "search.server-options" });
251
252
  const found = await serverSearchAnswer(source(), deps.store, {
252
253
  accountId: deps.accountId,
253
254
  account: deps.account,
@@ -276,9 +277,7 @@ const addCalendarCommandsDefinition = (program, deps) => {
276
277
  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
278
  const reference = queryTerms(words, PEOPLE_QUERY, "people list").get("event");
278
279
  const event = reference === undefined ? undefined : eventIdOf(reference);
279
- const filter = event === undefined
280
- ? await eventsFilter(["week"], { store: deps.store, accountId: deps.accountId, zone: zone(), now: now() })
281
- : undefined;
280
+ const filter = event === undefined ? weekFilter(deps.accountId, zone(), now()) : undefined;
282
281
  const people = await peopleAnswer(deps.store, deps.accountId, {
283
282
  ...(event === undefined ? {} : { event }),
284
283
  ...(filter ? { filter } : {}),
@@ -10,20 +10,20 @@ export declare const calendarTools: ({
10
10
  inputSchema: {
11
11
  type: string;
12
12
  properties: {
13
- limit?: undefined;
14
13
  account_id: {
15
14
  type: string;
16
15
  minimum: number;
17
16
  };
18
- words?: undefined;
17
+ query?: undefined;
19
18
  timezone?: undefined;
19
+ limit?: undefined;
20
20
  event?: undefined;
21
21
  } | {
22
22
  account_id: {
23
23
  type: string;
24
24
  minimum: number;
25
25
  };
26
- words: {
26
+ query: {
27
27
  type: string;
28
28
  minLength: number;
29
29
  };
@@ -38,9 +38,9 @@ export declare const calendarTools: ({
38
38
  };
39
39
  event?: undefined;
40
40
  } | {
41
- limit?: undefined;
42
- words?: undefined;
41
+ query?: undefined;
43
42
  timezone?: undefined;
43
+ limit?: undefined;
44
44
  event: {
45
45
  type: string;
46
46
  minLength: number;
@@ -50,9 +50,9 @@ export declare const calendarTools: ({
50
50
  minimum: number;
51
51
  };
52
52
  } | {
53
- limit?: undefined;
54
- words?: undefined;
53
+ query?: undefined;
55
54
  timezone?: undefined;
55
+ limit?: undefined;
56
56
  event?: undefined;
57
57
  account_id?: undefined;
58
58
  };
@@ -71,20 +71,20 @@ export declare const calendarTools: ({
71
71
  inputSchema: {
72
72
  type: string;
73
73
  properties: {
74
- limit?: undefined;
75
74
  account_id: {
76
75
  type: string;
77
76
  minimum: number;
78
77
  };
79
- words?: undefined;
78
+ query?: undefined;
80
79
  timezone?: undefined;
80
+ limit?: undefined;
81
81
  event?: undefined;
82
82
  } | {
83
83
  account_id: {
84
84
  type: string;
85
85
  minimum: number;
86
86
  };
87
- words: {
87
+ query: {
88
88
  type: string;
89
89
  minLength: number;
90
90
  };
@@ -99,9 +99,9 @@ export declare const calendarTools: ({
99
99
  };
100
100
  event?: undefined;
101
101
  } | {
102
- limit?: undefined;
103
- words?: undefined;
102
+ query?: undefined;
104
103
  timezone?: undefined;
104
+ limit?: undefined;
105
105
  event: {
106
106
  type: string;
107
107
  minLength: number;
@@ -111,9 +111,9 @@ export declare const calendarTools: ({
111
111
  minimum: number;
112
112
  };
113
113
  } | {
114
- limit?: undefined;
115
- words?: undefined;
114
+ query?: undefined;
116
115
  timezone?: undefined;
116
+ limit?: undefined;
117
117
  event?: undefined;
118
118
  account_id?: undefined;
119
119
  };
@@ -132,20 +132,20 @@ export declare const calendarTools: ({
132
132
  inputSchema: {
133
133
  type: string;
134
134
  properties: {
135
- limit?: undefined;
136
135
  account_id: {
137
136
  type: string;
138
137
  minimum: number;
139
138
  };
140
- words?: undefined;
139
+ query?: undefined;
141
140
  timezone?: undefined;
141
+ limit?: undefined;
142
142
  event?: undefined;
143
143
  } | {
144
144
  account_id: {
145
145
  type: string;
146
146
  minimum: number;
147
147
  };
148
- words: {
148
+ query: {
149
149
  type: string;
150
150
  minLength: number;
151
151
  };
@@ -160,9 +160,9 @@ export declare const calendarTools: ({
160
160
  };
161
161
  event?: undefined;
162
162
  } | {
163
- limit?: undefined;
164
- words?: undefined;
163
+ query?: undefined;
165
164
  timezone?: undefined;
165
+ limit?: undefined;
166
166
  event: {
167
167
  type: string;
168
168
  minLength: number;
@@ -172,9 +172,9 @@ export declare const calendarTools: ({
172
172
  minimum: number;
173
173
  };
174
174
  } | {
175
- limit?: undefined;
176
- words?: undefined;
175
+ query?: undefined;
177
176
  timezone?: undefined;
177
+ limit?: undefined;
178
178
  event?: undefined;
179
179
  account_id?: undefined;
180
180
  };
@@ -193,20 +193,20 @@ export declare const calendarTools: ({
193
193
  inputSchema: {
194
194
  type: string;
195
195
  properties: {
196
- limit?: undefined;
197
196
  account_id: {
198
197
  type: string;
199
198
  minimum: number;
200
199
  };
201
- words?: undefined;
200
+ query?: undefined;
202
201
  timezone?: undefined;
202
+ limit?: undefined;
203
203
  event?: undefined;
204
204
  } | {
205
205
  account_id: {
206
206
  type: string;
207
207
  minimum: number;
208
208
  };
209
- words: {
209
+ query: {
210
210
  type: string;
211
211
  minLength: number;
212
212
  };
@@ -221,9 +221,9 @@ export declare const calendarTools: ({
221
221
  };
222
222
  event?: undefined;
223
223
  } | {
224
- limit?: undefined;
225
- words?: undefined;
224
+ query?: undefined;
226
225
  timezone?: undefined;
226
+ limit?: undefined;
227
227
  event: {
228
228
  type: string;
229
229
  minLength: number;
@@ -233,9 +233,9 @@ export declare const calendarTools: ({
233
233
  minimum: number;
234
234
  };
235
235
  } | {
236
- limit?: undefined;
237
- words?: undefined;
236
+ query?: undefined;
238
237
  timezone?: undefined;
238
+ limit?: undefined;
239
239
  event?: undefined;
240
240
  account_id?: undefined;
241
241
  };
@@ -19,7 +19,7 @@ export const calendarTools = [
19
19
  required: [],
20
20
  },
21
21
  {
22
- name: "status",
22
+ name: "store_status",
23
23
  description: "how far each synced calendar's sync is: done, state partial or complete, and cursor_age_ms, which grows while " +
24
24
  "nothing moves it",
25
25
  properties: { account_id: integer },
@@ -27,10 +27,10 @@ export const calendarTools = [
27
27
  },
28
28
  {
29
29
  name: "events_list",
30
- description: "stored events, earliest first; `words` takes today, week, from:<date>, to:<date>, calendar:<id>, " +
31
- "changed:<date> (events new, moved or cancelled since, each with change and was) and overlapping (events " +
32
- "that clash, each with overlaps); a date in the zone given, UTC when absent",
33
- properties: { account_id: integer, words: text, timezone: text, limit: { ...integer, maximum: 1000 } },
30
+ description: "stored events, earliest first; `query` takes field terms joined by AND: starts_at (>=, <, or " +
31
+ "[a TO b}), ends_at>=, changed_at>= (events new, moved or cancelled since, each with change and was), " +
32
+ "calendar:<id>; a date in the zone given, UTC when absent; without starts_at or ends_at, from today on",
33
+ properties: { account_id: integer, query: text, timezone: text, limit: { ...integer, maximum: 1000 } },
34
34
  required: [],
35
35
  },
36
36
  {
@@ -163,7 +163,7 @@ const argumentsOf = (name, input) => {
163
163
  (typeof value !== "number" || !Number.isSafeInteger(value) || value < 1))
164
164
  throw new CliError("validation_error", `${key} must be a positive integer`);
165
165
  else if ([
166
- "words",
166
+ "query",
167
167
  "timezone",
168
168
  "event",
169
169
  "calendar",
@@ -225,13 +225,12 @@ export const callCalendarTool = async (store, name, input, { accountIds, failure
225
225
  case "calendars_list":
226
226
  answer = listed((await Promise.all(accounts.map((id) => store.calendars(id)))).flat().map(calendarItem));
227
227
  break;
228
- case "status":
228
+ case "store_status":
229
229
  answer = listed((await Promise.all(accounts.map((id) => calendarStatus(store, id, { profile: profileOf(id), now }).then(({ items }) => items)))).flat());
230
230
  break;
231
231
  case "events_list": {
232
- const words = typeof args.words === "string" ? args.words.trim().split(/\s+/u) : [];
233
232
  const limit = args.limit ?? 100;
234
- const lists = await Promise.all(accounts.map(async (accountId) => eventsAnswer(store, await eventsFilter(words, {
233
+ const lists = await Promise.all(accounts.map(async (accountId) => eventsAnswer(store, await eventsFilter(args.query, {
235
234
  store,
236
235
  accountId,
237
236
  zone: args.timezone ?? "UTC",
@@ -1,3 +1,5 @@
1
+ import { type Predicate, type QueryRegistry } from "@wirecat/cli-base/query";
2
+ import { type QueryWords } from "../cli/list-query.js";
1
3
  import { type EventDetails, type EventFilter, type EventStore, type StoredCalendar, type StoredEntry } from "../meetings/index.js";
2
4
  /** Who looks a person up: the shared store's `people.personOf`, for an attendee's `email` contact. */
3
5
  export type PersonOfEmail = (email: string) => Promise<string | null>;
@@ -17,18 +19,27 @@ export declare const calendarItem: (one: StoredCalendar) => {
17
19
  export declare const calendarIn: (store: EventStore, accountId: number, reference: string) => Promise<StoredCalendar>;
18
20
  export declare const eventIdOf: (reference: string | number) => number;
19
21
  export declare const plusDays: (date: string, days: number) => string;
20
- export declare const EVENTS_QUERY_HELP = "\nWords, any order:\n today | week today, or today and the next six days, in your time zone\n from:<date> to:<date> from the start of one day through the end of another (or a time with a zone)\n person:<email|id> events one person attends: an address, or a person id from search\n calendar:<id> one calendar, by its id or the provider's id\n changed:<date> events new, moved or cancelled since then, each with what changed (from the local copy)\n overlapping events that clash with another: timed, not cancelled, not declined, not marked free\nWithout a time word: events from the start of today on.";
22
+ /** `events list`'s query. The store answers `ends_at` and `changed_at` from below only; `starts_at` either way. */
23
+ export declare const EVENTS_QUERY: QueryRegistry;
24
+ export declare const EVENTS_QUERY_NOTE: string;
25
+ /** A time term as epoch ms: from `lower` on, and before `upper`. */
26
+ export declare const timeWindow: (node: Predicate, zone: string, now: number) => {
27
+ lower?: number;
28
+ upper?: number;
29
+ };
21
30
  /**
22
- * `events list` words into a store filter, for one account. A person named by address is found through the shared
23
- * store; one nobody knows matches no event.
31
+ * `events list`'s query into a store filter, for one account. A person named by address is found through the
32
+ * shared store; one nobody knows matches no event.
24
33
  */
25
- export declare const eventsFilter: (words: readonly string[], context: {
34
+ export declare const eventsFilter: (query: QueryWords, context: {
26
35
  store: EventStore;
27
36
  accountId: number;
28
37
  zone: string;
29
38
  now: number;
30
39
  personOf?: (reference: string) => Promise<number | null>;
31
40
  }) => Promise<EventFilter | null>;
41
+ /** Events that end after the start of today and start within seven days, in `zone`. */
42
+ export declare const weekFilter: (accountId: number, zone: string, now: number) => EventFilter;
32
43
  /** An event the account holds, with only its copies, or `not_found`: another account's event is not shown. */
33
44
  export declare const eventOf: (store: EventStore, accountId: number, id: number) => Promise<EventDetails>;
34
45
  export declare const entryItem: (entry: StoredEntry) => {