@wirecat/cli-base 0.51.0 → 0.52.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,24 @@ 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.52.0 — 25.10.2026
12
+
13
+ ### Added
14
+
15
+ - **`cli-dev audit --groups <file>`** compares the CLIs by the parity groups in that file instead of `PARITY_GROUPS`. The file is a JSON list in the shape `defineParityGroups` takes (`name`, `members`, `covers`, `differences`) and is checked the same way; a bad file stops the run with exit 2 and names the file. A CLI outside WireCatLabs keeps this file in its own repository and lists itself next to our CLIs, with no pull request here. `--differences` still adds to the groups in use, so with both flags it adds to the file's groups. Without the flag nothing changes.
16
+ - **`cli-dev <command> --help`** prints the usage, as `cli-dev --help` does, instead of failing on an unknown option.
17
+
18
+ - **A data-model page in the standards** (`WC-DATA-1` to `WC-DATA-7`, [Data model](docs/standards/data-model.md)). It says which records cli-core owns and which stay private to a CLI, keeps a source's records (a calendar entry, a Drive file, a mail contact) apart from the owner's entities (an event, a document, a person), puts every provider id in `external_id`, lists the standard field names, and makes every link record how it was made, with no link when two candidates tie. Nothing changes in code; older tables follow the rules when they next change.
19
+ ## 0.51.1 — 24.10.2026
20
+
21
+ ### Fixed
22
+
23
+ - The shared vocabulary recognizes `--draft` as a switch that saves text for review without sending. Enabling an adapter’s draft capability no longer creates an unknown-option-role lint failure.
24
+
25
+ ### Removed
26
+
27
+ - **No wait between releases and no weekly limit on breaking changes** (`WC-REL-8`, [Releases](docs/standards/releases.md)). A messenger CLI no longer waits two hours after its last version, and a shared library may ship a change that makes the CLIs change their code at any time. `WC-REL-9` is merged into `WC-REL-8`. The release lock stays. Nothing changes in code.
28
+
11
29
  ## 0.51.0 — 24.10.2026
12
30
 
13
31
  ### Added
package/dist/dev/main.js CHANGED
@@ -14,7 +14,7 @@ import { changelogProblems, docsProblems, JOURNAL_IDS, markdownFiles, packageVer
14
14
  import { AUDIT_BLOCKS, defineParityGroups, PARITY_GROUPS, REQUIRED_ROOTS } from "../standards/audit.js";
15
15
  import { defineReasons } from "../standards/errors.js";
16
16
  import { baselineProblems, baselineSchema, LINT_RULES, OVERRIDE_BUDGET, OVERRIDE_RULE_REVIEW, overridesProblems, overridesSchema, rulesForReview, } from "../standards/lint.js";
17
- import { issuesOf } from "../standards/registry.js";
17
+ import { issuesOf, RegistryError } from "../standards/registry.js";
18
18
  import { VOCABULARY } from "../standards/vocabulary.js";
19
19
  import { auditClis, scorecardTable, snakeKeys } from "./audit.js";
20
20
  import { readCapabilities } from "./capabilities.js";
@@ -49,14 +49,17 @@ const USAGE = `usage: cli-dev <command> [options]
49
49
  lint/baseline/<rule>.json to today's violations. --schemas
50
50
  is a JSON object of the CLI's own JSON Schemas by name
51
51
  audit (--cli <bin> | --commands <file>)... [--schemas <cli>=<file>]... [--reasons <cli>=<file>]...
52
- [--lint <cli>=<dir>]... [--capabilities <file>] [--complete] [--differences <file>] [--baseline <dir>] [--for <cli>] [--json] [--write-baseline]
52
+ [--lint <cli>=<dir>]... [--capabilities <file>] [--complete] [--groups <file>] [--differences <file>]
53
+ [--baseline <dir>] [--for <cli>] [--json] [--write-baseline]
53
54
  several CLIs against every lint rule and against each other:
54
55
  required roots, same path same contract, same flag same
55
56
  meaning, same entity same schema, same failure same reason.
56
57
  Prints the scorecard; fails on a new finding (one naming
57
58
  --for's CLI, when given). --lint judges that CLI's lint
58
59
  rules by its own lint/ folder; the rest by --baseline
59
- (audit/baseline)`;
60
+ (audit/baseline). --groups replaces PARITY_GROUPS with the
61
+ JSON list of parity groups in that file; --differences adds
62
+ to the groups in use`;
60
63
  const load = async (cwd, path) => (await import(pathToFileURL(isAbsolute(path) ? path : join(cwd, path)).href));
61
64
  const report = (io, problems, ok) => {
62
65
  for (const problem of problems)
@@ -335,6 +338,7 @@ const auditRun = (args, io) => {
335
338
  capabilities: { type: "string" },
336
339
  complete: { type: "boolean", default: false },
337
340
  differences: { type: "string" },
341
+ groups: { type: "string" },
338
342
  reasons: { type: "string", multiple: true },
339
343
  lint: { type: "string", multiple: true },
340
344
  baseline: { type: "string", default: "audit/baseline" },
@@ -445,6 +449,16 @@ const auditRun = (args, io) => {
445
449
  const baselineDir = at(values.baseline);
446
450
  const baseline = readBaseline(baselineDir, values.baseline, problems);
447
451
  let groups = PARITY_GROUPS;
452
+ if (values.groups) {
453
+ try {
454
+ groups = defineParityGroups(readJson(at(values.groups)));
455
+ }
456
+ catch (error) {
457
+ if (error instanceof RegistryError)
458
+ throw new Error(`${values.groups}: ${error.problems.join("; ")}`);
459
+ throw error;
460
+ }
461
+ }
448
462
  if (values.differences) {
449
463
  const schema = v.array(v.strictObject({
450
464
  group: v.string(),
@@ -508,6 +522,10 @@ const auditRun = (args, io) => {
508
522
  };
509
523
  export const main = async (argv, io) => {
510
524
  const [command, ...args] = argv;
525
+ if (args.includes("--help")) {
526
+ io.out(USAGE);
527
+ return 0;
528
+ }
511
529
  switch (command) {
512
530
  case "slow-tests": {
513
531
  const [file = "coverage/tests.json", count = "20"] = args;
@@ -291,6 +291,12 @@ export const HOW_FLAGS = [
291
291
  type: "duration",
292
292
  meaning: "how old a stored snapshot may be and still count as fresh",
293
293
  },
294
+ {
295
+ name: "--draft",
296
+ role: "mode",
297
+ type: "switch",
298
+ meaning: "save text for the owner to review and send, without sending a message",
299
+ },
294
300
  { name: "--md", role: "mode", type: "switch", meaning: "the text is the messenger's Markdown" },
295
301
  { name: "--no-backup", role: "mode", type: "switch", meaning: "do not back the file up before deleting it" },
296
302
  { name: "--no-input", role: "mode", type: "switch", meaning: "never prompt or open an interactive login" },
@@ -1,7 +1,7 @@
1
1
  # WireCat CLI standards
2
2
 
3
3
  This handbook holds the rules every WireCat CLI follows: tools, layout, commands, output, MCP, accounts,
4
- permissions, architecture, time, tests, security, documents, releases and CI. Read this page first. Then read the page
4
+ permissions, architecture, the data model, time, tests, security, documents, releases and CI. Read this page first. Then read the page
5
5
  for the area you are about to change.
6
6
 
7
7
  It ships in the `@wirecat/cli-base` package. A CLI that depends on cli-base has it at
@@ -35,11 +35,12 @@ rules it keeps. A new CLI is ready for its first release when every check under
35
35
  | [Architecture](architecture.md) | `ARCH` | 16 | Layers, dependency direction, vendor types, what goes in which package |
36
36
  | [Lifecycle](lifecycle.md) | `LIFE` | 13 | Opening late, who closes, one-shot exits, long-running commands, the `serve` relay, resumable syncs |
37
37
  | [Store](store.md) | `STORE` | 14 | Store areas, account scope, migrations, test isolation |
38
+ | [Data model](data-model.md) | `DATA` | 7 | Core entities, entities and source records, `external_id`, field names, links and duplicates |
38
39
  | [Testing](testing.md) | `TEST` | 17 | Sandbox, no network, no real waits, leak tests, coverage, contracts, test matrix, parity |
39
40
  | [Security](security.md) | `SECR` | 20 | Secrets, logs, keyring, argv, error text, untrusted text, scanners, workflow hardening, reporting |
40
41
  | [Documents](docs.md) | `DOC` | 20 | User pages and developer pages, generated pages, current facts, changelog |
41
42
  | [Comparisons](comparisons.md) | `CMP` | 11 | Which tools a comparison page lists, rows as user tasks, agents and safety, where others are better, proof of marks |
42
- | [Releases](releases.md) | `REL` | 16 | `bin/release`, the release lock, versions, release reports, trusted publishing |
43
+ | [Releases](releases.md) | `REL` | 15 | `bin/release`, the release lock, versions, release reports, trusted publishing |
43
44
  | [CI](ci.md) | `CI` | 12 | Hooks, the pull request budget, full validation, monthly runs |
44
45
  | [Time](time.md) | `TIME` | 6 | UTC in the store and in answers, the zone at input and display edges, date-only filters, tests in another zone |
45
46
  | [Style](style.md) | `STYLE` | 10 | Comments, TypeScript habits, commit messages |
@@ -0,0 +1,163 @@
1
+ # Data model
2
+
3
+ ## Scope
4
+
5
+ How records are modelled in the shared store: which entities cli-core owns, how a source's records relate to the
6
+ owner's real things, where provider ids go, what fields are called, and how records are linked without making
7
+ duplicates.
8
+
9
+ Rule ids on this page are `WC-DATA-<n>`. How to read a rule, how ids work and which rule wins are in the
10
+ [index](README.md).
11
+
12
+ Related pages: the store itself, its areas and migrations are in [Store](store.md); UTC times and date-only values are
13
+ in [Time](time.md); secrets that must never reach the store are in [Security](security.md).
14
+
15
+ ### Words used here
16
+
17
+ - **Entity:** the real thing the owner means — a person, an event, a document. It carries no provider id. Tables:
18
+ `persons`, `events`, `event_series`, `documents`.
19
+ - **Source record:** one source's copy of an entity — a mail contact, a Telegram user, a calendar entry, a Zoom
20
+ meeting, a Drive file, a local file. It carries the account or provider it came from and that provider's id.
21
+ Tables: `identities` (contacts), `calendar_entries`, `meetings`, `files`, `emails`, `messages`.
22
+ - **Link:** the reference from a source record to its entity, with how it was made.
23
+ - **Core entity:** an entity or source record whose table cli-core owns, because more than one tool may need it.
24
+
25
+ ### Who follows these rules today
26
+
27
+ These rules are new. Older tables break some of them; a change to one of those tables follows the rule, and nothing
28
+ is rewritten only to follow it:
29
+
30
+ - Drive files are `documents` rows under a folder account, with Google's facts in `front_matter` JSON and the folder
31
+ tree in `metadata.catalog` (WC-DATA-3, WC-DATA-5).
32
+ - Drive owners are linked by Google's user id under provider `google-drive`, mail senders by address under `email`,
33
+ and nothing joins the two (WC-DATA-6).
34
+ - `events` rows made from Zoom meetings have no source record of their own besides the meeting (WC-DATA-2 holds:
35
+ `meetings` is that record).
36
+
37
+ ## Rules
38
+
39
+ ### WC-DATA-1 · A record more than one tool needs is a core entity in cli-core; a CLI's own tables stay private
40
+
41
+ A core entity's table, model, store area and migration live in cli-core, and any CLI reads it through cli-core
42
+ (memo is the general reader). A CLI may keep tables of its own — checkpoints, caches, provider bookkeeping — and no
43
+ other CLI reads them. Transient state is never a core entity.
44
+
45
+ - **Why:** every tool and memo can rely on the same people, events and documents, while a tool's internals stay free
46
+ to change.
47
+ - **Enforced by:** review only.
48
+ - **Example:** calendars, calendar entries, events, persons, contacts, documents and files are core; drive's sync
49
+ checkpoint is drive's own.
50
+ - **Exceptions:** none. A CLI's own tables live in its own SQLite file next to the shared store, with its own
51
+ migrations, and refer to core rows by id only; no other CLI opens that file (NEED-1737). The shared store stays
52
+ cli-core's alone ([WC-STORE-5](store.md)).
53
+ - **Applies to:** cli-core and every CLI. **Since:** cli-base 0.52.0. **Rulings:** NEED-1730, NEED-1737.
54
+
55
+ ### WC-DATA-2 · Keep a source's records apart from the owner's entities, and link them
56
+
57
+ What a source returns is stored as a source record; the thing the owner means is an entity; the source record links
58
+ to its entity. One entity may combine source records from several sources and accounts.
59
+
60
+ - **Why:** the same meeting in two calendars and in Zoom is one event to the owner; the same document in Drive and on
61
+ disk is one document.
62
+ - **Enforced by:** review only.
63
+ - **Example:** a Google Calendar entry is a `calendar_entries` row with `event_id`; the Zoom recording of that meeting
64
+ is a `meetings` row with the same `event_id`.
65
+ - **Exceptions:** an entity the owner makes has no source record; a source record may wait unlinked until matching
66
+ runs.
67
+ - **Applies to:** cli-core. **Since:** cli-base 0.52.0. **Rulings:** NEED-1731.
68
+
69
+ ### WC-DATA-3 · A provider's id goes in `external_id`
70
+
71
+ The id a provider gives a record is stored in `external_id`, unique with `account_id` (or with `provider` where the
72
+ record has no account). Never in a column named after the provider (`google_id`, `file_id`) and never only in
73
+ `metadata`. A second id the provider also uses gets a named column of its own (`ical_uid`).
74
+
75
+ - **Why:** one place to look for every provider's id, and one unique key to update a record by.
76
+ - **Enforced by:** review only.
77
+ - **Example:** a Drive file: `files.external_id = '<Drive file id>'`; a Google calendar entry:
78
+ `calendar_entries.external_id = '<event id>'`, `ical_uid = '<iCalUID>'`.
79
+ - **Exceptions:** a local file has no provider id; its `external_id` is its path relative to its folder root.
80
+ - **Applies to:** cli-core. **Since:** cli-base 0.52.0. **Rulings:** NEED-1732.
81
+
82
+ ### WC-DATA-4 · Use the standard field names
83
+
84
+ | Meaning | Name |
85
+ |---|---|
86
+ | Primary key; a reference | `id`; `<entity>_id` (`event_id`, `document_id`) |
87
+ | Account, provider, provider's id | `account_id`, `provider`, `external_id` |
88
+ | Subtype; state | `kind`; `status` |
89
+ | Title of content (document, event, chat); name of a person, file or folder | `title`; `name` |
90
+ | Main text; its search form | `body`; `normalized_text` |
91
+ | Web link at the provider (never one that carries a secret) | `url` |
92
+ | File facts | `mime`, `size` (bytes), `content_hash` (sha-256 hex), `path` |
93
+ | Provider extras, front matter | `metadata` (JSON); front matter as `metadata.front_matter` |
94
+ | Our row's times | `created_at`, `updated_at`, `deleted_at` |
95
+ | The provider's times | `external_created_at`, `external_updated_at` |
96
+ | A span; last sync that returned the record | `starts_at`, `ends_at` (exclusive); `seen_at` |
97
+ | A date without a time | `<name>_date` (`YYYY-MM-DD`) |
98
+ | A yes/no | integer 0/1, an adjective (`all_day`, `starred`) |
99
+ | A link to any of several tables | `<noun>able_type`, `<noun>able_id` (`notable_type`) |
100
+ | History; search queue; folded text for filters | `<table>_revisions`; `<entity>_index_pending`; `<field>_folded` |
101
+
102
+ Times are UTC milliseconds ([Time](time.md)).
103
+
104
+ - **Why:** a reader of any table knows what a field holds, and a new table does not invent a third name for "edited
105
+ at".
106
+ - **Enforced by:** review only.
107
+ - **Example:** `files.external_updated_at`, not `modified_at` or `modifiedAt` in JSON.
108
+ - **Exceptions:** existing columns keep their names until their table changes for another reason.
109
+ - **Applies to:** cli-core. **Since:** cli-base 0.52.0. **Rulings:** NEED-1732.
110
+
111
+ ### WC-DATA-5 · A field the store filters, sorts, joins or lists on is a column; the rest is allow-listed JSON
112
+
113
+ - **Why:** columns can be indexed and checked; JSON keeps rare provider fields without a migration each.
114
+ - **Enforced by:** review only.
115
+ - **Example:** a Drive file's `mime`, `size` and `external_updated_at` are columns; its `sourceVersion` is
116
+ `metadata.version`.
117
+ - **Exceptions:** none. `metadata` never holds secrets or a provider's raw payload ([Security](security.md)).
118
+ - **Applies to:** cli-core. **Since:** cli-base 0.52.0. **Rulings:** NEED-1733.
119
+
120
+ ### WC-DATA-6 · Every link records how it was made; the owner's link wins; ambiguity makes no link
121
+
122
+ A link from a source record to its entity records its `method` (the rule, or `owner`), `confidence`, `author` and
123
+ time, and every change is logged. A rule never replaces an owner's link. When two candidates match equally well, no
124
+ link is made and the pair goes to the owner's review list. Title or time overlap alone never links records.
125
+
126
+ - **Why:** a wrong automatic merge is worse than a duplicate, and the owner must be able to see and undo it.
127
+ - **Enforced by:** review only.
128
+ - **Example:** `identity_links` and `identity_link_events` (contacts to persons); a Zoom meeting at 11:00 next to a
129
+ 10–11 and an 11–12 event links to neither by time alone.
130
+ - **Exceptions:** none.
131
+ - **Applies to:** cli-core. **Since:** cli-base 0.52.0. **Rulings:** NEED-1731.
132
+
133
+ ### WC-DATA-7 · A source record the provider stops returning is soft-deleted; its entity and links stay
134
+
135
+ - **Why:** a sync error or a revoked share must not erase what the owner linked, wrote or searched.
136
+ - **Enforced by:** review only.
137
+ - **Example:** a calendar sync whose token expired lists again and sets `deleted_at` on entries it did not see; their
138
+ events and meeting links stay.
139
+ - **Exceptions:** an entity with no live source record and no owner link may be removed by an explicit owner command.
140
+ - **Applies to:** cli-core. **Since:** cli-base 0.52.0. **Rulings:** none.
141
+
142
+ ## Planned — not built yet
143
+
144
+ From the Wave 3 requirements (cli-private `plans/2026-10-24-open-cli-core-design.md`, under the owner's review):
145
+
146
+ - a `files` table as the source record of `documents`: a materialised `path` plus `parent_id` to the folder's own
147
+ row, so a folder is found by path prefix and a document by part of its path (NEED-1739);
148
+ - memo's vault files as documents; an Obsidian vault is provider `obsidian` with its own metadata (tags, aliases,
149
+ wiki links), a plain folder is `local`; `notes` are only the owner's notes on entities (NEED-1738);
150
+ - a cli-core helper that opens a CLI's own SQLite file (NEED-1737);
151
+ - `calendars`, `calendar_entries` and `calendar_attendees` on the `events` hub;
152
+ - an `email` column on contacts, so one address joins one person across sources;
153
+ - one link log for events and documents, and one review list for ambiguous pairs;
154
+ - renaming `identities` to `contacts`.
155
+
156
+ ## Sources
157
+
158
+ These documents explain the rules on this page in more detail. Where one of them disagrees with this page, this page
159
+ holds.
160
+
161
+ - [cli-core `ARCHITECTURE.md`, "The store"](https://github.com/WireCatLabs/cli-core/blob/main/docs/dev/ARCHITECTURE.md#the-store)
162
+ - cli-private `plans/2026-10-24-open-cli-core-design.md` (private; the rulings NEED-1730 to NEED-1734 are summarised
163
+ above)
@@ -92,8 +92,8 @@ whatsapp-cli is the worked example: it has every file below. Compare with it whe
92
92
  8. **`SECURITY.md`**: how to report a vulnerability, linking cli-testing's security method. See
93
93
  [Security](security.md).
94
94
  9. **`bin/release`**, **`release.yml`** and a release skill, copied from the nearest CLI. `bin/release` takes the
95
- release lock (WC-REL-3) and npm's trusted publisher settings name `release.yml`. A messenger CLI also waits two
96
- hours between releases and checks a signed release report (WC-REL-8, WC-REL-10). See [Releases](releases.md).
95
+ release lock (WC-REL-3) and npm's trusted publisher settings name `release.yml`. A messenger CLI also checks a signed
96
+ release report (WC-REL-10). See [Releases](releases.md).
97
97
  10. **`src/version.ts`** kept in step with `package.json`. Proved by: `pnpm version:check`.
98
98
 
99
99
  ## Done when
@@ -88,29 +88,17 @@ on the [CI](ci.md) page.
88
88
  - **Exceptions:** a pre-release version (`0.41.0-rc.1`) is chosen by hand.
89
89
  - **Applies to:** all CLIs and shared libraries. **Since:** cli-base 0.41.0. **Rulings:** none.
90
90
 
91
- ### WC-REL-8 · A messenger CLI waits two hours between releases; a shared library does not wait
91
+ ### WC-REL-8 · Any package may release at any time
92
92
 
93
- - **Why:** a broken CLI release must be noticed before the next one lands on top of it. Every CLI waits for a library
94
- release, so a gap there only delays them.
95
- - **Enforced by:** `<app>: bin/release`.
96
- - **Example:** tg-cli, max-cli and whatsapp-cli's `bin/release` refuse to start within two hours of the last version
97
- on npm. cli-base and cli-core release one after another with no gap.
98
- - **Exceptions:** memo, drive and zoom have no gap in their `bin/release` today.
99
- - **Applies to:** messenger CLIs (gap); shared libraries (no gap). **Since:** cli-base 0.41.0. **Rulings:** NEED-854.
100
-
101
- ### WC-REL-9 · A change to a stable shared-library export that makes a CLI change its code ships at most once a week
102
-
103
- - **Why:** the limit keeps the CLIs from chasing versions; a change that no CLI has to follow gives them nothing to
104
- chase.
105
- - **Enforced by:** `cli-core: parity.yml` (it decides which changes count; review keeps the week).
106
- - **Example:** cli-core's parity jobs build tg-cli, max-cli and whatsapp-cli from their `main` against every pull
107
- request. A change to a stable export counts unless all three jobs pass with each CLI's `main` unchanged; its
108
- changelog still lists it under "Changed — may break callers" for outside users. It counts when a job fails, when it
109
- passes only against a CLI branch made for the change, and when it compiles but changes behaviour that any CLI must
110
- adapt to. Parity cannot see that last case, so when in doubt the change counts. Gather the changes that count into
111
- one release a week, and bump the library in every CLI that uses it on the same day.
112
- - **Exceptions:** none.
113
- - **Applies to:** shared libraries. **Since:** cli-base 0.41.0. **Rulings:** NEED-473, NEED-1552.
93
+ - **Why:** a wait between releases or a weekly limit on breaking changes only delays a fix; the owner removed both
94
+ limits on 2026-10-24.
95
+ - **Enforced by:** review only.
96
+ - **Example:** tg-cli can release an hour after its last version, and cli-core can ship a breaking change on the same
97
+ day as the previous one. Bump the library in every CLI that uses it on the day it ships.
98
+ - **Exceptions:** none. The release lock (WC-REL-3) still keeps two releases on one machine from running at once.
99
+ - **Applies to:** all CLIs and shared libraries. **Since:** cli-base 0.52.0. **Rulings:** NEED-1741.
100
+
101
+ ### WC-REL-9 · Merged into [WC-REL-8](#wc-rel-8--any-package-may-release-at-any-time)
114
102
 
115
103
  ### WC-REL-10 · A messenger CLI is released only with a release report the owner signed for that commit
116
104
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wirecat/cli-base",
3
- "version": "0.51.0",
3
+ "version": "0.52.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",