@wirecat/cli-core 0.399.0 → 0.401.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 (259) hide show
  1. package/CHANGELOG.md +111 -0
  2. package/README.md +1 -1
  3. package/dist/analysis/consents.d.ts +11 -6
  4. package/dist/analysis/consents.js +5 -2
  5. package/dist/cli/bot/mcp.js +2 -1
  6. package/dist/cli/bot/store.js +1 -1
  7. package/dist/cli/config-command.js +20 -1
  8. package/dist/cli/context.d.ts +2 -0
  9. package/dist/cli/context.js +1 -1
  10. package/dist/cli/effects.d.ts +4 -1
  11. package/dist/cli/effects.js +10 -2
  12. package/dist/cli/index.d.ts +3 -2
  13. package/dist/cli/index.js +3 -2
  14. package/dist/cli/json-fields.js +12 -1
  15. package/dist/cli/meeting-evidence-answer.d.ts +46 -0
  16. package/dist/cli/meeting-evidence-answer.js +22 -0
  17. package/dist/cli/meeting-preparation-answer.js +1 -13
  18. package/dist/cli/messenger/account-command.js +15 -6
  19. package/dist/cli/messenger/account-profile-query.d.ts +2 -0
  20. package/dist/cli/messenger/account-profile-query.js +13 -0
  21. package/dist/cli/messenger/account-sessions-command.js +1 -2
  22. package/dist/cli/messenger/admin-commands.js +12 -12
  23. package/dist/cli/messenger/admin-contacts-command.js +6 -3
  24. package/dist/cli/messenger/admin-folders-command.js +5 -5
  25. package/dist/cli/messenger/admin-moderation-command.js +4 -4
  26. package/dist/cli/messenger/admin-statistics-command.js +6 -14
  27. package/dist/cli/messenger/analysis-consents-query.d.ts +5 -0
  28. package/dist/cli/messenger/analysis-consents-query.js +20 -0
  29. package/dist/cli/messenger/archive-commands.js +3 -3
  30. package/dist/cli/messenger/attachments-command.js +3 -0
  31. package/dist/cli/messenger/backfill-command.js +5 -4
  32. package/dist/cli/messenger/chat-queries.d.ts +1 -0
  33. package/dist/cli/messenger/chat-queries.js +12 -2
  34. package/dist/cli/messenger/chats-command.js +2 -2
  35. package/dist/cli/messenger/chats-documents-command.js +7 -5
  36. package/dist/cli/messenger/chats-members-command.js +7 -3
  37. package/dist/cli/messenger/chats-read-command.js +1 -1
  38. package/dist/cli/messenger/chats-tracking-command.js +14 -6
  39. package/dist/cli/messenger/contacts-command.js +9 -5
  40. package/dist/cli/messenger/conversations-command.js +20 -16
  41. package/dist/cli/messenger/doctor-command.js +4 -1
  42. package/dist/cli/messenger/document-links-query.d.ts +2 -0
  43. package/dist/cli/messenger/document-links-query.js +9 -0
  44. package/dist/cli/messenger/download-command.js +1 -0
  45. package/dist/cli/messenger/flood-command.d.ts +2 -2
  46. package/dist/cli/messenger/flood-command.js +3 -3
  47. package/dist/cli/messenger/gaps-command.js +2 -2
  48. package/dist/cli/messenger/guard-commands.js +35 -13
  49. package/dist/cli/messenger/images-command.js +1 -0
  50. package/dist/cli/messenger/media-archive-command.js +2 -0
  51. package/dist/cli/messenger/media-archive-move-command.js +1 -0
  52. package/dist/cli/messenger/media-archive-timer-command.js +1 -1
  53. package/dist/cli/messenger/mentions-command.js +15 -2
  54. package/dist/cli/messenger/message-writes.d.ts +192 -2
  55. package/dist/cli/messenger/message-writes.js +6 -2
  56. package/dist/cli/messenger/messages-command.js +2 -0
  57. package/dist/cli/messenger/messages-delete-command.js +1 -0
  58. package/dist/cli/messenger/messages-edit-command.js +1 -0
  59. package/dist/cli/messenger/messages-evidence-command.js +3 -1
  60. package/dist/cli/messenger/messages-forward-command.js +1 -0
  61. package/dist/cli/messenger/messages-pin-command.js +3 -0
  62. package/dist/cli/messenger/messages-send-command.js +10 -6
  63. package/dist/cli/messenger/metadata-command.js +6 -1
  64. package/dist/cli/messenger/models-command.js +10 -5
  65. package/dist/cli/messenger/polls-command.js +4 -0
  66. package/dist/cli/messenger/private-people-command.js +3 -3
  67. package/dist/cli/messenger/rankings-command.js +6 -11
  68. package/dist/cli/messenger/reactions-command.js +2 -0
  69. package/dist/cli/messenger/recipients-query.d.ts +5 -0
  70. package/dist/cli/messenger/recipients-query.js +18 -0
  71. package/dist/cli/messenger/records-command.js +13 -8
  72. package/dist/cli/messenger/review.js +1 -1
  73. package/dist/cli/messenger/schedule-command.js +2 -2
  74. package/dist/cli/messenger/search-answers.js +2 -0
  75. package/dist/cli/messenger/search-command.js +3 -1
  76. package/dist/cli/messenger/searches-command.js +5 -1
  77. package/dist/cli/messenger/sends-query.d.ts +5 -0
  78. package/dist/cli/messenger/sends-query.js +33 -0
  79. package/dist/cli/messenger/serve-command.js +1 -0
  80. package/dist/cli/messenger/server-command.js +0 -3
  81. package/dist/cli/messenger/statistics-query.d.ts +14 -0
  82. package/dist/cli/messenger/statistics-query.js +143 -0
  83. package/dist/cli/messenger/stats-command.js +5 -5
  84. package/dist/cli/messenger/store-maintenance-command.js +10 -10
  85. package/dist/cli/messenger/stored.js +4 -1
  86. package/dist/cli/messenger/tags-command.js +7 -1
  87. package/dist/cli/messenger/tasks-command.js +57 -14
  88. package/dist/cli/messenger/transcribe-command.js +1 -0
  89. package/dist/cli/messenger/watch-answers.d.ts +6 -0
  90. package/dist/cli/messenger/watch-answers.js +69 -0
  91. package/dist/cli/messenger/watch-command.d.ts +1 -1
  92. package/dist/cli/messenger/watch-command.js +56 -7
  93. package/dist/cli/program.js +5 -1
  94. package/dist/cli/runs/command.js +25 -38
  95. package/dist/cli/runs/query.d.ts +5 -0
  96. package/dist/cli/runs/query.js +32 -0
  97. package/dist/cli/runs/search-command.d.ts +4 -0
  98. package/dist/cli/runs/search-command.js +27 -0
  99. package/dist/cli/runs/search-query.d.ts +4 -0
  100. package/dist/cli/runs/search-query.js +50 -0
  101. package/dist/cli/runs/search.d.ts +2 -1
  102. package/dist/cli/runs/search.js +6 -6
  103. package/dist/documents/index.d.ts +1 -0
  104. package/dist/documents/index.js +1 -0
  105. package/dist/domain/attachments.js +9 -1
  106. package/dist/domain/rankings-options.js +3 -3
  107. package/dist/entities/account-change.d.ts +89 -0
  108. package/dist/entities/account-change.js +20 -0
  109. package/dist/entities/account-profile.d.ts +7 -0
  110. package/dist/entities/account-profile.js +8 -0
  111. package/dist/entities/analysis-consent.d.ts +9 -0
  112. package/dist/entities/analysis-consent.js +4 -0
  113. package/dist/entities/attachment-results.d.ts +96 -0
  114. package/dist/entities/attachment-results.js +31 -0
  115. package/dist/entities/batch-result.d.ts +27 -0
  116. package/dist/entities/batch-result.js +18 -0
  117. package/dist/entities/chat-admin.d.ts +234 -0
  118. package/dist/entities/chat-admin.js +84 -0
  119. package/dist/entities/chat-change.d.ts +25 -0
  120. package/dist/entities/chat-change.js +11 -0
  121. package/dist/entities/chat-metadata.d.ts +34 -0
  122. package/dist/entities/chat-metadata.js +15 -0
  123. package/dist/entities/chat-moderation.d.ts +44 -0
  124. package/dist/entities/chat-moderation.js +37 -0
  125. package/dist/entities/chat-tracking.d.ts +26 -0
  126. package/dist/entities/chat-tracking.js +23 -0
  127. package/dist/entities/contact-activity.d.ts +52 -0
  128. package/dist/entities/contact-activity.js +42 -0
  129. package/dist/entities/contact-context.d.ts +1568 -0
  130. package/dist/entities/contact-context.js +191 -0
  131. package/dist/entities/contact-write.d.ts +22 -0
  132. package/dist/entities/contact-write.js +6 -0
  133. package/dist/entities/conversation-preparation.d.ts +63 -0
  134. package/dist/entities/conversation-preparation.js +23 -0
  135. package/dist/entities/conversation-results.d.ts +567 -0
  136. package/dist/entities/conversation-results.js +94 -0
  137. package/dist/entities/document-link.d.ts +13 -0
  138. package/dist/entities/document-link.js +11 -0
  139. package/dist/entities/extraction-report.d.ts +95 -0
  140. package/dist/entities/extraction-report.js +34 -0
  141. package/dist/entities/hit.d.ts +4 -0
  142. package/dist/entities/hit.js +4 -0
  143. package/dist/entities/image-embedding.d.ts +9 -0
  144. package/dist/entities/image-embedding.js +10 -0
  145. package/dist/entities/index.d.ts +8284 -1921
  146. package/dist/entities/index.js +178 -3
  147. package/dist/entities/listener.d.ts +2150 -0
  148. package/dist/entities/listener.js +46 -0
  149. package/dist/entities/media-archive.d.ts +43 -0
  150. package/dist/entities/media-archive.js +32 -0
  151. package/dist/entities/meeting-evidence.d.ts +86 -0
  152. package/dist/entities/meeting-evidence.js +25 -0
  153. package/dist/entities/meeting-preparation-parts.d.ts +14 -0
  154. package/dist/entities/meeting-preparation-parts.js +15 -13
  155. package/dist/entities/meeting-task-proposal.d.ts +14 -0
  156. package/dist/entities/meeting-task-proposal.js +19 -0
  157. package/dist/entities/meeting.d.ts +1 -1
  158. package/dist/entities/meeting.js +1 -1
  159. package/dist/entities/mention.d.ts +63 -0
  160. package/dist/entities/mention.js +44 -0
  161. package/dist/entities/message-change.d.ts +19 -0
  162. package/dist/entities/message-change.js +6 -0
  163. package/dist/entities/message-context.d.ts +617 -0
  164. package/dist/entities/message-context.js +20 -0
  165. package/dist/entities/message-read.d.ts +73 -0
  166. package/dist/entities/message-read.js +58 -0
  167. package/dist/entities/message-transfer.d.ts +379 -0
  168. package/dist/entities/message-transfer.js +83 -0
  169. package/dist/entities/message-write.d.ts +378 -0
  170. package/dist/entities/message-write.js +10 -0
  171. package/dist/entities/model-key.d.ts +14 -0
  172. package/dist/entities/model-key.js +10 -0
  173. package/dist/entities/poll.d.ts +40 -0
  174. package/dist/entities/poll.js +16 -0
  175. package/dist/entities/private-contact.d.ts +10 -0
  176. package/dist/entities/private-contact.js +4 -0
  177. package/dist/entities/reaction-change.d.ts +7 -0
  178. package/dist/entities/reaction-change.js +8 -0
  179. package/dist/entities/recipient.d.ts +29 -0
  180. package/dist/entities/recipient.js +11 -0
  181. package/dist/entities/recorded-run.d.ts +103 -0
  182. package/dist/entities/recorded-run.js +44 -0
  183. package/dist/entities/saved-search.d.ts +4 -0
  184. package/dist/entities/saved-search.js +2 -0
  185. package/dist/entities/send-attempt.d.ts +31 -0
  186. package/dist/entities/send-attempt.js +29 -0
  187. package/dist/entities/store-maintenance.d.ts +319 -0
  188. package/dist/entities/store-maintenance.js +145 -0
  189. package/dist/entities/store-records.d.ts +442 -0
  190. package/dist/entities/store-records.js +99 -0
  191. package/dist/entities/tag-change.d.ts +74 -0
  192. package/dist/entities/tag-change.js +31 -0
  193. package/dist/entities/task-pair.d.ts +31 -0
  194. package/dist/entities/task-pair.js +23 -0
  195. package/dist/entities/task-results.d.ts +524 -0
  196. package/dist/entities/task-results.js +142 -0
  197. package/dist/errors/reasons.js +3 -0
  198. package/dist/mcp/agent.d.ts +3 -1
  199. package/dist/mcp/agent.js +17 -10
  200. package/dist/mcp/bot/server.d.ts +2 -0
  201. package/dist/mcp/bot/server.js +1 -0
  202. package/dist/mcp/server.js +3 -1
  203. package/dist/mcp/tools/attachments.js +1 -0
  204. package/dist/meetings/cli/index.js +4 -2
  205. package/dist/meetings/cli/passage.js +1 -0
  206. package/dist/meetings/index.d.ts +2 -1
  207. package/dist/meetings/index.js +1 -0
  208. package/dist/meetings/links.d.ts +16 -0
  209. package/dist/meetings/read-answers.d.ts +185 -0
  210. package/dist/meetings/read-answers.js +213 -0
  211. package/dist/ports/messenger.d.ts +10 -1
  212. package/dist/runs/run.d.ts +10 -2
  213. package/dist/runs/run.js +11 -3
  214. package/dist/sends/guard.d.ts +1 -1
  215. package/dist/sends/guard.js +9 -5
  216. package/dist/sends/journal.d.ts +9 -2
  217. package/dist/sends/journal.js +9 -2
  218. package/dist/sends/permissions.d.ts +5 -1
  219. package/dist/sends/permissions.js +42 -2
  220. package/dist/sends/recipients.d.ts +8 -0
  221. package/dist/sends/recipients.js +9 -0
  222. package/dist/sends/upload.d.ts +4 -4
  223. package/dist/sends/upload.js +24 -20
  224. package/dist/services/admin-statistics.js +3 -3
  225. package/dist/services/chat-documents.d.ts +1 -0
  226. package/dist/services/chat-documents.js +9 -3
  227. package/dist/services/chats.d.ts +1 -1
  228. package/dist/services/chats.js +5 -2
  229. package/dist/services/documents/mirrors.d.ts +98 -0
  230. package/dist/services/documents/mirrors.js +144 -0
  231. package/dist/services/meeting-links.d.ts +2 -17
  232. package/dist/services/messages.js +1 -0
  233. package/dist/services/search-all-meetings.js +21 -8
  234. package/dist/services/task-rules.d.ts +1 -1
  235. package/dist/services/tasks.d.ts +2 -2
  236. package/dist/services/tasks.js +1 -1
  237. package/dist/store/exports.d.ts +1 -1
  238. package/dist/store/exports.js +1 -1
  239. package/dist/store/messages.d.ts +3 -1
  240. package/dist/store/messages.js +2 -2
  241. package/dist/store/roster.d.ts +4 -1
  242. package/dist/store/roster.js +2 -2
  243. package/dist/store/sqlite/chats.d.ts +12 -0
  244. package/dist/store/sqlite/chats.js +1 -0
  245. package/dist/store/sqlite/document-links.d.ts +1 -1
  246. package/dist/store/sqlite/document-links.js +5 -5
  247. package/dist/store/sqlite/drizzle/bun.js +3 -3
  248. package/dist/store/sqlite/drizzle/bun.js.map +1 -1
  249. package/dist/store/sqlite/drizzle/core.js +2 -2
  250. package/dist/store/sqlite/drizzle/core.js.map +1 -1
  251. package/dist/store/sqlite/drizzle/node.js +3 -3
  252. package/dist/store/sqlite/drizzle/node.js.map +1 -1
  253. package/dist/store/sqlite/drizzle/{shared-O7CTKP45.js → shared-HKXF6HKX.js} +57 -57
  254. package/dist/store/sqlite/drizzle/{shared-O7CTKP45.js.map → shared-HKXF6HKX.js.map} +1 -1
  255. package/dist/store/sqlite/involvements.js +2 -1
  256. package/dist/store/sqlite/roster.d.ts +2 -1
  257. package/dist/store/sqlite/roster.js +3 -2
  258. package/package.json +2 -2
  259. package/parity.json +14 -69
@@ -83,8 +83,8 @@ export const callsCommand = (messenger) => {
83
83
  /** `chats delete` and `chats clear` — for this account only; at the default level they ask first. */
84
84
  export const chatDeletionCommands = (messenger) => [
85
85
  annotate(new Command("delete"), {
86
+ output: { entity: "chat_removal" },
86
87
  effects: REMOTE_WRITE,
87
- mcp: false,
88
88
  examples: [`${messenger.app.command} personal chats delete "Book club"`],
89
89
  })
90
90
  .description("delete a chat from this account; the others in it keep it and its messages")
@@ -95,8 +95,8 @@ export const chatDeletionCommands = (messenger) => [
95
95
  renderAnswer(this, context, itemOf(snakeKeys(await context.withServices((services) => services.admin.deleteChat(chat)))));
96
96
  }),
97
97
  annotate(new Command("clear"), {
98
+ output: { entity: "chat_removal" },
98
99
  effects: REMOTE_WRITE,
99
- mcp: false,
100
100
  examples: [`${messenger.app.command} personal chats clear "Book club"`],
101
101
  })
102
102
  .description("delete every message in a chat for this account; the others in it keep theirs")
@@ -110,6 +110,7 @@ export const chatDeletionCommands = (messenger) => [
110
110
  /** `chats mute` and `unmute` — the owner's own notifications for one chat; nobody in it is told. */
111
111
  export const muteCommands = (messenger) => [
112
112
  annotate(new Command("mute"), {
113
+ output: { entity: "chat_muting" },
113
114
  effects: REMOTE_WRITE,
114
115
  examples: [
115
116
  `${messenger.app.command} chats mute "Book club"`,
@@ -126,6 +127,7 @@ export const muteCommands = (messenger) => [
126
127
  renderAnswer(this, context, itemOf(snakeKeys(await context.withServices((services) => services.account.mute(chat, when)))));
127
128
  }),
128
129
  annotate(new Command("unmute"), {
130
+ output: { entity: "chat_muting" },
129
131
  effects: REMOTE_WRITE,
130
132
  examples: [`${messenger.app.command} chats unmute "Book club"`],
131
133
  })
@@ -136,7 +138,8 @@ export const muteCommands = (messenger) => [
136
138
  renderAnswer(this, context, itemOf(snakeKeys(await context.withServices((services) => services.account.mute(chat, null)))));
137
139
  }),
138
140
  ];
139
- const onChatCommand = (messenger, name, description, change) => annotate(new Command(name), {
141
+ const onChatCommand = (messenger, name, description, entity, change) => annotate(new Command(name), {
142
+ output: { entity },
140
143
  effects: REMOTE_WRITE,
141
144
  examples: [`${messenger.app.command} chats ${name} 123456789`],
142
145
  })
@@ -148,16 +151,16 @@ const onChatCommand = (messenger, name, description, change) => annotate(new Com
148
151
  });
149
152
  /** `chats archive` and `unarchive` — the messenger's archive of chats, not the local store. */
150
153
  export const archiveCommands = (messenger) => [
151
- onChatCommand(messenger, "archive", "move a chat into the archive; nobody in it is told", (services, chat) => services.account.archive(chat, true)),
152
- onChatCommand(messenger, "unarchive", "bring an archived chat back to the chat list", (services, chat) => services.account.archive(chat, false)),
154
+ onChatCommand(messenger, "archive", "move a chat into the archive; nobody in it is told", "chat_archive", (services, chat) => services.account.archive(chat, true)),
155
+ onChatCommand(messenger, "unarchive", "bring an archived chat back to the chat list", "chat_archive", (services, chat) => services.account.archive(chat, false)),
153
156
  ];
154
157
  /** `chats pin` and `unpin` — where the chat sits in the owner's list; `messages pin` pins a message for everyone. */
155
158
  export const chatPinCommands = (messenger) => [
156
- onChatCommand(messenger, "pin", "keep a chat at the top of your chat list; nobody in it is told", (services, chat) => services.account.pinChat(chat, true)),
157
- onChatCommand(messenger, "unpin", "let a pinned chat move down the chat list again", (services, chat) => services.account.pinChat(chat, false)),
159
+ onChatCommand(messenger, "pin", "keep a chat at the top of your chat list; nobody in it is told", "chat_pin", (services, chat) => services.account.pinChat(chat, true)),
160
+ onChatCommand(messenger, "unpin", "let a pinned chat move down the chat list again", "chat_pin", (services, chat) => services.account.pinChat(chat, false)),
158
161
  ];
159
162
  /** `chats mark-unread` — undone by `chats mark-read`, which the other side does see. */
160
- export const markUnreadCommand = (messenger) => onChatCommand(messenger, "mark-unread", "mark a chat unread on your own devices; nobody in it is told", (services, chat) => services.account.markUnread(chat));
163
+ export const markUnreadCommand = (messenger) => onChatCommand(messenger, "mark-unread", "mark a chat unread on your own devices; nobody in it is told", "chat_unread", (services, chat) => services.account.markUnread(chat));
161
164
  const AUDIENCES = ["everyone", "contacts", "nobody"];
162
165
  const audienceOf = (flag, value) => {
163
166
  if (!AUDIENCES.includes(value))
@@ -202,6 +205,7 @@ export const privacyCommand = (messenger) => {
202
205
  const keys = messenger.privacySettings ?? PRIVACY_SETTINGS;
203
206
  const privacy = new Command("privacy").description("who may find, call or add the account");
204
207
  annotate(privacy.command("show"), {
208
+ output: { entity: "account_privacy" },
205
209
  effects: REMOTE_READ,
206
210
  examples: [`${tool} account privacy show`, `${tool} account privacy show --json`],
207
211
  })
@@ -214,6 +218,7 @@ export const privacyCommand = (messenger) => {
214
218
  const audience = keys.find((key) => !SWITCHES.has(key));
215
219
  const toggle = keys.find((key) => SWITCHES.has(key));
216
220
  const set = annotate(new Command("set"), {
221
+ output: { entity: "account_privacy_change" },
217
222
  effects: REMOTE_WRITE,
218
223
  examples: [audience, toggle].flatMap((key) => (key === undefined ? [] : [exampleOf(tool, key)])),
219
224
  }).description("change who may find, call or add the account; the settings not named stay");
@@ -113,7 +113,7 @@ const notes = (found, pretty, renderer) => {
113
113
  renderer.note(`${chat.title ?? chat.id}: its admins are not known, so only your answers count`);
114
114
  if (found.tasks && found.tasks.added + found.tasks.closed > 0)
115
115
  renderer.note(`tasks: ${found.tasks.added} opened, ${found.tasks.closed} closed by the rules` +
116
- (found.tasks.review > 0 ? `, ${found.tasks.review} pair(s) to review — tasks duplicates` : ""));
116
+ (found.tasks.review > 0 ? `, ${found.tasks.review} pair(s) to review — tasks duplicates list` : ""));
117
117
  if (found.partial && pretty)
118
118
  renderer.warn(PARTIAL_NOTE);
119
119
  if (found.offline && pretty)
@@ -192,7 +192,7 @@ export const scheduleCommand = (messenger) => {
192
192
  .join("\n");
193
193
  });
194
194
  });
195
- annotate(command.command("run"), { effects: REMOTE_WRITE, mcp: false })
195
+ annotate(command.command("run"), { effects: REMOTE_WRITE })
196
196
  .description("run a schedule now, as its timer would, and wait for it")
197
197
  .argument("<name>")
198
198
  .action(async function (name) {
@@ -216,7 +216,7 @@ export const scheduleCommand = (messenger) => {
216
216
  rmSync(file, { force: true });
217
217
  say({ name, removed: true }, [`Removed ${name}.`]);
218
218
  });
219
- annotate(command.command("exec"), { effects: REMOTE_WRITE, mcp: false })
219
+ annotate(command.command("exec"), { effects: REMOTE_WRITE })
220
220
  .description("what a timer runs, given the schedule's file; to run one by hand, use run")
221
221
  .argument("<file>")
222
222
  .action(async function (file) {
@@ -147,6 +147,8 @@ export const allHitItem = (item) => item.kind === "meeting"
147
147
  timestamp: item.timestamp,
148
148
  excerpt: item.text,
149
149
  offset_ms: item.startMs,
150
+ scope: item.scope,
151
+ id: String(item.id),
150
152
  }
151
153
  : defined({
152
154
  kind: item.kind,
@@ -6,6 +6,7 @@ import { searchNotesOf } from "../../services/notes-search.js";
6
6
  import { LOCAL_READ, REMOTE_READ } from "../effects.js";
7
7
  import { renderAnswer } from "../envelope.js";
8
8
  import { positiveCount } from "../paging.js";
9
+ import { runsSearchCommand } from "../runs/search-command.js";
9
10
  import { commandAt } from "../steps.js";
10
11
  import { messengerContext } from "./context.js";
11
12
  import { conversationsSearchCommand } from "./conversations-command.js";
@@ -159,7 +160,8 @@ export const searchCommand = (messenger, { topics = false } = {}) => {
159
160
  .addCommand(allCommand(messenger))
160
161
  .addCommand(messagesSearchCommand(messenger))
161
162
  .addCommand(notesCommand(messenger))
162
- .addCommand(conversationsSearchCommand(messenger));
163
+ .addCommand(conversationsSearchCommand(messenger))
164
+ .addCommand(runsSearchCommand(messenger.app));
163
165
  if (topics)
164
166
  search.addCommand(topicsSearchCommand(messenger));
165
167
  for (const [path, args] of Object.entries({
@@ -124,7 +124,11 @@ export const searchesCommand = (messenger) => {
124
124
  const deleted = await context.withServices((services) => services.searches.delete(reference));
125
125
  renderAnswer(this, context, itemOf(savedSearchItem(deleted)), () => `deleted ${deleted.name ?? `run ${deleted.id}`}\n`);
126
126
  });
127
- annotate(searches.command("clear"), { effects: LOCAL_WRITE, examples: [`${tool} searches clear`] })
127
+ annotate(searches.command("clear"), {
128
+ output: { entity: "search_history_clear" },
129
+ effects: LOCAL_WRITE,
130
+ examples: [`${tool} searches clear`],
131
+ })
128
132
  .description("empty the history; saved searches stay")
129
133
  .action(async function () {
130
134
  const context = messengerContext(this, messenger);
@@ -0,0 +1,5 @@
1
+ import { type QueryRegistry } from "@wirecat/cli-base/query";
2
+ import type { SendFilter } from "../../sends/journal.js";
3
+ import { type QueryWords } from "../list-query.js";
4
+ export declare const SENDS_QUERY: QueryRegistry;
5
+ export declare const sendFilter: (words: QueryWords) => SendFilter | undefined;
@@ -0,0 +1,33 @@
1
+ import { queryFieldsOf } from "@wirecat/cli-base/query";
2
+ import { sendAttempt } from "../../entities/index.js";
3
+ import { queryTerms } from "../list-query.js";
4
+ export const SENDS_QUERY = {
5
+ entity: "send_attempt",
6
+ backend: "native",
7
+ fields: queryFieldsOf(sendAttempt, {
8
+ native: ["outcome", "chat_id", "operation_id", "error_code", "key"],
9
+ examples: {
10
+ outcome: "outcome:failed",
11
+ chat_id: "chat_id:7",
12
+ operation_id: "operation_id:op-1",
13
+ error_code: "error_code:timeout",
14
+ key: "key:messages.send",
15
+ },
16
+ }).map((field) => field.native
17
+ ? { ...field, operators: field.operators.filter((operator) => operator === "term" || operator === "phrase") }
18
+ : field),
19
+ };
20
+ export const sendFilter = (words) => {
21
+ const terms = queryTerms(words, SENDS_QUERY, "sends list");
22
+ if (terms.size === 0)
23
+ return undefined;
24
+ const outcome = sendAttempt.entries.outcome.options.find((value) => value === terms.get("outcome"));
25
+ const chatId = terms.get("chat_id"), operationId = terms.get("operation_id"), errorCode = terms.get("error_code"), key = terms.get("key");
26
+ return {
27
+ ...(outcome === undefined ? {} : { outcome }),
28
+ ...(chatId === undefined ? {} : { chatId }),
29
+ ...(operationId === undefined ? {} : { operationId }),
30
+ ...(errorCode === undefined ? {} : { errorCode }),
31
+ ...(key === undefined ? {} : { key }),
32
+ };
33
+ };
@@ -31,6 +31,7 @@ export { lockPath, readLock, servingProfiles } from "../../background/lock.js";
31
31
  */
32
32
  export const serveCommand = (messenger) => {
33
33
  const command = annotate(new Command("serve"), {
34
+ output: { entity: "serve_summary" },
34
35
  effects: REMOTE_WRITE,
35
36
  lifetime: "persistent",
36
37
  examples: [`${messenger.app.command} personal serve`],
@@ -185,7 +185,6 @@ export const serverCommand = (messenger, options = {}) => {
185
185
  const withIdle = (sub) => options.idle ? sub.option("--idle <duration>", "stop after this long with nobody using it — 15m, 1h") : sub;
186
186
  withIdle(destroysNothing(annotate(command.command("start"), {
187
187
  effects: LOCAL_WRITE,
188
- mcp: false,
189
188
  examples: [`${app.command} personal server start`],
190
189
  }))
191
190
  .summary("start this profile's background server")
@@ -194,7 +193,6 @@ export const serverCommand = (messenger, options = {}) => {
194
193
  });
195
194
  destroysNothing(annotate(command.command("stop"), {
196
195
  effects: LOCAL_WRITE,
197
- mcp: false,
198
196
  examples: [`${app.command} personal server stop`],
199
197
  }))
200
198
  .description("stop this profile's serve — through the unit if it runs under one")
@@ -202,7 +200,6 @@ export const serverCommand = (messenger, options = {}) => {
202
200
  await stop(prepare(this));
203
201
  });
204
202
  withIdle(destroysNothing(annotate(command.command("restart"), {
205
- mcp: false,
206
203
  effects: LOCAL_WRITE,
207
204
  examples: [`${app.command} personal server restart`],
208
205
  })).description("stop it and start it again")).action(async function () {
@@ -0,0 +1,14 @@
1
+ import type { Command } from "commander";
2
+ import { type QueryWords } from "../list-query.js";
3
+ export type StatisticsQuery = "contacts-top" | "messages-top" | "discussion" | "unanswered" | "responses";
4
+ export declare const withStatisticsQuery: (command: Command, report: StatisticsQuery) => Command;
5
+ /** Report predicates become the existing source/aggregate parameters; the message query keeps its syntax. */
6
+ export interface StatisticsFilters {
7
+ text?: string;
8
+ messageKind?: string;
9
+ minMessages?: number;
10
+ minViews?: number;
11
+ maxReplies?: number;
12
+ olderThan?: string;
13
+ }
14
+ export declare const statisticsQuery: (words: QueryWords, report: StatisticsQuery) => StatisticsFilters;
@@ -0,0 +1,143 @@
1
+ import { annotate } from "@wirecat/cli-base/commands";
2
+ import { defineQuery, fieldsHelp, parseLucene, queryError, validateQuery, walkQuery, } from "@wirecat/cli-base/query";
3
+ import { QUERY_FIELDS } from "../../search/lucene/registry.js";
4
+ import { MESSAGE_PARSING } from "../../search/lucene/types.js";
5
+ import { LIST_QUERY_GUIDE, typedQuery } from "../list-query.js";
6
+ const FIELDS = {
7
+ "contacts-top": ["message_kind", "min_messages"],
8
+ "messages-top": ["message_kind"],
9
+ discussion: ["min_views", "max_replies"],
10
+ unanswered: ["older_than"],
11
+ responses: [],
12
+ };
13
+ const PATHS = {
14
+ "contacts-top": "stats contacts top",
15
+ "messages-top": "stats messages top",
16
+ discussion: "stats messages discussion",
17
+ unanswered: "stats messages unanswered",
18
+ responses: "stats contacts responses",
19
+ };
20
+ const FIELD_SPECS = [
21
+ {
22
+ name: "message_kind",
23
+ type: "enum",
24
+ values: ["all", "posts", "comments"],
25
+ operators: ["term", "phrase"],
26
+ example: "message_kind:posts",
27
+ placement: "and",
28
+ relation: "proven posts or comments selected before ranking",
29
+ },
30
+ {
31
+ name: "min_messages",
32
+ type: "num",
33
+ operators: ["term"],
34
+ example: "min_messages:5",
35
+ placement: "and",
36
+ relation: "minimum selected messages per ranked author",
37
+ },
38
+ {
39
+ name: "min_views",
40
+ type: "num",
41
+ operators: ["term"],
42
+ example: "min_views:10",
43
+ placement: "and",
44
+ relation: "minimum known cumulative views of a discussion post",
45
+ },
46
+ {
47
+ name: "max_replies",
48
+ type: "num",
49
+ operators: ["term"],
50
+ example: "max_replies:0",
51
+ placement: "and",
52
+ relation: "maximum observed discussion replies",
53
+ },
54
+ {
55
+ name: "older_than",
56
+ type: "kw",
57
+ operators: ["term", "phrase"],
58
+ example: "older_than:24h",
59
+ placement: "and",
60
+ relation: "minimum age of an unanswered question; positive duration",
61
+ },
62
+ ];
63
+ const fieldsFor = (report) => FIELD_SPECS.filter(({ name }) => FIELDS[report].includes(name));
64
+ export const withStatisticsQuery = (command, report) => {
65
+ const extra = fieldsFor(report);
66
+ const query = defineQuery({
67
+ entity: report.endsWith("top") ? "ranking" : "admin_report",
68
+ backend: "sql",
69
+ fields: [
70
+ ...QUERY_FIELDS.map((field) => ({
71
+ ...field,
72
+ example: field.name === "text" ? "text:invoice" : field.name === "topic" ? "topic:42" : field.example,
73
+ })),
74
+ ...extra,
75
+ ],
76
+ });
77
+ annotate(command, { query });
78
+ command.addHelpText("after", `\n${fieldsHelp({ fields: extra }, LIST_QUERY_GUIDE)}\nReport predicates are positive AND terms; the remaining query selects stored messages.\n`);
79
+ return command;
80
+ };
81
+ export const statisticsQuery = (words, report) => {
82
+ const source = typedQuery(words);
83
+ if (!source.trim())
84
+ return {};
85
+ const ast = parseLucene(source, MESSAGE_PARSING);
86
+ const reserved = new Set(FIELD_SPECS.map(({ name }) => name));
87
+ const hasReport = (node) => walkQuery(node).some(({ field }) => reserved.has(field));
88
+ if (!hasReport(ast.root))
89
+ return { text: source };
90
+ const context = { command: PATHS[report], guide: LIST_QUERY_GUIDE };
91
+ const values = new Map();
92
+ const original = (node) => {
93
+ const piece = source.slice(node.span.start, node.span.end);
94
+ if (node.kind === "boolean") {
95
+ const prefix = /(?:^|[\s(])([a-z_][a-z0-9_-]*)\s*:\s*$/i.exec(source.slice(0, node.span.start));
96
+ return prefix ? `${prefix[1]}:${piece}` : piece;
97
+ }
98
+ const named = /^([a-z_][a-z0-9_-]*)\s*[:<>]/i.exec(piece)?.[1];
99
+ return node.implicit || named?.toLowerCase() === node.field.toLowerCase() ? piece : `${node.field}:${piece}`;
100
+ };
101
+ const prune = (node) => {
102
+ if (!hasReport(node))
103
+ return original(node);
104
+ if (node.kind === "boolean") {
105
+ if (node.clauses.some(({ occur, node: child }) => occur === "should" || (occur === "must_not" && hasReport(child))))
106
+ queryError("query.unsupported-placement", node.span, "report predicates must be positive AND terms", context);
107
+ const kept = node.clauses
108
+ .map(({ occur, node: child }) => {
109
+ const part = prune(child);
110
+ return part === undefined ? undefined : occur === "must_not" ? `NOT (${part})` : part;
111
+ })
112
+ .filter((part) => part !== undefined);
113
+ return kept.length ? kept.map((part) => `(${part})`).join(" AND ") : undefined;
114
+ }
115
+ if (!FIELDS[report].includes(node.field))
116
+ queryError("query.unsupported-field", node.span, `${node.field} is not a filter for ${PATHS[report]}`, context);
117
+ validateQuery({ ...ast, root: node }, { entity: "statistics", backend: "sql", fields: fieldsFor(report) }, context);
118
+ if (values.has(node.field))
119
+ queryError("query.unsupported-placement", node.span, `${node.field} only once here`, context);
120
+ if (["min_messages", "min_views", "max_replies"].includes(node.field)) {
121
+ const value = Number(node.value);
122
+ if (!Number.isSafeInteger(value) || value < (node.field === "min_messages" ? 1 : 0))
123
+ queryError("query.invalid-number", node.span, `${node.field} takes a ${node.field === "min_messages" ? "positive" : "nonnegative"} integer`, context);
124
+ }
125
+ if (node.field === "older_than") {
126
+ const units = { s: 1000, m: 60_000, h: 3_600_000, d: 86_400_000, w: 604_800_000 };
127
+ const duration = Number(node.value.slice(0, -1)) * (units[node.value.at(-1) ?? ""] ?? 0);
128
+ if (!/^\d+(?:\.\d+)?[smhdw]$/.test(node.value) || !Number.isSafeInteger(duration) || duration <= 0)
129
+ queryError("query.invalid-duration", node.span, "older_than takes a positive duration such as 4h or 7d", context);
130
+ }
131
+ values.set(node.field, node.value);
132
+ return undefined;
133
+ };
134
+ const text = prune(ast.root);
135
+ return {
136
+ ...(text ? { text } : {}),
137
+ ...(values.has("message_kind") ? { messageKind: values.get("message_kind")?.toLowerCase() } : {}),
138
+ ...(values.has("min_messages") ? { minMessages: Number(values.get("min_messages")) } : {}),
139
+ ...(values.has("min_views") ? { minViews: Number(values.get("min_views")) } : {}),
140
+ ...(values.has("max_replies") ? { maxReplies: Number(values.get("max_replies")) } : {}),
141
+ ...(values.has("older_than") ? { olderThan: values.get("older_than") } : {}),
142
+ };
143
+ };
@@ -91,15 +91,15 @@ export const statsCommand = (messenger, loadRenderer = chartRenderer) => {
91
91
  "chats retention": '"Book club"',
92
92
  "chats show": '"Book club"',
93
93
  "contacts evidence": '101 --selection "$selection" --component messages',
94
- "contacts responses": '--chat "Project room"',
95
- "contacts top": '--chat "Book club" --limit 10',
94
+ "contacts responses": 'chat:"Project room"',
95
+ "contacts top": 'chat:"Book club" --limit 10',
96
96
  "messages counters refresh": '--chat "Book club" --dry-run',
97
97
  "messages counters show": '--chat "Book club" --limit 10',
98
- "messages discussion": '--chat "Book club"',
98
+ "messages discussion": 'chat:"Book club"',
99
99
  "messages evidence": '"$message" --selection "$selection" --component reactions',
100
100
  "messages show": '--chat "Book club" --by day',
101
- "messages top": '--chat "Book club" --limit 10',
102
- "messages unanswered": '--chat "Project room"',
101
+ "messages top": 'chat:"Book club" --limit 10',
102
+ "messages unanswered": 'chat:"Project room"',
103
103
  "tasks show": '--chat "Project room"',
104
104
  };
105
105
  for (const [path, args] of Object.entries(examples)) {
@@ -92,7 +92,7 @@ const refuseWhileServing = (command, messenger) => {
92
92
  throw new CliError("validation_error", `${messenger.app.command} serve is running for ${serving.join(", ")} — ${serving.map((profile) => `\`${messenger.app.command} ${profile} server stop\``).join(", ")} first`);
93
93
  }
94
94
  };
95
- const repairCommand = (messenger) => annotate(new Command("repair"), { effects: LOCAL_WRITE, mcp: false })
95
+ const repairCommand = (messenger) => annotate(new Command("repair"), { output: { entity: "store_repair" }, effects: LOCAL_WRITE })
96
96
  .summary("repair table shapes while keeping the old tables")
97
97
  .description("bring every table to this build's shape, deleting nothing: a table of the wrong shape is kept as a copy beside a new one")
98
98
  .option("--dry-run", "say what it would do, and change nothing")
@@ -142,7 +142,7 @@ const notesOf = (report, command) => {
142
142
  }
143
143
  return notes;
144
144
  };
145
- const copiesCommand = () => new Command("copies").description("the tables `store repair` kept as copies").addCommand(annotate(new Command("delete"), { effects: LOCAL_WRITE, mcp: false })
145
+ const copiesCommand = () => new Command("copies").description("the tables `store repair` kept as copies").addCommand(annotate(new Command("delete"), { output: { entity: "store_copy_removal" }, effects: LOCAL_WRITE })
146
146
  .description("delete one copy `store repair` kept, named exactly; refuses any other table")
147
147
  .argument("<name>", "the copy's name, as `store repair` printed it")
148
148
  .action(async function (name) {
@@ -152,7 +152,7 @@ const copiesCommand = () => new Command("copies").description("the tables `store
152
152
  const deleted = await reading(path, (database) => deleteCopy(database, name));
153
153
  result(this, { path, deleted });
154
154
  }));
155
- const infoCommand = () => annotate(new Command("info"), { effects: LOCAL_READ })
155
+ const infoCommand = () => annotate(new Command("info"), { output: { entity: "store_info" }, effects: LOCAL_READ })
156
156
  .summary("the local archive's format, size and contents")
157
157
  .description("the store file: where it is, its size, its schema and how many rows it holds; changes nothing")
158
158
  .action(async function () {
@@ -182,7 +182,7 @@ const infoCommand = () => annotate(new Command("info"), { effects: LOCAL_READ })
182
182
  * fetched" (NEED-399 A). Times are the chat's newest message by the messenger's list against the
183
183
  * newest message held, not `sync_ranges`: those are in message ids, which a chat list does not give.
184
184
  */
185
- const checkCommand = (messenger) => annotate(new Command("check"), { effects: LOCAL_READ })
185
+ const checkCommand = (messenger) => annotate(new Command("check"), { output: { entity: "store_check" }, effects: LOCAL_READ })
186
186
  .summary("check the local archive for problems")
187
187
  .description("whether the store is healthy — integrity, search indexes, disk, and which chats are behind")
188
188
  .action(async function () {
@@ -362,7 +362,7 @@ const chatsBehind = (database) => database
362
362
  held: row.held === null ? null : isoOf(row.held),
363
363
  refreshed: isoOf(row.refreshed),
364
364
  }));
365
- const migrateCommand = (messenger) => annotate(new Command("migrate"), { effects: LOCAL_WRITE, mcp: false })
365
+ const migrateCommand = (messenger) => annotate(new Command("migrate"), { output: { entity: "store_migration" }, effects: LOCAL_WRITE })
366
366
  .summary("migrate an older local archive")
367
367
  .description("bring the store up to this build's schema, then normalize, index and stem the messages and notes stored before it")
368
368
  .action(async function () {
@@ -431,7 +431,7 @@ const buildNoteIndex = (database, note, { reset = false } = {}) => {
431
431
  note(`indexing ${pending} notes`);
432
432
  return { notesIndexed: drainNoteIndex(database, stemmerCache()) };
433
433
  };
434
- const reindexCommand = (messenger) => destroysNothing(annotate(new Command("reindex"), { effects: LOCAL_WRITE }))
434
+ const reindexCommand = (messenger) => destroysNothing(annotate(new Command("reindex"), { output: { entity: "store_reindex" }, effects: LOCAL_WRITE }))
435
435
  .summary("rebuild the local archive's search indexes")
436
436
  .description("rebuild the word index, its typo vocabulary, the stems, the files' word index, the notes' indexes and who took part in what, from what is stored; loses nothing")
437
437
  .action(async function () {
@@ -478,7 +478,7 @@ const vacuumInto = async (path, target) => {
478
478
  }
479
479
  };
480
480
  const stampNow = () => new Date().toISOString().replace(/[:.]/g, "-");
481
- const backupCommand = () => destroysNothing(annotate(new Command("backup"), { effects: LOCAL_WRITE, mcp: false }))
481
+ const backupCommand = () => destroysNothing(annotate(new Command("backup"), { output: { entity: "store_backup" }, effects: LOCAL_WRITE }))
482
482
  .description("copy the store into a new file, while it is in use; never overwrites a file")
483
483
  .argument("<file>", "the new file")
484
484
  .option(...ENCRYPT_OPTION)
@@ -522,7 +522,7 @@ const backupCommand = () => destroysNothing(annotate(new Command("backup"), { ef
522
522
  }
523
523
  });
524
524
  /** The only way back into a sealed file: the format is this tool's own. */
525
- const decryptCommand = () => annotate(new Command("decrypt"), { effects: LOCAL_WRITE, mcp: false })
525
+ const decryptCommand = () => annotate(new Command("decrypt"), { output: { entity: "store_decrypt" }, effects: LOCAL_WRITE })
526
526
  .description("open a file written with --encrypt into a new file; asks for its password")
527
527
  .argument("<file>", "a file `store backup --encrypt` or `store export --encrypt` wrote")
528
528
  .requiredOption("--output <file>", "the new file, readable only by you")
@@ -544,7 +544,7 @@ const decryptCommand = () => annotate(new Command("decrypt"), { effects: LOCAL_W
544
544
  * process has the file open — it would go on writing to the file set aside — or while this CLI's
545
545
  * `serve` runs: that opens the store on its first write, so until then it holds nothing to see.
546
546
  */
547
- const restoreCommand = (messenger) => annotate(new Command("restore"), { effects: LOCAL_WRITE, mcp: false })
547
+ const restoreCommand = (messenger) => annotate(new Command("restore"), { output: { entity: "store_restore" }, effects: LOCAL_WRITE })
548
548
  .summary("restore a backup while keeping the current store")
549
549
  .description("put a backup in place of the store; the store it replaces is kept beside it, never deleted")
550
550
  .argument("<file>", "a file `store backup` wrote; one written with --encrypt asks for its password")
@@ -661,7 +661,7 @@ const quiesce = async (path, messenger, then, warn, waitMs = STORE_LOCK_WAIT_MS)
661
661
  * For a store this build cannot migrate: a backup beside it, then an empty store at this build's
662
662
  * schema. The backup is made before anything is deleted, and its path is said before the delete.
663
663
  */
664
- const resetCommand = (messenger) => annotate(new Command("reset"), { effects: LOCAL_WRITE, mcp: false })
664
+ const resetCommand = (messenger) => annotate(new Command("reset"), { output: { entity: "store_reset" }, effects: LOCAL_WRITE })
665
665
  .summary("back up the local archive and replace it with an empty one")
666
666
  .description("back the store up beside itself, then delete it and start an empty one at this build's schema; asks first, or --yes")
667
667
  .option("--no-backup", "delete the store without backing it up first")
@@ -417,7 +417,10 @@ export const stored = (messenger, { account, store, warn, events, pending, annou
417
417
  const sent = await messenger.send(chatId, text, options);
418
418
  // A scheduled message is not in the chat yet, and it will arrive under another id.
419
419
  if (options.at === undefined) {
420
- await save("messages.send", (opened) => saveSent(opened, account, chatId, sent.message, { via: "send" }));
420
+ await save("messages.send", async (opened) => {
421
+ for (const message of sent.messages ?? [sent.message])
422
+ await saveSent(opened, account, chatId, message, { via: "send" });
423
+ });
421
424
  }
422
425
  return sent;
423
426
  },
@@ -32,7 +32,11 @@ export const tagsCommand = (messenger) => {
32
32
  const tags = new Command("tags")
33
33
  .summary("tags on stored messages and other records")
34
34
  .description("your own labels on chats, people, messages and tasks, kept in the local store and never sent; tag: in a search finds them");
35
- changesStore(annotate(tags.command("auto"), { effects: FETCH, examples: [`${tool} tags auto --chat "Book club" --dry-run`] }))
35
+ changesStore(annotate(tags.command("auto"), {
36
+ output: { entity: "automatic_tags" },
37
+ effects: FETCH,
38
+ examples: [`${tool} tags auto --chat "Book club" --dry-run`],
39
+ }))
36
40
  .description("derive local group/channel tags from cached metadata using keyword rules")
37
41
  .option("--chat <chat>", "a stored group/channel; repeat to select several", (value, previous) => [...previous, value], [])
38
42
  .option("--limit <n>", "process at most 1–500 chats", positiveCount("--limit"), 50)
@@ -47,6 +51,7 @@ export const tagsCommand = (messenger) => {
47
51
  renderAnswer(this, context, itemOf(snakeKeys(done)));
48
52
  });
49
53
  targetOptions(annotate(tags.command("add"), {
54
+ output: { entity: "tag_change" },
50
55
  effects: LOCAL_WRITE,
51
56
  examples: [`${tool} tags add reading --chat "Book club"`],
52
57
  }).description("put tags on one chat, person, message or task"), messenger, "tag").action(async function (given) {
@@ -59,6 +64,7 @@ export const tagsCommand = (messenger) => {
59
64
  context.renderer.note(`already there: ${done.unchanged.join(", ")}`);
60
65
  });
61
66
  targetOptions(annotate(tags.command("remove"), {
67
+ output: { entity: "tag_change" },
62
68
  effects: LOCAL_WRITE,
63
69
  examples: [`${tool} tags remove reading --chat "Book club"`],
64
70
  }).description("take tags off one chat, person, message or task"), messenger, "untag")