@wirecat/cli-core 0.398.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 (40) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/dist/cli/analysis-choice.d.ts +1 -0
  3. package/dist/cli/analysis-choice.js +3 -0
  4. package/dist/cli/bot/admins.js +14 -4
  5. package/dist/cli/bot/command.js +38 -16
  6. package/dist/cli/bot/examples.d.ts +3 -0
  7. package/dist/cli/bot/examples.js +2 -0
  8. package/dist/cli/bot/mcp.js +4 -1
  9. package/dist/cli/bot/messages.js +17 -6
  10. package/dist/cli/bot/moderation.js +16 -4
  11. package/dist/cli/bot/people.js +16 -4
  12. package/dist/cli/bot/setup.js +27 -8
  13. package/dist/cli/bot/watch.js +7 -1
  14. package/dist/cli/embedding-choice.d.ts +2 -1
  15. package/dist/cli/embedding-choice.js +4 -1
  16. package/dist/cli/messenger/admin-commands.js +18 -9
  17. package/dist/cli/messenger/catch-up-answers.d.ts +2 -1
  18. package/dist/cli/messenger/catch-up-answers.js +6 -2
  19. package/dist/cli/messenger/chats-stats-command.js +1 -1
  20. package/dist/cli/messenger/contacts-command.js +2 -2
  21. package/dist/cli/messenger/context.d.ts +2 -0
  22. package/dist/cli/messenger/conversations-command.js +27 -13
  23. package/dist/cli/messenger/hearing-command.d.ts +1 -1
  24. package/dist/cli/messenger/hearing-command.js +1 -1
  25. package/dist/cli/messenger/inbox.js +6 -4
  26. package/dist/cli/messenger/messages-stats-command.js +1 -1
  27. package/dist/cli/messenger/metadata-command.js +1 -1
  28. package/dist/cli/messenger/private-people-command.js +5 -3
  29. package/dist/cli/messenger/replies-edit-command.d.ts +1 -1
  30. package/dist/cli/messenger/replies-edit-command.js +13 -5
  31. package/dist/cli/messenger/retention-command.js +1 -1
  32. package/dist/cli/messenger/review.js +6 -4
  33. package/dist/cli/messenger/searches-command.js +1 -1
  34. package/dist/cli/messenger/serve-relay.d.ts +1 -0
  35. package/dist/cli/messenger/stats-command.js +1 -1
  36. package/dist/cli/messenger/tags-command.js +11 -5
  37. package/dist/cli/messenger/transcribe-command.js +1 -1
  38. package/dist/mcp/prompts.js +8 -2
  39. package/package.json +3 -3
  40. package/parity.json +68 -42
@@ -6,7 +6,10 @@ import { isLocal, remoteModel } from "../embeddings/remote.js";
6
6
  import { readOnlyLockOf, readOnlyRefusal } from "../ports/app.js";
7
7
  import { typedCommand } from "../runs/scope.js";
8
8
  /** A local model id, or a remote model with its key from `models text key set`. */
9
- export const embeddingChoice = ({ model, provider, baseUrl, dims, concurrency }, app, settings, env, { needKey = true, keyring } = {}) => {
9
+ export const embeddingChoice = ({ model, provider, embeddingProvider, baseUrl, dims, concurrency }, app, settings, env, { needKey = true, keyring } = {}) => {
10
+ if (embeddingProvider !== undefined && provider !== undefined)
11
+ throw new CliError("validation_error", "give --embedding-provider or its legacy --provider spelling, not both");
12
+ provider = embeddingProvider ?? provider;
10
13
  const flagsChooseProvider = (provider !== undefined && provider !== (settings.embeddingProvider ?? "local")) ||
11
14
  (baseUrl !== undefined && baseUrl.replace(/\/+$/, "") !== settings.embeddingBaseUrl?.replace(/\/+$/, ""));
12
15
  provider ??= flagsChooseProvider ? "openai" : (settings.embeddingProvider ?? "local");
@@ -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 { ADMIN_RIGHTS, GROUP_SETTINGS, } from "../../domain/models.js";
6
6
  import { readUpload, typesText } from "../../sends/upload.js";
@@ -22,19 +22,19 @@ const SETTING_FLAGS = {
22
22
  onlyAdminsSend: ["only-admins-send <on|off>", "only admins may send messages"],
23
23
  onlyAdminsEditInfo: ["only-admins-edit-info <on|off>", "only admins may change the name, photo and description"],
24
24
  disappearingSeconds: [
25
- "disappear-time <duration|off>",
25
+ "disappear-after <duration|off>",
26
26
  "new messages disappear this long after sending: 24h, 7d, 90d; off keeps them",
27
27
  ],
28
28
  };
29
29
  const flagOf = (key) => `--${SETTING_FLAGS[key][0].split(" ")[0]}`;
30
- /** Where commander puts the flag's value: `--disappear-time` is `disappearTime`, not the setting's own name. */
30
+ /** Where commander puts the flag's value: `--disappear-after` is `disappearAfter`, not the setting's own name. */
31
31
  const optionKey = (key) => flagOf(key)
32
32
  .slice(2)
33
33
  .replace(/-(\w)/g, (_, letter) => letter.toUpperCase());
34
34
  const settingsOf = (keys, options) => {
35
35
  const changes = {};
36
36
  for (const key of keys) {
37
- const value = options[optionKey(key)];
37
+ const value = options[optionKey(key)] ?? (key === "disappearingSeconds" ? options.disappearTime : undefined);
38
38
  if (value === undefined)
39
39
  continue;
40
40
  if (key === "disappearingSeconds") {
@@ -67,8 +67,16 @@ const updateCommand = (messenger) => {
67
67
  update.option("--photo <file>", `a new photo for it — ${photoHelp(messenger)}`);
68
68
  if (messenger.groupPhotoRemoval === true)
69
69
  update.option("--remove-photo", "take its photo away; not with --photo");
70
- for (const key of keys)
71
- update.option(`--${SETTING_FLAGS[key][0]}`, SETTING_FLAGS[key][1]);
70
+ for (const key of keys) {
71
+ const option = new Option(`--${SETTING_FLAGS[key][0]}`, SETTING_FLAGS[key][1]);
72
+ if (key === "disappearingSeconds")
73
+ option.conflicts("disappearTime");
74
+ update.addOption(option);
75
+ }
76
+ if (keys.includes("disappearingSeconds"))
77
+ update.addOption(new Option("--disappear-time <duration|off>", "compatibility alias for --disappear-after")
78
+ .hideHelp()
79
+ .conflicts("disappearAfter"));
72
80
  return update.action(async function (chat) {
73
81
  const context = messengerContext(this, messenger);
74
82
  const { title, description, photo, removePhoto, ...rest } = this.opts();
@@ -210,6 +218,7 @@ const requestsCommand = (messenger) => {
210
218
  renderAnswer(this, context, body, lines(({ person_id, name, username, requested_at }) => [person_id, name ?? "", username ? `@${username}` : "", requested_at ?? ""].join(" ")));
211
219
  }));
212
220
  const everyone = messenger.joinRequestsAll !== false;
221
+ const byLink = everyone && messenger.joinRequestsByLink !== false;
213
222
  for (const [verb, accept, help] of [
214
223
  ["accept", true, "let them in; the group sees them join"],
215
224
  ["decline", false, "turn the request away"],
@@ -219,9 +228,9 @@ const requestsCommand = (messenger) => {
219
228
  .argument("<chat>", messenger.chatArgument)
220
229
  .argument(everyone ? "[person]" : "<person>", "who asked: an id from `chats requests list`");
221
230
  if (everyone)
222
- answer
223
- .option("--all", "every pending request, counted against the hourly limit first")
224
- .option("--link <link>", "with --all: only the requests made by this invite link");
231
+ answer.option("--all", "every pending request, counted against the hourly limit first");
232
+ if (byLink)
233
+ answer.option("--link <link>", "with --all: only the requests made by this invite link");
225
234
  requests.addCommand(answer.action(async function (chat, person) {
226
235
  const context = messengerContext(this, messenger);
227
236
  const { all, link } = this.opts();
@@ -1,5 +1,5 @@
1
1
  import type { Meta } from "@wirecat/cli-base/standards";
2
- import type { Inbox, Review } from "../../domain/models.js";
2
+ import type { ChatKind, Inbox, Review } from "../../domain/models.js";
3
3
  import { type Hearing } from "../../speech/hearing.js";
4
4
  import type { AccountKey } from "../../store/store.js";
5
5
  import { type QueryWords } from "../list-query.js";
@@ -11,6 +11,7 @@ export declare const REVIEW_QUERY: import("@wirecat/cli-base/query").QueryRegist
11
11
  export declare const catchUpQuery: (words: QueryWords, registry: typeof INBOX_QUERY, command: string) => {
12
12
  since?: number;
13
13
  chat?: string;
14
+ kinds?: ChatKind[];
14
15
  meta?: Partial<Meta>;
15
16
  };
16
17
  export declare const inboxAnswer: (inbox: Inbox, hearing: Hearing | undefined, reader: AccountKey, transcribe: boolean, meta?: Partial<Meta>) => import("../envelope.js").ListBody<{
@@ -1,17 +1,19 @@
1
1
  import { defineQuery, queryError } from "@wirecat/cli-base/query";
2
+ import { kindsOf } from "../../services/inbox.js";
2
3
  import { heard, hearingMissing } from "../../speech/hearing.js";
3
4
  import { listOf } from "../envelope.js";
4
5
  import { listQuery, nativeField, nativeTerms, timeBounds, typedQuery } from "../list-query.js";
5
6
  import { nextStep } from "../steps.js";
6
7
  import { messageItem } from "./message-items.js";
7
8
  const date = nativeField({ name: "date", type: "time", operators: ["range"], example: "date>2h" });
9
+ const kind = nativeField({ name: "kind", type: "kw", operators: ["term", "phrase"], example: "kind:group,channel" });
8
10
  /** Where `inbox` starts reading: everything after a time. */
9
- export const INBOX_QUERY = defineQuery({ entity: "message", backend: "native", fields: [date] });
11
+ export const INBOX_QUERY = defineQuery({ entity: "message", backend: "native", fields: [date, kind] });
10
12
  /** Where `review` starts, and the one chat it reads. */
11
13
  export const REVIEW_QUERY = defineQuery({
12
14
  entity: "message",
13
15
  backend: "native",
14
- fields: [date, nativeField({ name: "chat", type: "kw", operators: ["term", "phrase"], example: "chat:7" })],
16
+ fields: [date, kind, nativeField({ name: "chat", type: "kw", operators: ["term", "phrase"], example: "chat:7" })],
15
17
  });
16
18
  /** The query's start and chat. A catch-up reads forward from one point, so an end is refused. */
17
19
  export const catchUpQuery = (words, registry, command) => {
@@ -23,9 +25,11 @@ export const catchUpQuery = (words, registry, command) => {
23
25
  command,
24
26
  });
25
27
  const chat = terms.get("chat")?.value;
28
+ const kinds = terms.get("kind")?.value;
26
29
  return {
27
30
  ...(since === undefined ? {} : { since }),
28
31
  ...(chat === undefined ? {} : { chat }),
32
+ ...(kinds === undefined ? {} : { kinds: kindsOf(kinds, "kind:") }),
29
33
  ...(meta === undefined ? {} : { meta }),
30
34
  };
31
35
  };
@@ -14,7 +14,7 @@ export const statsCommand = (messenger) => annotate(new Command("show"), { effec
14
14
  `joins and leaves — counted from the local store; ${messenger.history === "store" ? "joins and leaves too" : "joins and leaves are asked of the messenger"}`)
15
15
  .argument("<chat>", messenger.chatArgument)
16
16
  .option("--since-time <time>", `ISO 8601, or 2h / 1d ago; ${EVENTS_DAYS} days ago if not given`)
17
- .option("--by <day|week>", "also one row per calendar day or week (weeks start on Monday)", periodOf)
17
+ .option("--by <group>", "also one row per calendar day or week (weeks start on Monday)", periodOf)
18
18
  .option("--timezone <zone>", "the IANA timezone for calendar days")
19
19
  .action(async function (chat) {
20
20
  const context = messengerContext(this, messenger);
@@ -297,8 +297,8 @@ export const contactsCommand = (messenger) => {
297
297
  link: '"Alex Example" zoom:101',
298
298
  list: "--limit 10",
299
299
  lookup: "",
300
- "notes add": '"Alex Example" --file ./note.txt',
301
- "notes edit": '"Alex Example" 1 --revision 1 --file ./note.txt',
300
+ "notes add": '"Alex Example" --text-file ./note.txt',
301
+ "notes edit": '"Alex Example" 1 --revision 1 --text-file ./note.txt',
302
302
  "notes list": '"Alex Example"',
303
303
  "notes remove": '"Alex Example" 1',
304
304
  "notes show": '"Alex Example" 1',
@@ -158,6 +158,8 @@ export interface Messenger extends ServiceMessenger {
158
158
  inviteLinkUpdate?: boolean;
159
159
  /** Whether every pending join request is answered at once — `chats requests accept --all`; yes when unset. */
160
160
  joinRequestsAll?: boolean;
161
+ /** Whether `--all` can take only the requests made by one invite link — `--link`; yes when unset. */
162
+ joinRequestsByLink?: boolean;
161
163
  /** Whether a channel post can be commented on — `messages send --comment-to`; yes when unset. */
162
164
  channelComments?: boolean;
163
165
  /** Whether people added can be shown the messages from before they came — `members add --history`; yes when unset. */
@@ -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) => {
@@ -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";
@@ -41,7 +41,7 @@ export const inboxCommand = (messenger) => annotate(new Command("inbox"), {
41
41
  .option("--new", "what arrived since the last check, each message once — for scheduled runs")
42
42
  .option("--limit <n>", "at most this many per chat, the newest", positiveCount("--limit"))
43
43
  .option("--all", "muted and archived chats too — left out unless they mention you or reply to you")
44
- .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())
45
45
  .option(...TRANSCRIBE_OPTION)
46
46
  .option(...MODEL_OPTION)
47
47
  .addHelpText("after", "To mark a chat read, use `chats mark-read <chat>`. The other side sees it.")
@@ -51,8 +51,10 @@ export const inboxCommand = (messenger) => annotate(new Command("inbox"), {
51
51
  const context = messengerContext(this, messenger);
52
52
  const { app } = messenger;
53
53
  const { settings, renderer, format, env } = context;
54
- const kinds = kind === undefined ? undefined : kindsOf(kind);
55
- 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));
56
58
  if (fresh && since !== undefined)
57
59
  throw new CliError("validation_error", "--new keeps its own point — not with a date in the query");
58
60
  const saved = fresh && since === undefined
@@ -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");
@@ -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))
@@ -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";
@@ -34,7 +34,7 @@ export const reviewCommand = (messenger) => annotate(new Command("review"), {
34
34
  .description("every message, yours too, in chats that changed since a point — for reviewing who owes what")
35
35
  .addHelpText("after", queryHelp(REVIEW_QUERY))
36
36
  .argument("[query...]", `where the last review ended and which chat: date>2h, chat:"Book club"; ${REVIEW_DAYS} days ago if no date`)
37
- .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())
38
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`)
39
39
  .option("--all", "muted and archived chats too — left out unless they mention you or reply to you")
40
40
  .option(...TRANSCRIBE_OPTION)
@@ -47,7 +47,9 @@ export const reviewCommand = (messenger) => annotate(new Command("review"), {
47
47
  const hearWith = modelWith(options.transcribe, options.model);
48
48
  const context = messengerContext(this, messenger);
49
49
  const { settings, renderer, format, env } = context;
50
- 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");
51
53
  if (options.new && (from !== undefined || options.unanswered !== undefined)) {
52
54
  // An open question stays open: moving past it would drop it from the next review.
53
55
  throw new CliError("validation_error", "--new keeps its own point — not with a date or --unanswered");
@@ -62,7 +64,7 @@ export const reviewCommand = (messenger) => annotate(new Command("review"), {
62
64
  : undefined;
63
65
  const since = from ?? saved?.first ?? reviewStart();
64
66
  const hours = options.unanswered === undefined ? undefined : unansweredHours(options.unanswered);
65
- const kinds = options.kind === undefined ? undefined : kindsOf(options.kind);
67
+ const kinds = queriedKinds ?? (options.kind === undefined ? undefined : kindsOf(options.kind));
66
68
  let hearing;
67
69
  const transcribe = options.transcribe === true;
68
70
  const { found, reader } = await context.withServices(async (services, connect) => {
@@ -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) {
@@ -78,6 +78,7 @@ export declare const serveRelay: (command: Command, messenger: Messenger) => {
78
78
  inviteLinkRevoke?: boolean;
79
79
  inviteLinkUpdate?: boolean;
80
80
  joinRequestsAll?: boolean;
81
+ joinRequestsByLink?: boolean;
81
82
  channelComments?: boolean;
82
83
  addsWithHistory?: boolean;
83
84
  adminRights?: readonly import("../../domain/models.js").AdminRight[];
@@ -49,7 +49,7 @@ export const statsCommand = (messenger, loadRenderer = chartRenderer) => {
49
49
  .description("a chart's data from a chat's statistics, and optionally a dark SVG or PNG image")
50
50
  .argument("<chat>", messenger.chatArgument)
51
51
  .option("--chart-kind <messages|active|membership>", "what to draw: messages, active authors, or joins and leaves", chartKindOf, "messages")
52
- .option("--by <day|week>", "one point per calendar day or week (weeks start on Monday)", chartPeriodOf, "day")
52
+ .option("--by <group>", "one point per calendar day or week (weeks start on Monday)", chartPeriodOf, "day")
53
53
  .option("--since-time <time>", `ISO 8601, or 2h / 1d ago; ${EVENTS_DAYS} days ago if not given`)
54
54
  .option("--timezone <zone>", "the IANA timezone for calendar days")
55
55
  .option("--output <file>", "write a dark image to a new .svg or .png file")
@@ -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 { TAGGING_QUERY } from "../../services/tags.js";
5
5
  import { changesStore, FETCH, LOCAL_READ, LOCAL_WRITE } from "../effects.js";
6
6
  import { itemOf, renderAnswer, snakeKeys } from "../envelope.js";
@@ -23,7 +23,7 @@ const describe = ({ type, chatId, personId, messageId, taskId }) => type === "ch
23
23
  const line = (one) => `${one.tag} ${one.type} ${one.name ?? ""} (${one.target})`;
24
24
  const sourceOf = (value) => {
25
25
  if (value !== "manual" && value !== "auto")
26
- throw new CliError("validation_error", "--source takes manual or auto");
26
+ throw new CliError("validation_error", "--claim takes manual or auto");
27
27
  return value;
28
28
  };
29
29
  /** The owner's labels, in the local store only: `tag:` in a search finds what they label. */
@@ -35,7 +35,7 @@ export const tagsCommand = (messenger) => {
35
35
  changesStore(annotate(tags.command("auto"), { effects: FETCH, examples: [`${tool} tags auto --chat "Book club" --dry-run`] }))
36
36
  .description("derive local group/channel tags from cached metadata using keyword rules")
37
37
  .option("--chat <chat>", "a stored group/channel; repeat to select several", (value, previous) => [...previous, value], [])
38
- .option("--limit <number>", "process at most 1–500 chats", positiveCount("--limit"), 50)
38
+ .option("--limit <n>", "process at most 1–500 chats", positiveCount("--limit"), 50)
39
39
  .option("--sync-first", "read current descriptions from the messenger before classifying")
40
40
  .option("--dry-run", "preview cached classification without changing the store")
41
41
  .action(async function () {
@@ -62,12 +62,18 @@ export const tagsCommand = (messenger) => {
62
62
  effects: LOCAL_WRITE,
63
63
  examples: [`${tool} tags remove reading --chat "Book club"`],
64
64
  }).description("take tags off one chat, person, message or task"), messenger, "untag")
65
- .option("--source <manual|auto>", "remove only this ownership claim", sourceOf)
65
+ .addOption(new Option("--claim <claim>", "remove only this ownership claim: manual or auto")
66
+ .argParser(sourceOf)
67
+ .conflicts("source"))
68
+ .addOption(new Option("--source <manual|auto>", "compatibility alias for --claim")
69
+ .argParser(sourceOf)
70
+ .hideHelp()
71
+ .conflicts("claim"))
66
72
  .action(async function (given) {
67
73
  const context = messengerContext(this, messenger);
68
74
  refuseLocalWrite(context, messenger.app.command, "tags.remove");
69
75
  const target = this.opts();
70
- const done = await context.withServices((services) => services.tags.remove(target, given, this.opts().source));
76
+ const done = await context.withServices((services) => services.tags.remove(target, given, this.opts().claim ?? this.opts().source));
71
77
  renderAnswer(this, context, itemOf(snakeKeys(done)), () => done.removed.length ? `${describe(done.target)}: ${done.removed.join(", ")} removed\n` : "");
72
78
  if (done.unchanged.length && context.format === "pretty")
73
79
  context.renderer.note(`not there: ${done.unchanged.join(", ")}`);
@@ -17,7 +17,7 @@ export const transcribeSubcommand = (messages, messenger) => annotate(messages.c
17
17
  .argument("<chat>", messenger.chatArgument)
18
18
  .argument("<message>", "the id of a voice message")
19
19
  .option("--local", "use the model on this machine, never the messenger")
20
- .option("--model <id>", "which downloaded model; implies --local (`models audio list`)")
20
+ .option("--model <model>", "which downloaded model; implies --local (`models audio list`)")
21
21
  .action(async function (chat, messageId) {
22
22
  const context = messengerContext(this, messenger);
23
23
  const choice = {
@@ -42,9 +42,15 @@ export const registerPrompts = (server, { command, name }) => {
42
42
  ?.split(",")
43
43
  .map((one) => one.trim())
44
44
  .filter((one) => one.length > 0);
45
+ const query = [
46
+ kinds?.length ? `kind:${JSON.stringify(kinds.join(","))}` : "",
47
+ mode === undefined || mode === "unread" || mode === "new" ? "" : after(mode),
48
+ ]
49
+ .filter(Boolean)
50
+ .join(" ");
45
51
  const args = {
46
- ...(kinds?.length ? { kind: kinds.join(",") } : {}),
47
- ...(mode === undefined || mode === "unread" ? {} : mode === "new" ? { new: true } : { query: after(mode) }),
52
+ ...(query ? { query } : {}),
53
+ ...(mode === "new" ? { new: true } : {}),
48
54
  };
49
55
  return asked([
50
56
  `Catch me up on ${name}. Call ${call(`${command}_read`, "inbox", args)} once.`,