@wirecat/cli-core 0.399.0 → 0.400.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.
Files changed (221) hide show
  1. package/CHANGELOG.md +82 -0
  2. package/dist/analysis/consents.d.ts +11 -6
  3. package/dist/analysis/consents.js +5 -2
  4. package/dist/cli/bot/mcp.js +2 -1
  5. package/dist/cli/config-command.js +20 -1
  6. package/dist/cli/context.d.ts +2 -0
  7. package/dist/cli/context.js +1 -1
  8. package/dist/cli/effects.d.ts +4 -1
  9. package/dist/cli/effects.js +10 -2
  10. package/dist/cli/index.d.ts +1 -1
  11. package/dist/cli/index.js +1 -1
  12. package/dist/cli/json-fields.js +12 -1
  13. package/dist/cli/messenger/account-command.js +15 -6
  14. package/dist/cli/messenger/account-profile-query.d.ts +2 -0
  15. package/dist/cli/messenger/account-profile-query.js +13 -0
  16. package/dist/cli/messenger/account-sessions-command.js +1 -0
  17. package/dist/cli/messenger/admin-commands.js +12 -12
  18. package/dist/cli/messenger/admin-contacts-command.js +6 -3
  19. package/dist/cli/messenger/admin-folders-command.js +5 -5
  20. package/dist/cli/messenger/admin-moderation-command.js +4 -4
  21. package/dist/cli/messenger/admin-statistics-command.js +6 -14
  22. package/dist/cli/messenger/analysis-consents-query.d.ts +5 -0
  23. package/dist/cli/messenger/analysis-consents-query.js +20 -0
  24. package/dist/cli/messenger/archive-commands.js +3 -3
  25. package/dist/cli/messenger/attachments-command.js +3 -0
  26. package/dist/cli/messenger/backfill-command.js +5 -4
  27. package/dist/cli/messenger/chat-queries.d.ts +1 -0
  28. package/dist/cli/messenger/chat-queries.js +12 -2
  29. package/dist/cli/messenger/chats-command.js +2 -2
  30. package/dist/cli/messenger/chats-documents-command.js +7 -5
  31. package/dist/cli/messenger/chats-members-command.js +7 -3
  32. package/dist/cli/messenger/chats-read-command.js +1 -1
  33. package/dist/cli/messenger/chats-tracking-command.js +14 -6
  34. package/dist/cli/messenger/contacts-command.js +9 -5
  35. package/dist/cli/messenger/conversations-command.js +20 -16
  36. package/dist/cli/messenger/doctor-command.js +5 -1
  37. package/dist/cli/messenger/document-links-query.d.ts +2 -0
  38. package/dist/cli/messenger/document-links-query.js +9 -0
  39. package/dist/cli/messenger/download-command.js +1 -0
  40. package/dist/cli/messenger/flood-command.d.ts +2 -2
  41. package/dist/cli/messenger/flood-command.js +3 -3
  42. package/dist/cli/messenger/gaps-command.js +2 -2
  43. package/dist/cli/messenger/guard-commands.js +35 -13
  44. package/dist/cli/messenger/images-command.js +1 -0
  45. package/dist/cli/messenger/media-archive-command.js +2 -0
  46. package/dist/cli/messenger/media-archive-move-command.js +1 -0
  47. package/dist/cli/messenger/media-archive-timer-command.js +1 -1
  48. package/dist/cli/messenger/mentions-command.js +15 -2
  49. package/dist/cli/messenger/messages-command.js +2 -0
  50. package/dist/cli/messenger/messages-delete-command.js +1 -0
  51. package/dist/cli/messenger/messages-edit-command.js +1 -0
  52. package/dist/cli/messenger/messages-evidence-command.js +3 -1
  53. package/dist/cli/messenger/messages-forward-command.js +1 -0
  54. package/dist/cli/messenger/messages-pin-command.js +3 -0
  55. package/dist/cli/messenger/messages-send-command.js +1 -0
  56. package/dist/cli/messenger/metadata-command.js +6 -1
  57. package/dist/cli/messenger/models-command.js +10 -2
  58. package/dist/cli/messenger/polls-command.js +4 -0
  59. package/dist/cli/messenger/private-people-command.js +3 -3
  60. package/dist/cli/messenger/rankings-command.js +6 -11
  61. package/dist/cli/messenger/reactions-command.js +2 -0
  62. package/dist/cli/messenger/recipients-query.d.ts +5 -0
  63. package/dist/cli/messenger/recipients-query.js +18 -0
  64. package/dist/cli/messenger/records-command.js +13 -6
  65. package/dist/cli/messenger/search-command.js +3 -1
  66. package/dist/cli/messenger/searches-command.js +5 -1
  67. package/dist/cli/messenger/sends-query.d.ts +5 -0
  68. package/dist/cli/messenger/sends-query.js +33 -0
  69. package/dist/cli/messenger/serve-command.js +1 -0
  70. package/dist/cli/messenger/statistics-query.d.ts +14 -0
  71. package/dist/cli/messenger/statistics-query.js +143 -0
  72. package/dist/cli/messenger/stats-command.js +5 -5
  73. package/dist/cli/messenger/store-maintenance-command.d.ts +1 -1
  74. package/dist/cli/messenger/store-maintenance-command.js +10 -10
  75. package/dist/cli/messenger/tags-command.js +7 -1
  76. package/dist/cli/messenger/tasks-command.js +47 -8
  77. package/dist/cli/messenger/transcribe-command.js +1 -0
  78. package/dist/cli/messenger/watch-answers.d.ts +6 -0
  79. package/dist/cli/messenger/watch-answers.js +69 -0
  80. package/dist/cli/messenger/watch-command.d.ts +1 -1
  81. package/dist/cli/messenger/watch-command.js +56 -7
  82. package/dist/cli/program.js +5 -1
  83. package/dist/cli/runs/command.js +25 -38
  84. package/dist/cli/runs/query.d.ts +5 -0
  85. package/dist/cli/runs/query.js +32 -0
  86. package/dist/cli/runs/search-command.d.ts +4 -0
  87. package/dist/cli/runs/search-command.js +27 -0
  88. package/dist/cli/runs/search-query.d.ts +4 -0
  89. package/dist/cli/runs/search-query.js +50 -0
  90. package/dist/cli/runs/search.d.ts +2 -1
  91. package/dist/cli/runs/search.js +6 -6
  92. package/dist/domain/attachments.js +9 -1
  93. package/dist/domain/rankings-options.js +3 -3
  94. package/dist/entities/account-change.d.ts +89 -0
  95. package/dist/entities/account-change.js +20 -0
  96. package/dist/entities/account-profile.d.ts +7 -0
  97. package/dist/entities/account-profile.js +8 -0
  98. package/dist/entities/analysis-consent.d.ts +9 -0
  99. package/dist/entities/analysis-consent.js +4 -0
  100. package/dist/entities/attachment-results.d.ts +96 -0
  101. package/dist/entities/attachment-results.js +31 -0
  102. package/dist/entities/batch-result.d.ts +27 -0
  103. package/dist/entities/batch-result.js +18 -0
  104. package/dist/entities/chat-admin.d.ts +234 -0
  105. package/dist/entities/chat-admin.js +84 -0
  106. package/dist/entities/chat-change.d.ts +25 -0
  107. package/dist/entities/chat-change.js +11 -0
  108. package/dist/entities/chat-metadata.d.ts +34 -0
  109. package/dist/entities/chat-metadata.js +15 -0
  110. package/dist/entities/chat-moderation.d.ts +44 -0
  111. package/dist/entities/chat-moderation.js +37 -0
  112. package/dist/entities/chat-tracking.d.ts +26 -0
  113. package/dist/entities/chat-tracking.js +23 -0
  114. package/dist/entities/contact-activity.d.ts +52 -0
  115. package/dist/entities/contact-activity.js +42 -0
  116. package/dist/entities/contact-context.d.ts +1568 -0
  117. package/dist/entities/contact-context.js +191 -0
  118. package/dist/entities/contact-write.d.ts +22 -0
  119. package/dist/entities/contact-write.js +6 -0
  120. package/dist/entities/conversation-preparation.d.ts +63 -0
  121. package/dist/entities/conversation-preparation.js +23 -0
  122. package/dist/entities/conversation-results.d.ts +567 -0
  123. package/dist/entities/conversation-results.js +94 -0
  124. package/dist/entities/document-link.d.ts +13 -0
  125. package/dist/entities/document-link.js +11 -0
  126. package/dist/entities/extraction-report.d.ts +95 -0
  127. package/dist/entities/extraction-report.js +34 -0
  128. package/dist/entities/image-embedding.d.ts +9 -0
  129. package/dist/entities/image-embedding.js +10 -0
  130. package/dist/entities/index.d.ts +8006 -1928
  131. package/dist/entities/index.js +174 -3
  132. package/dist/entities/listener.d.ts +2150 -0
  133. package/dist/entities/listener.js +46 -0
  134. package/dist/entities/media-archive.d.ts +43 -0
  135. package/dist/entities/media-archive.js +32 -0
  136. package/dist/entities/meeting-task-proposal.d.ts +14 -0
  137. package/dist/entities/meeting-task-proposal.js +19 -0
  138. package/dist/entities/meeting.d.ts +1 -1
  139. package/dist/entities/meeting.js +1 -1
  140. package/dist/entities/mention.d.ts +63 -0
  141. package/dist/entities/mention.js +44 -0
  142. package/dist/entities/message-change.d.ts +19 -0
  143. package/dist/entities/message-change.js +6 -0
  144. package/dist/entities/message-context.d.ts +617 -0
  145. package/dist/entities/message-context.js +20 -0
  146. package/dist/entities/message-read.d.ts +73 -0
  147. package/dist/entities/message-read.js +58 -0
  148. package/dist/entities/message-transfer.d.ts +379 -0
  149. package/dist/entities/message-transfer.js +83 -0
  150. package/dist/entities/message-write.d.ts +191 -0
  151. package/dist/entities/message-write.js +8 -0
  152. package/dist/entities/model-key.d.ts +14 -0
  153. package/dist/entities/model-key.js +10 -0
  154. package/dist/entities/poll.d.ts +40 -0
  155. package/dist/entities/poll.js +16 -0
  156. package/dist/entities/private-contact.d.ts +10 -0
  157. package/dist/entities/private-contact.js +4 -0
  158. package/dist/entities/reaction-change.d.ts +7 -0
  159. package/dist/entities/reaction-change.js +8 -0
  160. package/dist/entities/recipient.d.ts +29 -0
  161. package/dist/entities/recipient.js +11 -0
  162. package/dist/entities/recorded-run.d.ts +103 -0
  163. package/dist/entities/recorded-run.js +44 -0
  164. package/dist/entities/saved-search.d.ts +4 -0
  165. package/dist/entities/saved-search.js +2 -0
  166. package/dist/entities/send-attempt.d.ts +31 -0
  167. package/dist/entities/send-attempt.js +29 -0
  168. package/dist/entities/store-maintenance.d.ts +319 -0
  169. package/dist/entities/store-maintenance.js +145 -0
  170. package/dist/entities/store-records.d.ts +442 -0
  171. package/dist/entities/store-records.js +99 -0
  172. package/dist/entities/tag-change.d.ts +74 -0
  173. package/dist/entities/tag-change.js +31 -0
  174. package/dist/entities/task-pair.d.ts +31 -0
  175. package/dist/entities/task-pair.js +23 -0
  176. package/dist/entities/task-results.d.ts +524 -0
  177. package/dist/entities/task-results.js +142 -0
  178. package/dist/errors/reasons.js +1 -0
  179. package/dist/mcp/agent.d.ts +3 -1
  180. package/dist/mcp/agent.js +9 -6
  181. package/dist/mcp/bot/server.d.ts +2 -0
  182. package/dist/mcp/bot/server.js +1 -0
  183. package/dist/mcp/server.js +3 -1
  184. package/dist/mcp/tools/attachments.js +1 -0
  185. package/dist/meetings/cli/index.js +1 -1
  186. package/dist/meetings/cli/passage.js +1 -0
  187. package/dist/meetings/index.d.ts +2 -1
  188. package/dist/meetings/index.js +1 -0
  189. package/dist/meetings/links.d.ts +16 -0
  190. package/dist/meetings/mcp/index.d.ts +15 -15
  191. package/dist/meetings/read-answers.d.ts +167 -0
  192. package/dist/meetings/read-answers.js +183 -0
  193. package/dist/runs/run.d.ts +10 -2
  194. package/dist/runs/run.js +11 -3
  195. package/dist/sends/journal.d.ts +8 -1
  196. package/dist/sends/journal.js +9 -2
  197. package/dist/sends/permissions.d.ts +5 -1
  198. package/dist/sends/permissions.js +11 -1
  199. package/dist/sends/recipients.d.ts +8 -0
  200. package/dist/sends/recipients.js +9 -0
  201. package/dist/services/admin-statistics.js +3 -3
  202. package/dist/services/chat-documents.d.ts +1 -0
  203. package/dist/services/chat-documents.js +9 -3
  204. package/dist/services/chats.d.ts +1 -1
  205. package/dist/services/chats.js +5 -2
  206. package/dist/services/meeting-links.d.ts +2 -17
  207. package/dist/store/exports.d.ts +1 -1
  208. package/dist/store/exports.js +1 -1
  209. package/dist/store/messages.d.ts +3 -1
  210. package/dist/store/messages.js +2 -2
  211. package/dist/store/roster.d.ts +4 -1
  212. package/dist/store/roster.js +2 -2
  213. package/dist/store/sqlite/chats.d.ts +12 -0
  214. package/dist/store/sqlite/chats.js +1 -0
  215. package/dist/store/sqlite/document-links.d.ts +1 -1
  216. package/dist/store/sqlite/document-links.js +5 -5
  217. package/dist/store/sqlite/involvements.js +2 -1
  218. package/dist/store/sqlite/roster.d.ts +2 -1
  219. package/dist/store/sqlite/roster.js +3 -2
  220. package/package.json +1 -1
  221. package/parity.json +8 -66
package/CHANGELOG.md CHANGED
@@ -5,6 +5,88 @@ Notable changes to `@wirecat/cli-core` (`@wirecat/cli-messaging` up to 0.261.0,
5
5
  minor version may break callers, and says how under "Changed — may break callers". `pnpm docs:check` checks the
6
6
  shape of this file.
7
7
 
8
+ ## Unreleased
9
+
10
+ ## 0.400.0 — 24.10.2026
11
+
12
+ ### Fixed
13
+
14
+ - **Downloads skip service events and keyboards.** A message whose only attachment is a group event (`control`) or an inline keyboard no longer makes a media archive or `attachments download` fail with `provider.invalid-response`; both count as non-files like polls and links.
15
+
16
+ - Account lists accept profile and current-profile query terms, and declare their answer fields for discovery and projection.
17
+
18
+ - **Account change and privacy schemas.** Profile updates, ended sessions and privacy reads and writes declare their existing answers for field discovery and projection. Privacy fields remain optional where a messenger does not expose a setting.
19
+
20
+ - **`contacts timeline` stays in its account for tasks.** A task assigned to the person now shows only when its project belongs to the asked account or to no account; before, tasks from another account of the same messenger showed too. Task rows now also name the account of their project.
21
+
22
+ ### Added
23
+
24
+ - **Meeting answer schemas.** `review`, `mentions list`, `stats meetings show` and `top`, `meetings people`, `meetings passage` and `meetings task-proposal` declare their answer entities (`meeting_review`, `meeting_link`, `meeting_stats`, `meeting_speaker`, `meeting_participants`, `meeting_passage`, `meeting_task_proposal`) for field discovery and projection. `meetings/` exports the item builders the meeting CLIs share: `meetingReviewItem`, `meetingLinkItem`, `meetingStatsItem`, `meetingSpeakerItem`.
25
+
26
+ - **Listener answers declare their fields.** Live messages and events, and the final serve summary, expose exact schemas for discovery and cross-CLI output contracts. Provider-only attachment references stay private.
27
+
28
+ - **Poll answer schemas.** Poll reads, vote and close receipts, and newly sent poll messages declare their answer entities for field discovery and projection.
29
+
30
+ - **Image embedding progress has declared fields.** Discovery and projection describe the bounded batch’s model, embedded, skipped and failed counts, and whether more pictures remain. The existing receipt and processing behavior stay unchanged.
31
+
32
+ - **Model key receipt schemas.** Storing or removing a model-provider key declares its provider and storage sources for field discovery and projection. No credential value appears in either schema or answer.
33
+
34
+ - Recipient lists accept ID and title queries without changing the allowlist state. Recipient commands declare their answer schemas for discovery and field projection.
35
+
36
+ - **Cached chat metadata declares its answers.** Cached reads describe absent or populated metadata; refresh receipts describe each successful chat or failure and whether more chats remain. Existing keys and cached-read behavior stay unchanged.
37
+
38
+ - **Contact answers have declared fields.** Address-book writes, private aliases and note removal, activity, bot checks, identity links, context and preparation expose their exact answer schemas. Context describes both the cross-account report and selected-chat message windows at every detail level. Field projection recognizes both shapes and refuses unknown fields before reading contact data. CLI, SDK and MCP receipts keep their existing shapes.
39
+
40
+ - **Mention answers have declared fields.** Mentions, details, linked domains, confirmed places, decisions and rebuild receipts expose their existing schemas for discovery, JSON field projection and MCP. Numeric local IDs and cross-account decision behavior stay unchanged.
41
+
42
+ - **Reaction answer schemas.** Add and remove declare the same reaction-change entity, including the operation id and a null reaction when removed. Field discovery and projection now describe these write receipts.
43
+
44
+ - **Tag changes declare their results.** Add and remove expose precise chat, contact, message and task target shapes. Automatic tagging describes each classification or failed chat, including preview status and remaining work; existing receipts and ownership claims stay unchanged.
45
+
46
+ - Send history includes a concrete example in help and command discovery.
47
+
48
+ - Chat document lists accept a source-message reference query, filtered in the store before link pagination, and declare their document-link answer fields.
49
+
50
+ - Doctor reports include a concrete creation example in help and command discovery.
51
+
52
+ - Send-attempt lists accept queries over their stored metadata before pagination. Continuation steps keep the query, and discovery declares the answer fields.
53
+
54
+ - **Clearing search history declares its receipt.** Discovery and JSON projection expose the existing cleared count; saved searches remain untouched.
55
+
56
+ - Recorded-run lists accept metadata queries before pagination, and recorded-run commands declare their answer schemas. Continuation steps keep the query.
57
+
58
+ - **Message read and write schemas.** Showing a message, sending, editing, forwarding, pinning, starring and deleting declare their existing answer entities for field discovery and projection. Write receipts retain their operation id.
59
+
60
+ - **Chat-state write schemas.** Muting, archiving, pinning, marking unread, deleting a chat and clearing its history declare their existing write receipts for field discovery and projection, including their operation ids.
61
+
62
+ - **Chat administration declares its answers.** Invite inspection, posting identities, read receipts, roster fetches, membership and group changes, invitations, join requests, administrators, folder writes and moderation expose their exact existing result fields, including operation identifiers and partial outcomes.
63
+
64
+ - **Message read schemas.** Evidence packets, voice transcripts and conversation links declare separate answer entities for field discovery and projection, preserving existing response keys.
65
+
66
+ - **Conversation results have declared schemas.** Builds, analysis, message batches, link changes, embedding progress, readiness, related conversations and search hits describe their existing answer fields for discovery and projection, including bounded preparation and empty batches.
67
+
68
+ - **Message context, downloads and sync have declared answers.** Context describes both message windows and bounded thread reports. Downloads retain file paths and partial extraction results; sync describes estimates, chat and all-chat progress, preparation and background jobs. General chat evidence packets and exact statistics evidence now explain their separate purposes in help and the guide.
69
+
70
+ - Analysis-consent lists accept chat and provider-identity queries, and consent commands declare their answer fields. Reading a filtered list leaves remembered permissions unchanged.
71
+
72
+ - **Attachment and media archive results have declared fields.** Extraction reports and streamed file rows, retained-file transfers, saved text, archive downloads, integrity checks, relocations and timer management describe their existing receipts. MCP resource/image transfers keep byte metadata in the same shape while carrying bytes separately.
73
+
74
+ - Tracked-chat lists accept a query over chat IDs and titles, filtered in the store. Tracking commands declare their answer schemas for field projection and discovery.
75
+
76
+ - **Task actions declare their answers.** Task cards, source previews, creation and close receipts, relationships, similarity and duplicate decisions, merges, categories and rule scans expose their exact existing fields for discovery and projection. Stored tasks and actions keep their behavior.
77
+
78
+ - **Store commands declare their answers.** Archive coverage, exports, selected-chat clearing, gap plans and repairs, background jobs, health checks, maintenance and backup/restore receipts describe their existing result fields. Missing-file and partial-result variants remain explicit; stored data and command behavior stay unchanged.
79
+
80
+ ### Changed — may break callers
81
+
82
+ - **Watch uses the shared JSONL contract.** Read each message or event from `{ type: "item", data }` and wait for the final `type: "end"` record. Message and event fields are snake_case, message values use the canonical read shape with references, and live counter observations remain available. Normal stops and timeouts finish complete; a source failure finishes incomplete and returns its failure code. Update readers of the old raw lines.
83
+
84
+ - **MCP offers the commands that change what a later run may do, behind `mcp.authority`.** `config set|unset`, `schedule command|agent|remove`, auto-reply rules and consents, recipients, chat rules, `server install`, `skill install`, model keys, media archive timers and `flood clear` are MCP tools again, offered only where the profile sets `permissions.mcp.authority` to `allow`; it is `deny` by default, so nothing changes until the owner turns it on. An agent cannot change that key through `config set`. What to change in a CLI: `grantsAuthority` no longer adds `mcp: false`, so `commands --json` no longer marks these commands, and a catalogue built with `toolsFromCommands` without `levels` lists them; update a pinned catalogue and a document that lists what MCP leaves out. A command of the CLI's own marked `grantsAuthority` (max `recipients add`) is gated the same way. A caller of `createBotServer` passes `authority: changesAuthorityAt(root)` to hide them; without it they are listed and refuse when called.
85
+
86
+ - **Diagnostic search is `search runs`.** Replace `runs search`; saved MCP requests use the new command path. Filters are query terms: `status:failed`, `error_code:timeout`, `operation:messages.download` and `profile:work`, alongside literal diagnostic words. Unsupported operators are refused; time bounds and paging remain options. The local search still needs no linked messenger profile, filters before pagination and returns diagnostic fields without message contents. No alias remains for the retired path.
87
+
88
+ - **Statistics select rows through their query.** Rankings use `message_kind:` with `posts`, `comments` or `all`, and author rankings `min_messages:5`; discussion uses `min_views:10` and `max_replies:0`; unanswered questions use `older_than:24h`. These predicates replace the corresponding filter flags and reach the existing SQL selection before aggregation or row limits. The same reports select chats with `chat:` and exact words with `exact:` instead of `--chat` and `--exact`; source and timezone remain options. Saved CLI/MCP requests must migrate. Stored selections and SDK parameters retain their meanings.
89
+
8
90
  ## 0.399.0 — 23.10.2026
9
91
 
10
92
  ### Changed — may break callers
@@ -4,9 +4,14 @@ export interface AnalysisConsent {
4
4
  provider: string;
5
5
  at: string;
6
6
  }
7
- export declare const analysisConsents: (store: MessageStore, account: AccountKey) => {
8
- list: () => Promise<AnalysisConsent[]>;
9
- has: (chat: string, provider: string) => Promise<boolean>;
10
- remember: (chat: string, provider: string) => Promise<void>;
11
- revoke: (chat?: string, provider?: string) => Promise<void>;
12
- };
7
+ export interface AnalysisConsentFilter {
8
+ chat?: string;
9
+ provider?: string;
10
+ }
11
+ export interface AnalysisConsents {
12
+ list(filter?: AnalysisConsentFilter): Promise<AnalysisConsent[]>;
13
+ has(chat: string, provider: string): Promise<boolean>;
14
+ remember(chat: string, provider: string): Promise<void>;
15
+ revoke(chat?: string, provider?: string): Promise<void>;
16
+ }
17
+ export declare const analysisConsents: (store: MessageStore, account: AccountKey) => AnalysisConsents;
@@ -1,7 +1,7 @@
1
1
  import { CliError } from "@wirecat/cli-base";
2
2
  const KEY = "analysis-consents-v1";
3
3
  export const analysisConsents = (store, account) => {
4
- const list = async () => {
4
+ const list = async (filter) => {
5
5
  const state = await store.sync.syncState(account, KEY);
6
6
  if (!state)
7
7
  return [];
@@ -10,7 +10,10 @@ export const analysisConsents = (store, account) => {
10
10
  if (!Array.isArray(items) ||
11
11
  items.some((item) => !item || typeof item.chat !== "string" || typeof item.provider !== "string" || typeof item.at !== "string"))
12
12
  throw new Error("invalid");
13
- return items;
13
+ return filter === undefined
14
+ ? items
15
+ : items.filter((item) => (filter.chat === undefined || item.chat === filter.chat) &&
16
+ (filter.provider === undefined || item.provider === filter.provider));
14
17
  }
15
18
  catch {
16
19
  throw new CliError("configuration_error", "stored analysis consent is invalid; revoke it before running analysis");
@@ -3,7 +3,7 @@ import { CliError } from "@wirecat/cli-base";
3
3
  import { annotate, describeProgram } from "@wirecat/cli-base/commands";
4
4
  import { Command } from "commander";
5
5
  import { environmentOf } from "../context.js";
6
- import { LOCAL_READ, REMOTE_WRITE } from "../effects.js";
6
+ import { changesAuthorityAt, LOCAL_READ, REMOTE_WRITE } from "../effects.js";
7
7
  import { itemOf, renderAnswer } from "../envelope.js";
8
8
  import { serverEntry } from "../messenger/mcp-command.js";
9
9
  import { nextStep, rootOf } from "../steps.js";
@@ -28,6 +28,7 @@ export const botMcpCommand = (bot) => {
28
28
  await serveBotOverStdio({
29
29
  bot,
30
30
  program: { run, commands: describeProgram(rootOf(this)) },
31
+ authority: changesAuthorityAt(rootOf(this)),
31
32
  settings: context.settings,
32
33
  levels: () => bot.resolveSettings(this.optsWithGlobals(), { env: context.env, kind: "bot" }).permissions,
33
34
  env: context.env,
@@ -2,8 +2,9 @@ import { CliError, pathsAreOverridden } from "@wirecat/cli-base";
2
2
  import { annotate } from "@wirecat/cli-base/commands";
3
3
  import { Command } from "commander";
4
4
  import { envName } from "../ports/app.js";
5
+ import { AUTHORITY_KEY } from "../sends/permissions.js";
5
6
  import { loadSnakeCaseConfig } from "./config-keys.js";
6
- import { baseContext } from "./context.js";
7
+ import { baseContext, environmentOf } from "./context.js";
7
8
  import { grantsAuthority, LOCAL_READ, LOCAL_WRITE } from "./effects.js";
8
9
  import { itemOf, renderAnswer } from "./envelope.js";
9
10
  import { knownBeside, knownPermissionKeys } from "./permission-keys.js";
@@ -94,6 +95,8 @@ export const configCommand = (app, config, { permissionKey } = {}) => {
94
95
  // The defaults are every other profile's settings too, and default_profile picks one of them.
95
96
  throw new CliError("permission_error", `this process is locked to profile ${settings.profile} (${lock}) — ${setting === "default_profile" ? setting : "--defaults"} changes other profiles`);
96
97
  }
98
+ if (environmentOf(this).agent)
99
+ refuseAuthorityKey(setting, action === "set" ? String(given) : undefined);
97
100
  if (action === "set")
98
101
  refuseUnknownKey(this, setting, String(given), permissionKey);
99
102
  const saved = config.changeSetting(settings.configPath, {
@@ -133,6 +136,22 @@ export const refuseUnknownKey = (command, setting, value, permissionKey) => {
133
136
  (beside.length > 0 ? ` — the known ones there are ${beside.join(", ")}` : ""));
134
137
  }
135
138
  };
139
+ /**
140
+ * An agent may hold `mcp.authority` and change other settings, but never the key that let it in: on any profile, in
141
+ * the defaults, or by replacing `permissions` whole. Only the owner, in a terminal, turns it on or off (NEED-1662).
142
+ */
143
+ const refuseAuthorityKey = (setting, value) => {
144
+ const keys = setting.startsWith("permissions.")
145
+ ? [setting.slice("permissions.".length)]
146
+ : setting === "permissions"
147
+ ? value === undefined
148
+ ? ["mcp"]
149
+ : objectKeys(value)
150
+ : [];
151
+ if (!keys.some((key) => key === "mcp" || key.startsWith("mcp.")))
152
+ return;
153
+ throw new CliError("permission_error", `an agent cannot change permissions.${AUTHORITY_KEY} — the owner sets it in a terminal`, { permission: AUTHORITY_KEY });
154
+ };
136
155
  /** The keys of a `permissions` object given whole; anything else is left to the schema to refuse. */
137
156
  const objectKeys = (value) => {
138
157
  try {
@@ -85,6 +85,8 @@ export interface AgentRun {
85
85
  failure: (error: ErrorObject) => void;
86
86
  /** The server's own stricter levels, `key=level`, as `mcp --permission` takes them; they never loosen. */
87
87
  overrides?: readonly string[];
88
+ /** The server's levels allow `mcp.authority`: a command that changes what a later run may do may run. */
89
+ authority?: boolean;
88
90
  }
89
91
  export interface RelayedRun {
90
92
  /** The profile serve holds; a command for another is refused before it touches the connection. */
@@ -72,7 +72,7 @@ export const baseContext = (command, resolveSettings) => {
72
72
  run: (body, { unbounded = false, name } = {}) => environment.app
73
73
  ? recorded({
74
74
  app: environment.app,
75
- // An agent's run is named `mcp <command>`, so `runs search` and `sends list` tell it from the owner's.
75
+ // An agent's run is named `mcp <command>`, so `search runs` and `sends list` tell it from the owner's.
76
76
  command: name ?? `${environment.agent ? "mcp " : ""}${commandPath(command)}`,
77
77
  profile: settings.profile,
78
78
  record: settings.record,
@@ -14,13 +14,16 @@ export declare const NO_EFFECTS: Effects;
14
14
  /**
15
15
  * Marks a local write that changes what a later run may do: a schedule, a setting, the recipient list, an
16
16
  * auto-reply, an agent's setup. `<PREFIX>_READ_ONLY` refuses it, or an agent under the lock could hand an
17
- * unlocked run the send it may not make itself (SEC-47). For the same reason no MCP server offers it (`mcp: false`).
17
+ * unlocked run the send it may not make itself (SEC-47). For the same reason an MCP server offers it only where the
18
+ * profile allows `mcp.authority`, which is off until the owner turns it on (NEED-1662).
18
19
  * `unless` names a flag that makes the run only look (`upgrade --check`), which the lock lets through.
19
20
  */
20
21
  export declare const grantsAuthority: <T extends Command>(command: T, { unless }?: {
21
22
  unless?: string;
22
23
  }) => T;
23
24
  export declare const changesAuthority: (command: Command) => boolean;
25
+ /** `changesAuthority` by command path under `root`, as an MCP catalogue asks it; an unknown path is not one. */
26
+ export declare const changesAuthorityAt: (root: Command) => (path: readonly string[]) => boolean;
24
27
  export declare const changesAuthorityNow: (command: Command) => boolean;
25
28
  /**
26
29
  * Marks a command that links or unlinks the account — `session start`, `session end`, a setup that logs in. It never
@@ -16,14 +16,22 @@ const AUTHORITY = new WeakMap();
16
16
  /**
17
17
  * Marks a local write that changes what a later run may do: a schedule, a setting, the recipient list, an
18
18
  * auto-reply, an agent's setup. `<PREFIX>_READ_ONLY` refuses it, or an agent under the lock could hand an
19
- * unlocked run the send it may not make itself (SEC-47). For the same reason no MCP server offers it (`mcp: false`).
19
+ * unlocked run the send it may not make itself (SEC-47). For the same reason an MCP server offers it only where the
20
+ * profile allows `mcp.authority`, which is off until the owner turns it on (NEED-1662).
20
21
  * `unless` names a flag that makes the run only look (`upgrade --check`), which the lock lets through.
21
22
  */
22
23
  export const grantsAuthority = (command, { unless } = {}) => {
23
24
  AUTHORITY.set(command, unless);
24
- return annotate(command, { mcp: false });
25
+ return command;
25
26
  };
26
27
  export const changesAuthority = (command) => AUTHORITY.has(command);
28
+ /** `changesAuthority` by command path under `root`, as an MCP catalogue asks it; an unknown path is not one. */
29
+ export const changesAuthorityAt = (root) => (path) => {
30
+ let at = root;
31
+ for (const word of path)
32
+ at = at?.commands.find((one) => one.name() === word);
33
+ return at !== undefined && changesAuthority(at);
34
+ };
27
35
  export const changesAuthorityNow = (command) => {
28
36
  if (!AUTHORITY.has(command))
29
37
  return false;
@@ -53,7 +53,7 @@ export { loadSnakeCaseConfig } from "./config-keys.js";
53
53
  export { type AgentRun, type BaseContext, type BaseEnvironment, baseContext, environmentOf, outputFor, provide, secretInputOf, } from "./context.js";
54
54
  export { type Closeable, withDeadline } from "./deadline.js";
55
55
  export { DEFAULT_CONFIG, ensureDefaultConfig } from "./default-config.js";
56
- export { changesAuthority, destroysNothing, FETCH, grantsAuthority, isLogin, isOtherServiceCall, isStoreChange, judgedEffects, judgedTargets, LOCAL_READ, LOCAL_WRITE, logsInOrOut, NO_EFFECTS, ownerOnlyOption, REMOTE_READ, REMOTE_WRITE, reachesOtherService, refuseUnderReadOnly, } from "./effects.js";
56
+ export { changesAuthority, changesAuthorityAt, destroysNothing, FETCH, grantsAuthority, isLogin, isOtherServiceCall, isStoreChange, judgedEffects, judgedTargets, LOCAL_READ, LOCAL_WRITE, logsInOrOut, NO_EFFECTS, ownerOnlyOption, REMOTE_READ, REMOTE_WRITE, reachesOtherService, refuseUnderReadOnly, } from "./effects.js";
57
57
  export { type AnswerOutput, answerSchema, type Body, type ItemBody, itemAnswer, itemOf, jsonlRecords, judgedAnswer, type ListBody, type ListInput, listOf, metaNow, printItem, renderAnswer, sealed, snakeKeys, snakeKeysKeeping, } from "./envelope.js";
58
58
  export { withAbort } from "./execution.js";
59
59
  export { checkGuide, checkStep, type GuideOptions, type GuideReference, type GuideStep, guideCommand, loadScenarios, MEMO_SCENARIOS, type Scenario, shellProblems, } from "./guide.js";
package/dist/cli/index.js CHANGED
@@ -48,7 +48,7 @@ export { loadSnakeCaseConfig } from "./config-keys.js";
48
48
  export { baseContext, environmentOf, outputFor, provide, secretInputOf, } from "./context.js";
49
49
  export { withDeadline } from "./deadline.js";
50
50
  export { DEFAULT_CONFIG, ensureDefaultConfig } from "./default-config.js";
51
- export { changesAuthority, destroysNothing, FETCH, grantsAuthority, isLogin, isOtherServiceCall, isStoreChange, judgedEffects, judgedTargets, LOCAL_READ, LOCAL_WRITE, logsInOrOut, NO_EFFECTS, ownerOnlyOption, REMOTE_READ, REMOTE_WRITE, reachesOtherService, refuseUnderReadOnly, } from "./effects.js";
51
+ export { changesAuthority, changesAuthorityAt, destroysNothing, FETCH, grantsAuthority, isLogin, isOtherServiceCall, isStoreChange, judgedEffects, judgedTargets, LOCAL_READ, LOCAL_WRITE, logsInOrOut, NO_EFFECTS, ownerOnlyOption, REMOTE_READ, REMOTE_WRITE, reachesOtherService, refuseUnderReadOnly, } from "./effects.js";
52
52
  export { answerSchema, itemAnswer, itemOf, jsonlRecords, judgedAnswer, listOf, metaNow, printItem, renderAnswer, sealed, snakeKeys, snakeKeysKeeping, } from "./envelope.js";
53
53
  export { withAbort } from "./execution.js";
54
54
  export { checkGuide, checkStep, guideCommand, loadScenarios, MEMO_SCENARIOS, shellProblems, } from "./guide.js";
@@ -21,6 +21,17 @@ export const entityFields = (table) => {
21
21
  return name !== undefined && Object.hasOwn(table, name) ? namesOf(table[name]) : undefined;
22
22
  };
23
23
  };
24
+ const schemaFields = (schema) => {
25
+ if (typeof schema !== "object" || schema === null)
26
+ return undefined;
27
+ if ("entries" in schema && typeof schema.entries === "object" && schema.entries !== null)
28
+ return Object.keys(schema.entries);
29
+ if ("type" in schema && schema.type === "union" && "options" in schema && Array.isArray(schema.options)) {
30
+ const branches = schema.options.map(schemaFields);
31
+ return branches.every((fields) => fields !== undefined) ? [...new Set(branches.flat())] : undefined;
32
+ }
33
+ return undefined;
34
+ };
24
35
  /** The fields of the answer a command declares (`annotate(…, { output })`); nothing until it declares one. */
25
36
  export const answerFields = (command, resolve) => {
26
37
  const own = resolve?.(command);
@@ -28,7 +39,7 @@ export const answerFields = (command, resolve) => {
28
39
  return [...own];
29
40
  const name = declaredEntity(command);
30
41
  const schema = name === undefined ? undefined : ENTITIES[name];
31
- return schema && "entries" in schema ? namesOf(schema) : undefined;
42
+ return schemaFields(schema);
32
43
  };
33
44
  const PATHS = /^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)*(,[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)*)*$/;
34
45
  const topOf = (fields, known) => fields.split(",").map((path) => {
@@ -6,7 +6,9 @@ import { storePath } from "../../store/path.js";
6
6
  import { openStore } from "../../store/store.js";
7
7
  import { REMOTE_READ, REMOTE_WRITE } from "../effects.js";
8
8
  import { itemOf, renderAnswer } from "../envelope.js";
9
+ import { queryTerms, withQuery } from "../list-query.js";
9
10
  import { NONE_NAMED } from "../settings.js";
11
+ import { ACCOUNT_PROFILES_QUERY } from "./account-profile-query.js";
10
12
  import { accountSessionsCommand } from "./account-sessions-command.js";
11
13
  import { recalledAccount } from "./accounts.js";
12
14
  import { photoHelp, readProfilePhoto } from "./admin-commands.js";
@@ -18,16 +20,19 @@ export const accountCommand = (messenger) => {
18
20
  const tool = `${messenger.app.command} personal`;
19
21
  const account = new Command("account")
20
22
  .description("the logged-in account")
21
- .addCommand(annotate(new Command("list"), { effects: REMOTE_READ, examples: [`${tool} account list`] })
23
+ .addCommand(withQuery(annotate(new Command("list"), { effects: REMOTE_READ, examples: [`${tool} account list`] }), ACCOUNT_PROFILES_QUERY)
22
24
  .summary("profiles on this computer and their signed-in accounts")
23
25
  .description("every profile on this computer, and the account each is logged in as; asks the messenger nothing")
24
- .action(async function () {
26
+ .action(async function (words) {
27
+ const terms = queryTerms(words, ACCOUNT_PROFILES_QUERY, "account list");
25
28
  const context = messengerContext(this, messenger, { needsProfile: false });
26
29
  const { app, provider } = messenger;
27
30
  const current = context.settings.sources.profile === NONE_NAMED ? undefined : context.settings.profile;
28
- const profiles = [
29
- ...new Set([...(current === undefined ? [] : [current]), ...context.settings.profilesHere]),
30
- ].sort();
31
+ const profiles = [...new Set([...(current === undefined ? [] : [current]), ...context.settings.profilesHere])]
32
+ .filter((profile) => terms.get("profile") === undefined || profile === terms.get("profile"))
33
+ .filter((profile) => terms.get("current") === undefined ||
34
+ (profile === current) === (terms.get("current")?.toLowerCase() === "true"))
35
+ .sort();
31
36
  const store = existsSync(storePath(context.env))
32
37
  ? await openStore({ env: context.env }).catch(() => undefined)
33
38
  : undefined;
@@ -61,7 +66,11 @@ export const accountCommand = (messenger) => {
61
66
  const account = await context.withMessenger((connection) => connection.me());
62
67
  renderAnswer(this, context, itemOf(accountAnswer(messenger.provider, whole ? account : maskedAccount(account))));
63
68
  }))
64
- .addCommand(annotate(new Command("update"), { effects: REMOTE_WRITE, examples: [`${tool} account update --first-name Alex`] })
69
+ .addCommand(annotate(new Command("update"), {
70
+ effects: REMOTE_WRITE,
71
+ output: { entity: "account_change" },
72
+ examples: [`${tool} account update --first-name Alex`],
73
+ })
65
74
  .description("change the name, the description or the photo everyone sees on your profile")
66
75
  .option("--first-name <name>", "your first name")
67
76
  .option("--last-name <name>", "your last name")
@@ -0,0 +1,2 @@
1
+ import { type QueryRegistry } from "@wirecat/cli-base/query";
2
+ export declare const ACCOUNT_PROFILES_QUERY: QueryRegistry;
@@ -0,0 +1,13 @@
1
+ import { queryFieldsOf } from "@wirecat/cli-base/query";
2
+ import { accountProfile } from "../../entities/index.js";
3
+ export const ACCOUNT_PROFILES_QUERY = {
4
+ entity: "account_profile",
5
+ backend: "native",
6
+ fields: queryFieldsOf(accountProfile, {
7
+ native: ["profile", "current"],
8
+ examples: { profile: "profile:work", current: "current:true" },
9
+ }).map((field) => ({
10
+ ...field,
11
+ operators: field.operators.filter((operator) => operator === "term" || operator === "phrase"),
12
+ })),
13
+ };
@@ -26,6 +26,7 @@ export const accountSessionsCommand = (messenger) => {
26
26
  // No MCP tool, at any permission level: it logs the owner out of the phone.
27
27
  sessions.addCommand(annotate(new Command("end"), {
28
28
  effects: REMOTE_WRITE,
29
+ output: { entity: "ended_account_sessions" },
29
30
  mcp: false,
30
31
  examples: [`${tool} account sessions end --others`],
31
32
  })
@@ -57,7 +57,7 @@ export const readProfilePhoto = (messenger, path, env) => readUpload("photo", pa
57
57
  });
58
58
  const updateCommand = (messenger) => {
59
59
  const keys = messenger.groupSettings ?? GROUP_SETTINGS;
60
- const update = annotate(new Command("update"), { effects: REMOTE_WRITE })
60
+ const update = annotate(new Command("update"), { output: { entity: "chat_group_change" }, effects: REMOTE_WRITE })
61
61
  .summary("change a group or channel's profile and settings")
62
62
  .description("rename a group or channel, change its description, or turn one of its settings on or off")
63
63
  .argument("<chat>", messenger.chatArgument)
@@ -93,7 +93,7 @@ const updateCommand = (messenger) => {
93
93
  };
94
94
  const linkCommand = (messenger) => {
95
95
  const link = new Command("link").description("a group's invite link");
96
- annotate(link.command("show"), { effects: REMOTE_READ })
96
+ annotate(link.command("show"), { output: { entity: "chat_invite" }, effects: REMOTE_READ })
97
97
  .description("the invite link, if you may see it")
98
98
  .argument("<chat>", messenger.chatArgument)
99
99
  .action(async function (chat) {
@@ -101,7 +101,7 @@ const linkCommand = (messenger) => {
101
101
  renderItem(this, context, await context.withServices((services) => services.admin.link(chat)));
102
102
  });
103
103
  if (messenger.inviteLinkCreate !== false)
104
- link.addCommand(annotate(new Command("create"), { effects: REMOTE_WRITE })
104
+ link.addCommand(annotate(new Command("create"), { output: { entity: "chat_invite_change" }, effects: REMOTE_WRITE })
105
105
  .description("make another invite link; nobody is told until you share it")
106
106
  .argument("<chat>", messenger.chatArgument)
107
107
  .option("--approval", "who joins by it asks first, and an admin lets them in; it then has no use limit")
@@ -132,7 +132,7 @@ const linkCommand = (messenger) => {
132
132
  ].join(" ")));
133
133
  }));
134
134
  if (messenger.inviteLinkRevoke !== false)
135
- link.addCommand(annotate(new Command("revoke"), { effects: REMOTE_WRITE })
135
+ link.addCommand(annotate(new Command("revoke"), { output: { entity: "chat_invite_change" }, effects: REMOTE_WRITE })
136
136
  .description("stop one link; for the group's own link, the answer is the new one")
137
137
  .argument("<chat>", messenger.chatArgument)
138
138
  .argument("<link>", "the link, as `chats link list` shows it")
@@ -141,7 +141,7 @@ const linkCommand = (messenger) => {
141
141
  renderItem(this, context, await context.withServices((services) => services.admin.revokeLink(chat, link)));
142
142
  }));
143
143
  if (messenger.inviteLinkUpdate === true)
144
- link.addCommand(annotate(new Command("update"), { effects: REMOTE_WRITE })
144
+ link.addCommand(annotate(new Command("update"), { output: { entity: "chat_invite_change" }, effects: REMOTE_WRITE })
145
145
  .description("change one of your invite links, the group's own one too")
146
146
  .argument("<chat>", messenger.chatArgument)
147
147
  .argument("<link>", "the link, as `chats link list` shows it")
@@ -158,7 +158,7 @@ const linkCommand = (messenger) => {
158
158
  ...(maxUses === undefined ? {} : { maxUses }),
159
159
  })));
160
160
  }));
161
- link.addCommand(annotate(new Command("reset"), { effects: REMOTE_WRITE })
161
+ link.addCommand(annotate(new Command("reset"), { output: { entity: "chat_group_change" }, effects: REMOTE_WRITE })
162
162
  .description("replace the invite link; the old one stops working")
163
163
  .argument("<chat>", messenger.chatArgument)
164
164
  .action(async function (chat) {
@@ -171,7 +171,7 @@ const adminsCommand = (messenger) => {
171
171
  const offered = messenger.adminRights ?? ADMIN_RIGHTS;
172
172
  const admins = new Command("admins").description("give or take back a member's admin rights");
173
173
  const picks = messenger.pickAdminRights !== false;
174
- const add = annotate(new Command("add"), { effects: REMOTE_WRITE })
174
+ const add = annotate(new Command("add"), { output: { entity: "chat_admin_change" }, effects: REMOTE_WRITE })
175
175
  .summary(picks ? "make a member an admin with these rights" : "make a member an admin, with every right")
176
176
  .description(picks
177
177
  ? "make a member an admin with these rights"
@@ -185,7 +185,7 @@ const adminsCommand = (messenger) => {
185
185
  const rights = picks ? rightsOf(offered, this.opts().can) : [...offered];
186
186
  renderItem(this, context, await context.withServices((services) => services.admin.addAdmin(chat, person, rights)));
187
187
  }));
188
- admins.addCommand(annotate(new Command("remove"), { effects: REMOTE_WRITE })
188
+ admins.addCommand(annotate(new Command("remove"), { output: { entity: "chat_admin_change" }, effects: REMOTE_WRITE })
189
189
  .description("take an admin's rights back; they stay a member")
190
190
  .argument("<chat>", messenger.chatArgument)
191
191
  .argument("<person>", "an id, or part of a name")
@@ -223,7 +223,7 @@ const requestsCommand = (messenger) => {
223
223
  ["accept", true, "let them in; the group sees them join"],
224
224
  ["decline", false, "turn the request away"],
225
225
  ]) {
226
- const answer = annotate(new Command(verb), { effects: REMOTE_WRITE })
226
+ const answer = annotate(new Command(verb), { output: { entity: "chat_request_change" }, effects: REMOTE_WRITE })
227
227
  .description(help)
228
228
  .argument("<chat>", messenger.chatArgument)
229
229
  .argument(everyone ? "[person]" : "<person>", "who asked: an id from `chats requests list`");
@@ -247,7 +247,7 @@ const requestsCommand = (messenger) => {
247
247
  };
248
248
  /** `chats create`, `join`, `leave`, `update` and `link`: the help text is max-cli's, the one both tools say (standard, help rule 2). */
249
249
  export const groupCommands = (messenger) => [
250
- annotate(new Command("create"), { effects: REMOTE_WRITE })
250
+ annotate(new Command("create"), { output: { entity: "chat_group_change" }, effects: REMOTE_WRITE })
251
251
  .description("create a group or a channel; the people added are told")
252
252
  .argument("<title>", "the group's name")
253
253
  .argument("[person...]", "people to add: an id, or part of a name")
@@ -257,14 +257,14 @@ export const groupCommands = (messenger) => [
257
257
  const { channel } = this.opts();
258
258
  renderItem(this, context, await context.withServices((services) => services.admin.create({ title, people, channel: channel === true })));
259
259
  }),
260
- annotate(new Command("join"), { effects: REMOTE_WRITE })
260
+ annotate(new Command("join"), { output: { entity: "chat_group_change" }, effects: REMOTE_WRITE })
261
261
  .description("join a group or channel by its link; the others in it see that you joined")
262
262
  .argument("<link>", `an invite link, or a public one`)
263
263
  .action(async function (link) {
264
264
  const context = messengerContext(this, messenger);
265
265
  renderItem(this, context, await context.withServices((services) => services.admin.join(link)));
266
266
  }),
267
- annotate(new Command("leave"), { effects: REMOTE_WRITE })
267
+ annotate(new Command("leave"), { output: { entity: "chat_removal" }, effects: REMOTE_WRITE })
268
268
  .description("leave a group or channel; the others in it see that you left")
269
269
  .argument("<chat>", messenger.chatArgument)
270
270
  .action(async function (chat) {
@@ -18,7 +18,10 @@ export const contactWriteCommands = (messenger) => {
18
18
  ["remove", "remove a person from your contacts; the chat stays, a name you gave them may not"],
19
19
  ["block", "stop a person from writing to you — they need not be a contact"],
20
20
  ["unblock", "let a blocked person write to you again"],
21
- ].map(([name, description, summary]) => annotate(new Command(name), { effects: REMOTE_WRITE })
21
+ ].map(([name, description, summary]) => annotate(new Command(name), {
22
+ effects: REMOTE_WRITE,
23
+ output: { entity: name === "add" ? "contact_update" : "contact_state_change" },
24
+ })
22
25
  .summary(summary ?? description)
23
26
  .description(description)
24
27
  .argument("<person>", PERSON)
@@ -27,7 +30,7 @@ export const contactWriteCommands = (messenger) => {
27
30
  const done = await context.withServices((services) => services.people[name](person));
28
31
  renderAnswer(this, context, itemOf(snakeKeys(done)));
29
32
  }));
30
- const rename = annotate(new Command("rename"), { effects: REMOTE_WRITE })
33
+ const rename = annotate(new Command("rename"), { effects: REMOTE_WRITE, output: { entity: "contact_update" } })
31
34
  .summary("change a contact's name in the messenger address book")
32
35
  .description("rename the contact in the messenger address book; use contacts alias for a private local name")
33
36
  .argument("<person>", PERSON)
@@ -39,7 +42,7 @@ export const contactWriteCommands = (messenger) => {
39
42
  renderAnswer(this, context, itemOf(snakeKeys(renamed)));
40
43
  });
41
44
  /** A file rather than argv: these are phone numbers, and argv is read by `ps` and kept by the shell. */
42
- const importing = annotate(new Command("import"), { effects: REMOTE_WRITE })
45
+ const importing = annotate(new Command("import"), { effects: REMOTE_WRITE, output: { entity: "contact_import" } })
43
46
  .description("upload phone numbers and add the people the messenger has under them")
44
47
  .argument("<file>", "one person per line: number, then a comma, a tab or a semicolon, then the name")
45
48
  .action(async function (file) {
@@ -57,7 +57,7 @@ export const foldersCommand = (messenger) => {
57
57
  const found = await context.withServices((services) => services.folders.show(folder));
58
58
  renderAnswer(this, context, itemOf(folderAnswer(found)));
59
59
  }));
60
- folders.addCommand(withRules(annotate(new Command("create"), { effects: REMOTE_WRITE })
60
+ folders.addCommand(withRules(annotate(new Command("create"), { output: { entity: "chat_folder_change" }, effects: REMOTE_WRITE })
61
61
  .description("create a chat folder")
62
62
  .argument("<title>", "the folder's name; the app may refuse a long one")
63
63
  .option("--chat <chat>", "a chat to put in it, by id or name; repeat it for more", collect), messenger, false).action(async function (title) {
@@ -66,7 +66,7 @@ export const foldersCommand = (messenger) => {
66
66
  const rules = rulesOf(this);
67
67
  renderItem(this, context, await context.withServices((services) => services.folders.create(title, chat, rules)));
68
68
  }));
69
- folders.addCommand(withRules(annotate(new Command("update"), { effects: REMOTE_WRITE })
69
+ folders.addCommand(withRules(annotate(new Command("update"), { output: { entity: "chat_folder_change" }, effects: REMOTE_WRITE })
70
70
  .description("rename a folder, or change which chats are in it")
71
71
  .argument("<folder>", "folder id, or its title exactly")
72
72
  .option("--title <title>", "a new name")
@@ -82,7 +82,7 @@ export const foldersCommand = (messenger) => {
82
82
  };
83
83
  renderItem(this, context, await context.withServices((services) => services.folders.update(folder, edit)));
84
84
  }));
85
- folders.addCommand(annotate(new Command("delete"), { effects: REMOTE_WRITE })
85
+ folders.addCommand(annotate(new Command("delete"), { output: { entity: "chat_folder_removal" }, effects: REMOTE_WRITE })
86
86
  .description("delete a folder; the chats in it stay")
87
87
  .argument("<folder>", "folder id, or its title exactly")
88
88
  .action(async function (folder) {
@@ -90,7 +90,7 @@ export const foldersCommand = (messenger) => {
90
90
  renderItem(this, context, await context.withServices((services) => services.folders.delete(folder)));
91
91
  }));
92
92
  if (messenger.folderOrder !== false)
93
- folders.addCommand(annotate(new Command("order"), { effects: REMOTE_WRITE })
93
+ folders.addCommand(annotate(new Command("order"), { output: { entity: "chat_folder_order" }, effects: REMOTE_WRITE })
94
94
  .description("put folders in this order; the ones not named keep theirs after them")
95
95
  .argument("<folders...>", "folder ids, or titles exactly, first one first")
96
96
  .action(async function (typed) {
@@ -98,7 +98,7 @@ export const foldersCommand = (messenger) => {
98
98
  renderItem(this, context, await context.withServices((services) => services.folders.order(typed)));
99
99
  }));
100
100
  if (messenger.folderJoin !== false)
101
- folders.addCommand(annotate(new Command("join"), { effects: REMOTE_WRITE })
101
+ folders.addCommand(annotate(new Command("join"), { output: { entity: "chat_folder_change" }, effects: REMOTE_WRITE })
102
102
  .description("add a folder someone shared by a link; joins every chat in it, and the others there see you joined")
103
103
  .argument("<link>", "the folder's link, as t.me/addlist/…")
104
104
  .action(async function (link) {
@@ -12,14 +12,14 @@ import { messengerContext } from "./context.js";
12
12
  /** `chats rules show|set|unset`: the rules live in a file of this profile, and only the chat's id is asked of the messenger. */
13
13
  export const rulesCommand = (messenger) => {
14
14
  const rules = new Command("rules").description("what `chats moderate` judges a group by, kept in a file of this profile");
15
- annotate(rules.command("show"), { effects: LOCAL_READ })
15
+ annotate(rules.command("show"), { output: { entity: "chat_rules" }, effects: LOCAL_READ })
16
16
  .description("the group's rules; the defaults, marked not saved, if it has none yet")
17
17
  .argument("<chat>", messenger.chatArgument)
18
18
  .action(async function (chat) {
19
19
  const context = messengerContext(this, messenger);
20
20
  renderItem(this, context, await context.withServices((services) => services.moderation.rules(chat)));
21
21
  });
22
- rules.addCommand(grantsAuthority(annotate(new Command("set"), { effects: LOCAL_WRITE }))
22
+ rules.addCommand(grantsAuthority(annotate(new Command("set"), { output: { entity: "chat_rules" }, effects: LOCAL_WRITE }))
23
23
  .description("change one rule; the group's first change writes every rule with its default")
24
24
  .argument("<chat>", messenger.chatArgument)
25
25
  .argument("<key>", `one of: ${RULE_KEYS.join(", ")}`)
@@ -28,7 +28,7 @@ export const rulesCommand = (messenger) => {
28
28
  const context = messengerContext(this, messenger);
29
29
  renderItem(this, context, await context.withServices((services) => services.moderation.set(chat, key, value)));
30
30
  }));
31
- rules.addCommand(grantsAuthority(annotate(new Command("unset"), { effects: LOCAL_WRITE }))
31
+ rules.addCommand(grantsAuthority(annotate(new Command("unset"), { output: { entity: "chat_rules" }, effects: LOCAL_WRITE }))
32
32
  .description("put one rule back to its default")
33
33
  .argument("<chat>", messenger.chatArgument)
34
34
  .argument("<key>", `one of: ${RULE_KEYS.join(", ")}`)
@@ -43,7 +43,7 @@ export const rulesCommand = (messenger) => {
43
43
  * for what the rules name, within each action's level in the group's rules. Nothing watches groups
44
44
  * in the background.
45
45
  */
46
- export const moderateCommand = (messenger) => annotate(new Command("moderate"), { effects: REMOTE_WRITE })
46
+ export const moderateCommand = (messenger) => annotate(new Command("moderate"), { output: { entity: "chat_moderation" }, effects: REMOTE_WRITE })
47
47
  .description("judge a group's new messages and members by its rules, and act as they allow")
48
48
  .argument("<chat>", messenger.chatArgument)
49
49
  .option("--since-time <time>", "judge what came after this ISO 8601 time, or 2h / 1d ago; the saved point stays")