@wirecat/cli-core 0.507.0 → 0.508.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 +36 -0
- package/dist/calendar/cli/answers.d.ts +5 -2
- package/dist/calendar/cli/answers.js +5 -2
- package/dist/calendar/cli/index.js +44 -33
- package/dist/calendar/reads.d.ts +2 -1
- package/dist/calendar/reads.js +4 -3
- package/dist/calendar/server-search.d.ts +31 -0
- package/dist/calendar/server-search.js +28 -0
- package/dist/cli/bot/token.js +2 -2
- package/dist/embeddings/keys.js +1 -1
- package/dist/mail/sync.d.ts +2 -0
- package/dist/mail/sync.js +20 -0
- package/dist/meetings/reads.d.ts +1 -0
- package/dist/store/sqlite/transaction.d.ts +1 -0
- package/dist/store/sqlite/transaction.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,42 @@ shape of this file.
|
|
|
7
7
|
|
|
8
8
|
## Unreleased
|
|
9
9
|
|
|
10
|
+
## 0.508.0 — 31.10.2026
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **`events list --page <n>`: the next page of stored events.** `events list` said `hasMore` and had no way to read on;
|
|
15
|
+
it now takes `--page` from 1 with `--limit` events a page (100), and the answer's `page` is the number shown, so a
|
|
16
|
+
host's `listOf` can offer `page.next` (WC-OUT-5). `eventsAnswer` takes the page as a third argument.
|
|
17
|
+
- **`search events --backend both`** asks the store and the provider's own search together, where `--backend` is
|
|
18
|
+
offered (`deps.serverSearch`). The stored hits come first in the stored search's order, `found_by` gaining
|
|
19
|
+
`server` on each the provider also found; then the hits only the provider found, earliest first. One page, with no
|
|
20
|
+
`next_offset`; `has_more` when either side has more. It refuses `--offline`, `--cursor` and `--newest` as
|
|
21
|
+
`--backend server` does, and takes `--exact` for the stored side. `calendar_search_hit` allows `server` in a stored
|
|
22
|
+
hit's `found_by`. An occurrence the store keeps under another id than the provider's search names shows twice.
|
|
23
|
+
|
|
24
|
+
### Changed — may break callers
|
|
25
|
+
|
|
26
|
+
- **`events create --id` and `events update --following --id` are now `--operation-id`** (WC-CMD-10, WC-OUT-10): the
|
|
27
|
+
value is the `operation_id` the answer and an `outcome_unknown` already name. `--id` is refused as an unknown
|
|
28
|
+
option; errors name `--operation-id`. The calendar MCP tools keep their `id` argument.
|
|
29
|
+
- **`search events` no longer refuses `--backend both`** with `search.one-backend`; see Added.
|
|
30
|
+
|
|
31
|
+
### Fixed
|
|
32
|
+
|
|
33
|
+
- **`search events --cursor` help** said "a page's next_offset", a key the answer a host prints does not have; it now
|
|
34
|
+
says to go on from where the last page stopped, as `page.next` gives it.
|
|
35
|
+
|
|
36
|
+
- **A budgeted `syncMail` waits out a locked store.** When another process (memo's scheduled sync) holds the store's
|
|
37
|
+
write lock past its 5 s busy timeout, the sync used to fail with `database is locked`; it now waits 2 s, 4 s, ... (at
|
|
38
|
+
most 5 times, each a request of the budget) and reads the round again. When the budget has no room the run ends as
|
|
39
|
+
`partial`. `options.wait` lets a test skip the pause.
|
|
40
|
+
- **A bot token, a bot's named secret or an AI provider's key the owner sets again goes back to the keyring.** After
|
|
41
|
+
one keyring failure such a secret stayed in the private file for good, as cli-base 0.68.0 keeps a plain write where
|
|
42
|
+
the secret is. `BotTokenStore.write`, `writeSecret` and `embeddingKeys().write` now write with `{ signIn: true }`:
|
|
43
|
+
the keyring is tried and, once it takes the secret, the file copy is removed (WC-SECR-4). `writeKeyring` was
|
|
44
|
+
keyring-only already; the CAS key is only read here.
|
|
45
|
+
|
|
10
46
|
## 0.507.0 — 31.10.2026
|
|
11
47
|
|
|
12
48
|
### Added
|
|
@@ -156,7 +156,10 @@ export declare const calendarEvent: v.StrictObjectSchema<{
|
|
|
156
156
|
}, undefined>, undefined>;
|
|
157
157
|
readonly untrusted: v.OptionalSchema<v.ArraySchema<v.StringSchema<undefined>, undefined>, undefined>;
|
|
158
158
|
}, undefined>;
|
|
159
|
-
/**
|
|
159
|
+
/**
|
|
160
|
+
* A hit of the stored search (with `--backend both`, `server` too when the provider also found it), or one the
|
|
161
|
+
* provider's own search found and nothing stored.
|
|
162
|
+
*/
|
|
160
163
|
export declare const calendarSearchHit: v.UnionSchema<[v.StrictObjectSchema<{
|
|
161
164
|
readonly provider: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
162
165
|
readonly account: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
@@ -172,7 +175,7 @@ export declare const calendarSearchHit: v.UnionSchema<[v.StrictObjectSchema<{
|
|
|
172
175
|
readonly excerpt: v.StringSchema<undefined>;
|
|
173
176
|
readonly occurrences: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
|
|
174
177
|
readonly updated_at: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoTimestampAction<string, "an ISO 8601 time in UTC">, v.EndsWithAction<string, "Z", "an ISO 8601 time in UTC, ending in `Z`">]>;
|
|
175
|
-
readonly found_by: v.ArraySchema<v.PicklistSchema<["words", "meaning"], undefined>, undefined>;
|
|
178
|
+
readonly found_by: v.ArraySchema<v.PicklistSchema<["words", "meaning", "server"], undefined>, undefined>;
|
|
176
179
|
}, undefined>, v.StrictObjectSchema<{
|
|
177
180
|
readonly provider: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
178
181
|
readonly account: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
@@ -138,7 +138,10 @@ const hitFields = {
|
|
|
138
138
|
url: v.optional(text),
|
|
139
139
|
untrusted,
|
|
140
140
|
};
|
|
141
|
-
/**
|
|
141
|
+
/**
|
|
142
|
+
* A hit of the stored search (with `--backend both`, `server` too when the provider also found it), or one the
|
|
143
|
+
* provider's own search found and nothing stored.
|
|
144
|
+
*/
|
|
142
145
|
export const calendarSearchHit = v.union([
|
|
143
146
|
v.strictObject({
|
|
144
147
|
...hitFields,
|
|
@@ -146,7 +149,7 @@ export const calendarSearchHit = v.union([
|
|
|
146
149
|
excerpt: text,
|
|
147
150
|
occurrences: count,
|
|
148
151
|
updated_at: time,
|
|
149
|
-
found_by: v.array(v.picklist(["words", "meaning"])),
|
|
152
|
+
found_by: v.array(v.picklist(["words", "meaning", "server"])),
|
|
150
153
|
}),
|
|
151
154
|
v.strictObject({
|
|
152
155
|
...hitFields,
|
|
@@ -12,7 +12,7 @@ import { linkPeople } from "../../services/people.js";
|
|
|
12
12
|
import { BUSY_QUERY, BUSY_QUERY_NOTE, busyAnswer, busyQuery, workingHoursOf } from "../busy.js";
|
|
13
13
|
import { CALENDAR_UNTRUSTED_FIELDS, calendarIn, calendarItem, EVENTS_QUERY, EVENTS_QUERY_NOTE, eventAnswer, eventIdOf, eventOf, eventsAnswer, eventsFilter, peopleAnswer, resolveAnswer, reviewsAnswer, weekFilter, } from "../reads.js";
|
|
14
14
|
import { REPEAT_HELP } from "../recurrence.js";
|
|
15
|
-
import { serverSearchAnswer } from "../server-search.js";
|
|
15
|
+
import { bothSearchAnswer, serverSearchAnswer } from "../server-search.js";
|
|
16
16
|
import { calendarStatus, syncCalendars } from "../sync.js";
|
|
17
17
|
import { createEvent, deleteEvent, EVENT_RESPONSES, moveEvent, respondToEvent, updateEvent, } from "../writes.js";
|
|
18
18
|
import { CALENDARS_QUERY, declareCalendarAnswers, PEOPLE_QUERY } from "./answers.js";
|
|
@@ -201,8 +201,9 @@ const addCalendarCommandsDefinition = (program, deps) => {
|
|
|
201
201
|
}), command);
|
|
202
202
|
});
|
|
203
203
|
const events = annotate(program.command("events").description("the account's stored events"), { effects: GROUP });
|
|
204
|
-
example(withQuery(annotate(events.command("list"), { effects: LOCAL_READ }).description("list stored events, earliest first"), EVENTS_QUERY).addHelpText("after", EVENTS_QUERY_NOTE), "events list starts_at>=2026-11-02 starts_at<2026-11-07 --json", "events list ends_at>=today person:ana@example.com")
|
|
205
|
-
.option("--limit <n>", "events
|
|
204
|
+
example(withQuery(annotate(events.command("list"), { effects: LOCAL_READ }).description("list stored events, earliest first"), EVENTS_QUERY).addHelpText("after", EVENTS_QUERY_NOTE), "events list starts_at>=2026-11-02 starts_at<2026-11-07 --json", "events list ends_at>=today person:ana@example.com", "events list starts_at>=2026-11-02 --limit 50 --page 2")
|
|
205
|
+
.option("--limit <n>", "events per page, 1–1000", integer, 100)
|
|
206
|
+
.option("--page <n>", "which page, starting at 1", integer, 1)
|
|
206
207
|
.action(async (words, opts, command) => {
|
|
207
208
|
const filter = await eventsFilter(words, {
|
|
208
209
|
store: deps.store,
|
|
@@ -211,7 +212,7 @@ const addCalendarCommandsDefinition = (program, deps) => {
|
|
|
211
212
|
now: now(),
|
|
212
213
|
personOf: personId,
|
|
213
214
|
});
|
|
214
|
-
print(eachMarked(await eventsAnswer(deps.store, filter && { ...filter, limit: opts.limit }), EVENT_ITEM_UNTRUSTED), command);
|
|
215
|
+
print(eachMarked(await eventsAnswer(deps.store, filter && { ...filter, limit: opts.limit }, opts.page), EVENT_ITEM_UNTRUSTED), command);
|
|
215
216
|
});
|
|
216
217
|
example(annotate(events.command("show"), { effects: LOCAL_READ })
|
|
217
218
|
.description("one event: its calendar copies, attendees with their person, Drive files and history")
|
|
@@ -225,49 +226,59 @@ const addCalendarCommandsDefinition = (program, deps) => {
|
|
|
225
226
|
const search = example(annotate(searches.command("events"), { effects: deps.serverSearch ? LOCAL_OR_REMOTE_READ : LOCAL_READ })
|
|
226
227
|
.description(deps.serverSearch
|
|
227
228
|
? "find stored events by words, and by meaning when its model is downloaded; with --backend server, ask " +
|
|
228
|
-
"the provider instead"
|
|
229
|
+
"the provider instead, with --backend both, both"
|
|
229
230
|
: "find stored events by words, and by meaning when its model is downloaded")
|
|
230
|
-
.argument("<query...>", "what to find in titles, descriptions, places and attendees"), "search events roadmap planning --json", ...(deps.serverSearch
|
|
231
|
+
.argument("<query...>", "what to find in titles, descriptions, places and attendees"), "search events roadmap planning --json", ...(deps.serverSearch
|
|
232
|
+
? ["search events --backend server quarterly review", "search events --backend both quarterly review"]
|
|
233
|
+
: []))
|
|
231
234
|
.option("--limit <n>", "events per page", integer, 20)
|
|
232
|
-
.option("--cursor <cursor>", "
|
|
235
|
+
.option("--cursor <cursor>", "go on from where the last page stopped; page.next gives it", (text) => (text === "0" ? 0 : integer(text)), 0)
|
|
233
236
|
.option("--exact", "every word as written, no word stems")
|
|
234
237
|
.option("--newest", "latest start first instead of best first");
|
|
235
238
|
if (deps.serverSearch)
|
|
236
239
|
search.addOption(new Option("--backend <archive|server|both>", "where to search: archive, the local store, or server, the provider's own search over the synced calendars, " +
|
|
237
|
-
"earliest first"
|
|
240
|
+
"earliest first; both gives the store's hits, each the provider also found marked server, then the " +
|
|
241
|
+
"provider's other hits, one page with no cursor")
|
|
238
242
|
.choices(["archive", "server", "both"])
|
|
239
243
|
.default("archive"));
|
|
240
244
|
search.action(async (words, opts, command) => {
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
245
|
+
const archived = () => searchEvents(messages("search"), words.join(" "), {
|
|
246
|
+
accounts: [deps.account],
|
|
247
|
+
limit: opts.limit,
|
|
248
|
+
offset: opts.cursor,
|
|
249
|
+
timezone: zone(),
|
|
250
|
+
command: program.name(),
|
|
251
|
+
...(opts.exact ? { exact: true } : {}),
|
|
252
|
+
...(opts.newest ? { newest: true } : {}),
|
|
253
|
+
});
|
|
254
|
+
if (opts.backend === "server" || opts.backend === "both") {
|
|
255
|
+
const flag = `search events --backend ${opts.backend}`;
|
|
246
256
|
if (command.optsWithGlobals().offline)
|
|
247
|
-
throw new CliError("permission_error",
|
|
257
|
+
throw new CliError("permission_error", `${flag} asks the provider; leave out --offline`, {
|
|
248
258
|
reason: "search.offline",
|
|
249
259
|
});
|
|
250
|
-
if (opts.
|
|
251
|
-
throw new CliError("validation_error",
|
|
252
|
-
|
|
260
|
+
if (opts.backend === "server" && opts.exact)
|
|
261
|
+
throw new CliError("validation_error", `${flag} takes no --exact: the provider matches words its own way`, {
|
|
262
|
+
reason: "search.server-options",
|
|
263
|
+
});
|
|
264
|
+
if (opts.cursor || opts.newest)
|
|
265
|
+
throw new CliError("validation_error", `${flag} takes neither --cursor nor --newest: the provider ranks and pages its own hits`, { reason: "search.server-options" });
|
|
266
|
+
const served = await serverSearchAnswer(source(), deps.store, {
|
|
253
267
|
accountId: deps.accountId,
|
|
254
268
|
account: deps.account,
|
|
255
269
|
query: words.join(" "),
|
|
256
270
|
limit: opts.limit,
|
|
257
271
|
});
|
|
258
|
-
|
|
272
|
+
if (opts.backend === "server")
|
|
273
|
+
print({ ...served, untrusted: SERVER_SEARCH_UNTRUSTED }, command);
|
|
274
|
+
else
|
|
275
|
+
print({
|
|
276
|
+
...(await bothSearchAnswer(deps.store, deps.accountId, await archived(), served, opts.limit)),
|
|
277
|
+
untrusted: SEARCH_UNTRUSTED,
|
|
278
|
+
}, command);
|
|
259
279
|
return;
|
|
260
280
|
}
|
|
261
|
-
|
|
262
|
-
accounts: [deps.account],
|
|
263
|
-
limit: opts.limit,
|
|
264
|
-
offset: opts.cursor,
|
|
265
|
-
timezone: zone(),
|
|
266
|
-
command: program.name(),
|
|
267
|
-
...(opts.exact ? { exact: true } : {}),
|
|
268
|
-
...(opts.newest ? { newest: true } : {}),
|
|
269
|
-
});
|
|
270
|
-
print({ ...found, untrusted: SEARCH_UNTRUSTED }, command);
|
|
281
|
+
print({ ...(await archived()), untrusted: SEARCH_UNTRUSTED }, command);
|
|
271
282
|
});
|
|
272
283
|
if (deps.writes)
|
|
273
284
|
addEventWrites(events, deps.writes, { deps, now, zone, print, example });
|
|
@@ -322,7 +333,7 @@ const addEventWrites = (events, writes, { deps, now, zone, print, example }) =>
|
|
|
322
333
|
zone: zone(),
|
|
323
334
|
names: {
|
|
324
335
|
notify: "--notify or --no-notify",
|
|
325
|
-
id: "--id",
|
|
336
|
+
id: "--operation-id",
|
|
326
337
|
calendar: "--calendar",
|
|
327
338
|
series: "--series",
|
|
328
339
|
following: "--following",
|
|
@@ -362,10 +373,10 @@ const addEventWrites = (events, writes, { deps, now, zone, print, example }) =>
|
|
|
362
373
|
.option("--calendar <id>", "the calendar, when the event is on more than one of the account's");
|
|
363
374
|
example(notify(fields(write("create"), false)
|
|
364
375
|
.description("add an event to a calendar; with --repeat, one that repeats")
|
|
365
|
-
.argument("<calendar>", "the calendar's id, or the provider's id")).option("--id <id>", "the
|
|
376
|
+
.argument("<calendar>", "the calendar's id, or the provider's id")).option("--operation-id <id>", "the operation_id a create that ended in outcome_unknown named: a retry finds that event"), "events create 3 --title 'Example planning' --start-time 2026-11-02T10:00 --end-time 2026-11-02T11:00", "events create 3 --title 'Example review' --start-date 2026-11-03 --end-date 2026-11-03 " +
|
|
366
377
|
"--attendee ana@example.com --no-notify", "events create 3 --title 'Example standup' --start-time 2026-11-02T09:30 --end-time 2026-11-02T09:45 " +
|
|
367
378
|
"--repeat 'FREQ=WEEKLY;BYDAY=MO,WE,FR;COUNT=12'").action(async (calendar, opts, command) => {
|
|
368
|
-
const { notify: choice, id, ...rest } = opts;
|
|
379
|
+
const { notify: choice, operationId: id, ...rest } = opts;
|
|
369
380
|
printWritten(await createEvent(context(command), calendar, fieldsOf(rest), {
|
|
370
381
|
...(choice === undefined ? {} : { notify: choice }),
|
|
371
382
|
...(id === undefined ? {} : { id }),
|
|
@@ -377,8 +388,8 @@ const addEventWrites = (events, writes, { deps, now, zone, print, example }) =>
|
|
|
377
388
|
.option("--series", "change the whole repeating event, not only this occurrence")
|
|
378
389
|
.option("--following", "change this occurrence and every later one: the repeating event ends before it and a new one starts " +
|
|
379
390
|
"with the change")
|
|
380
|
-
.option("--id <id>", "with --following, the
|
|
381
|
-
const { calendar: _calendar, series: _series, following: _following, notify: _notify, id, ...rest } = opts;
|
|
391
|
+
.option("--operation-id <id>", "with --following, the operation_id an outcome_unknown named: a retry finds that new series")), "events update event:42 --start-time 2026-11-02T14:00 --end-time 2026-11-02T15:00 --notify", "events update event:42 --series --repeat 'FREQ=WEEKLY;BYDAY=TU;UNTIL=20261231T230000Z' --no-notify", "events update event:42 --following --start-time 2026-11-09T10:00 --end-time 2026-11-09T10:30 --no-notify").action(async (reference, opts, command) => {
|
|
392
|
+
const { calendar: _calendar, series: _series, following: _following, notify: _notify, operationId: id, ...rest } = opts;
|
|
382
393
|
printWritten(await updateEvent(context(command), reference, fieldsOf(rest), {
|
|
383
394
|
...choicesOf(opts),
|
|
384
395
|
...(id === undefined ? {} : { id }),
|
package/dist/calendar/reads.d.ts
CHANGED
|
@@ -182,7 +182,8 @@ export declare const resolveAnswer: (store: EventStore, accountId: number, revie
|
|
|
182
182
|
status: "apart" | "linked" | "open";
|
|
183
183
|
chosen: string | null;
|
|
184
184
|
}>;
|
|
185
|
-
|
|
185
|
+
/** One page of the events, from 1: `filter.limit` events each. */
|
|
186
|
+
export declare const eventsAnswer: (store: EventStore, filter: EventFilter | null, number?: number) => Promise<{
|
|
186
187
|
limit: number;
|
|
187
188
|
hasMore: boolean;
|
|
188
189
|
items: {
|
package/dist/calendar/reads.js
CHANGED
|
@@ -331,10 +331,11 @@ const overlapsOf = async (store, rows, filter) => {
|
|
|
331
331
|
other.endsAt > event.startsAt),
|
|
332
332
|
]));
|
|
333
333
|
};
|
|
334
|
-
|
|
334
|
+
/** One page of the events, from 1: `filter.limit` events each. */
|
|
335
|
+
export const eventsAnswer = async (store, filter, number = 1) => {
|
|
335
336
|
if (filter === null)
|
|
336
337
|
return listed([]);
|
|
337
|
-
const { rows, limit, hasMore } = await page(store, filter);
|
|
338
|
+
const { rows, limit, hasMore } = await page(store, { ...filter, offset: (number - 1) * (filter.limit ?? 100) });
|
|
338
339
|
const changes = filter.changedSince === undefined ? null : await changesOf(store, rows, filter, filter.changedSince);
|
|
339
340
|
const overlaps = filter.overlapping ? await overlapsOf(store, rows, filter) : null;
|
|
340
341
|
return listed(rows.map((event) => {
|
|
@@ -350,5 +351,5 @@ export const eventsAnswer = async (store, filter) => {
|
|
|
350
351
|
: {}),
|
|
351
352
|
...(overlaps ? { overlaps: (overlaps.get(event.id) ?? []).map(({ id }) => `event:${id}`) } : {}),
|
|
352
353
|
};
|
|
353
|
-
}), { limit, hasMore });
|
|
354
|
+
}), { limit, hasMore, page: number });
|
|
354
355
|
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { EventStore } from "../meetings/index.js";
|
|
2
|
+
import type { EventsFound, FoundEvent } from "../services/events-search.js";
|
|
2
3
|
import type { AccountKey } from "../store/store.js";
|
|
3
4
|
import type { CalendarSource } from "./source.js";
|
|
4
5
|
/**
|
|
@@ -29,3 +30,33 @@ export declare const serverSearchAnswer: (source: CalendarSource, store: EventSt
|
|
|
29
30
|
}[];
|
|
30
31
|
has_more: boolean;
|
|
31
32
|
}>;
|
|
33
|
+
type FoundBy = FoundEvent["found_by"][number] | "server";
|
|
34
|
+
type ServerFound = Awaited<ReturnType<typeof serverSearchAnswer>>;
|
|
35
|
+
/**
|
|
36
|
+
* `--backend both`: the stored search's hits in its own order, each one the provider also found marked `server`, then
|
|
37
|
+
* what only the provider found, earliest first. One page and no cursor: the two sides rank and page differently.
|
|
38
|
+
* A provider hit is matched by its stored copy, so an occurrence the store keeps under another id shows twice.
|
|
39
|
+
*/
|
|
40
|
+
export declare const bothSearchAnswer: (store: EventStore, accountId: number, archived: EventsFound, served: ServerFound, limit: number) => Promise<{
|
|
41
|
+
query: string;
|
|
42
|
+
by: "both";
|
|
43
|
+
meaning_skipped?: string | undefined;
|
|
44
|
+
hits: ({
|
|
45
|
+
provider: string;
|
|
46
|
+
account: string;
|
|
47
|
+
calendar: string;
|
|
48
|
+
external_id: string;
|
|
49
|
+
title: string | null;
|
|
50
|
+
starts_at: string | null;
|
|
51
|
+
ends_at: string | null;
|
|
52
|
+
all_day: boolean;
|
|
53
|
+
status: "confirmed" | "tentative";
|
|
54
|
+
location: string | null;
|
|
55
|
+
url?: string | undefined;
|
|
56
|
+
found_by: "server"[];
|
|
57
|
+
} | (Omit<FoundEvent, "found_by"> & {
|
|
58
|
+
found_by: FoundBy[];
|
|
59
|
+
}))[];
|
|
60
|
+
has_more: boolean;
|
|
61
|
+
}>;
|
|
62
|
+
export {};
|
|
@@ -39,3 +39,31 @@ export const serverSearchAnswer = async (source, store, { accountId, account, qu
|
|
|
39
39
|
hits.sort((a, b) => (a.starts_at ?? "").localeCompare(b.starts_at ?? ""));
|
|
40
40
|
return { query, by: "server", hits: hits.slice(0, limit), has_more: more || hits.length > limit };
|
|
41
41
|
};
|
|
42
|
+
/**
|
|
43
|
+
* `--backend both`: the stored search's hits in its own order, each one the provider also found marked `server`, then
|
|
44
|
+
* what only the provider found, earliest first. One page and no cursor: the two sides rank and page differently.
|
|
45
|
+
* A provider hit is matched by its stored copy, so an occurrence the store keeps under another id shows twice.
|
|
46
|
+
*/
|
|
47
|
+
export const bothSearchAnswer = async (store, accountId, archived, served, limit) => {
|
|
48
|
+
const calendars = new Map((await store.calendars(accountId)).map((calendar) => [calendar.externalId, calendar]));
|
|
49
|
+
const hits = archived.hits.map((hit) => ({ ...hit }));
|
|
50
|
+
const byRef = new Map(hits.map((hit) => [hit.ref, hit]));
|
|
51
|
+
const onlyServer = [];
|
|
52
|
+
for (const hit of served.hits) {
|
|
53
|
+
const calendar = calendars.get(hit.calendar);
|
|
54
|
+
const entry = calendar ? await store.entry(calendar.id, hit.external_id) : null;
|
|
55
|
+
const held = entry?.eventId == null ? undefined : byRef.get(`event:${entry.eventId}`);
|
|
56
|
+
if (held)
|
|
57
|
+
held.found_by = [...held.found_by, "server"];
|
|
58
|
+
else
|
|
59
|
+
onlyServer.push(hit);
|
|
60
|
+
}
|
|
61
|
+
const merged = [...hits, ...onlyServer];
|
|
62
|
+
return {
|
|
63
|
+
query: archived.query,
|
|
64
|
+
by: "both",
|
|
65
|
+
...(archived.meaning_skipped === undefined ? {} : { meaning_skipped: archived.meaning_skipped }),
|
|
66
|
+
hits: merged.slice(0, limit),
|
|
67
|
+
has_more: archived.has_more || served.has_more || merged.length > limit,
|
|
68
|
+
};
|
|
69
|
+
};
|
package/dist/cli/bot/token.js
CHANGED
|
@@ -40,7 +40,7 @@ export class BotTokenStore {
|
|
|
40
40
|
return stored ? { token: stored.secret, source: stored.source } : undefined;
|
|
41
41
|
}
|
|
42
42
|
write(token) {
|
|
43
|
-
return this.#credentials.write(this.#account, token);
|
|
43
|
+
return this.#credentials.write(this.#account, token, { signIn: true });
|
|
44
44
|
}
|
|
45
45
|
readKeyring() {
|
|
46
46
|
const stored = this.readStored();
|
|
@@ -69,7 +69,7 @@ export class BotTokenStore {
|
|
|
69
69
|
return this.#secretOf(name).read(`${this.#account}:${name}`);
|
|
70
70
|
}
|
|
71
71
|
writeSecret(name, secret) {
|
|
72
|
-
return this.#secretOf(name).write(`${this.#account}:${name}`, secret);
|
|
72
|
+
return this.#secretOf(name).write(`${this.#account}:${name}`, secret, { signIn: true });
|
|
73
73
|
}
|
|
74
74
|
removeSecret(name) {
|
|
75
75
|
return this.#secretOf(name).remove(`${this.#account}:${name}`);
|
package/dist/embeddings/keys.js
CHANGED
|
@@ -33,7 +33,7 @@ export const embeddingKeys = (app, env = process.env, keyring) => {
|
|
|
33
33
|
: undefined;
|
|
34
34
|
return shared ? { key: shared, source: "env" } : undefined;
|
|
35
35
|
},
|
|
36
|
-
write: (provider, key) => credentials(provider).write(account(provider), key),
|
|
36
|
+
write: (provider, key) => credentials(provider).write(account(provider), key, { signIn: true }),
|
|
37
37
|
remove: (provider) => credentials(provider).remove(account(provider)),
|
|
38
38
|
};
|
|
39
39
|
};
|
package/dist/mail/sync.d.ts
CHANGED
|
@@ -101,5 +101,7 @@ export interface MailSyncOptions {
|
|
|
101
101
|
* is tried once more at the end of the run and, if it still fails, left for the next one. `max` defaults to no limit.
|
|
102
102
|
*/
|
|
103
103
|
budget?: RunBudget;
|
|
104
|
+
/** Pauses before a round is read again after the store was locked; tests pass one that does not wait. */
|
|
105
|
+
wait?: (ms: number) => Promise<void>;
|
|
104
106
|
}
|
|
105
107
|
export declare const syncMail: (source: MailSource, store: MessageStore, options: MailSyncOptions) => Promise<MailSyncResult>;
|
package/dist/mail/sync.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { setTimeout } from "node:timers/promises";
|
|
2
2
|
import { CliError } from "@wirecat/cli-base";
|
|
3
3
|
import { importEngine } from "../attachments/extract.js";
|
|
4
|
+
import { isBusy } from "../store/sqlite/transaction.js";
|
|
4
5
|
import { addressOf, FETCH_CHUNK, MAX_ATTACHMENT_CHARS, readAttachments } from "./attachments.js";
|
|
5
6
|
import { BULK, bulkBox } from "./bulk.js";
|
|
6
7
|
import { factsOf } from "./facts.js";
|
|
@@ -26,6 +27,8 @@ const RESUME_FROM = 5_000;
|
|
|
26
27
|
export { MAX_ATTACHMENT_CHARS } from "./attachments.js";
|
|
27
28
|
export { RETRYABLE_ATTACHMENTS } from "./retry.js";
|
|
28
29
|
const LIST_SLICE = 200;
|
|
30
|
+
const STORE_BUSY_TRIES = 5;
|
|
31
|
+
const STORE_BUSY_WAIT_MS = 2_000;
|
|
29
32
|
export const syncMail = async (source, store, options) => {
|
|
30
33
|
const { budget, index } = options;
|
|
31
34
|
if (budget === undefined || options.retryAttachments || options.account.mode === "mbox")
|
|
@@ -45,14 +48,31 @@ export const syncMail = async (source, store, options) => {
|
|
|
45
48
|
asked: 0,
|
|
46
49
|
};
|
|
47
50
|
const rounds = [];
|
|
51
|
+
let busyWaits = 0;
|
|
48
52
|
const goOn = async () => {
|
|
49
53
|
for (;;) {
|
|
50
54
|
plan.asked = 0;
|
|
51
55
|
plan.remaining = 0;
|
|
56
|
+
const triedBefore = plan.tried.size;
|
|
52
57
|
try {
|
|
53
58
|
rounds.push(await syncRound(source, store, { ...options, index: undefined, max: options.max ?? Number.POSITIVE_INFINITY }, plan));
|
|
54
59
|
}
|
|
55
60
|
catch (error) {
|
|
61
|
+
// Another process (memo's scheduled sync) held the store's write lock past its busy timeout. The slices this
|
|
62
|
+
// run committed stay; the round's messages are asked for again after a pause, while the budget has room.
|
|
63
|
+
if (isBusy(error) && busyWaits < STORE_BUSY_TRIES && !options.signal?.aborted) {
|
|
64
|
+
busyWaits++;
|
|
65
|
+
for (const key of [...plan.tried].slice(triedBefore))
|
|
66
|
+
plan.tried.delete(key);
|
|
67
|
+
const stop = budget.take();
|
|
68
|
+
if (stop === null) {
|
|
69
|
+
await (options.wait ?? setTimeout)(STORE_BUSY_WAIT_MS * busyWaits);
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
plan.stopped = stop;
|
|
73
|
+
plan.remaining = Math.max(plan.remaining, 1);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
56
76
|
// A request cut at the deadline, or failed after the budget was spent, is the run's end, not its failure: what
|
|
57
77
|
// earlier rounds saved is kept and the next run goes on. While the budget has room the error is real.
|
|
58
78
|
const transient = error instanceof CliError && (error.code === "timeout" || error.code === "network_error");
|
package/dist/meetings/reads.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { CacheDatabase } from "../driver.js";
|
|
2
|
+
export declare const isBusy: (error: unknown) => boolean;
|
|
2
3
|
/**
|
|
3
4
|
* One write transaction. The outermost takes the write lock at once (`BEGIN IMMEDIATE`): a deferred one that
|
|
4
5
|
* reads first fails with SQLITE_BUSY when another process writes in between, and tg, max, memo and zm share
|
|
@@ -3,7 +3,7 @@ import { drainMentions } from "./mention-drain.js";
|
|
|
3
3
|
import { drainStems, stemmerCache } from "./stems.js";
|
|
4
4
|
const SQLITE_BUSY = 5;
|
|
5
5
|
const pause = new Int32Array(new SharedArrayBuffer(4));
|
|
6
|
-
const isBusy = (error) => {
|
|
6
|
+
export const isBusy = (error) => {
|
|
7
7
|
const { errcode, errno } = (error ?? {});
|
|
8
8
|
const code = errcode ?? errno;
|
|
9
9
|
return typeof code === "number" && (code & 0xff) === SQLITE_BUSY;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wirecat/cli-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.508.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",
|