@wirecat/cli-core 0.541.0 → 0.543.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 +39 -1
- package/dist/calendar/cli/answers.d.ts +19 -0
- package/dist/calendar/cli/answers.js +4 -0
- package/dist/calendar/cli/index.js +3 -0
- package/dist/calendar/cli/remind.d.ts +12 -0
- package/dist/calendar/cli/remind.js +115 -0
- package/dist/calendar/index.d.ts +1 -1
- package/dist/calendar/index.js +1 -1
- package/dist/calendar/prepare.d.ts +5 -0
- package/dist/calendar/prepare.js +6 -6
- package/dist/calendar/remind.d.ts +51 -0
- package/dist/calendar/remind.js +79 -0
- package/dist/calendar/writer.d.ts +2 -0
- package/dist/calendar/writer.js +9 -1
- package/dist/cli/index.d.ts +1 -1
- package/dist/cli/messenger/observed.js +1 -1
- package/dist/cli/messenger/store-maintenance-command.d.ts +1 -1
- package/dist/cli/messenger/stored.d.ts +1 -1
- package/dist/cli/messenger/stored.js +9 -5
- package/dist/errors/reasons.js +8 -0
- package/dist/meetings/mcp/index.d.ts +15 -15
- package/dist/oauth/authorize.js +21 -5
- package/dist/oauth/provider.d.ts +3 -1
- package/dist/ports/messenger.d.ts +8 -2
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -5,7 +5,45 @@ 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
|
-
##
|
|
8
|
+
## 0.543.0 — 02.11.2026
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **`events remind`: tell once about each event that starts soon, for cron or a timer to run every few minutes**,
|
|
13
|
+
as gcalcli's `remind` is run. It syncs first unless `--offline`, then takes the events that start within
|
|
14
|
+
`--due-within <duration>` (`10m` if not given, `1m` to `1d`) and take the owner's time: timed, not cancelled, not
|
|
15
|
+
declined, no focus time, out-of-office or working location. It prints each one, or, after `--`, runs the owner's
|
|
16
|
+
program once per event, with the event as JSON on its standard input and `<PREFIX>_EVENT=event:<id>` and
|
|
17
|
+
`<PREFIX>_PROFILE` in its environment. The program runs without a shell, and the event's text is never on its
|
|
18
|
+
command line: an invitation is a stranger's text. A program still running after 60 s is stopped.
|
|
19
|
+
- What was reminded is kept per profile in `<state>/checkpoints/<profile>/events-remind.json` (0600), one key per
|
|
20
|
+
event and start, so a moved meeting is reminded again. An event whose program failed is not kept, and the next
|
|
21
|
+
run tries it again; the answer is then `configuration_error` (`remind.hook-failed`). A lock keeps two runs from
|
|
22
|
+
reminding the same event. `--dry-run` runs, syncs and remembers nothing.
|
|
23
|
+
- A failed sync does not stop it: the answer says so in `meta.issues` and reminds from the store.
|
|
24
|
+
- The command needs the CLI's `writes` (`CalendarCommandDeps.writes`), whose settings it reads. It is never an MCP
|
|
25
|
+
tool.
|
|
26
|
+
- **`EVENTS_REMIND_HOOK_KEY` (`events.remind.hook`), in `CALENDAR_WRITE_KEYS` and `CALENDAR_WRITE_DEFAULTS` as
|
|
27
|
+
`deny`.** Running a program is a big permission, and `ask` would not be enough: `--yes` answers `ask`, and anything
|
|
28
|
+
that can type a command can add it. The owner allows it with `config set permissions.events.remind.hook allow`.
|
|
29
|
+
Under `<PREFIX>_READ_ONLY` the program never runs, whatever the level.
|
|
30
|
+
- **A pushed history batch tells its messenger whether it was saved.** `feed` and `listenHistory` take a
|
|
31
|
+
`HistoryBatchHandler`, `(batch) => void | Promise<boolean>`: the CLI's own handler resolves `true` once the batch is
|
|
32
|
+
in the local store and `false` when it could not be written, so an adapter can keep a batch it cannot get again
|
|
33
|
+
until it is stored. An adapter that ignores the answer works as before.
|
|
34
|
+
|
|
35
|
+
### Changed — may break callers
|
|
36
|
+
|
|
37
|
+
- **`@wirecat/cli-base` 0.73.0**, for the verb `remind`. The peer range is now `>=0.73.0 <0.74.0`.
|
|
38
|
+
|
|
39
|
+
## 0.542.0 — 02.11.2026
|
|
40
|
+
|
|
41
|
+
### Added
|
|
42
|
+
|
|
43
|
+
- **`redirect: { kind: "loopback", host: "localhost" }` signs in through `http://localhost:<port>/callback`.** Microsoft
|
|
44
|
+
Entra ignores the port only for `localhost` redirect URIs, so a public-client app registered as
|
|
45
|
+
`http://localhost/callback` needs it. The listener then takes `127.0.0.1` and, when the host has IPv6, `::1`, because
|
|
46
|
+
the browser may resolve `localhost` to either. Without `host` nothing changes: Google and the rest keep `127.0.0.1`.
|
|
9
47
|
|
|
10
48
|
## 0.541.0 — 02.11.2026
|
|
11
49
|
|
|
@@ -157,6 +157,25 @@ export declare const calendarEvent: v.StrictObjectSchema<{
|
|
|
157
157
|
}, undefined>, undefined>;
|
|
158
158
|
readonly untrusted: v.OptionalSchema<v.ArraySchema<v.StringSchema<undefined>, undefined>, undefined>;
|
|
159
159
|
}, undefined>;
|
|
160
|
+
export declare const calendarReminder: v.StrictObjectSchema<{
|
|
161
|
+
readonly id: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>;
|
|
162
|
+
readonly event_series_id: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
163
|
+
readonly title: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
164
|
+
readonly description: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
165
|
+
readonly location: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
166
|
+
readonly starts_at: v.NullableSchema<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`">]>, undefined>;
|
|
167
|
+
readonly ends_at: v.NullableSchema<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`">]>, undefined>;
|
|
168
|
+
readonly timezone: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
169
|
+
readonly origin: v.PicklistSchema<["auto", "owner", "calendar"], undefined>;
|
|
170
|
+
readonly status: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
171
|
+
readonly all_day: v.BooleanSchema<undefined>;
|
|
172
|
+
readonly source_deleted_at: v.NullableSchema<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`">]>, undefined>;
|
|
173
|
+
readonly deleted_at: v.NullableSchema<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`">]>, undefined>;
|
|
174
|
+
readonly created_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 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`">]>;
|
|
176
|
+
readonly untrusted: v.OptionalSchema<v.ArraySchema<v.StringSchema<undefined>, undefined>, undefined>;
|
|
177
|
+
readonly ref: v.StringSchema<undefined>;
|
|
178
|
+
}, undefined>;
|
|
160
179
|
/**
|
|
161
180
|
* A hit of the stored search (with `--backend both`, `server` too when the provider also found it), or one the
|
|
162
181
|
* provider's own search found and nothing stored.
|
|
@@ -127,6 +127,9 @@ export const calendarEvent = v.strictObject({
|
|
|
127
127
|
})),
|
|
128
128
|
untrusted,
|
|
129
129
|
});
|
|
130
|
+
/** One event `events remind` told about: the event, its reference, and the fields a stranger wrote. */
|
|
131
|
+
const { change: _change, changed_at: _changedAt, was: _was, overlapping: _overlapping, overlaps: _overlaps, ...listed } = event.entries;
|
|
132
|
+
export const calendarReminder = v.strictObject({ ...listed, ref: text });
|
|
130
133
|
const hitFields = {
|
|
131
134
|
provider: nullableText,
|
|
132
135
|
account: nullableText,
|
|
@@ -241,6 +244,7 @@ const ANSWERS = {
|
|
|
241
244
|
"events show": { name: "calendar_event", schema: calendarEvent },
|
|
242
245
|
"events prepare": { name: "calendar_event_preparation", schema: calendarEventPreparation },
|
|
243
246
|
"events export": { name: "calendar_export", schema: calendarExport },
|
|
247
|
+
"events remind": { name: "calendar_reminder", schema: calendarReminder },
|
|
244
248
|
"events import": { name: "calendar_import", schema: calendarImport },
|
|
245
249
|
"events create": { name: "calendar_event_write", schema: calendarEventWrite },
|
|
246
250
|
"events update": { name: "calendar_event_write", schema: calendarEventWrite },
|
|
@@ -20,6 +20,7 @@ import { bothSearchAnswer, serverSearchAnswer } from "../server-search.js";
|
|
|
20
20
|
import { calendarStatus, syncCalendars } from "../sync.js";
|
|
21
21
|
import { createEvent, createEventFromText, deleteEvent, EVENT_RESPONSES, IMPORT_MAX, importEvents, moveEvent, respondToEvent, updateEvent, } from "../writes.js";
|
|
22
22
|
import { CALENDARS_QUERY, declareCalendarAnswers, PEOPLE_QUERY } from "./answers.js";
|
|
23
|
+
import { addRemind } from "./remind.js";
|
|
23
24
|
const LOCAL_READ = { remote: "none", local: "read" };
|
|
24
25
|
const LOCAL_WRITE = { remote: "none", local: "write" };
|
|
25
26
|
const FETCH = { remote: "read", local: "write" };
|
|
@@ -300,6 +301,8 @@ const addCalendarCommandsDefinition = (program, deps) => {
|
|
|
300
301
|
});
|
|
301
302
|
print({ ...prepared, meta: { ...meta, ...window.meta, ...clockMeta(now(), zone()) } }, command);
|
|
302
303
|
});
|
|
304
|
+
if (deps.writes)
|
|
305
|
+
addRemind(events, deps.writes, { deps, now, zone, print, example });
|
|
303
306
|
const searches = annotate(program.command("search").description("find stored things by text: events"), {
|
|
304
307
|
effects: GROUP,
|
|
305
308
|
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Command } from "commander";
|
|
2
|
+
import type { CalendarWrites } from "../writes.js";
|
|
3
|
+
import type { CalendarCommandDeps } from "./index.js";
|
|
4
|
+
interface RemindHelpers {
|
|
5
|
+
deps: CalendarCommandDeps;
|
|
6
|
+
now: () => number;
|
|
7
|
+
zone: () => string;
|
|
8
|
+
print: (value: unknown, command: Command) => void;
|
|
9
|
+
example: (command: Command, ...lines: string[]) => Command;
|
|
10
|
+
}
|
|
11
|
+
export declare const addRemind: (events: Command, writes: CalendarWrites, { deps, now, zone, print, example }: RemindHelpers) => void;
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { CliError, resolvePaths } from "@wirecat/cli-base";
|
|
2
|
+
import { annotate } from "@wirecat/cli-base/commands";
|
|
3
|
+
import { clockMeta } from "@wirecat/cli-base/standards";
|
|
4
|
+
import { runBudgetOf } from "../../cli/context.js";
|
|
5
|
+
import { destroysNothing, FETCH } from "../../cli/effects.js";
|
|
6
|
+
import { terminalAsker } from "../../cli/messenger/ask.js";
|
|
7
|
+
import { parseDuration } from "../../domain/duration.js";
|
|
8
|
+
import { readOnlyRefusal } from "../../ports/app.js";
|
|
9
|
+
import { levelRefusal } from "../../sends/guard.js";
|
|
10
|
+
import { DEFAULT_PERMISSIONS, levelFor } from "../../sends/permissions.js";
|
|
11
|
+
import { dueEvents, openReminders, reminderItem, remindersPath, runHook } from "../remind.js";
|
|
12
|
+
import { syncCalendars } from "../sync.js";
|
|
13
|
+
import { CALENDAR_WRITE_DEFAULTS, EVENTS_REMIND_HOOK_KEY } from "../writer.js";
|
|
14
|
+
const REMIND_MIN_MS = 60_000;
|
|
15
|
+
const REMIND_MAX_MS = 86_400_000;
|
|
16
|
+
export const addRemind = (events, writes, { deps, now, zone, print, example }) => {
|
|
17
|
+
const remind = example(destroysNothing(annotate(events.command("remind"), { effects: FETCH, mcp: false }))
|
|
18
|
+
.description("tell once about each event that starts within --due-within: print it, or run your program for it with the " +
|
|
19
|
+
"event as JSON on its standard input; for cron or a timer every few minutes, syncing first unless --offline")
|
|
20
|
+
.summary("tell once about each event that starts soon, for a timer to run")
|
|
21
|
+
.argument("[program...]", "after --: the program to run for each event, and its own arguments; no shell. Needs events.remind.hook " +
|
|
22
|
+
"allowed"), "events remind --due-within 10m", "events remind --due-within 15m -- /home/me/bin/meeting-reminder")
|
|
23
|
+
.option("--due-within <duration>", "the events that start within this long from now, 1m to 1d", "10m")
|
|
24
|
+
.option("--dry-run", "say what it would remind; run nothing, sync nothing, remember nothing");
|
|
25
|
+
remind.action(async (program, opts, command) => {
|
|
26
|
+
const within = parseDuration(opts.dueWithin, "--due-within");
|
|
27
|
+
if (within < REMIND_MIN_MS || within > REMIND_MAX_MS)
|
|
28
|
+
throw new CliError("validation_error", "--due-within takes 1m to 1d", { reason: "remind.window" });
|
|
29
|
+
const settings = writes.settings();
|
|
30
|
+
const env = writes.env ?? process.env;
|
|
31
|
+
const hook = program.length > 0;
|
|
32
|
+
const defaults = { ...DEFAULT_PERMISSIONS, ...CALENDAR_WRITE_DEFAULTS, ...settings.permissionDefaults };
|
|
33
|
+
const { level, key: named } = levelFor(settings.permissions, EVENTS_REMIND_HOOK_KEY, defaults);
|
|
34
|
+
if (hook && !opts.dryRun) {
|
|
35
|
+
if (settings.readOnlyLock)
|
|
36
|
+
throw readOnlyRefusal(settings.readOnlyLock, "run a program for a reminder");
|
|
37
|
+
if (level === "deny" || level === "readonly")
|
|
38
|
+
throw levelRefusal({
|
|
39
|
+
profile: settings.profile,
|
|
40
|
+
command: writes.app.command,
|
|
41
|
+
key: EVENTS_REMIND_HOOK_KEY,
|
|
42
|
+
level,
|
|
43
|
+
named,
|
|
44
|
+
permissionSources: settings.permissionSources,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
const at = now();
|
|
48
|
+
const issues = [];
|
|
49
|
+
const accountName = `${deps.account.provider}:${deps.account.account}`;
|
|
50
|
+
if (!opts.dryRun && deps.source && !command.optsWithGlobals().offline) {
|
|
51
|
+
// A reminder from what the store already holds beats none: a failed sync is said in meta and the run goes on.
|
|
52
|
+
try {
|
|
53
|
+
const report = await syncCalendars(deps.source, deps.store, {
|
|
54
|
+
accountId: deps.accountId,
|
|
55
|
+
now: at,
|
|
56
|
+
budget: runBudgetOf(command),
|
|
57
|
+
...(deps.expand ? { expand: deps.expand } : {}),
|
|
58
|
+
});
|
|
59
|
+
if (report.synced.some((one) => one.error !== undefined))
|
|
60
|
+
issues.push({ code: "provider_error", reason: "sync.partial", source: accountName });
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
issues.push({
|
|
64
|
+
code: error instanceof CliError ? error.code : "provider_error",
|
|
65
|
+
reason: "sync.failed",
|
|
66
|
+
source: accountName,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const window = { from: at, to: at + within };
|
|
71
|
+
const path = remindersPath(resolvePaths({ appName: writes.app.appName, prefix: writes.app.envPrefix, env }).state, settings.profile);
|
|
72
|
+
const reminders = opts.dryRun ? undefined : openReminders(path, at);
|
|
73
|
+
try {
|
|
74
|
+
const due = await dueEvents(deps.store, deps.accountId, window, reminders?.reminded ?? {});
|
|
75
|
+
if (hook && !opts.dryRun && due.length > 0 && level === "ask")
|
|
76
|
+
await terminalAsker(command)(EVENTS_REMIND_HOOK_KEY, { chatId: null, count: due.length });
|
|
77
|
+
const items = [];
|
|
78
|
+
const failed = [];
|
|
79
|
+
for (const details of due) {
|
|
80
|
+
const item = reminderItem(details);
|
|
81
|
+
if (hook && !opts.dryRun) {
|
|
82
|
+
const exitCode = await runHook(program, `${JSON.stringify(item)}\n`, {
|
|
83
|
+
...env,
|
|
84
|
+
[`${writes.app.envPrefix}_PROFILE`]: settings.profile,
|
|
85
|
+
[`${writes.app.envPrefix}_EVENT`]: item.ref,
|
|
86
|
+
});
|
|
87
|
+
if (exitCode !== 0) {
|
|
88
|
+
failed.push({ ref: item.ref, exit_code: exitCode });
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
reminders?.remember(details);
|
|
93
|
+
items.push(item);
|
|
94
|
+
}
|
|
95
|
+
if (failed.length)
|
|
96
|
+
throw new CliError("configuration_error", `${program[0]} failed for ${failed.length} of ${due.length} events ` +
|
|
97
|
+
`(${failed.map((one) => `${one.ref}: exit ${one.exit_code}`).join(", ")}); the next run tries them again`, { reason: "remind.hook-failed", reminded: items.map(({ ref }) => ref), failed, retryable: true });
|
|
98
|
+
print({
|
|
99
|
+
items,
|
|
100
|
+
page: 1,
|
|
101
|
+
limit: Math.max(1, items.length),
|
|
102
|
+
hasMore: false,
|
|
103
|
+
meta: {
|
|
104
|
+
complete: issues.length === 0,
|
|
105
|
+
...(issues.length ? { issues } : {}),
|
|
106
|
+
window: { since: new Date(window.from).toISOString(), until: new Date(window.to).toISOString() },
|
|
107
|
+
...clockMeta(at, zone()),
|
|
108
|
+
},
|
|
109
|
+
}, command);
|
|
110
|
+
}
|
|
111
|
+
finally {
|
|
112
|
+
reminders?.close();
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
};
|
package/dist/calendar/index.d.ts
CHANGED
|
@@ -3,5 +3,5 @@ export { type IcsEvent, type IcsSkip, readIcs, writeIcs } from "./ics.js";
|
|
|
3
3
|
export { repeatRule } from "./recurrence.js";
|
|
4
4
|
export type { BusyTimes, CalendarExpander, CalendarSource, CalendarSourceTraits, ChangePage, ChangeResult, FreeBusyRequest, SearchPage, SyncWindow, } from "./source.js";
|
|
5
5
|
export { type CalendarCursor, type CalendarSyncOptions, type CalendarSyncReport, type CalendarSyncResult, calendarStatus, calendarSyncWindow, type PendingRound, parseCalendarCursor, syncCalendars, } from "./sync.js";
|
|
6
|
-
export { CALENDAR_WRITE_DEFAULTS, CALENDAR_WRITE_KEYS, type CalendarWrite, type CalendarWriter, calendarConflict, EVENTS_DELETE_SERIES_KEY, EVENTS_NOTIFY_KEY, type EventDraft, type EventPatch, type EventResponse, type FollowingScope, type WriteScope, } from "./writer.js";
|
|
6
|
+
export { CALENDAR_WRITE_DEFAULTS, CALENDAR_WRITE_KEYS, type CalendarWrite, type CalendarWriter, calendarConflict, EVENTS_DELETE_SERIES_KEY, EVENTS_NOTIFY_KEY, EVENTS_REMIND_HOOK_KEY, type EventDraft, type EventPatch, type EventResponse, type FollowingScope, type WriteScope, } from "./writer.js";
|
|
7
7
|
export type { CalendarWrites } from "./writes.js";
|
package/dist/calendar/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { readIcs, writeIcs } from "./ics.js";
|
|
2
2
|
export { repeatRule } from "./recurrence.js";
|
|
3
3
|
export { calendarStatus, calendarSyncWindow, parseCalendarCursor, syncCalendars, } from "./sync.js";
|
|
4
|
-
export { CALENDAR_WRITE_DEFAULTS, CALENDAR_WRITE_KEYS, calendarConflict, EVENTS_DELETE_SERIES_KEY, EVENTS_NOTIFY_KEY, } from "./writer.js";
|
|
4
|
+
export { CALENDAR_WRITE_DEFAULTS, CALENDAR_WRITE_KEYS, calendarConflict, EVENTS_DELETE_SERIES_KEY, EVENTS_NOTIFY_KEY, EVENTS_REMIND_HOOK_KEY, } from "./writer.js";
|
|
@@ -7,6 +7,11 @@ export declare const PREPARE_MESSAGES = 5;
|
|
|
7
7
|
/** Attendees past this many are listed without their messages: a meeting of fifty would not fit any budget. */
|
|
8
8
|
export declare const PREPARE_PEOPLE = 10;
|
|
9
9
|
export declare const PREPARE_BYTES: number;
|
|
10
|
+
/**
|
|
11
|
+
* An event that takes the owner's time: timed, not cancelled, not declined by the owner, and an ordinary event (no
|
|
12
|
+
* focus time, out-of-office or working location).
|
|
13
|
+
*/
|
|
14
|
+
export declare const ownsTime: (details: EventDetails) => boolean;
|
|
10
15
|
/** The account's next meeting that has not started yet, by the rule of `isMeeting`. */
|
|
11
16
|
export declare const nextMeeting: (store: EventStore, accountId: number, now: number) => Promise<EventDetails>;
|
|
12
17
|
export interface PreparationOptions {
|
package/dist/calendar/prepare.js
CHANGED
|
@@ -16,19 +16,19 @@ const NEXT_PAGE = 50;
|
|
|
16
16
|
const nameOf = ({ provider, account }) => `${provider}:${account}`;
|
|
17
17
|
const size = (value) => Buffer.byteLength(JSON.stringify(value), "utf8");
|
|
18
18
|
/**
|
|
19
|
-
*
|
|
20
|
-
* time, out-of-office or working location)
|
|
19
|
+
* An event that takes the owner's time: timed, not cancelled, not declined by the owner, and an ordinary event (no
|
|
20
|
+
* focus time, out-of-office or working location).
|
|
21
21
|
*/
|
|
22
|
-
const
|
|
22
|
+
export const ownsTime = (details) => {
|
|
23
23
|
if (details.event.allDay || details.event.status === "cancelled")
|
|
24
24
|
return false;
|
|
25
25
|
const live = details.entries.filter((entry) => entry.deletedAt === null && entry.status !== "cancelled");
|
|
26
26
|
if (!live.length || live.every((entry) => entry.ownResponse === "declined"))
|
|
27
27
|
return false;
|
|
28
|
-
|
|
29
|
-
return false;
|
|
30
|
-
return details.participants.some((one) => !one.self && !one.resource);
|
|
28
|
+
return !live.every((entry) => entry.eventType !== null && entry.eventType !== "default");
|
|
31
29
|
};
|
|
30
|
+
/** The next meeting: an event that takes the owner's time, with someone other than the owner and the rooms invited. */
|
|
31
|
+
const isMeeting = (details) => ownsTime(details) && details.participants.some((one) => !one.self && !one.resource);
|
|
32
32
|
/** The account's next meeting that has not started yet, by the rule of `isMeeting`. */
|
|
33
33
|
export const nextMeeting = async (store, accountId, now) => {
|
|
34
34
|
for (let offset = 0; offset < NEXT_SCAN; offset += NEXT_PAGE) {
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { type EventDetails, type EventStore } from "../meetings/index.js";
|
|
2
|
+
/** A program that has not finished by then is stopped, so a stuck one does not hold the next run's lock. */
|
|
3
|
+
export declare const HOOK_TIMEOUT_MS = 60000;
|
|
4
|
+
export type Reminded = Record<string, string>;
|
|
5
|
+
export declare const remindersPath: (state: string, profile: string) => string;
|
|
6
|
+
/** Events that take the owner's time and start in the window, earliest first, and are not yet reminded. */
|
|
7
|
+
export declare const dueEvents: (store: EventStore, accountId: number, window: {
|
|
8
|
+
from: number;
|
|
9
|
+
to: number;
|
|
10
|
+
}, reminded: Reminded) => Promise<EventDetails[]>;
|
|
11
|
+
export declare const reminderItem: (details: EventDetails) => {
|
|
12
|
+
id: string;
|
|
13
|
+
event_series_id: string | null;
|
|
14
|
+
title: string | null;
|
|
15
|
+
description: string | null;
|
|
16
|
+
location: string | null;
|
|
17
|
+
starts_at: string | null;
|
|
18
|
+
ends_at: string | null;
|
|
19
|
+
timezone: string | null;
|
|
20
|
+
origin: "auto" | "calendar" | "owner";
|
|
21
|
+
status: string | null;
|
|
22
|
+
all_day: boolean;
|
|
23
|
+
source_deleted_at: string | null;
|
|
24
|
+
deleted_at: string | null;
|
|
25
|
+
created_at: string;
|
|
26
|
+
updated_at: string;
|
|
27
|
+
change?: "cancelled" | "moved" | "new" | undefined;
|
|
28
|
+
changed_at?: string | null | undefined;
|
|
29
|
+
was?: {
|
|
30
|
+
starts_at: string | null;
|
|
31
|
+
ends_at: string | null;
|
|
32
|
+
} | undefined;
|
|
33
|
+
overlapping?: boolean | undefined;
|
|
34
|
+
overlaps?: string[] | undefined;
|
|
35
|
+
ref: string;
|
|
36
|
+
untrusted: string[];
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Runs the owner's program with the reminder as JSON on its standard input, never through a shell: the event's text
|
|
40
|
+
* is a stranger's, and only the program's own argv, typed by the owner, is on its command line.
|
|
41
|
+
*/
|
|
42
|
+
export declare const runHook: (argv: readonly string[], input: string, env: NodeJS.ProcessEnv, timeoutMs?: number) => Promise<number>;
|
|
43
|
+
/**
|
|
44
|
+
* What was reminded, and the lock that keeps two runs from reminding the same event at once. `remember` keeps a day
|
|
45
|
+
* of past starts and forgets older ones.
|
|
46
|
+
*/
|
|
47
|
+
export declare const openReminders: (path: string, now: number) => {
|
|
48
|
+
reminded: Reminded;
|
|
49
|
+
remember: (details: EventDetails) => void;
|
|
50
|
+
close: () => void;
|
|
51
|
+
};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { CliError } from "@wirecat/cli-base";
|
|
3
|
+
import { checkpointPath, readCheckpoint, saveCheckpoint } from "../background/checkpoints.js";
|
|
4
|
+
import { releaseLock, takeLock } from "../background/lock.js";
|
|
5
|
+
import { eventItem } from "../meetings/index.js";
|
|
6
|
+
import { ownsTime } from "./prepare.js";
|
|
7
|
+
import { eventOf } from "./reads.js";
|
|
8
|
+
const DAY = 86_400_000;
|
|
9
|
+
/** More would be a calendar that is not the owner's own; the rest wait for the next run. */
|
|
10
|
+
const REMIND_MAX = 50;
|
|
11
|
+
/** A program that has not finished by then is stopped, so a stuck one does not hold the next run's lock. */
|
|
12
|
+
export const HOOK_TIMEOUT_MS = 60_000;
|
|
13
|
+
const KILL_AFTER_MS = 5_000;
|
|
14
|
+
const UNTRUSTED = ["title", "description", "location"];
|
|
15
|
+
/** One key per event and start: a meeting moved to another time is reminded again. */
|
|
16
|
+
const keyOf = (details) => `${details.event.id}@${new Date(details.event.startsAt ?? 0).toISOString()}`;
|
|
17
|
+
export const remindersPath = (state, profile) => checkpointPath(state, profile, "events-remind");
|
|
18
|
+
/** Events that take the owner's time and start in the window, earliest first, and are not yet reminded. */
|
|
19
|
+
export const dueEvents = async (store, accountId, window, reminded) => {
|
|
20
|
+
const due = [];
|
|
21
|
+
for (const one of await store.events({ accountId, startsFrom: window.from, to: window.to, limit: REMIND_MAX })) {
|
|
22
|
+
const details = await eventOf(store, accountId, one.id).catch(() => undefined);
|
|
23
|
+
if (details && ownsTime(details) && reminded[keyOf(details)] === undefined)
|
|
24
|
+
due.push(details);
|
|
25
|
+
}
|
|
26
|
+
return due;
|
|
27
|
+
};
|
|
28
|
+
export const reminderItem = (details) => ({
|
|
29
|
+
...eventItem(details.event),
|
|
30
|
+
ref: `event:${details.event.id}`,
|
|
31
|
+
untrusted: UNTRUSTED,
|
|
32
|
+
});
|
|
33
|
+
/**
|
|
34
|
+
* Runs the owner's program with the reminder as JSON on its standard input, never through a shell: the event's text
|
|
35
|
+
* is a stranger's, and only the program's own argv, typed by the owner, is on its command line.
|
|
36
|
+
*/
|
|
37
|
+
export const runHook = (argv, input, env, timeoutMs = HOOK_TIMEOUT_MS) => new Promise((resolve) => {
|
|
38
|
+
const [program = "", ...args] = argv;
|
|
39
|
+
const child = spawn(program, args, { env, shell: false, stdio: ["pipe", "ignore", "inherit"], windowsHide: true });
|
|
40
|
+
let kill;
|
|
41
|
+
const stop = setTimeout(() => {
|
|
42
|
+
child.kill("SIGTERM");
|
|
43
|
+
kill = setTimeout(() => child.kill("SIGKILL"), KILL_AFTER_MS);
|
|
44
|
+
}, timeoutMs);
|
|
45
|
+
const done = (code) => {
|
|
46
|
+
clearTimeout(stop);
|
|
47
|
+
if (kill)
|
|
48
|
+
clearTimeout(kill);
|
|
49
|
+
resolve(code);
|
|
50
|
+
};
|
|
51
|
+
child.on("error", () => done(127));
|
|
52
|
+
child.on("close", (code, signal) => done(code ?? (signal === "SIGKILL" ? 137 : 143)));
|
|
53
|
+
// A program that reads nothing closes its end first; that is its choice, not a failure.
|
|
54
|
+
child.stdin.on("error", () => { });
|
|
55
|
+
child.stdin.end(input);
|
|
56
|
+
});
|
|
57
|
+
/**
|
|
58
|
+
* What was reminded, and the lock that keeps two runs from reminding the same event at once. `remember` keeps a day
|
|
59
|
+
* of past starts and forgets older ones.
|
|
60
|
+
*/
|
|
61
|
+
export const openReminders = (path, now) => {
|
|
62
|
+
const lock = `${path}.lock`;
|
|
63
|
+
const held = takeLock(lock, { pid: process.pid, startedAt: new Date(now).toISOString() });
|
|
64
|
+
if (held)
|
|
65
|
+
throw new CliError("validation_error", `another events remind is running (pid ${held.pid}); this run did nothing`, {
|
|
66
|
+
reason: "remind.running",
|
|
67
|
+
retryable: true,
|
|
68
|
+
});
|
|
69
|
+
const reminded = { ...readCheckpoint(path)?.cursor };
|
|
70
|
+
return {
|
|
71
|
+
reminded,
|
|
72
|
+
remember: (details) => {
|
|
73
|
+
reminded[keyOf(details)] = new Date(now).toISOString();
|
|
74
|
+
const kept = Object.fromEntries(Object.entries(reminded).filter(([key]) => Date.parse(key.slice(key.indexOf("@") + 1)) >= now - DAY));
|
|
75
|
+
saveCheckpoint(path, { cursor: kept, done: Object.keys(kept).length }, new Date(now));
|
|
76
|
+
},
|
|
77
|
+
close: () => releaseLock(lock),
|
|
78
|
+
};
|
|
79
|
+
};
|
|
@@ -93,6 +93,8 @@ export type CalendarWrite = "create" | "quick-add" | "import" | "update" | "dele
|
|
|
93
93
|
/** Mailing the other attendees: an agent may write to the calendar and still not to people (WC-PERM-14's spirit). */
|
|
94
94
|
export declare const EVENTS_NOTIFY_KEY: PermissionKey;
|
|
95
95
|
export declare const EVENTS_DELETE_SERIES_KEY: PermissionKey;
|
|
96
|
+
/** `events remind -- <program>`: runs a program of the owner's on this machine, so it waits for the owner's allow. */
|
|
97
|
+
export declare const EVENTS_REMIND_HOOK_KEY: PermissionKey;
|
|
96
98
|
/** The keys the calendar writes check beyond their command paths, for a CLI's `commandPermissions({ extra })`. */
|
|
97
99
|
export declare const CALENDAR_WRITE_KEYS: readonly PermissionKey[];
|
|
98
100
|
/**
|
package/dist/calendar/writer.js
CHANGED
|
@@ -2,8 +2,14 @@ import { CliError } from "@wirecat/cli-base";
|
|
|
2
2
|
/** Mailing the other attendees: an agent may write to the calendar and still not to people (WC-PERM-14's spirit). */
|
|
3
3
|
export const EVENTS_NOTIFY_KEY = "events.notify";
|
|
4
4
|
export const EVENTS_DELETE_SERIES_KEY = "events.delete.series";
|
|
5
|
+
/** `events remind -- <program>`: runs a program of the owner's on this machine, so it waits for the owner's allow. */
|
|
6
|
+
export const EVENTS_REMIND_HOOK_KEY = "events.remind.hook";
|
|
5
7
|
/** The keys the calendar writes check beyond their command paths, for a CLI's `commandPermissions({ extra })`. */
|
|
6
|
-
export const CALENDAR_WRITE_KEYS = [
|
|
8
|
+
export const CALENDAR_WRITE_KEYS = [
|
|
9
|
+
EVENTS_NOTIFY_KEY,
|
|
10
|
+
EVENTS_DELETE_SERIES_KEY,
|
|
11
|
+
EVENTS_REMIND_HOOK_KEY,
|
|
12
|
+
];
|
|
7
13
|
/**
|
|
8
14
|
* Built in, and only ever tightening (WC-PERM-7). A change that is easy to undo goes ahead, as tg's and drive's writes
|
|
9
15
|
* do; a deletion asks; a whole series is not deleted, and nobody is mailed.
|
|
@@ -18,6 +24,8 @@ export const CALENDAR_WRITE_DEFAULTS = {
|
|
|
18
24
|
"events.delete": "ask",
|
|
19
25
|
[EVENTS_DELETE_SERIES_KEY]: "deny",
|
|
20
26
|
[EVENTS_NOTIFY_KEY]: "deny",
|
|
27
|
+
// Not ask: `--yes` answers ask, and anything that can type a gcal command can add it.
|
|
28
|
+
[EVENTS_REMIND_HOOK_KEY]: "deny",
|
|
21
29
|
};
|
|
22
30
|
/** What an adapter throws when the provider refused a write because the entry's `etag` is old (HTTP 412). */
|
|
23
31
|
export const calendarConflict = (message = "the event changed at the provider since the last sync") => new CliError("validation_error", `${message}; sync and try again`, { reason: "event.conflict" });
|
package/dist/cli/index.d.ts
CHANGED
|
@@ -14,7 +14,7 @@ export { registerLinkConversationsPrompt } from "../mcp/prompts.js";
|
|
|
14
14
|
export { agentBody, agentJson, agentResult, UNTRUSTED, UNTRUSTED_INSTRUCTION, UNTRUSTED_NEUTRAL } from "../mcp/text.js";
|
|
15
15
|
export { MEETING_RESULT_ENTITIES } from "../meetings/index.js";
|
|
16
16
|
export { type AppIdentity, envName, readOnlyLockOf, readOnlyRefusal } from "../ports/app.js";
|
|
17
|
-
export type { AccountEditing, AccountHealth, AccountRecords, AccountSettings, AccountStanding, AccountTools, After, BotChats, ChannelComments, ChatDeletion, ChatFolders, ChatReading, ContactBook, Download, ForumControl, ForumState, GroupAdmin, GroupModeration, HistoryBatch, HistoryChatEvent, HistoryReaction, HistoryStar, HistoryVote, InviteLinks, JoinRequests, LiveUpdates, MessageButtons, MessageEditing, MessageMedia, MessagePermalinks, MessagePins, MessagePolls, MessageReactions, MessageSearch, MessageStars, MessengerAdapter, MessengerCore, NewPoll, OfficialStats, PassiveUpdates, PersonProfiles, ProfileChange, ProfilePhotos, PushedHistory, ReadState, RemoteFile, ScheduledMessages, SenderIdentities, SenderSearch, SendOptions, Sent, ServerQuery, ServerReads, ThreadAddressing, TopicEditing, TopicHistory, TopicReadState, Transcript, } from "../ports/messenger.js";
|
|
17
|
+
export type { AccountEditing, AccountHealth, AccountRecords, AccountSettings, AccountStanding, AccountTools, After, BotChats, ChannelComments, ChatDeletion, ChatFolders, ChatReading, ContactBook, Download, ForumControl, ForumState, GroupAdmin, GroupModeration, HistoryBatch, HistoryBatchHandler, HistoryChatEvent, HistoryReaction, HistoryStar, HistoryVote, InviteLinks, JoinRequests, LiveUpdates, MessageButtons, MessageEditing, MessageMedia, MessagePermalinks, MessagePins, MessagePolls, MessageReactions, MessageSearch, MessageStars, MessengerAdapter, MessengerCore, NewPoll, OfficialStats, PassiveUpdates, PersonProfiles, ProfileChange, ProfilePhotos, PushedHistory, ReadState, RemoteFile, ScheduledMessages, SenderIdentities, SenderSearch, SendOptions, Sent, ServerQuery, ServerReads, ThreadAddressing, TopicEditing, TopicHistory, TopicReadState, Transcript, } from "../ports/messenger.js";
|
|
18
18
|
export type { Fetching, MediaOption, MessengerTraits } from "../ports/traits.js";
|
|
19
19
|
export { senderFacts } from "../replies/dry-run.js";
|
|
20
20
|
export { type ReplyRender, replyRenderer } from "../replies/rendering.js";
|
|
@@ -3,9 +3,9 @@ import type { CacheDatabase } from "../../store/driver.js";
|
|
|
3
3
|
import type { Messenger } from "./context.js";
|
|
4
4
|
/** `doctor`'s short summary of the store. Must answer for a broken file, so an error is a field. */
|
|
5
5
|
export declare const storeSummary: (env: NodeJS.ProcessEnv) => Promise<{
|
|
6
|
-
error?: undefined;
|
|
7
6
|
path: string;
|
|
8
7
|
exists: boolean;
|
|
8
|
+
error?: undefined;
|
|
9
9
|
} | {
|
|
10
10
|
path: string;
|
|
11
11
|
exists: boolean;
|
|
@@ -10,7 +10,7 @@ export interface Saving {
|
|
|
10
10
|
warn: Warn;
|
|
11
11
|
events: EventSink;
|
|
12
12
|
/** Saves still writing, for the closer to wait on: `watch` does not hold the next event for one. */
|
|
13
|
-
pending?: Set<Promise<
|
|
13
|
+
pending?: Set<Promise<unknown>>;
|
|
14
14
|
/** `storedChats` calls still running, apart from `pending`: one abandoned at exit leaves nothing unsaved. */
|
|
15
15
|
announcing?: Set<Promise<void>>;
|
|
16
16
|
deletedWithoutChat?: DeletionScope;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { CliError } from "@wirecat/cli-base";
|
|
2
|
-
import { capability, throughWrapper } from "../../ports/messenger.js";
|
|
2
|
+
import { capability, throughWrapper, } from "../../ports/messenger.js";
|
|
3
3
|
import { foldName, isId, pickPerson } from "../../resolve.js";
|
|
4
4
|
import { codeOf } from "../../sends/guarded.js";
|
|
5
5
|
import { chatIdIn } from "../../services/message-resolution.js";
|
|
@@ -25,8 +25,9 @@ export const stored = (messenger, { account, store, warn, events, pending, annou
|
|
|
25
25
|
const attempt = async (operation, write) => {
|
|
26
26
|
try {
|
|
27
27
|
const opened = await store();
|
|
28
|
-
if (opened)
|
|
29
|
-
|
|
28
|
+
if (!opened)
|
|
29
|
+
return false;
|
|
30
|
+
await write(opened);
|
|
30
31
|
// After the write, so saves keep their order; a convenience for naming this account from another app.
|
|
31
32
|
if (opened && !recorded && profile) {
|
|
32
33
|
recorded = true;
|
|
@@ -34,9 +35,11 @@ export const stored = (messenger, { account, store, warn, events, pending, annou
|
|
|
34
35
|
.then(() => opened.accounts.saveProfile(profile.app, profile.name, account))
|
|
35
36
|
.catch(() => { });
|
|
36
37
|
}
|
|
38
|
+
return true;
|
|
37
39
|
}
|
|
38
40
|
catch (error) {
|
|
39
41
|
notWritten(operation, error);
|
|
42
|
+
return false;
|
|
40
43
|
}
|
|
41
44
|
};
|
|
42
45
|
const save = (operation, writing) => {
|
|
@@ -152,7 +155,7 @@ export const stored = (messenger, { account, store, warn, events, pending, annou
|
|
|
152
155
|
// A refused re-key leaves its chat split, never the others or the batch's history unsaved.
|
|
153
156
|
const saving = previous.then(async () => {
|
|
154
157
|
await saveRekeys(rekeys, round);
|
|
155
|
-
|
|
158
|
+
return attempt(operation, (opened) => fill(opened, account, batch));
|
|
156
159
|
});
|
|
157
160
|
previous = saving;
|
|
158
161
|
pending?.add(saving);
|
|
@@ -162,7 +165,8 @@ export const stored = (messenger, { account, store, warn, events, pending, annou
|
|
|
162
165
|
pending?.delete(saving);
|
|
163
166
|
rekeysOnly.delete(saving);
|
|
164
167
|
});
|
|
165
|
-
onBatch(batch);
|
|
168
|
+
void onBatch(batch);
|
|
169
|
+
return saving;
|
|
166
170
|
};
|
|
167
171
|
};
|
|
168
172
|
// A method not listed here saves nothing and passes through; a lane that should save adds its line.
|
package/dist/errors/reasons.js
CHANGED
|
@@ -57,6 +57,14 @@ const OTHERS = [
|
|
|
57
57
|
["sync.partial", "provider_error", "some calendars did not sync; the next sync retries them"],
|
|
58
58
|
validation("event.conflict", "the event changed at the provider since the last sync; sync and try again"),
|
|
59
59
|
validation("event.notify-required", "the event has other attendees; say whether to notify them"),
|
|
60
|
+
validation("remind.window", "--due-within is outside 1m to 1d"),
|
|
61
|
+
validation("remind.running", "another events remind is running; this run did nothing"),
|
|
62
|
+
[
|
|
63
|
+
"remind.hook-failed",
|
|
64
|
+
"configuration_error",
|
|
65
|
+
"the reminder program failed for some events; the next run retries them",
|
|
66
|
+
],
|
|
67
|
+
["sync.failed", "provider_error", "the sync failed; the answer comes from what the store already holds"],
|
|
60
68
|
validation("event.repeat-invalid", "the repeat rule is outside the RFC 5545 subset the calendars take"),
|
|
61
69
|
validation("event.repeat-kind", "the series' rule holds a date or a time of the kind the event is leaving"),
|
|
62
70
|
validation("event.repeat-split", "the series cannot be split at this occurrence, or was split there already"),
|
|
@@ -38,6 +38,7 @@ export declare const meetingTools: ({
|
|
|
38
38
|
max_read_bytes?: undefined;
|
|
39
39
|
cursor?: undefined;
|
|
40
40
|
} | {
|
|
41
|
+
limit?: undefined;
|
|
41
42
|
event_id?: undefined;
|
|
42
43
|
series_id?: undefined;
|
|
43
44
|
page?: undefined;
|
|
@@ -53,11 +54,11 @@ export declare const meetingTools: ({
|
|
|
53
54
|
format?: undefined;
|
|
54
55
|
transcript_id?: undefined;
|
|
55
56
|
query?: undefined;
|
|
56
|
-
limit?: undefined;
|
|
57
57
|
scan_limit?: undefined;
|
|
58
58
|
max_read_bytes?: undefined;
|
|
59
59
|
cursor?: undefined;
|
|
60
60
|
} | {
|
|
61
|
+
limit?: undefined;
|
|
61
62
|
event_id?: undefined;
|
|
62
63
|
series_id?: undefined;
|
|
63
64
|
page?: undefined;
|
|
@@ -75,11 +76,11 @@ export declare const meetingTools: ({
|
|
|
75
76
|
format?: undefined;
|
|
76
77
|
transcript_id?: undefined;
|
|
77
78
|
query?: undefined;
|
|
78
|
-
limit?: undefined;
|
|
79
79
|
scan_limit?: undefined;
|
|
80
80
|
max_read_bytes?: undefined;
|
|
81
81
|
cursor?: undefined;
|
|
82
82
|
} | {
|
|
83
|
+
limit?: undefined;
|
|
83
84
|
event_id?: undefined;
|
|
84
85
|
series_id?: undefined;
|
|
85
86
|
page?: undefined;
|
|
@@ -101,7 +102,6 @@ export declare const meetingTools: ({
|
|
|
101
102
|
minimum: number;
|
|
102
103
|
};
|
|
103
104
|
query?: undefined;
|
|
104
|
-
limit?: undefined;
|
|
105
105
|
scan_limit?: undefined;
|
|
106
106
|
max_read_bytes?: undefined;
|
|
107
107
|
cursor?: undefined;
|
|
@@ -188,6 +188,7 @@ export declare const meetingTools: ({
|
|
|
188
188
|
max_read_bytes?: undefined;
|
|
189
189
|
cursor?: undefined;
|
|
190
190
|
} | {
|
|
191
|
+
limit?: undefined;
|
|
191
192
|
event_id?: undefined;
|
|
192
193
|
series_id?: undefined;
|
|
193
194
|
page?: undefined;
|
|
@@ -203,11 +204,11 @@ export declare const meetingTools: ({
|
|
|
203
204
|
format?: undefined;
|
|
204
205
|
transcript_id?: undefined;
|
|
205
206
|
query?: undefined;
|
|
206
|
-
limit?: undefined;
|
|
207
207
|
scan_limit?: undefined;
|
|
208
208
|
max_read_bytes?: undefined;
|
|
209
209
|
cursor?: undefined;
|
|
210
210
|
} | {
|
|
211
|
+
limit?: undefined;
|
|
211
212
|
event_id?: undefined;
|
|
212
213
|
series_id?: undefined;
|
|
213
214
|
page?: undefined;
|
|
@@ -225,11 +226,11 @@ export declare const meetingTools: ({
|
|
|
225
226
|
format?: undefined;
|
|
226
227
|
transcript_id?: undefined;
|
|
227
228
|
query?: undefined;
|
|
228
|
-
limit?: undefined;
|
|
229
229
|
scan_limit?: undefined;
|
|
230
230
|
max_read_bytes?: undefined;
|
|
231
231
|
cursor?: undefined;
|
|
232
232
|
} | {
|
|
233
|
+
limit?: undefined;
|
|
233
234
|
event_id?: undefined;
|
|
234
235
|
series_id?: undefined;
|
|
235
236
|
page?: undefined;
|
|
@@ -251,7 +252,6 @@ export declare const meetingTools: ({
|
|
|
251
252
|
minimum: number;
|
|
252
253
|
};
|
|
253
254
|
query?: undefined;
|
|
254
|
-
limit?: undefined;
|
|
255
255
|
scan_limit?: undefined;
|
|
256
256
|
max_read_bytes?: undefined;
|
|
257
257
|
cursor?: undefined;
|
|
@@ -338,6 +338,7 @@ export declare const meetingTools: ({
|
|
|
338
338
|
max_read_bytes?: undefined;
|
|
339
339
|
cursor?: undefined;
|
|
340
340
|
} | {
|
|
341
|
+
limit?: undefined;
|
|
341
342
|
event_id?: undefined;
|
|
342
343
|
series_id?: undefined;
|
|
343
344
|
page?: undefined;
|
|
@@ -353,11 +354,11 @@ export declare const meetingTools: ({
|
|
|
353
354
|
format?: undefined;
|
|
354
355
|
transcript_id?: undefined;
|
|
355
356
|
query?: undefined;
|
|
356
|
-
limit?: undefined;
|
|
357
357
|
scan_limit?: undefined;
|
|
358
358
|
max_read_bytes?: undefined;
|
|
359
359
|
cursor?: undefined;
|
|
360
360
|
} | {
|
|
361
|
+
limit?: undefined;
|
|
361
362
|
event_id?: undefined;
|
|
362
363
|
series_id?: undefined;
|
|
363
364
|
page?: undefined;
|
|
@@ -375,11 +376,11 @@ export declare const meetingTools: ({
|
|
|
375
376
|
format?: undefined;
|
|
376
377
|
transcript_id?: undefined;
|
|
377
378
|
query?: undefined;
|
|
378
|
-
limit?: undefined;
|
|
379
379
|
scan_limit?: undefined;
|
|
380
380
|
max_read_bytes?: undefined;
|
|
381
381
|
cursor?: undefined;
|
|
382
382
|
} | {
|
|
383
|
+
limit?: undefined;
|
|
383
384
|
event_id?: undefined;
|
|
384
385
|
series_id?: undefined;
|
|
385
386
|
page?: undefined;
|
|
@@ -401,7 +402,6 @@ export declare const meetingTools: ({
|
|
|
401
402
|
minimum: number;
|
|
402
403
|
};
|
|
403
404
|
query?: undefined;
|
|
404
|
-
limit?: undefined;
|
|
405
405
|
scan_limit?: undefined;
|
|
406
406
|
max_read_bytes?: undefined;
|
|
407
407
|
cursor?: undefined;
|
|
@@ -488,6 +488,7 @@ export declare const meetingTools: ({
|
|
|
488
488
|
max_read_bytes?: undefined;
|
|
489
489
|
cursor?: undefined;
|
|
490
490
|
} | {
|
|
491
|
+
limit?: undefined;
|
|
491
492
|
event_id?: undefined;
|
|
492
493
|
series_id?: undefined;
|
|
493
494
|
page?: undefined;
|
|
@@ -503,11 +504,11 @@ export declare const meetingTools: ({
|
|
|
503
504
|
format?: undefined;
|
|
504
505
|
transcript_id?: undefined;
|
|
505
506
|
query?: undefined;
|
|
506
|
-
limit?: undefined;
|
|
507
507
|
scan_limit?: undefined;
|
|
508
508
|
max_read_bytes?: undefined;
|
|
509
509
|
cursor?: undefined;
|
|
510
510
|
} | {
|
|
511
|
+
limit?: undefined;
|
|
511
512
|
event_id?: undefined;
|
|
512
513
|
series_id?: undefined;
|
|
513
514
|
page?: undefined;
|
|
@@ -525,11 +526,11 @@ export declare const meetingTools: ({
|
|
|
525
526
|
format?: undefined;
|
|
526
527
|
transcript_id?: undefined;
|
|
527
528
|
query?: undefined;
|
|
528
|
-
limit?: undefined;
|
|
529
529
|
scan_limit?: undefined;
|
|
530
530
|
max_read_bytes?: undefined;
|
|
531
531
|
cursor?: undefined;
|
|
532
532
|
} | {
|
|
533
|
+
limit?: undefined;
|
|
533
534
|
event_id?: undefined;
|
|
534
535
|
series_id?: undefined;
|
|
535
536
|
page?: undefined;
|
|
@@ -551,7 +552,6 @@ export declare const meetingTools: ({
|
|
|
551
552
|
minimum: number;
|
|
552
553
|
};
|
|
553
554
|
query?: undefined;
|
|
554
|
-
limit?: undefined;
|
|
555
555
|
scan_limit?: undefined;
|
|
556
556
|
max_read_bytes?: undefined;
|
|
557
557
|
cursor?: undefined;
|
|
@@ -638,6 +638,7 @@ export declare const meetingTools: ({
|
|
|
638
638
|
max_read_bytes?: undefined;
|
|
639
639
|
cursor?: undefined;
|
|
640
640
|
} | {
|
|
641
|
+
limit?: undefined;
|
|
641
642
|
event_id?: undefined;
|
|
642
643
|
series_id?: undefined;
|
|
643
644
|
page?: undefined;
|
|
@@ -653,11 +654,11 @@ export declare const meetingTools: ({
|
|
|
653
654
|
format?: undefined;
|
|
654
655
|
transcript_id?: undefined;
|
|
655
656
|
query?: undefined;
|
|
656
|
-
limit?: undefined;
|
|
657
657
|
scan_limit?: undefined;
|
|
658
658
|
max_read_bytes?: undefined;
|
|
659
659
|
cursor?: undefined;
|
|
660
660
|
} | {
|
|
661
|
+
limit?: undefined;
|
|
661
662
|
event_id?: undefined;
|
|
662
663
|
series_id?: undefined;
|
|
663
664
|
page?: undefined;
|
|
@@ -675,11 +676,11 @@ export declare const meetingTools: ({
|
|
|
675
676
|
format?: undefined;
|
|
676
677
|
transcript_id?: undefined;
|
|
677
678
|
query?: undefined;
|
|
678
|
-
limit?: undefined;
|
|
679
679
|
scan_limit?: undefined;
|
|
680
680
|
max_read_bytes?: undefined;
|
|
681
681
|
cursor?: undefined;
|
|
682
682
|
} | {
|
|
683
|
+
limit?: undefined;
|
|
683
684
|
event_id?: undefined;
|
|
684
685
|
series_id?: undefined;
|
|
685
686
|
page?: undefined;
|
|
@@ -701,7 +702,6 @@ export declare const meetingTools: ({
|
|
|
701
702
|
minimum: number;
|
|
702
703
|
};
|
|
703
704
|
query?: undefined;
|
|
704
|
-
limit?: undefined;
|
|
705
705
|
scan_limit?: undefined;
|
|
706
706
|
max_read_bytes?: undefined;
|
|
707
707
|
cursor?: undefined;
|
package/dist/oauth/authorize.js
CHANGED
|
@@ -46,7 +46,7 @@ export const authorize = async (options) => {
|
|
|
46
46
|
else
|
|
47
47
|
fail(new CliError("authentication_error", `${provider.name} sign-in was declined`, { reason: "oauth.declined" }));
|
|
48
48
|
};
|
|
49
|
-
const
|
|
49
|
+
const handler = (request, response) => {
|
|
50
50
|
let url;
|
|
51
51
|
try {
|
|
52
52
|
url = new URL(request.url ?? "/", "http://127.0.0.1");
|
|
@@ -69,7 +69,11 @@ export const authorize = async (options) => {
|
|
|
69
69
|
}
|
|
70
70
|
response.end("Sign-in received; you can close this tab");
|
|
71
71
|
settle(url.searchParams);
|
|
72
|
-
}
|
|
72
|
+
};
|
|
73
|
+
const server = createServer(handler);
|
|
74
|
+
const localhost = provider.redirect.kind === "loopback" && provider.redirect.host === "localhost";
|
|
75
|
+
// `localhost` may resolve to ::1 in the browser; the IPv6 listener is best effort, as some hosts have no IPv6.
|
|
76
|
+
const server6 = localhost ? createServer(handler) : undefined;
|
|
73
77
|
const onAbort = () => fail(cancelled());
|
|
74
78
|
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
75
79
|
const timer = setTimeout(() => fail(new CliError("timeout", `${provider.name} sign-in timed out`, { reason: "oauth.timeout" })), options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
@@ -81,10 +85,19 @@ export const authorize = async (options) => {
|
|
|
81
85
|
const address = server.address();
|
|
82
86
|
if (!address || typeof address === "string")
|
|
83
87
|
throw new CliError("configuration_error", "could not start the local sign-in listener");
|
|
88
|
+
if (server6) {
|
|
89
|
+
server6.on("error", () => { });
|
|
90
|
+
await new Promise((resolve) => {
|
|
91
|
+
server6.once("error", () => resolve());
|
|
92
|
+
server6.listen(address.port, "::1", () => resolve());
|
|
93
|
+
});
|
|
94
|
+
}
|
|
84
95
|
const random = randomBytes(32).toString("base64url");
|
|
85
96
|
const relay = provider.redirect.kind === "relay";
|
|
86
97
|
state = relay ? `${address.port}.${random}` : random;
|
|
87
|
-
const redirectUri = provider.redirect.kind === "relay"
|
|
98
|
+
const redirectUri = provider.redirect.kind === "relay"
|
|
99
|
+
? provider.redirect.url
|
|
100
|
+
: `http://${localhost ? "localhost" : "127.0.0.1"}:${address.port}/callback`;
|
|
88
101
|
const url = new URL(provider.authorizeUrl);
|
|
89
102
|
for (const [key, value] of Object.entries({
|
|
90
103
|
...provider.authorizeParams,
|
|
@@ -124,7 +137,10 @@ export const authorize = async (options) => {
|
|
|
124
137
|
clearTimeout(timer);
|
|
125
138
|
options.signal?.removeEventListener("abort", onAbort);
|
|
126
139
|
paste.abort();
|
|
127
|
-
server
|
|
128
|
-
|
|
140
|
+
for (const each of server6 ? [server, server6] : [server]) {
|
|
141
|
+
each.closeAllConnections();
|
|
142
|
+
if (each.listening)
|
|
143
|
+
await new Promise((resolve) => each.close(() => resolve()));
|
|
144
|
+
}
|
|
129
145
|
}
|
|
130
146
|
};
|
package/dist/oauth/provider.d.ts
CHANGED
|
@@ -39,11 +39,13 @@ export interface OAuthProvider {
|
|
|
39
39
|
revokeUrl?: string;
|
|
40
40
|
client: OAuthClient;
|
|
41
41
|
/**
|
|
42
|
-
* `loopback` sends the browser to `http://127.0.0.1:<port>/callback
|
|
42
|
+
* `loopback` sends the browser to `http://127.0.0.1:<port>/callback`, or to `http://localhost:<port>/callback` with
|
|
43
|
+
* `host: "localhost"` (the listener then takes `127.0.0.1` and `::1`). `relay` sends it to a fixed page that
|
|
43
44
|
* forwards to the loopback port, which it reads from `state` (`<port>.<random>`).
|
|
44
45
|
*/
|
|
45
46
|
redirect: {
|
|
46
47
|
kind: "loopback";
|
|
48
|
+
host?: "localhost";
|
|
47
49
|
} | {
|
|
48
50
|
kind: "relay";
|
|
49
51
|
url: string;
|
|
@@ -348,7 +348,7 @@ export interface PassiveUpdates {
|
|
|
348
348
|
* has it saved by a command that never feeds. Optional within the group, so an adapter typed as having
|
|
349
349
|
* `PassiveUpdates` before it existed still is one.
|
|
350
350
|
*/
|
|
351
|
-
listenHistory?(onBatch:
|
|
351
|
+
listenHistory?(onBatch: HistoryBatchHandler, signal: AbortSignal): Promise<void>;
|
|
352
352
|
}
|
|
353
353
|
/** One voter's vote in the poll message `messageId` of `chatId` carries, as `PollVoteEvent` has it. */
|
|
354
354
|
export interface HistoryVote {
|
|
@@ -398,10 +398,16 @@ export interface HistoryBatch {
|
|
|
398
398
|
}[];
|
|
399
399
|
events?: HistoryChatEvent[];
|
|
400
400
|
}
|
|
401
|
+
/**
|
|
402
|
+
* Takes one pushed batch. The CLI's own handler resolves `true` once the batch is in the local store and `false` when it
|
|
403
|
+
* could not be written, so a messenger that pushes history once can keep a batch until it is saved; a handler that
|
|
404
|
+
* stores nothing returns nothing.
|
|
405
|
+
*/
|
|
406
|
+
export type HistoryBatchHandler = (batch: HistoryBatch) => void | Promise<boolean>;
|
|
401
407
|
/** For a messenger that pushes its history to the client instead of answering for it (`Messenger.history: "store"`). */
|
|
402
408
|
export interface PushedHistory {
|
|
403
409
|
/** Chats, people and past messages as the messenger pushes them, until `signal` aborts. */
|
|
404
|
-
feed(onBatch:
|
|
410
|
+
feed(onBatch: HistoryBatchHandler, signal: AbortSignal): Promise<void>;
|
|
405
411
|
/**
|
|
406
412
|
* Every chat id the store holds for this account, handed over once as `feed` starts, so a chat nothing names again
|
|
407
413
|
* can still be re-keyed through a later batch's `rekeys`. `recipients`, when given, are ids the profile's recipient
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wirecat/cli-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.543.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",
|
|
@@ -251,7 +251,7 @@
|
|
|
251
251
|
"@types/node": "^22.20.5",
|
|
252
252
|
"@types/pngjs": "6.0.5",
|
|
253
253
|
"@vitest/coverage-v8": "^5.0.5",
|
|
254
|
-
"@wirecat/cli-base": "0.
|
|
254
|
+
"@wirecat/cli-base": "0.73.0",
|
|
255
255
|
"@wirecat/cli-core-legacy-tags": "npm:@wirecat/cli-core@0.414.0",
|
|
256
256
|
"commander": "15.0.0",
|
|
257
257
|
"cspell": "10.4.0",
|
|
@@ -271,7 +271,7 @@
|
|
|
271
271
|
},
|
|
272
272
|
"peerDependencies": {
|
|
273
273
|
"@napi-rs/canvas": ">=0.1.69 <2",
|
|
274
|
-
"@wirecat/cli-base": ">=0.
|
|
274
|
+
"@wirecat/cli-base": ">=0.73.0 <0.74.0",
|
|
275
275
|
"commander": ">=15"
|
|
276
276
|
},
|
|
277
277
|
"bin": {
|