@wirecat/cli-core 0.548.0 → 0.550.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 +232 -0
- package/dist/calendar/cli/index.d.ts +10 -1
- package/dist/calendar/cli/index.js +9 -3
- package/dist/calendar/cli/search-all.d.ts +17 -0
- package/dist/calendar/cli/search-all.js +72 -0
- package/dist/cli/answer-entity.d.ts +4 -0
- package/dist/cli/answer-entity.js +5 -0
- package/dist/cli/bot/context.d.ts +2 -0
- package/dist/cli/compact.d.ts +41 -0
- package/dist/cli/compact.js +313 -0
- package/dist/cli/context.d.ts +11 -2
- package/dist/cli/context.js +15 -6
- package/dist/cli/envelope.d.ts +5 -1
- package/dist/cli/envelope.js +15 -4
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +2 -0
- package/dist/cli/messenger/search-command.d.ts +10 -0
- package/dist/cli/messenger/search-command.js +19 -13
- package/dist/cli/messenger/store-maintenance-command.d.ts +1 -1
- package/dist/cli/program.d.ts +3 -0
- package/dist/cli/program.js +7 -1
- package/dist/domain/providers.d.ts +3 -0
- package/dist/domain/providers.js +20 -4
- package/dist/domain/ref-arguments.js +3 -7
- package/dist/domain/ref-summaries.d.ts +56 -0
- package/dist/domain/ref-summaries.js +63 -0
- package/dist/entities/index.d.ts +11 -0
- package/dist/entities/relationship.d.ts +2 -0
- package/dist/entities/relationship.js +2 -0
- package/dist/entities/task-results.d.ts +10 -0
- package/dist/entities/task-results.js +1 -0
- package/dist/errors/reasons.js +2 -0
- package/dist/mail/partial.d.ts +9 -3
- package/dist/mail/partial.js +35 -10
- package/dist/mail/source.d.ts +7 -1
- package/dist/mail/sync.js +1 -1
- package/dist/output-detail.js +9 -5
- package/dist/output.d.ts +5 -1
- package/dist/output.js +29 -4
- package/dist/services/proposals/kinds/relationship-update.d.ts +4 -0
- package/dist/services/proposals/kinds/relationship-update.js +53 -0
- package/dist/services/proposals/registry.js +2 -0
- package/dist/store/decisions.d.ts +3 -1
- package/dist/store/decisions.js +7 -1
- package/dist/store/index.d.ts +2 -1
- package/dist/store/sqlite/email-locations.d.ts +22 -1
- package/dist/store/sqlite/email-locations.js +29 -2
- package/dist/store/sqlite/emails.d.ts +3 -7
- package/dist/store/sqlite/emails.js +4 -2
- package/dist/store/sqlite/evidence-sources.d.ts +2 -2
- package/dist/store/sqlite/evidence-sources.js +41 -77
- package/dist/store/sqlite/knowledge.d.ts +17 -23
- package/dist/store/sqlite/knowledge.js +13 -13
- package/dist/store/sqlite/manifest.js +2 -0
- package/dist/store/sqlite/migrations.generated.js +12 -0
- package/dist/store/sqlite/notes.d.ts +5 -0
- package/dist/store/sqlite/notes.js +6 -2
- package/dist/store/sqlite/open-store.js +2 -0
- package/dist/store/sqlite/proposed-actions.d.ts +1 -1
- package/dist/store/sqlite/proposed-actions.js +10 -2
- package/dist/store/sqlite/ref-summaries.d.ts +17 -0
- package/dist/store/sqlite/ref-summaries.js +378 -0
- package/dist/store/sqlite/relation-proposals.d.ts +40 -0
- package/dist/store/sqlite/relation-proposals.js +88 -1
- package/dist/store/sqlite/schema.d.ts +16 -0
- package/dist/store/sqlite/schema.js +1 -0
- package/dist/store/sqlite/suggested-links.d.ts +10 -0
- package/dist/store/sqlite/suggested-links.js +68 -0
- package/dist/store/store.d.ts +3 -0
- package/dist/store/store.js +5 -0
- package/package.json +3 -3
- package/parity.json +4 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,238 @@ shape of this file.
|
|
|
7
7
|
|
|
8
8
|
## Unreleased
|
|
9
9
|
|
|
10
|
+
## 0.550.0 — 02.11.2026
|
|
11
|
+
|
|
12
|
+
### Changed — may break callers
|
|
13
|
+
|
|
14
|
+
- **cli-core registers `mail.search-refused`.** A CLI that listed it in its own reasons gets "listed twice" from
|
|
15
|
+
`defineReasons` and must drop its entry; mail does.
|
|
16
|
+
- **`MailStore` has `markPartial(accountId, externalId, mark)`, and `partialEmails` answers `stuck` and leaves out an
|
|
17
|
+
email marked `partial.stuck`.** A store of its own must add both. `PartialOutcome` (`syncMail`'s `partial`) gains
|
|
18
|
+
`stuck`, and `partial` is answered whenever any of `completed`, `left` or `stuck` is not 0.
|
|
19
|
+
- **cli-core now needs `@wirecat/cli-base` 0.74.0**, which holds the `--compact` switch's name. A CLI that pins
|
|
20
|
+
cli-base bumps it to 0.74.0 together with cli-core.
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
- **A partial email the server no longer gives at its stored place is given up on instead of read whole on every
|
|
25
|
+
sync.** `completePartial` reads it at its newest stored location under that location's UIDVALIDITY (the new optional
|
|
26
|
+
`Quick.validity`, which a source checks before reading), counts each failed read in `metadata.partial.tries`, and
|
|
27
|
+
after 3, or at once when the place holds another message, marks it `partial.stuck`: it stays readable as it is, is
|
|
28
|
+
counted in `partial.stuck`, and no longer takes one of the 100 places a run reads, so it never holds back newer ones.
|
|
29
|
+
An email with no stored location is given up on at once.
|
|
30
|
+
- **A server search never stores a partial copy over an email already stored whole.** A sync in another process could
|
|
31
|
+
save the email between the search's check and its write; the write then dropped every attachment. The store now
|
|
32
|
+
keeps a live whole email as it is when a copy marked `partial` is saved over it.
|
|
33
|
+
- **`--brief` now cuts a messenger search hit.** It only knew mail's camelCase hits, so `search messages --brief` in
|
|
34
|
+
tg, max and wa printed every field; it now keeps `ref`, `chat_title`, `timestamp`, `sender_name`, `excerpt` and
|
|
35
|
+
`score`. The default and `--full` answers are unchanged. Commands that print through `outputFor` now honour
|
|
36
|
+
`--brief` and `--full` too, and refuse them together as the others do.
|
|
37
|
+
|
|
38
|
+
### Added
|
|
39
|
+
|
|
40
|
+
- **`--compact`: one line per item for an agent, from the same answer `--json` prints.** `messages list <chat>
|
|
41
|
+
--compact` prints `#<id> · 10-01 09:12 · Анна · <text>` under one header line that names the shared ref prefix
|
|
42
|
+
(`ref: msg:max/500/-1001/<id>`), what every row has in common (the chat's title), the date span and the zone. Rows
|
|
43
|
+
from several chats keep their whole refs. Text stays on one line with control and direction characters made
|
|
44
|
+
visible, and is cut at 400 characters with `…(+N)`; `--full` keeps it whole. The paging, `missing`, the next steps
|
|
45
|
+
and an incomplete answer follow as `# more:`, `# missing:`, `# next:` and `# complete: false` lines on stdout, and
|
|
46
|
+
each command line in them carries `--compact` again. On 1 000 messages of one chat it is about 48 % of the JSON
|
|
47
|
+
bytes; the texts alone are 40 %. `--compact` with `--json`, `--json=…`, `--jsonl` or `--brief` fails with
|
|
48
|
+
`validation_error` (`output.conflict`); it never prompts, and exits with the code JSON would. MCP keeps JSON.
|
|
49
|
+
- **Every command that answers through `renderAnswer` gets it.** A command draws with its own row
|
|
50
|
+
(`answerEntity(cmd, name, schema, { row })`), its app's (`rows: { name: shape }` in the program definition,
|
|
51
|
+
which also draws an app entity listed inside one thing), the shared one in `SHARED_ROWS` (`message`,
|
|
52
|
+
`message.summary`, `message.hit`, `chat`), or the generic row: key, ref or id · title, name or path · state,
|
|
53
|
+
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,
|
|
54
|
+
options)` is pure, for an app that prints its own answers.
|
|
55
|
+
- What a command prints without `renderAnswer` stays JSON under `--compact`.
|
|
56
|
+
|
|
57
|
+
## 0.549.0 — 02.11.2026
|
|
58
|
+
|
|
59
|
+
### Added
|
|
60
|
+
|
|
61
|
+
- **A calendar CLI can mount `search all`.** Given `app` (its identity) beside `messages`, `addCalendarCommands` adds
|
|
62
|
+
`search all` beside `search events`: calendar events, messages from the other WireCat tools and notes in the shared
|
|
63
|
+
store, and a meeting account's transcripts only when `--source` names one, ranked together as a messenger's
|
|
64
|
+
`search all` ranks them. It takes the same `--only`, `--source`, `--max-meetings`, `--limit`, `--page`, `--exact`
|
|
65
|
+
and `--timezone` (`searchAllOptions`, now shared with the messenger's), refuses mail as the messenger's does, and
|
|
66
|
+
answers each account it read in `meta.accounts`. Without a messenger server it reads the local store only. A process
|
|
67
|
+
locked to one profile reads only its own calendar and refuses `--source`. `CalendarCommandDeps` gains `app`, `note`
|
|
68
|
+
(the "searched:" and "not searched:" lines for a person) and a `view` argument to `write`, the text a person reads.
|
|
69
|
+
- **A server's refused SEARCH fails that folder, not the run, and proves nothing.** A mail source throws
|
|
70
|
+
`mail.search-refused` (`SEARCH_REFUSED`, `searchRefused`) when the server answers SEARCH with NO or BAD. `syncMail`
|
|
71
|
+
reads the other folders, answers `state: "partial"` with `refusedFolders`, and for the refused folder marks no mail
|
|
72
|
+
deleted, changes no membership, moves no resume point and leaves `mail_coverage` as it was. Before, a mail source that
|
|
73
|
+
read the refusal as "nothing found" made the folder look empty.
|
|
74
|
+
- **Server search reads big mail text-first, and the next sync completes it.** A candidate is read up to
|
|
75
|
+
`SERVER_BOUNDS.textBytes` (256 KiB, `fetchRaw(items, { bytes })`); one cut short is stored with its text and no
|
|
76
|
+
attachments, marked `metadata.partial`, and `meta.issues` says `search.server-partial`. `syncMail` then reads up to
|
|
77
|
+
100 partial emails whole and clears the mark (`partial: { completed, left }`; `completePartial: false` skips it).
|
|
78
|
+
`MailStore.partialEmails` lists them. No store migration: the mark is a JSON key older builds ignore.
|
|
79
|
+
- **`readClientFile` (from `@wirecat/cli-core/oauth`) reads a user's own Google Desktop OAuth client file** (the
|
|
80
|
+
`{ "installed": { client_id, client_secret } }` JSON from Google's console). It refuses a file other users can read
|
|
81
|
+
(`sign-in.client-file-open`) and a file of the wrong shape (`sign-in.client-file-invalid`), and never puts the file's
|
|
82
|
+
contents in an error. gcal, drive and mail each kept a copy of this reader.
|
|
83
|
+
- **`openBrowser(url, browser?)` is exported from `@wirecat/cli-core/cli`**, so a CLI that does not start through
|
|
84
|
+
`startCli` opens the sign-in page the same way: `open`, `rundll32` or `xdg-open`, or the named browser.
|
|
85
|
+
- **iCalendar feeds as read-only calendars.** With `deps.feeds` (`icsFeeds`), `calendars add <url> [--title]` follows a
|
|
86
|
+
public holiday calendar, a sports schedule or a published `.ics` address, and `calendars remove <calendar>` stops.
|
|
87
|
+
`sync` reads each feed after the provider (`syncWithFeeds`), so its events show in `events list`,
|
|
88
|
+
`search events`, `overlapping:` and `events remind`. Repeating events are expanded here (`expandSeries`: RRULE in
|
|
89
|
+
the event's own zone, RDATE, EXDATE and overrides). The address is a secret input, kept by the CLI and never
|
|
90
|
+
printed; a fetch follows redirects only to https and stops at 10 MB and 30 s. `readIcs` takes
|
|
91
|
+
`{ overrides: true }` and returns the calendar's `name`. See `docs/dev/CALENDARS.md`, "Feeds".
|
|
92
|
+
- **History limits for pushed history, per profile.** A messenger whose history is pushed (`history: "store"`,
|
|
93
|
+
WhatsApp) saves one-to-one chats back `history_dialog_days` (1095 by default), groups of more than two people back
|
|
94
|
+
`history_group_days` (365) and nothing of a group above `history_group_members_max` people (50); at most 1095 days.
|
|
95
|
+
The chat row stays, so the chat is still listed. Live messages, `messages sync` and what the store already holds are
|
|
96
|
+
never dropped. `messages sync <chat> --full-history` fetches a chat and keeps its pushed history in full from then on
|
|
97
|
+
(`history_included` in the answer). `ChatsStore` gains `historySkips`, `historyShapes`, `recordHistorySkips` and
|
|
98
|
+
`setHistoryIncluded`; `Saving` takes `historyLimits`.
|
|
99
|
+
- **Helpers for reading money and questions in a chat, for the project status work.** `amountsOf(text)` returns
|
|
100
|
+
every amount in a text, in order (`amountOf` is its first). `MONEY_REQUEST`, `MONEY_DONE` and `PROMISE` word lists
|
|
101
|
+
(ru, en, es) with `moneyClassesOf(text)` say which a text uses. `questions(messages, { asker: "owner" })` lists the
|
|
102
|
+
owner's own questions and whether anyone but the owner answered them, by a reply or by speaking next; the default,
|
|
103
|
+
`"others"`, is what it did before. `linkedChatsOf` is exported, now with each link's `role`, and so is
|
|
104
|
+
`AGENDA_DEFAULTS`, the silence `tasks agenda` waits before it lists a chat.
|
|
105
|
+
- **A command's own history handler learns whether each batch reached the store**: `HistoryBatchHandler` takes
|
|
106
|
+
`saved`, a promise of the same `true` or `false` the messenger gets, so a link can count only what was kept.
|
|
107
|
+
- **Every messenger command that takes a chat or a message also takes the reference other WireCat CLIs print**:
|
|
108
|
+
`chat:<provider>/<account>/<chat>` wherever a chat is named (`messages list chat:max/34871122/63488582`, bot
|
|
109
|
+
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
|
|
110
|
+
account is refused, naming the account it belongs to and the profiles of this CLI the store saw on it; it never
|
|
111
|
+
switches account. One of another messenger is refused with the command that opens it (`open it with tg <profile>
|
|
112
|
+
messages list chat:telegram/…`), and another kind (`document:413` given as a chat) with what the argument takes.
|
|
113
|
+
- **`event:<id>` and `meeting:<account>/<meeting>[/<transcript>[/<cue>]]` parse with `parseReference`** and
|
|
114
|
+
`canonicalReference`, as every other kind: `event:N` was printed by the calendar and person answers and could not be
|
|
115
|
+
read back. The kinds are one table, `REF_KINDS`; `looksLikeReference(text)` tells a reference from a title such as
|
|
116
|
+
`Note: plan`. Reference errors carry a reason: `ref.malformed`, `ref.unknown-kind` (with the kind meant:
|
|
117
|
+
`"doc:413" … did you mean document:413?`), `ref.wrong-kind`, `ref.wrong-provider`, `ref.wrong-account`.
|
|
118
|
+
- **`messages list` reads a window bounded on both sides, offline too.** The query takes a start and an end,
|
|
119
|
+
`'date>=2026-10-01 date<2026-10-20'` or `'date:[2026-10-01 TO 2026-10-19]'`, and `id>` with `date<`. A start reads
|
|
120
|
+
forward, oldest first, and stops at the end; an end alone reads back from it, newest page first, as before.
|
|
121
|
+
`--offline` and a messenger whose history is read from the store (WhatsApp) now answer both ways from the store
|
|
122
|
+
instead of refusing. A forward cursor carries the end, `after:<id>~<epoch ms>`, so the next page stops at the same
|
|
123
|
+
place. `--since`, `--since-time`, `--until`, `--until-time`, `--after-time` and `--before-time` on `messages list`
|
|
124
|
+
fail with the query form to type instead. The store's `messages()` window takes `after` (a message or a moment)
|
|
125
|
+
and `until`.
|
|
126
|
+
- **One proposal inbox behind one check: `proposalRegistry(store, policy)`, `ProposalKind`, `parseProposalIds`
|
|
127
|
+
(`@wirecat/cli-core/services`) and `mayDecide` (`@wirecat/cli-core/store`).** The registry lists every pending
|
|
128
|
+
proposal newest first across kinds (`relationship`, `key_document`, `fact`, `memory`, `findings` for evidence
|
|
129
|
+
findings, and `action` for every other stored proposal), and decides many ids at once, each on its own: the answer
|
|
130
|
+
lists `decided` and `refused`, and one refusal never undoes the others. A new kind registers itself with its id
|
|
131
|
+
`prefix` or the `stored` proposal kinds it owns; nothing else changes. `parseProposalIds` reads
|
|
132
|
+
`17070-17079,fact:500-502,17080/f2` and refuses a range across prefixes. Unconfirmed agent and rule links show as
|
|
133
|
+
`link:<id>` (relationships) and `key:<id>` (key documents) until the store moves them.
|
|
134
|
+
- **`knowledge.proposeRelation(scope, input, by)`**: an agent's or a rule's relationship is a stored `relationship`
|
|
135
|
+
proposal for the owner, not a link; the same pending pair returns the proposal already held. Removing a link to a
|
|
136
|
+
project, or rejecting any suggested link, leaves a rejected `relationship` proposal, so a later scan can skip it.
|
|
137
|
+
- **`proposedActions.list({ kinds, except, project, proposedBy })`**, `proposedBy` on `facts.list` and
|
|
138
|
+
`memories.list`, `order: "created"` on `facts.list`, and the bot's `kind` (`agent` or `rule`) on the author of a
|
|
139
|
+
proposed action, a fact, a memory and an evidence findings batch; bots may have the kind `rule`, and an `Author` may
|
|
140
|
+
name it.
|
|
141
|
+
- **`knowledge.decideLinkProposal(id, decision)`** decides a stored `relationship` or `key_document` proposal, and
|
|
142
|
+
**`knowledge.decideSuggestedLink(id, decision)`** an older unconfirmed link, each in one transaction: the link and
|
|
143
|
+
the decision land together or not at all. A decision's `reason` is kept: on a rejected proposal as its result, on a
|
|
144
|
+
fact in `metadata.decision`, on an evidence findings receipt.
|
|
145
|
+
- **`store.refs.summaries(refs, { excerpt? })` says what any ref names**, a page at a time: kind, title, what holds
|
|
146
|
+
it, when, provider, account, the CLI that opens it and whether it is `available`, `deleted` or `unavailable`. One
|
|
147
|
+
statement per kind present, offline. A malformed ref comes back `unknown`, never thrown, so one bad row cannot fail
|
|
148
|
+
a page. Titles hold no message text; `{ excerpt: true }` adds at most 280 characters. Read once per page.
|
|
149
|
+
- **`refLabel(summary)` and `openOf(summary, { profilesFor, local? })`** (`./cli`): the one label
|
|
150
|
+
(`Chat «Title» (MAX)`) and the command that opens a ref. `openOf` never picks a profile: with none or several,
|
|
151
|
+
`argv` keeps `<profile>` and `profiles` lists the choices. Kinds no CLI owns open with the caller's `local` words.
|
|
152
|
+
- **`ProviderEntry.opens`**: the read command that opens each kind of ref (`{ document: "documents show" }`). A CLI
|
|
153
|
+
writes it with `registerProvider`; tg, max and wa open `chat`, `message` and `contact` from the built-ins. A kind
|
|
154
|
+
this build does not know is dropped, so an older build still reads a newer entry.
|
|
155
|
+
- **A relationship's purpose (store version 44).** `links.purpose`, read as `purpose` on a relationship, a link and
|
|
156
|
+
the `relationship` entity, says what a chat is for in a project; `null` means `work`. The store keeps no list of
|
|
157
|
+
purposes: the caller checks one against its config. `relate` and `proposeRelation` take `purpose`.
|
|
158
|
+
- **`knowledge.updateRelation(scope, id, { role?, purpose?, by })`** sets a relationship's role and purpose; `null`
|
|
159
|
+
clears one. The owner's change applies; an agent's is a `relationship_update` proposal, listed and decided through
|
|
160
|
+
the proposal registry, and `knowledge.decideRelationUpdate(id, decision)` decides it in one transaction.
|
|
161
|
+
|
|
162
|
+
### Changed — may break callers
|
|
163
|
+
|
|
164
|
+
- **`SERVER_BOUNDS.maxMessageBytes`, `ServerSearched.tooLarge` and `search.server-too-large` are gone**: a big
|
|
165
|
+
candidate is no longer left on the server; see `textBytes`, `ServerSearched.partial` and `search.server-partial`.
|
|
166
|
+
- **An option after `--` is read as the option.** `messages sync -- -100 --json` now syncs `-100` and answers in JSON;
|
|
167
|
+
it failed with "too many arguments". After `--`, a word that is exactly one of the command's options, its own or a
|
|
168
|
+
global one (`--json`, `--limit 5`, `--json=id`), moves in front of `--`; every other word stays a positional. A
|
|
169
|
+
negative id needs no `--` at all. Commands that take free text keep every word after `--` literal: `messages send` and
|
|
170
|
+
`edit` (also the bot's), `polls create`, `topics create`, `chats create`, `chats folders create`, `contacts rename`,
|
|
171
|
+
`contacts aliases set`, `messages press`, `bot commands set`, `schedule command`, `events remind`, `complete`, and the
|
|
172
|
+
notes templates. Mark such a command with `annotate(command, { literal_after_dashes: true })`; MCP calls are never
|
|
173
|
+
lifted. A script that passed the literal word `--json` as a positional of another command now gets the option instead.
|
|
174
|
+
- **A write to a calendar the account may only read refuses before anything is asked or journaled**
|
|
175
|
+
(`calendar.read-only`): create, quick add, import, update, delete, respond and move, for a feed and for a provider's
|
|
176
|
+
`reader` or `free-busy` calendar alike. Before, only the target of a move was checked; the provider refused the rest.
|
|
177
|
+
`calendars busy` and `search --backend server` leave feed calendars out.
|
|
178
|
+
- **A locator of another account or messenger is refused with a new text and a reason.** `messages show`, `context`,
|
|
179
|
+
`link` and `download` said "that locator belongs to another account; select its profile first" or "…belongs to
|
|
180
|
+
another messenger"; they now say `<ref> belongs to account <id>; profile <name> is account <id>` (`ref.wrong-account`)
|
|
181
|
+
or `<ref> is a <messenger> message; open it with …` (`ref.wrong-provider`). A script matching the old text matches
|
|
182
|
+
the reason instead.
|
|
183
|
+
- **A text of the form `<kind>:<no spaces>` given as a chat is read as a reference**, so a chat titled `note:plan` is
|
|
184
|
+
no longer found by that title; give its id or a part of the title.
|
|
185
|
+
- The provider seeds of `telegram`, `max` and `whatsapp` name their CLI (`command: tg`, `max`, `wa`), which a
|
|
186
|
+
provider clash message now shows (`telegram (messages, from tg)`).
|
|
187
|
+
- **Every decide method takes who decides, a `Decider`** (`"owner"` or `{ bot, model?, reason }`):
|
|
188
|
+
`facts.accept(ref, by)`, `facts.reject(ref, by)`, `memories.confirm(ref, by)`, `knowledge.confirmRelation(scope, id,
|
|
189
|
+
by)`, `knowledge.removeRelation(scope, id, by)`, `proposedActions.approve(id, by, result?)`,
|
|
190
|
+
`proposedActions.reject(id, by)`, `approveAsTask(store, id, by)`, and `by` is now required in
|
|
191
|
+
`evidenceFindings.review` and `meetingFindings.review`. `DocumentKnowledge.fact` and `.memory` take `by` for a
|
|
192
|
+
`confirm`, and `memories.markStale(ref, by)` and `.memory(…, "stale", by)` take it too: marking an agent's memory
|
|
193
|
+
stale is how it is turned down. `knowledge.relate` needs `by`: the owner's relationship is a link, an agent's is a
|
|
194
|
+
`relationship` proposal it returns instead. `notes.addLink` refuses any confirmed link without `by: "owner"`, so an
|
|
195
|
+
agent writes `confirmed: false` (the task service now does). `notes.removeLink(id, by)`, `facts.reverse(ref, by)`,
|
|
196
|
+
`facts.remove(ref, by)` and `memories.remove(ref, by)` take who removes; the document port's `addMemory` needs
|
|
197
|
+
`author`, `addFact` and `addLink` need `by`, with no owner default. `proposalRegistry(store)` takes no policy: the
|
|
198
|
+
host sets it once with `openStore({ decisions })`, and `store.decisionPolicy` is what the registry and every store
|
|
199
|
+
check read. A host passes the owner only when the owner runs the command, and
|
|
200
|
+
the agent's bot and reason otherwise.
|
|
201
|
+
An agent's confirmation of a relationship is recorded in the link's provenance, and of a fact or proposal as its
|
|
202
|
+
decider.
|
|
203
|
+
- **Evidence sources read through `store.refs`.** `memories.sources`, `facts.sources` and `evidenceFindings.sources`
|
|
204
|
+
keep their shape; a contact now says `kind: "contact"` (was `identity`), a mail ref the store lacks says `email`
|
|
205
|
+
(was `message`), and kinds that had no title before (person, project, task, chat…) now have one.
|
|
206
|
+
- **Unconfirmed agent and rule links move into proposals (store version 44).** The first writable open after the
|
|
207
|
+
upgrade turns each into a `relationship` or `key_document` proposal with its bot, reason and time, and deletes the
|
|
208
|
+
link; Drive's editor links stay. Only the owner decides a moved one, since its bot came from free text. An older
|
|
209
|
+
build no longer lists or confirms these as relationships, and a project transfer no longer carries them. Decide them
|
|
210
|
+
through the proposal registry by their new ids; `link:<id>` and `key:<id>` remain only for suggestions an older
|
|
211
|
+
build writes later.
|
|
212
|
+
|
|
213
|
+
### Fixed
|
|
214
|
+
|
|
215
|
+
- **A sum written in groups of three read as the wrong amount: `50 000 ₽` was `0` and `4 300 ₽` was `300`**, because
|
|
216
|
+
only the last group was read. Groups after a space, a no-break or thin space, a dot or a comma (`50 000`,
|
|
217
|
+
`1.200 €`, `$1,200`) are one number now, a decimal part after them is kept (`5 000.50 ₽`), and amounts keep two
|
|
218
|
+
decimals instead of a whole number (`15.5 eur` was 16). This is the amount the duplicate check compares between
|
|
219
|
+
two tasks, so a reminder worded with a grouped sum is no longer compared on the wrong figure.
|
|
220
|
+
- **`page.next` of `messages list` and `messages comments` runs as written.** It repeated the query's `id`/`date`
|
|
221
|
+
bound beside `--cursor`, which the command refuses as two starting points; it now keeps only `topic:` and
|
|
222
|
+
`starred:`.
|
|
223
|
+
- **An inclusive start keeps a message sent exactly at it.** `messages list 'date>=2026-10-01'` skipped a message
|
|
224
|
+
sent at the first millisecond of the day, and `date<="…"` one sent at that moment.
|
|
225
|
+
|
|
226
|
+
### Security
|
|
227
|
+
|
|
228
|
+
- **An agent can no longer decide its own proposal.** Before, `facts.accept`/`reject`,
|
|
229
|
+
`memories.confirm`, `knowledge.confirmRelation`, `proposedActions.approve`/`reject`, `approveAsTask` and both
|
|
230
|
+
findings reviews never asked who decided, so over MCP an agent could confirm what it had proposed itself. Every one
|
|
231
|
+
of them, and the registry, now runs `mayDecide`: the owner always may; an agent needs a reason
|
|
232
|
+
(`proposal.reason-required`), is refused its own bot's proposal (`proposal.own`), a rule scorer's proposal and one
|
|
233
|
+
that names no author (`proposal.owner-only`), and every decision when the host turns agent decisions off
|
|
234
|
+
(`proposals.agent-decisions-off`). An agent can no longer add a memory already `confirmed`, mark its own memory
|
|
235
|
+
stale, write a confirmed project link itself, or remove a link. A link records only free-text provenance, which
|
|
236
|
+
anyone can write, so only the owner decides an unconfirmed link or removes a link; the rejected record a removal
|
|
237
|
+
leaves names who removed it. Findings an agent accepted for the owner record that agent, not the owner. Only the
|
|
238
|
+
owner removes or reverses an accepted fact or another bot's memory; an agent withdraws only its own row while it is
|
|
239
|
+
proposed. Turning agent decisions off now holds for every store method, not only the registry, and a bot is the
|
|
240
|
+
same bot whatever its letter case (`Claude-Code` is `claude-code`).
|
|
241
|
+
|
|
10
242
|
## 0.548.0 — 02.11.2026
|
|
11
243
|
|
|
12
244
|
### Added
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type Command } from "commander";
|
|
2
2
|
import type { EventStore } from "../../meetings/index.js";
|
|
3
|
+
import type { AppIdentity } from "../../ports/app.js";
|
|
3
4
|
import type { AccountKey, MessageStore } from "../../store/store.js";
|
|
4
5
|
import { type CalendarFeeds } from "../feeds.js";
|
|
5
6
|
import type { CalendarExpander, CalendarSource } from "../source.js";
|
|
@@ -39,7 +40,15 @@ export interface CalendarCommandDeps {
|
|
|
39
40
|
embedded: number;
|
|
40
41
|
left: boolean;
|
|
41
42
|
}>;
|
|
42
|
-
|
|
43
|
+
/** `view` is the text a person reads in place of the default rendering, where a command has one. */
|
|
44
|
+
write: (value: unknown, format: "json" | "jsonl" | "text", view?: () => string) => void;
|
|
45
|
+
/** A line for the person beside the answer, never in it: which accounts `search all` read and which it skipped. */
|
|
46
|
+
note?: (text: string) => void;
|
|
47
|
+
/**
|
|
48
|
+
* The CLI itself. With it and `messages`, `search` also has `all`: messages, notes, events and named meetings in the
|
|
49
|
+
* shared store, as a messenger's `search all`.
|
|
50
|
+
*/
|
|
51
|
+
app?: AppIdentity;
|
|
43
52
|
now?: () => number;
|
|
44
53
|
/** The zone `today`, `week` and a bare date are read in; the computer's when absent. */
|
|
45
54
|
timezone?: string;
|
|
@@ -23,6 +23,7 @@ import { calendarStatus } from "../sync.js";
|
|
|
23
23
|
import { createEvent, createEventFromText, deleteEvent, EVENT_RESPONSES, IMPORT_MAX, importEvents, moveEvent, respondToEvent, updateEvent, } from "../writes.js";
|
|
24
24
|
import { CALENDARS_QUERY, declareCalendarAnswers, PEOPLE_QUERY } from "./answers.js";
|
|
25
25
|
import { addRemind } from "./remind.js";
|
|
26
|
+
import { addSearchAll } from "./search-all.js";
|
|
26
27
|
const LOCAL_READ = { remote: "none", local: "read" };
|
|
27
28
|
const LOCAL_WRITE = { remote: "none", local: "write" };
|
|
28
29
|
const FETCH = { remote: "read", local: "write" };
|
|
@@ -343,9 +344,14 @@ const addCalendarCommandsDefinition = (program, deps) => {
|
|
|
343
344
|
});
|
|
344
345
|
if (deps.writes)
|
|
345
346
|
addRemind(events, deps.writes, { deps, now, zone, print, example });
|
|
346
|
-
const
|
|
347
|
-
|
|
348
|
-
|
|
347
|
+
const app = deps.messages ? deps.app : undefined;
|
|
348
|
+
const searches = annotate(program
|
|
349
|
+
.command("search")
|
|
350
|
+
.description(app
|
|
351
|
+
? "find stored things by text: events, or all of messages, notes and events"
|
|
352
|
+
: "find stored things by text: events"), { effects: GROUP });
|
|
353
|
+
if (app)
|
|
354
|
+
addSearchAll(searches, { deps, app, zone, messages, example });
|
|
349
355
|
const search = example(annotate(searches.command("events"), { effects: deps.serverSearch ? LOCAL_OR_REMOTE_READ : LOCAL_READ })
|
|
350
356
|
.description(deps.serverSearch
|
|
351
357
|
? "find stored events by words, and by meaning when its model is downloaded; with --backend server, ask " +
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import type { AppIdentity } from "../../ports/app.js";
|
|
3
|
+
import type { CalendarCommandDeps } from "./index.js";
|
|
4
|
+
interface SearchAllHelpers {
|
|
5
|
+
/** Read through, never copied: a CLI's deps may be getters that work only once a profile is open. */
|
|
6
|
+
deps: CalendarCommandDeps;
|
|
7
|
+
app: AppIdentity;
|
|
8
|
+
zone: () => string;
|
|
9
|
+
messages: (what: string) => NonNullable<CalendarCommandDeps["messages"]>;
|
|
10
|
+
example: (command: Command, ...lines: string[]) => Command;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* `search all` beside `search events`: the messenger's shared search over the store, without a messenger server.
|
|
14
|
+
* Events are the calendar's own kind and always read; meeting transcripts only when `--source` names a meeting account.
|
|
15
|
+
*/
|
|
16
|
+
export declare const addSearchAll: (searches: Command, { deps, app, zone, messages, example }: SearchAllHelpers) => void;
|
|
17
|
+
export {};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { CliError } from "@wirecat/cli-base";
|
|
2
|
+
import { annotate } from "@wirecat/cli-base/commands";
|
|
3
|
+
import { Command } from "commander";
|
|
4
|
+
import { LOCAL_READ } from "../../cli/effects.js";
|
|
5
|
+
import { historySkipsOf } from "../../cli/messenger/history-gaps.js";
|
|
6
|
+
import { allAnswer, offsetOf, SOURCE_SEARCHED } from "../../cli/messenger/search-answers.js";
|
|
7
|
+
import { searchAllLine, searchAllOptions } from "../../cli/messenger/search-command.js";
|
|
8
|
+
import { SAVE_HELP } from "../../meetings/index.js";
|
|
9
|
+
import { meaningLane } from "../../services/messages-meaning.js";
|
|
10
|
+
import { searchAllWithMeetings } from "../../services/search-all-meetings.js";
|
|
11
|
+
const DEFAULT_LIMIT = 20;
|
|
12
|
+
/**
|
|
13
|
+
* `search all` beside `search events`: the messenger's shared search over the store, without a messenger server.
|
|
14
|
+
* Events are the calendar's own kind and always read; meeting transcripts only when `--source` names a meeting account.
|
|
15
|
+
*/
|
|
16
|
+
export const addSearchAll = (searches, { deps, app, zone, messages, example }) => {
|
|
17
|
+
const all = annotate(new Command("all"), { effects: LOCAL_READ, partial: true, output: { entity: "hit" } })
|
|
18
|
+
.summary("search stored messages, notes, calendar events and selected meeting transcripts")
|
|
19
|
+
.description("search calendar events, messages from the other WireCat tools and notes in the local store, and a meeting " +
|
|
20
|
+
"account's transcripts when --source names one — best match first; not mail (the separate memo CLI " +
|
|
21
|
+
"searches it: memo search mail). Use it when you do not know where something was written; search events " +
|
|
22
|
+
"reads the calendar with all its fields")
|
|
23
|
+
.argument("<query...>", 'strict Lucene query: words, "phrases", AND/OR/NOT, field groups and date ranges')
|
|
24
|
+
.addHelpText("after", SAVE_HELP)
|
|
25
|
+
.addHelpText("after", "--json: the list is items; each result's reference is ref, with its kind.");
|
|
26
|
+
searches.addCommand(searchAllOptions(all));
|
|
27
|
+
example(all, "search all roadmap", 'search all "quarterly review" --source telegram,gcal --json');
|
|
28
|
+
all.action(async (words, _opts, leaf) => {
|
|
29
|
+
const opts = leaf.opts();
|
|
30
|
+
if (deps.locked && opts.source !== undefined)
|
|
31
|
+
throw new CliError("permission_error", "this process is locked to one profile; search all reads its own calendar only, without --source", { reason: "profile.locked" });
|
|
32
|
+
const store = messages("search all");
|
|
33
|
+
const limit = opts.limit ?? DEFAULT_LIMIT;
|
|
34
|
+
const offset = offsetOf(opts.page, limit);
|
|
35
|
+
const env = deps.writes?.env ?? process.env;
|
|
36
|
+
const found = await searchAllWithMeetings(store, deps.account, {
|
|
37
|
+
text: words.join(" "),
|
|
38
|
+
limit,
|
|
39
|
+
...(offset === undefined ? {} : { offset }),
|
|
40
|
+
...(opts.exact ? { exact: true } : {}),
|
|
41
|
+
timezone: opts.timezone ?? zone(),
|
|
42
|
+
env,
|
|
43
|
+
only: opts.only ?? SOURCE_SEARCHED,
|
|
44
|
+
...(opts.source === undefined ? {} : { source: opts.source }),
|
|
45
|
+
...(opts.maxMeetings === undefined ? {} : { maxMeetings: opts.maxMeetings }),
|
|
46
|
+
...(deps.locked ? { scope: { events: [deps.account] } } : {}),
|
|
47
|
+
}, { app }, undefined, meaningLane({ env, messenger: { app }, store: async () => store }));
|
|
48
|
+
const note = deps.note ?? (() => undefined);
|
|
49
|
+
const globals = leaf.optsWithGlobals();
|
|
50
|
+
const format = globals.json ? "json" : globals.jsonl ? "jsonl" : "text";
|
|
51
|
+
if (format === "text")
|
|
52
|
+
note(`searched: ${found.accounts.map(({ provider, account }) => `${provider}:${account}`).join(", ") || "nothing"}`);
|
|
53
|
+
for (const { resource, source, reason } of found.skipped)
|
|
54
|
+
note(`${resource ?? source} not searched: ${reason}`);
|
|
55
|
+
if (found.notes?.meaningSkipped)
|
|
56
|
+
note(`notes by words only: ${found.notes.meaningSkipped}`);
|
|
57
|
+
if (found.events?.meaningSkipped)
|
|
58
|
+
note(`events by words only: ${found.events.meaningSkipped}`);
|
|
59
|
+
const again = {
|
|
60
|
+
query: words.join(" "),
|
|
61
|
+
...(opts.only === undefined ? {} : { only: opts.only.join(",") }),
|
|
62
|
+
...(opts.source === undefined ? {} : { source: opts.source }),
|
|
63
|
+
...(opts.maxMeetings === undefined ? {} : { max_meetings: opts.maxMeetings }),
|
|
64
|
+
...(opts.exact ? { exact: true } : {}),
|
|
65
|
+
...(opts.timezone === undefined ? {} : { timezone: opts.timezone }),
|
|
66
|
+
};
|
|
67
|
+
if (!found.items.length)
|
|
68
|
+
note("nothing found in what the local store holds");
|
|
69
|
+
const skips = await historySkipsOf(store, found.accounts).catch(() => []);
|
|
70
|
+
deps.write(allAnswer("search all", found, { limit, page: opts.page ?? 1, again }, opts.exact === true, skips), format, () => found.items.map(searchAllLine).join(""));
|
|
71
|
+
});
|
|
72
|
+
};
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import type { Command } from "commander";
|
|
2
2
|
import type * as v from "valibot";
|
|
3
|
+
import type { RowShape } from "./compact.js";
|
|
3
4
|
/** Provider-specific wire fields must not redefine an entity shared by every CLI. */
|
|
4
5
|
export declare const answerEntity: (command: Command, name: string, schema: v.GenericSchema, options?: {
|
|
5
6
|
project?: v.GenericSchema;
|
|
7
|
+
row?: RowShape;
|
|
6
8
|
}) => Command;
|
|
7
9
|
export declare const answerEntitySchema: (command: Command | undefined) => v.GenericSchema | undefined;
|
|
8
10
|
export declare const answerProjectionSchema: (command: Command | undefined) => v.GenericSchema | undefined;
|
|
11
|
+
/** The row a command declared with `answerEntity(…, { row })`, while it still answers that entity. */
|
|
12
|
+
export declare const answerRow: (command: Command | undefined) => RowShape | undefined;
|
|
@@ -29,3 +29,8 @@ export const answerProjectionSchema = (command) => {
|
|
|
29
29
|
const own = command && schemas.get(command);
|
|
30
30
|
return own && metaOf(command).output?.entity === own.name ? own.project : undefined;
|
|
31
31
|
};
|
|
32
|
+
/** The row a command declared with `answerEntity(…, { row })`, while it still answers that entity. */
|
|
33
|
+
export const answerRow = (command) => {
|
|
34
|
+
const own = command && schemas.get(command);
|
|
35
|
+
return own && metaOf(command).output?.entity === own.name ? own.row : undefined;
|
|
36
|
+
};
|
|
@@ -15,6 +15,8 @@ export declare const botContext: (command: Command, bot: BotMessenger) => {
|
|
|
15
15
|
format: import("@wirecat/cli-base").RenderFormat;
|
|
16
16
|
color: boolean;
|
|
17
17
|
streams: import("@wirecat/cli-base").Streams;
|
|
18
|
+
compact?: boolean;
|
|
19
|
+
full?: boolean;
|
|
18
20
|
env: NodeJS.ProcessEnv;
|
|
19
21
|
keyring?: import("@wirecat/cli-base").KeyringStore;
|
|
20
22
|
run: <T>(body: (events: import("../index.js").EventSink) => Promise<T>, options?: {
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { Command } from "commander";
|
|
2
|
+
import type { Body } from "./envelope.js";
|
|
3
|
+
/** How one column prints. `text` by default. */
|
|
4
|
+
export type RowValue = "text" | "time" | "day" | "count" | "files" | "reply" | "forward";
|
|
5
|
+
export interface RowColumn {
|
|
6
|
+
/** A snake_case field path in the item (`sender_name`, `last.timestamp`); of several, the first that is set. */
|
|
7
|
+
field: string | readonly string[];
|
|
8
|
+
as?: RowValue;
|
|
9
|
+
/** Printed for a field that is `true`, and after a count: `{ field: ["sender_is_me", "sender_name"], tag: "me" }`. */
|
|
10
|
+
tag?: string;
|
|
11
|
+
}
|
|
12
|
+
export interface RowShape {
|
|
13
|
+
/** Printed in order, joined by " · "; the first is the ref. A missing value is left out. */
|
|
14
|
+
columns: readonly RowColumn[];
|
|
15
|
+
/** The free text, printed last; the first non-empty field wins. */
|
|
16
|
+
text?: readonly string[];
|
|
17
|
+
/** An item answer's lists, drawn as rows of another entity: `{ items: "evidence_item" }`. */
|
|
18
|
+
sections?: Readonly<Record<string, string>>;
|
|
19
|
+
}
|
|
20
|
+
export interface CompactOptions {
|
|
21
|
+
/** IANA zone for times; the answer's `meta.timezone` wins. */
|
|
22
|
+
timezone: string;
|
|
23
|
+
/** Characters of text kept per row; 0 keeps all (`--full`). Default 400. */
|
|
24
|
+
textChars?: number;
|
|
25
|
+
}
|
|
26
|
+
export declare const TEXT_CHARS = 400;
|
|
27
|
+
/** Rows for cli-core's shared entities. */
|
|
28
|
+
export declare const SHARED_ROWS: Readonly<Record<string, RowShape>>;
|
|
29
|
+
/** Any record: what names it, what it is called, what state it is in, then what it says. */
|
|
30
|
+
export declare const GENERIC_ROW: RowShape;
|
|
31
|
+
export type RowsResolver = (command: Command | undefined) => RowShape | undefined;
|
|
32
|
+
/** An app's own rows by entity name (or `name.view`). */
|
|
33
|
+
export type RowTable = Readonly<Record<string, RowShape>>;
|
|
34
|
+
/** The row an entity named in a section is drawn with: the app's, the shared one, else the generic one. */
|
|
35
|
+
export declare const rowsOfEntity: (table?: RowTable) => (entity: string) => RowShape;
|
|
36
|
+
/** An app's own rows by entity name, as `entityFields` takes its fields; a shared name is refused. */
|
|
37
|
+
export declare const entityRows: (table: RowTable) => RowsResolver;
|
|
38
|
+
/** The row a command's answer is drawn with: its own, its app's, the shared one, else the generic one. */
|
|
39
|
+
export declare const rowShapeOf: (command: Command | undefined, resolve?: RowsResolver) => RowShape;
|
|
40
|
+
/** The whole answer as compact text: header, rows, footer. Pure: no store, no clock. */
|
|
41
|
+
export declare const compactText: <T>(body: Body<T>, shape: RowShape, options: CompactOptions, rowsOf?: (entity: string) => RowShape) => string;
|