@wirecat/cli-core 0.397.0 → 0.399.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 (74) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/README.md +12 -9
  3. package/dist/cli/analysis-choice.d.ts +1 -0
  4. package/dist/cli/analysis-choice.js +3 -0
  5. package/dist/cli/bot/admins.js +14 -4
  6. package/dist/cli/bot/command.js +38 -16
  7. package/dist/cli/bot/examples.d.ts +3 -0
  8. package/dist/cli/bot/examples.js +2 -0
  9. package/dist/cli/bot/mcp.js +4 -1
  10. package/dist/cli/bot/messages.js +17 -6
  11. package/dist/cli/bot/moderation.js +16 -4
  12. package/dist/cli/bot/people.js +16 -4
  13. package/dist/cli/bot/setup.js +27 -8
  14. package/dist/cli/bot/watch.js +7 -1
  15. package/dist/cli/commands-command.js +1 -1
  16. package/dist/cli/config-command.js +1 -1
  17. package/dist/cli/embedding-choice.d.ts +2 -1
  18. package/dist/cli/embedding-choice.js +4 -1
  19. package/dist/cli/guide.js +0 -1
  20. package/dist/cli/help/command-help.d.ts +9 -2
  21. package/dist/cli/help/command-help.js +22 -4
  22. package/dist/cli/help/first-screen.js +3 -3
  23. package/dist/cli/messenger/account-command.js +2 -1
  24. package/dist/cli/messenger/admin-commands.js +18 -9
  25. package/dist/cli/messenger/archive-commands.js +2 -2
  26. package/dist/cli/messenger/backfill-command.js +3 -3
  27. package/dist/cli/messenger/catch-up-answers.d.ts +4 -1
  28. package/dist/cli/messenger/catch-up-answers.js +11 -3
  29. package/dist/cli/messenger/chats-command.js +2 -1
  30. package/dist/cli/messenger/chats-stats-command.js +1 -1
  31. package/dist/cli/messenger/complete-command.js +5 -1
  32. package/dist/cli/messenger/contacts-command.js +4 -3
  33. package/dist/cli/messenger/context.d.ts +7 -0
  34. package/dist/cli/messenger/conversations-command.js +27 -13
  35. package/dist/cli/messenger/doctor-command.js +2 -2
  36. package/dist/cli/messenger/flood-command.js +1 -1
  37. package/dist/cli/messenger/hearing-command.d.ts +1 -1
  38. package/dist/cli/messenger/hearing-command.js +1 -1
  39. package/dist/cli/messenger/inbox.js +7 -4
  40. package/dist/cli/messenger/lacks.d.ts +7 -0
  41. package/dist/cli/messenger/lacks.js +18 -1
  42. package/dist/cli/messenger/media-archive-move-command.js +0 -1
  43. package/dist/cli/messenger/media-archive-timer-command.js +2 -2
  44. package/dist/cli/messenger/messages-command.js +2 -1
  45. package/dist/cli/messenger/messages-stats-command.js +1 -1
  46. package/dist/cli/messenger/metadata-command.js +1 -1
  47. package/dist/cli/messenger/models-command.js +5 -2
  48. package/dist/cli/messenger/private-people-command.js +5 -3
  49. package/dist/cli/messenger/replies-command.js +1 -1
  50. package/dist/cli/messenger/replies-edit-command.d.ts +1 -1
  51. package/dist/cli/messenger/replies-edit-command.js +13 -5
  52. package/dist/cli/messenger/retention-command.js +1 -1
  53. package/dist/cli/messenger/review.js +13 -6
  54. package/dist/cli/messenger/schedule-command.js +3 -3
  55. package/dist/cli/messenger/search-command.js +2 -2
  56. package/dist/cli/messenger/searches-command.js +1 -1
  57. package/dist/cli/messenger/serve-command.js +5 -1
  58. package/dist/cli/messenger/serve-relay.d.ts +2 -0
  59. package/dist/cli/messenger/server-command.js +12 -3
  60. package/dist/cli/messenger/stats-command.js +1 -1
  61. package/dist/cli/messenger/store-maintenance-command.js +3 -3
  62. package/dist/cli/messenger/tags-command.js +11 -5
  63. package/dist/cli/messenger/transcribe-command.js +1 -1
  64. package/dist/cli/messenger/watch-command.js +5 -1
  65. package/dist/cli/runs/command.js +2 -3
  66. package/dist/cli/skill-command.js +1 -1
  67. package/dist/domain/models.d.ts +2 -0
  68. package/dist/mcp/prompts.js +8 -2
  69. package/dist/services/deps.d.ts +1 -1
  70. package/dist/services/deps.js +1 -1
  71. package/dist/services/inbox.js +5 -5
  72. package/dist/services/search-kind.js +1 -1
  73. package/package.json +4 -4
  74. package/parity.json +134 -234
package/CHANGELOG.md CHANGED
@@ -5,6 +5,71 @@ 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
+ ## 0.399.0 — 23.10.2026
9
+
10
+ ### Changed — may break callers
11
+
12
+ - **Flags have one meaning.** Use `--analysis-provider` for analysis, `--embedding-provider` for vector computation, `--consent-provider` for revoking a recorded provider identity, `--claim` for tag ownership, `--answer-model` for removing stored answers and `--template-mode` for reply templates. `inbox` and `review` take `kind:group,channel` in their query. Group message lifetime is `--disappear-after`, a duration or `off`. Legacy CLI spellings remain hidden for one release and cannot be combined with their replacements. MCP callers use the canonical argument names and `query` for chat kinds; update saved MCP requests.
13
+
14
+ - cli-core requires cli-base 0.50.2 or newer within the 0.50 line for the approved option roles and messenger vocabulary.
15
+
16
+ - **Private contact notes use `--text-file`.** `--file` continues to work as a hidden compatibility alias; giving both is refused before any note is changed. New help and discovery reserve `--file` for message attachments.
17
+
18
+ ### Fixed
19
+
20
+ - **Count and grouping placeholders match the shared vocabulary.** Processing limits use `<n>` and statistics grouping uses `<group>` across supported fields and calendar units; accepted values and defaults stay the same.
21
+
22
+ ### Added
23
+
24
+ - **The shared `bot` commands have help examples and short summaries.** Each leaf under `bot` shows one to three
25
+ examples in the calling CLI's own words, and every summary fits 80 characters; `bot recipients add|remove` describe
26
+ their `<chat>`. A CLI that baselined these for `command-example-count`, `command-help-too-long` or
27
+ `missing-argument-help` sees "stale baseline entry" and removes the lines; tg, max and wa grow no baseline for them.
28
+ - **`Messenger.joinRequestsByLink`**: whether `chats requests accept|decline --all` can be narrowed with `--link`;
29
+ yes when unset. `joinRequestsAll` now turns on `--all` alone, so a messenger that answers every request at once
30
+ but does not say which invite link each came by (MAX) offers `--all` without `--link`.
31
+
32
+ ## 0.398.0 — 23.10.2026
33
+
34
+ ### Added
35
+
36
+ - Top-level summaries, listeners and shell completion include examples in help and command discovery.
37
+
38
+ - Clearing a remembered flood hold includes an example in help and command discovery.
39
+
40
+ - **`Messenger.never`**: the adapter methods a messenger can never have, as opposed to `lacks`, what it does not have
41
+ yet. Their commands refuse as before and are also left out of help, `commands --json` and MCP. A command a
42
+ messenger only `lacks` stays in sight. For WhatsApp: `endOtherSessions` (only the phone ends other devices) and
43
+ `removeContact`.
44
+
45
+ - **`review --offline` reads the local store** instead of refusing, for every messenger. It shows only what was
46
+ already synced and says so (`missing` in JSON, a warning on the terminal); its next step starts from the same
47
+ point, and `--new` keeps its saved point, so an online review still sees what was never synced.
48
+
49
+ - **MCP offers more commands**, each limited by the profile's permissions: `runs list|show|path`, `store status|info|check|export|reindex`,
50
+ `store jobs cancel|retry|clear`, `attachments move`, `attachments jobs status`, `config show`, `commands schema`, `guide`,
51
+ `skill show`, `doctor report`, the `models` lists, `replies status|test|consents show`, `schedule list`,
52
+ `server status|logs`, and on the bot server `list`, `chats leave`, `chats admins add|remove`, `commands set|clear` and
53
+ `webhooks list|set|delete`. What stays off and why is in [the messenger rules](docs/dev/STANDARD.md#mcp).
54
+
55
+ ### Security
56
+
57
+ - `@modelcontextprotocol/server` 2.3.0 clears the three high-severity advisories `npm audit` reported for every
58
+ WireCat CLI.
59
+
60
+ ### Fixed
61
+
62
+ - **A pasted help example runs.** The `Example:` line of an account command's `--help`, generated or written, names
63
+ the profile `personal` (`tg personal messages list Team`), so it no longer fails with "name a profile".
64
+
65
+ - `search mail` with no mail in the store points at `memo sync --only mail`; it named a `--mail` option memo does not have.
66
+
67
+ - `parity.json` matches tg and max: `guide`, `mentions`, `runs search`, `tasks reject` and `--brief`/`--full` are in
68
+ both, and each gap planned for tg names its tg-cli backlog id.
69
+
70
+ - `search all --help` says the messenger's server is asked by default and names `--backend archive` and `--offline`
71
+ to keep it to the local store.
72
+
8
73
  ## 0.397.0 — 23.10.2026
9
74
 
10
75
  ### Changed — may break callers
package/README.md CHANGED
@@ -4,7 +4,8 @@
4
4
 
5
5
  # @wirecat/cli-core
6
6
 
7
- The engine behind the WireCat command-line tools for Telegram, MAX, mail, notes and meetings. Everything that
7
+ The engine behind the WireCat command-line tools for Telegram, MAX, WhatsApp, mail, notes, meetings and Google
8
+ Drive. Everything that
8
9
  does not depend on one messenger lives here: the data model, a local SQLite store with full-text and semantic
9
10
  search, a Lucene query language, the shared commands, an MCP server for AI agents, and the guard around every
10
11
  write. A messenger plugs in as one adapter and gets all of it.
@@ -36,6 +37,7 @@ write. A messenger plugs in as one adapter and gets all of it.
36
37
  | [Testing](docs/dev/TESTING.md) · [Parity audit](docs/dev/PARITY-AUDIT.md) | The sandbox, the coverage floor, the checks across tg and max |
37
38
  | [Tool boundaries](docs/dev/TOOL-BOUNDARIES.md) | Which tool owns which command |
38
39
  | [API reference](docs/REFERENCE.md) | Every entry point and the detailed contract of each feature |
40
+ | [Roadmap](docs/roadmap.md) | What is being built next for developers on this package |
39
41
  | [Changelog](CHANGELOG.md) | What each version changed, and what may break callers |
40
42
 
41
43
  ## What it is
@@ -72,7 +74,7 @@ It runs on Node 22.16+ and 24+, and on Bun.
72
74
 
73
75
  ```mermaid
74
76
  flowchart TB
75
- tools["tg · max · memo · zm · drive"] --> interface
77
+ tools["tg · max · wa · memo · zm · drive"] --> interface
76
78
  subgraph core ["@wirecat/cli-core"]
77
79
  interface["Interface: CLI commands and MCP tools"] --> services["Services: one use case each"]
78
80
  services --> ports["Ports: MessengerAdapter · MeetingSource · TaskStore"]
@@ -88,7 +90,7 @@ Five layers, and each calls only the layers below it: domain, adapters behind a
88
90
 
89
91
  - **The CLI and the MCP server are two adapters over the same services.** A command and its MCP tool answer with
90
92
  the same data, because one service produces it.
91
- - **A port with one required core and optional groups.** `MessengerCore` is what every messenger must do. Seventeen
93
+ - **A port with one required core and optional groups.** `MessengerCore` is what every messenger must do. The other
92
94
  groups — server reads, polls, forums, reactions and the rest — are optional and reached through `capability()`.
93
95
  A messenger without a group refuses the command before connecting; it never crashes and never silently drops an
94
96
  option.
@@ -116,8 +118,9 @@ code and its document must agree. A search plan that reads an index once per mes
116
118
  - **One query language for every list and every source.** A TypeScript port of the Apache Lucene 9.12.3 parsers,
117
119
  with no runtime parser dependency. Phrases, `AND` / `OR` / `NOT`, `+required`, grouping, field groups like
118
120
  `from:(alice OR bob)`, ranges `date:[2026-01-01 TO 2026-02-01}`, comparisons `size>10MB`, wildcards and regular
119
- expressions. 27 fields: sender, chat, date, file name, MIME type, transcript, subject, label and more. Fuzzy
120
- and proximity operators are refused with an error that names the alternative.
121
+ expressions. 27 fields: sender, chat, date, file name, MIME type, transcript, subject, label and more.
122
+ Proximity `"invoice paid"~3` finds the words close together in one message; the fuzzy operator `word~1` is
123
+ refused with an error that names the alternative.
121
124
  [Syntax](docs/search/query-language.md) · [grammar](docs/search/query-language-spec.md)
122
125
  - **Any three letters inside a word.** An FTS5 trigram index finds `port` inside `report`. A
123
126
  second, word index ranks by BM25.
@@ -211,8 +214,9 @@ they deliberately differ. A rule changes in its own pull request, before the cod
211
214
  - **Built for agents.** `--no-input` never prompts. Input and output are bounded (16 MiB in, 4 MiB out) and fail
212
215
  visibly instead of truncating. One-shot commands time out after 30 seconds and close what they hold. `--dry-run`
213
216
  previews any write.
214
- - **The tool never picks an account.** The profile is the first word of every account command; without one, the
215
- command stops and lists the choices — even when there is only one.
217
+ - **The tool never picks an account.** Every account command names its profile — the first word in tg, max and
218
+ wa, an option such as `--account` or `--profile` in the others; without one, the command stops and lists the
219
+ choices — even when there is only one.
216
220
  - **Run records keep ids and timings, never content.** A failure is kept by default, so `runs show` can explain it
217
221
  without anyone sending a log full of messages.
218
222
  - **Tests that cannot touch real data.** A sandbox keeps every test away from the owner's store, and no test may
@@ -253,8 +257,7 @@ describe("the adapter keeps the port's promises", () => {
253
257
  **Before 1.0, any release may break a caller.** Releases are frequent, sometimes several a day, and any of them
254
258
  may change any entry point. Every such change is listed in the [changelog](CHANGELOG.md) under "Changed — may break
255
259
  callers", with what to change. tg-cli and max-cli pin an exact version and move to a new one in their own pull request.
256
- Pin an exact version too: as of 18.10.2026, 132 of the 321 versions in the changelog break callers, and no patch
257
- release has.
260
+ Pin an exact version too.
258
261
  The full list of entry points and what each holds is in the [API reference](docs/REFERENCE.md).
259
262
 
260
263
  ⚠ **Errors are recognised by shape, not by class** (`isCliFailure`). A package linked during development brings its
@@ -3,6 +3,7 @@ import type { AnalysisProvider } from "../analysis/provider.js";
3
3
  import type { AppIdentity } from "../ports/app.js";
4
4
  import type { Settings } from "./settings.js";
5
5
  export interface AnalysisOptions {
6
+ analysisProvider?: string;
6
7
  provider?: string;
7
8
  model?: string;
8
9
  baseUrl?: string;
@@ -2,6 +2,9 @@ import { CliError } from "@wirecat/cli-base";
2
2
  import { endpoint } from "../analysis/settings.js";
3
3
  import { embeddingKeys, endpointKeyName } from "../embeddings/keys.js";
4
4
  export const analysisChoice = (options, app, settings, env, keyring) => {
5
+ if (options.analysisProvider !== undefined && options.provider !== undefined)
6
+ throw new CliError("validation_error", "give --analysis-provider or its legacy --provider spelling, not both");
7
+ options = { ...options, provider: options.analysisProvider ?? options.provider };
5
8
  const configured = { ...settings.models?.default, ...settings.models?.analysis };
6
9
  settings = {
7
10
  ...settings,
@@ -9,6 +9,7 @@ import { renderAnswer, snakeKeys } from "../envelope.js";
9
9
  import { rightsOf } from "../messenger/admin-commands.js";
10
10
  import { renderItem, wholeList } from "../messenger/chat-answers.js";
11
11
  import { botContext, online } from "./context.js";
12
+ import { botExamples } from "./examples.js";
12
13
  import { botCan } from "./messages.js";
13
14
  const CHAT = "a chat id, or the title of a chat this bot has seen";
14
15
  const PERSON = "the person's user id";
@@ -23,7 +24,7 @@ const personOf = (typed) => {
23
24
  export const botAdminsCommand = (bot) => {
24
25
  const offered = bot.adminRights ?? ADMIN_RIGHTS;
25
26
  const command = new Command("admins").description("the admins of a chat the bot is an admin in");
26
- annotate(command.command("list"), { effects: REMOTE_READ })
27
+ annotate(command.command("list"), { effects: REMOTE_READ, examples: botExamples(bot, "chats admins list Team") })
27
28
  .description("the chat's admins and what each may do")
28
29
  .argument("<chat>", CHAT)
29
30
  .action(async function (chat) {
@@ -35,7 +36,10 @@ export const botAdminsCommand = (bot) => {
35
36
  renderAnswer(this, context, wholeList("bot chats admins list", admins.map(snakeKeys)));
36
37
  });
37
38
  });
38
- annotate(command.command("add"), { effects: REMOTE_WRITE, mcp: false })
39
+ annotate(command.command("add"), {
40
+ effects: REMOTE_WRITE,
41
+ examples: botExamples(bot, "chats admins add Team 42 --title Helper"),
42
+ })
39
43
  .description("make a member an admin with these rights")
40
44
  .argument("<chat>", CHAT)
41
45
  .argument("<person>", PERSON)
@@ -61,7 +65,10 @@ export const botAdminsCommand = (bot) => {
61
65
  });
62
66
  });
63
67
  });
64
- annotate(command.command("remove"), { effects: REMOTE_WRITE, mcp: false })
68
+ annotate(command.command("remove"), {
69
+ effects: REMOTE_WRITE,
70
+ examples: botExamples(bot, "chats admins remove Team 42"),
71
+ })
65
72
  .description("take an admin's rights back; they stay a member")
66
73
  .argument("<chat>", CHAT)
67
74
  .argument("<person>", PERSON)
@@ -85,7 +92,10 @@ export const botAdminsCommand = (bot) => {
85
92
  */
86
93
  export const botMembersCommand = (bot) => {
87
94
  const command = new Command("members").description("the people in a chat the bot is an admin in");
88
- annotate(command.command("remove"), { effects: REMOTE_WRITE })
95
+ annotate(command.command("remove"), {
96
+ effects: REMOTE_WRITE,
97
+ examples: botExamples(bot, "chats members remove Team 42 --block"),
98
+ })
89
99
  .description("take a person out of a chat; their messages stay")
90
100
  .argument("<chat>", CHAT)
91
101
  .argument("<person>", PERSON)
@@ -9,6 +9,7 @@ import { itemOf, renderAnswer, snakeKeys } from "../envelope.js";
9
9
  import { renderItem, wholeList } from "../messenger/chat-answers.js";
10
10
  import { botAdminsCommand, botMembersCommand } from "./admins.js";
11
11
  import { botContext, online } from "./context.js";
12
+ import { botExamples } from "./examples.js";
12
13
  import { botMcpCommand } from "./mcp.js";
13
14
  import { botCan, botIdOf, botMessagesCommand } from "./messages.js";
14
15
  import { botModerateCommand, botRulesCommand } from "./moderation.js";
@@ -25,7 +26,9 @@ const secretSources = (bot, tokens) => bot.secrets?.length
25
26
  : {};
26
27
  const authCommand = (bot, tokenVariable) => {
27
28
  const auth = annotate(new Command("auth"), { mcp: false }).description("the bot token this profile uses");
28
- const set = grantsAuthority(annotate(auth.command("set"), { effects: LOCAL_WRITE })).description(`check a bot token with ${bot.name ?? "the messenger"}, then keep it — typed at a hidden prompt or piped on stdin`);
29
+ const set = grantsAuthority(annotate(auth.command("set"), { effects: LOCAL_WRITE, examples: botExamples(bot, "auth set") }))
30
+ .description(`check a bot token with ${bot.name ?? "the messenger"}, then keep it — typed at a hidden prompt or piped on stdin`)
31
+ .summary("check a bot token, then keep it; or keep a named secret beside it");
29
32
  const secrets = bot.secrets ?? [];
30
33
  if (secrets.length > 0)
31
34
  set.option("--secret <name>", `keep this secret instead of the token, the same way: ${secrets.map((one) => `${one.name} (${one.description})`).join("; ")}`);
@@ -68,7 +71,7 @@ const authCommand = (bot, tokenVariable) => {
68
71
  });
69
72
  });
70
73
  });
71
- annotate(auth.command("show"), { effects: LOCAL_READ })
74
+ annotate(auth.command("show"), { effects: LOCAL_READ, examples: botExamples(bot, "auth show") })
72
75
  .description("where this profile's bot token comes from, and which bot it is")
73
76
  .action(async function () {
74
77
  const context = online(botContext(this, bot), this);
@@ -85,7 +88,7 @@ const authCommand = (bot, tokenVariable) => {
85
88
  });
86
89
  });
87
90
  });
88
- grantsAuthority(annotate(auth.command("remove"), { effects: LOCAL_WRITE }))
91
+ grantsAuthority(annotate(auth.command("remove"), { effects: LOCAL_WRITE, examples: botExamples(bot, "auth remove") }))
89
92
  .description("forget this profile's bot token")
90
93
  .action(async function () {
91
94
  const context = botContext(this, bot);
@@ -97,7 +100,7 @@ const authCommand = (bot, tokenVariable) => {
97
100
  });
98
101
  return auth;
99
102
  };
100
- const meCommand = (bot) => annotate(new Command("me"), { effects: REMOTE_READ })
103
+ const meCommand = (bot) => annotate(new Command("me"), { effects: REMOTE_READ, examples: botExamples(bot, "me") })
101
104
  .description("the bot this profile's token belongs to: id, name and username")
102
105
  .action(async function () {
103
106
  const context = online(botContext(this, bot), this);
@@ -111,8 +114,9 @@ const meCommand = (bot) => annotate(new Command("me"), { effects: REMOTE_READ })
111
114
  }
112
115
  });
113
116
  });
114
- const listCommand = (bot, tokenVariable) => annotate(new Command("list"), { effects: LOCAL_READ, mcp: false })
117
+ const listCommand = (bot, tokenVariable) => annotate(new Command("list"), { effects: LOCAL_READ, examples: botExamples(bot, "list", "list --check") })
115
118
  .description(`every name on this machine that has a bot token; --check asks ${bot.name ?? "the messenger"} which bot each is`)
119
+ .summary("every bot profile on this machine; --check asks which bot each is")
116
120
  .option("--check", "ask the messenger who each bot is, with its token")
117
121
  .action(async function () {
118
122
  const context = botContext(this, bot);
@@ -154,14 +158,20 @@ const listCommand = (bot, tokenVariable) => annotate(new Command("list"), { effe
154
158
  });
155
159
  });
156
160
  const recipientsCommand = (bot) => {
157
- const command = new Command("recipients").description("the chats this bot may write to; with no list, every chat — `clear` removes the list");
158
- annotate(command.command("list"), { effects: LOCAL_READ })
161
+ const command = new Command("recipients")
162
+ .description("the chats this bot may write to; with no list, every chat — `clear` removes the list")
163
+ .summary("the chats this bot may write to; with no list, every chat");
164
+ annotate(command.command("list"), { effects: LOCAL_READ, examples: botExamples(bot, "recipients list") })
159
165
  .description("the chats on the list, or nothing when there is no list")
160
166
  .action(async function () {
161
167
  const context = botContext(this, bot);
162
168
  await context.run(async () => renderAnswer(this, context, wholeList("bot recipients list", (context.recipients().read() ?? []).map(snakeKeys))));
163
169
  });
164
- grantsAuthority(annotate(command.command("add <chat>"), { effects: LOCAL_WRITE }))
170
+ grantsAuthority(annotate(command.command("add"), {
171
+ effects: LOCAL_WRITE,
172
+ examples: botExamples(bot, "recipients add Team"),
173
+ }))
174
+ .argument("<chat>", "a chat id, `user:<id>`, or the title of a chat this bot has seen")
165
175
  .description("allow a chat: its id, `user:<id>`, or the title of a chat this bot has seen")
166
176
  .action(async function (chat) {
167
177
  const context = botContext(this, bot);
@@ -172,13 +182,17 @@ const recipientsCommand = (bot) => {
172
182
  renderAnswer(this, context, wholeList("bot recipients list", (context.recipients().read() ?? []).map(snakeKeys)));
173
183
  });
174
184
  });
175
- grantsAuthority(annotate(command.command("remove <chat>"), { effects: LOCAL_WRITE }))
185
+ grantsAuthority(annotate(command.command("remove"), {
186
+ effects: LOCAL_WRITE,
187
+ examples: botExamples(bot, "recipients remove Team"),
188
+ }))
189
+ .argument("<chat>", "the chat id, or the title as the list shows it")
176
190
  .description("take a chat off the list")
177
191
  .action(async function (chat) {
178
192
  const context = botContext(this, bot);
179
193
  await context.run(async () => renderItem(this, context, { removed: context.recipients().remove(chat) ?? null }));
180
194
  });
181
- grantsAuthority(annotate(command.command("clear"), { effects: LOCAL_WRITE }))
195
+ grantsAuthority(annotate(command.command("clear"), { effects: LOCAL_WRITE, examples: botExamples(bot, "recipients clear") }))
182
196
  .description("remove the list: the bot may write to any chat again")
183
197
  .action(async function () {
184
198
  const context = botContext(this, bot);
@@ -188,21 +202,24 @@ const recipientsCommand = (bot) => {
188
202
  };
189
203
  const sendsCommand = (bot) => new Command("sends")
190
204
  .description("what this bot sent, edited and deleted from this machine — ids and outcomes, never text")
191
- .addCommand(annotate(new Command("list"), { effects: LOCAL_READ })
205
+ .summary("what this bot sent, edited and deleted from here — never the text")
206
+ .addCommand(annotate(new Command("list"), { effects: LOCAL_READ, examples: botExamples(bot, "sends list") })
192
207
  .description("every send, edit and delete this bot made from this machine, with its outcome")
193
208
  .action(async function () {
194
209
  const context = botContext(this, bot);
195
210
  await context.run(async () => renderAnswer(this, context, wholeList("bot sends list", context.journal().entries().map(snakeKeys))));
196
211
  }));
197
212
  const chatsCommand = (bot) => {
198
- const command = new Command("chats").description(`the chats this bot is in — ${bot.name ?? "the messenger"} gives a bot no list of them, so \`list\` shows the ones it has seen`);
199
- annotate(command.command("list"), { effects: REMOTE_READ })
213
+ const command = new Command("chats")
214
+ .description(`the chats this bot is in — ${bot.name ?? "the messenger"} gives a bot no list of them, so \`list\` shows the ones it has seen`)
215
+ .summary("the chats this bot is in, and what it does there");
216
+ annotate(command.command("list"), { effects: REMOTE_READ, examples: botExamples(bot, "chats list") })
200
217
  .description(`chats this bot has seen on this machine — not a complete list from ${bot.name ?? "the messenger"}`)
201
218
  .action(async function () {
202
219
  const context = botContext(this, bot);
203
220
  await context.run(async () => renderAnswer(this, context, wholeList("bot chats list", context.registry.list().map(snakeKeys))));
204
221
  });
205
- annotate(command.command("show"), { effects: REMOTE_READ })
222
+ annotate(command.command("show"), { effects: REMOTE_READ, examples: botExamples(bot, "chats show Team") })
206
223
  .description(`one chat from ${bot.name ?? "the messenger"}, and remember it`)
207
224
  .argument("<chat>", "a chat id, user:<id> for a person, or the title of a chat this bot has seen")
208
225
  .action(async function (chat) {
@@ -216,7 +233,10 @@ const chatsCommand = (bot) => {
216
233
  renderItem(this, context, found);
217
234
  });
218
235
  });
219
- annotate(command.command("leave"), { effects: REMOTE_WRITE, mcp: false })
236
+ annotate(command.command("leave"), {
237
+ effects: REMOTE_WRITE,
238
+ examples: botExamples(bot, "chats leave Team"),
239
+ })
220
240
  .description("take the bot out of a chat; only an admin of the chat can bring it back")
221
241
  .argument("<chat>", "a chat id, or the title of a chat this bot has seen")
222
242
  .action(async function (chat) {
@@ -230,8 +250,9 @@ const chatsCommand = (bot) => {
230
250
  renderAnswer(this, context, itemOf({ operation_id: operationId, chat_id: ref, left: true }));
231
251
  });
232
252
  });
233
- annotate(command.command("action"), { effects: REMOTE_WRITE })
253
+ annotate(command.command("action"), { effects: REMOTE_WRITE, examples: botExamples(bot, "chats action Team typing") })
234
254
  .description("show what the bot is doing in a chat — typing, sending a photo — for a few seconds")
255
+ .summary("show what the bot is doing in a chat, for a few seconds")
235
256
  .argument("<chat>", "a chat id, user:<id> for a person, or the title of a chat this bot has seen")
236
257
  .addArgument(new Argument("<action>", "what the chat sees").choices(BOT_ACTIONS))
237
258
  .action(async function (chat, action) {
@@ -263,6 +284,7 @@ export const botCommand = (bot) => {
263
284
  const tokenVariable = envName(bot.app, "BOT_TOKEN");
264
285
  const command = new Command("bot")
265
286
  .description(`a ${bot.name ?? "messenger"} bot, through the official Bot API and a bot token — not your personal account`)
287
+ .summary("a bot account, through the official Bot API and a bot token")
266
288
  .addCommand(authCommand(bot, tokenVariable))
267
289
  .addCommand(listCommand(bot, tokenVariable))
268
290
  .addCommand(chatsCommand(bot))
@@ -0,0 +1,3 @@
1
+ import type { BotMessenger } from "./port.js";
2
+ /** Help examples for a shared bot command, in the calling CLI's own words, for a bot profile named `sales`. */
3
+ export declare const botExamples: (bot: BotMessenger, ...lines: string[]) => string[];
@@ -0,0 +1,2 @@
1
+ /** Help examples for a shared bot command, in the calling CLI's own words, for a bot profile named `sales`. */
2
+ export const botExamples = (bot, ...lines) => lines.map((line) => `${bot.app.command} sales bot ${line}`);
@@ -8,11 +8,13 @@ import { itemOf, renderAnswer } from "../envelope.js";
8
8
  import { serverEntry } from "../messenger/mcp-command.js";
9
9
  import { nextStep, rootOf } from "../steps.js";
10
10
  import { botContext } from "./context.js";
11
+ import { botExamples } from "./examples.js";
11
12
  /** `bot mcp`, mounted only for a messenger that hands in its `run` on `BotMessenger.mcp`. */
12
13
  export const botMcpCommand = (bot) => {
13
14
  const { app } = bot;
14
15
  const command = annotate(new Command("mcp"), { effects: REMOTE_WRITE, lifetime: "persistent", mcp: false })
15
16
  .description(`serve this bot to an agent over MCP, on stdin and stdout — \`claude mcp add sales-bot -- ${app.command} sales bot mcp\``)
17
+ .summary("serve this bot to an agent over MCP, on stdin and stdout")
16
18
  .action(async function () {
17
19
  const context = botContext(this, bot);
18
20
  if (!context.tokens.read()) {
@@ -33,8 +35,9 @@ export const botMcpCommand = (bot) => {
33
35
  ...(mcp.skill ? { skill: mcp.skill } : {}),
34
36
  }, context.streams.diagnostic);
35
37
  });
36
- command.addCommand(annotate(new Command("config"), { effects: LOCAL_READ })
38
+ command.addCommand(annotate(new Command("config"), { effects: LOCAL_READ, examples: botExamples(bot, "mcp config") })
37
39
  .description("print the mcpServers entry for Claude Desktop, Cursor and others, with full paths; writes nothing")
40
+ .summary("print the mcpServers entry for an MCP client; writes nothing")
38
41
  .action(function () {
39
42
  const context = botContext(this, bot);
40
43
  const { settings, env } = context;
@@ -14,6 +14,7 @@ import { messageItem } from "../messenger/message-items.js";
14
14
  import { readAll } from "../messenger/stdin.js";
15
15
  import { positiveCount } from "../paging.js";
16
16
  import { botContext } from "./context.js";
17
+ import { botExamples } from "./examples.js";
17
18
  /** The adapter's `method`, or a refusal naming what this messenger's bot cannot do. */
18
19
  export const botCan = (adapter, method, bot, what) => {
19
20
  const found = adapter[method];
@@ -59,7 +60,10 @@ const seen = (context, message) => {
59
60
  if (/^-?\d+$/.test(message.chatId))
60
61
  context.registry.observe([{ id: message.chatId }]);
61
62
  };
62
- const sendCommand = (bot) => annotate(new Command("send"), { effects: REMOTE_WRITE })
63
+ const sendCommand = (bot) => annotate(new Command("send"), {
64
+ effects: REMOTE_WRITE,
65
+ examples: botExamples(bot, 'messages send Team "Doors open at 9"', 'messages send Team "Here it is" --file report.pdf'),
66
+ })
63
67
  .description("send a message as the bot; without [text], the text is read from stdin")
64
68
  .argument("<chat>", "a chat id, user:<id> for a person, or the title of a chat this bot has seen")
65
69
  .argument("[text]", "the message")
@@ -120,9 +124,10 @@ const sendCommand = (bot) => annotate(new Command("send"), { effects: REMOTE_WRI
120
124
  renderAnswer(this, context, written(context, botId, { operationId }, message));
121
125
  });
122
126
  });
123
- const listCommand = (bot) => annotate(new Command("list"), { effects: REMOTE_READ })
127
+ const listCommand = (bot) => annotate(new Command("list"), { effects: REMOTE_READ, examples: botExamples(bot, "messages list Team --limit 20") })
124
128
  .description(`the latest messages in a chat; where ${bot.name ?? "the messenger"} gives a bot no history, and with --offline, ` +
125
129
  "the ones this bot has seen on this machine")
130
+ .summary("the latest messages in a chat")
126
131
  .argument("<chat>", "a chat id, user:<id> for a person, or the title of a chat this bot has seen")
127
132
  .option("--limit <n>", "how many, the newest", positiveCount("--limit"))
128
133
  .action(async function (chat) {
@@ -155,7 +160,7 @@ const listCommand = (bot) => annotate(new Command("list"), { effects: REMOTE_REA
155
160
  show(this, context, bot, botId, messages);
156
161
  });
157
162
  });
158
- const showCommand = (bot) => annotate(new Command("show"), { effects: REMOTE_READ })
163
+ const showCommand = (bot) => annotate(new Command("show"), { effects: REMOTE_READ, examples: botExamples(bot, "messages show Team 101") })
159
164
  .description("one message by its id in a chat")
160
165
  .argument("<chat>", "a chat id, user:<id> for a person, or the title of a chat this bot has seen")
161
166
  .argument("<message>", "message id")
@@ -187,7 +192,10 @@ const showCommand = (bot) => annotate(new Command("show"), { effects: REMOTE_REA
187
192
  show(this, context, bot, botId, [found], true);
188
193
  });
189
194
  });
190
- const editCommand = (bot) => annotate(new Command("edit"), { effects: REMOTE_WRITE })
195
+ const editCommand = (bot) => annotate(new Command("edit"), {
196
+ effects: REMOTE_WRITE,
197
+ examples: botExamples(bot, 'messages edit Team 101 "Doors open at 10"'),
198
+ })
191
199
  .description("replace the text of a message the bot sent")
192
200
  .argument("<chat>", "a chat id, user:<id> for a person, or the title of a chat this bot has seen")
193
201
  .argument("<message>", "message id")
@@ -217,7 +225,7 @@ const editCommand = (bot) => annotate(new Command("edit"), { effects: REMOTE_WRI
217
225
  renderAnswer(this, context, written(context, botId, { operationId }, message));
218
226
  });
219
227
  });
220
- const deleteCommand = (bot) => annotate(new Command("delete"), { effects: REMOTE_WRITE })
228
+ const deleteCommand = (bot) => annotate(new Command("delete"), { effects: REMOTE_WRITE, examples: botExamples(bot, "messages delete Team 101") })
221
229
  .description("delete messages in a chat the bot can delete in; it cannot be undone")
222
230
  .argument("<chat>", "a chat id, user:<id> for a person, or the title of a chat this bot has seen")
223
231
  .argument("<messages...>", "message ids")
@@ -235,7 +243,10 @@ const deleteCommand = (bot) => annotate(new Command("delete"), { effects: REMOTE
235
243
  });
236
244
  });
237
245
  const pinCommand = (bot, pin) => {
238
- const command = annotate(new Command(pin ? "pin" : "unpin"), { effects: REMOTE_WRITE })
246
+ const command = annotate(new Command(pin ? "pin" : "unpin"), {
247
+ effects: REMOTE_WRITE,
248
+ examples: botExamples(bot, `messages ${pin ? "pin" : "unpin"} Team 101`),
249
+ })
239
250
  .description(pin ? "pin a message in a chat; quietly unless --notify" : "unpin a message in a chat")
240
251
  .argument("<chat>", "a chat id, user:<id> for a person, or the title of a chat this bot has seen")
241
252
  .argument("<message>", "message id");
@@ -13,6 +13,7 @@ import { grantsAuthority, LOCAL_READ, LOCAL_WRITE, ownerOnlyOption, REMOTE_WRITE
13
13
  import { itemOf, renderAnswer, snakeKeys } from "../envelope.js";
14
14
  import { answerOf } from "../messenger/ask.js";
15
15
  import { botContext, online } from "./context.js";
16
+ import { botExamples } from "./examples.js";
16
17
  import { botCan, botIdOf } from "./messages.js";
17
18
  import { botFiles } from "./registry.js";
18
19
  const CHAT = "a group's id, or the title of a group this bot has seen";
@@ -34,15 +35,19 @@ const shown = (context, chatId, rules) => {
34
35
  /** `bot chats rules show|set|unset`: kept on this machine, the chat named the bot's way. */
35
36
  export const botRulesCommand = (bot) => {
36
37
  const command = new Command("rules").description("a chat's moderation rules for this bot, kept on this machine");
37
- annotate(command.command("show"), { effects: LOCAL_READ })
38
+ annotate(command.command("show"), { effects: LOCAL_READ, examples: botExamples(bot, "chats rules show Team") })
38
39
  .description("the chat's rules; the defaults, marked not saved, if it has none yet")
39
40
  .argument("<chat>", CHAT)
40
41
  .action(function (chat) {
41
42
  const context = botContext(this, bot);
42
43
  renderAnswer(this, context, shown(context, context.chatRef(chat), rulesOf(context, bot)));
43
44
  });
44
- grantsAuthority(annotate(command.command("set"), { effects: LOCAL_WRITE }))
45
+ grantsAuthority(annotate(command.command("set"), {
46
+ effects: LOCAL_WRITE,
47
+ examples: botExamples(bot, "chats rules set Team links delete"),
48
+ }))
45
49
  .description(`change one rule — ${RULE_KEYS.join(", ")}`)
50
+ .summary("change one moderation rule of a chat")
46
51
  .argument("<chat>", CHAT)
47
52
  .argument("<key>", "the rule")
48
53
  .argument("<value>", "its new value")
@@ -56,7 +61,10 @@ export const botRulesCommand = (bot) => {
56
61
  rules.set(chatId, titleOf(context, chatId), key, value);
57
62
  renderAnswer(this, context, shown(context, chatId, rules));
58
63
  });
59
- grantsAuthority(annotate(command.command("unset"), { effects: LOCAL_WRITE }))
64
+ grantsAuthority(annotate(command.command("unset"), {
65
+ effects: LOCAL_WRITE,
66
+ examples: botExamples(bot, "chats rules unset Team links"),
67
+ }))
60
68
  .description("put one rule back to its default")
61
69
  .argument("<chat>", CHAT)
62
70
  .argument("<key>", "the rule")
@@ -136,8 +144,12 @@ const yes = (answer) => /^\s*y(es)?\s*$/i.test(answer);
136
144
  * `bot chats moderate`: the personal `chats moderate`, done by the bot. Typing it is the consent for
137
145
  * what the rules name. A removal bans unless `--no-block`.
138
146
  */
139
- export const botModerateCommand = (bot) => annotate(new Command("moderate"), { effects: REMOTE_WRITE })
147
+ export const botModerateCommand = (bot) => annotate(new Command("moderate"), {
148
+ effects: REMOTE_WRITE,
149
+ examples: botExamples(bot, "chats moderate Team --dry-run"),
150
+ })
140
151
  .description("judge a group's new messages and joins by its rules, and act as they allow — as the bot")
152
+ .summary("act on a group's new messages and joins by its rules, as the bot")
141
153
  .argument("<chat>", CHAT)
142
154
  .option("--since-time <time>", "judge what came after this ISO 8601 time, or 2h / 1d ago; the saved point stays")
143
155
  .option("--dry-run", "judge and plan; do nothing")
@@ -11,6 +11,7 @@ import { messageItem } from "../messenger/message-items.js";
11
11
  import { positiveCount } from "../paging.js";
12
12
  import { asFirstWord } from "../profile.js";
13
13
  import { botContext } from "./context.js";
14
+ import { botExamples } from "./examples.js";
14
15
  import { botCan } from "./messages.js";
15
16
  import { ChatRegistry, registryProfiles } from "./registry.js";
16
17
  /** What "common chats" rests on: who has written where in this copy, not who is a member. */
@@ -122,13 +123,16 @@ const cardOf = async (store, context, bot, who, across, limit) => {
122
123
  };
123
124
  /** `bot contacts show`: one person this bot has seen write, from the local copy. */
124
125
  export const botContactsCommand = (bot) => {
125
- const command = new Command("contacts").description(`people this bot has seen write — from the local copy on this machine, never asking ${bot.name ?? "the messenger"} unless told to`);
126
- acrossOptions(annotate(command.command("show"), { effects: REMOTE_READ }))
126
+ const command = new Command("contacts")
127
+ .description(`people this bot has seen write — from the local copy on this machine, never asking ${bot.name ?? "the messenger"} unless told to`)
128
+ .summary("the people this bot has seen write, from the local copy");
129
+ acrossOptions(annotate(command.command("show"), { effects: REMOTE_READ, examples: botExamples(bot, "contacts show @ann") }))
127
130
  .argument("<who>", "an id, @username or part of a name")
128
131
  .option("--limit <n>", "how many messages from the private chat", positiveCount("--limit"))
129
132
  .option("--sync-first", "read the private chat with them again from the messenger first — one request")
130
133
  .description("one person: the chats they wrote in (with their last message there) and the latest messages of their " +
131
134
  "private chat with the bot")
135
+ .summary("one person: the chats they wrote in and their chat with the bot")
132
136
  .action(async function (who, options) {
133
137
  const context = botContext(this, bot);
134
138
  const across = { allBots: options.allBots === true, bots: options.bots ?? [] };
@@ -186,7 +190,10 @@ const search = (context, bot, text, from, options) => {
186
190
  /** `bot search messages`: the messages this bot's local copy holds. */
187
191
  export const botSearchCommand = (bot) => {
188
192
  const group = new Command("search").description("find what this bot's local copy holds, by text");
189
- acrossOptions(annotate(group.command("messages"), { effects: REMOTE_READ }))
193
+ acrossOptions(annotate(group.command("messages"), {
194
+ effects: REMOTE_READ,
195
+ examples: botExamples(bot, "search messages refund", "search messages invoice --from @ann"),
196
+ }))
190
197
  .argument("[query...]", "the words to find")
191
198
  .option("--limit <n>", "how many", positiveCount("--limit"))
192
199
  .option("--page <n>", "which page, starting at 1", positiveCount("--page"))
@@ -195,6 +202,7 @@ export const botSearchCommand = (bot) => {
195
202
  .description("search the messages this bot has read, sent or received on this machine — the local copy only, best " +
196
203
  'match first; a Lucene query: every word must appear; "a phrase", -word, a OR b, from: chat: date: has:; ' +
197
204
  "by text, by --from, or both")
205
+ .summary("search the messages this bot has seen, in the local copy, by text")
198
206
  .action(async function (query, options) {
199
207
  const context = botContext(this, bot);
200
208
  const limit = options.limit ?? context.settings.limit;
@@ -222,12 +230,16 @@ export const botSearchCommand = (bot) => {
222
230
  };
223
231
  /** `bot messages between`, over the local copy, added to the shared `messages` group. */
224
232
  export const addBotCopyReads = (messages, bot) => {
225
- acrossOptions(annotate(messages.command("between"), { effects: REMOTE_READ }))
233
+ acrossOptions(annotate(messages.command("between"), {
234
+ effects: REMOTE_READ,
235
+ examples: botExamples(bot, "messages between @ann @bob"),
236
+ }))
226
237
  .argument("<people...>", "two or more people — an id, @username or part of a name each")
227
238
  .option("--limit <n>", "how many of the latest messages from each chat", positiveCount("--limit"))
228
239
  .description("what two or more people wrote in the chats they have all written in — from the local copy, grouped by " +
229
240
  "chat, oldest first; --limit counts per chat. Common chats are the ones this copy saw each of them " +
230
241
  `write in, not a member list from ${bot.name ?? "the messenger"}`)
242
+ .summary("what two or more people wrote in the chats they share, from the local copy")
231
243
  .action(async function (references, options) {
232
244
  const context = botContext(this, bot);
233
245
  if (references.length < 2)