@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
@@ -1,6 +1,7 @@
1
1
  import { CliError } from "@wirecat/cli-base";
2
- import { dayBoundary, zonedMoment } from "@wirecat/cli-base/query";
3
- import { eventChange, eventItem, listed, } from "../meetings/index.js";
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
- export const EVENTS_QUERY_HELP = `
49
- Words, any order:
50
- today | week today, or today and the next six days, in your time zone
51
- from:<date> to:<date> from the start of one day through the end of another (or a time with a zone)
52
- person:<email|id> events one person attends: an address, or a person id from search
53
- calendar:<id> one calendar, by its id or the provider's id
54
- changed:<date> events new, moved or cancelled since then, each with what changed (from the local copy)
55
- overlapping events that clash with another: timed, not cancelled, not declined, not marked free
56
- Without a time word: events from the start of today on.`;
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` words into a store filter, for one account. A person named by address is found through the shared
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 (words, context) => {
100
+ export const eventsFilter = async (query, context) => {
62
101
  const filter = { accountId: context.accountId };
63
- const today = localDate(context.now, context.zone);
102
+ const { zone, now } = context;
64
103
  const span = (text) => ({ start: 0, end: text.length });
65
- const day = (date) => dayBoundary(date, context.zone, false, span(date));
66
- let timed = false;
67
- for (const word of words) {
68
- const [, field, value = ""] = /^([a-z]+):(.+)$/u.exec(word) ?? [];
69
- if (word === "today" || word === "week") {
70
- filter.from = day(today);
71
- filter.to = day(plusDays(today, word === "today" ? 1 : 7));
72
- timed = true;
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 === "from") {
75
- filter.from = zonedMoment(value, context.zone, span(value));
76
- timed = true;
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 === "to") {
79
- filter.to = zonedMoment(value, context.zone, span(value), { end: true }) + 1;
80
- timed = true;
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 (!timed)
102
- filter.from = day(today);
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, context.zone);
149
+ filter.fromDate = localDate(filter.from, zone);
105
150
  if (filter.to !== undefined)
106
- filter.toDate = plusDays(localDate(filter.to - 1, context.zone), 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)) &&
@@ -1,5 +1,5 @@
1
1
  import { type Renderer, type RenderFormat, type Streams } from "@wirecat/cli-base";
2
- import { type ItemEnvelope, type ListEnvelope, type Meta, type Missing, type NextStep, type StepInput } from "@wirecat/cli-base/standards";
2
+ import { type ItemEnvelope, type ListEnvelope, type Meta, type Missing, type NextStep, type StepInput, type Total } from "@wirecat/cli-base/standards";
3
3
  import type { Command } from "commander";
4
4
  import * as v from "valibot";
5
5
  export type ListBody<T> = Omit<ListEnvelope<T>, "meta"> & {
@@ -19,6 +19,8 @@ export interface ListInput<T> {
19
19
  paging?: "cursor";
20
20
  /** The page shown, from 1, for a list paged by `--page`. */
21
21
  page?: number;
22
+ /** How many match, every page together, for a search that counts them. */
23
+ total?: Total;
22
24
  /** The command's own arguments and query, so `page.next` reruns the same list. */
23
25
  args?: Record<string, unknown>;
24
26
  next?: NextStep[];
@@ -33,11 +35,19 @@ export declare const answerSchema: (item?: v.GenericSchema) => v.UnionSchema<[v.
33
35
  readonly limit: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 1, undefined>]>;
34
36
  readonly has_more: v.BooleanSchema<undefined>;
35
37
  readonly next_page: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 2, undefined>]>, undefined>;
38
+ readonly total: v.OptionalSchema<v.StrictObjectSchema<{
39
+ readonly value: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
40
+ readonly relation: v.PicklistSchema<["eq", "gte"], undefined>;
41
+ }, undefined>, undefined>;
36
42
  }, undefined>, v.StrictObjectSchema<{
37
43
  readonly mode: v.LiteralSchema<"cursor", undefined>;
38
44
  readonly limit: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 1, undefined>]>;
39
45
  readonly has_more: v.BooleanSchema<undefined>;
40
46
  readonly next_cursor: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "must not be empty">]>, undefined>;
47
+ readonly total: v.OptionalSchema<v.StrictObjectSchema<{
48
+ readonly value: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
49
+ readonly relation: v.PicklistSchema<["eq", "gte"], undefined>;
50
+ }, undefined>, undefined>;
41
51
  }, undefined>], undefined>;
42
52
  readonly next: v.OptionalSchema<v.SchemaWithPipe<readonly [v.ArraySchema<v.VariantSchema<"kind", [v.StrictObjectSchema<{
43
53
  readonly kind: v.LiteralSchema<"command", undefined>;
@@ -14,7 +14,7 @@ const capped = (steps) => (steps.length > 0 ? { next: steps.slice(0, MAX_NEXT_ST
14
14
  * page where the source has no cursor.
15
15
  */
16
16
  export const listOf = (command, input) => {
17
- const { items, limit, has_more, cursor, paging, page, args = {}, next = [], missing, meta } = input;
17
+ const { items, limit, has_more, cursor, paging, page, total, args = {}, next = [], missing, meta } = input;
18
18
  const byCursor = paging === "cursor" || cursor !== undefined;
19
19
  const more = has_more
20
20
  ? [
@@ -28,8 +28,20 @@ export const listOf = (command, input) => {
28
28
  const body = {
29
29
  items,
30
30
  page: byCursor
31
- ? { mode: "cursor", limit, has_more, ...(has_more && cursor !== undefined ? { next_cursor: cursor } : {}) }
32
- : { mode: "page", limit, has_more, ...(has_more ? { next_page: (page ?? 1) + 1 } : {}) },
31
+ ? {
32
+ mode: "cursor",
33
+ limit,
34
+ has_more,
35
+ ...(has_more && cursor !== undefined ? { next_cursor: cursor } : {}),
36
+ ...(total ? { total } : {}),
37
+ }
38
+ : {
39
+ mode: "page",
40
+ limit,
41
+ has_more,
42
+ ...(has_more ? { next_page: (page ?? 1) + 1 } : {}),
43
+ ...(total ? { total } : {}),
44
+ },
33
45
  ...capped([...more, ...next]),
34
46
  ...(missing && missing.length > 0 ? { missing } : {}),
35
47
  ...(meta ? { meta } : {}),
@@ -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";
@@ -96,7 +96,7 @@ export { httpTokenFile, type McpEnvironment, type McpHost, serverEntry } from ".
96
96
  export { type MessageItem, messageItem, type ReadMessage } from "./messenger/message-items.js";
97
97
  export { type ModeProblem, type PrivateFiles, privateFiles, withSqliteSidecars } from "./messenger/private-files.js";
98
98
  export { buildReport as buildDoctorReport, type Report as DoctorReport, type ReportContext as DoctorReportContext, reportFileName as doctorReportFileName, } from "./messenger/report.js";
99
- export { allAnswer, allHitItem, coverageGaps, type HitItem, type MessageHitItem, messageHitItem, type NoteHitItem, noteHitItem, } from "./messenger/search-answers.js";
99
+ export { allAnswer, allHitItem, coverageGaps, type HitItem, type MessageHitItem, messageHitItem, type NoteHitItem, noteHitItem, shownOf, } from "./messenger/search-answers.js";
100
100
  export { SEARCHES_QUERY } from "./messenger/searches-command.js";
101
101
  export { servingProfiles } from "./messenger/serve-command.js";
102
102
  export { DAY_MS, FIRST_FETCH_MS, type MemberFetches, memberFetches } from "./messenger/serve-members.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";
@@ -87,7 +87,7 @@ export { httpTokenFile, serverEntry } from "./messenger/mcp-command.js";
87
87
  export { messageItem } from "./messenger/message-items.js";
88
88
  export { privateFiles, withSqliteSidecars } from "./messenger/private-files.js";
89
89
  export { buildReport as buildDoctorReport, reportFileName as doctorReportFileName, } from "./messenger/report.js";
90
- export { allAnswer, allHitItem, coverageGaps, messageHitItem, noteHitItem, } from "./messenger/search-answers.js";
90
+ export { allAnswer, allHitItem, coverageGaps, messageHitItem, noteHitItem, shownOf, } from "./messenger/search-answers.js";
91
91
  export { SEARCHES_QUERY } from "./messenger/searches-command.js";
92
92
  export { servingProfiles } from "./messenger/serve-command.js";
93
93
  export { DAY_MS, FIRST_FETCH_MS, memberFetches } from "./messenger/serve-members.js";
@@ -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.
@@ -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
- if (terms.has(node.field))
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;
@@ -345,11 +345,19 @@ export declare const meetingPreparationAnswer: (packet: MeetingPreparation) => i
345
345
  limit: number;
346
346
  has_more: boolean;
347
347
  next_page?: number | undefined;
348
+ total?: {
349
+ value: number;
350
+ relation: "eq" | "gte";
351
+ } | undefined;
348
352
  } | {
349
353
  mode: "cursor";
350
354
  limit: number;
351
355
  has_more: boolean;
352
356
  next_cursor?: string | undefined;
357
+ total?: {
358
+ value: number;
359
+ relation: "eq" | "gte";
360
+ } | undefined;
353
361
  };
354
362
  coverage: {
355
363
  archive_completeness: "unknown";
@@ -65,11 +65,19 @@ export declare const mapPreparationProjectEvidence: (section: NonNullable<Meetin
65
65
  limit: number;
66
66
  has_more: boolean;
67
67
  next_page?: number | undefined;
68
+ total?: {
69
+ value: number;
70
+ relation: "eq" | "gte";
71
+ } | undefined;
68
72
  } | {
69
73
  mode: "cursor";
70
74
  limit: number;
71
75
  has_more: boolean;
72
76
  next_cursor?: string | undefined;
77
+ total?: {
78
+ value: number;
79
+ relation: "eq" | "gte";
80
+ } | undefined;
73
81
  };
74
82
  coverage: {
75
83
  archive_completeness: "unknown";
@@ -374,11 +374,19 @@ export declare const readMeetingPreparation: (read: MeetingPreparationReader, in
374
374
  limit: number;
375
375
  has_more: boolean;
376
376
  next_page?: number | undefined;
377
+ total?: {
378
+ value: number;
379
+ relation: "eq" | "gte";
380
+ } | undefined;
377
381
  } | {
378
382
  mode: "cursor";
379
383
  limit: number;
380
384
  has_more: boolean;
381
385
  next_cursor?: string | undefined;
386
+ total?: {
387
+ value: number;
388
+ relation: "eq" | "gte";
389
+ } | undefined;
382
390
  };
383
391
  coverage: {
384
392
  archive_completeness: "unknown";
@@ -34,11 +34,19 @@ export declare const readMeetingSeriesInventory: (read: MeetingSeriesInventoryRe
34
34
  limit: number;
35
35
  has_more: boolean;
36
36
  next_page?: number | undefined;
37
+ total?: {
38
+ value: number;
39
+ relation: "eq" | "gte";
40
+ } | undefined;
37
41
  } | {
38
42
  mode: "cursor";
39
43
  limit: number;
40
44
  has_more: boolean;
41
45
  next_cursor?: string | undefined;
46
+ total?: {
47
+ value: number;
48
+ relation: "eq" | "gte";
49
+ } | undefined;
42
50
  };
43
51
  fingerprint: string;
44
52
  limits: {
@@ -12,7 +12,7 @@ import { renderAnswer } from "../envelope.js";
12
12
  import { positiveCount } from "../paging.js";
13
13
  import { archiveSummary } from "./archive-summary.js";
14
14
  import { messengerContext } from "./context.js";
15
- import { meaningNote, messagesAnswer, offsetOf } from "./search-answers.js";
15
+ import { meaningNote, messagesAnswer, offsetOf, shownOf } from "./search-answers.js";
16
16
  import { backendOptions, backendRequest, noteServer } from "./search-backend-options.js";
17
17
  import { syncOptions, syncRequest, syncsFirst } from "./search-sync-options.js";
18
18
  import { threadOptions, threadRequest } from "./thread-options.js";
@@ -184,6 +184,9 @@ const searchAction = (messenger) => async function (given) {
184
184
  if (elsewhere.length > 0) {
185
185
  context.renderer.note(`hits in ${elsewhere.join(", ")} open in that messenger's own CLI, by the locator: messages context msg:…`);
186
186
  }
187
+ const shown = shownOf(found.items.length, offsetOf(page, limit) ?? 0, found.total);
188
+ if (shown)
189
+ context.renderer.note(shown);
187
190
  if (found.items.length === 0)
188
191
  context.renderer.note(found.server && !found.server.skipped
189
192
  ? "nothing found in the local store or on the messenger's server"
@@ -1,6 +1,7 @@
1
1
  import type { Missing, NextStep } from "@wirecat/cli-base/standards";
2
2
  import type { InferOutput } from "valibot";
3
3
  import type { ENTITIES } from "../../entities/index.js";
4
+ import type { SearchTotal } from "../../search/total.js";
4
5
  import type { ChatCoverage } from "../../services/chat-coverage.js";
5
6
  import type { FoundMessage, SearchFound } from "../../services/messages.js";
6
7
  import type { MeaningSkipped } from "../../services/messages-discovery.js";
@@ -33,6 +34,8 @@ export interface Paging {
33
34
  /** The query and options `page.next` repeats. */
34
35
  again?: Record<string, unknown>;
35
36
  }
37
+ /** "showing 20 of 144", for the human view under a page of hits; `+` when the total is a lower bound. */
38
+ export declare const shownOf: (shown: number, offset: number, total: SearchTotal | undefined) => string | undefined;
36
39
  /** `search messages` and `search mail`, CLI and MCP alike. */
37
40
  export declare const messagesAnswer: (command: string, found: SearchFound, { limit, page, again }: Paging, query?: string) => ListBody<MessageHitItem>;
38
41
  export declare const noteHitItem: (hit: FoundNote) => NoteHitItem;
@@ -227,6 +227,10 @@ const pictureItems = (found) => {
227
227
  untrusted: ["sender_name"],
228
228
  }));
229
229
  };
230
+ /** "showing 20 of 144", for the human view under a page of hits; `+` when the total is a lower bound. */
231
+ export const shownOf = (shown, offset, total) => total === undefined || shown === 0
232
+ ? undefined
233
+ : `showing ${offset + 1}–${offset + shown} of ${total.value}${total.relation === "gte" ? "+" : ""}`;
230
234
  /** `search messages` and `search mail`, CLI and MCP alike. */
231
235
  export const messagesAnswer = (command, found, { limit, page, again }, query = "") => {
232
236
  const accounts = found.coverage?.accounts ?? found.accounts ?? [];
@@ -235,6 +239,7 @@ export const messagesAnswer = (command, found, { limit, page, again }, query = "
235
239
  limit,
236
240
  has_more: found.hasMore && (page ?? 1) * limit <= MAX_SEARCH_OFFSET,
237
241
  page: page ?? 1,
242
+ ...(found.total ? { total: found.total } : {}),
238
243
  ...(again ? { args: again } : {}),
239
244
  ...gapsOf(found),
240
245
  meta: metaOf(accounts, found.skipped, coverageGaps(found).missing.length > 0),
@@ -297,6 +302,7 @@ export const notesAnswer = (command, found, { limit, page, again }, exact = fals
297
302
  limit,
298
303
  has_more: found.has_more && found.next_offset !== undefined,
299
304
  page: page ?? 1,
305
+ total: found.total,
300
306
  ...(again ? { args: again } : {}),
301
307
  ...meaningGap(found.meaning_skipped, exact),
302
308
  meta: metaOf(found.accounts, found.skipped),
@@ -11,7 +11,7 @@ import { commandAt } from "../steps.js";
11
11
  import { messengerContext } from "./context.js";
12
12
  import { conversationsSearchCommand } from "./conversations-command.js";
13
13
  import { messagesSearchCommand } from "./messages-search-command.js";
14
- import { allAnswer, meaningNote, notesAnswer, offsetOf, SOURCE_SEARCHED, } from "./search-answers.js";
14
+ import { allAnswer, meaningNote, notesAnswer, offsetOf, SOURCE_SEARCHED, shownOf, } from "./search-answers.js";
15
15
  import { backendOptions, backendRequest, noteServer } from "./search-backend-options.js";
16
16
  import { topicsSearchCommand } from "./topics-command.js";
17
17
  const onlyOf = (value) => {
@@ -150,6 +150,9 @@ const notesCommand = (messenger) => annotate(new Command("notes"), { effects: LO
150
150
  renderAnswer(this, context, notesAnswer("search notes", found, { limit, page: options.page ?? 1, again }), () => {
151
151
  if (found.hits.length === 0)
152
152
  context.renderer.note("no note matches");
153
+ const shown = shownOf(found.hits.length, ((options.page ?? 1) - 1) * limit, found.total);
154
+ if (shown)
155
+ context.renderer.note(shown);
153
156
  return found.hits
154
157
  .map((hit) => `${singleLine(hit.title ?? hit.path ?? hit.ref)} ${hit.ref} (${hit.found_by.join(", ")})${hit.url ? `\n ${singleLine(hit.url)}` : ""}\n ${singleLine(hit.excerpt)}\n`)
155
158
  .join("");
@@ -5529,11 +5529,19 @@ export declare const ENTITIES: {
5529
5529
  readonly limit: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 1, undefined>]>;
5530
5530
  readonly has_more: v.BooleanSchema<undefined>;
5531
5531
  readonly next_page: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 2, undefined>]>, undefined>;
5532
+ readonly total: v.OptionalSchema<v.StrictObjectSchema<{
5533
+ readonly value: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
5534
+ readonly relation: v.PicklistSchema<["eq", "gte"], undefined>;
5535
+ }, undefined>, undefined>;
5532
5536
  }, undefined>, v.StrictObjectSchema<{
5533
5537
  readonly mode: v.LiteralSchema<"cursor", undefined>;
5534
5538
  readonly limit: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 1, undefined>]>;
5535
5539
  readonly has_more: v.BooleanSchema<undefined>;
5536
5540
  readonly next_cursor: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "must not be empty">]>, undefined>;
5541
+ readonly total: v.OptionalSchema<v.StrictObjectSchema<{
5542
+ readonly value: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
5543
+ readonly relation: v.PicklistSchema<["eq", "gte"], undefined>;
5544
+ }, undefined>, undefined>;
5537
5545
  }, undefined>], undefined>;
5538
5546
  readonly fingerprint: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, 200, undefined>]>;
5539
5547
  readonly limits: v.StrictObjectSchema<{
@@ -5689,11 +5697,19 @@ export declare const ENTITIES: {
5689
5697
  readonly limit: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 1, undefined>]>;
5690
5698
  readonly has_more: v.BooleanSchema<undefined>;
5691
5699
  readonly next_page: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 2, undefined>]>, undefined>;
5700
+ readonly total: v.OptionalSchema<v.StrictObjectSchema<{
5701
+ readonly value: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
5702
+ readonly relation: v.PicklistSchema<["eq", "gte"], undefined>;
5703
+ }, undefined>, undefined>;
5692
5704
  }, undefined>, v.StrictObjectSchema<{
5693
5705
  readonly mode: v.LiteralSchema<"cursor", undefined>;
5694
5706
  readonly limit: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 1, undefined>]>;
5695
5707
  readonly has_more: v.BooleanSchema<undefined>;
5696
5708
  readonly next_cursor: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "must not be empty">]>, undefined>;
5709
+ readonly total: v.OptionalSchema<v.StrictObjectSchema<{
5710
+ readonly value: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
5711
+ readonly relation: v.PicklistSchema<["eq", "gte"], undefined>;
5712
+ }, undefined>, undefined>;
5697
5713
  }, undefined>], undefined>;
5698
5714
  readonly coverage: v.StrictObjectSchema<{
5699
5715
  readonly archive_completeness: v.LiteralSchema<"unknown", undefined>;
@@ -6052,11 +6068,19 @@ export declare const ENTITIES: {
6052
6068
  readonly limit: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 1, undefined>]>;
6053
6069
  readonly has_more: v.BooleanSchema<undefined>;
6054
6070
  readonly next_page: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 2, undefined>]>, undefined>;
6071
+ readonly total: v.OptionalSchema<v.StrictObjectSchema<{
6072
+ readonly value: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
6073
+ readonly relation: v.PicklistSchema<["eq", "gte"], undefined>;
6074
+ }, undefined>, undefined>;
6055
6075
  }, undefined>, v.StrictObjectSchema<{
6056
6076
  readonly mode: v.LiteralSchema<"cursor", undefined>;
6057
6077
  readonly limit: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 1, undefined>]>;
6058
6078
  readonly has_more: v.BooleanSchema<undefined>;
6059
6079
  readonly next_cursor: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "must not be empty">]>, undefined>;
6080
+ readonly total: v.OptionalSchema<v.StrictObjectSchema<{
6081
+ readonly value: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
6082
+ readonly relation: v.PicklistSchema<["eq", "gte"], undefined>;
6083
+ }, undefined>, undefined>;
6060
6084
  }, undefined>], undefined>;
6061
6085
  readonly coverage: v.StrictObjectSchema<{
6062
6086
  readonly archive_completeness: v.LiteralSchema<"unknown", undefined>;
@@ -345,11 +345,19 @@ export declare const meetingPreparation: v.StrictObjectSchema<{
345
345
  readonly limit: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 1, undefined>]>;
346
346
  readonly has_more: v.BooleanSchema<undefined>;
347
347
  readonly next_page: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 2, undefined>]>, undefined>;
348
+ readonly total: v.OptionalSchema<v.StrictObjectSchema<{
349
+ readonly value: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
350
+ readonly relation: v.PicklistSchema<["eq", "gte"], undefined>;
351
+ }, undefined>, undefined>;
348
352
  }, undefined>, v.StrictObjectSchema<{
349
353
  readonly mode: v.LiteralSchema<"cursor", undefined>;
350
354
  readonly limit: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 1, undefined>]>;
351
355
  readonly has_more: v.BooleanSchema<undefined>;
352
356
  readonly next_cursor: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "must not be empty">]>, undefined>;
357
+ readonly total: v.OptionalSchema<v.StrictObjectSchema<{
358
+ readonly value: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
359
+ readonly relation: v.PicklistSchema<["eq", "gte"], undefined>;
360
+ }, undefined>, undefined>;
353
361
  }, undefined>], undefined>;
354
362
  readonly coverage: v.StrictObjectSchema<{
355
363
  readonly archive_completeness: v.LiteralSchema<"unknown", undefined>;
@@ -19,11 +19,19 @@ export declare const meetingSeriesInventory: v.StrictObjectSchema<{
19
19
  readonly limit: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 1, undefined>]>;
20
20
  readonly has_more: v.BooleanSchema<undefined>;
21
21
  readonly next_page: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 2, undefined>]>, undefined>;
22
+ readonly total: v.OptionalSchema<v.StrictObjectSchema<{
23
+ readonly value: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
24
+ readonly relation: v.PicklistSchema<["eq", "gte"], undefined>;
25
+ }, undefined>, undefined>;
22
26
  }, undefined>, v.StrictObjectSchema<{
23
27
  readonly mode: v.LiteralSchema<"cursor", undefined>;
24
28
  readonly limit: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 1, undefined>]>;
25
29
  readonly has_more: v.BooleanSchema<undefined>;
26
30
  readonly next_cursor: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "must not be empty">]>, undefined>;
31
+ readonly total: v.OptionalSchema<v.StrictObjectSchema<{
32
+ readonly value: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
33
+ readonly relation: v.PicklistSchema<["eq", "gte"], undefined>;
34
+ }, undefined>, undefined>;
27
35
  }, undefined>], undefined>;
28
36
  readonly fingerprint: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>, v.MaxLengthAction<string, 200, undefined>]>;
29
37
  readonly limits: v.StrictObjectSchema<{
@@ -412,11 +412,19 @@ export declare const callMeetingPreparationTool: (read: MeetingPreparationReader
412
412
  limit: number;
413
413
  has_more: boolean;
414
414
  next_page?: number | undefined;
415
+ total?: {
416
+ value: number;
417
+ relation: "eq" | "gte";
418
+ } | undefined;
415
419
  } | {
416
420
  mode: "cursor";
417
421
  limit: number;
418
422
  has_more: boolean;
419
423
  next_cursor?: string | undefined;
424
+ total?: {
425
+ value: number;
426
+ relation: "eq" | "gte";
427
+ } | undefined;
420
428
  };
421
429
  coverage: {
422
430
  archive_completeness: "unknown";