@wirecat/cli-base 0.69.0 → 0.71.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.
package/CHANGELOG.md CHANGED
@@ -8,6 +8,39 @@ Versions before 0.8.0 are in the [git tags](https://github.com/WireCatLabs/cli-b
8
8
  Every entry says what changed as a caller sees it, why, and what to watch for — the rules are
9
9
  [`docs/dev/CONVENTIONS.md`](docs/dev/CONVENTIONS.md#the-changelog).
10
10
 
11
+ ## 0.71.0 — 01.11.2026
12
+
13
+ ### Added
14
+
15
+ - **A vocabulary flag can list the value names other domains give it (`otherValues`).** `--to` stays `<chat>` and also
16
+ takes `<address>` (mail); `--subject` stays `<ref>` and also takes `<text>`. `flag-two-meanings` accepts those and
17
+ reports any other value name. A CLI that declares `--to <address>` or `--subject <text>` needs no override.
18
+
19
+ ### Fixed
20
+
21
+ - **0.70.0 made every messenger CLI fail `flag-two-meanings`.** It wrote `--to` as `<chat|address>` and `--subject`
22
+ as `<ref|text>`, and the lint compares the value name as written, so `--to <chat>` was reported. Both go back to
23
+ the original value name, and mail's names are listed beside them. Do not use 0.70.0.
24
+
25
+ ## 0.70.0 — 01.11.2026
26
+
27
+ ### Added
28
+
29
+ - **Mail words in the vocabulary: the `drafts` resource and `--cc`, `--bcc`, `--reply-all`.** mail-cli saves drafts
30
+ in a mailbox's Drafts folder (`mail <profile> drafts list|create|update|delete`) and addresses e-mail with copies;
31
+ the command lint reported each as a word it did not know.
32
+ - **`--to` takes a chat or an e-mail address, `--subject` takes a reference or a subject line.** The owner ruled
33
+ that mail shares these two words with the messengers and notes instead of renaming them. `--to` is now
34
+ `<chat|address>`, a `string` (was an `id`); `--subject` is `<ref|text>`. The meaning per domain is in each flag's
35
+ text, so `flag-two-meanings` stops reporting mail's use. Mail-cli can drop its overrides for both.
36
+
37
+ ### Fixed
38
+
39
+ - **mail is compared in its own parity group, `mail-shared`, which leaves out `search`.** mail's `search messages` is
40
+ cli-core's mail search, a different command from the messengers' under the same path, and `audit --complete` took
41
+ its twenty-odd differences as inherited. mail is compared on every other shared root, and moves back into `all`
42
+ once its search is aligned with the messengers' or renamed.
43
+
11
44
  ## 0.69.0 — 01.11.2026
12
45
 
13
46
  ### Added
package/dist/dev/lint.js CHANGED
@@ -229,7 +229,10 @@ const CHECKS = {
229
229
  }));
230
230
  },
231
231
  "flag-two-meanings": (document, vocabulary) => {
232
- const registered = new Map(vocabulary.flags.map((flag) => [flag.name, flag.value?.slice(1, -1).toLowerCase() ?? "switch"]));
232
+ const registered = new Map(vocabulary.flags.map((flag) => [
233
+ flag.name,
234
+ [flag.value?.slice(1, -1).toLowerCase() ?? "switch", ...(flag.otherValues ?? [])],
235
+ ]));
233
236
  const uses = new Map();
234
237
  const note = (option, where) => {
235
238
  const name = longName(option.flags);
@@ -246,8 +249,8 @@ const CHECKS = {
246
249
  const found = [];
247
250
  for (const [name, kinds] of [...uses].sort(([a], [b]) => byText(a, b))) {
248
251
  const expected = registered.get(name);
249
- const off = expected === undefined ? [] : [...kinds.keys()].filter((kind) => kind !== expected);
250
- if (kinds.size < 2 && off.length === 0)
252
+ const off = expected === undefined ? [] : [...kinds.keys()].filter((kind) => !expected.includes(kind));
253
+ if (expected === undefined ? kinds.size < 2 : off.length === 0)
251
254
  continue;
252
255
  const described = [...kinds]
253
256
  .map(([kind, where]) => `${kind === "switch" ? "a switch" : `<${kind}>`} in ${where.slice(0, 3).join(", ")}`)
@@ -256,7 +259,7 @@ const CHECKS = {
256
259
  target: name,
257
260
  message: expected === undefined
258
261
  ? `${name} has ${kinds.size} meanings: ${described}`
259
- : `${name} is ${expected === "switch" ? "a switch" : `<${expected}>`} in the vocabulary, but: ${described}`,
262
+ : `${name} is ${expected[0] === "switch" ? "a switch" : expected.map((kind) => `<${kind}>`).join(" or ")} in the vocabulary, but: ${described}`,
260
263
  });
261
264
  }
262
265
  return found;
@@ -68,19 +68,35 @@ export const defineParityGroups = (input) => parseRegistry("parity groups", v.ar
68
68
  .map((entry) => `${group.name}: "${entry.target}" has \`absent\`, which measures the values itself; drop \`expected\``),
69
69
  ]),
70
70
  ]);
71
+ const MEMO_HAS_NO_PROFILES = [
72
+ "mcp serve",
73
+ "mcp revoke",
74
+ "mcp server start",
75
+ "mcp server status",
76
+ "mcp server stop",
77
+ ].map((path) => ({
78
+ rule: "same-path-contract",
79
+ target: `${path} --profiles`,
80
+ absent: "memo",
81
+ reason: "memo has no profiles: it reads other CLIs' stores and selects their accounts with --source and --account " +
82
+ "(https://github.com/WireCatLabs/cli-base/issues/390)",
83
+ }));
71
84
  export const PARITY_GROUPS = defineParityGroups([
72
85
  { name: "messengers", members: ["tg", "max"], covers: "tree", differences: [] },
73
86
  {
74
87
  name: "all",
75
- members: ["tg", "max", "memo", "drive", "zm", "wa", "mail"],
88
+ members: ["tg", "max", "memo", "drive", "zm", "wa"],
76
89
  covers: ["commands", "doctor", "skill", "mcp", "upgrade", "search", "notes", "meetings"],
77
- differences: ["mcp serve", "mcp revoke", "mcp server start", "mcp server status", "mcp server stop"].map((path) => ({
78
- rule: "same-path-contract",
79
- target: `${path} --profiles`,
80
- absent: "memo",
81
- reason: "memo has no profiles: it reads other CLIs' stores and selects their accounts with --source and --account " +
82
- "(https://github.com/WireCatLabs/cli-base/issues/390)",
83
- })),
90
+ differences: MEMO_HAS_NO_PROFILES,
91
+ },
92
+ // mail's `search messages` is cli-core's mail search, not the messengers' command: --complete took their 20-odd
93
+ // differences as inherited (https://github.com/WireCatLabs/cli-base/issues/390). mail is compared everywhere else
94
+ // and moves back into "all" once its search is aligned with the messengers' or renamed.
95
+ {
96
+ name: "mail-shared",
97
+ members: ["tg", "max", "memo", "drive", "zm", "wa", "mail"],
98
+ covers: ["commands", "doctor", "skill", "mcp", "upgrade", "notes", "meetings"],
99
+ differences: MEMO_HAS_NO_PROFILES,
84
100
  },
85
101
  // gcal is compared on what it already shares with every CLI; doctor, mcp, search and skill wait until the published
86
102
  // CLIs agree there (https://github.com/WireCatLabs/cli-base/issues/390), then gcal moves back into "all".
@@ -0,0 +1,2 @@
1
+ import type { VocabularyFlag } from "./vocabulary.js";
2
+ export declare const MAIL_FLAGS: VocabularyFlag[];
@@ -0,0 +1,22 @@
1
+ export const MAIL_FLAGS = [
2
+ {
3
+ name: "--cc",
4
+ value: "<address>",
5
+ role: "value",
6
+ type: "string",
7
+ meaning: "an e-mail address that gets a copy; repeat it for more",
8
+ },
9
+ {
10
+ name: "--bcc",
11
+ value: "<address>",
12
+ role: "value",
13
+ type: "string",
14
+ meaning: "an e-mail address that gets a blind copy; repeat it for more",
15
+ },
16
+ {
17
+ name: "--reply-all",
18
+ role: "value",
19
+ type: "switch",
20
+ meaning: "with --reply-to, an e-mail reply also goes to everyone the message went to",
21
+ },
22
+ ];
@@ -442,7 +442,14 @@ export const WHAT_FLAGS = [
442
442
  meaning: "keep a token an operation returns only in this bot profile's keyring; never print it",
443
443
  },
444
444
  { name: "--task", value: "<id>", role: "target", type: "id", meaning: "a task, by id" },
445
- { name: "--to", value: "<chat>", role: "target", type: "id", meaning: "the chat it goes to" },
445
+ {
446
+ name: "--to",
447
+ value: "<chat>",
448
+ role: "target",
449
+ type: "id",
450
+ otherValues: ["address"],
451
+ meaning: "who it goes to: a chat in a messenger (<chat>), an e-mail address in mail (<address>, repeated for more)",
452
+ },
446
453
  {
447
454
  name: "--topic-name",
448
455
  value: "<name>",
@@ -892,7 +899,8 @@ export const WHAT_FLAGS = [
892
899
  value: "<ref>",
893
900
  role: "value",
894
901
  type: "string",
895
- meaning: "what it is about, as a reference",
902
+ otherValues: ["text"],
903
+ meaning: "what it is about: a reference in a note or task (<ref>), the subject line in mail (<text>)",
896
904
  replaces: ["--about"],
897
905
  },
898
906
  {
@@ -43,6 +43,11 @@ export declare const vocabularySchema: v.StrictObjectSchema<{
43
43
  readonly type: v.PicklistSchema<readonly ["switch", "count", "number", "string", "id", "time", "duration", "choice", "list", "path", "cursor"], undefined>;
44
44
  readonly role: v.PicklistSchema<["action" | "budget" | "mode" | "output" | "paging" | "query" | "scope" | "sort" | "target" | "value", ...("action" | "budget" | "mode" | "output" | "paging" | "query" | "scope" | "sort" | "target" | "value")[]], undefined>;
45
45
  readonly meaning: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "must not be empty">]>;
46
+ /**
47
+ * Value names a domain other than the main one gives the flag, such as mail's `--to <address>` beside the
48
+ * messengers' `<chat>`. The meaning for each says which is which. `flag-two-meanings` accepts these and no others.
49
+ */
50
+ readonly otherValues: v.OptionalSchema<v.ArraySchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a value name such as address">]>, undefined>, undefined>;
46
51
  readonly unit: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "must not be empty">]>, undefined>;
47
52
  /** Other names for the same flag, which `flag-synonym` refuses. */
48
53
  readonly replaces: v.OptionalSchema<v.ArraySchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a long flag">]>, undefined>, undefined>;
@@ -1,6 +1,7 @@
1
1
  import * as v from "valibot";
2
2
  import { CALENDAR_FLAGS } from "./flags-calendar.js";
3
3
  import { HOW_FLAGS } from "./flags-how.js";
4
+ import { MAIL_FLAGS } from "./flags-mail.js";
4
5
  import { WHAT_FLAGS } from "./flags-what.js";
5
6
  import { duplicates, parseRegistry, text, word } from "./registry.js";
6
7
  export const FLAG_TYPES = [
@@ -48,6 +49,11 @@ const flagSchema = v.strictObject({
48
49
  type: v.picklist(FLAG_TYPES),
49
50
  role: v.picklist(Object.keys(FLAG_ROLES)),
50
51
  meaning: text,
52
+ /**
53
+ * Value names a domain other than the main one gives the flag, such as mail's `--to <address>` beside the
54
+ * messengers' `<chat>`. The meaning for each says which is which. `flag-two-meanings` accepts these and no others.
55
+ */
56
+ otherValues: v.optional(v.array(v.pipe(v.string(), v.regex(/^[a-z][a-z0-9-]*$/, "a value name such as address")))),
51
57
  unit: v.optional(text),
52
58
  /** Other names for the same flag, which `flag-synonym` refuses. */
53
59
  replaces: v.optional(v.array(v.pipe(v.string(), v.regex(/^--[a-z][a-z0-9]*(-[a-z0-9]+)*$/, "a long flag")))),
@@ -212,6 +218,7 @@ const collections = [
212
218
  ["chats", "conversations in a messenger"],
213
219
  ["contacts", "identities in a messenger's contact list"],
214
220
  ["messages", "posts in a messenger chat, or emails"],
221
+ ["drafts", "e-mails saved in a mailbox's Drafts folder, written and not sent"],
215
222
  ["polls", "polls in a chat"],
216
223
  ["reactions", "reactions to a message"],
217
224
  ["recipients", "who a message goes to"],
@@ -359,5 +366,5 @@ export const VOCABULARY = defineVocabulary({
359
366
  ],
360
367
  views: views.map(([entry, meaning]) => ({ word: entry, meaning })),
361
368
  roots: roots.map(([entry, meaning]) => ({ word: entry, meaning })),
362
- flags: [...HOW_FLAGS, ...WHAT_FLAGS, ...CALENDAR_FLAGS],
369
+ flags: [...HOW_FLAGS, ...WHAT_FLAGS, ...CALENDAR_FLAGS, ...MAIL_FLAGS],
363
370
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wirecat/cli-base",
3
- "version": "0.69.0",
3
+ "version": "0.71.0",
4
4
  "description": "The parts every command line tool needs: output modes, terminal rendering, error model, exit codes, credentials, clocks",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Viacheslav Ptsarev",