@wirecat/cli-core 0.396.0 → 0.398.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 (102) hide show
  1. package/CHANGELOG.md +74 -1
  2. package/README.md +13 -9
  3. package/dist/background/daily-archive-timer.d.ts +40 -0
  4. package/dist/background/daily-archive-timer.js +181 -0
  5. package/dist/background/index.d.ts +2 -0
  6. package/dist/background/index.js +1 -0
  7. package/dist/cli/bot/admins.js +2 -2
  8. package/dist/cli/bot/command.js +2 -2
  9. package/dist/cli/bot/setup.js +3 -3
  10. package/dist/cli/commands-command.js +1 -1
  11. package/dist/cli/config-command.js +1 -1
  12. package/dist/cli/guide.js +0 -1
  13. package/dist/cli/help/command-help.d.ts +9 -2
  14. package/dist/cli/help/command-help.js +22 -4
  15. package/dist/cli/help/first-screen.js +3 -3
  16. package/dist/cli/messenger/account-command.js +3 -1
  17. package/dist/cli/messenger/account-sessions-command.js +3 -1
  18. package/dist/cli/messenger/admin-commands.js +1 -0
  19. package/dist/cli/messenger/admin-contacts-command.js +9 -3
  20. package/dist/cli/messenger/archive-commands.js +5 -3
  21. package/dist/cli/messenger/attachments-command.js +17 -7
  22. package/dist/cli/messenger/backfill-command.js +7 -3
  23. package/dist/cli/messenger/catch-up-answers.d.ts +2 -0
  24. package/dist/cli/messenger/catch-up-answers.js +5 -1
  25. package/dist/cli/messenger/chats-command.js +3 -1
  26. package/dist/cli/messenger/chats-members-command.js +4 -0
  27. package/dist/cli/messenger/chats-stats-command.js +1 -0
  28. package/dist/cli/messenger/chats-tracking-command.js +4 -1
  29. package/dist/cli/messenger/complete-command.js +6 -2
  30. package/dist/cli/messenger/contacts-command.js +11 -4
  31. package/dist/cli/messenger/context.d.ts +5 -0
  32. package/dist/cli/messenger/conversations-command.js +18 -1
  33. package/dist/cli/messenger/counters-command.js +1 -0
  34. package/dist/cli/messenger/doctor-command.js +2 -2
  35. package/dist/cli/messenger/download-command.js +1 -0
  36. package/dist/cli/messenger/flood-command.js +2 -1
  37. package/dist/cli/messenger/guard-commands.js +5 -4
  38. package/dist/cli/messenger/inbox.js +2 -0
  39. package/dist/cli/messenger/lacks.d.ts +7 -0
  40. package/dist/cli/messenger/lacks.js +18 -1
  41. package/dist/cli/messenger/mcp-command.js +6 -2
  42. package/dist/cli/messenger/media-archive-command.d.ts +3 -0
  43. package/dist/cli/messenger/media-archive-command.js +72 -0
  44. package/dist/cli/messenger/media-archive-move-command.d.ts +3 -0
  45. package/dist/cli/messenger/media-archive-move-command.js +39 -0
  46. package/dist/cli/messenger/media-archive-timer-command.d.ts +3 -0
  47. package/dist/cli/messenger/media-archive-timer-command.js +70 -0
  48. package/dist/cli/messenger/messages-command.js +2 -1
  49. package/dist/cli/messenger/messages-comments-command.js +1 -0
  50. package/dist/cli/messenger/messages-delete-command.js +1 -1
  51. package/dist/cli/messenger/messages-search-command.js +1 -0
  52. package/dist/cli/messenger/messages-stats-command.js +1 -0
  53. package/dist/cli/messenger/models-command.js +8 -2
  54. package/dist/cli/messenger/private-people-command.js +12 -10
  55. package/dist/cli/messenger/rankings-command.js +2 -0
  56. package/dist/cli/messenger/reactions-command.js +8 -2
  57. package/dist/cli/messenger/replies-command.js +1 -1
  58. package/dist/cli/messenger/retention-command.js +1 -0
  59. package/dist/cli/messenger/review.js +8 -2
  60. package/dist/cli/messenger/schedule-command.js +3 -3
  61. package/dist/cli/messenger/search-command.js +7 -3
  62. package/dist/cli/messenger/searches-command.js +24 -7
  63. package/dist/cli/messenger/serve-command.js +5 -1
  64. package/dist/cli/messenger/serve-relay.d.ts +1 -0
  65. package/dist/cli/messenger/server-command.js +17 -3
  66. package/dist/cli/messenger/store-maintenance-command.d.ts +1 -1
  67. package/dist/cli/messenger/store-maintenance-command.js +10 -3
  68. package/dist/cli/messenger/tags-command.js +6 -2
  69. package/dist/cli/messenger/tasks-command.js +10 -7
  70. package/dist/cli/messenger/transcribe-command.js +1 -0
  71. package/dist/cli/messenger/watch-command.js +6 -1
  72. package/dist/cli/runs/command.js +2 -3
  73. package/dist/cli/skill-command.js +1 -1
  74. package/dist/domain/models.d.ts +2 -0
  75. package/dist/entities/contact-read.d.ts +119 -0
  76. package/dist/entities/contact-read.js +50 -0
  77. package/dist/entities/index.d.ts +120 -1
  78. package/dist/entities/index.js +5 -1
  79. package/dist/meetings/mcp/index.d.ts +15 -15
  80. package/dist/parity/catalogue.js +1 -1
  81. package/dist/parity/manifest.d.ts +3 -1
  82. package/dist/parity/manifest.js +2 -0
  83. package/dist/parity/wording.js +2 -0
  84. package/dist/sends/permissions.js +7 -0
  85. package/dist/services/deps.d.ts +1 -1
  86. package/dist/services/deps.js +1 -1
  87. package/dist/services/inbox.js +5 -5
  88. package/dist/services/index.d.ts +5 -0
  89. package/dist/services/index.js +2 -0
  90. package/dist/services/media-archive-files.d.ts +10 -0
  91. package/dist/services/media-archive-files.js +110 -0
  92. package/dist/services/media-archive-locks.d.ts +6 -0
  93. package/dist/services/media-archive-locks.js +71 -0
  94. package/dist/services/media-archive-manifest.d.ts +29 -0
  95. package/dist/services/media-archive-manifest.js +116 -0
  96. package/dist/services/media-archive-move.d.ts +21 -0
  97. package/dist/services/media-archive-move.js +163 -0
  98. package/dist/services/media-archive.d.ts +34 -0
  99. package/dist/services/media-archive.js +165 -0
  100. package/dist/services/search-kind.js +1 -1
  101. package/package.json +2 -2
  102. package/parity.json +116 -193
@@ -58,6 +58,7 @@ export const readProfilePhoto = (messenger, path, env) => readUpload("photo", pa
58
58
  const updateCommand = (messenger) => {
59
59
  const keys = messenger.groupSettings ?? GROUP_SETTINGS;
60
60
  const update = annotate(new Command("update"), { effects: REMOTE_WRITE })
61
+ .summary("change a group or channel's profile and settings")
61
62
  .description("rename a group or channel, change its description, or turn one of its settings on or off")
62
63
  .argument("<chat>", messenger.chatArgument)
63
64
  .option("--title <title>", "the new name")
@@ -10,11 +10,16 @@ const PERSON = "person id — `contacts lookup` finds one — or part of a known
10
10
  /** The address book's writes, in max-cli's words. */
11
11
  export const contactWriteCommands = (messenger) => {
12
12
  const simple = [
13
- ["add", "add a person to your contacts — `contacts list` still shows only people you have a dialog with"],
13
+ [
14
+ "add",
15
+ "add a person to your contacts — `contacts list` still shows only people you have a dialog with",
16
+ "add a person to your contacts",
17
+ ],
14
18
  ["remove", "remove a person from your contacts; the chat stays, a name you gave them may not"],
15
19
  ["block", "stop a person from writing to you — they need not be a contact"],
16
20
  ["unblock", "let a blocked person write to you again"],
17
- ].map(([name, description]) => annotate(new Command(name), { effects: REMOTE_WRITE })
21
+ ].map(([name, description, summary]) => annotate(new Command(name), { effects: REMOTE_WRITE })
22
+ .summary(summary ?? description)
18
23
  .description(description)
19
24
  .argument("<person>", PERSON)
20
25
  .action(async function (person) {
@@ -23,10 +28,11 @@ export const contactWriteCommands = (messenger) => {
23
28
  renderAnswer(this, context, itemOf(snakeKeys(done)));
24
29
  }));
25
30
  const rename = annotate(new Command("rename"), { effects: REMOTE_WRITE })
31
+ .summary("change a contact's name in the messenger address book")
26
32
  .description("rename the contact in the messenger address book; use contacts alias for a private local name")
27
33
  .argument("<person>", PERSON)
28
34
  .argument("<first-name>", "the name you want to see for them")
29
- .argument("[last-name]")
35
+ .argument("[last-name]", "the last name you want to see for them")
30
36
  .action(async function (person, firstName, lastName) {
31
37
  const context = messengerContext(this, messenger);
32
38
  const renamed = await context.withServices((services) => services.people.rename(person, firstName, lastName));
@@ -64,7 +64,8 @@ export const storeCommand = (messenger) => {
64
64
  * What the local store holds, per chat — read from the store alone. Whether a fetch reached a
65
65
  * chat's very first message is not recorded, so it is not claimed: the stretches held are shown.
66
66
  */
67
- const statusCommand = (messenger) => annotate(new Command("status"), { effects: LOCAL_READ, mcp: false })
67
+ const statusCommand = (messenger) => annotate(new Command("status"), { effects: LOCAL_READ })
68
+ .summary("what the local archive holds and what is missing")
68
69
  .description("per chat: messages stored, the oldest and newest, and the stretches held completely")
69
70
  .argument("[chat]", messenger.chatArgument)
70
71
  .action(async function (chat) {
@@ -131,9 +132,10 @@ const clearCommand = (messenger) => annotate(new Command("clear"), { effects: LO
131
132
  * `--format markdown` for a transcript a person reads. With `--output-dir`, any number of chats into a
132
133
  * folder, and each later run into the same folder adds only what changed.
133
134
  */
134
- const exportCommand = (messenger) => destroysNothing(annotate(new Command("export"), { effects: LOCAL_WRITE, mcp: false }))
135
+ const exportCommand = (messenger) => destroysNothing(annotate(new Command("export"), { effects: LOCAL_WRITE }))
136
+ .summary("export stored messages to files")
135
137
  .description("a chat's stored messages, oldest first: --jsonl one message per line, --json the list envelope of messages; never asks the messenger")
136
- .argument("[chats...]", `${messenger.chatArgument}; several with --output-dir`)
138
+ .argument("[chat...]", `${messenger.chatArgument}; several with --output-dir`)
137
139
  .option("--format <format>", "jsonl: one message per line, the default with --output; markdown: a transcript with a heading per day, replies and forwards quoted")
138
140
  .option("--since-time <time>", "only from this ISO 8601 time, or 30m / 2h / 1d ago, on")
139
141
  .option("--output <file>", "write JSON lines, or the transcript, to this new file, readable only by you")
@@ -15,6 +15,7 @@ import { withPaging } from "../paging.js";
15
15
  import { readsStdin } from "../stdin-field.js";
16
16
  import { nextStep } from "../steps.js";
17
17
  import { messengerContext, refuseLocalWrite } from "./context.js";
18
+ import { addMediaArchiveCommands } from "./media-archive-command.js";
18
19
  import { stopOnSignal } from "./patience.js";
19
20
  import { readAll } from "./stdin.js";
20
21
  const count = (value) => {
@@ -38,6 +39,7 @@ const extractCommand = (messenger) => annotate(new Command("extract"), {
38
39
  effects: FETCH,
39
40
  examples: [`${messenger.app.command} personal attachments extract --chat "Book club" --limit 10`],
40
41
  })
42
+ .summary("extract text from retained message files")
41
43
  .description("read the text of downloaded files — text, PDF/DOCX text layers, ODT/ODS/XLSX/PPTX/EPUB — into the local store, where search finds them")
42
44
  .option("--chat <chat>", `only this chat's files; ${messenger.chatArgument}`)
43
45
  .option("--from-dir <dir>", "match files in this nonrecursive directory; needs --chat")
@@ -127,7 +129,9 @@ const row = ({ message, position, name, local_path, extraction }) => `${message}
127
129
  const listCommand = (messenger) => withQuery(withPaging(annotate(new Command("list"), {
128
130
  effects: REMOTE_READ,
129
131
  examples: [`${messenger.app.command} personal attachments list --limit 10`],
130
- }).description("files of stored messages, newest first: where each was saved and whether its text is held — never the text")), ATTACHMENT_QUERY).action(async function (words) {
132
+ })
133
+ .summary("stored message files, saved paths and text status")
134
+ .description("files of stored messages, newest first: where each was saved and whether its text is held — never the text")), ATTACHMENT_QUERY).action(async function (words) {
131
135
  const context = messengerContext(this, messenger);
132
136
  const query = listQuery(typedQuery(words), ATTACHMENT_QUERY, "attachments list");
133
137
  const found = await context.withServices((services) => services.attachments.list({ ...pageOf(context.settings), ...(query ? { query } : {}) }));
@@ -175,6 +179,7 @@ const textCommand = (messenger) => {
175
179
  name: "text",
176
180
  description: "the text of the file",
177
181
  })
182
+ .summary("save the text read from one message file")
178
183
  .description("keep the text an agent read from a file — a scan, a photo — so search finds it; nothing is sent")
179
184
  .argument("<chat>", `${messenger.chatArgument}; or a msg: locator, with no message id after it`)
180
185
  .argument("[message]", "the message id")
@@ -196,9 +201,14 @@ const textCommand = (messenger) => {
196
201
  return text;
197
202
  };
198
203
  /** Files of stored messages: what is in them, for `content:` in a search. Nothing here is sent. */
199
- export const attachmentsCommand = (messenger) => new Command("attachments")
200
- .description("the files of stored messages: their text in the local store, where search finds it")
201
- .addCommand(extractCommand(messenger))
202
- .addCommand(listCommand(messenger))
203
- .addCommand(showCommand(messenger))
204
- .addCommand(textCommand(messenger));
204
+ export const attachmentsCommand = (messenger) => {
205
+ const attachments = new Command("attachments")
206
+ .summary("stored message files and their retained text")
207
+ .description("the files of stored messages: their text in the local store, where search finds it")
208
+ .addCommand(extractCommand(messenger))
209
+ .addCommand(listCommand(messenger))
210
+ .addCommand(showCommand(messenger))
211
+ .addCommand(textCommand(messenger));
212
+ addMediaArchiveCommands(attachments, messenger);
213
+ return attachments;
214
+ };
@@ -39,6 +39,7 @@ export const syncCommand = (messenger) => {
39
39
  effects: FETCH,
40
40
  cooperative: true,
41
41
  })
42
+ .summary("fetch and save a chat's message history")
42
43
  .description("bring a chat's history in the local store up to date, newest first; run it again to continue; --all syncs every chat")
43
44
  .argument("[chat]", messenger.chatArgument)
44
45
  .option("--all", `every chat, most recently active first — what search needs; the last ${ALL_SINCE} unless --since-time or --last`)
@@ -224,6 +225,7 @@ export const jobsCommand = (messenger) => {
224
225
  context.renderer.note(`no ${query ? "matching " : ""}background sync jobs for this profile`);
225
226
  });
226
227
  annotate(command.command("show"), { effects: LOCAL_READ })
228
+ .summary("one archive job's status and log")
227
229
  .description("one background job — the newest when none is named — and what the store now holds of its chat")
228
230
  .argument("[job]", "the job id `messages sync --background` printed")
229
231
  .action(async function (id) {
@@ -235,7 +237,8 @@ export const jobsCommand = (messenger) => {
235
237
  : await context.withServices((services) => services.archive.held(chatId)).catch(() => undefined);
236
238
  renderAnswer(this, context, itemOf(snakeKeys({ ...jobDetails(job), ...(held ? { held } : {}), log: job.log })));
237
239
  });
238
- destroysNothing(annotate(command.command("cancel"), { effects: LOCAL_WRITE, mcp: false }))
240
+ destroysNothing(annotate(command.command("cancel"), { effects: LOCAL_WRITE }))
241
+ .summary("cancel a queued or running archive job")
239
242
  .description("stop a running background job after its current page; a later sync resumes where it stopped")
240
243
  .argument("<job>", "the job id")
241
244
  .action(function (id) {
@@ -248,7 +251,8 @@ export const jobsCommand = (messenger) => {
248
251
  process.kill(job.pid, "SIGTERM");
249
252
  renderAnswer(this, context, itemOf({ job: job.id, cancelled: true }));
250
253
  });
251
- destroysNothing(annotate(command.command("retry"), { effects: LOCAL_WRITE, mcp: false }))
254
+ destroysNothing(annotate(command.command("retry"), { effects: LOCAL_WRITE }))
255
+ .summary("retry an archive job")
252
256
  .description("start a partial, failed or died job again, as a new job; the sync resumes where the store stopped")
253
257
  .argument("[job]", "the job id")
254
258
  .option("--failed", "every chat whose newest job partial, failed or died")
@@ -288,7 +292,7 @@ export const jobsCommand = (messenger) => {
288
292
  if (items.length === 0 && context.format === "pretty")
289
293
  context.renderer.note("no partial, failed or died jobs to retry");
290
294
  });
291
- destroysNothing(annotate(command.command("clear"), { effects: LOCAL_WRITE, mcp: false }))
295
+ destroysNothing(annotate(command.command("clear"), { effects: LOCAL_WRITE }))
292
296
  .description("forget finished jobs and remove their logs; a running job is kept")
293
297
  .action(function () {
294
298
  const context = messengerContext(this, messenger);
@@ -49,6 +49,8 @@ export declare const inboxAnswer: (inbox: Inbox, hearing: Hearing | undefined, r
49
49
  }[][] | undefined;
50
50
  }[];
51
51
  }>;
52
+ /** What `review --offline` cannot see. */
53
+ export declare const OFFLINE_REVIEW = "offline: only what was already synced to the local store; run review without --offline for the rest";
52
54
  /**
53
55
  * A review the next one goes on from: from `until` when nothing was left out, else from the same start again.
54
56
  * Saved points (`--new`) carry this themselves, and an `--unanswered` look is not a review to continue.
@@ -48,7 +48,7 @@ const nameOf = (chat) => chat.title ?? chat.id;
48
48
  * What every catch-up answer shares: the messages, oldest first, each naming its chat; what was left out as
49
49
  * `missing`; and a chat cut short as a `messages.list` step, after the steps the command puts first.
50
50
  */
51
- const catchUpList = (command, found, hearing, reader, { transcribe, first = [], meta = {} }) => {
51
+ const catchUpList = (command, found, hearing, reader, { transcribe, first = [], meta = {}, left = [], }) => {
52
52
  const read = found.chats
53
53
  .flatMap((chat) => heard(chat.messages, hearing).map((message) => ({ ...message, chatTitle: chat.title })))
54
54
  .sort((a, b) => Date.parse(a.timestamp) - Date.parse(b.timestamp));
@@ -70,6 +70,7 @@ const catchUpList = (command, found, hearing, reader, { transcribe, first = [],
70
70
  ? [{ field: "items", why: `${found.quiet} muted or archived chats left out`, step: `${command}.all` }]
71
71
  : []),
72
72
  ...hearingMissing(hearing, transcribe),
73
+ ...left,
73
74
  ];
74
75
  return listOf(command, {
75
76
  items: read.map((message) => summaryOf(message, reader)),
@@ -81,6 +82,8 @@ const catchUpList = (command, found, hearing, reader, { transcribe, first = [],
81
82
  });
82
83
  };
83
84
  export const inboxAnswer = (inbox, hearing, reader, transcribe, meta = {}) => catchUpList("inbox", inbox, hearing, reader, { transcribe, meta });
85
+ /** What `review --offline` cannot see. */
86
+ export const OFFLINE_REVIEW = "offline: only what was already synced to the local store; run review without --offline for the rest";
84
87
  const quoted = (value) => `"${value.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`;
85
88
  /**
86
89
  * A review the next one goes on from: from `until` when nothing was left out, else from the same start again.
@@ -95,6 +98,7 @@ export const reviewAnswer = (found, hearing, reader, { transcribe, fresh, chat,
95
98
  return catchUpList("review", found, hearing, reader, {
96
99
  transcribe,
97
100
  first: continues ? [nextStep("review.next", { args: { query: query.join(" ") } })] : [],
101
+ ...(found.offline ? { left: [{ field: "items", why: OFFLINE_REVIEW }] } : {}),
98
102
  ...(meta === undefined ? {} : { meta }),
99
103
  });
100
104
  };
@@ -19,6 +19,7 @@ import { membersCommand } from "./chats-members-command.js";
19
19
  import { markReadCommand } from "./chats-read-command.js";
20
20
  import { trackingCommand } from "./chats-tracking-command.js";
21
21
  import { messengerContext } from "./context.js";
22
+ import { hideNever } from "./lacks.js";
22
23
  import { archiveCommands, chatDeletionCommands, chatPinCommands, markUnreadCommand, mediaCommand, muteCommands, } from "./records-command.js";
23
24
  export const chatsCommand = (messenger) => {
24
25
  const chats = new Command("chats").description("the account's chats");
@@ -37,6 +38,7 @@ export const chatsCommand = (messenger) => {
37
38
  // Where the history is pushed, the store answers, --offline too.
38
39
  ...(messenger.history === "store" ? {} : { offline: false }),
39
40
  })
41
+ .summary("stored membership and title changes")
40
42
  .description("who joined, left, was added or removed, and by whom — from the chat's service messages; " +
41
43
  `the last ${EVENTS_DAYS} days unless the query names time`)
42
44
  .argument("<chat>", messenger.chatArgument), { output: { entity: "chat_event" } }), EVENTS_QUERY).action(async function (chat, words) {
@@ -152,7 +154,7 @@ export const chatsCommand = (messenger) => {
152
154
  if (command)
153
155
  annotate(command, { examples: [`${messenger.app.command} personal chats ${path} ${args}`.trimEnd()] });
154
156
  }
155
- return chats;
157
+ return hideNever(messenger, chats);
156
158
  };
157
159
  /** Where the history is pushed, the chat list and a chat's card answer from the store and never connect. */
158
160
  const storedRead = (messenger) => (messenger.history === "store" ? LOCAL_READ : REMOTE_READ);
@@ -15,6 +15,7 @@ import { messengerContext } from "./context.js";
15
15
  export const membersCommand = (messenger) => {
16
16
  const members = new Command("members").description("who is in a group");
17
17
  members.addCommand(annotate(withQuery(withPaging(annotate(new Command("list"), { effects: REMOTE_READ })
18
+ .summary("group members and their roles")
18
19
  .description("everyone in a group, a page at a time, with their role and when they were last seen")
19
20
  .argument("<chat>", messenger.chatArgument)), MEMBERS_QUERY), { output: { entity: "member" } }).action(async function (chat, words) {
20
21
  const context = messengerContext(this, messenger);
@@ -23,6 +24,7 @@ export const membersCommand = (messenger) => {
23
24
  renderAnswer(this, context, body, lines(({ id, name, username, role }) => [id, name ?? "", username ? `@${username}` : "", role ?? ""].join(" ")));
24
25
  }));
25
26
  members.addCommand(withQuery(annotate(annotate(new Command("audit"), { effects: REMOTE_READ, offline: false })
27
+ .summary("members that look like bots, with reasons")
26
28
  .description("members that look like bots, each with its reasons — read from the member list and the local store; " +
27
29
  "never one request per person, and it removes nobody")
28
30
  .argument("<chat>", messenger.chatArgument)
@@ -54,6 +56,7 @@ export const membersCommand = (messenger) => {
54
56
  ].join(" ")));
55
57
  }));
56
58
  members.addCommand(withQuery(annotate(annotate(new Command("history"), { effects: LOCAL_READ })
59
+ .summary("stored membership changes")
57
60
  .description("who joined, who left and whose profile changed, oldest first — what chats members fetch recorded in the " +
58
61
  "local store; never asks the messenger")
59
62
  .argument("<chat>", messenger.chatArgument), { output: { entity: "chat_event" } }), HISTORY_QUERY).action(async function (chat, words) {
@@ -69,6 +72,7 @@ export const membersCommand = (messenger) => {
69
72
  ].join(" ")));
70
73
  }));
71
74
  const fetch = annotate(new Command("fetch"), { effects: FETCH, offline: false })
75
+ .summary("save the group's current membership")
72
76
  .description("read a group's whole member list into the local store's member history: who joined, who left, daily " +
73
77
  "counts and profile changes; someone is recorded as gone only when every member was read")
74
78
  .argument("<chat>", messenger.chatArgument);
@@ -9,6 +9,7 @@ import { messengerContext } from "./context.js";
9
9
  import { chatStatsAnswer, MEMBERS_UNRECORDED, refuseJsonl } from "./stats-answers.js";
10
10
  const PERIODS = ["day", "week"];
11
11
  export const statsCommand = (messenger) => annotate(new Command("show"), { effects: REMOTE_READ, output: { entity: "chat_stats" } })
12
+ .summary("activity and participation in one chat")
12
13
  .description("a group's or channel's numbers for a period: messages, active members, replies, reactions, questions answered, " +
13
14
  `joins and leaves — counted from the local store; ${messenger.history === "store" ? "joins and leaves too" : "joins and leaves are asked of the messenger"}`)
14
15
  .argument("<chat>", messenger.chatArgument)
@@ -7,7 +7,9 @@ import { lines, wholeList } from "./chat-answers.js";
7
7
  import { messengerContext, refuseLocalWrite } from "./context.js";
8
8
  /** The chats whose member lists `serve` fetches daily; kept in the local store, never sent. */
9
9
  export const trackingCommand = (messenger) => {
10
- const tracking = new Command("tracking").description("the chats whose member lists serve fetches daily into the local store — chats members fetch --track adds one");
10
+ const tracking = new Command("tracking")
11
+ .summary("chats whose member lists are fetched daily")
12
+ .description("the chats whose member lists serve fetches daily into the local store — chats members fetch --track adds one");
11
13
  annotate(tracking.command("list"), { effects: LOCAL_READ })
12
14
  .description("every tracked chat: since when, and its last member count")
13
15
  .action(async function () {
@@ -22,6 +24,7 @@ export const trackingCommand = (messenger) => {
22
24
  ].join(" ")));
23
25
  });
24
26
  annotate(tracking.command("show"), { effects: LOCAL_READ })
27
+ .summary("one chat's member tracking and daily counts")
25
28
  .description(`one chat: whether it is tracked, and its member count per day for the last ${TRACKED_DAYS} days`)
26
29
  .argument("<chat>", messenger.chatArgument)
27
30
  .action(async function (chat) {
@@ -19,9 +19,13 @@ const SHELLS = ["zsh", "bash", "fish", "powershell"];
19
19
  * ⚠ **A Tab never connects and never creates anything.** Names come from the message store if it
20
20
  * exists — no store, no names — and nothing goes to stderr: a shell shows whatever it is given.
21
21
  */
22
- export const completeCommand = (messenger, config, options = {}) => annotate(new Command("complete"), { effects: LOCAL_READ, mcp: false })
22
+ export const completeCommand = (messenger, config, options = {}) => annotate(new Command("complete"), {
23
+ effects: LOCAL_READ,
24
+ mcp: false,
25
+ examples: [`${messenger.app.command} complete zsh`],
26
+ })
23
27
  .description(`shell completion: \`${messenger.app.command} complete zsh\` prints the script to source`)
24
- .argument("[words...]")
28
+ .argument("[words...]", "zsh for the shell script, or command words to complete")
25
29
  .allowUnknownOption()
26
30
  .helpOption(false)
27
31
  .action(async function (words) {
@@ -19,6 +19,7 @@ import { commandAt, nextStep } from "../steps.js";
19
19
  import { contactWriteCommands } from "./admin-contacts-command.js";
20
20
  import { contactAnswer, lines, pageOf } from "./chat-answers.js";
21
21
  import { messengerContext } from "./context.js";
22
+ import { hideNever } from "./lacks.js";
22
23
  import { CONTACTS_QUERY, notesAllowed } from "./people-queries.js";
23
24
  import { privatePeopleCommands } from "./private-people-command.js";
24
25
  import { syncsFirst } from "./search-sync-options.js";
@@ -68,7 +69,8 @@ export const contactsCommand = (messenger) => {
68
69
  }));
69
70
  renderAnswer(this, context, itemOf(contactAnswer(account, card)));
70
71
  }));
71
- annotate(contacts.command("profile"), { effects: REMOTE_READ })
72
+ annotate(contacts.command("profile"), { effects: REMOTE_READ, output: { entity: "contact_profile" } })
73
+ .summary("a person's profile from the messenger")
72
74
  .description("everything the messenger says about one person — handles, flags, last seen, when they registered — and " +
73
75
  "how many of their messages the store holds in each chat you share, the first and the last, and the " +
74
76
  "earlier names and usernames the store saw them with")
@@ -86,6 +88,7 @@ export const contactsCommand = (messenger) => {
86
88
  }));
87
89
  });
88
90
  const contextCommand = annotate(contacts.command("context"), { effects: REMOTE_READ })
91
+ .summary("what the local archive knows about a person")
89
92
  .description("what the store holds about one person, in every messenger linked to them: shared chats, the last " +
90
93
  "messages each way, their recent messages, where others mentioned them — never connects")
91
94
  .argument("<person>", "their id, @username, or part of their name")
@@ -167,6 +170,7 @@ export const contactsCommand = (messenger) => {
167
170
  }));
168
171
  });
169
172
  annotate(contacts.command("timeline"), { effects: REMOTE_READ })
173
+ .summary("one person's activity across their linked identities")
170
174
  .description("everything one person took part in, in every messenger linked to them — messages they wrote or were " +
171
175
  "mentioned in, chats, mail, meetings, tasks — newest first, from the store; never connects")
172
176
  .argument("<person>", "their id, @username, or part of their name")
@@ -208,6 +212,7 @@ export const contactsCommand = (messenger) => {
208
212
  }));
209
213
  });
210
214
  annotate(contacts.command("check"), { effects: REMOTE_READ })
215
+ .summary("check a person's profile and messages for signs of abuse")
211
216
  .description(registriesCover(messenger.provider)
212
217
  ? "whether one person looks like a bot, a fake or a spammer: their profile, what they wrote in the store, " +
213
218
  `and the public ban lists (${Object.values(REGISTRIES)
@@ -235,6 +240,7 @@ export const contactsCommand = (messenger) => {
235
240
  renderAnswer(this, context, itemOf(snakeKeys(answer)));
236
241
  });
237
242
  annotate(contacts.command("link"), { effects: LOCAL_WRITE })
243
+ .summary("link identities that belong to one person")
238
244
  .description("record that two people in the store are one person — the same name is never enough")
239
245
  .argument("<person>", "their id, @username, or part of their name")
240
246
  .argument("<other>", "the same in another messenger of the store, as <messenger>:<person> — max:Ana; me: it is you, as zoom:<id> me")
@@ -255,7 +261,8 @@ export const contactsCommand = (messenger) => {
255
261
  * ⚠ **The number is asked for or piped, never an argument**: argv is read by `ps` and kept by shell
256
262
  * history, and a phone number is personal data.
257
263
  */
258
- annotate(contacts.command("lookup"), { effects: REMOTE_READ })
264
+ annotate(contacts.command("lookup"), { effects: REMOTE_READ, output: { entity: "contact_lookup" } })
265
+ .summary("find an account by a phone number")
259
266
  .description("who has this phone number — asks for it, or reads it from stdin; never an argument")
260
267
  // Commander's own refusal would repeat the number on stderr.
261
268
  .allowExcessArguments()
@@ -267,7 +274,7 @@ export const contactsCommand = (messenger) => {
267
274
  const phone = phoneOf(await readSecret("phone number: ", { ...secretInputOf(this), echo: true }));
268
275
  renderAnswer(this, context, itemOf(snakeKeys(await context.withServices((services) => services.people.lookup(phone)))));
269
276
  });
270
- annotate(contacts.command("sync"), { effects: FETCH })
277
+ annotate(contacts.command("sync"), { effects: FETCH, output: { entity: "contact_sync" } })
271
278
  .description("take the whole contact list from the messenger into the local store")
272
279
  .action(async function () {
273
280
  const context = messengerContext(this, messenger);
@@ -309,5 +316,5 @@ export const contactsCommand = (messenger) => {
309
316
  if (command)
310
317
  annotate(command, { examples: [`${messenger.app.command} personal contacts ${path} ${args}`.trimEnd()] });
311
318
  }
312
- return contacts;
319
+ return hideNever(messenger, contacts);
313
320
  };
@@ -95,6 +95,11 @@ export interface Messenger extends ServiceMessenger {
95
95
  * connecting, as the adapter would refuse after; one connection per device makes that connection a cost.
96
96
  */
97
97
  lacks?: readonly (keyof MessengerAdapter)[];
98
+ /**
99
+ * Adapter methods this messenger can never have, where `lacks` is what it does not have yet: their commands refuse
100
+ * the same way and are also hidden from help, `commands` and MCP. WhatsApp: only the phone ends other sessions.
101
+ */
102
+ never?: readonly (keyof MessengerAdapter)[];
98
103
  /** Whether a message can be deleted for the owner alone; yes when unset. No: every delete is for everyone (WhatsApp). */
99
104
  deleteForMe?: boolean;
100
105
  /** Whether the messenger has forum topics: `--topic`, `topic:` in `messages list` and the `topics_*` tools; yes when unset. */
@@ -30,8 +30,11 @@ import { readAll } from "./stdin.js";
30
30
  * mentions, one sender's messages in a row. From the store alone; nothing is built until asked (phase 3).
31
31
  */
32
32
  export const conversationsCommand = (messenger) => {
33
- const conversations = new Command("conversations").description("the conversations inside a chat, found in the stored messages by replies, mentions and who wrote next");
33
+ const conversations = new Command("conversations")
34
+ .summary("group stored messages into conversations")
35
+ .description("the conversations inside a chat, found in the stored messages by replies, mentions and who wrote next");
34
36
  boundOptions(reachesOtherService(annotate(conversations.command("build"), { effects: REMOTE_WRITE }))
37
+ .summary("build conversation links from stored messages")
35
38
  .description("find a chat's conversations in what the store holds, replacing the last build; without --chat, every " +
36
39
  "chat that changed since its build and every group never built; never asks the messenger")
37
40
  .option("--chat <chat>", messenger.chatArgument)
@@ -111,6 +114,7 @@ export const conversationsCommand = (messenger) => {
111
114
  renderAnswer(this, context, itemOf(snakeKeys(built)), () => `${built.messages} messages → ${built.conversations} conversations, ${built.links} links (rules v${built.rulesVersion})\n`);
112
115
  });
113
116
  withQuery(annotate(conversations.command("list"), { effects: LOCAL_READ })
117
+ .summary("stored conversations in one chat")
114
118
  .description("a chat's conversations, the newest first: when, how many messages, how many people")
115
119
  .argument("<chat>", messenger.chatArgument), CONVERSATION_QUERY)
116
120
  .option("--limit <n>", "how many", positiveCount("--limit"))
@@ -123,6 +127,7 @@ export const conversationsCommand = (messenger) => {
123
127
  context.renderer.note("no conversations match");
124
128
  });
125
129
  annotate(conversations.command("show"), { effects: LOCAL_READ })
130
+ .summary("one conversation and its messages")
126
131
  .description("one conversation's messages, oldest first — by its id, or the one a message is in")
127
132
  .argument("<conversation>", `a conversation id from \`conversations list\`; or ${messenger.chatArgument}, with a message`)
128
133
  .argument("[message]", "a message id in that chat: show the conversation it is in")
@@ -151,6 +156,7 @@ export const conversationsCommand = (messenger) => {
151
156
  renderAnswer(this, context, conversationShown(account, { summary, messages }));
152
157
  });
153
158
  withModelOptions(annotate(conversations.command("related"), { effects: LOCAL_READ })
159
+ .summary("conversations nearest in meaning to a message's conversation")
154
160
  .description("the conversations nearest in meaning to the one a message is in, in every built chat, best first — " +
155
161
  "from the vectors `conversations embed` stored; runs no model")
156
162
  .argument("<chat>", messenger.chatArgument)
@@ -166,6 +172,7 @@ export const conversationsCommand = (messenger) => {
166
172
  context.renderer.note(`no other conversation has vectors of ${found.model}`);
167
173
  });
168
174
  withModelOptions(annotate(conversations.command("status"), { effects: LOCAL_READ })
175
+ .summary("which chats are ready for conversation search")
169
176
  .description("how fresh each built chat's conversations and vectors are: messages the build has not seen, chunks " +
170
177
  "with a current, stale or missing vector")
171
178
  .option("--chat <chat>", `only this chat: ${messenger.chatArgument}`)).action(async function () {
@@ -179,9 +186,11 @@ export const conversationsCommand = (messenger) => {
179
186
  });
180
187
  const batches = conversations
181
188
  .command("batches")
189
+ .summary("batches waiting for an agent's conversation links")
182
190
  .description("windows of a chat for your own AI agent to link: which earlier message each one answers");
183
191
  const sizeOption = (command) => command.option("--size <n>", `messages to answer per batch, ${BATCH_SIZE.min}–${BATCH_SIZE.max}; ${BATCH_SIZE.default} by default`, positiveCount("--size"));
184
192
  sizeOption(annotate(batches.command("status"), { effects: LOCAL_READ })
193
+ .summary("progress of conversation-link batches")
185
194
  .description("how many messages still wait for an answer, in how many batches, and how much text")
186
195
  .requiredOption("--chat <chat>", messenger.chatArgument)).action(async function () {
187
196
  const { chat, size = BATCH_SIZE.default } = this.opts();
@@ -191,6 +200,7 @@ export const conversationsCommand = (messenger) => {
191
200
  `${status.characters} characters (about ${status.tokensEstimate} tokens)\n`);
192
201
  });
193
202
  sizeOption(annotate(batches.command("next"), { effects: LOCAL_READ })
203
+ .summary("get the next batch of messages to link")
194
204
  .description("the next window to answer, with the messages before it; message text goes to stdout only")
195
205
  .requiredOption("--chat <chat>", messenger.chatArgument)).action(async function () {
196
206
  const { chat, size = BATCH_SIZE.default } = this.opts();
@@ -221,6 +231,7 @@ export const conversationsCommand = (messenger) => {
221
231
  description: '{ "model", "skill"?, "answers": [{ "message", "parent", "confidence" }] }',
222
232
  json: true,
223
233
  })
234
+ .summary("save an agent's conversation-link answers")
224
235
  .description('store your agent\'s answer to a batch, read as JSON from stdin: { "model", "answers": [{ "message", ' +
225
236
  '"parent", "confidence" }] }; all or nothing')
226
237
  .requiredOption("--batch <id>", "the batch id `conversations batches next` printed")
@@ -233,6 +244,7 @@ export const conversationsCommand = (messenger) => {
233
244
  renderAnswer(this, context, itemOf(stored, { next: [nextStep("conversations.build", { args: { chat: stored.chat } })] }), () => `${stored.stored} answers stored\n`);
234
245
  });
235
246
  annotate(links.command("clear"), { effects: LOCAL_WRITE })
247
+ .summary("remove an agent's saved conversation-link answers")
236
248
  .description("drop your agent's answers for a chat, or only one model's; messages are never touched")
237
249
  .requiredOption("--chat <chat>", messenger.chatArgument)
238
250
  .option("--model <model>", "only the answers this model gave")
@@ -244,6 +256,7 @@ export const conversationsCommand = (messenger) => {
244
256
  renderAnswer(this, context, itemOf(cleared), () => `${cleared.cleared} answers dropped\n`);
245
257
  });
246
258
  const embed = withModelOptions(reachesOtherService(annotate(new Command("embed"), { effects: REMOTE_WRITE }))
259
+ .summary("prepare conversation vectors for meaning search")
247
260
  .description("compute a vector for each chunk of a chat's conversations for search by meaning — on this machine, or " +
248
261
  "with --provider through a service and your key; resumes where it stopped; without --chat, every " +
249
262
  "built chat with chunks left, on this machine only")
@@ -290,6 +303,7 @@ export const conversationsCommand = (messenger) => {
290
303
  }), () => `${done.embedded} chunks embedded with ${done.model}${done.skipped ? `, ${done.skipped} changed since the build` : ""}\n`);
291
304
  });
292
305
  withModelOptions(annotate(embed.command("status"), { effects: LOCAL_READ })
306
+ .summary("which conversation vectors are ready or stale")
293
307
  .description("how many chunks of a chat have a vector of the model, how many are left, and what is left costs")
294
308
  .option("--chat <chat>", messenger.chatArgument)).action(async function () {
295
309
  const options = this.optsWithGlobals();
@@ -303,6 +317,7 @@ export const conversationsCommand = (messenger) => {
303
317
  "\n");
304
318
  });
305
319
  withModelOptions(destroysNothing(annotate(embed.command("clear"), { effects: LOCAL_WRITE }))
320
+ .summary("remove stored conversation vectors")
306
321
  .description("drop a chat's vectors, or only one model's; messages and conversations are never touched")
307
322
  .option("--chat <chat>", messenger.chatArgument)).action(async function () {
308
323
  const options = this.optsWithGlobals();
@@ -479,6 +494,7 @@ export const linksCommand = (messenger) => annotate(new Command("links"), {
479
494
  examples: [`${messenger.app.command} personal messages links "Book club" 101`],
480
495
  effects: LOCAL_READ,
481
496
  })
497
+ .summary("why a message belongs to its conversation")
482
498
  .description("why a message is in its conversation: each link it has, and the chain of answers back to the start")
483
499
  .argument("<chat>", messenger.chatArgument)
484
500
  .argument("<message>", "the message id")
@@ -511,6 +527,7 @@ const readinessLine = ({ chat, state, graph, pending, vectors }, model) => graph
511
527
  `${vectors.missing} missing of ${vectors.chunks} chunks`;
512
528
  /** `search conversations`: the conversations nearest to a query, in meaning and in words. */
513
529
  export const conversationsSearchCommand = (messenger) => syncOptions(boundOptions(withModelOptions(reachesOtherService(annotate(new Command("conversations"), { effects: REMOTE_READ })))), messenger)
530
+ .summary("find conversations by words and meaning")
514
531
  .description("the conversations nearest to a query in meaning and in words, best first, in one chat or every one — " +
515
532
  "meaning after `conversations embed`; runs on this machine")
516
533
  .argument("<query>", "what to look for, in your own words, in any language the model reads")
@@ -10,6 +10,7 @@ export const countersCommand = (messenger) => {
10
10
  const parent = new Command("counters").description("per-counter observations and bounded remote refresh");
11
11
  for (const operation of ["show", "refresh"]) {
12
12
  const command = new Command(operation)
13
+ .summary("refresh message counters from the messenger")
13
14
  .description(operation === "show"
14
15
  ? "show saved counter values and their observation freshness"
15
16
  : "read authoritative counters for bounded messages and update their local observations")
@@ -57,7 +57,7 @@ const INCLUDES = [
57
57
  const EXCLUDES = ["message text", "chat titles", "names", "phone numbers", "the session", "the app credentials"];
58
58
  const reportCommand = (messenger) => {
59
59
  const { app } = messenger;
60
- const report = annotate(new Command("report"), { effects: REMOTE_READ, mcp: false })
60
+ const report = annotate(new Command("report"), { effects: REMOTE_READ })
61
61
  .description("what a problem report holds; writes nothing")
62
62
  .action(function () {
63
63
  const output = outputFor(this);
@@ -74,7 +74,7 @@ const reportCommand = (messenger) => {
74
74
  `About a given run: ${create} --run <id> (ids: ${lineOf(this, "runs list")})`,
75
75
  ].join("\n"));
76
76
  });
77
- destroysNothing(annotate(report.command("create"), { effects: LOCAL_WRITE }))
77
+ destroysNothing(annotate(report.command("create"), { effects: LOCAL_WRITE, mcp: false }))
78
78
  .description("write a problem report to a file, and say where to send it")
79
79
  .option("--run <id>", "the run the report is about; the newest failed one if not given")
80
80
  .option("--output <file>", "where to write it; a new file in this directory if not given")
@@ -21,6 +21,7 @@ export const downloadSubcommand = (messages, messenger) => annotate(messages.com
21
21
  examples: [`${messenger.app.command} personal messages download "Book club" 101 --output-dir ./downloads`],
22
22
  effects: FETCH,
23
23
  })
24
+ .summary("download files attached to stored messages")
24
25
  .description("save a message's photos, files, videos and voice notes to a folder — or a whole chat's with --all")
25
26
  .argument("<chat>", messenger.chatArgument)
26
27
  .argument("[message]", "the message id; left out with --all")
@@ -14,7 +14,8 @@ export const floodCommand = (messenger) => {
14
14
  const name = messenger.name ?? app.command;
15
15
  return annotate(new Command("flood"), { mcp: false })
16
16
  .description(`the waits ${name} asked this profile to keep, and a hold on its writes`)
17
- .addCommand(grantsAuthority(annotate(new Command("clear"), { effects: LOCAL_WRITE }))
17
+ .addCommand(grantsAuthority(annotate(new Command("clear"), { effects: LOCAL_WRITE, examples: [`${app.command} personal flood clear`] }))
18
+ .summary("clear a profile's stored rate-limit hold")
18
19
  .description(`forget them, lift the hold and the profile's pace, once ${name} no longer limits the account; changes nothing there`)
19
20
  .action(async function () {
20
21
  const context = messengerContext(this, messenger);
@@ -14,9 +14,10 @@ import { messengerContext } from "./context.js";
14
14
  * a model that a message talked into writing somewhere, not an agent set on getting around it.
15
15
  */
16
16
  export const recipientsCommand = (messenger) => {
17
+ const tool = `${messenger.app.command} personal`;
17
18
  const listFor = (profile, env) => new RecipientList(recipientsPathFor(messenger.app, profile, env), messenger.app.command);
18
19
  const command = new Command("recipients").description("the chats this profile may send to, when the list is on");
19
- annotate(command.command("list"), { effects: LOCAL_READ })
20
+ annotate(command.command("list"), { effects: LOCAL_READ, examples: [`${tool} recipients list`] })
20
21
  .description("the chats on the list; empty and off until the first add")
21
22
  .action(async function () {
22
23
  const context = messengerContext(this, messenger);
@@ -28,7 +29,7 @@ export const recipientsCommand = (messenger) => {
28
29
  else if (chats.length === 0)
29
30
  renderer.note("the recipient list is on and empty — this profile may send nowhere");
30
31
  });
31
- grantsAuthority(annotate(command.command("add"), { effects: FETCH }))
32
+ grantsAuthority(annotate(command.command("add"), { effects: FETCH, examples: [`${tool} recipients add "Book club"`] }))
32
33
  .argument("<chat>", messenger.chatArgument)
33
34
  .description("allow sending to this chat; the first add turns the list on")
34
35
  .action(async function (chat) {
@@ -43,7 +44,7 @@ export const recipientsCommand = (messenger) => {
43
44
  });
44
45
  renderAnswer(this, context, itemOf({ id: found.id, title: found.title, added }));
45
46
  });
46
- grantsAuthority(annotate(command.command("remove"), { effects: LOCAL_WRITE }))
47
+ grantsAuthority(annotate(command.command("remove"), { effects: LOCAL_WRITE, examples: [`${tool} recipients remove "Book club"`] }))
47
48
  .argument("<chat>", "chat id, or the title as the list shows it")
48
49
  .description("stop allowing this chat; the list stays on")
49
50
  .action(async function (chat) {
@@ -54,7 +55,7 @@ export const recipientsCommand = (messenger) => {
54
55
  throw new CliError("not_found", `${chat.trim()} is not on the recipient list of ${settings.profile}`);
55
56
  renderAnswer(this, context, itemOf({ id: gone.id, title: gone.title, removed: true }));
56
57
  });
57
- grantsAuthority(annotate(command.command("clear"), { effects: LOCAL_WRITE }))
58
+ grantsAuthority(annotate(command.command("clear"), { effects: LOCAL_WRITE, examples: [`${tool} recipients clear`] }))
58
59
  .description("delete the list, which turns it off: this profile may send to any chat again")
59
60
  .action(async function () {
60
61
  const context = messengerContext(this, messenger);