@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 +22 -0
- package/dist/background/after-upgrade.js +6 -4
- package/dist/calendar/mcp/index.js +10 -4
- package/dist/calendar/reads.d.ts +23 -4
- package/dist/calendar/reads.js +23 -3
- package/dist/calendar/testing/memory.js +8 -2
- package/dist/cli/help/command-help.js +3 -0
- package/dist/meetings/event-store.d.ts +4 -0
- package/dist/meetings/reads.d.ts +10 -7
- package/dist/meetings/reads.js +5 -1
- package/dist/store/sqlite/calendar.js +13 -8
- package/package.json +1 -1
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 — \`${
|
|
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)} — \`${
|
|
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}` : ""} — \`${
|
|
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 — \`${
|
|
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
|
|
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
|
|
218
|
-
const items = lists
|
|
219
|
-
|
|
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":
|
package/dist/calendar/reads.d.ts
CHANGED
|
@@ -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
|
}>;
|
package/dist/calendar/reads.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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) =>
|
|
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 ||
|
|
413
|
-
|
|
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. */
|
package/dist/meetings/reads.d.ts
CHANGED
|
@@ -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
|
-
|
|
15
|
-
|
|
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
|
}>;
|
package/dist/meetings/reads.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
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.
|
|
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",
|