@wirecat/cli-core 0.544.0 → 0.546.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 +88 -0
- package/dist/calendar/busy.js +4 -1
- package/dist/calendar/cli/answers.d.ts +31 -0
- package/dist/calendar/cli/answers.js +8 -0
- package/dist/calendar/cli/index.d.ts +6 -0
- package/dist/calendar/cli/index.js +43 -4
- package/dist/calendar/cli/remind.js +2 -1
- package/dist/calendar/expand.d.ts +17 -0
- package/dist/calendar/expand.js +340 -0
- package/dist/calendar/feeds.d.ts +96 -0
- package/dist/calendar/feeds.js +324 -0
- package/dist/calendar/ics.d.ts +15 -4
- package/dist/calendar/ics.js +18 -7
- package/dist/calendar/index.d.ts +3 -1
- package/dist/calendar/index.js +2 -0
- package/dist/calendar/server-search.js +3 -1
- package/dist/calendar/writes.js +15 -4
- package/dist/cli/after-dashes.d.ts +16 -0
- package/dist/cli/after-dashes.js +41 -0
- package/dist/cli/bot/context.d.ts +1 -1
- package/dist/cli/bot/context.js +9 -1
- package/dist/cli/bot/messages.js +2 -0
- package/dist/cli/bot/setup.js +1 -0
- package/dist/cli/contract-templates.js +2 -2
- package/dist/cli/entry.d.ts +2 -0
- package/dist/cli/entry.js +3 -2
- package/dist/cli/index.d.ts +2 -1
- package/dist/cli/index.js +2 -1
- package/dist/cli/messenger/admin-commands.js +5 -1
- package/dist/cli/messenger/admin-contacts-command.js +5 -1
- package/dist/cli/messenger/admin-folders-command.js +5 -1
- package/dist/cli/messenger/archive-commands.js +20 -2
- package/dist/cli/messenger/backfill-command.js +13 -2
- package/dist/cli/messenger/complete-command.js +1 -0
- package/dist/cli/messenger/context.js +3 -0
- package/dist/cli/messenger/history-gaps.d.ts +12 -0
- package/dist/cli/messenger/history-gaps.js +41 -0
- package/dist/cli/messenger/messages-command.js +9 -22
- package/dist/cli/messenger/messages-edit-command.js +1 -0
- package/dist/cli/messenger/messages-press-command.js +1 -0
- package/dist/cli/messenger/messages-search-command.js +5 -1
- package/dist/cli/messenger/messages-send-command.js +1 -0
- package/dist/cli/messenger/observed.js +2 -2
- package/dist/cli/messenger/polls-command.js +1 -0
- package/dist/cli/messenger/private-people-command.js +5 -1
- package/dist/cli/messenger/search-answers.d.ts +3 -2
- package/dist/cli/messenger/search-answers.js +12 -5
- package/dist/cli/messenger/search-command.js +3 -1
- package/dist/cli/messenger/stored.d.ts +6 -1
- package/dist/cli/messenger/stored.js +94 -5
- package/dist/cli/messenger/topics-command.js +1 -1
- package/dist/cli/program.js +17 -6
- package/dist/cli/schedule/schedule-command.js +1 -0
- package/dist/cli/settings.d.ts +9 -0
- package/dist/cli/settings.js +20 -0
- package/dist/cli/shared-command-exports.d.ts +24 -0
- package/dist/cli/steps.js +1 -0
- package/dist/domain/message-link.d.ts +3 -0
- package/dist/domain/message-link.js +9 -14
- package/dist/domain/money-words.d.ts +11 -0
- package/dist/domain/money-words.js +38 -0
- package/dist/domain/providers.d.ts +2 -0
- package/dist/domain/providers.js +15 -3
- package/dist/domain/ref-arguments.d.ts +31 -0
- package/dist/domain/ref-arguments.js +79 -0
- package/dist/domain/references.d.ts +20 -2
- package/dist/domain/references.js +132 -47
- package/dist/entities/index.d.ts +1 -0
- package/dist/entities/message-transfer.d.ts +4 -0
- package/dist/entities/message-transfer.js +2 -0
- package/dist/errors/reasons.js +18 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/meetings/mcp/index.d.ts +15 -15
- package/dist/oauth/client-file.d.ts +14 -0
- package/dist/oauth/client-file.js +28 -0
- package/dist/oauth/index.d.ts +1 -0
- package/dist/oauth/index.js +1 -0
- package/dist/ports/messenger.d.ts +3 -2
- package/dist/resolve.d.ts +3 -1
- package/dist/resolve.js +8 -0
- package/dist/search/task-signature.d.ts +5 -1
- package/dist/search/task-signature.js +27 -9
- package/dist/services/agenda.d.ts +5 -0
- package/dist/services/agenda.js +3 -1
- package/dist/services/history-limits.d.ts +45 -0
- package/dist/services/history-limits.js +50 -0
- package/dist/services/index.d.ts +5 -1
- package/dist/services/index.js +5 -1
- package/dist/services/message-resolution.d.ts +4 -1
- package/dist/services/message-resolution.js +19 -3
- package/dist/services/messages.js +15 -13
- package/dist/services/money-words.d.ts +3 -0
- package/dist/services/money-words.js +5 -0
- package/dist/services/questions.d.ts +7 -2
- package/dist/services/questions.js +13 -4
- package/dist/services/stored-project-gathering.d.ts +9 -0
- package/dist/services/stored-project-gathering.js +9 -4
- package/dist/services/task-categories.d.ts +2 -0
- package/dist/services/task-categories.js +19 -8
- package/dist/store/chats.d.ts +10 -0
- package/dist/store/chats.js +25 -0
- package/dist/store/sqlite/chats.js +2 -1
- package/dist/store/sqlite/evidence-sources.js +2 -2
- package/dist/store/sqlite/history-skips.d.ts +24 -0
- package/dist/store/sqlite/history-skips.js +95 -0
- package/dist/store/sqlite/identities.js +1 -1
- package/dist/store/sqlite/knowledge.js +7 -2
- package/dist/store/sqlite/notes.js +4 -4
- package/dist/store/sqlite/things.d.ts +0 -1
- package/dist/store/sqlite/things.js +25 -24
- package/dist/store/store.d.ts +1 -0
- package/package.json +1 -1
- package/parity.json +4 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,94 @@ shape of this file.
|
|
|
7
7
|
|
|
8
8
|
## Unreleased
|
|
9
9
|
|
|
10
|
+
## 0.546.0 — 02.11.2026
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **`readClientFile` (from `@wirecat/cli-core/oauth`) reads a user's own Google Desktop OAuth client file** (the
|
|
15
|
+
`{ "installed": { client_id, client_secret } }` JSON from Google's console). It refuses a file other users can read
|
|
16
|
+
(`sign-in.client-file-open`) and a file of the wrong shape (`sign-in.client-file-invalid`), and never puts the file's
|
|
17
|
+
contents in an error. gcal, drive and mail each kept a copy of this reader.
|
|
18
|
+
- **`openBrowser(url, browser?)` is exported from `@wirecat/cli-core/cli`**, so a CLI that does not start through
|
|
19
|
+
`startCli` opens the sign-in page the same way: `open`, `rundll32` or `xdg-open`, or the named browser.
|
|
20
|
+
- **iCalendar feeds as read-only calendars.** With `deps.feeds` (`icsFeeds`), `calendars add <url> [--title]` follows a
|
|
21
|
+
public holiday calendar, a sports schedule or a published `.ics` address, and `calendars remove <calendar>` stops.
|
|
22
|
+
`sync` reads each feed after the provider (`syncWithFeeds`), so its events show in `events list`,
|
|
23
|
+
`search events`, `overlapping:` and `events remind`. Repeating events are expanded here (`expandSeries`: RRULE in
|
|
24
|
+
the event's own zone, RDATE, EXDATE and overrides). The address is a secret input, kept by the CLI and never
|
|
25
|
+
printed; a fetch follows redirects only to https and stops at 10 MB and 30 s. `readIcs` takes
|
|
26
|
+
`{ overrides: true }` and returns the calendar's `name`. See `docs/dev/CALENDARS.md`, "Feeds".
|
|
27
|
+
- **History limits for pushed history, per profile.** A messenger whose history is pushed (`history: "store"`,
|
|
28
|
+
WhatsApp) saves one-to-one chats back `history_dialog_days` (1095 by default), groups of more than two people back
|
|
29
|
+
`history_group_days` (365) and nothing of a group above `history_group_members_max` people (50); at most 1095 days.
|
|
30
|
+
The chat row stays, so the chat is still listed. Live messages, `messages sync` and what the store already holds are
|
|
31
|
+
never dropped. `messages sync <chat> --full-history` fetches a chat and keeps its pushed history in full from then on
|
|
32
|
+
(`history_included` in the answer). `ChatsStore` gains `historySkips`, `historyShapes`, `recordHistorySkips` and
|
|
33
|
+
`setHistoryIncluded`; `Saving` takes `historyLimits`.
|
|
34
|
+
- **Helpers for reading money and questions in a chat, for the project status work.** `amountsOf(text)` returns
|
|
35
|
+
every amount in a text, in order (`amountOf` is its first). `MONEY_REQUEST`, `MONEY_DONE` and `PROMISE` word lists
|
|
36
|
+
(ru, en, es) with `moneyClassesOf(text)` say which a text uses. `questions(messages, { asker: "owner" })` lists the
|
|
37
|
+
owner's own questions and whether anyone but the owner answered them, by a reply or by speaking next; the default,
|
|
38
|
+
`"others"`, is what it did before. `linkedChatsOf` is exported, now with each link's `role`, and so is
|
|
39
|
+
`AGENDA_DEFAULTS`, the silence `tasks agenda` waits before it lists a chat.
|
|
40
|
+
- **A command's own history handler learns whether each batch reached the store**: `HistoryBatchHandler` takes
|
|
41
|
+
`saved`, a promise of the same `true` or `false` the messenger gets, so a link can count only what was kept.
|
|
42
|
+
- **Every messenger command that takes a chat or a message also takes the reference other WireCat CLIs print**:
|
|
43
|
+
`chat:<provider>/<account>/<chat>` wherever a chat is named (`messages list chat:max/34871122/63488582`, bot
|
|
44
|
+
commands too), and `msg:<provider>/<account>/<chat>/<message>` wherever a chat or a message is, online and offline. A person lookup takes `contact:<provider>/<id>`. A reference of another
|
|
45
|
+
account is refused, naming the account it belongs to and the profiles of this CLI the store saw on it; it never
|
|
46
|
+
switches account. One of another messenger is refused with the command that opens it (`open it with tg <profile>
|
|
47
|
+
messages list chat:telegram/…`), and another kind (`document:413` given as a chat) with what the argument takes.
|
|
48
|
+
- **`event:<id>` and `meeting:<account>/<meeting>[/<transcript>[/<cue>]]` parse with `parseReference`** and
|
|
49
|
+
`canonicalReference`, as every other kind: `event:N` was printed by the calendar and person answers and could not be
|
|
50
|
+
read back. The kinds are one table, `REF_KINDS`; `looksLikeReference(text)` tells a reference from a title such as
|
|
51
|
+
`Note: plan`. Reference errors carry a reason: `ref.malformed`, `ref.unknown-kind` (with the kind meant:
|
|
52
|
+
`"doc:413" … did you mean document:413?`), `ref.wrong-kind`, `ref.wrong-provider`, `ref.wrong-account`.
|
|
53
|
+
|
|
54
|
+
### Changed — may break callers
|
|
55
|
+
|
|
56
|
+
- **An option after `--` is read as the option.** `messages sync -- -100 --json` now syncs `-100` and answers in JSON;
|
|
57
|
+
it failed with "too many arguments". After `--`, a word that is exactly one of the command's options, its own or a
|
|
58
|
+
global one (`--json`, `--limit 5`, `--json=id`), moves in front of `--`; every other word stays a positional. A
|
|
59
|
+
negative id needs no `--` at all. Commands that take free text keep every word after `--` literal: `messages send` and
|
|
60
|
+
`edit` (also the bot's), `polls create`, `topics create`, `chats create`, `chats folders create`, `contacts rename`,
|
|
61
|
+
`contacts aliases set`, `messages press`, `bot commands set`, `schedule command`, `events remind`, `complete`, and the
|
|
62
|
+
notes templates. Mark such a command with `annotate(command, { literal_after_dashes: true })`; MCP calls are never
|
|
63
|
+
lifted. A script that passed the literal word `--json` as a positional of another command now gets the option instead.
|
|
64
|
+
- **A write to a calendar the account may only read refuses before anything is asked or journaled**
|
|
65
|
+
(`calendar.read-only`): create, quick add, import, update, delete, respond and move, for a feed and for a provider's
|
|
66
|
+
`reader` or `free-busy` calendar alike. Before, only the target of a move was checked; the provider refused the rest.
|
|
67
|
+
`calendars busy` and `search --backend server` leave feed calendars out.
|
|
68
|
+
- **A locator of another account or messenger is refused with a new text and a reason.** `messages show`, `context`,
|
|
69
|
+
`link` and `download` said "that locator belongs to another account; select its profile first" or "…belongs to
|
|
70
|
+
another messenger"; they now say `<ref> belongs to account <id>; profile <name> is account <id>` (`ref.wrong-account`)
|
|
71
|
+
or `<ref> is a <messenger> message; open it with …` (`ref.wrong-provider`). A script matching the old text matches
|
|
72
|
+
the reason instead.
|
|
73
|
+
- **A text of the form `<kind>:<no spaces>` given as a chat is read as a reference**, so a chat titled `note:plan` is
|
|
74
|
+
no longer found by that title; give its id or a part of the title.
|
|
75
|
+
- The provider seeds of `telegram`, `max` and `whatsapp` name their CLI (`command: tg`, `max`, `wa`), which a
|
|
76
|
+
provider clash message now shows (`telegram (messages, from tg)`).
|
|
77
|
+
|
|
78
|
+
### Fixed
|
|
79
|
+
|
|
80
|
+
- **A sum written in groups of three read as the wrong amount: `50 000 ₽` was `0` and `4 300 ₽` was `300`**, because
|
|
81
|
+
only the last group was read. Groups after a space, a no-break or thin space, a dot or a comma (`50 000`,
|
|
82
|
+
`1.200 €`, `$1,200`) are one number now, a decimal part after them is kept (`5 000.50 ₽`), and amounts keep two
|
|
83
|
+
decimals instead of a whole number (`15.5 eur` was 16). This is the amount the duplicate check compares between
|
|
84
|
+
two tasks, so a reminder worded with a grouped sum is no longer compared on the wrong figure.
|
|
85
|
+
|
|
86
|
+
## 0.545.0 — 02.11.2026
|
|
87
|
+
|
|
88
|
+
### Added
|
|
89
|
+
|
|
90
|
+
- **History limits for pushed history, per profile.** A messenger whose history is pushed (`history: "store"`,
|
|
91
|
+
WhatsApp) saves one-to-one chats back `history_dialog_days` (1095 by default), groups of more than two people back
|
|
92
|
+
`history_group_days` (365) and nothing of a group above `history_group_members_max` people (50); at most 1095 days.
|
|
93
|
+
The chat row stays, so the chat is still listed. Live messages, `messages sync` and what the store already holds are
|
|
94
|
+
never dropped. `messages sync <chat> --full-history` fetches a chat and keeps its pushed history in full from then on
|
|
95
|
+
(`history_included` in the answer). `ChatsStore` gains `historySkips`, `historyShapes`, `recordHistorySkips` and
|
|
96
|
+
`setHistoryIncluded`; `Saving` takes `historyLimits`.
|
|
97
|
+
|
|
10
98
|
## 0.544.0 — 02.11.2026
|
|
11
99
|
|
|
12
100
|
### Added
|
package/dist/calendar/busy.js
CHANGED
|
@@ -2,6 +2,7 @@ import { CliError } from "@wirecat/cli-base";
|
|
|
2
2
|
import { dayBoundary, queryError, queryFieldsOf, zonedMoment, } from "@wirecat/cli-base/query";
|
|
3
3
|
import { LIST_QUERY_GUIDE, listQuery, nativeTermLists, typedQuery } from "../cli/list-query.js";
|
|
4
4
|
import { calendarBusy } from "./cli/answers.js";
|
|
5
|
+
import { isFeedCalendar } from "./feeds.js";
|
|
5
6
|
import { calendarIn, plusDays, timeWindow } from "./reads.js";
|
|
6
7
|
/** Google answers for 50 calendars at most (`calendarExpansionMax`, reference/freebusy/query). */
|
|
7
8
|
const MOST = 50;
|
|
@@ -60,6 +61,8 @@ export const busyQuery = async (query, context) => {
|
|
|
60
61
|
to = timeWindow(node, zone, now).upper;
|
|
61
62
|
else if (field === "calendar") {
|
|
62
63
|
const calendar = await calendarIn(context.store, context.accountId, node.value);
|
|
64
|
+
if (isFeedCalendar(calendar))
|
|
65
|
+
throw new CliError("validation_error", `calendar ${calendar.id} is a feed, which the provider has no busy times for; read it with events list`, { reason: "busy.feed" });
|
|
63
66
|
if (!calendars.some(({ id }) => id === calendar.id))
|
|
64
67
|
calendars.push(calendar);
|
|
65
68
|
}
|
|
@@ -76,7 +79,7 @@ export const busyQuery = async (query, context) => {
|
|
|
76
79
|
throw new CliError("validation_error", "the window ends before it starts");
|
|
77
80
|
const asked = calendars.length
|
|
78
81
|
? calendars
|
|
79
|
-
: (await context.store.calendars(context.accountId)).filter((
|
|
82
|
+
: (await context.store.calendars(context.accountId)).filter((calendar) => calendar.synced && !calendar.deletedAt && !isFeedCalendar(calendar));
|
|
80
83
|
if (asked.length === 0 && people.size === 0)
|
|
81
84
|
throw new CliError("validation_error", "no calendar syncs yet; name one with calendar:<id>, or a person:<email>", {
|
|
82
85
|
reason: "busy.no-calendars",
|
|
@@ -11,6 +11,31 @@ export declare const calendar: v.StrictObjectSchema<{
|
|
|
11
11
|
readonly synced: v.BooleanSchema<undefined>;
|
|
12
12
|
readonly seen_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>;
|
|
13
13
|
}, undefined>;
|
|
14
|
+
/** `calendars add`: the feed's calendar, how many events the feed holds now, and the sync to run next. */
|
|
15
|
+
export declare const calendarFeed: v.StrictObjectSchema<{
|
|
16
|
+
readonly id: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>;
|
|
17
|
+
readonly external_id: v.StringSchema<undefined>;
|
|
18
|
+
readonly title: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
19
|
+
readonly timezone: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
20
|
+
readonly access_role: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
21
|
+
readonly primary: v.BooleanSchema<undefined>;
|
|
22
|
+
readonly synced: v.BooleanSchema<undefined>;
|
|
23
|
+
readonly seen_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>;
|
|
24
|
+
readonly events: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
|
|
25
|
+
readonly hint: v.StringSchema<undefined>;
|
|
26
|
+
}, undefined>;
|
|
27
|
+
/** `calendars remove`: the feed's calendar, no longer synced, and how many stored entries were marked deleted. */
|
|
28
|
+
export declare const calendarFeedRemoval: v.StrictObjectSchema<{
|
|
29
|
+
readonly id: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>;
|
|
30
|
+
readonly external_id: v.StringSchema<undefined>;
|
|
31
|
+
readonly title: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
32
|
+
readonly timezone: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
33
|
+
readonly access_role: v.NullableSchema<v.StringSchema<undefined>, undefined>;
|
|
34
|
+
readonly primary: v.BooleanSchema<undefined>;
|
|
35
|
+
readonly synced: v.BooleanSchema<undefined>;
|
|
36
|
+
readonly seen_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>;
|
|
37
|
+
readonly removed: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
|
|
38
|
+
}, undefined>;
|
|
14
39
|
export declare const calendarBusy: v.StrictObjectSchema<{
|
|
15
40
|
readonly start: 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`">]>;
|
|
16
41
|
readonly end: 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`">]>;
|
|
@@ -75,6 +100,12 @@ export declare const calendarSync: v.StrictObjectSchema<{
|
|
|
75
100
|
readonly embedded: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
|
|
76
101
|
readonly left: v.BooleanSchema<undefined>;
|
|
77
102
|
}, undefined>, undefined>;
|
|
103
|
+
/** Each feed's events as read, and how many it could not take (`syncWithFeeds`). */
|
|
104
|
+
readonly feeds: v.OptionalSchema<v.ArraySchema<v.StrictObjectSchema<{
|
|
105
|
+
readonly calendar_id: v.NumberSchema<undefined>;
|
|
106
|
+
readonly events: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
|
|
107
|
+
readonly skipped: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
|
|
108
|
+
}, undefined>, undefined>, undefined>;
|
|
78
109
|
}, undefined>;
|
|
79
110
|
export declare const calendarSyncProgress: v.StrictObjectSchema<{
|
|
80
111
|
readonly calendar_id: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.MinLengthAction<string, 1, undefined>]>;
|
|
@@ -19,6 +19,10 @@ export const calendar = v.strictObject({
|
|
|
19
19
|
synced: v.boolean(),
|
|
20
20
|
seen_at: nullableTime,
|
|
21
21
|
});
|
|
22
|
+
/** `calendars add`: the feed's calendar, how many events the feed holds now, and the sync to run next. */
|
|
23
|
+
export const calendarFeed = v.strictObject({ ...calendar.entries, events: count, hint: text });
|
|
24
|
+
/** `calendars remove`: the feed's calendar, no longer synced, and how many stored entries were marked deleted. */
|
|
25
|
+
export const calendarFeedRemoval = v.strictObject({ ...calendar.entries, removed: count });
|
|
22
26
|
const busyRow = v.strictObject({
|
|
23
27
|
/** One of the account's calendars, by its id; or `person`, by the address asked. */
|
|
24
28
|
calendar: v.optional(id),
|
|
@@ -68,6 +72,8 @@ export const calendarSync = v.strictObject({
|
|
|
68
72
|
hint: v.optional(text),
|
|
69
73
|
/** What `sync --embed` embedded: `embedEvents`' answer. */
|
|
70
74
|
embeddings: v.optional(v.strictObject({ model: text, embedded: count, left: v.boolean() })),
|
|
75
|
+
/** Each feed's events as read, and how many it could not take (`syncWithFeeds`). */
|
|
76
|
+
feeds: v.optional(v.array(v.strictObject({ calendar_id: v.number(), events: count, skipped: count }))),
|
|
71
77
|
});
|
|
72
78
|
export const calendarSyncProgress = v.strictObject({
|
|
73
79
|
calendar_id: id,
|
|
@@ -237,6 +243,8 @@ const ANSWERS = {
|
|
|
237
243
|
"calendars list": { name: "calendar", schema: calendar },
|
|
238
244
|
"calendars sync": { name: "calendar", schema: calendar },
|
|
239
245
|
"calendars unsync": { name: "calendar", schema: calendar },
|
|
246
|
+
"calendars add": { name: "calendar_feed", schema: calendarFeed },
|
|
247
|
+
"calendars remove": { name: "calendar_feed_removal", schema: calendarFeedRemoval },
|
|
240
248
|
"calendars busy": { name: "calendar_busy", schema: calendarBusy },
|
|
241
249
|
sync: { name: "calendar_sync", schema: calendarSync },
|
|
242
250
|
"store status": { name: "calendar_sync_progress", schema: calendarSyncProgress },
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type Command } from "commander";
|
|
2
2
|
import type { EventStore } from "../../meetings/index.js";
|
|
3
3
|
import type { AccountKey, MessageStore } from "../../store/store.js";
|
|
4
|
+
import { type CalendarFeeds } from "../feeds.js";
|
|
4
5
|
import type { CalendarExpander, CalendarSource } from "../source.js";
|
|
5
6
|
import { type CalendarWrites } from "../writes.js";
|
|
6
7
|
export interface CalendarCommandDeps {
|
|
@@ -27,6 +28,11 @@ export interface CalendarCommandDeps {
|
|
|
27
28
|
serverSearch?: boolean;
|
|
28
29
|
/** For a source with `expands: "local"`. */
|
|
29
30
|
expand?: CalendarExpander;
|
|
31
|
+
/**
|
|
32
|
+
* The iCalendar feeds the profile follows, as read-only calendars of its account: mounts `calendars add|remove`,
|
|
33
|
+
* and `sync` and `calendars list` read them after the provider. Read only once a profile is open.
|
|
34
|
+
*/
|
|
35
|
+
feeds?: CalendarFeeds;
|
|
30
36
|
/** After `sync --embed`: embeds new events for search by meaning (`embedEvents`), as drive's `sync --embed`. */
|
|
31
37
|
embed?: (max: number) => Promise<{
|
|
32
38
|
model: string;
|
|
@@ -5,19 +5,21 @@ import { clockMeta } from "@wirecat/cli-base/standards";
|
|
|
5
5
|
import { Argument, InvalidArgumentError, Option } from "commander";
|
|
6
6
|
import { environmentOf, runBudgetOf } from "../../cli/context.js";
|
|
7
7
|
import { honoursDryRun } from "../../cli/dry-run-guard.js";
|
|
8
|
-
import { ownerOnlyOption, writesFileOnlyWith } from "../../cli/effects.js";
|
|
8
|
+
import { changesStore, ownerOnlyOption, writesFileOnlyWith } from "../../cli/effects.js";
|
|
9
9
|
import { queryTerms, withQuery } from "../../cli/list-query.js";
|
|
10
10
|
import { terminalAsker } from "../../cli/messenger/ask.js";
|
|
11
11
|
import { lineOf } from "../../cli/steps.js";
|
|
12
|
+
import { secretInput } from "../../runs/run-context.js";
|
|
12
13
|
import { searchEvents } from "../../services/events-search.js";
|
|
13
14
|
import { dateWindow } from "../../services/moment.js";
|
|
14
15
|
import { linkPeople } from "../../services/people.js";
|
|
15
16
|
import { BUSY_QUERY, BUSY_QUERY_NOTE, busyAnswer, busyQuery, workingHoursOf } from "../busy.js";
|
|
17
|
+
import { isFeedCalendar, syncWithFeeds } from "../feeds.js";
|
|
16
18
|
import { eventPreparation, nextMeeting, PREPARE_MESSAGES } from "../prepare.js";
|
|
17
19
|
import { accountsMeta, CALENDAR_UNTRUSTED_FIELDS, calendarIn, calendarItem, EVENTS_QUERY, EVENTS_QUERY_NOTE, EXPORT_MAX, eventAnswer, eventIdOf, eventOf, eventsAnswer, eventsFilter, eventsIcs, peopleAnswer, resolveAnswer, reviewsAnswer, sourceAccounts, weekFilter, } from "../reads.js";
|
|
18
20
|
import { REPEAT_HELP } from "../recurrence.js";
|
|
19
21
|
import { bothSearchAnswer, serverSearchAnswer } from "../server-search.js";
|
|
20
|
-
import { calendarStatus
|
|
22
|
+
import { calendarStatus } from "../sync.js";
|
|
21
23
|
import { createEvent, createEventFromText, deleteEvent, EVENT_RESPONSES, IMPORT_MAX, importEvents, moveEvent, respondToEvent, updateEvent, } from "../writes.js";
|
|
22
24
|
import { CALENDARS_QUERY, declareCalendarAnswers, PEOPLE_QUERY } from "./answers.js";
|
|
23
25
|
import { addRemind } from "./remind.js";
|
|
@@ -28,6 +30,8 @@ const REMOTE_READ = { remote: "read", local: "none" };
|
|
|
28
30
|
const LOCAL_OR_REMOTE_READ = { remote: "read", local: "read" };
|
|
29
31
|
const REMOTE_WRITE = { remote: "write", local: "write" };
|
|
30
32
|
const GROUP = { remote: "none", local: "none" };
|
|
33
|
+
/** Every time a store can hold, for marking all of a calendar's entries at once. */
|
|
34
|
+
const EVERYWHERE = { from: -8.64e15, to: 8.64e15 };
|
|
31
35
|
/** How many pieces one `sync --embed` embeds, as drive's. */
|
|
32
36
|
const EMBED_MAX = 600;
|
|
33
37
|
// A list's marker goes on each item: a host's list envelope is strict and `--jsonl` prints only the items.
|
|
@@ -95,7 +99,8 @@ const addCalendarCommandsDefinition = (program, deps) => {
|
|
|
95
99
|
const calendars = annotate(program.command("calendars").description("the account's calendars"), { effects: GROUP });
|
|
96
100
|
withQuery(example(annotate(calendars.command("list"), { effects: FETCH, offline: false }).description("list the account's calendars from the provider, and which of them sync"), "calendars list --json", "calendars list synced:true"), CALENDARS_QUERY).action(async (words, _opts, command) => {
|
|
97
101
|
const terms = [...queryTerms(words, CALENDARS_QUERY, "calendars list")];
|
|
98
|
-
const
|
|
102
|
+
const found = [...(await source().calendars()), ...((await deps.feeds?.source.calendars()) ?? [])];
|
|
103
|
+
const listed = await deps.store.saveCalendars(deps.accountId, found, now());
|
|
99
104
|
const items = listed
|
|
100
105
|
.map(calendarItem)
|
|
101
106
|
.filter((item) => terms.every(([field, value]) => String(item[field]) === value));
|
|
@@ -112,6 +117,40 @@ const addCalendarCommandsDefinition = (program, deps) => {
|
|
|
112
117
|
await deps.store.setCalendarSynced(calendar.id, synced, now());
|
|
113
118
|
print(calendarItem({ ...calendar, synced }), command);
|
|
114
119
|
});
|
|
120
|
+
if (deps.feeds) {
|
|
121
|
+
const feeds = () => deps.feeds;
|
|
122
|
+
example(
|
|
123
|
+
// A fetch, but also a new secret and a new address every later sync reads: the read-only lock refuses it.
|
|
124
|
+
changesStore(annotate(calendars.command("add"), { effects: FETCH, offline: false, mcp: false }))
|
|
125
|
+
.description("follow an iCalendar feed, such as a holiday calendar or a published .ics address, as a read-only calendar " +
|
|
126
|
+
"of this profile; sync reads it again each time. The address is kept as a secret and never printed")
|
|
127
|
+
.summary("follow an iCalendar feed as a read-only calendar")
|
|
128
|
+
.addArgument(secretInput(new Argument("<url>", "the feed's https://, http:// or webcal:// address")))
|
|
129
|
+
.option("--title <title>", "the calendar's title; the feed's own name if not given"), "calendars add webcal://example.com/holidays.ics --title Holidays").action(async (url, opts, command) => {
|
|
130
|
+
if (command.optsWithGlobals().offline)
|
|
131
|
+
throw new CliError("permission_error", "calendars add reads the feed; leave out --offline");
|
|
132
|
+
const { calendar, events } = await feeds().add(url, opts.title === undefined ? {} : { title: opts.title });
|
|
133
|
+
const [saved] = await deps.store.saveCalendars(deps.accountId, [calendar], now());
|
|
134
|
+
print({
|
|
135
|
+
...calendarItem(saved),
|
|
136
|
+
events,
|
|
137
|
+
hint: `run ${lineOf(command, "sync")} to store its events`,
|
|
138
|
+
}, command);
|
|
139
|
+
});
|
|
140
|
+
example(annotate(calendars.command("remove"), { effects: LOCAL_WRITE, mcp: false })
|
|
141
|
+
.description("stop following a feed calendars add added: its address is forgotten and its stored events go")
|
|
142
|
+
.argument("<calendar>", "the feed calendar's id, its ics: id, or its title"), "calendars remove 7").action(async (reference, _opts, command) => {
|
|
143
|
+
const calendar = await calendarIn(deps.store, deps.accountId, reference);
|
|
144
|
+
if (!isFeedCalendar(calendar))
|
|
145
|
+
throw new CliError("validation_error", `calendar ${calendar.id} is not a feed; calendars remove takes only feeds`, {
|
|
146
|
+
reason: "feed.not-a-feed",
|
|
147
|
+
});
|
|
148
|
+
await feeds().remove(calendar.externalId);
|
|
149
|
+
await deps.store.setCalendarSynced(calendar.id, false, now());
|
|
150
|
+
const removed = await deps.store.markUnseen(calendar.id, now() + 1, EVERYWHERE, now());
|
|
151
|
+
print({ ...calendarItem({ ...calendar, synced: false }), removed }, command);
|
|
152
|
+
});
|
|
153
|
+
}
|
|
115
154
|
if (deps.freeBusy)
|
|
116
155
|
example(withQuery(annotate(calendars.command("busy"), { effects: REMOTE_READ, offline: false }).description("when calendars and people are taken, asked of the provider and not stored; with --duration-minutes, " +
|
|
117
156
|
"the free stretches they share"), BUSY_QUERY).addHelpText("after", BUSY_QUERY_NOTE), "calendars busy person:ana@example.com --json", "calendars busy start>=2026-11-02 end<=2026-11-06 person:ana@example.com --duration-minutes 30 " +
|
|
@@ -146,7 +185,7 @@ const addCalendarCommandsDefinition = (program, deps) => {
|
|
|
146
185
|
if (command.optsWithGlobals().full)
|
|
147
186
|
throw new CliError("validation_error", "sync starts over with --rescan; --full only widens output");
|
|
148
187
|
const { signal } = environmentOf(command);
|
|
149
|
-
const report = await
|
|
188
|
+
const report = await syncWithFeeds(source(), deps.feeds, deps.store, {
|
|
150
189
|
accountId: deps.accountId,
|
|
151
190
|
now: now(),
|
|
152
191
|
budget: runBudgetOf(command, opts.maxPages),
|
|
@@ -14,7 +14,7 @@ import { CALENDAR_WRITE_DEFAULTS, EVENTS_REMIND_HOOK_KEY } from "../writer.js";
|
|
|
14
14
|
const REMIND_MIN_MS = 60_000;
|
|
15
15
|
const REMIND_MAX_MS = 86_400_000;
|
|
16
16
|
export const addRemind = (events, writes, { deps, now, zone, print, example }) => {
|
|
17
|
-
const remind = example(destroysNothing(annotate(events.command("remind"), { effects: FETCH, mcp: false }))
|
|
17
|
+
const remind = example(destroysNothing(annotate(events.command("remind"), { effects: FETCH, mcp: false, literal_after_dashes: true }))
|
|
18
18
|
.description("tell once about each event that starts within --due-within: print it, or run your program for it with the " +
|
|
19
19
|
"event as JSON on its standard input; for cron or a timer every few minutes, syncing first unless --offline")
|
|
20
20
|
.summary("tell once about each event that starts soon, for a timer to run")
|
|
@@ -50,6 +50,7 @@ export const addRemind = (events, writes, { deps, now, zone, print, example }) =
|
|
|
50
50
|
if (!opts.dryRun && deps.source && !command.optsWithGlobals().offline) {
|
|
51
51
|
// A reminder from what the store already holds beats none: a failed sync is said in meta and the run goes on.
|
|
52
52
|
try {
|
|
53
|
+
// Feeds wait for `sync`: a timer runs this every few minutes, and a feed's server is not Google.
|
|
53
54
|
const report = await syncCalendars(deps.source, deps.store, {
|
|
54
55
|
accountId: deps.accountId,
|
|
55
56
|
now: at,
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { EntryInput } from "../meetings/index.js";
|
|
2
|
+
import type { SyncWindow } from "./source.js";
|
|
3
|
+
/**
|
|
4
|
+
* A wall-clock time in `zone` as epoch ms. A time a clock change skips is read with the offset before the change, so it
|
|
5
|
+
* lands after it, and a repeated one takes its first moment (RFC 5545 §3.3.5): a slot is moved, never dropped.
|
|
6
|
+
*/
|
|
7
|
+
export declare const localMoment: (date: string, time: string, zone: string) => number;
|
|
8
|
+
/** Why a series cannot be expanded here, or `null` when it can. */
|
|
9
|
+
export declare const unexpandable: (recurrence: readonly string[] | null | undefined) => string | null;
|
|
10
|
+
/**
|
|
11
|
+
* The slots of a series inside the window, as occurrences: its RRULE, plus RDATE, less EXDATE, which come back
|
|
12
|
+
* `cancelled` so a stored slot is cancelled. Times repeat in the series' own zone, so a weekly 10:00 stays 10:00
|
|
13
|
+
* across a clock change. An override (an iCalendar RECURRENCE-ID) takes the place of the slot it names.
|
|
14
|
+
*/
|
|
15
|
+
export declare const expandSeries: (series: EntryInput, window: SyncWindow, overrides?: readonly (EntryInput & {
|
|
16
|
+
originalStartsAt: number;
|
|
17
|
+
})[]) => EntryInput[];
|