@wirecat/cli-core 0.501.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.
- package/CHANGELOG.md +25 -1
- package/dist/calendar/busy.d.ts +9 -2
- package/dist/calendar/busy.js +61 -39
- package/dist/calendar/cli/answers.js +2 -2
- package/dist/calendar/cli/index.js +20 -21
- package/dist/calendar/mcp/index.d.ts +16 -16
- package/dist/calendar/mcp/index.js +8 -9
- package/dist/calendar/reads.d.ts +15 -4
- package/dist/calendar/reads.js +93 -43
- package/dist/calendar/testing/memory.js +4 -0
- package/dist/cli/index.d.ts +1 -1
- package/dist/cli/index.js +1 -1
- package/dist/cli/list-query.d.ts +5 -0
- package/dist/cli/list-query.js +9 -3
- package/dist/meetings/event-store.d.ts +4 -0
- package/dist/meetings/testing/event-store-contract.js +23 -0
- package/dist/store/sqlite/calendar-events.js +5 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,7 +5,31 @@ 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
|
-
##
|
|
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`.
|
|
9
33
|
|
|
10
34
|
## 0.501.0 — 31.10.2026
|
|
11
35
|
|
package/dist/calendar/busy.d.ts
CHANGED
|
@@ -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
|
-
|
|
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;
|
package/dist/calendar/busy.js
CHANGED
|
@@ -1,13 +1,8 @@
|
|
|
1
1
|
import { CliError } from "@wirecat/cli-base";
|
|
2
|
-
import { dayBoundary, zonedMoment } from "@wirecat/cli-base/query";
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
const
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
if (
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
47
|
-
|
|
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 {
|
|
13
|
-
import { CALENDAR_UNTRUSTED_FIELDS, calendarIn, calendarItem,
|
|
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
|
-
|
|
104
|
-
"
|
|
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
|
-
|
|
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
|
|
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("<
|
|
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 } : {}),
|
|
@@ -14,7 +14,7 @@ export declare const calendarTools: ({
|
|
|
14
14
|
type: string;
|
|
15
15
|
minimum: number;
|
|
16
16
|
};
|
|
17
|
-
|
|
17
|
+
query?: undefined;
|
|
18
18
|
timezone?: undefined;
|
|
19
19
|
limit?: undefined;
|
|
20
20
|
event?: undefined;
|
|
@@ -23,7 +23,7 @@ export declare const calendarTools: ({
|
|
|
23
23
|
type: string;
|
|
24
24
|
minimum: number;
|
|
25
25
|
};
|
|
26
|
-
|
|
26
|
+
query: {
|
|
27
27
|
type: string;
|
|
28
28
|
minLength: number;
|
|
29
29
|
};
|
|
@@ -38,7 +38,7 @@ export declare const calendarTools: ({
|
|
|
38
38
|
};
|
|
39
39
|
event?: undefined;
|
|
40
40
|
} | {
|
|
41
|
-
|
|
41
|
+
query?: undefined;
|
|
42
42
|
timezone?: undefined;
|
|
43
43
|
limit?: undefined;
|
|
44
44
|
event: {
|
|
@@ -50,7 +50,7 @@ export declare const calendarTools: ({
|
|
|
50
50
|
minimum: number;
|
|
51
51
|
};
|
|
52
52
|
} | {
|
|
53
|
-
|
|
53
|
+
query?: undefined;
|
|
54
54
|
timezone?: undefined;
|
|
55
55
|
limit?: undefined;
|
|
56
56
|
event?: undefined;
|
|
@@ -75,7 +75,7 @@ export declare const calendarTools: ({
|
|
|
75
75
|
type: string;
|
|
76
76
|
minimum: number;
|
|
77
77
|
};
|
|
78
|
-
|
|
78
|
+
query?: undefined;
|
|
79
79
|
timezone?: undefined;
|
|
80
80
|
limit?: undefined;
|
|
81
81
|
event?: undefined;
|
|
@@ -84,7 +84,7 @@ export declare const calendarTools: ({
|
|
|
84
84
|
type: string;
|
|
85
85
|
minimum: number;
|
|
86
86
|
};
|
|
87
|
-
|
|
87
|
+
query: {
|
|
88
88
|
type: string;
|
|
89
89
|
minLength: number;
|
|
90
90
|
};
|
|
@@ -99,7 +99,7 @@ export declare const calendarTools: ({
|
|
|
99
99
|
};
|
|
100
100
|
event?: undefined;
|
|
101
101
|
} | {
|
|
102
|
-
|
|
102
|
+
query?: undefined;
|
|
103
103
|
timezone?: undefined;
|
|
104
104
|
limit?: undefined;
|
|
105
105
|
event: {
|
|
@@ -111,7 +111,7 @@ export declare const calendarTools: ({
|
|
|
111
111
|
minimum: number;
|
|
112
112
|
};
|
|
113
113
|
} | {
|
|
114
|
-
|
|
114
|
+
query?: undefined;
|
|
115
115
|
timezone?: undefined;
|
|
116
116
|
limit?: undefined;
|
|
117
117
|
event?: undefined;
|
|
@@ -136,7 +136,7 @@ export declare const calendarTools: ({
|
|
|
136
136
|
type: string;
|
|
137
137
|
minimum: number;
|
|
138
138
|
};
|
|
139
|
-
|
|
139
|
+
query?: undefined;
|
|
140
140
|
timezone?: undefined;
|
|
141
141
|
limit?: undefined;
|
|
142
142
|
event?: undefined;
|
|
@@ -145,7 +145,7 @@ export declare const calendarTools: ({
|
|
|
145
145
|
type: string;
|
|
146
146
|
minimum: number;
|
|
147
147
|
};
|
|
148
|
-
|
|
148
|
+
query: {
|
|
149
149
|
type: string;
|
|
150
150
|
minLength: number;
|
|
151
151
|
};
|
|
@@ -160,7 +160,7 @@ export declare const calendarTools: ({
|
|
|
160
160
|
};
|
|
161
161
|
event?: undefined;
|
|
162
162
|
} | {
|
|
163
|
-
|
|
163
|
+
query?: undefined;
|
|
164
164
|
timezone?: undefined;
|
|
165
165
|
limit?: undefined;
|
|
166
166
|
event: {
|
|
@@ -172,7 +172,7 @@ export declare const calendarTools: ({
|
|
|
172
172
|
minimum: number;
|
|
173
173
|
};
|
|
174
174
|
} | {
|
|
175
|
-
|
|
175
|
+
query?: undefined;
|
|
176
176
|
timezone?: undefined;
|
|
177
177
|
limit?: undefined;
|
|
178
178
|
event?: undefined;
|
|
@@ -197,7 +197,7 @@ export declare const calendarTools: ({
|
|
|
197
197
|
type: string;
|
|
198
198
|
minimum: number;
|
|
199
199
|
};
|
|
200
|
-
|
|
200
|
+
query?: undefined;
|
|
201
201
|
timezone?: undefined;
|
|
202
202
|
limit?: undefined;
|
|
203
203
|
event?: undefined;
|
|
@@ -206,7 +206,7 @@ export declare const calendarTools: ({
|
|
|
206
206
|
type: string;
|
|
207
207
|
minimum: number;
|
|
208
208
|
};
|
|
209
|
-
|
|
209
|
+
query: {
|
|
210
210
|
type: string;
|
|
211
211
|
minLength: number;
|
|
212
212
|
};
|
|
@@ -221,7 +221,7 @@ export declare const calendarTools: ({
|
|
|
221
221
|
};
|
|
222
222
|
event?: undefined;
|
|
223
223
|
} | {
|
|
224
|
-
|
|
224
|
+
query?: undefined;
|
|
225
225
|
timezone?: undefined;
|
|
226
226
|
limit?: undefined;
|
|
227
227
|
event: {
|
|
@@ -233,7 +233,7 @@ export declare const calendarTools: ({
|
|
|
233
233
|
minimum: number;
|
|
234
234
|
};
|
|
235
235
|
} | {
|
|
236
|
-
|
|
236
|
+
query?: undefined;
|
|
237
237
|
timezone?: undefined;
|
|
238
238
|
limit?: undefined;
|
|
239
239
|
event?: undefined;
|
|
@@ -19,7 +19,7 @@ export const calendarTools = [
|
|
|
19
19
|
required: [],
|
|
20
20
|
},
|
|
21
21
|
{
|
|
22
|
-
name: "
|
|
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; `
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
properties: { account_id: integer,
|
|
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
|
-
"
|
|
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 "
|
|
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(
|
|
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",
|
package/dist/calendar/reads.d.ts
CHANGED
|
@@ -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
|
-
|
|
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`
|
|
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: (
|
|
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) => {
|
package/dist/calendar/reads.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { CliError } from "@wirecat/cli-base";
|
|
2
|
-
import { dayBoundary,
|
|
3
|
-
import {
|
|
2
|
+
import { dateRange, dayBoundary, queryError, queryFieldsOf, } from "@wirecat/cli-base/query";
|
|
3
|
+
import { LIST_QUERY_GUIDE, listQuery, nativeTerms, typedQuery } from "../cli/list-query.js";
|
|
4
|
+
import { event, eventChange, eventItem, listed, } from "../meetings/index.js";
|
|
4
5
|
const iso = (at) => (at === null ? null : new Date(at).toISOString());
|
|
5
6
|
/** Fields whose text whoever sent the invitation wrote: an agent reads them as data. */
|
|
6
7
|
export const CALENDAR_UNTRUSTED_FIELDS = [
|
|
@@ -45,65 +46,114 @@ export const plusDays = (date, days) => {
|
|
|
45
46
|
const [year, month, day] = date.split("-").map(Number);
|
|
46
47
|
return new Date(Date.UTC(year, month - 1, day + days)).toISOString().slice(0, 10);
|
|
47
48
|
};
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
49
|
+
const LIST = "events list";
|
|
50
|
+
const oneSided = (field, example) => (node) => {
|
|
51
|
+
if (node.operator !== "range" || node.upper !== "*")
|
|
52
|
+
queryError("query.unsupported-operator", node.span, `${field} takes only an earliest moment, such as ${example}`, {
|
|
53
|
+
command: LIST,
|
|
54
|
+
guide: LIST_QUERY_GUIDE,
|
|
55
|
+
});
|
|
56
|
+
};
|
|
57
|
+
/** `events list`'s query. The store answers `ends_at` and `changed_at` from below only; `starts_at` either way. */
|
|
58
|
+
export const EVENTS_QUERY = {
|
|
59
|
+
entity: "event",
|
|
60
|
+
backend: "native",
|
|
61
|
+
fields: queryFieldsOf(event, {
|
|
62
|
+
native: ["starts_at", "ends_at", "changed_at", "calendar", "person"],
|
|
63
|
+
relations: {
|
|
64
|
+
calendar: { type: "kw", example: "calendar:3", relation: "one calendar, by its id or the provider's id" },
|
|
65
|
+
person: {
|
|
66
|
+
type: "kw",
|
|
67
|
+
example: "person:ana@example.com",
|
|
68
|
+
relation: "events one person attends: an address, or a person id from search",
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
examples: {
|
|
72
|
+
starts_at: "starts_at:[2026-11-02 TO 2026-11-07}",
|
|
73
|
+
ends_at: "ends_at>=2026-11-02",
|
|
74
|
+
changed_at: "changed_at>=2026-10-01",
|
|
75
|
+
},
|
|
76
|
+
validate: {
|
|
77
|
+
ends_at: oneSided("ends_at", "ends_at>=2026-11-02; for the latest, write starts_at<2026-11-07"),
|
|
78
|
+
changed_at: oneSided("changed_at", "changed_at>=2026-10-01"),
|
|
79
|
+
},
|
|
80
|
+
}).map((field) => field.native
|
|
81
|
+
? { ...field, operators: field.type === "time" ? ["term", "range"] : ["term", "phrase"] }
|
|
82
|
+
: field),
|
|
83
|
+
};
|
|
84
|
+
export const EVENTS_QUERY_NOTE = "\nends_at keeps events that end after its moment, so one that ends exactly then is left out. Without starts_at " +
|
|
85
|
+
"or ends_at: events that end after the start of today, in your time zone.";
|
|
86
|
+
/** A time term as epoch ms: from `lower` on, and before `upper`. */
|
|
87
|
+
export const timeWindow = (node, zone, now) => {
|
|
88
|
+
const range = node.operator === "range"
|
|
89
|
+
? dateRange(node.value, node.upper ?? "*", node.lower_inclusive !== false, node.upper_inclusive !== false, zone, node.span, now)
|
|
90
|
+
: dateRange(node.value, node.value, true, true, zone, node.span, now);
|
|
91
|
+
return {
|
|
92
|
+
...(range.lower === undefined ? {} : { lower: range.lower_inclusive ? range.lower : range.lower + 1 }),
|
|
93
|
+
...(range.upper === undefined ? {} : { upper: range.upper_inclusive ? range.upper + 1 : range.upper }),
|
|
94
|
+
};
|
|
95
|
+
};
|
|
57
96
|
/**
|
|
58
|
-
* `events list`
|
|
59
|
-
* store; one nobody knows matches no event.
|
|
97
|
+
* `events list`'s query into a store filter, for one account. A person named by address is found through the
|
|
98
|
+
* shared store; one nobody knows matches no event.
|
|
60
99
|
*/
|
|
61
|
-
export const eventsFilter = async (
|
|
100
|
+
export const eventsFilter = async (query, context) => {
|
|
62
101
|
const filter = { accountId: context.accountId };
|
|
63
|
-
const
|
|
102
|
+
const { zone, now } = context;
|
|
64
103
|
const span = (text) => ({ start: 0, end: text.length });
|
|
65
|
-
const day = (date) => dayBoundary(date,
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
104
|
+
const day = (date) => dayBoundary(date, zone, false, span(date));
|
|
105
|
+
for (const [field, node] of nativeTerms(listQuery(typedQuery(query), EVENTS_QUERY, LIST), LIST)) {
|
|
106
|
+
if (field === "starts_at") {
|
|
107
|
+
const { lower, upper } = timeWindow(node, zone, now);
|
|
108
|
+
if (lower !== undefined)
|
|
109
|
+
filter.startsFrom = lower;
|
|
110
|
+
if (upper !== undefined)
|
|
111
|
+
filter.to = upper;
|
|
73
112
|
}
|
|
74
|
-
else if (field === "
|
|
75
|
-
|
|
76
|
-
|
|
113
|
+
else if (field === "ends_at") {
|
|
114
|
+
// Strictly after, as the default window: `ends_at>=today` leaves out yesterday's all-day event, which ends at
|
|
115
|
+
// today's midnight.
|
|
116
|
+
const { lower } = timeWindow(node, zone, now);
|
|
117
|
+
if (lower !== undefined)
|
|
118
|
+
filter.from = lower;
|
|
77
119
|
}
|
|
78
|
-
else if (field === "
|
|
79
|
-
|
|
80
|
-
|
|
120
|
+
else if (field === "changed_at") {
|
|
121
|
+
const { lower } = timeWindow(node, zone, now);
|
|
122
|
+
if (lower !== undefined)
|
|
123
|
+
filter.changedSince = lower;
|
|
81
124
|
}
|
|
82
|
-
else if (field === "changed")
|
|
83
|
-
filter.changedSince = zonedMoment(value, context.zone, span(value));
|
|
84
|
-
else if (word === "overlapping")
|
|
85
|
-
filter.overlapping = true;
|
|
86
125
|
else if (field === "calendar")
|
|
87
|
-
filter.calendarId = (await calendarIn(context.store, context.accountId, value)).id;
|
|
126
|
+
filter.calendarId = (await calendarIn(context.store, context.accountId, node.value)).id;
|
|
88
127
|
else if (field === "person") {
|
|
89
128
|
if (!context.personOf)
|
|
90
129
|
throw new CliError("configuration_error", "this tool cannot look people up");
|
|
91
|
-
const person = await context.personOf(value);
|
|
130
|
+
const person = await context.personOf(node.value);
|
|
92
131
|
if (person === null)
|
|
93
132
|
return null;
|
|
94
133
|
filter.personId = person;
|
|
95
134
|
}
|
|
96
|
-
else
|
|
97
|
-
throw new CliError("validation_error", `${word} is not an events list word${EVENTS_QUERY_HELP}`, {
|
|
98
|
-
reason: "query.unknown_word",
|
|
99
|
-
});
|
|
100
135
|
}
|
|
101
|
-
if (
|
|
102
|
-
filter.from = day(
|
|
136
|
+
if (filter.from === undefined && filter.startsFrom === undefined && filter.to === undefined)
|
|
137
|
+
filter.from = day(localDate(now, zone));
|
|
138
|
+
return datedFilter(filter, zone);
|
|
139
|
+
};
|
|
140
|
+
/** Events that end after the start of today and start within seven days, in `zone`. */
|
|
141
|
+
export const weekFilter = (accountId, zone, now) => {
|
|
142
|
+
const today = localDate(now, zone);
|
|
143
|
+
const day = (date) => dayBoundary(date, zone, false, { start: 0, end: date.length });
|
|
144
|
+
return datedFilter({ accountId, from: day(today), to: day(plusDays(today, 7)) }, zone);
|
|
145
|
+
};
|
|
146
|
+
/** The local days an all-day event is compared by, from the filter's instants. */
|
|
147
|
+
const datedFilter = (filter, zone) => {
|
|
103
148
|
if (filter.from !== undefined)
|
|
104
|
-
filter.fromDate = localDate(filter.from,
|
|
149
|
+
filter.fromDate = localDate(filter.from, zone);
|
|
105
150
|
if (filter.to !== undefined)
|
|
106
|
-
filter.toDate = plusDays(localDate(filter.to - 1,
|
|
151
|
+
filter.toDate = plusDays(localDate(filter.to - 1, zone), 1);
|
|
152
|
+
if (filter.startsFrom !== undefined) {
|
|
153
|
+
const date = localDate(filter.startsFrom, zone);
|
|
154
|
+
const midnight = dayBoundary(date, zone, false, { start: 0, end: date.length });
|
|
155
|
+
filter.startsFromDate = midnight < filter.startsFrom ? plusDays(date, 1) : date;
|
|
156
|
+
}
|
|
107
157
|
return filter;
|
|
108
158
|
};
|
|
109
159
|
/**
|
|
@@ -474,6 +474,10 @@ export const memoryEventStore = () => {
|
|
|
474
474
|
(event.allDay && filter.toDate !== undefined
|
|
475
475
|
? has(event, (one) => one.startDate !== null && one.startDate < (filter.toDate ?? ""))
|
|
476
476
|
: event.startsAt !== null && event.startsAt < filter.to)) &&
|
|
477
|
+
(filter.startsFrom === undefined ||
|
|
478
|
+
(event.allDay && filter.startsFromDate !== undefined
|
|
479
|
+
? has(event, (one) => one.startDate !== null && one.startDate >= (filter.startsFromDate ?? ""))
|
|
480
|
+
: event.startsAt !== null && event.startsAt >= filter.startsFrom)) &&
|
|
477
481
|
(filter.calendarId === undefined || has(event, (one) => one.calendarId === filter.calendarId)) &&
|
|
478
482
|
(filter.accountId === undefined ||
|
|
479
483
|
has(event, (one) => calendarOf(one.calendarId).accountId === filter.accountId)) &&
|
package/dist/cli/index.d.ts
CHANGED
|
@@ -77,7 +77,7 @@ export { type FirstScreen, type FirstScreenLine, firstScreen, installHelp, WIREC
|
|
|
77
77
|
export { type HelpSection, inSection, SECTIONS } from "./help/sections.js";
|
|
78
78
|
export { bufferedInput, type InputPolicy, inputPolicy, MAX_BUFFERED_INPUT, MAX_SECRET_INPUT, provideInputPolicy, } from "./input-policy.js";
|
|
79
79
|
export { answerFields, checkJson, type EntityFields, entityFields, type JsonFieldsResolver, type JsonRequest, liftJson, } from "./json-fields.js";
|
|
80
|
-
export { commandPath, LIST_QUERY_GUIDE, listQuery, nativeField, nativeTerms, pagedList, pageOf, type QueryWords, queryArgument, queryHelp, queryTerms, searchText, termField, timeBounds, typedQuery, unfilteredQuery, withQuery, } from "./list-query.js";
|
|
80
|
+
export { commandPath, LIST_QUERY_GUIDE, listQuery, nativeField, nativeTermLists, nativeTerms, pagedList, pageOf, type QueryWords, queryArgument, queryHelp, queryTerms, searchText, termField, timeBounds, typedQuery, unfilteredQuery, withQuery, } from "./list-query.js";
|
|
81
81
|
export { MCP_HTTP_DEFAULT_PORT, type McpServerOptions, type McpServerTarget, mcpServerCommand, } from "./mcp-server-command.js";
|
|
82
82
|
export { formatMeetingBatch, prepareMeetingBatch } from "./meeting-batch.js";
|
|
83
83
|
export { meetingEvidenceItem, meetingPersonContextItem } from "./meeting-evidence-answer.js";
|
package/dist/cli/index.js
CHANGED
|
@@ -69,7 +69,7 @@ export { firstScreen, installHelp, WIRECAT_TOOLS } from "./help/first-screen.js"
|
|
|
69
69
|
export { inSection, SECTIONS } from "./help/sections.js";
|
|
70
70
|
export { bufferedInput, inputPolicy, MAX_BUFFERED_INPUT, MAX_SECRET_INPUT, provideInputPolicy, } from "./input-policy.js";
|
|
71
71
|
export { answerFields, checkJson, entityFields, liftJson, } from "./json-fields.js";
|
|
72
|
-
export { commandPath, LIST_QUERY_GUIDE, listQuery, nativeField, nativeTerms, pagedList, pageOf, queryArgument, queryHelp, queryTerms, searchText, termField, timeBounds, typedQuery, unfilteredQuery, withQuery, } from "./list-query.js";
|
|
72
|
+
export { commandPath, LIST_QUERY_GUIDE, listQuery, nativeField, nativeTermLists, nativeTerms, pagedList, pageOf, queryArgument, queryHelp, queryTerms, searchText, termField, timeBounds, typedQuery, unfilteredQuery, withQuery, } from "./list-query.js";
|
|
73
73
|
export { MCP_HTTP_DEFAULT_PORT, mcpServerCommand, } from "./mcp-server-command.js";
|
|
74
74
|
export { formatMeetingBatch, prepareMeetingBatch } from "./meeting-batch.js";
|
|
75
75
|
export { meetingEvidenceItem, meetingPersonContextItem } from "./meeting-evidence-answer.js";
|
package/dist/cli/list-query.d.ts
CHANGED
|
@@ -39,6 +39,11 @@ export declare const queryArgument: (registry: QueryRegistry) => v.OptionalSchem
|
|
|
39
39
|
* question the source cannot ask, so it is refused rather than answered from a part of the list.
|
|
40
40
|
*/
|
|
41
41
|
export declare const nativeTerms: (ast: QueryAst | undefined, command: string) => Map<string, Predicate>;
|
|
42
|
+
/**
|
|
43
|
+
* As `nativeTerms`, but each `repeatable` field may be named again, every term kept in order: `person:a person:b`
|
|
44
|
+
* asks for both, as AND reads.
|
|
45
|
+
*/
|
|
46
|
+
export declare const nativeTermLists: (ast: QueryAst | undefined, command: string, repeatable?: readonly string[]) => Map<string, Predicate[]>;
|
|
42
47
|
/**
|
|
43
48
|
* A store that compares whole values: each field's `field:value` or `field:"two words"`, enum values lowercased
|
|
44
49
|
* as validation already accepts them. A range or wildcard is refused rather than read as a value.
|
package/dist/cli/list-query.js
CHANGED
|
@@ -91,7 +91,12 @@ export const queryArgument = (registry) => {
|
|
|
91
91
|
* The terms of a query a provider answers: field:value joined by AND, each field once. Anything else is a
|
|
92
92
|
* question the source cannot ask, so it is refused rather than answered from a part of the list.
|
|
93
93
|
*/
|
|
94
|
-
export const nativeTerms = (ast, command) =>
|
|
94
|
+
export const nativeTerms = (ast, command) => new Map([...nativeTermLists(ast, command)].map(([field, [node]]) => [field, node]));
|
|
95
|
+
/**
|
|
96
|
+
* As `nativeTerms`, but each `repeatable` field may be named again, every term kept in order: `person:a person:b`
|
|
97
|
+
* asks for both, as AND reads.
|
|
98
|
+
*/
|
|
99
|
+
export const nativeTermLists = (ast, command, repeatable = []) => {
|
|
95
100
|
const terms = new Map();
|
|
96
101
|
if (!ast)
|
|
97
102
|
return terms;
|
|
@@ -106,9 +111,10 @@ export const nativeTerms = (ast, command) => {
|
|
|
106
111
|
}
|
|
107
112
|
return;
|
|
108
113
|
}
|
|
109
|
-
|
|
114
|
+
const held = terms.get(node.field);
|
|
115
|
+
if (held && !repeatable.includes(node.field))
|
|
110
116
|
queryError("query.unsupported-placement", node.span, `${node.field} only once here`, context);
|
|
111
|
-
terms.set(node.field, node);
|
|
117
|
+
terms.set(node.field, [...(held ?? []), node]);
|
|
112
118
|
};
|
|
113
119
|
add(ast.root);
|
|
114
120
|
return terms;
|
|
@@ -185,6 +185,10 @@ export interface EventFilter {
|
|
|
185
185
|
from?: number;
|
|
186
186
|
/** Events that start before this instant. */
|
|
187
187
|
to?: number;
|
|
188
|
+
/** Events that start at or after this instant. */
|
|
189
|
+
startsFrom?: number;
|
|
190
|
+
/** All-day events starting on or after this local day (`YYYY-MM-DD`); `startsFrom` without it. */
|
|
191
|
+
startsFromDate?: string;
|
|
188
192
|
/** All-day events, which have dates and no instant, ending after this local day (`YYYY-MM-DD`); `from` without it. */
|
|
189
193
|
fromDate?: string;
|
|
190
194
|
/** Events `eventChange` finds new, moved or cancelled at or after this instant. */
|
|
@@ -281,6 +281,29 @@ export const eventStoreContract = (make) => {
|
|
|
281
281
|
])
|
|
282
282
|
assert.deepEqual((await store.events(filter)).map(({ title }) => title).length, 3, JSON.stringify(filter));
|
|
283
283
|
}),
|
|
284
|
+
test("a list from a start keeps the event that starts exactly then, and an all-day event by its local date", async ({ store, calendar, }) => {
|
|
285
|
+
const day = 24 * HOUR;
|
|
286
|
+
const day1 = new Date(later + day).toISOString().slice(0, 10);
|
|
287
|
+
const day0 = new Date(later).toISOString().slice(0, 10);
|
|
288
|
+
// Midnight of day1 an hour east of UTC; the all-day events are stored at a midnight three hours east.
|
|
289
|
+
const startsFrom = Date.parse(day1) - HOUR;
|
|
290
|
+
const allDay = (id, date, next) => single(id, Date.parse(date) - 3 * HOUR, {
|
|
291
|
+
title: id,
|
|
292
|
+
allDay: true,
|
|
293
|
+
startDate: date,
|
|
294
|
+
endDate: next,
|
|
295
|
+
endsAt: Date.parse(next) - 3 * HOUR,
|
|
296
|
+
});
|
|
297
|
+
await store.saveEntries(calendar, [
|
|
298
|
+
single("at", startsFrom, { title: "at" }),
|
|
299
|
+
single("before", startsFrom - 1, { title: "before" }),
|
|
300
|
+
allDay("on", day1, new Date(later + 2 * day).toISOString().slice(0, 10)),
|
|
301
|
+
allDay("day before", day0, day1),
|
|
302
|
+
], 2);
|
|
303
|
+
const titles = async (filter) => (await store.events(filter)).map(({ title }) => title).sort();
|
|
304
|
+
assert.deepEqual(await titles({ startsFrom, startsFromDate: day1 }), ["at", "on"]);
|
|
305
|
+
assert.deepEqual(await titles({ startsFrom }), ["at"]);
|
|
306
|
+
}),
|
|
284
307
|
test("a list of changes since an instant finds events new, moved or cancelled since, and says which", async ({ store, calendar, }) => {
|
|
285
308
|
await store.saveEntries(calendar, ["same", "moved", "back", "cancelled"].map((id) => single(id, later)), 2);
|
|
286
309
|
await store.saveEntries(calendar, [
|
|
@@ -19,6 +19,7 @@ export const listEvents = (database, filter) => {
|
|
|
19
19
|
};
|
|
20
20
|
dated("end_date > ?", filter.fromDate, "coalesce(e.ends_at, e.starts_at + 1) > ?", filter.from);
|
|
21
21
|
dated("start_date < ?", filter.toDate, "e.starts_at < ?", filter.to);
|
|
22
|
+
dated("start_date >= ?", filter.startsFromDate, "e.starts_at >= ?", filter.startsFrom);
|
|
22
23
|
// Bounds on starts_at alone let SQLite seek its index; the CASE above cannot. They are wider than the CASE: by the
|
|
23
24
|
// longest stored event back, and by a day each way for an all-day event's zone.
|
|
24
25
|
const longest = () => Number(database.prepare("SELECT coalesce(max(ends_at - starts_at), 0) AS ms FROM events WHERE deleted_at IS NULL").get()
|
|
@@ -31,6 +32,10 @@ export const listEvents = (database, filter) => {
|
|
|
31
32
|
clauses.push("e.starts_at < ?");
|
|
32
33
|
params.push(filter.to + DAY);
|
|
33
34
|
}
|
|
35
|
+
if (filter.startsFrom !== undefined && filter.startsFromDate !== undefined) {
|
|
36
|
+
clauses.push("e.starts_at >= ?");
|
|
37
|
+
params.push(filter.startsFrom - DAY);
|
|
38
|
+
}
|
|
34
39
|
if (filter.calendarId !== undefined) {
|
|
35
40
|
clauses.push(entries("ce.calendar_id = ?"));
|
|
36
41
|
params.push(filter.calendarId);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wirecat/cli-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.502.0",
|
|
4
4
|
"description": "The messenger-neutral half of a messaging CLI: domain model, message locators, rendering, name resolution, the SQLite seam, the command skeleton and shared read commands, the send guard, run records and the message store",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Viacheslav Ptsarev",
|