@wirecat/cli-core 0.474.0 → 0.476.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 CHANGED
@@ -5,6 +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
+ ## Unreleased
9
+
10
+ ## 0.476.0 — 29.10.2026
11
+
12
+ ### Fixed
13
+
14
+ - **A sign-in that cannot be read says why under `--debug` and in the run record.** `credentialStorage` threw
15
+ `configuration_error` (`oauth.not-saved`, "could not reach the saved sign-in") and dropped the keyring's own error,
16
+ so a locked keyring, a missing Secret Service and an unreachable D-Bus looked the same. The keyring's message is now
17
+ the error's `cause`: `--debug` and `runs show` print it, the JSON answer still does not. A secret the keyring echoes
18
+ back on a failed write is masked first.
19
+
20
+ ## 0.475.0 — 29.10.2026
21
+
22
+ ### Fixed
23
+
24
+ - **`events list` matches all-day events by their dates** (WC-TIME-5). An all-day event is stored as midnight in its
25
+ calendar's zone, so an owner in another zone got the next day's all-day event in `from:2026-11-02 to:2026-11-02`.
26
+ `eventsFilter` now also sets `fromDate` / `toDate` (the window's local days, `toDate` exclusive), and both event
27
+ stores compare an all-day event's `start_date` / `end_date` with them. Timed events, and a filter without the dates,
28
+ match as before.
29
+ - **`events list` reports `hasMore`.** It was always false; it is now true when another event follows the page (also
30
+ across accounts in the MCP `events_list`). `listed()` takes an optional `{ limit, hasMore }`.
31
+ - **`people list` says when it counted only its first 1000 events**, with `meta: { complete: true, truncated: true }`.
32
+
8
33
  ## 0.474.0 — 29.10.2026
9
34
 
10
35
  ### Fixed
@@ -209,14 +209,20 @@ export const callCalendarTool = async (store, name, input, { accountIds, failure
209
209
  break;
210
210
  case "events_list": {
211
211
  const words = typeof args.words === "string" ? args.words.trim().split(/\s+/u) : [];
212
- const lists = await Promise.all(accounts.map(async (accountId) => (await eventsAnswer(store, await eventsFilter(words, {
212
+ const limit = args.limit ?? 100;
213
+ const lists = await Promise.all(accounts.map(async (accountId) => eventsAnswer(store, await eventsFilter(words, {
213
214
  store,
214
215
  accountId,
215
216
  zone: args.timezone ?? "UTC",
216
217
  now,
217
- }).then((filter) => filter && { ...filter, limit: args.limit ?? 100 }))).items));
218
- const items = lists.flat().sort((a, b) => (a.starts_at ?? "").localeCompare(b.starts_at ?? ""));
219
- answer = listed(items.slice(0, args.limit ?? 100));
218
+ }).then((filter) => filter && { ...filter, limit }))));
219
+ const items = lists
220
+ .flatMap((one) => one.items)
221
+ .sort((a, b) => (a.starts_at ?? "").localeCompare(b.starts_at ?? ""));
222
+ answer = listed(items.slice(0, limit), {
223
+ limit,
224
+ hasMore: items.length > limit || lists.some((one) => one.hasMore),
225
+ });
220
226
  break;
221
227
  }
222
228
  case "event_show":
@@ -109,6 +109,8 @@ export declare const peopleAnswer: (store: EventStore, accountId: number, option
109
109
  filter?: EventFilter;
110
110
  personOf?: PersonOfEmail;
111
111
  }) => Promise<{
112
+ limit: number;
113
+ hasMore: boolean;
112
114
  items: {
113
115
  email: string;
114
116
  name: string | null;
@@ -120,11 +122,30 @@ export declare const peopleAnswer: (store: EventStore, accountId: number, option
120
122
  person: string | null;
121
123
  }[];
122
124
  page: number;
125
+ } | {
123
126
  limit: number;
124
127
  hasMore: boolean;
128
+ items: {
129
+ email: string;
130
+ name: string | null;
131
+ response: import("../meetings/event-store.js").ParticipantResponse | null;
132
+ organizer: boolean;
133
+ optional: boolean;
134
+ resource: boolean;
135
+ self: boolean;
136
+ person: string | null;
137
+ events: number;
138
+ }[];
139
+ page: number;
140
+ meta: {
141
+ complete: boolean;
142
+ truncated: boolean;
143
+ };
125
144
  }>;
126
145
  /** Open link choices, with each candidate event's title and time so the owner can pick. */
127
146
  export declare const reviewsAnswer: (store: EventStore) => Promise<{
147
+ limit: number;
148
+ hasMore: boolean;
128
149
  items: {
129
150
  id: string;
130
151
  entry_id: string;
@@ -136,8 +157,6 @@ export declare const reviewsAnswer: (store: EventStore) => Promise<{
136
157
  }[];
137
158
  }[];
138
159
  page: number;
139
- limit: number;
140
- hasMore: boolean;
141
160
  accounts: string;
142
161
  }>;
143
162
  export declare const resolveAnswer: (store: EventStore, review: string, choice: string, now: number) => Promise<{
@@ -146,6 +165,8 @@ export declare const resolveAnswer: (store: EventStore, review: string, choice:
146
165
  chosen: string | null;
147
166
  }>;
148
167
  export declare const eventsAnswer: (store: EventStore, filter: EventFilter | null) => Promise<{
168
+ limit: number;
169
+ hasMore: boolean;
149
170
  items: {
150
171
  id: string;
151
172
  event_series_id: string | null;
@@ -165,6 +186,4 @@ export declare const eventsAnswer: (store: EventStore, filter: EventFilter | nul
165
186
  untrusted?: string[] | undefined;
166
187
  }[];
167
188
  page: number;
168
- limit: number;
169
- hasMore: boolean;
170
189
  }>;
@@ -93,6 +93,10 @@ export const eventsFilter = async (words, context) => {
93
93
  }
94
94
  if (!timed)
95
95
  filter.from = day(today);
96
+ if (filter.from !== undefined)
97
+ filter.fromDate = localDate(filter.from, context.zone);
98
+ if (filter.to !== undefined)
99
+ filter.toDate = plusDays(localDate(filter.to - 1, context.zone), 1);
96
100
  return filter;
97
101
  };
98
102
  /**
@@ -169,13 +173,23 @@ export const eventAnswer = async (details, personOf) => ({
169
173
  changed_at: new Date(one.createdAt).toISOString(),
170
174
  })),
171
175
  });
176
+ const EVENTS_COUNTED = 1000;
177
+ /** One page of events, and whether another follows; probed with one row, since a store reads at most 1000. */
178
+ const page = async (store, filter) => {
179
+ const rows = await store.events(filter);
180
+ const limit = filter.limit ?? 100;
181
+ const hasMore = rows.length === limit &&
182
+ (await store.events({ ...filter, offset: (filter.offset ?? 0) + limit, limit: 1 })).length > 0;
183
+ return { rows, limit, hasMore };
184
+ };
172
185
  /** One event's attendees, or everyone attending the events of the filter, with how many of them each attends. */
173
186
  export const peopleAnswer = async (store, accountId, options) => {
174
187
  if (options.event !== undefined)
175
188
  return listed(await attendees((await eventOf(store, accountId, options.event)).participants, options.personOf));
176
189
  const counted = new Map();
177
190
  const mine = await calendarIds(store, accountId);
178
- for (const event of await store.events({ ...options.filter, accountId, limit: 1000 })) {
191
+ const { rows, hasMore } = await page(store, { ...options.filter, accountId, limit: EVENTS_COUNTED });
192
+ for (const event of rows) {
179
193
  const details = await store.event(event.id);
180
194
  const seen = new Set();
181
195
  for (const one of details ? ownCopies(details, mine).participants : []) {
@@ -190,9 +204,10 @@ export const peopleAnswer = async (store, accountId, options) => {
190
204
  }
191
205
  }
192
206
  const people = await attendees([...counted.values()].map(({ participant }) => participant), options.personOf);
193
- return listed(people
207
+ const answer = listed(people
194
208
  .map((one) => ({ ...one, events: counted.get(one.email)?.events ?? 0 }))
195
209
  .sort((a, b) => b.events - a.events || a.email.localeCompare(b.email)));
210
+ return hasMore ? { ...answer, meta: { complete: true, truncated: true } } : answer;
196
211
  };
197
212
  /** Open link choices, with each candidate event's title and time so the owner can pick. */
198
213
  export const reviewsAnswer = async (store) => {
@@ -226,4 +241,9 @@ export const resolveAnswer = async (store, review, choice, now) => {
226
241
  chosen: resolved.chosenId === null ? null : `event:${resolved.chosenId}`,
227
242
  };
228
243
  };
229
- export const eventsAnswer = async (store, filter) => listed(filter === null ? [] : (await store.events(filter)).map(eventItem));
244
+ export const eventsAnswer = async (store, filter) => {
245
+ if (filter === null)
246
+ return listed([]);
247
+ const { rows, limit, hasMore } = await page(store, filter);
248
+ return listed(rows.map(eventItem), { limit, hasMore });
249
+ };
@@ -409,8 +409,14 @@ export const memoryEventStore = () => {
409
409
  const has = (event, check) => entries.some((one) => one.eventId === event.id && check(one));
410
410
  return [...events.values()]
411
411
  .filter((event) => event.deletedAt === null &&
412
- (filter.from === undefined || (event.endsAt ?? (event.startsAt ?? 0) + 1) > filter.from) &&
413
- (filter.to === undefined || (event.startsAt !== null && event.startsAt < filter.to)) &&
412
+ (filter.from === undefined ||
413
+ (event.allDay && filter.fromDate !== undefined
414
+ ? has(event, (one) => (one.endDate ?? "") > (filter.fromDate ?? ""))
415
+ : (event.endsAt ?? (event.startsAt ?? 0) + 1) > filter.from)) &&
416
+ (filter.to === undefined ||
417
+ (event.allDay && filter.toDate !== undefined
418
+ ? has(event, (one) => one.startDate !== null && one.startDate < (filter.toDate ?? ""))
419
+ : event.startsAt !== null && event.startsAt < filter.to)) &&
414
420
  (filter.calendarId === undefined || has(event, (one) => one.calendarId === filter.calendarId)) &&
415
421
  (filter.accountId === undefined ||
416
422
  has(event, (one) => calendarOf(one.calendarId).accountId === filter.accountId)) &&
@@ -181,6 +181,10 @@ export interface EventFilter {
181
181
  from?: number;
182
182
  /** Events that start before this instant. */
183
183
  to?: number;
184
+ /** All-day events, which have dates and no instant, ending after this local day (`YYYY-MM-DD`); `from` without it. */
185
+ fromDate?: string;
186
+ /** All-day events starting before this local day (exclusive); `to` without it. */
187
+ toDate?: string;
184
188
  accountId?: number;
185
189
  calendarId?: number;
186
190
  /** Events whose entries list one of this person's contacts. */
@@ -10,17 +10,20 @@ export declare class MeetingError extends CliError {
10
10
  }
11
11
  export declare const positiveInteger: (value: unknown) => number;
12
12
  export declare const meetingShow: (store: MeetingStore, id: number, accountId?: number) => Promise<MeetingDetails>;
13
- export declare const listed: <T>(items: T[]) => {
14
- items: T[];
15
- page: number;
13
+ export declare const listed: <T>(items: T[], page?: {
14
+ limit: number;
15
+ hasMore: boolean;
16
+ }) => {
16
17
  limit: number;
17
18
  hasMore: boolean;
19
+ items: T[];
20
+ page: number;
18
21
  };
19
22
  export declare const meetingPeople: (store: MeetingStore & Partial<MeetingParticipantSearchStore>, query: string, accountId?: number, options?: Omit<MeetingParticipantSearchOptions, "accountId">) => Promise<{
20
- items: import("./model.js").Participant[];
21
- page: number;
22
23
  limit: number;
23
24
  hasMore: boolean;
25
+ items: import("./model.js").Participant[];
26
+ page: number;
24
27
  } | {
25
28
  schema_version: 1;
26
29
  kind: "meeting_participants";
@@ -76,8 +79,8 @@ export declare const meetingDetailsAnswer: (store: MeetingStore, id: number, acc
76
79
  export declare const meetingTranscriptAnswer: (store: MeetingStore, id: number, accountId?: number, options?: TranscriptReadOptions) => Promise<MeetingListBody<MeetingTranscriptItem>>;
77
80
  export declare const meetingSummaryAnswer: (store: MeetingStore, id: number, accountId?: number) => Promise<MeetingListBody<MeetingSummaryItem>>;
78
81
  export declare const meetingsSearch: (store: MeetingStore, query: string, filter?: MeetingFilter) => Promise<{
79
- items: import("./store.js").SearchHit[];
80
- page: number;
81
82
  limit: number;
82
83
  hasMore: boolean;
84
+ items: import("./store.js").SearchHit[];
85
+ page: number;
83
86
  }>;
@@ -33,7 +33,11 @@ export const meetingShow = async (store, id, accountId) => {
33
33
  throw new MeetingError("not_found", "meeting not found");
34
34
  return details;
35
35
  };
36
- export const listed = (items) => ({ items, page: 1, limit: items.length, hasMore: false });
36
+ export const listed = (items, page = { limit: items.length, hasMore: false }) => ({
37
+ items,
38
+ page: 1,
39
+ ...page,
40
+ });
37
41
  export const meetingPeople = async (store, query, accountId, options = {}) => {
38
42
  if (accountId === undefined)
39
43
  validateMeetingParticipantQuery(query);
@@ -136,9 +136,14 @@ export const zoomTokenCodec = {
136
136
  });
137
137
  },
138
138
  };
139
- const keyringFailure = () => new CliError("configuration_error", "could not reach the saved sign-in; use an available keyring or file storage", {
140
- reason: "oauth.not-saved",
141
- });
139
+ // The keyring's own words go to `--debug` and the run record as the cause, never into the answer; a store that
140
+ // echoes what it was handed loses the secret from them first.
141
+ const keyringFailure = (thrown, secret) => {
142
+ const failure = new CliError("configuration_error", "could not reach the saved sign-in; use an available keyring or file storage", { reason: "oauth.not-saved" });
143
+ const words = thrown instanceof Error ? thrown.message : String(thrown);
144
+ failure.cause = new Error(secret ? words.replaceAll(secret, "[secret]") : words);
145
+ return failure;
146
+ };
142
147
  export const credentialStorage = (options) => {
143
148
  if (!options.credentials.keyring)
144
149
  throw new CliError("configuration_error", "a sign-in store needs its keyring passed in, never a default");
@@ -149,16 +154,16 @@ export const credentialStorage = (options) => {
149
154
  try {
150
155
  return credentials.read(name)?.secret;
151
156
  }
152
- catch {
153
- throw keyringFailure();
157
+ catch (error) {
158
+ throw keyringFailure(error);
154
159
  }
155
160
  };
156
161
  const save = (value) => {
157
162
  try {
158
163
  credentials.write(account, value);
159
164
  }
160
- catch {
161
- throw keyringFailure();
165
+ catch (error) {
166
+ throw keyringFailure(error, value);
162
167
  }
163
168
  };
164
169
  const current = () => {
@@ -189,8 +194,8 @@ export const credentialStorage = (options) => {
189
194
  try {
190
195
  return credentials.remove(account);
191
196
  }
192
- catch {
193
- throw keyringFailure();
197
+ catch (error) {
198
+ throw keyringFailure(error);
194
199
  }
195
200
  },
196
201
  };
@@ -542,14 +542,19 @@ export const eventStoreOver = (context) => {
542
542
  const clauses = ["e.deleted_at IS NULL"];
543
543
  const params = [];
544
544
  const entries = (extra) => `EXISTS (SELECT 1 FROM calendar_entries ce JOIN calendars c ON c.id = ce.calendar_id WHERE ce.event_id = e.id AND ${extra})`;
545
- if (filter.from !== undefined) {
546
- clauses.push("coalesce(e.ends_at, e.starts_at + 1) > ?");
547
- params.push(filter.from);
548
- }
549
- if (filter.to !== undefined) {
550
- clauses.push("e.starts_at < ?");
551
- params.push(filter.to);
552
- }
545
+ const dated = (column, date, instant, at) => {
546
+ if (at === undefined)
547
+ return;
548
+ if (date === undefined) {
549
+ clauses.push(instant);
550
+ params.push(at);
551
+ return;
552
+ }
553
+ clauses.push(`(CASE WHEN e.all_day = 1 THEN EXISTS (SELECT 1 FROM calendar_entries ce WHERE ce.event_id = e.id AND ce.${column}) ELSE ${instant} END)`);
554
+ params.push(date, at);
555
+ };
556
+ dated("end_date > ?", filter.fromDate, "coalesce(e.ends_at, e.starts_at + 1) > ?", filter.from);
557
+ dated("start_date < ?", filter.toDate, "e.starts_at < ?", filter.to);
553
558
  if (filter.calendarId !== undefined) {
554
559
  clauses.push(entries("ce.calendar_id = ?"));
555
560
  params.push(filter.calendarId);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wirecat/cli-core",
3
- "version": "0.474.0",
3
+ "version": "0.476.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",