@wirecat/cli-core 0.550.0 → 0.552.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 +235 -0
- package/README.md +1 -1
- package/THIRD_PARTY_NOTICES +41 -0
- package/dist/background/schedules.d.ts +12 -1
- package/dist/background/schedules.js +65 -24
- package/dist/background/units.d.ts +16 -9
- package/dist/background/units.js +26 -11
- package/dist/calendar/ics.d.ts +1 -1
- package/dist/calendar/ics.js +5 -2
- package/dist/calendar/windows-zones.generated.d.ts +1 -0
- package/dist/calendar/windows-zones.generated.js +146 -0
- package/dist/cli/compact.js +2 -0
- package/dist/cli/context.d.ts +2 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.js +3 -0
- package/dist/cli/meeting-findings-input.js +4 -3
- package/dist/cli/meeting-preparation-answer.d.ts +3 -0
- package/dist/cli/meeting-preparation-projects.d.ts +3 -0
- package/dist/cli/meeting-preparation-read.d.ts +3 -0
- package/dist/cli/messenger/backfill-command.js +28 -1
- package/dist/cli/messenger/context.js +1 -0
- package/dist/cli/messenger/history-gaps.js +4 -4
- package/dist/cli/messenger/mcp-command.js +1 -0
- package/dist/cli/messenger/messages-delete-command.js +7 -8
- package/dist/cli/messenger/projects-command.js +47 -2
- package/dist/cli/messenger/search-answers.js +8 -1
- package/dist/cli/messenger/serve-catch-up.d.ts +43 -0
- package/dist/cli/messenger/serve-catch-up.js +90 -0
- package/dist/cli/messenger/serve-command.js +54 -1
- package/dist/cli/messenger/server-command.d.ts +4 -3
- package/dist/cli/messenger/server-command.js +30 -12
- package/dist/cli/payment-query.d.ts +8 -0
- package/dist/cli/payment-query.js +27 -0
- package/dist/cli/program.js +9 -4
- package/dist/cli/project-status-rows.d.ts +8 -0
- package/dist/cli/project-status-rows.js +102 -0
- package/dist/cli/project-status.d.ts +132 -0
- package/dist/cli/project-status.js +248 -0
- package/dist/cli/schedule/schedule-command.js +5 -1
- package/dist/cli/steps.js +4 -1
- package/dist/domain/knowledge.d.ts +1 -1
- package/dist/domain/knowledge.js +1 -1
- package/dist/domain/status-words.d.ts +3 -0
- package/dist/domain/status-words.js +44 -0
- package/dist/entities/fact.d.ts +12 -1
- package/dist/entities/fact.js +13 -0
- package/dist/entities/hit.d.ts +5 -0
- package/dist/entities/hit.js +5 -0
- package/dist/entities/index.d.ts +45 -2
- package/dist/entities/listener.d.ts +6 -0
- package/dist/entities/listener.js +1 -0
- package/dist/entities/meeting-findings.d.ts +1 -1
- package/dist/entities/meeting-preparation.d.ts +3 -0
- package/dist/entities/message-transfer.d.ts +16 -0
- package/dist/entities/message-transfer.js +6 -0
- package/dist/errors/reasons.js +1 -0
- package/dist/mcp/app-server.js +1 -0
- package/dist/mcp/http/oauth.d.ts +2 -1
- package/dist/mcp/http/oauth.js +2 -2
- package/dist/mcp/http/serve.d.ts +2 -0
- package/dist/mcp/http/serve.js +2 -1
- package/dist/mcp/meeting-preparation.d.ts +3 -0
- package/dist/mcp/server.js +1 -0
- package/dist/mcp/surface.js +1 -1
- package/dist/meetings/answers.d.ts +5 -0
- package/dist/meetings/answers.js +8 -0
- package/dist/meetings/cli/index.js +14 -0
- package/dist/meetings/findings-rules.d.ts +14 -3
- package/dist/meetings/findings-rules.js +16 -2
- package/dist/meetings/index.d.ts +5 -3
- package/dist/meetings/index.js +3 -1
- package/dist/meetings/ingest.js +2 -1
- package/dist/meetings/language.d.ts +5 -0
- package/dist/meetings/language.js +57 -0
- package/dist/meetings/mcp/search.d.ts +3 -0
- package/dist/meetings/meeting-context.d.ts +2 -3
- package/dist/meetings/meeting-context.js +3 -4
- package/dist/meetings/payment.d.ts +27 -0
- package/dist/meetings/payment.js +81 -0
- package/dist/meetings/project-evidence-answer.d.ts +3 -0
- package/dist/meetings/result-items.d.ts +9 -0
- package/dist/meetings/result-items.js +7 -0
- package/dist/meetings/store.d.ts +22 -0
- package/dist/meetings/testing/memory.js +3 -0
- package/dist/meetings/transcription.d.ts +2 -0
- package/dist/meetings/transcription.js +5 -1
- package/dist/search/lucene/types.d.ts +7 -0
- package/dist/search/lucene/types.js +11 -1
- package/dist/services/archive.d.ts +13 -0
- package/dist/services/archive.js +56 -5
- package/dist/services/deps.d.ts +3 -0
- package/dist/services/history-limits.d.ts +2 -0
- package/dist/services/index.d.ts +4 -1
- package/dist/services/index.js +4 -1
- package/dist/services/job-query.d.ts +5 -2
- package/dist/services/job-query.js +5 -2
- package/dist/services/link-scorers/alias-hits.d.ts +7 -0
- package/dist/services/link-scorers/alias-hits.js +54 -0
- package/dist/services/link-scorers/chat-senders.d.ts +6 -0
- package/dist/services/link-scorers/chat-senders.js +56 -0
- package/dist/services/link-scorers/chat-title.d.ts +6 -0
- package/dist/services/link-scorers/chat-title.js +44 -0
- package/dist/services/link-scorers/email-domain.d.ts +6 -0
- package/dist/services/link-scorers/email-domain.js +64 -0
- package/dist/services/link-scorers/index.d.ts +9 -0
- package/dist/services/link-scorers/index.js +8 -0
- package/dist/services/link-scorers/mail-thread.d.ts +3 -0
- package/dist/services/link-scorers/mail-thread.js +74 -0
- package/dist/services/link-scorers/member-chats.d.ts +3 -0
- package/dist/services/link-scorers/member-chats.js +40 -0
- package/dist/services/link-scorers/member-dialog.d.ts +3 -0
- package/dist/services/link-scorers/member-dialog.js +34 -0
- package/dist/services/link-scorers/scan.d.ts +41 -0
- package/dist/services/link-scorers/scan.js +244 -0
- package/dist/services/link-scorers/shared.d.ts +26 -0
- package/dist/services/link-scorers/shared.js +48 -0
- package/dist/services/link-scorers/types.d.ts +77 -0
- package/dist/services/link-scorers/types.js +1 -0
- package/dist/services/messages-archive-search.js +3 -2
- package/dist/services/messages-combined.js +2 -2
- package/dist/services/messages-discovery.js +2 -2
- package/dist/services/notes-search.js +4 -5
- package/dist/services/project-status/index.d.ts +71 -0
- package/dist/services/project-status/index.js +196 -0
- package/dist/services/project-status/items.d.ts +10 -0
- package/dist/services/project-status/items.js +23 -0
- package/dist/services/project-status/signals/amounts.d.ts +20 -0
- package/dist/services/project-status/signals/amounts.js +91 -0
- package/dist/services/project-status/signals/children.d.ts +13 -0
- package/dist/services/project-status/signals/children.js +28 -0
- package/dist/services/project-status/signals/coverage.d.ts +13 -0
- package/dist/services/project-status/signals/coverage.js +30 -0
- package/dist/services/project-status/signals/index.d.ts +3 -0
- package/dist/services/project-status/signals/index.js +21 -0
- package/dist/services/project-status/signals/last-message.d.ts +4 -0
- package/dist/services/project-status/signals/last-message.js +22 -0
- package/dist/services/project-status/signals/payments.d.ts +9 -0
- package/dist/services/project-status/signals/payments.js +24 -0
- package/dist/services/project-status/signals/promises.d.ts +17 -0
- package/dist/services/project-status/signals/promises.js +109 -0
- package/dist/services/project-status/signals/questions.d.ts +8 -0
- package/dist/services/project-status/signals/questions.js +38 -0
- package/dist/services/project-status/signals/reply-next.d.ts +19 -0
- package/dist/services/project-status/signals/reply-next.js +75 -0
- package/dist/services/project-status/signals/topics.d.ts +10 -0
- package/dist/services/project-status/signals/topics.js +50 -0
- package/dist/services/project-status/types.d.ts +84 -0
- package/dist/services/project-status/types.js +1 -0
- package/dist/services/proposals/index.d.ts +1 -0
- package/dist/services/proposals/index.js +1 -0
- package/dist/services/proposals/kinds/findings.d.ts +3 -0
- package/dist/services/proposals/kinds/findings.js +8 -1
- package/dist/services/search-all-meetings.d.ts +3 -0
- package/dist/services/search-all-meetings.js +6 -1
- package/dist/speech/local.d.ts +2 -0
- package/dist/speech/local.js +8 -1
- package/dist/speech/models.d.ts +2 -0
- package/dist/speech/models.js +2 -0
- package/dist/store/decisions.d.ts +5 -1
- package/dist/store/decisions.js +5 -1
- package/dist/store/driver.d.ts +11 -0
- package/dist/store/driver.js +13 -0
- package/dist/store/drivers/node-sqlite.d.ts +1 -1
- package/dist/store/drivers/node-sqlite.js +3 -0
- package/dist/store/evidence-findings.js +1 -0
- package/dist/store/index.d.ts +2 -1
- package/dist/store/index.js +1 -0
- package/dist/store/project-transfer/request.d.ts +1 -1
- package/dist/store/project-transfer/schema.d.ts +2 -2
- package/dist/store/project-transfer/snapshot-model.d.ts +2 -2
- package/dist/store/sqlite/evidence-findings.js +5 -1
- package/dist/store/sqlite/facts.d.ts +13 -0
- package/dist/store/sqlite/facts.js +64 -6
- package/dist/store/sqlite/findings-core.js +14 -1
- package/dist/store/sqlite/knowledge.d.ts +16 -2
- package/dist/store/sqlite/knowledge.js +1 -0
- package/dist/store/sqlite/lucene.d.ts +1 -1
- package/dist/store/sqlite/lucene.js +33 -7
- package/dist/store/sqlite/meeting-hit-search.js +10 -3
- package/dist/store/sqlite/meeting-languages.d.ts +4 -0
- package/dist/store/sqlite/meeting-languages.js +36 -0
- package/dist/store/sqlite/meetings.d.ts +2 -1
- package/dist/store/sqlite/meetings.js +9 -3
- package/dist/store/sqlite/note-search.js +2 -2
- package/dist/store/sqlite/project-transfer/snapshot.d.ts +1 -1
- package/dist/store/sqlite/relation-proposals.d.ts +29 -2
- package/dist/store/sqlite/relation-proposals.js +75 -4
- package/dist/store/store.d.ts +1 -0
- package/package.json +3 -3
- package/parity.json +11 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,241 @@ shape of this file.
|
|
|
7
7
|
|
|
8
8
|
## Unreleased
|
|
9
9
|
|
|
10
|
+
## 0.552.0 — 02.11.2026
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **`serve` reads back on start what arrived while nothing listened.** A provider's own catch-up drops a gap it
|
|
15
|
+
finds too long (Telegram's `differenceTooLong`, and `channelDifferenceTooLong` past 100 updates in a channel), so
|
|
16
|
+
30 seconds after connecting `serve` lists the chats once and reads each chat whose newest message is newer than the
|
|
17
|
+
newest one held from before it started back to that one; a chat never held, back to the newest message held
|
|
18
|
+
anywhere. At most 7 days back, 500 messages a chat and 5 minutes; an empty store is left to `messages sync --all`,
|
|
19
|
+
and a messenger that pushes its history (`history: "store"`) is skipped. The summary gains
|
|
20
|
+
`caught_up: { chats, fetched, failed, stopped }`; the log gains `start_sync`, `start_sync_chat_failed` and
|
|
21
|
+
`start_sync_failed`.
|
|
22
|
+
- **A meeting transcript knows its language.** `transcriptInput` (provider fetches and imports) and local
|
|
23
|
+
transcription fill `language` instead of `null`: a one-language speech model gives its own (`gigaam-v3` and
|
|
24
|
+
`gigaam-v3-ctc` write Russian, `LocalSpeechRecognitionModel.language`), any other transcript is read from its
|
|
25
|
+
letters — Cyrillic is `ru` (`uk` when Ukrainian letters show), Latin is told apart by frequent words (`en`, `es`,
|
|
26
|
+
`de`, `fr`, `it`, `pt`), and a mixed, short or undecided one stays `null`. A meeting that is mostly Russian with a
|
|
27
|
+
few English lines is `ru`. `transcriptLanguage(texts)` is exported for providers that build their own transcripts.
|
|
28
|
+
- **Meeting search hits say the transcript's language and where in it the line is**: `language`, `line` (from 1) and
|
|
29
|
+
`lines_total` on transcript hits of `search meetings` and `search all` ("line 161 of 192").
|
|
30
|
+
- **`meetings repair`** previews filling what older builds left out of stored transcripts — their language, for
|
|
31
|
+
transcripts that have none; `--yes` stores it (`meeting_repair`, `MeetingStore.fillTranscriptLanguages`). Nothing is
|
|
32
|
+
written on a read.
|
|
33
|
+
|
|
34
|
+
### Changed — may break callers
|
|
35
|
+
|
|
36
|
+
- **A search that runs out of time fails with `timeout` and the reason `query.time-limit`**, no longer with
|
|
37
|
+
`validation_error`, `query.too-large` and `budget: "time"` or `"milliseconds"`. The message says the query is valid
|
|
38
|
+
and the time limit was reached. The details keep `budget: "time"` and `complete: false`, and add `limit_ms`. A caller
|
|
39
|
+
that told the person to change the query on `query.too-large` no longer does it for a slow search.
|
|
40
|
+
- **`server install` runs `systemctl --user` (or `launchctl enable`)**, and fails where that does not work, after it
|
|
41
|
+
wrote the file. **Under launchd `server stop` only unloads the agent**: it starts again at the next login, as a
|
|
42
|
+
systemd unit does; `server uninstall` stops that. `Platform` (`@wirecat/cli-core/background`) drops `atLogin` and
|
|
43
|
+
requires `install` and `uninstall`; its `state` may answer `enabled`.
|
|
44
|
+
|
|
45
|
+
### Fixed
|
|
46
|
+
|
|
47
|
+
- **A search stops at its time limit even inside one long SQLite statement.** A statement runs to its end before any
|
|
48
|
+
timer can fire, so a search over a large store ran far past the limit: 66 s against a 2 s limit, and past a
|
|
49
|
+
command's `--timeout 60s`. Under Node the message search statements now carry a check of the limit on every row
|
|
50
|
+
(the SQL function `wc_time_limit`) and stop when it passes. Bun has no SQL functions; there the limit is still
|
|
51
|
+
checked only between statements.
|
|
52
|
+
- **`server install` enables the unit, so serve starts with the owner's session after a reboot.** It wrote the unit
|
|
53
|
+
and never enabled it, and after a reboot nothing started. Under systemd it runs `systemctl --user enable` (still
|
|
54
|
+
`WantedBy=default.target`, so it also starts at boot under linger and over ssh); under launchd it clears a
|
|
55
|
+
`launchctl disable` and the agent no longer carries `Disabled`. Nothing starts now; `server start` does that.
|
|
56
|
+
A unit written with moved folders (a development checkout) is not enabled, so a branch build never starts at
|
|
57
|
+
login on the owner's keyring. `server uninstall` disables before it removes the file. `server status` answers
|
|
58
|
+
`starts_at_login` and `unit.enabled`. A unit installed before this needs `server install` once more.
|
|
59
|
+
- **A timer that fires before the login unlocks the keyring tries again.** A daily or one-off schedule that catches up
|
|
60
|
+
at boot ran before the keyring was unlocked and failed for the day. `schedule exec` now runs it again when it fails
|
|
61
|
+
with reason `credentials.unreachable` (tg and max give it when a profile that has logged in here cannot read its
|
|
62
|
+
sign-in): a minute later, then up to 15 minutes apart, for up to 12 hours, and notifies once, with how it ended.
|
|
63
|
+
`schedule run` and repeating schedules do not wait. `runSchedule` takes `untilUnlocked` and `wait` for this.
|
|
64
|
+
- **A sync whose reader goes away finishes its work.** A closed stdout or stderr (EPIPE) aborted every command, so a
|
|
65
|
+
desktop that closed the pipe ended an 18-minute `messages sync` with `write EPIPE`. A cooperative command (a sync)
|
|
66
|
+
and a command waiting in an `untilStopped()` phase (`schedule run`, `schedule exec`) now keep working, their later
|
|
67
|
+
output goes nowhere, and the run record says how they ended. Other commands still end with 0, as `| head` expects.
|
|
68
|
+
|
|
69
|
+
## 0.551.0 — 02.11.2026
|
|
70
|
+
|
|
71
|
+
### Added
|
|
72
|
+
|
|
73
|
+
- **`messages sync` keeps to the history limits too**, for every messenger: it does not read a group above
|
|
74
|
+
`history_group_members_max` people and stops a chat at its window (`history_dialog_days`, `history_group_days`). The
|
|
75
|
+
answer says `limited` (`group_too_large`, `older_than_window`; `--all` counts both), the skip is recorded for
|
|
76
|
+
`store status` and the search answers' `missing.history_limits`, and `--full-history` reads past it. Live messages
|
|
77
|
+
are never dropped.
|
|
78
|
+
- **`readClientFile` (from `@wirecat/cli-core/oauth`) reads a user's own Google Desktop OAuth client file** (the
|
|
79
|
+
`{ "installed": { client_id, client_secret } }` JSON from Google's console). It refuses a file other users can read
|
|
80
|
+
(`sign-in.client-file-open`) and a file of the wrong shape (`sign-in.client-file-invalid`), and never puts the file's
|
|
81
|
+
contents in an error. gcal, drive and mail each kept a copy of this reader.
|
|
82
|
+
- **`openBrowser(url, browser?)` is exported from `@wirecat/cli-core/cli`**, so a CLI that does not start through
|
|
83
|
+
`startCli` opens the sign-in page the same way: `open`, `rundll32` or `xdg-open`, or the named browser.
|
|
84
|
+
- **iCalendar feeds as read-only calendars.** With `deps.feeds` (`icsFeeds`), `calendars add <url> [--title]` follows a
|
|
85
|
+
public holiday calendar, a sports schedule or a published `.ics` address, and `calendars remove <calendar>` stops.
|
|
86
|
+
`sync` reads each feed after the provider (`syncWithFeeds`), so its events show in `events list`,
|
|
87
|
+
`search events`, `overlapping:` and `events remind`. Repeating events are expanded here (`expandSeries`: RRULE in
|
|
88
|
+
the event's own zone, RDATE, EXDATE and overrides). The address is a secret input, kept by the CLI and never
|
|
89
|
+
printed; a fetch follows redirects only to https and stops at 10 MB and 30 s. `readIcs` takes
|
|
90
|
+
`{ overrides: true }` and returns the calendar's `name`. See `docs/dev/CALENDARS.md`, "Feeds".
|
|
91
|
+
- **History limits for pushed history, per profile.** A messenger whose history is pushed (`history: "store"`,
|
|
92
|
+
WhatsApp) saves one-to-one chats back `history_dialog_days` (1095 by default), groups of more than two people back
|
|
93
|
+
`history_group_days` (365) and nothing of a group above `history_group_members_max` people (50); at most 1095 days.
|
|
94
|
+
The chat row stays, so the chat is still listed. Live messages, `messages sync` and what the store already holds are
|
|
95
|
+
never dropped. `messages sync <chat> --full-history` fetches a chat and keeps its pushed history in full from then on
|
|
96
|
+
(`history_included` in the answer). `ChatsStore` gains `historySkips`, `historyShapes`, `recordHistorySkips` and
|
|
97
|
+
`setHistoryIncluded`; `Saving` takes `historyLimits`.
|
|
98
|
+
- **Helpers for reading money and questions in a chat, for the project status work.** `amountsOf(text)` returns
|
|
99
|
+
every amount in a text, in order (`amountOf` is its first). `MONEY_REQUEST`, `MONEY_DONE` and `PROMISE` word lists
|
|
100
|
+
(ru, en, es) with `moneyClassesOf(text)` say which a text uses. `questions(messages, { asker: "owner" })` lists the
|
|
101
|
+
owner's own questions and whether anyone but the owner answered them, by a reply or by speaking next; the default,
|
|
102
|
+
`"others"`, is what it did before. `linkedChatsOf` is exported, now with each link's `role`, and so is
|
|
103
|
+
`AGENDA_DEFAULTS`, the silence `tasks agenda` waits before it lists a chat.
|
|
104
|
+
- **A command's own history handler learns whether each batch reached the store**: `HistoryBatchHandler` takes
|
|
105
|
+
`saved`, a promise of the same `true` or `false` the messenger gets, so a link can count only what was kept.
|
|
106
|
+
- **Every messenger command that takes a chat or a message also takes the reference other WireCat CLIs print**:
|
|
107
|
+
`chat:<provider>/<account>/<chat>` wherever a chat is named (`messages list chat:max/34871122/63488582`, bot
|
|
108
|
+
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
|
|
109
|
+
account is refused, naming the account it belongs to and the profiles of this CLI the store saw on it; it never
|
|
110
|
+
switches account. One of another messenger is refused with the command that opens it (`open it with tg <profile>
|
|
111
|
+
messages list chat:telegram/…`), and another kind (`document:413` given as a chat) with what the argument takes.
|
|
112
|
+
- **`event:<id>` and `meeting:<account>/<meeting>[/<transcript>[/<cue>]]` parse with `parseReference`** and
|
|
113
|
+
`canonicalReference`, as every other kind: `event:N` was printed by the calendar and person answers and could not be
|
|
114
|
+
read back. The kinds are one table, `REF_KINDS`; `looksLikeReference(text)` tells a reference from a title such as
|
|
115
|
+
`Note: plan`. Reference errors carry a reason: `ref.malformed`, `ref.unknown-kind` (with the kind meant:
|
|
116
|
+
`"doc:413" … did you mean document:413?`), `ref.wrong-kind`, `ref.wrong-provider`, `ref.wrong-account`.
|
|
117
|
+
- **`messages list` reads a window bounded on both sides, offline too.** The query takes a start and an end,
|
|
118
|
+
`'date>=2026-10-01 date<2026-10-20'` or `'date:[2026-10-01 TO 2026-10-19]'`, and `id>` with `date<`. A start reads
|
|
119
|
+
forward, oldest first, and stops at the end; an end alone reads back from it, newest page first, as before.
|
|
120
|
+
`--offline` and a messenger whose history is read from the store (WhatsApp) now answer both ways from the store
|
|
121
|
+
instead of refusing. A forward cursor carries the end, `after:<id>~<epoch ms>`, so the next page stops at the same
|
|
122
|
+
place. `--since`, `--since-time`, `--until`, `--until-time`, `--after-time` and `--before-time` on `messages list`
|
|
123
|
+
fail with the query form to type instead. The store's `messages()` window takes `after` (a message or a moment)
|
|
124
|
+
and `until`.
|
|
125
|
+
- **`projects status <project>`: where a project stands, from the store alone, with no model call.** For each chat
|
|
126
|
+
confirmed as the project's or a child project's, in this profile's account: the last message, who replies next
|
|
127
|
+
(you, them, a member a message names, or nobody), open questions both ways, promises (stored tasks, promise facts,
|
|
128
|
+
and first person future words such as "пришлю" or "I'll send"), amounts with their direction and whether a later
|
|
129
|
+
message says paid, payment facts, how long each `--context-query` (words that all appear in one text) has been
|
|
130
|
+
quiet on their side, and how much of the chat the store holds. Open amounts are summed per direction and currency. Word matches are marked `basis: pattern`;
|
|
131
|
+
a message saying "paid" is `paid-mentioned`, never paid. At most two steps: sync the chat held in part, then read
|
|
132
|
+
the chat that needs you most. The other accounts' linked chats are named in `out_of_scope`. tg and max get it with
|
|
133
|
+
the `projects` group; `--compact` draws each signal with its own row.
|
|
134
|
+
- **For an app: `projectStatus(store, input)`, a list of signals in `STATUS_SIGNALS`.** A signal is one file with an
|
|
135
|
+
id, a version and `read`; one that throws is named in `failed` and the others still answer. Each built in signal
|
|
136
|
+
has its compact row in `STATUS_SIGNAL_ROWS`, also in `SHARED_ROWS`. `projectStatusAnswer` and `projectStatusText`
|
|
137
|
+
build the answer and the human form. `input.purposes` picks which chat purposes are read (`["work"]` until links
|
|
138
|
+
carry one). The `messages.list` step takes a `query`.
|
|
139
|
+
- **`--compact`: one line per item for an agent, from the same answer `--json` prints.** `messages list <chat>
|
|
140
|
+
--compact` prints `#<id> · 10-01 09:12 · Анна · <text>` under one header line that names the shared ref prefix
|
|
141
|
+
(`ref: msg:max/500/-1001/<id>`), what every row has in common (the chat's title), the date span and the zone. Rows
|
|
142
|
+
from several chats keep their whole refs. Text stays on one line with control and direction characters made
|
|
143
|
+
visible, and is cut at 400 characters with `…(+N)`; `--full` keeps it whole. The paging, `missing`, the next steps
|
|
144
|
+
and an incomplete answer follow as `# more:`, `# missing:`, `# next:` and `# complete: false` lines on stdout, and
|
|
145
|
+
each command line in them carries `--compact` again. On 1 000 messages of one chat it is about 48 % of the JSON
|
|
146
|
+
bytes; the texts alone are 40 %. `--compact` with `--json`, `--json=…`, `--jsonl` or `--brief` fails with
|
|
147
|
+
`validation_error` (`output.conflict`); it never prompts, and exits with the code JSON would. MCP keeps JSON.
|
|
148
|
+
- **Every command that answers through `renderAnswer` gets it.** A command draws with its own row
|
|
149
|
+
(`answerEntity(cmd, name, schema, { row })`), its app's (`rows: { name: shape }` in the program definition,
|
|
150
|
+
which also draws an app entity listed inside one thing), the shared one in `SHARED_ROWS` (`message`,
|
|
151
|
+
`message.summary`, `message.hit`, `chat`), or the generic row: key, ref or id · title, name or path · state,
|
|
152
|
+
status or due date · text. A record or a list is never printed as a column's value. `RowShape` is data, and `compactText(body, shape,
|
|
153
|
+
options)` is pure, for an app that prints its own answers.
|
|
154
|
+
- What a command prints without `renderAnswer` stays JSON under `--compact`.
|
|
155
|
+
- **`scanProjectLinks(store, { project, sources, since, write, scorers?, threshold?, limit?, purposes? })` and
|
|
156
|
+
`LinkScorer` (`@wirecat/cli-core/services`): memo proposes a project's chats, mail threads and people itself.**
|
|
157
|
+
Six scorers in `SCORERS` (`chat-title`, `alias-hits`, `member-chats`, `member-dialog`, `chat-senders`,
|
|
158
|
+
`mail-thread`) and the shared mail domain rule as `email-domain` in `PERSON_SCORERS`. Signals combine as
|
|
159
|
+
1 − Π(1 − w); at most 20 new proposals a run by the rule bot `memo-scan`, never confirmed by themselves. Each
|
|
160
|
+
carries `score` and `signals` (scorer, weight, why: ids and counts only). Linked and rejected pairs are skipped,
|
|
161
|
+
a pending one is updated in place; people are matched by identity only, never by name. `write: false` writes nothing.
|
|
162
|
+
- **`knowledge.relationState(scope, { from, to, kind })`** says whether a link joins a pair, whether a proposal for it
|
|
163
|
+
was rejected, and which one is pending; `proposeRelation` takes `score` and `signals`.
|
|
164
|
+
- **Payments are facts of type `payment`.** The amount (text, two decimals), ISO 4217 currency, direction (`to_us` or
|
|
165
|
+
`from_us`), counterparty ref or name, due date, state (`expected`, `paid`, `cancelled`) and item ride in
|
|
166
|
+
`metadata.payment`, read back as `Fact.payment`; no migration. A payment fact cites 1 to 8 references. Paid is a new
|
|
167
|
+
fact that supersedes the expected one, so the history stays. Only the owner accepts or rejects a payment:
|
|
168
|
+
`mayDecide` takes `ownerOnly`. `facts.list({ payment: { open, direction, state } })` filters them; `open` is
|
|
169
|
+
expected and still holding. `paymentTitle(fact)` reads "1 200 EUR to us from Ana, due 31.10 (expected)".
|
|
170
|
+
- **Agents stage payments as findings of kind `payment`** in `projects actions add`; confirming one writes and
|
|
171
|
+
accepts the fact, and a batch holding one is the owner's to decide. The inbox summarises each finding through
|
|
172
|
+
`FACT_SUMMARIES`, one row per kind. `projects status` lists payment facts with their title and adds the expected
|
|
173
|
+
ones to `money.open`, counting an ask the fact cites once.
|
|
174
|
+
- **For an app's `facts list`: `PAYMENT_QUERY_FIELDS` (`open:true`, `direction:to_us`, `state:paid`) and
|
|
175
|
+
`paymentFilterOf(terms)`.**
|
|
176
|
+
|
|
177
|
+
### Changed — may break callers
|
|
178
|
+
|
|
179
|
+
- **An option after `--` is read as the option.** `messages sync -- -100 --json` now syncs `-100` and answers in JSON;
|
|
180
|
+
it failed with "too many arguments". After `--`, a word that is exactly one of the command's options, its own or a
|
|
181
|
+
global one (`--json`, `--limit 5`, `--json=id`), moves in front of `--`; every other word stays a positional. A
|
|
182
|
+
negative id needs no `--` at all. Commands that take free text keep every word after `--` literal: `messages send` and
|
|
183
|
+
`edit` (also the bot's), `polls create`, `topics create`, `chats create`, `chats folders create`, `contacts rename`,
|
|
184
|
+
`contacts aliases set`, `messages press`, `bot commands set`, `schedule command`, `events remind`, `complete`, and the
|
|
185
|
+
notes templates. Mark such a command with `annotate(command, { literal_after_dashes: true })`; MCP calls are never
|
|
186
|
+
lifted. A script that passed the literal word `--json` as a positional of another command now gets the option instead.
|
|
187
|
+
- **A write to a calendar the account may only read refuses before anything is asked or journaled**
|
|
188
|
+
(`calendar.read-only`): create, quick add, import, update, delete, respond and move, for a feed and for a provider's
|
|
189
|
+
`reader` or `free-busy` calendar alike. Before, only the target of a move was checked; the provider refused the rest.
|
|
190
|
+
`calendars busy` and `search --backend server` leave feed calendars out.
|
|
191
|
+
- **A locator of another account or messenger is refused with a new text and a reason.** `messages show`, `context`,
|
|
192
|
+
`link` and `download` said "that locator belongs to another account; select its profile first" or "…belongs to
|
|
193
|
+
another messenger"; they now say `<ref> belongs to account <id>; profile <name> is account <id>` (`ref.wrong-account`)
|
|
194
|
+
or `<ref> is a <messenger> message; open it with …` (`ref.wrong-provider`). A script matching the old text matches
|
|
195
|
+
the reason instead.
|
|
196
|
+
- **A text of the form `<kind>:<no spaces>` given as a chat is read as a reference**, so a chat titled `note:plan` is
|
|
197
|
+
no longer found by that title; give its id or a part of the title.
|
|
198
|
+
- The provider seeds of `telegram`, `max` and `whatsapp` name their CLI (`command: tg`, `max`, `wa`), which a
|
|
199
|
+
provider clash message now shows (`telegram (messages, from tg)`).
|
|
200
|
+
- **cli-core now needs `@wirecat/cli-base` 0.75.0**, which holds the `--full-history` switch's name and the help
|
|
201
|
+
without `OR` for a list its provider filters. A CLI that pins cli-base bumps it to 0.75.0 together with cli-core.
|
|
202
|
+
- **`FACT_TYPES` gains `payment`, and a `Finding` may be a `PaymentFinding`.** A caller that lists facts with a fixed
|
|
203
|
+
type list, or reads `finding.title` on every finding that is not a fact, narrows by `kind` first. A memo older than
|
|
204
|
+
this release refuses a payment fact in `facts list`; ship memo's bump the same day. `MEETING_FACT_TYPES` is gone:
|
|
205
|
+
`FACT_TYPES` is the one list.
|
|
206
|
+
|
|
207
|
+
### Fixed
|
|
208
|
+
|
|
209
|
+
- **`messages delete` with no message id refuses with `validation_error`** before it connects. An empty id (`""`
|
|
210
|
+
or a lone comma) used to reach the messenger as a deletion of nothing and answer success.
|
|
211
|
+
- **An MCP read or write whose answer named a step only the person can take (`user_action`) failed as a bug.** The
|
|
212
|
+
router dropped next steps this server does not offer by their command, and such a step has none. It is kept now;
|
|
213
|
+
a command step this server does not offer is still dropped.
|
|
214
|
+
- **A sum written in groups of three read as the wrong amount: `50 000 ₽` was `0` and `4 300 ₽` was `300`**, because
|
|
215
|
+
only the last group was read. Groups after a space, a no-break or thin space, a dot or a comma (`50 000`,
|
|
216
|
+
`1.200 €`, `$1,200`) are one number now, a decimal part after them is kept (`5 000.50 ₽`), and amounts keep two
|
|
217
|
+
decimals instead of a whole number (`15.5 eur` was 16). This is the amount the duplicate check compares between
|
|
218
|
+
two tasks, so a reminder worded with a grouped sum is no longer compared on the wrong figure.
|
|
219
|
+
- **`page.next` of `messages list` and `messages comments` runs as written.** It repeated the query's `id`/`date`
|
|
220
|
+
bound beside `--cursor`, which the command refuses as two starting points; it now keeps only `topic:` and
|
|
221
|
+
`starred:`.
|
|
222
|
+
- **An inclusive start keeps a message sent exactly at it.** `messages list 'date>=2026-10-01'` skipped a message
|
|
223
|
+
sent at the first millisecond of the day, and `date<="…"` one sent at that moment.
|
|
224
|
+
- **`--brief` now cuts a messenger search hit.** It only knew mail's camelCase hits, so `search messages --brief` in
|
|
225
|
+
tg, max and wa printed every field; it now keeps `ref`, `chat_title`, `timestamp`, `sender_name`, `excerpt` and
|
|
226
|
+
`score`. The default and `--full` answers are unchanged. Commands that print through `outputFor` now honour
|
|
227
|
+
`--brief` and `--full` too, and refuse them together as the others do.
|
|
228
|
+
- **An Outlook or Exchange feed's events are read, no longer skipped.** Those feeds name time zones the Windows way,
|
|
229
|
+
`TZID:W. Europe Standard Time`, and the iCalendar reader took only IANA names (`Europe/Berlin`), so `calendars add`
|
|
230
|
+
and `sync` skipped every timed event of such a feed and counted it under `skipped`. A Windows name is now read as
|
|
231
|
+
the IANA zone Unicode CLDR gives it worldwide (territory `001`), in DTSTART, DTEND, RECURRENCE-ID, and in the TZID of
|
|
232
|
+
RDATE and EXDATE, which are stored with the IANA name. The 142 names are a table generated from CLDR release-49
|
|
233
|
+
(`scripts/windows-zones.ts` regenerates it); no new dependency. A TZID that is neither, such as Outlook's
|
|
234
|
+
`(UTC+01:00) Amsterdam, Berlin, Bern`, is still skipped.
|
|
235
|
+
- **A list whose provider filters it no longer says in `--help` that its query takes `OR`.** With
|
|
236
|
+
`@wirecat/cli-base` 0.74.0 → 0.75.0, its `Syntax:` line names only what it takes: `field:value` terms joined by AND,
|
|
237
|
+
and the phrase, `word*` and range forms where a field takes them. Every CLI picks this up at its next cli-core bump.
|
|
238
|
+
- **The browser login page of `mcp serve --http` names what the app asks to reach and the right command.** It always
|
|
239
|
+
said "reach your messages through `<app> mcp --http`"; a CLI that is not a messenger now says what it serves (gcal:
|
|
240
|
+
"your Google Calendar"), and every CLI names `<app> mcp serve --http`. `HttpOptions` takes `reaches` for the
|
|
241
|
+
wording; unset, the page says "your `<app>` data".
|
|
242
|
+
- **`store jobs list` says in its help and its MCP description that its query takes `OR`.** The list reads every job
|
|
243
|
+
file and takes the whole query language, but its registry said the source filters it (`native`).
|
|
244
|
+
|
|
10
245
|
## 0.550.0 — 02.11.2026
|
|
11
246
|
|
|
12
247
|
### Changed — may break callers
|
package/README.md
CHANGED
|
@@ -172,7 +172,7 @@ run a read, run a write — so an agent's context does not fill with a hundred t
|
|
|
172
172
|
over HTTP on 127.0.0.1 behind the owner's tunnel, with a one-owner OAuth login.
|
|
173
173
|
|
|
174
174
|
`serve` keeps the store current, one process per profile by a lock file; `server install` adds a systemd user unit
|
|
175
|
-
or a launchd agent.
|
|
175
|
+
or a launchd agent that starts at every login.
|
|
176
176
|
|
|
177
177
|
### Every write behind a guard
|
|
178
178
|
|
package/THIRD_PARTY_NOTICES
CHANGED
|
@@ -758,3 +758,44 @@ Licensed under SIL Open Font License 1.1; see dist/charts/fonts/OFL.txt and SOUR
|
|
|
758
758
|
|
|
759
759
|
PNG rasterization uses @resvg/resvg-js under MPL-2.0. Its original sources are
|
|
760
760
|
available at https://github.com/thx/resvg-js/tree/v2.6.2.
|
|
761
|
+
|
|
762
|
+
Windows time zone names mapped to IANA zones (dist/calendar/windows-zones.generated.js), generated from
|
|
763
|
+
Unicode CLDR release-49, common/supplemental/windowsZones.xml (https://github.com/unicode-org/cldr).
|
|
764
|
+
|
|
765
|
+
Copyright © 2001–2026 Unicode, Inc.
|
|
766
|
+
|
|
767
|
+
NOTICE TO USER: Carefully read the following legal agreement. BY
|
|
768
|
+
DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING DATA FILES, AND/OR
|
|
769
|
+
SOFTWARE, YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE
|
|
770
|
+
TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT
|
|
771
|
+
DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE.
|
|
772
|
+
|
|
773
|
+
Permission is hereby granted, free of charge, to any person obtaining a
|
|
774
|
+
copy of data files and any associated documentation (the "Data Files") or
|
|
775
|
+
software and any associated documentation (the "Software") to deal in the
|
|
776
|
+
Data Files or Software without restriction, including without limitation
|
|
777
|
+
the rights to use, copy, modify, merge, publish, distribute, and/or sell
|
|
778
|
+
copies of the Data Files or Software, and to permit persons to whom the
|
|
779
|
+
Data Files or Software are furnished to do so, provided that either (a)
|
|
780
|
+
this copyright and permission notice appear with all copies of the Data
|
|
781
|
+
Files or Software, or (b) this copyright and permission notice appear in
|
|
782
|
+
associated Documentation.
|
|
783
|
+
|
|
784
|
+
THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
|
|
785
|
+
KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
786
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF
|
|
787
|
+
THIRD PARTY RIGHTS.
|
|
788
|
+
|
|
789
|
+
IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE
|
|
790
|
+
BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES,
|
|
791
|
+
OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
|
|
792
|
+
WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
|
|
793
|
+
ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA
|
|
794
|
+
FILES OR SOFTWARE.
|
|
795
|
+
|
|
796
|
+
Except as contained in this notice, the name of a copyright holder shall
|
|
797
|
+
not be used in advertising or otherwise to promote the sale, use or other
|
|
798
|
+
dealings in these Data Files or Software without prior written
|
|
799
|
+
authorization of the copyright holder.
|
|
800
|
+
|
|
801
|
+
SPDX-License-Identifier: Unicode-3.0
|
|
@@ -97,14 +97,25 @@ export declare const timerFor: (app: AppIdentity, system: ServerSystem, env: Nod
|
|
|
97
97
|
export declare const notify: (system: ServerSystem, env: NodeJS.ProcessEnv, title: string, body: string) => Promise<boolean>;
|
|
98
98
|
/** Finds a program on PATH, as the timer will not have the shell's PATH to find it by. */
|
|
99
99
|
export declare const findProgram: (name: string, env: NodeJS.ProcessEnv, platform: NodeJS.Platform) => string | undefined;
|
|
100
|
+
/**
|
|
101
|
+
* The reason tg and max give when a profile that has logged in here cannot read its sign-in: the keyring is
|
|
102
|
+
* locked or out of reach, which a later try fixes and a new login does not.
|
|
103
|
+
*/
|
|
104
|
+
export declare const CREDENTIALS_UNREACHABLE = "credentials.unreachable";
|
|
100
105
|
/**
|
|
101
106
|
* One run: the command or the agent with the schedule's variables, its output kept beside the schedule,
|
|
102
107
|
* the last result in `last.json`. An agent runs under `<PREFIX>_READ_ONLY`, so nothing it is talked
|
|
103
108
|
* into can send. A one-off removes itself afterwards.
|
|
109
|
+
*
|
|
110
|
+
* `untilUnlocked` is for the timer's run: a timer that catches up at boot runs before the login unlocks the
|
|
111
|
+
* keyring, so a run that fails for that reason is tried again, a minute then up to 15 minutes apart, for up to
|
|
112
|
+
* 12 hours. A schedule that repeats every few minutes is left to its next tick; a run by hand fails at once.
|
|
104
113
|
*/
|
|
105
|
-
export declare const runSchedule: (app: AppIdentity, system: ServerSystem, env: NodeJS.ProcessEnv, file: string, { now, timer, signal, killAfterMs, }?: {
|
|
114
|
+
export declare const runSchedule: (app: AppIdentity, system: ServerSystem, env: NodeJS.ProcessEnv, file: string, { now, timer, signal, killAfterMs, untilUnlocked, wait, }?: {
|
|
106
115
|
now?: () => Date;
|
|
107
116
|
timer?: Timer;
|
|
108
117
|
signal?: AbortSignal;
|
|
109
118
|
killAfterMs?: number;
|
|
119
|
+
untilUnlocked?: boolean;
|
|
120
|
+
wait?: (ms: number, signal?: AbortSignal) => Promise<void>;
|
|
110
121
|
}) => Promise<LastRun>;
|
|
@@ -328,16 +328,79 @@ export const findProgram = (name, env, platform) => {
|
|
|
328
328
|
const KEEP_RUNS = 30;
|
|
329
329
|
/** How long a stopped child has to save and answer after SIGTERM before it is killed. */
|
|
330
330
|
const KILL_AFTER_MS = 10_000;
|
|
331
|
+
/**
|
|
332
|
+
* The reason tg and max give when a profile that has logged in here cannot read its sign-in: the keyring is
|
|
333
|
+
* locked or out of reach, which a later try fixes and a new login does not.
|
|
334
|
+
*/
|
|
335
|
+
export const CREDENTIALS_UNREACHABLE = "credentials.unreachable";
|
|
336
|
+
const LOCKED_FIRST_WAIT_MS = 60_000;
|
|
337
|
+
const LOCKED_LONGEST_WAIT_MS = 15 * 60_000;
|
|
338
|
+
/** A night's catch-up waits for the morning's login; past that the run fails as it is. */
|
|
339
|
+
const LOCKED_GIVE_UP_MS = 12 * 60 * 60_000;
|
|
340
|
+
const lockedOut = (last) => last.reason?.endsWith(`(${CREDENTIALS_UNREACHABLE})`) === true;
|
|
341
|
+
const pause = (ms, signal) => new Promise((resolve) => {
|
|
342
|
+
if (signal?.aborted)
|
|
343
|
+
return resolve();
|
|
344
|
+
const done = () => {
|
|
345
|
+
clearTimeout(timer);
|
|
346
|
+
signal?.removeEventListener("abort", done);
|
|
347
|
+
resolve();
|
|
348
|
+
};
|
|
349
|
+
const timer = setTimeout(done, ms);
|
|
350
|
+
signal?.addEventListener("abort", done, { once: true });
|
|
351
|
+
});
|
|
331
352
|
/**
|
|
332
353
|
* One run: the command or the agent with the schedule's variables, its output kept beside the schedule,
|
|
333
354
|
* the last result in `last.json`. An agent runs under `<PREFIX>_READ_ONLY`, so nothing it is talked
|
|
334
355
|
* into can send. A one-off removes itself afterwards.
|
|
356
|
+
*
|
|
357
|
+
* `untilUnlocked` is for the timer's run: a timer that catches up at boot runs before the login unlocks the
|
|
358
|
+
* keyring, so a run that fails for that reason is tried again, a minute then up to 15 minutes apart, for up to
|
|
359
|
+
* 12 hours. A schedule that repeats every few minutes is left to its next tick; a run by hand fails at once.
|
|
335
360
|
*/
|
|
336
|
-
export const runSchedule = async (app, system, env, file, { now = () => new Date(), timer, signal, killAfterMs = KILL_AFTER_MS, } = {}) => {
|
|
361
|
+
export const runSchedule = async (app, system, env, file, { now = () => new Date(), timer, signal, killAfterMs = KILL_AFTER_MS, untilUnlocked = false, wait = pause, } = {}) => {
|
|
337
362
|
const schedule = readSchedule(file);
|
|
338
363
|
const folder = join(file, "..");
|
|
339
364
|
const runs = join(folder, "runs");
|
|
340
365
|
mkdirSync(runs, { recursive: true, mode: 0o700 });
|
|
366
|
+
const agent = schedule.kind === "agent";
|
|
367
|
+
const retries = untilUnlocked && !("every" in schedule.when);
|
|
368
|
+
const firstAt = now().getTime();
|
|
369
|
+
let last = await attemptSchedule(app, system, env, schedule, folder, { now, signal, killAfterMs });
|
|
370
|
+
for (let delay = LOCKED_FIRST_WAIT_MS; retries && lockedOut(last) && !signal?.aborted && now().getTime() - firstAt + delay <= LOCKED_GIVE_UP_MS; delay = Math.min(delay * 2, LOCKED_LONGEST_WAIT_MS)) {
|
|
371
|
+
await wait(delay, signal);
|
|
372
|
+
if (signal?.aborted)
|
|
373
|
+
break;
|
|
374
|
+
last = await attemptSchedule(app, system, env, schedule, folder, { now, signal, killAfterMs });
|
|
375
|
+
}
|
|
376
|
+
const { exitCode, output } = last;
|
|
377
|
+
const errors = agent ? output.replace(/\.md$/, ".log") : output;
|
|
378
|
+
if (agent && exitCode === 0)
|
|
379
|
+
copyFileSync(output, join(folder, "latest.md"));
|
|
380
|
+
const stampOf = (file) => file.split(".")[0] ?? file;
|
|
381
|
+
const kept = new Set([...new Set(readdirSync(runs).map(stampOf))].sort().slice(-KEEP_RUNS));
|
|
382
|
+
for (const file of readdirSync(runs))
|
|
383
|
+
if (!kept.has(stampOf(file)))
|
|
384
|
+
rmSync(join(runs, file), { force: true });
|
|
385
|
+
const title = `${app.command} ${schedule.name}`;
|
|
386
|
+
if (agent && exitCode === 0) {
|
|
387
|
+
const first = readFileSync(output, "utf8")
|
|
388
|
+
.split("\n")
|
|
389
|
+
.find((line) => line.trim()) ?? "report ready";
|
|
390
|
+
await notify(system, env, title, `${first.trim().slice(0, 180)}\n${join(folder, "latest.md")}`);
|
|
391
|
+
}
|
|
392
|
+
else if (exitCode !== 0) {
|
|
393
|
+
await notify(system, env, title, `failed (exit ${exitCode}) — ${errors}`);
|
|
394
|
+
}
|
|
395
|
+
if ("once" in schedule.when) {
|
|
396
|
+
await timer?.remove(schedule.name);
|
|
397
|
+
rmSync(file, { force: true });
|
|
398
|
+
}
|
|
399
|
+
return last;
|
|
400
|
+
};
|
|
401
|
+
/** One run of the schedule's command or agent: its output in `runs/`, its result in `last.json`. */
|
|
402
|
+
const attemptSchedule = async (app, system, env, schedule, folder, { now, signal, killAfterMs }) => {
|
|
403
|
+
const runs = join(folder, "runs");
|
|
341
404
|
const stamp = now()
|
|
342
405
|
.toISOString()
|
|
343
406
|
.replaceAll(":", "-")
|
|
@@ -398,33 +461,11 @@ export const runSchedule = async (app, system, env, file, { now = () => new Date
|
|
|
398
461
|
exitCode,
|
|
399
462
|
durationMs: ended.getTime() - began,
|
|
400
463
|
});
|
|
401
|
-
|
|
464
|
+
return recordRun(join(folder, "last.json"), {
|
|
402
465
|
at: ended.toISOString(),
|
|
403
466
|
exitCode,
|
|
404
467
|
output,
|
|
405
468
|
traceId: trace.traceId,
|
|
406
469
|
...(exitCode === 0 ? {} : failureOf(readFileSync(errors, "utf8"), exitCode)),
|
|
407
470
|
});
|
|
408
|
-
if (agent && exitCode === 0)
|
|
409
|
-
copyFileSync(output, join(folder, "latest.md"));
|
|
410
|
-
const stampOf = (file) => file.split(".")[0] ?? file;
|
|
411
|
-
const kept = new Set([...new Set(readdirSync(runs).map(stampOf))].sort().slice(-KEEP_RUNS));
|
|
412
|
-
for (const file of readdirSync(runs))
|
|
413
|
-
if (!kept.has(stampOf(file)))
|
|
414
|
-
rmSync(join(runs, file), { force: true });
|
|
415
|
-
const title = `${app.command} ${schedule.name}`;
|
|
416
|
-
if (agent && exitCode === 0) {
|
|
417
|
-
const first = readFileSync(output, "utf8")
|
|
418
|
-
.split("\n")
|
|
419
|
-
.find((line) => line.trim()) ?? "report ready";
|
|
420
|
-
await notify(system, env, title, `${first.trim().slice(0, 180)}\n${join(folder, "latest.md")}`);
|
|
421
|
-
}
|
|
422
|
-
else if (exitCode !== 0) {
|
|
423
|
-
await notify(system, env, title, `failed (exit ${exitCode}) — ${errors}`);
|
|
424
|
-
}
|
|
425
|
-
if ("once" in schedule.when) {
|
|
426
|
-
await timer?.remove(schedule.name);
|
|
427
|
-
rmSync(file, { force: true });
|
|
428
|
-
}
|
|
429
|
-
return last;
|
|
430
471
|
};
|
|
@@ -33,21 +33,28 @@ export interface Platform {
|
|
|
33
33
|
text: (unit: Unit, app: AppIdentity) => string;
|
|
34
34
|
start: (unit: Unit) => string[][];
|
|
35
35
|
stop: (unit: Unit) => string[][];
|
|
36
|
-
state: (unit: Unit) => Promise<
|
|
37
|
-
loaded: boolean;
|
|
38
|
-
active: boolean;
|
|
39
|
-
pid?: number;
|
|
40
|
-
detail?: string;
|
|
41
|
-
exitCode?: number;
|
|
42
|
-
}>;
|
|
36
|
+
state: (unit: Unit) => Promise<UnitState>;
|
|
43
37
|
logs: (unit: Unit, lines: number) => Promise<string>;
|
|
44
38
|
/** What must exist before the unit first runs. */
|
|
45
39
|
prepare?: (unit: Unit) => void;
|
|
46
|
-
/**
|
|
47
|
-
|
|
40
|
+
/** Run once the file is written: the unit starts with every login from then on, and nothing starts now. */
|
|
41
|
+
install: (unit: Unit) => string[][];
|
|
42
|
+
/** Run before the file is removed: it no longer starts at login. */
|
|
43
|
+
uninstall: (unit: Unit) => string[][];
|
|
44
|
+
}
|
|
45
|
+
export interface UnitState {
|
|
46
|
+
loaded: boolean;
|
|
47
|
+
active: boolean;
|
|
48
|
+
/** It starts with the owner's session: at login, or at boot when systemd keeps the user's services (linger). */
|
|
49
|
+
enabled?: boolean;
|
|
50
|
+
pid?: number;
|
|
51
|
+
detail?: string;
|
|
52
|
+
exitCode?: number;
|
|
48
53
|
}
|
|
49
54
|
export declare const systemd: (app: AppIdentity, system: ServerSystem, env: NodeJS.ProcessEnv) => Platform;
|
|
50
55
|
export declare const launchd: (app: AppIdentity, system: ServerSystem, env: NodeJS.ProcessEnv) => Platform;
|
|
56
|
+
/** `launchctl print-disabled` lists overrides as `"label" => disabled` (older macOS: `=> true`); none is enabled. */
|
|
57
|
+
export declare const launchdEnabled: (listing: string, label: string) => boolean;
|
|
51
58
|
/**
|
|
52
59
|
* serve's log lines for a person: a log record as `time severity body`, anything else — a line from an
|
|
53
60
|
* older serve, the summary it prints on stop — as it is. A journal's own prefix is kept.
|
package/dist/background/units.js
CHANGED
|
@@ -61,9 +61,16 @@ export const systemd = (app, system, env) => {
|
|
|
61
61
|
...(unit.noRestartOn?.length ? [`RestartPreventExitStatus=${unit.noRestartOn.join(" ")}`] : []),
|
|
62
62
|
"",
|
|
63
63
|
"[Install]",
|
|
64
|
+
// Not graphical-session.target: a desktop without systemd's session targets, or a machine reached only
|
|
65
|
+
// over ssh, would never start it. A keyring still locked at boot ends serve with a code that restarts.
|
|
64
66
|
"WantedBy=default.target",
|
|
65
67
|
"",
|
|
66
68
|
].join("\n"),
|
|
69
|
+
install: (unit) => [
|
|
70
|
+
["systemctl", "--user", "daemon-reload"],
|
|
71
|
+
["systemctl", "--user", "enable", unit.name],
|
|
72
|
+
],
|
|
73
|
+
uninstall: (unit) => [["systemctl", "--user", "disable", unit.name]],
|
|
67
74
|
start: (unit) => [
|
|
68
75
|
["systemctl", "--user", "daemon-reload"],
|
|
69
76
|
["systemctl", "--user", "start", unit.name],
|
|
@@ -87,6 +94,8 @@ export const systemd = (app, system, env) => {
|
|
|
87
94
|
"ExecMainCode",
|
|
88
95
|
"-p",
|
|
89
96
|
"ExecMainStatus",
|
|
97
|
+
"-p",
|
|
98
|
+
"UnitFileState",
|
|
90
99
|
], env);
|
|
91
100
|
const fields = Object.fromEntries(ran.stdout
|
|
92
101
|
.split("\n")
|
|
@@ -98,13 +107,13 @@ export const systemd = (app, system, env) => {
|
|
|
98
107
|
return {
|
|
99
108
|
loaded: fields.LoadState === "loaded",
|
|
100
109
|
active: fields.ActiveState === "active",
|
|
110
|
+
...(fields.UnitFileState ? { enabled: fields.UnitFileState === "enabled" } : {}),
|
|
101
111
|
...(pid > 0 ? { pid } : {}),
|
|
102
112
|
...(exitCode > 0 ? { exitCode } : {}),
|
|
103
113
|
...(fields.ActiveState ? { detail: `${fields.ActiveState} (${fields.SubState})` } : {}),
|
|
104
114
|
};
|
|
105
115
|
},
|
|
106
116
|
logs: (unit, lines) => checked(["journalctl", "--user", "-u", unit.name, "-n", String(lines), "--no-pager"]),
|
|
107
|
-
atLogin: (unit) => `at every login too: systemctl --user enable ${unit.name.replace(/\.service$/, "")}`,
|
|
108
117
|
};
|
|
109
118
|
};
|
|
110
119
|
const xml = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">");
|
|
@@ -130,8 +139,6 @@ export const launchd = (app, system, env) => {
|
|
|
130
139
|
" <dict>",
|
|
131
140
|
...Object.entries(unit.environment).map(([name, value]) => ` <key>${xml(name)}</key><string>${xml(value)}</string>`),
|
|
132
141
|
" </dict>",
|
|
133
|
-
// launchd loads every agent here at login; disabled, it waits for `server start` to enable it.
|
|
134
|
-
" <key>Disabled</key><true/>",
|
|
135
142
|
// biome-ignore lint/security/noSecrets: launchd plist template, not a key
|
|
136
143
|
" <key>RunAtLoad</key><true/>",
|
|
137
144
|
...(unit.noRestartOn?.length ? [] : [" <key>KeepAlive</key><dict><key>SuccessfulExit</key><false/></dict>"]),
|
|
@@ -142,25 +149,27 @@ export const launchd = (app, system, env) => {
|
|
|
142
149
|
"</plist>",
|
|
143
150
|
"",
|
|
144
151
|
].join("\n"),
|
|
145
|
-
//
|
|
152
|
+
// launchd loads every agent in the folder at login, RunAtLoad starts it; `enable` clears a `disable` left by
|
|
153
|
+
// an older stop or uninstall, which would outrank the file.
|
|
154
|
+
install: (unit) => [["launchctl", "enable", `${domain}/${unit.name}`]],
|
|
155
|
+
uninstall: (unit) => [["launchctl", "disable", `${domain}/${unit.name}`]],
|
|
146
156
|
start: (unit) => [
|
|
147
157
|
["launchctl", "enable", `${domain}/${unit.name}`],
|
|
148
158
|
["launchctl", "bootstrap", domain, unit.path],
|
|
149
159
|
],
|
|
150
|
-
stop: (unit) => [
|
|
151
|
-
["launchctl", "bootout", `${domain}/${unit.name}`],
|
|
152
|
-
["launchctl", "disable", `${domain}/${unit.name}`],
|
|
153
|
-
],
|
|
160
|
+
stop: (unit) => [["launchctl", "bootout", `${domain}/${unit.name}`]],
|
|
154
161
|
state: async (unit) => {
|
|
162
|
+
const enabled = launchdEnabled((await system.run(["launchctl", "print-disabled", domain], env)).stdout, unit.name);
|
|
155
163
|
const ran = await system.run(["launchctl", "print", `${domain}/${unit.name}`], env);
|
|
156
164
|
if (ran.code !== 0)
|
|
157
|
-
return { loaded: false, active: false };
|
|
165
|
+
return { loaded: false, active: false, enabled };
|
|
158
166
|
const state = /^\s*state = (.+)$/m.exec(ran.stdout)?.[1]?.trim();
|
|
159
167
|
const pid = Number(/^\s*pid = (\d+)$/m.exec(ran.stdout)?.[1]);
|
|
160
168
|
const exitCode = Number(/^\s*last exit code = (\d+)/m.exec(ran.stdout)?.[1]);
|
|
161
169
|
return {
|
|
162
170
|
loaded: true,
|
|
163
171
|
active: state === "running",
|
|
172
|
+
enabled,
|
|
164
173
|
...(pid > 0 ? { pid } : {}),
|
|
165
174
|
...(exitCode > 0 ? { exitCode } : {}),
|
|
166
175
|
...(state ? { detail: state } : {}),
|
|
@@ -172,9 +181,14 @@ export const launchd = (app, system, env) => {
|
|
|
172
181
|
mkdirSync(dirname(unit.logPath), { recursive: true, mode: 0o700 });
|
|
173
182
|
},
|
|
174
183
|
logs: async (unit, lines) => tail(unit.logPath, lines),
|
|
175
|
-
atLogin: (unit) => `from then on it also starts at every login, until \`${app.command} ${unit.profile} server stop\``,
|
|
176
184
|
};
|
|
177
185
|
};
|
|
186
|
+
/** `launchctl print-disabled` lists overrides as `"label" => disabled` (older macOS: `=> true`); none is enabled. */
|
|
187
|
+
export const launchdEnabled = (listing, label) => {
|
|
188
|
+
const quoted = label.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
189
|
+
const value = new RegExp(`"${quoted}"\\s*=>\\s*(\\w+)`).exec(listing)?.[1];
|
|
190
|
+
return value !== "disabled" && value !== "true";
|
|
191
|
+
};
|
|
178
192
|
/**
|
|
179
193
|
* serve's log lines for a person: a log record as `time severity body`, anything else — a line from an
|
|
180
194
|
* older serve, the summary it prints on stop — as it is. A journal's own prefix is kept.
|
|
@@ -207,9 +221,10 @@ export const noUnits = (app, system) => {
|
|
|
207
221
|
text: refuse,
|
|
208
222
|
start: refuse,
|
|
209
223
|
stop: refuse,
|
|
224
|
+
install: refuse,
|
|
225
|
+
uninstall: refuse,
|
|
210
226
|
state: async () => ({ loaded: false, active: false }),
|
|
211
227
|
logs: async () => "",
|
|
212
|
-
atLogin: () => "",
|
|
213
228
|
};
|
|
214
229
|
};
|
|
215
230
|
export const platformFor = (app, system, env) => {
|
package/dist/calendar/ics.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { EntryStatus, EntryTransparency, ParticipantResponse } from "../mee
|
|
|
2
2
|
/**
|
|
3
3
|
* The part of iCalendar (RFC 5545, https://www.rfc-editor.org/rfc/rfc5545) the calendar commands read and write: one
|
|
4
4
|
* VCALENDAR of VEVENTs, with no VTIMEZONE. Times are written in UTC; a time read with a TZID keeps that zone's name,
|
|
5
|
-
* which the provider takes as it is.
|
|
5
|
+
* which the provider takes as it is. A Windows zone name, as Outlook and Exchange write it, is read as its IANA zone.
|
|
6
6
|
*/
|
|
7
7
|
export interface IcsEvent {
|
|
8
8
|
uid: string;
|
package/dist/calendar/ics.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { CliError } from "@wirecat/cli-base";
|
|
2
2
|
import { zonedMoment } from "@wirecat/cli-base/query";
|
|
3
|
+
import { WINDOWS_ZONES } from "./windows-zones.generated.js";
|
|
3
4
|
const CRLF = "\r\n";
|
|
4
5
|
const LINE_OCTETS = 75;
|
|
5
6
|
const escapeText = (text) => text
|
|
@@ -129,6 +130,8 @@ const contentLine = (line) => {
|
|
|
129
130
|
const unescapeText = (text) => text.replace(/\\([\\;,nN])/gu, (_match, char) => (char === "n" || char === "N" ? "\n" : char));
|
|
130
131
|
const DATE = /^(\d{4})(\d{2})(\d{2})$/u;
|
|
131
132
|
const DATE_TIME = /^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})(Z?)$/u;
|
|
133
|
+
/** `W. Europe Standard Time` → `Europe/Berlin`, CLDR's zone for the name worldwide; any other TZID as written. */
|
|
134
|
+
const ianaZone = (tzid) => (Object.hasOwn(WINDOWS_ZONES, tzid) ? WINDOWS_ZONES[tzid] : tzid);
|
|
132
135
|
/** A DATE or DATE-TIME: UTC with `Z`, in its TZID, or floating, read in `zone`. */
|
|
133
136
|
const momentOf = (line, zone) => {
|
|
134
137
|
const value = line.value.trim();
|
|
@@ -146,7 +149,7 @@ const momentOf = (line, zone) => {
|
|
|
146
149
|
if (utc)
|
|
147
150
|
return { at: Date.parse(`${local}Z`), zone: "UTC" };
|
|
148
151
|
const named = line.params.TZID;
|
|
149
|
-
const where = named
|
|
152
|
+
const where = named === undefined ? zone : ianaZone(named);
|
|
150
153
|
try {
|
|
151
154
|
return { at: zonedMoment(local, where, { start: 0, end: local.length }), zone: where };
|
|
152
155
|
}
|
|
@@ -231,7 +234,7 @@ const eventOf = (lines, zone, overrides = false) => {
|
|
|
231
234
|
const recurrence = lines
|
|
232
235
|
.filter(({ name }) => ["RRULE", "RDATE", "EXDATE"].includes(name))
|
|
233
236
|
.map(({ name, params, value }) => {
|
|
234
|
-
const kept = Object.entries(params).map(([key, param]) => `;${key}=${paramValue(param)}`);
|
|
237
|
+
const kept = Object.entries(params).map(([key, param]) => `;${key}=${paramValue(key === "TZID" ? ianaZone(param) : param)}`);
|
|
235
238
|
return `${name}${kept.join("")}:${value.trim()}`;
|
|
236
239
|
});
|
|
237
240
|
if (recurrence.length)
|