@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
@@ -29,10 +29,12 @@ export const PARTIAL_NOTE = "the messenger did not list every chat, and did not
29
29
  * `date>2h` is a one-off look from a time of your choosing and leaves the points where they were.
30
30
  */
31
31
  export const inboxCommand = (messenger) => annotate(new Command("inbox"), {
32
+ examples: [`${messenger.app.command} personal inbox --limit 10`],
32
33
  effects: REMOTE_READ,
33
34
  output: { entity: "message", view: "summary" },
34
35
  query: INBOX_QUERY,
35
36
  })
37
+ .summary("unread chats and their newest messages")
36
38
  .description("other people's unread messages in every chat; --new for what arrived since the last check")
37
39
  .addHelpText("after", queryHelp(INBOX_QUERY))
38
40
  .argument("[query...]", "from when: date>2h, date>2026-10-01; the saved point stays put")
@@ -1,3 +1,4 @@
1
+ import type { Command } from "commander";
1
2
  import type { MessengerAdapter } from "../../ports/messenger.js";
2
3
  type Need = readonly [method: keyof MessengerAdapter, what: string];
3
4
  /**
@@ -9,10 +10,16 @@ export declare const COMMAND_NEEDS: Readonly<Record<string, Need>>;
9
10
  interface Lacking {
10
11
  channelComments?: boolean;
11
12
  lacks?: readonly (keyof MessengerAdapter)[];
13
+ never?: readonly (keyof MessengerAdapter)[];
12
14
  history?: "server" | "store";
13
15
  }
14
16
  /** The one method this command needs and this messenger never has; none when it has it or the store answers. */
15
17
  export declare const lackedNeed: (messenger: Lacking | undefined, path: string) => Need | undefined;
16
18
  /** Refuses, before a connection, a command whose one method this messenger never has. */
17
19
  export declare const refuseLacking: (messenger: Lacking | undefined, path: readonly string[]) => void;
20
+ /**
21
+ * Hides under `group` each command whose one method this messenger can `never` have, from help, `commands` and MCP;
22
+ * typed anyway, it still refuses. A method it only `lacks` keeps its command in sight, since it may come.
23
+ */
24
+ export declare const hideNever: <T extends Command>(messenger: Lacking, group: T) => T;
18
25
  export {};
@@ -32,7 +32,7 @@ export const lackedNeed = (messenger, path) => {
32
32
  return undefined;
33
33
  if (path === "messages comments" && messenger?.channelComments === false)
34
34
  return need;
35
- if (!messenger?.lacks?.includes(need[0]))
35
+ if (!messenger?.lacks?.includes(need[0]) && !messenger?.never?.includes(need[0]))
36
36
  return undefined;
37
37
  return messenger.history === "store" && STORE_ANSWERS.has(path) ? undefined : need;
38
38
  };
@@ -42,3 +42,20 @@ export const refuseLacking = (messenger, path) => {
42
42
  if (need)
43
43
  throw new CliError("validation_error", `this messenger cannot ${need[1]}`);
44
44
  };
45
+ /**
46
+ * Hides under `group` each command whose one method this messenger can `never` have, from help, `commands` and MCP;
47
+ * typed anyway, it still refuses. A method it only `lacks` keeps its command in sight, since it may come.
48
+ */
49
+ export const hideNever = (messenger, group) => {
50
+ const walk = (command, path) => {
51
+ for (const child of command.commands) {
52
+ const at = [...path, child.name()];
53
+ const need = COMMAND_NEEDS[at.join(" ")];
54
+ if (need && messenger.never?.includes(need[0]))
55
+ child._hidden = true;
56
+ walk(child, at);
57
+ }
58
+ };
59
+ walk(group, [group.name()]);
60
+ return group;
61
+ };
@@ -21,7 +21,9 @@ export const httpTokenFile = (app, profile, env) => join(resolvePaths({ appName:
21
21
  const withFlags = (command) => command.option("--permission <key=level>", "make a permission stricter for this server only, like messages.send=deny; it never loosens the profile; repeat for more keys", (value, previous = []) => [...previous, value]);
22
22
  export const mcpCommand = (messenger) => {
23
23
  const { app } = messenger;
24
- const command = withFlags(annotate(new Command("mcp"), { effects: REMOTE_WRITE, lifetime: "persistent", mcp: false }).description(`serve this profile to an agent over MCP, on stdin and stdout โ€” \`claude mcp add ${app.command} -- ${app.command} <profile> mcp\``))
24
+ const command = withFlags(annotate(new Command("mcp"), { effects: REMOTE_WRITE, lifetime: "persistent", mcp: false })
25
+ .summary("serve this profile's commands to an AI agent")
26
+ .description(`serve this profile to an agent over MCP, on stdin and stdout โ€” \`claude mcp add ${app.command} -- ${app.command} <profile> mcp\``))
25
27
  .option("--http", "serve over HTTP on 127.0.0.1 for ChatGPT and Claude in the browser, behind your tunnel; the profile's permissions decide")
26
28
  .option("--port <port>", `the local port for --http (default ${DEFAULT_PORT})`)
27
29
  .option("--public-url <url>", "the tunnel's https address the browser apps use, e.g. https://<name>.ts.net")
@@ -55,7 +57,9 @@ export const mcpCommand = (messenger) => {
55
57
  command.addCommand(withFlags(annotate(new Command("config"), {
56
58
  effects: LOCAL_READ,
57
59
  examples: [`${app.command} personal mcp config`],
58
- }).description("print the mcpServers entry for Claude Desktop, Cursor and others, with full paths; writes nothing")).action(async function () {
60
+ })
61
+ .summary("print this profile's local MCP configuration")
62
+ .description("print the mcpServers entry for Claude Desktop, Cursor and others, with full paths; writes nothing")).action(async function () {
59
63
  const flags = this.optsWithGlobals();
60
64
  const context = messengerContext(this, messenger);
61
65
  const { settings, env } = context;
@@ -0,0 +1,3 @@
1
+ import type { Command } from "commander";
2
+ import { type Messenger } from "./context.js";
3
+ export declare const addMediaArchiveCommands: (archive: Command, messenger: Messenger) => void;
@@ -0,0 +1,72 @@
1
+ import { CliError } from "@wirecat/cli-base";
2
+ import { annotate } from "@wirecat/cli-base/commands";
3
+ import { parseDuration } from "../../domain/duration.js";
4
+ import { refusedPlace } from "../../sends/upload.js";
5
+ import { FETCHING } from "../../services/archive.js";
6
+ import { archiveMedia, checkMediaArchive, validateMediaSince } from "../../services/media-archive.js";
7
+ import { environmentOf, setExitCode } from "../context.js";
8
+ import { FETCH, LOCAL_READ } from "../effects.js";
9
+ import { itemOf, renderAnswer, snakeKeys } from "../envelope.js";
10
+ import { messengerContext } from "./context.js";
11
+ import { addMediaArchiveMove } from "./media-archive-move-command.js";
12
+ import { addMediaArchiveTimers } from "./media-archive-timer-command.js";
13
+ import { stopOnSignal } from "./patience.js";
14
+ export const addMediaArchiveCommands = (archive, messenger) => {
15
+ annotate(archive.command("download"), {
16
+ effects: FETCH,
17
+ examples: [`${messenger.app.command} attachments download ./media-archive`],
18
+ })
19
+ .description("archive media from selected chats; verify and keep existing copies")
20
+ .addHelpText("after", `\nMedia is saved only when you request a download; serve does not save these copies.\nTo disable an installed daily backup: ${messenger.app.command} <profile> attachments jobs stop <dir>.`)
21
+ .argument("<dir>", "archive directory; created if missing")
22
+ .option("--chat <chat>", `only this chat; repeat for more; ${messenger.chatArgument}`, (value, previous) => [...previous, value], [])
23
+ .option("--since-time <time>", "include messages from this ISO timestamp with a time zone")
24
+ .option("--pause <duration>", "pause between history pages", messenger.fetching?.pause ?? FETCHING.pause)
25
+ .action(async function (directory) {
26
+ const context = messengerContext(this, messenger);
27
+ const options = this.opts();
28
+ validateMediaSince(options.sinceTime);
29
+ const pauseMs = parseDuration(options.pause, "--pause");
30
+ if (context.settings.offline)
31
+ throw new CliError("validation_error", "archiving media needs online mode");
32
+ if (refusedPlace(directory, messenger.app, context.env))
33
+ throw new CliError("validation_error", "the archive cannot write credential, application or message-store folders");
34
+ const stop = stopOnSignal(this);
35
+ const environment = environmentOf(this);
36
+ const signal = AbortSignal.any([
37
+ stop.signal,
38
+ ...[environment.signal, environment.commandSignal].filter((one) => one !== undefined),
39
+ ]);
40
+ try {
41
+ const result = await context.withServices((services) => archiveMedia(services, {
42
+ directory,
43
+ chats: options.chat,
44
+ sinceTime: options.sinceTime,
45
+ pauseMs,
46
+ signal,
47
+ pageSize: messenger.history === "store" ? 100 : (messenger.fetching ?? FETCHING).page,
48
+ orderByTime: messenger.history !== "store" && messenger.fetching?.orderBy === "time",
49
+ }));
50
+ renderAnswer(this, context, itemOf(snakeKeys(result)), () => `${result.saved} saved, ${result.existing} verified copies already present; ${result.chats} chats\n`);
51
+ }
52
+ finally {
53
+ stop.release();
54
+ }
55
+ });
56
+ annotate(archive.command("check"), {
57
+ effects: LOCAL_READ,
58
+ examples: [`${messenger.app.command} attachments check ./media-archive`],
59
+ })
60
+ .description("rehash all archived files without connecting to the messenger")
61
+ .argument("<dir>", "archive directory")
62
+ .action(async function (directory) {
63
+ const context = messengerContext(this, messenger);
64
+ const environment = environmentOf(this);
65
+ const result = await checkMediaArchive(directory, environment.commandSignal ?? environment.signal);
66
+ if (!result.valid)
67
+ setExitCode(this, 1);
68
+ renderAnswer(this, context, itemOf(snakeKeys(result)), () => `${result.checked} files checked; ${result.files.filter((one) => !one.valid).length} missing or changed\n`);
69
+ });
70
+ addMediaArchiveTimers(archive, messenger);
71
+ addMediaArchiveMove(archive, messenger);
72
+ };
@@ -0,0 +1,3 @@
1
+ import type { Command } from "commander";
2
+ import { type Messenger } from "./context.js";
3
+ export declare const addMediaArchiveMove: (attachments: Command, messenger: Messenger) => void;
@@ -0,0 +1,39 @@
1
+ import { CliError } from "@wirecat/cli-base";
2
+ import { annotate } from "@wirecat/cli-base/commands";
3
+ import { refusedPlace } from "../../sends/upload.js";
4
+ import { moveMediaArchive } from "../../services/media-archive-move.js";
5
+ import { environmentOf } from "../context.js";
6
+ import { LOCAL_WRITE } from "../effects.js";
7
+ import { itemOf, renderAnswer, snakeKeys } from "../envelope.js";
8
+ import { messengerContext, refuseLocalWrite } from "./context.js";
9
+ export const addMediaArchiveMove = (attachments, messenger) => {
10
+ annotate(attachments.command("move"), {
11
+ effects: LOCAL_WRITE,
12
+ examples: [
13
+ `${messenger.app.command} attachments move ./media-archive ./older-images --before-time 2026-10-01T00:00:00Z --dry-run`,
14
+ ],
15
+ })
16
+ .description("move older images into another verified archive; remember their new locations")
17
+ .argument("<dir>", "source media archive directory")
18
+ .argument("<archive-dir>", "destination archive directory; created if needed")
19
+ .requiredOption("--before-time <time>", "move images whose message date is before this ISO timestamp with zone")
20
+ .option("--dry-run", "show counts and bytes without moving files or creating the destination")
21
+ .addHelpText("after", "\nCopies are verified at the destination before local image bytes are removed.\nSource manifests retain relocation records; checks follow them and downloads do not fetch them again.\nImages without a recorded message date are left in place and counted as undated.")
22
+ .action(async function (directory, archiveDirectory) {
23
+ const context = messengerContext(this, messenger);
24
+ const options = this.opts();
25
+ if (!options.dryRun)
26
+ refuseLocalWrite(context, messenger.app.command, "attachments.move");
27
+ if ([directory, archiveDirectory].some((folder) => refusedPlace(folder, messenger.app, context.env)))
28
+ throw new CliError("validation_error", "media archives cannot use credential, application or message-store folders");
29
+ const environment = environmentOf(this);
30
+ const result = await moveMediaArchive({
31
+ directory,
32
+ archiveDirectory,
33
+ beforeTime: options.beforeTime,
34
+ dryRun: options.dryRun,
35
+ signal: environment.commandSignal ?? environment.signal,
36
+ });
37
+ renderAnswer(this, context, itemOf(snakeKeys(result)), () => `${result.dryRun ? `${result.selected} images selected (${result.selectedBytes} bytes)` : `${result.moved} images moved (${result.movedBytes} bytes)`}; ${result.undated} undated images left in place\n`);
38
+ });
39
+ };
@@ -0,0 +1,3 @@
1
+ import type { Command } from "commander";
2
+ import { type Messenger } from "./context.js";
3
+ export declare const addMediaArchiveTimers: (attachments: Command, messenger: Messenger) => void;
@@ -0,0 +1,70 @@
1
+ import { realpath } from "node:fs/promises";
2
+ import { resolve } from "node:path";
3
+ import { CliError } from "@wirecat/cli-base";
4
+ import { annotate } from "@wirecat/cli-base/commands";
5
+ import { dailyArchiveTimer, manageDailyArchiveTimer, runArchiveTimerCommand, } from "../../background/daily-archive-timer.js";
6
+ import { thisMachine } from "../../background/system.js";
7
+ import { refusedPlace } from "../../sends/upload.js";
8
+ import { validateMediaSince } from "../../services/media-archive.js";
9
+ import { environmentOf } from "../context.js";
10
+ import { grantsAuthority, LOCAL_READ, LOCAL_WRITE } from "../effects.js";
11
+ import { itemOf, renderAnswer, snakeKeys } from "../envelope.js";
12
+ import { messengerContext } from "./context.js";
13
+ export const addMediaArchiveTimers = (attachments, messenger) => {
14
+ const archive = attachments.command("jobs").description("daily media archive timers for this profile");
15
+ for (const action of ["install", "start", "stop", "status", "uninstall"]) {
16
+ const command = annotate(archive.command(action), {
17
+ effects: action === "status" ? LOCAL_READ : LOCAL_WRITE,
18
+ ...(action === "status" ? {} : { mcp: false }),
19
+ examples: [`${messenger.app.command} attachments jobs ${action} ./media-archive`],
20
+ })
21
+ .description({
22
+ install: "install a Linux daily archive timer for this directory; starts nothing",
23
+ start: "enable the installed archive timer at login and start its daily schedule",
24
+ stop: "disable the archive timer; a download already running finishes",
25
+ status: "show whether this directory's archive timer is installed and active",
26
+ uninstall: "remove this directory's stopped archive timer; keep archived files",
27
+ }[action])
28
+ .argument("<dir>", "archive directory, used with the same profile as install");
29
+ if (action !== "status")
30
+ grantsAuthority(command);
31
+ if (action === "install")
32
+ command
33
+ .option("--at <HH:mm>", "daily start time", "03:00")
34
+ .option("--timezone <zone>", "IANA time zone of the daily start time", "UTC")
35
+ .option("--chat <chat>", `only this chat; repeat for more; ${messenger.chatArgument}`, (value, previous) => [...previous, value], [])
36
+ .option("--since-time <time>", "include messages from this ISO timestamp with a time zone");
37
+ command.action(async function (directory) {
38
+ const context = messengerContext(this, messenger);
39
+ const environment = environmentOf(this);
40
+ const signal = environment.commandSignal ?? environment.signal;
41
+ const system = environment.system ?? {
42
+ ...thisMachine(),
43
+ run: (args, env) => runArchiveTimerCommand(args, env, signal),
44
+ };
45
+ const options = this.opts();
46
+ validateMediaSince(options.sinceTime);
47
+ if (action === "install" && refusedPlace(directory, messenger.app, context.env))
48
+ throw new CliError("validation_error", "the archive cannot write credential, application or message-store folders");
49
+ // Canonicalize existing destinations so a symlink alias addresses the same timer.
50
+ const root = await realpath(directory).catch((error) => {
51
+ if (error.code !== "ENOENT")
52
+ throw error;
53
+ return resolve(directory);
54
+ });
55
+ const plan = dailyArchiveTimer({
56
+ app: messenger.app,
57
+ profile: context.profile,
58
+ directory: root,
59
+ entry: system.entry,
60
+ env: context.env,
61
+ chats: options.chat,
62
+ sinceTime: options.sinceTime,
63
+ at: options.at,
64
+ timeZone: options.timezone,
65
+ });
66
+ const result = await manageDailyArchiveTimer(plan, action, system, context.env, environment.commandSignal ?? environment.signal);
67
+ renderAnswer(this, context, itemOf(snakeKeys(result)), () => `${result.name}: ${action}\n`);
68
+ });
69
+ }
70
+ };
@@ -17,6 +17,7 @@ import { linksCommand } from "./conversations-command.js";
17
17
  import { downloadSubcommand } from "./download-command.js";
18
18
  import { heardItems, hearForCommand, hearingMissing, MODEL_OPTION, spokenItems, TRANSCRIBE_OPTION, } from "./hearing-command.js";
19
19
  import { cursorOf, historyQuery, historyWindow } from "./history-query.js";
20
+ import { hideNever } from "./lacks.js";
20
21
  import { threadAnswer } from "./message-answers.js";
21
22
  import { messageItem, messageItems, viewOf } from "./message-items.js";
22
23
  import { commentsCommand } from "./messages-comments-command.js";
@@ -174,7 +175,7 @@ export const messagesCommand = (messenger) => {
174
175
  messages.addCommand(commentsCommand(messenger));
175
176
  messages.addCommand(linksCommand(messenger));
176
177
  messages.addCommand(syncCommand(messenger));
177
- return messages;
178
+ return hideNever(messenger, messages);
178
179
  };
179
180
  /** Mail lives in the mail tables of the store, whichever messenger CLI reads it. */
180
181
  const isMailLocator = (chat, message) => message === undefined && isLocator(chat) && parseLocator(chat).provider === "email";
@@ -14,6 +14,7 @@ export const commentsCommand = (messenger) => {
14
14
  examples: [`${messenger.app.command} personal messages comments "News channel" 101 --limit 20`],
15
15
  effects: REMOTE_READ,
16
16
  })
17
+ .summary("comments on a channel post")
17
18
  .description("the comments under a channel post, oldest to newest; they live in its discussion group")
18
19
  .addHelpText("after", queryHelp(COMMENTS_QUERY))
19
20
  .argument("<chat>", `the channel: ${messenger.chatArgument}`)
@@ -17,7 +17,7 @@ export const deleteCommand = (messenger) => annotate(new Command("delete"), {
17
17
  })
18
18
  .description("delete messages for you only; with --for-everyone, for everyone in the chat")
19
19
  .argument("<chat>", messenger.chatArgument)
20
- .argument("<messages...>", `the message ids, by spaces or commas, at most ${DELETE_AT_ONCE}`)
20
+ .argument("<message...>", `the message ids, by spaces or commas, at most ${DELETE_AT_ONCE}`)
21
21
  .addOption(ownerOnlyOption("--for-everyone", "delete for everyone in the chat, not only for you โ€” they cannot get it back"))
22
22
  .addOption(ownerOnlyOption("--allow-dangerous", "go ahead without the question an ask level puts before a deletion"))
23
23
  .action(async function (chat, messages) {
@@ -26,6 +26,7 @@ export const messagesSearchCommand = (messenger) => {
26
26
  });
27
27
  const syncing = syncsFirst(messenger) ? "; optionally fetches new messages with --sync-first" : "";
28
28
  return command
29
+ .summary("find messages by words and filters")
29
30
  .description(messenger.serverSearch
30
31
  ? `search messenger messages in the local store and on the messenger's server (--backend)${syncing}; --discover searches the archive only`
31
32
  : `search messenger messages in the local store โ€” what was read, fetched or kept by serve${syncing}; --discover searches the archive only`)
@@ -9,6 +9,7 @@ import { syncOptions, syncRequest, syncsFirst } from "./search-sync-options.js";
9
9
  import { countsAnswer, refuseJsonl } from "./stats-answers.js";
10
10
  const GROUPINGS = ["chat", "sender", "day", "hour"];
11
11
  export const messagesStatsCommand = (messenger) => syncOptions(annotate(new Command("show"), { effects: REMOTE_READ, output: { entity: "message_counts" } }), messenger)
12
+ .summary("count stored messages by chat, sender, day or hour")
12
13
  .description(`how many stored messages match, by chat, sender, day or hour โ€” the local store only${syncsFirst(messenger) ? "; optionally fetches new messages with --sync-first" : ""}`)
13
14
  .argument("[query...]", "a strict Lucene query, as for search messages; none counts every stored message; with --saved, more words AND-ed to it")
14
15
  .option("--by <chat|sender|day|hour>", "what to count by (default: chat)", groupingOf)
@@ -58,9 +58,10 @@ const modelLine = (item, width) => `${item.default ? "*" : " "} ${item.id.padEnd
58
58
  (item.downloaded ? "downloaded" : "โ€”".padEnd(10));
59
59
  export const modelsCommand = (messenger) => {
60
60
  const tool = `${messenger.app.command} personal`;
61
- const models = annotate(new Command("models"), { mcp: false }).description("models that run on this machine");
61
+ const models = new Command("models").description("models that run on this machine");
62
62
  const audio = models.command("audio").description("speech models for transcribing voice messages");
63
63
  withQuery(annotate(audio.command("list"), { effects: LOCAL_READ, examples: [`${tool} models audio list`] }), MODELS_QUERY)
64
+ .summary("speech models and their download status")
64
65
  .description("the speech models, most suitable first, which are downloaded, and which one is the default")
65
66
  .action(async function (words) {
66
67
  const context = baseContext(this, messenger.resolveSettings);
@@ -75,6 +76,7 @@ export const modelsCommand = (messenger) => {
75
76
  });
76
77
  });
77
78
  reachesOtherService(annotate(audio.command("download"), {
79
+ mcp: false,
78
80
  effects: FETCH,
79
81
  output: { entity: "model" },
80
82
  examples: [`${tool} models audio download parakeet-v3`],
@@ -107,6 +109,7 @@ export const modelsCommand = (messenger) => {
107
109
  });
108
110
  const text = models.command("text").description("embedding models for searching conversations by meaning");
109
111
  withQuery(annotate(text.command("list"), { effects: LOCAL_READ, examples: [`${tool} models text list`] }), MODELS_QUERY)
112
+ .summary("embedding models and their download status")
110
113
  .description("the embedding models, most suitable first, which are downloaded, and which one is the default")
111
114
  .action(async function (words) {
112
115
  const context = baseContext(this, messenger.resolveSettings);
@@ -121,12 +124,14 @@ export const modelsCommand = (messenger) => {
121
124
  });
122
125
  });
123
126
  reachesOtherService(annotate(text.command("download"), {
127
+ mcp: false,
124
128
  effects: FETCH,
125
129
  output: { entity: "model" },
126
130
  examples: [`${tool} models text download ${DEFAULT_TEXT_MODEL}`],
127
131
  }))
128
132
  .argument("<model>", "a model id from `models text list`")
129
133
  .addOption(new Option("--accept-terms", "accept the model's licence terms, for a model that has its own"))
134
+ .summary("download and verify an embedding model")
130
135
  .description("download an embedding model once, checked against the sha256 this version expects")
131
136
  .action(async function (id, options) {
132
137
  const context = baseContext(this, messenger.resolveSettings);
@@ -168,7 +173,7 @@ export const modelsCommand = (messenger) => {
168
173
  /** `images models`: registered with picture search, the only thing these models serve. */
169
174
  export const imageModelsCommand = (messenger) => {
170
175
  const tool = messenger.app.command;
171
- const image = annotate(new Command("models"), { mcp: false }).description("models that read pictures, for `images embed` and picture search");
176
+ const image = new Command("models").description("models that read pictures, for `images embed` and picture search");
172
177
  withQuery(annotate(image.command("list"), { effects: LOCAL_READ, examples: [`${tool} images models list`] }), MODELS_QUERY)
173
178
  .description("the picture models, which are downloaded, and which one is the default")
174
179
  .action(async function (words) {
@@ -184,6 +189,7 @@ export const imageModelsCommand = (messenger) => {
184
189
  });
185
190
  });
186
191
  reachesOtherService(annotate(image.command("download"), {
192
+ mcp: false,
187
193
  effects: FETCH,
188
194
  output: { entity: "model" },
189
195
  examples: [`${tool} images models download ${DEFAULT_IMAGE_MODEL}`],
@@ -16,8 +16,8 @@ export const privatePeopleCommands = (messenger) => {
16
16
  const alias = new Command("alias").description("a private local display name in the selected account");
17
17
  annotate(alias.command("set"), { effects: LOCAL_WRITE })
18
18
  .description("your own name for a person in this account; kept on this machine, never sent")
19
- .argument("<person>")
20
- .argument("<alias>")
19
+ .argument("<person>", "the person's id, reference or known name")
20
+ .argument("<alias>", "the private name to use for this person")
21
21
  .action(async function (person, value) {
22
22
  const context = messengerContext(this, messenger);
23
23
  refuseLocalWrite(context, messenger.app.command, "contacts.alias.set");
@@ -25,7 +25,7 @@ export const privatePeopleCommands = (messenger) => {
25
25
  });
26
26
  annotate(alias.command("rm"), { effects: LOCAL_WRITE })
27
27
  .description("drop your local name for a person; their own name shows again")
28
- .argument("<person>")
28
+ .argument("<person>", "the person's id, reference or known name")
29
29
  .action(async function (person) {
30
30
  const context = messengerContext(this, messenger);
31
31
  refuseLocalWrite(context, messenger.app.command, "contacts.alias.rm");
@@ -34,7 +34,7 @@ export const privatePeopleCommands = (messenger) => {
34
34
  const notes = new Command("notes").description("your private notes on a stored contact, the same in every account that sees them");
35
35
  withQuery(annotate(notes.command("list"), { effects: LOCAL_READ })
36
36
  .description("every private note you keep on a person, oldest first")
37
- .argument("<person>"), NOTES_QUERY).action(async function (person, words) {
37
+ .argument("<person>", "the person's id, reference or known name"), NOTES_QUERY).action(async function (person, words) {
38
38
  const context = messengerContext(this, messenger);
39
39
  listQuery(typedQuery(words), NOTES_QUERY, "contacts notes list");
40
40
  const items = await context.withServices((services) => services.privatePeople.notes(person));
@@ -42,8 +42,8 @@ export const privatePeopleCommands = (messenger) => {
42
42
  });
43
43
  annotate(notes.command("show"), { effects: LOCAL_READ, output: { entity: "contact_note" } })
44
44
  .description("one private note on a person, with its revision for edit")
45
- .argument("<person>")
46
- .argument("<id>")
45
+ .argument("<person>", "the person's id, reference or known name")
46
+ .argument("<id>", "the note id from contacts notes list")
47
47
  .action(async function (person, id) {
48
48
  const context = messengerContext(this, messenger);
49
49
  renderAnswer(this, context, itemOf(await context.withServices((services) => services.privatePeople.note(person, id))));
@@ -56,10 +56,12 @@ export const privatePeopleCommands = (messenger) => {
56
56
  .description(name === "add"
57
57
  ? "add a private note on a person from stdin or --file; never sent"
58
58
  : "replace a note's text; --revision must match the one you read")
59
- .argument("<person>")
59
+ .argument("<person>", "the person's id, reference or known name")
60
60
  .addOption(ownerOnlyOption("--file <path>", "read note text from a file; omitted or - reads stdin"));
61
61
  if (name === "edit")
62
- command.argument("<id>").requiredOption("--revision <n>", "the revision you read before editing");
62
+ command
63
+ .argument("<id>", "the note id from contacts notes list")
64
+ .requiredOption("--revision <n>", "the revision you read before editing");
63
65
  command.action(async function (person, id) {
64
66
  const context = messengerContext(this, messenger);
65
67
  refuseLocalWrite(context, messenger.app.command, `contacts.notes.${name}`);
@@ -76,8 +78,8 @@ export const privatePeopleCommands = (messenger) => {
76
78
  }
77
79
  annotate(notes.command("remove"), { effects: LOCAL_WRITE })
78
80
  .description("delete one private note on a person")
79
- .argument("<person>")
80
- .argument("<id>")
81
+ .argument("<person>", "the person's id, reference or known name")
82
+ .argument("<id>", "the note id from contacts notes list")
81
83
  .action(async function (person, id) {
82
84
  const context = messengerContext(this, messenger);
83
85
  refuseLocalWrite(context, messenger.app.command, "contacts.notes.remove");
@@ -25,6 +25,7 @@ const selectionOf = (value) => {
25
25
  };
26
26
  export const rankingsTopCommand = (messenger, target) => {
27
27
  const command = syncOptions(annotate(new Command("top"), { effects: REMOTE_READ, output: { entity: "ranking" } }), messenger)
28
+ .summary("rank messages or people by a measure or score")
28
29
  .description(`rank ${target === "messages" ? "stored messages" : "the human authors of stored messages"} by a measure or explainable score; counters are snapshots with per-field observation freshness`)
29
30
  .argument("[query...]", "a strict Lucene query; none selects every stored message")
30
31
  .addOption(new Option("--measure <name>", "the ranking metric; not with score or weights").choices(target === "messages" ? [...MESSAGE_MEASURES] : [...AUTHOR_MEASURES]))
@@ -86,6 +87,7 @@ const emojiSplit = (reactions) => [
86
87
  ...(reactions.length > SHOWN_REACTIONS ? [`+${reactions.length - SHOWN_REACTIONS}`] : []),
87
88
  ].join(" ยท ");
88
89
  export const rankingEvidenceCommand = (messenger, target) => annotate(new Command("evidence"), { effects: LOCAL_READ, output: { entity: "evidence" } })
90
+ .summary("evidence for an exact statistics selection")
89
91
  .description("bounded messages, answer pairs or retention members from an exact drilldown selection")
90
92
  .argument(target === "messages" ? "<message>" : "<person>", target === "messages"
91
93
  ? "the canonical message locator, or retention cohort reference, from drilldown"
@@ -6,7 +6,10 @@ import { messengerContext } from "./context.js";
6
6
  /** `reactions add|remove`: the owner's own reaction on a message. */
7
7
  export const reactionsCommand = (messenger) => {
8
8
  const reactions = new Command("reactions").description("react to messages");
9
- annotate(reactions.command("add"), { effects: REMOTE_WRITE })
9
+ annotate(reactions.command("add"), {
10
+ effects: REMOTE_WRITE,
11
+ examples: [`${messenger.app.command} personal reactions add "Book club" 101 ๐Ÿ‘`],
12
+ })
10
13
  .description("put your reaction on a message; it replaces the one you had")
11
14
  .argument("<chat>", messenger.chatArgument)
12
15
  .argument("<message>", "the message id")
@@ -16,7 +19,10 @@ export const reactionsCommand = (messenger) => {
16
19
  const reacted = await context.withServices((services) => services.messages.react({ chat, message: message.trim(), emoji: emoji.trim() }));
17
20
  renderAnswer(this, context, itemOf(snakeKeys(reacted)));
18
21
  });
19
- annotate(reactions.command("remove"), { effects: REMOTE_WRITE })
22
+ annotate(reactions.command("remove"), {
23
+ effects: REMOTE_WRITE,
24
+ examples: [`${messenger.app.command} personal reactions remove "Book club" 101`],
25
+ })
20
26
  .description("take your reaction off a message")
21
27
  .argument("<chat>", messenger.chatArgument)
22
28
  .argument("<message>", "the message id")
@@ -14,7 +14,7 @@ import { replyConsentsCommand } from "./replies-consents-command.js";
14
14
  import { addReplyEditors } from "./replies-edit-command.js";
15
15
  /** `replies`: rules that answer messages for the owner, from `serve`, and only to the people and chats the audience allows. */
16
16
  export const repliesCommand = (messenger) => {
17
- const replies = annotate(new Command("replies"), { mcp: false }).description("rules that answer messages for you, kept in a file of this profile");
17
+ const replies = new Command("replies").description("rules that answer messages for you, kept in a file of this profile");
18
18
  if (messenger.autoReplies === false)
19
19
  replies.hook("preAction", () => {
20
20
  throw new CliError("validation_error", `${messenger.name ?? messenger.app.command} cannot send automatic replies`);
@@ -6,6 +6,7 @@ import { positiveCount } from "../paging.js";
6
6
  import { messengerContext } from "./context.js";
7
7
  import { refuseJsonl, retentionAnswer } from "./stats-answers.js";
8
8
  export const retentionCommand = (messenger) => annotate(new Command("retention"), { effects: REMOTE_READ, output: { entity: "retention" } })
9
+ .summary("joining cohorts and observed membership checkpoints")
9
10
  .description("joining cohorts and observed checkpoint membership from saved roster observations")
10
11
  .argument("<chat>", messenger.chatArgument)
11
12
  .option("--since-time <time>", "joining period starts at ISO 8601 or a relative time; last 90 days by default")
@@ -10,7 +10,7 @@ import { REMOTE_READ } from "../effects.js";
10
10
  import { renderAnswer } from "../envelope.js";
11
11
  import { queryHelp } from "../list-query.js";
12
12
  import { pointsOf } from "./catch-up.js";
13
- import { catchUpQuery, REVIEW_QUERY, reviewAnswer } from "./catch-up-answers.js";
13
+ import { catchUpQuery, OFFLINE_REVIEW, REVIEW_QUERY, reviewAnswer } from "./catch-up-answers.js";
14
14
  import { messengerContext } from "./context.js";
15
15
  import { heardItems, hearForCommand, MODEL_OPTION, spokenItems, TRANSCRIBE_OPTION } from "./hearing-command.js";
16
16
  import { PARTIAL_NOTE } from "./inbox.js";
@@ -25,10 +25,12 @@ export const unansweredHours = (value, flag = "--unanswered") => value === true
25
25
  * tasks in the local store from what it read (`../../services/task-rules.ts`); nothing reaches the messenger.
26
26
  */
27
27
  export const reviewCommand = (messenger) => annotate(new Command("review"), {
28
+ examples: [`${messenger.app.command} personal review "date>2h"`],
28
29
  effects: REMOTE_READ,
29
30
  output: { entity: "message", view: "summary" },
30
31
  query: REVIEW_QUERY,
31
32
  })
33
+ .summary("chats with new activity or unanswered messages")
32
34
  .description("every message, yours too, in chats that changed since a point โ€” for reviewing who owes what")
33
35
  .addHelpText("after", queryHelp(REVIEW_QUERY))
34
36
  .argument("[query...]", `where the last review ended and which chat: date>2h, chat:"Book club"; ${REVIEW_DAYS} days ago if no date`)
@@ -38,6 +40,7 @@ export const reviewCommand = (messenger) => annotate(new Command("review"), {
38
40
  .option(...TRANSCRIBE_OPTION)
39
41
  .option(...MODEL_OPTION)
40
42
  .option("--new", "what changed since the last `review --new`, a point per chat โ€” for scheduled runs")
43
+ .addHelpText("after", "With --offline it reads the local store: only what was already synced, and --new keeps its point.")
41
44
  .addHelpText("after", "To mark a chat read, use `chats mark-read <chat>`. The other side sees it.")
42
45
  .action(async function (query) {
43
46
  const options = this.opts();
@@ -98,7 +101,8 @@ export const reviewCommand = (messenger) => annotate(new Command("review"), {
98
101
  .sort((a, b) => Date.parse(a.timestamp) - Date.parse(b.timestamp));
99
102
  return read.length === 0 ? "" : renderMessages(spokenItems(read, hearing), viewOf(context, messenger));
100
103
  });
101
- saved?.save(found.checked ?? {});
104
+ if (!found.offline)
105
+ saved?.save(found.checked ?? {});
102
106
  });
103
107
  /** What the answer's steps do not say: a person's context for the review just shown. */
104
108
  const notes = (found, pretty, renderer) => {
@@ -110,6 +114,8 @@ const notes = (found, pretty, renderer) => {
110
114
  (found.tasks.review > 0 ? `, ${found.tasks.review} pair(s) to review โ€” tasks duplicates` : ""));
111
115
  if (found.partial && pretty)
112
116
  renderer.warn(PARTIAL_NOTE);
117
+ if (found.offline && pretty)
118
+ renderer.warn(OFFLINE_REVIEW);
113
119
  if (found.unanswered)
114
120
  renderer.note(`questions from ${found.since} to ${found.until} still open after ${found.unanswered.olderThanHours} hours; ` +
115
121
  "an answer after the review's end is not seen");
@@ -61,7 +61,7 @@ const withWhen = (command) => command
61
61
  */
62
62
  export const scheduleCommand = (messenger) => {
63
63
  const { app } = messenger;
64
- const command = annotate(new Command("schedule"), { mcp: false }).description(`run a ${app.command} command or an agent later or on a timer (systemd, launchd, Task Scheduler); an agent cannot send`);
64
+ const command = new Command("schedule").description(`run a ${app.command} command or an agent later or on a timer (systemd, launchd, Task Scheduler); an agent cannot send`);
65
65
  const prepare = (self) => {
66
66
  const context = messengerContext(self, messenger);
67
67
  const system = environmentOf(self).system ?? thisMachine();
@@ -192,7 +192,7 @@ export const scheduleCommand = (messenger) => {
192
192
  .join("\n");
193
193
  });
194
194
  });
195
- annotate(command.command("run"), { effects: REMOTE_WRITE })
195
+ annotate(command.command("run"), { effects: REMOTE_WRITE, mcp: false })
196
196
  .description("run a schedule now, as its timer would, and wait for it")
197
197
  .argument("<name>")
198
198
  .action(async function (name) {
@@ -216,7 +216,7 @@ export const scheduleCommand = (messenger) => {
216
216
  rmSync(file, { force: true });
217
217
  say({ name, removed: true }, [`Removed ${name}.`]);
218
218
  });
219
- annotate(command.command("exec"), { effects: REMOTE_WRITE })
219
+ annotate(command.command("exec"), { effects: REMOTE_WRITE, mcp: false })
220
220
  .description("what a timer runs, given the schedule's file; to run one by hand, use run")
221
221
  .argument("<file>")
222
222
  .action(async function (file) {