@wirecat/cli-core 0.473.0 → 0.475.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
@@ -7,6 +7,28 @@ shape of this file.
7
7
 
8
8
  ## Unreleased
9
9
 
10
+ ## 0.475.0 — 29.10.2026
11
+
12
+ ### Fixed
13
+
14
+ - **`events list` matches all-day events by their dates** (WC-TIME-5). An all-day event is stored as midnight in its
15
+ 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`.
16
+ `eventsFilter` now also sets `fromDate` / `toDate` (the window's local days, `toDate` exclusive), and both event
17
+ stores compare an all-day event's `start_date` / `end_date` with them. Timed events, and a filter without the dates,
18
+ match as before.
19
+ - **`events list` reports `hasMore`.** It was always false; it is now true when another event follows the page (also
20
+ across accounts in the MCP `events_list`). `listed()` takes an optional `{ limit, hasMore }`.
21
+ - **`people list` says when it counted only its first 1000 events**, with `meta: { complete: true, truncated: true }`.
22
+
23
+ ## 0.474.0 — 29.10.2026
24
+
25
+ ### Fixed
26
+
27
+ - **Help no longer puts a profile into an example that has `--profiles`**: `<cli> mcp serve --profiles
28
+ work,personal` was shown as `<cli> personal mcp serve --profiles work,personal`, which `mcp serve` refuses ("name
29
+ one profile or --profiles, not both"). After an upgrade, the restart of an `mcp server start` server of several
30
+ profiles suggests `mcp server status|start` with the `--profiles` it was started with, not without.
31
+
10
32
  ## 0.473.0 — 29.10.2026
11
33
 
12
34
  ### Added
@@ -146,6 +146,8 @@ const errorText = (error) => error.code === "EPERM"
146
146
  const restartOne = async (envPrefix, lockFile, lock, env, system) => {
147
147
  const launch = readHttpLaunch(httpLaunchFile(lockFile));
148
148
  const name = launch?.invocation ?? lockFile;
149
+ const set = launch?.argv.indexOf("--profiles") ?? -1;
150
+ const again = (verb) => `${name} mcp server ${verb}${set === -1 ? "" : ` --profiles ${launch?.argv[set + 1]}`}`;
149
151
  const from = lock.version ?? null;
150
152
  const left = (reason, rest = {}) => ({
151
153
  name,
@@ -170,7 +172,7 @@ const restartOne = async (envPrefix, lockFile, lock, env, system) => {
170
172
  for (let look = 0; look < STOP_LOOKS && system.alive(lock.pid); look++)
171
173
  await system.pause();
172
174
  if (system.alive(lock.pid))
173
- return left(`asked it to stop, and it still ran after 15 s — \`${name} mcp server status\``);
175
+ return left(`asked it to stop, and it still ran after 15 s — \`${again("status")}\``);
174
176
  const log = lockFile.replace(/\.lock$/, ".log");
175
177
  const { [`${envPrefix}_TIMEOUT`]: _timeout, ...inherited } = env;
176
178
  let pid;
@@ -178,7 +180,7 @@ const restartOne = async (envPrefix, lockFile, lock, env, system) => {
178
180
  pid = system.spawn(launch.argv, { ...inherited, ...launch.env }, log);
179
181
  }
180
182
  catch (error) {
181
- return left(`stopped it, and could not start it again: ${errorText(error)} — \`${name} mcp server start\``);
183
+ return left(`stopped it, and could not start it again: ${errorText(error)} — \`${again("start")}\``);
182
184
  }
183
185
  for (let look = 0; look < START_LOOKS; look++) {
184
186
  const now = readHttpLock(lockFile);
@@ -186,11 +188,11 @@ const restartOne = async (envPrefix, lockFile, lock, env, system) => {
186
188
  return { name, pid: lock.pid, restarted: true, from, to: now.version ?? null, newPid: pid };
187
189
  if (!system.alive(pid)) {
188
190
  const last = readableLog(tail(log, 5)).trim();
189
- return left(`stopped it, and the new one exited before it listened${last ? `: ${last}` : ""} — \`${name} mcp server start\``, { newPid: pid });
191
+ return left(`stopped it, and the new one exited before it listened${last ? `: ${last}` : ""} — \`${again("start")}\``, { newPid: pid });
190
192
  }
191
193
  await system.pause();
192
194
  }
193
- return left(`started it again (PID ${pid}), and it was not listening after 30 s — \`${name} mcp server status\``, {
195
+ return left(`started it again (PID ${pid}), and it was not listening after 30 s — \`${again("status")}\``, {
194
196
  newPid: pid,
195
197
  });
196
198
  };
@@ -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)) &&
@@ -179,6 +179,9 @@ const withProfile = (command, example) => {
179
179
  const top = commandPathOf(command)[0];
180
180
  if (!profiled.has(root) || top === undefined || !takesProfile(command))
181
181
  return example;
182
+ // `--profiles` names the profiles itself; a profile word beside it is refused.
183
+ if (example.split(" ").includes("--profiles"))
184
+ return example;
182
185
  const prefix = `${root.name()} ${top}`;
183
186
  return example === prefix || example.startsWith(`${prefix} `)
184
187
  ? `${root.name()} ${EXAMPLE_PROFILE} ${example.slice(root.name().length + 1)}`
@@ -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);
@@ -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.473.0",
3
+ "version": "0.475.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",