@wirecat/cli-core 0.397.0 → 0.399.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/README.md +12 -9
  3. package/dist/cli/analysis-choice.d.ts +1 -0
  4. package/dist/cli/analysis-choice.js +3 -0
  5. package/dist/cli/bot/admins.js +14 -4
  6. package/dist/cli/bot/command.js +38 -16
  7. package/dist/cli/bot/examples.d.ts +3 -0
  8. package/dist/cli/bot/examples.js +2 -0
  9. package/dist/cli/bot/mcp.js +4 -1
  10. package/dist/cli/bot/messages.js +17 -6
  11. package/dist/cli/bot/moderation.js +16 -4
  12. package/dist/cli/bot/people.js +16 -4
  13. package/dist/cli/bot/setup.js +27 -8
  14. package/dist/cli/bot/watch.js +7 -1
  15. package/dist/cli/commands-command.js +1 -1
  16. package/dist/cli/config-command.js +1 -1
  17. package/dist/cli/embedding-choice.d.ts +2 -1
  18. package/dist/cli/embedding-choice.js +4 -1
  19. package/dist/cli/guide.js +0 -1
  20. package/dist/cli/help/command-help.d.ts +9 -2
  21. package/dist/cli/help/command-help.js +22 -4
  22. package/dist/cli/help/first-screen.js +3 -3
  23. package/dist/cli/messenger/account-command.js +2 -1
  24. package/dist/cli/messenger/admin-commands.js +18 -9
  25. package/dist/cli/messenger/archive-commands.js +2 -2
  26. package/dist/cli/messenger/backfill-command.js +3 -3
  27. package/dist/cli/messenger/catch-up-answers.d.ts +4 -1
  28. package/dist/cli/messenger/catch-up-answers.js +11 -3
  29. package/dist/cli/messenger/chats-command.js +2 -1
  30. package/dist/cli/messenger/chats-stats-command.js +1 -1
  31. package/dist/cli/messenger/complete-command.js +5 -1
  32. package/dist/cli/messenger/contacts-command.js +4 -3
  33. package/dist/cli/messenger/context.d.ts +7 -0
  34. package/dist/cli/messenger/conversations-command.js +27 -13
  35. package/dist/cli/messenger/doctor-command.js +2 -2
  36. package/dist/cli/messenger/flood-command.js +1 -1
  37. package/dist/cli/messenger/hearing-command.d.ts +1 -1
  38. package/dist/cli/messenger/hearing-command.js +1 -1
  39. package/dist/cli/messenger/inbox.js +7 -4
  40. package/dist/cli/messenger/lacks.d.ts +7 -0
  41. package/dist/cli/messenger/lacks.js +18 -1
  42. package/dist/cli/messenger/media-archive-move-command.js +0 -1
  43. package/dist/cli/messenger/media-archive-timer-command.js +2 -2
  44. package/dist/cli/messenger/messages-command.js +2 -1
  45. package/dist/cli/messenger/messages-stats-command.js +1 -1
  46. package/dist/cli/messenger/metadata-command.js +1 -1
  47. package/dist/cli/messenger/models-command.js +5 -2
  48. package/dist/cli/messenger/private-people-command.js +5 -3
  49. package/dist/cli/messenger/replies-command.js +1 -1
  50. package/dist/cli/messenger/replies-edit-command.d.ts +1 -1
  51. package/dist/cli/messenger/replies-edit-command.js +13 -5
  52. package/dist/cli/messenger/retention-command.js +1 -1
  53. package/dist/cli/messenger/review.js +13 -6
  54. package/dist/cli/messenger/schedule-command.js +3 -3
  55. package/dist/cli/messenger/search-command.js +2 -2
  56. package/dist/cli/messenger/searches-command.js +1 -1
  57. package/dist/cli/messenger/serve-command.js +5 -1
  58. package/dist/cli/messenger/serve-relay.d.ts +2 -0
  59. package/dist/cli/messenger/server-command.js +12 -3
  60. package/dist/cli/messenger/stats-command.js +1 -1
  61. package/dist/cli/messenger/store-maintenance-command.js +3 -3
  62. package/dist/cli/messenger/tags-command.js +11 -5
  63. package/dist/cli/messenger/transcribe-command.js +1 -1
  64. package/dist/cli/messenger/watch-command.js +5 -1
  65. package/dist/cli/runs/command.js +2 -3
  66. package/dist/cli/skill-command.js +1 -1
  67. package/dist/domain/models.d.ts +2 -0
  68. package/dist/mcp/prompts.js +8 -2
  69. package/dist/services/deps.d.ts +1 -1
  70. package/dist/services/deps.js +1 -1
  71. package/dist/services/inbox.js +5 -5
  72. package/dist/services/search-kind.js +1 -1
  73. package/package.json +4 -4
  74. package/parity.json +134 -234
@@ -1,6 +1,6 @@
1
1
  import { CliError } from "@wirecat/cli-base";
2
2
  import { annotate } from "@wirecat/cli-base/commands";
3
- import { Command } from "commander";
3
+ import { Command, Option } from "commander";
4
4
  import { analysisConsents } from "../../analysis/consents.js";
5
5
  import { openAnalysis, providerIdentity } from "../../analysis/provider.js";
6
6
  import { runAnalysis } from "../../analysis/runner.js";
@@ -39,7 +39,10 @@ export const conversationsCommand = (messenger) => {
39
39
  "chat that changed since its build and every group never built; never asks the messenger")
40
40
  .option("--chat <chat>", messenger.chatArgument)
41
41
  .option("--analyze", "link batches using the configured analysis provider; requires --chat and remembers consent for this chat/provider")
42
- .option("--provider <provider>", "analysis: agent, openai or anthropic")
42
+ .addOption(new Option("--analysis-provider <provider>", "analysis: agent, openai or anthropic").conflicts("provider"))
43
+ .addOption(new Option("--provider <provider>", "compatibility alias for --analysis-provider")
44
+ .hideHelp()
45
+ .conflicts("analysisProvider"))
43
46
  .option("--model <model>", "analysis model; overrides analysis_model")
44
47
  .option("--base-url <url>", "analysis API endpoint; overrides analysis_base_url")
45
48
  .option("--size <n>", "analysis answer messages per batch, 10–200; default 50", positiveCount("--size"))
@@ -95,7 +98,8 @@ export const conversationsCommand = (messenger) => {
95
98
  renderAnswer(this, context, itemOf(snakeKeys(result)));
96
99
  return;
97
100
  }
98
- if (options.provider !== undefined ||
101
+ if (options.analysisProvider !== undefined ||
102
+ options.provider !== undefined ||
99
103
  options.model !== undefined ||
100
104
  options.baseUrl !== undefined ||
101
105
  options.size !== undefined ||
@@ -247,18 +251,19 @@ export const conversationsCommand = (messenger) => {
247
251
  .summary("remove an agent's saved conversation-link answers")
248
252
  .description("drop your agent's answers for a chat, or only one model's; messages are never touched")
249
253
  .requiredOption("--chat <chat>", messenger.chatArgument)
250
- .option("--model <model>", "only the answers this model gave")
254
+ .addOption(new Option("--answer-model <model>", "only the stored answers this model gave").conflicts("model"))
255
+ .addOption(new Option("--model <model>", "compatibility alias for --answer-model").hideHelp().conflicts("answerModel"))
251
256
  .action(async function () {
252
- const { chat, model } = this.opts();
257
+ const { chat, model, answerModel } = this.opts();
253
258
  const context = messengerContext(this, messenger);
254
259
  refuseLocalWrite(context, messenger.app.command, LINKS_KEY);
255
- const cleared = await context.withServices((services) => services.conversations.clearAnswers(chat, model));
260
+ const cleared = await context.withServices((services) => services.conversations.clearAnswers(chat, answerModel ?? model));
256
261
  renderAnswer(this, context, itemOf(cleared), () => `${cleared.cleared} answers dropped\n`);
257
262
  });
258
263
  const embed = withModelOptions(reachesOtherService(annotate(new Command("embed"), { effects: REMOTE_WRITE }))
259
264
  .summary("prepare conversation vectors for meaning search")
260
265
  .description("compute a vector for each chunk of a chat's conversations for search by meaning — on this machine, or " +
261
- "with --provider through a service and your key; resumes where it stopped; without --chat, every " +
266
+ "with --embedding-provider through a service and your key; resumes where it stopped; without --chat, every " +
262
267
  "built chat with chunks left, on this machine only")
263
268
  .option("--chat <chat>", messenger.chatArgument))
264
269
  .option("--threads <n>", "local: threads in all (default: min(8, cores))", positiveCount("--threads"))
@@ -324,7 +329,10 @@ export const conversationsCommand = (messenger) => {
324
329
  const chat = chatOf(options);
325
330
  const context = messengerContext(this, messenger);
326
331
  refuseLocalWrite(context, messenger.app.command, EMBED_KEY);
327
- const given = options.model !== undefined || options.provider !== undefined || options.baseUrl !== undefined;
332
+ const given = options.model !== undefined ||
333
+ options.embeddingProvider !== undefined ||
334
+ options.provider !== undefined ||
335
+ options.baseUrl !== undefined;
328
336
  const model = given ? choiceOf(options, messenger, context, { needKey: false }) : undefined;
329
337
  const cleared = await context.withServices((services) => services.embeddings.clear(chat, model));
330
338
  renderAnswer(this, context, itemOf(cleared), () => `${cleared.cleared} vectors dropped\n`);
@@ -365,16 +373,19 @@ export const conversationsCommand = (messenger) => {
365
373
  annotate(consents.command("revoke"), { effects: LOCAL_WRITE })
366
374
  .description("forget analysis permissions so the next analysis asks again")
367
375
  .option("--chat <chat>", "revoke only this chat's consents; defaults to every chat")
368
- .option("--provider <identity>", "exact provider identity from consents list; defaults to every provider")
376
+ .addOption(new Option("--consent-provider <identity>", "exact provider identity from consents list; defaults to every provider").conflicts("provider"))
377
+ .addOption(new Option("--provider <identity>", "compatibility alias for --consent-provider")
378
+ .hideHelp()
379
+ .conflicts("consentProvider"))
369
380
  .action(async function () {
370
- const { chat, provider } = this.opts();
381
+ const { chat, provider, consentProvider } = this.opts();
371
382
  const context = messengerContext(this, messenger);
372
383
  refuseLocalWrite(context, messenger.app.command, LINKS_KEY);
373
384
  await context.withStore(async (store, account) => {
374
385
  const chatId = chat === undefined
375
386
  ? undefined
376
387
  : (await servicesFor(storedDeps(messenger, store, account, context.guard)).conversations.batchStatus(chat, BATCH_SIZE.default)).chat;
377
- await analysisConsents(store, account).revoke(chatId, provider);
388
+ await analysisConsents(store, account).revoke(chatId, consentProvider ?? provider);
378
389
  });
379
390
  renderAnswer(this, context, itemOf({ revoked: true }));
380
391
  });
@@ -393,7 +404,10 @@ const chatOf = ({ chat }) => {
393
404
  };
394
405
  const withModelOptions = (command) => command
395
406
  .option("--model <model>", "local: a model id from `models text list` (default: e5-small); remote: the provider's model")
396
- .option("--provider <provider>", "embedding provider: local or openai; flags override profile settings")
407
+ .addOption(new Option("--embedding-provider <provider>", "embedding provider: local or openai; flags override profile settings").conflicts("provider"))
408
+ .addOption(new Option("--provider <provider>", "compatibility alias for --embedding-provider")
409
+ .hideHelp()
410
+ .conflicts("embeddingProvider"))
397
411
  .option("--base-url <url>", "a server with OpenAI's /v1/embeddings: Gemini, Jina, or Ollama and LM Studio on this machine")
398
412
  .option("--dims <n>", "remote: the vector size — needed with --base-url; shortens an OpenAI model's", positiveCount("--dims"));
399
413
  /** How much one run over many chats may do: the owner's machine, not a background job, pays for it. */
@@ -412,7 +426,7 @@ const bounds = ({ maxChats, maxChunks }) => ({
412
426
  /** Work nobody named chat by chat never sends chat text off the machine. */
413
427
  const localOnly = (model, what) => {
414
428
  if (typeof model !== "string") {
415
- throw new CliError("validation_error", `${what} uses the model on this machine only — drop --provider and --base-url, or embed one chat with --chat`);
429
+ throw new CliError("validation_error", `${what} uses the model on this machine only — drop --embedding-provider and --base-url, or embed one chat with --chat`);
416
430
  }
417
431
  };
418
432
  const renderRefreshed = (command, context, refreshed) => {
@@ -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")
@@ -14,7 +14,7 @@ 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
18
  .summary("clear a profile's stored rate-limit hold")
19
19
  .description(`forget them, lift the hold and the profile's pace, once ${name} no longer limits the account; changes nothing there`)
20
20
  .action(async function () {
@@ -2,7 +2,7 @@ import type { Message } from "../../domain/models.js";
2
2
  import { type Hearing } from "../../speech/hearing.js";
3
3
  import type { Messenger, MessengerContext, ReadConnection } from "./context.js";
4
4
  export declare const TRANSCRIBE_OPTION: readonly ["--transcribe", "turn voice messages not heard yet into text — by the messenger where it can, else by a model on this machine; can take minutes"];
5
- export declare const MODEL_OPTION: readonly ["--model <id>", "which downloaded speech model hears them, with --transcribe; `models audio list` shows them"];
5
+ export declare const MODEL_OPTION: readonly ["--model <model>", "which downloaded speech model hears them, with --transcribe; `models audio list` shows them"];
6
6
  /**
7
7
  * Kept transcripts on every read; new ones with `--transcribe`, using the read's connection when
8
8
  * supplied. It is released before local recognition. `undefined` when there is no voice message, and then no file is opened.
@@ -5,7 +5,7 @@ export const TRANSCRIBE_OPTION = [
5
5
  "turn voice messages not heard yet into text — by the messenger where it can, else by a model on this machine; can take minutes",
6
6
  ];
7
7
  export const MODEL_OPTION = [
8
- "--model <id>",
8
+ "--model <model>",
9
9
  "which downloaded speech model hears them, with --transcribe; `models audio list` shows them",
10
10
  ];
11
11
  /**
@@ -1,6 +1,6 @@
1
1
  import { CliError } from "@wirecat/cli-base";
2
2
  import { annotate } from "@wirecat/cli-base/commands";
3
- import { Command } from "commander";
3
+ import { Command, Option } from "commander";
4
4
  import { renderMessages } from "../../render/messages.js";
5
5
  import { CHAT_KINDS } from "../../services/chats.js";
6
6
  import { kindsOf } from "../../services/inbox.js";
@@ -29,6 +29,7 @@ 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,
@@ -40,7 +41,7 @@ export const inboxCommand = (messenger) => annotate(new Command("inbox"), {
40
41
  .option("--new", "what arrived since the last check, each message once — for scheduled runs")
41
42
  .option("--limit <n>", "at most this many per chat, the newest", positiveCount("--limit"))
42
43
  .option("--all", "muted and archived chats too — left out unless they mention you or reply to you")
43
- .option("--kind <kinds>", `only chats of these kinds, comma-separated: ${CHAT_KINDS.join(", ")}`)
44
+ .addOption(new Option("--kind <kinds>", `compatibility spelling for kind:; ${CHAT_KINDS.join(", ")}`).hideHelp())
44
45
  .option(...TRANSCRIBE_OPTION)
45
46
  .option(...MODEL_OPTION)
46
47
  .addHelpText("after", "To mark a chat read, use `chats mark-read <chat>`. The other side sees it.")
@@ -50,8 +51,10 @@ export const inboxCommand = (messenger) => annotate(new Command("inbox"), {
50
51
  const context = messengerContext(this, messenger);
51
52
  const { app } = messenger;
52
53
  const { settings, renderer, format, env } = context;
53
- const kinds = kind === undefined ? undefined : kindsOf(kind);
54
- const { since, meta } = catchUpQuery(query, INBOX_QUERY, "inbox");
54
+ const { since, meta, kinds: queriedKinds } = catchUpQuery(query, INBOX_QUERY, "inbox");
55
+ if (kind !== undefined && queriedKinds !== undefined)
56
+ throw new CliError("validation_error", "give kind: or its legacy --kind spelling, not both");
57
+ const kinds = queriedKinds ?? (kind === undefined ? undefined : kindsOf(kind));
55
58
  if (fresh && since !== undefined)
56
59
  throw new CliError("validation_error", "--new keeps its own point — not with a date in the query");
57
60
  const saved = fresh && since === undefined
@@ -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
+ };
@@ -9,7 +9,6 @@ import { messengerContext, refuseLocalWrite } from "./context.js";
9
9
  export const addMediaArchiveMove = (attachments, messenger) => {
10
10
  annotate(attachments.command("move"), {
11
11
  effects: LOCAL_WRITE,
12
- mcp: false,
13
12
  examples: [
14
13
  `${messenger.app.command} attachments move ./media-archive ./older-images --before-time 2026-10-01T00:00:00Z --dry-run`,
15
14
  ],
@@ -11,11 +11,11 @@ import { grantsAuthority, LOCAL_READ, LOCAL_WRITE } from "../effects.js";
11
11
  import { itemOf, renderAnswer, snakeKeys } from "../envelope.js";
12
12
  import { messengerContext } from "./context.js";
13
13
  export const addMediaArchiveTimers = (attachments, messenger) => {
14
- const archive = annotate(attachments.command("jobs"), { mcp: false }).description("daily media archive timers for this profile");
14
+ const archive = attachments.command("jobs").description("daily media archive timers for this profile");
15
15
  for (const action of ["install", "start", "stop", "status", "uninstall"]) {
16
16
  const command = annotate(archive.command(action), {
17
17
  effects: action === "status" ? LOCAL_READ : LOCAL_WRITE,
18
- mcp: false,
18
+ ...(action === "status" ? {} : { mcp: false }),
19
19
  examples: [`${messenger.app.command} attachments jobs ${action} ./media-archive`],
20
20
  })
21
21
  .description({
@@ -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";
@@ -12,7 +12,7 @@ export const messagesStatsCommand = (messenger) => syncOptions(annotate(new Comm
12
12
  .summary("count stored messages by chat, sender, day or hour")
13
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" : ""}`)
14
14
  .argument("[query...]", "a strict Lucene query, as for search messages; none counts every stored message; with --saved, more words AND-ed to it")
15
- .option("--by <chat|sender|day|hour>", "what to count by (default: chat)", groupingOf)
15
+ .option("--by <group>", "count by chat, sender, day or hour (default: chat)", groupingOf)
16
16
  .option("--chat <chat>", `only this chat — the same as chat: in the query; ${messenger.chatArgument}`)
17
17
  .option("--source <accounts>", "every account of this messenger held in the store; personal, bots or all — the same as in: in the query")
18
18
  .option("--limit <n>", "how many rows", positiveCount("--limit"))
@@ -23,7 +23,7 @@ export const metadataCommand = (messenger) => {
23
23
  .description("fetch group and channel descriptions again and cache them")
24
24
  .option("--chat <chat>", "stored group/channel; repeat for several", (value, previous) => [...previous, value], [])
25
25
  .option("--only-missing", "only chats with no metadata yet; without --chat, every stored group/channel")
26
- .option("--limit <number>", "process at most 1–500 chats", positiveCount("--limit"), 50)
26
+ .option("--limit <n>", "process at most 1–500 chats", positiveCount("--limit"), 50)
27
27
  .action(async function () {
28
28
  const context = messengerContext(this, messenger);
29
29
  refuseLocalWrite(context, messenger.app.command, "metadata.refresh");
@@ -58,7 +58,7 @@ 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
64
  .summary("speech models and their download status")
@@ -76,6 +76,7 @@ export const modelsCommand = (messenger) => {
76
76
  });
77
77
  });
78
78
  reachesOtherService(annotate(audio.command("download"), {
79
+ mcp: false,
79
80
  effects: FETCH,
80
81
  output: { entity: "model" },
81
82
  examples: [`${tool} models audio download parakeet-v3`],
@@ -123,6 +124,7 @@ export const modelsCommand = (messenger) => {
123
124
  });
124
125
  });
125
126
  reachesOtherService(annotate(text.command("download"), {
127
+ mcp: false,
126
128
  effects: FETCH,
127
129
  output: { entity: "model" },
128
130
  examples: [`${tool} models text download ${DEFAULT_TEXT_MODEL}`],
@@ -171,7 +173,7 @@ export const modelsCommand = (messenger) => {
171
173
  /** `images models`: registered with picture search, the only thing these models serve. */
172
174
  export const imageModelsCommand = (messenger) => {
173
175
  const tool = messenger.app.command;
174
- 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");
175
177
  withQuery(annotate(image.command("list"), { effects: LOCAL_READ, examples: [`${tool} images models list`] }), MODELS_QUERY)
176
178
  .description("the picture models, which are downloaded, and which one is the default")
177
179
  .action(async function (words) {
@@ -187,6 +189,7 @@ export const imageModelsCommand = (messenger) => {
187
189
  });
188
190
  });
189
191
  reachesOtherService(annotate(image.command("download"), {
192
+ mcp: false,
190
193
  effects: FETCH,
191
194
  output: { entity: "model" },
192
195
  examples: [`${tool} images models download ${DEFAULT_IMAGE_MODEL}`],
@@ -54,10 +54,11 @@ export const privatePeopleCommands = (messenger) => {
54
54
  output: { entity: "contact_note" },
55
55
  }), { name: "text", description: "the note's text" })
56
56
  .description(name === "add"
57
- ? "add a private note on a person from stdin or --file; never sent"
57
+ ? "add a private note on a person from stdin or --text-file; never sent"
58
58
  : "replace a note's text; --revision must match the one you read")
59
59
  .argument("<person>", "the person's id, reference or known name")
60
- .addOption(ownerOnlyOption("--file <path>", "read note text from a file; omitted or - reads stdin"));
60
+ .addOption(ownerOnlyOption("--text-file <path>", "read note text from a file; omitted or - reads stdin").conflicts("file"))
61
+ .addOption(ownerOnlyOption("--file <path>", "compatibility alias for --text-file").hideHelp().conflicts("textFile"));
61
62
  if (name === "edit")
62
63
  command
63
64
  .argument("<id>", "the note id from contacts notes list")
@@ -65,7 +66,8 @@ export const privatePeopleCommands = (messenger) => {
65
66
  command.action(async function (person, id) {
66
67
  const context = messengerContext(this, messenger);
67
68
  refuseLocalWrite(context, messenger.app.command, `contacts.notes.${name}`);
68
- const { file, revision } = this.opts();
69
+ const { textFile, file: legacyFile, revision, } = this.opts();
70
+ const file = textFile ?? legacyFile;
69
71
  const text = file === undefined || file === "-" ? await readAll(context.stdin) : await readFile(file, "utf8");
70
72
  const version = Number(revision);
71
73
  if (name === "edit" && (!Number.isSafeInteger(version) || version < 1))
@@ -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`);
@@ -1,5 +1,5 @@
1
1
  import { Command } from "commander";
2
2
  import { type Messenger } from "./context.js";
3
- export declare const REPLY_EDIT_OPTIONS: readonly [readonly ["--do <actions>", "actions: reply, task, or both, comma-separated"], readonly ["--kinds <kinds>", "chat kinds: dialog, group; comma-separated, empty for any"], readonly ["--allow-chats <ids>", "only these chat ids, comma-separated; empty for any"], readonly ["--deny-chats <ids>", "leave these chat ids out, comma-separated; empty clears"], readonly ["--words <words>", "match any of these whole words, comma-separated; empty clears"], readonly ["--question", "match only questions"], readonly ["--no-question", "do not require a question"], readonly ["--mentions-me", "require a mention of you or a reply to you"], readonly ["--no-mentions-me", "do not require a mention of you or a reply to you"], readonly ["--allow-people <ids>", "only these sender ids, comma-separated; empty for any"], readonly ["--deny-people <ids>", "leave these sender ids out, comma-separated; empty clears"], readonly ["--contacts-only", "match only contacts"], readonly ["--no-contacts-only", "do not require a contact"], readonly ["--template <text>", "the reply template"], readonly ["--model <mode>", "legacy template mode: fill-only or may-reword; use ai blocks instead"], readonly ["--as-reply", "send as a reply to the matched message"], readonly ["--no-as-reply", "send without linking to the matched message"], readonly ["--per-chat <limit>", "at most this many per chat, such as 1/12h"], readonly ["--per-person <limit>", "at most this many per person, such as 1/1d"], readonly ["--outside <hours>", "answer outside this 24-hour window, such as 09:00-19:00"], readonly ["--days <days>", "days of the working window, such as mon-fri or sat,sun"], readonly ["--timezone <zone>", "the IANA timezone for the working window"], readonly ["--no-hours", "clear the working window"]];
3
+ export declare const REPLY_EDIT_OPTIONS: readonly [readonly ["--do <actions>", "actions: reply, task, or both, comma-separated"], readonly ["--kinds <kinds>", "chat kinds: dialog, group; comma-separated, empty for any"], readonly ["--allow-chats <ids>", "only these chat ids, comma-separated; empty for any"], readonly ["--deny-chats <ids>", "leave these chat ids out, comma-separated; empty clears"], readonly ["--words <words>", "match any of these whole words, comma-separated; empty clears"], readonly ["--question", "match only questions"], readonly ["--no-question", "do not require a question"], readonly ["--mentions-me", "require a mention of you or a reply to you"], readonly ["--no-mentions-me", "do not require a mention of you or a reply to you"], readonly ["--allow-people <ids>", "only these sender ids, comma-separated; empty for any"], readonly ["--deny-people <ids>", "leave these sender ids out, comma-separated; empty clears"], readonly ["--contacts-only", "match only contacts"], readonly ["--no-contacts-only", "do not require a contact"], readonly ["--template <text>", "the reply template"], readonly ["--template-mode <mode>", "legacy template mode: fill-only or may-reword; use ai blocks instead"], readonly ["--as-reply", "send as a reply to the matched message"], readonly ["--no-as-reply", "send without linking to the matched message"], readonly ["--per-chat <limit>", "at most this many per chat, such as 1/12h"], readonly ["--per-person <limit>", "at most this many per person, such as 1/1d"], readonly ["--outside <hours>", "answer outside this 24-hour window, such as 09:00-19:00"], readonly ["--days <days>", "days of the working window, such as mon-fri or sat,sun"], readonly ["--timezone <zone>", "the IANA timezone for the working window"], readonly ["--no-hours", "clear the working window"]];
4
4
  export declare const REPLY_AUDIENCE_OPTIONS: readonly [readonly ["--reply <mode>", "answer all or only listed senders and chats: all, listed"], readonly ["--allow-people <ids>", "replace allowed sender ids, comma-separated; empty clears"], readonly ["--allow-chats <ids>", "replace allowed chat ids, comma-separated; empty clears"], readonly ["--deny-people <ids>", "replace denied sender ids, comma-separated; empty clears; deny wins"], readonly ["--deny-chats <ids>", "replace denied chat ids, comma-separated; empty clears; deny wins"]];
5
5
  export declare const addReplyEditors: (replies: Command, messenger: Messenger) => void;
@@ -1,5 +1,5 @@
1
1
  import { annotate } from "@wirecat/cli-base/commands";
2
- import { Command } from "commander";
2
+ import { Command, Option } from "commander";
3
3
  import { addRule, editAudience, editRule, switchRule } from "../../replies/edit.js";
4
4
  import { audienceWarnings, repliesPathFor } from "../../replies/rules.js";
5
5
  import { grantsAuthority, LOCAL_WRITE } from "../effects.js";
@@ -20,7 +20,7 @@ export const REPLY_EDIT_OPTIONS = [
20
20
  ["--contacts-only", "match only contacts"],
21
21
  ["--no-contacts-only", "do not require a contact"],
22
22
  ["--template <text>", "the reply template"],
23
- ["--model <mode>", "legacy template mode: fill-only or may-reword; use ai blocks instead"],
23
+ ["--template-mode <mode>", "legacy template mode: fill-only or may-reword; use ai blocks instead"],
24
24
  ["--as-reply", "send as a reply to the matched message"],
25
25
  ["--no-as-reply", "send without linking to the matched message"],
26
26
  ["--per-chat <limit>", "at most this many per chat, such as 1/12h"],
@@ -59,8 +59,13 @@ export const addReplyEditors = (replies, messenger) => {
59
59
  const edit = grantsAuthority(annotate(new Command("edit"), { effects: LOCAL_WRITE }))
60
60
  .description("change only the named fields of a reply rule; lists replace the whole list")
61
61
  .argument("<id>", "the rule's id");
62
- for (const [flag, help] of REPLY_EDIT_OPTIONS)
63
- edit.option(flag, help);
62
+ for (const [flag, help] of REPLY_EDIT_OPTIONS) {
63
+ const option = new Option(flag, help);
64
+ if (flag === "--template-mode <mode>")
65
+ option.conflicts("model");
66
+ edit.addOption(option);
67
+ }
68
+ edit.addOption(new Option("--model <mode>", "compatibility alias for --template-mode").hideHelp().conflicts("templateMode"));
64
69
  // Commander gives negated-only flags a true default; omission must preserve the saved field.
65
70
  edit.action(function (id) {
66
71
  const options = this.opts();
@@ -68,7 +73,10 @@ export const addReplyEditors = (replies, messenger) => {
68
73
  delete options.hours;
69
74
  const context = messengerContext(this, messenger);
70
75
  const { settings, env } = context;
71
- renderItem(this, context, editRule(repliesPathFor(messenger.app, settings.profile, env), messenger.provider, id, options));
76
+ renderItem(this, context, editRule(repliesPathFor(messenger.app, settings.profile, env), messenger.provider, id, {
77
+ ...options,
78
+ model: options.templateMode ?? options.model,
79
+ }));
72
80
  });
73
81
  replies.addCommand(edit);
74
82
  const audience = grantsAuthority(annotate(new Command("audience"), { effects: LOCAL_WRITE })).description("show the reply audience, who the rules may answer, or replace its named fields; a new file answers everyone a rule matches");
@@ -13,7 +13,7 @@ export const retentionCommand = (messenger) => annotate(new Command("retention")
13
13
  .option("--until-time <time>", "joining period ends at this time; now by default")
14
14
  .option("--checkpoints <durations>", "up to 10 increasing joining ages, comma separated; 1d,7d,30d by default")
15
15
  .option("--within <duration>", "activity and early departure window after joining; 7d by default")
16
- .addOption(new Option("--by <day|week>", "group joining dates by calendar day or Monday week").choices(["day", "week"]))
16
+ .addOption(new Option("--by <group>", "group joining dates by calendar day or Monday week").choices(["day", "week"]))
17
17
  .option("--timezone <zone>", "IANA timezone for joining cohorts")
18
18
  .option("--limit <n>", "cohorts and member evidence, 1–100", positiveCount("--limit"))
19
19
  .action(async function (chat) {
@@ -1,6 +1,6 @@
1
1
  import { CliError } from "@wirecat/cli-base";
2
2
  import { annotate } from "@wirecat/cli-base/commands";
3
- import { Command } from "commander";
3
+ import { Command, Option } from "commander";
4
4
  import { parseDuration } from "../../domain/duration.js";
5
5
  import { renderMessages } from "../../render/messages.js";
6
6
  import { CHAT_KINDS } from "../../services/chats.js";
@@ -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,6 +25,7 @@ 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,
@@ -33,19 +34,22 @@ export const reviewCommand = (messenger) => annotate(new Command("review"), {
33
34
  .description("every message, yours too, in chats that changed since a point — for reviewing who owes what")
34
35
  .addHelpText("after", queryHelp(REVIEW_QUERY))
35
36
  .argument("[query...]", `where the last review ended and which chat: date>2h, chat:"Book club"; ${REVIEW_DAYS} days ago if no date`)
36
- .option("--kind <kinds>", `only chats of these kinds, comma-separated: ${CHAT_KINDS.join(", ")}`)
37
+ .addOption(new Option("--kind <kinds>", `compatibility spelling for kind:; ${CHAT_KINDS.join(", ")}`).hideHelp())
37
38
  .option("--unanswered [duration]", `only questions to you or a group's admins that nobody answered, asked at least this long ago — 4h, 1d; ${UNANSWERED_HOURS}h if not given`)
38
39
  .option("--all", "muted and archived chats too — left out unless they mention you or reply to you")
39
40
  .option(...TRANSCRIBE_OPTION)
40
41
  .option(...MODEL_OPTION)
41
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.")
42
44
  .addHelpText("after", "To mark a chat read, use `chats mark-read <chat>`. The other side sees it.")
43
45
  .action(async function (query) {
44
46
  const options = this.opts();
45
47
  const hearWith = modelWith(options.transcribe, options.model);
46
48
  const context = messengerContext(this, messenger);
47
49
  const { settings, renderer, format, env } = context;
48
- const { since: from, chat, meta } = catchUpQuery(query, REVIEW_QUERY, "review");
50
+ const { since: from, chat, meta, kinds: queriedKinds } = catchUpQuery(query, REVIEW_QUERY, "review");
51
+ if (options.kind !== undefined && queriedKinds !== undefined)
52
+ throw new CliError("validation_error", "give kind: or its legacy --kind spelling, not both");
49
53
  if (options.new && (from !== undefined || options.unanswered !== undefined)) {
50
54
  // An open question stays open: moving past it would drop it from the next review.
51
55
  throw new CliError("validation_error", "--new keeps its own point — not with a date or --unanswered");
@@ -60,7 +64,7 @@ export const reviewCommand = (messenger) => annotate(new Command("review"), {
60
64
  : undefined;
61
65
  const since = from ?? saved?.first ?? reviewStart();
62
66
  const hours = options.unanswered === undefined ? undefined : unansweredHours(options.unanswered);
63
- const kinds = options.kind === undefined ? undefined : kindsOf(options.kind);
67
+ const kinds = queriedKinds ?? (options.kind === undefined ? undefined : kindsOf(options.kind));
64
68
  let hearing;
65
69
  const transcribe = options.transcribe === true;
66
70
  const { found, reader } = await context.withServices(async (services, connect) => {
@@ -99,7 +103,8 @@ export const reviewCommand = (messenger) => annotate(new Command("review"), {
99
103
  .sort((a, b) => Date.parse(a.timestamp) - Date.parse(b.timestamp));
100
104
  return read.length === 0 ? "" : renderMessages(spokenItems(read, hearing), viewOf(context, messenger));
101
105
  });
102
- saved?.save(found.checked ?? {});
106
+ if (!found.offline)
107
+ saved?.save(found.checked ?? {});
103
108
  });
104
109
  /** What the answer's steps do not say: a person's context for the review just shown. */
105
110
  const notes = (found, pretty, renderer) => {
@@ -111,6 +116,8 @@ const notes = (found, pretty, renderer) => {
111
116
  (found.tasks.review > 0 ? `, ${found.tasks.review} pair(s) to review — tasks duplicates` : ""));
112
117
  if (found.partial && pretty)
113
118
  renderer.warn(PARTIAL_NOTE);
119
+ if (found.offline && pretty)
120
+ renderer.warn(OFFLINE_REVIEW);
114
121
  if (found.unanswered)
115
122
  renderer.note(`questions from ${found.since} to ${found.until} still open after ${found.unanswered.olderThanHours} hours; ` +
116
123
  "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) {
@@ -36,8 +36,8 @@ const allCommand = (messenger) => {
36
36
  const command = annotate(new Command("all"), { effects: REMOTE_READ, partial: true })
37
37
  .summary("search stored messages, notes and selected meeting transcripts")
38
38
  .description((messenger.serverSearch
39
- ? "search messenger messages and notes in the local store, and the messenger's server for messages " +
40
- "(--backend; notes are local only)"
39
+ ? "search messenger messages and notes in the local store, and by default the messenger's server for " +
40
+ "messages too (notes are local only; --backend archive or --offline keeps it to the local store)"
41
41
  : "search messenger messages and notes in the local store") +
42
42
  ", and a meeting account's transcripts when --source names one — best match first; not mail (the separate memo CLI searches it: memo search mail), " +
43
43
  "tasks, people, memories or projects. Use it when you do not know where something was written; search messages " +
@@ -45,7 +45,7 @@ export const searchesCommand = (messenger) => {
45
45
  .option("--exact", "bare words and quotes match their exact form only, as exact:word does; text: still matches every form")
46
46
  .option("--context <n>", "messages before and after each hit", wholeCount)
47
47
  .option("--timezone <zone>", "the IANA timezone for calendar date boundaries")
48
- .option("--by <chat|sender|day|hour>", "what stats messages show --saved counts by", groupingOf)
48
+ .option("--by <group>", "count by chat, sender, day or hour for stats messages show --saved", groupingOf)
49
49
  .option("--selection <json>", "save the resolved parent ranking query and options from a drilldown")
50
50
  .option("--replace", "overwrite a saved search of the same name")
51
51
  .action(async function (name, words) {
@@ -30,7 +30,11 @@ export { lockPath, readLock, servingProfiles } from "../../background/lock.js";
30
30
  * taken over. Started by a person, a service unit or `server start` — never by itself (NEED-9).
31
31
  */
32
32
  export const serveCommand = (messenger) => {
33
- const command = annotate(new Command("serve"), { effects: REMOTE_WRITE, lifetime: "persistent" }).description("keep the local store current until stopped — what a systemd or launchd unit runs");
33
+ const command = annotate(new Command("serve"), {
34
+ effects: REMOTE_WRITE,
35
+ lifetime: "persistent",
36
+ examples: [`${messenger.app.command} personal serve`],
37
+ }).description("keep the local store current until stopped — what a systemd or launchd unit runs");
34
38
  command.action(async function () {
35
39
  const relay = messenger.connection === "exclusive" && process.platform !== "win32" ? serveRelay(this, messenger) : undefined;
36
40
  const context = recordDiagnostics(messengerContext(this, relay?.messenger ?? messenger), messenger.app);