@wirecat/cli-base 0.63.0 → 0.65.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 (42) hide show
  1. package/CHANGELOG.md +232 -0
  2. package/dist/codegen/discovery.d.ts +3 -3
  3. package/dist/codegen/discovery.js +18 -9
  4. package/dist/codegen/index.d.ts +3 -1
  5. package/dist/codegen/index.js +3 -1
  6. package/dist/codegen/json-schema.d.ts +8 -0
  7. package/dist/codegen/json-schema.js +78 -0
  8. package/dist/commands/index.d.ts +1 -1
  9. package/dist/commands/index.js +1 -1
  10. package/dist/credentials.d.ts +1 -1
  11. package/dist/dev/bundle.d.ts +31 -0
  12. package/dist/dev/bundle.js +280 -0
  13. package/dist/dev/main.js +48 -1
  14. package/dist/skill/index.d.ts +1 -1
  15. package/dist/skill/index.js +2 -2
  16. package/dist/standards/audit.js +8 -0
  17. package/dist/standards/flags-calendar.d.ts +2 -0
  18. package/dist/standards/flags-calendar.js +72 -0
  19. package/dist/standards/flags-how.js +7 -1
  20. package/dist/standards/flags-what.js +7 -0
  21. package/dist/standards/vocabulary.js +13 -2
  22. package/docs/standards/README.md +5 -5
  23. package/docs/standards/accounts.md +44 -11
  24. package/docs/standards/architecture.md +19 -12
  25. package/docs/standards/ci.md +33 -12
  26. package/docs/standards/commands.md +35 -10
  27. package/docs/standards/data-model.md +5 -1
  28. package/docs/standards/docs.md +4 -1
  29. package/docs/standards/kit-surface.md +6 -5
  30. package/docs/standards/layout.md +32 -0
  31. package/docs/standards/lifecycle.md +69 -8
  32. package/docs/standards/mcp.md +48 -24
  33. package/docs/standards/new-cli.md +30 -16
  34. package/docs/standards/output.md +13 -2
  35. package/docs/standards/permissions.md +46 -3
  36. package/docs/standards/releases.md +21 -5
  37. package/docs/standards/security.md +16 -8
  38. package/docs/standards/store.md +4 -3
  39. package/docs/standards/testing.md +19 -8
  40. package/docs/standards/time.md +8 -1
  41. package/docs/standards/toolchain.md +24 -18
  42. package/package.json +6 -1
@@ -15,9 +15,12 @@ Rule ids on this page are `WC-MCP-<n>`. How to read a rule, how ids work and whi
15
15
 
16
16
  ### Who follows these rules today
17
17
 
18
- tg, max and wa serve cli-core's personal server (`<cli> <profile> mcp`) and bot server (`<cli> <profile> bot mcp`).
19
- zoom serves the three shared tools over its own commands from zoom-cli 0.35.0 (`zm <profile> mcp`, cli-core's
20
- `toolsFromCommands` and `runCommandForAgent`). memo and drive have their own servers: memo has hand-written tools, and
18
+ tg, max and wa serve cli-core's personal server (`<cli> <profile> mcp serve`) and bot server (`<cli> <profile> bot mcp serve`).
19
+ `mcp serve --profiles` ([WC-MCP-9](#wc-mcp-9--a-server-serves-the-profiles-it-is-told-with-one-connection-for-each)) is not
20
+ built yet: cli-core serves one named profile.
21
+ zoom serves the three shared tools over its own commands from zoom-cli 0.35.0 (`zm <profile> mcp serve`, cli-core's
22
+ `toolsFromCommands` and `runCommandForAgent`), and gcal does the same (`gcal <profile> mcp`; it moves to `mcp serve`
23
+ when it takes cli-core 0.463.0). memo and drive have their own servers: memo has hand-written tools, and
21
24
  drive builds one tool per command from its command tree and skips commands through its own list. Those two are known
22
25
  gaps against these rules.
23
26
 
@@ -66,12 +69,22 @@ Every entry in the catalogue must be a command the CLI has.
66
69
 
67
70
  Every command says whether it writes. `read` refuses a command that writes, and `write` refuses one that only reads.
68
71
  Both declare the answer envelope (a list `{ items, page, … }` or one thing `{ item, … }`) as their `outputSchema`.
69
-
70
- - **Why:** an agent host can let `read` run freely and ask a person before `write`.
72
+ A command that reads the service and only saves what it read into the owner's local store, such as `sync` or
73
+ `calendars list`, is a read and runs through `read`
74
+ ([WC-CMD-17](commands.md#wc-cmd-17--declare-effects-on-every-command) defines it).
75
+
76
+ - **Why:** an agent host can let `read` run freely and ask a person before `write`. `read` carries the MCP
77
+ `readOnlyHint`, which the [MCP schema](https://modelcontextprotocol.io/specification/2025-11-25/schema#toolannotations)
78
+ describes as "the tool does not modify its environment". A local copy of what the service already holds changes
79
+ nothing the owner would have to undo, so a sync stays under it; sending it through `write` would make a host ask a
80
+ person before every refresh.
71
81
  - **Enforced by:** `cli-core: src/mcp/mcp.test.ts` · `cli-core: src/mcp/output-schema.test.ts`.
72
- - **Example:** `max_read { "command": "messages send", … }` is refused.
73
- - **Exceptions:** none.
74
- - **Applies to:** all CLIs. **Since:** cli-base 0.41.0. **Rulings:** none.
82
+ - **Example:** `max_read { "command": "messages send", … }` is refused. Once the gap below is closed,
83
+ `gcal_read { "command": "sync" }` runs.
84
+ - **Exceptions:** none. Known gap, as of 2026-10-27: cli-core decides with cli-base's `writes()`, which still counts
85
+ the local write, so `sync` runs through `write` until that follow-up lands (WC-CMD-17).
86
+ - **Applies to:** all CLIs. **Since:** cli-base 0.41.0 (what counts as a read: cli-base 0.65.0). **Rulings:** the
87
+ owner's ruling on what counts as a read over MCP (2026-10-27).
75
88
 
76
89
  ### WC-MCP-5 · Unknown arguments are refused before anything runs
77
90
 
@@ -116,29 +129,36 @@ profile's permissions, the recipient list and the hourly limit decide.
116
129
 
117
130
  ### WC-MCP-8 · No `mcp` flag grants access
118
131
 
119
- `mcp --permission <key=level>` may only make a level stricter for that server. An override that would loosen the
132
+ `mcp serve --permission <key=level>` may only make a level stricter for that server. An override that would loosen the
120
133
  profile is refused at start-up with `validation_error` naming the key. This includes a parent key that would hide a
121
- stricter key under it, and lifting a built-in `ask`. `mcp setup --allow-writes` only confirms, at install time, that
134
+ stricter key under it, and lifting a built-in `ask`. `mcp add <client> --allow-writes` only confirms, at install time, that
122
135
  the owner saw the profile offers writes.
123
136
 
124
137
  - **Why:** whoever writes the agent's MCP config must not get more than the profile allows.
125
138
  - **Enforced by:** `cli-core: src/cli/messenger/mcp-command-http.test.ts`.
126
- - **Example:** `max work mcp --permission messages=readonly` works. `max work mcp --permission messages.delete=allow`
139
+ - **Example:** `max work mcp serve --permission messages=readonly` works. `max work mcp serve --permission messages.delete=allow`
127
140
  is refused, because the built-in level is `ask`.
128
- - **Exceptions:** `bot mcp` takes no `--permission` at all. cli-base's flag registry still describes `--allow-writes`
141
+ - **Exceptions:** `bot mcp serve` takes no `--permission` at all. cli-base's flag registry still describes `--allow-writes`
129
142
  as "permit writing"; that wording is a gap.
130
143
  - **Applies to:** all CLIs. **Since:** cli-base 0.41.0. **Rulings:** NEED-1423.
131
144
 
132
- ### WC-MCP-9 · A server speaks for one named profile over one connection
145
+ ### WC-MCP-9 · A server serves the profiles it is told, with one connection for each
133
146
 
134
- `mcp` needs a profile, like every command ([WC-ACCT-1](accounts.md)). The server opens one messenger connection and
135
- every call shares it.
147
+ `mcp serve` serves one named profile (`<cli> <profile> mcp serve`), or the profiles that `--profiles` lists:
148
+ `--profiles work,personal`, or `--profiles all` for every profile on this computer. With neither, it refuses with
149
+ `validation_error` and lists the profiles, like any missing profile ([WC-ACCT-3](accounts.md)). It never serves every
150
+ profile by default. The server opens one messenger connection for each profile it serves, and every call for that
151
+ profile shares it. When it serves more than one profile, each call names its profile; a call without one is refused
152
+ and lists the profiles served. Each call follows its own profile's permissions.
136
153
 
137
- - **Why:** a connection per call would break the messenger's pace limits and look unlike the official client.
154
+ - **Why:** which accounts an agent can reach is the owner's choice, written in the agent's config. A connection per
155
+ call would break the messenger's pace limits and look unlike the official client.
138
156
  - **Enforced by:** review only.
139
- - **Example:** `max work mcp`. Plain `max mcp` stops and lists the profiles.
157
+ - **Example:** `max work mcp serve`, `max mcp serve --profiles work,personal` or `max mcp serve --profiles all`.
158
+ Under the last two, a call reads `max_read { "profile": "work", "command": "inbox", "arguments": {} }`.
159
+ `max mcp serve` alone is refused, and its message lists `personal` and `work`.
140
160
  - **Exceptions:** the bot server needs no held connection: each Bot API call is plain HTTPS.
141
- - **Applies to:** messenger CLIs. **Since:** cli-base 0.41.0. **Rulings:** none.
161
+ - **Applies to:** messenger CLIs. **Since:** cli-base 0.41.0. **Rulings:** NEED-1881.
142
162
 
143
163
  ### WC-MCP-10 · A tool and its command give the same answer and the same error
144
164
 
@@ -170,15 +190,16 @@ it carries.
170
190
 
171
191
  ### WC-MCP-12 · An MCP server over HTTP follows the same permissions and short-lived logins
172
192
 
173
- `mcp --http` serves on `127.0.0.1` for agents in a browser, behind the owner's tunnel. Writes follow the profile's
193
+ `mcp serve --http` serves on `127.0.0.1` for agents in a browser, behind the owner's tunnel. Writes follow the profile's
174
194
  permissions. An access token lives one hour and a refresh token 30 days, replaced on each use. To log in, a browser app
175
- needs a one-time code that the owner reads in a terminal. `--revoke` ends every login.
195
+ needs a one-time code that the owner reads in a terminal. `mcp revoke` ends every login.
176
196
 
177
197
  - **Why:** a server reachable from a browser must not be a softer way in than the local one.
178
198
  - **Enforced by:** review only.
179
- - **Example:** `max work mcp --http --public-url https://<name>.ts.net`.
199
+ - **Example:** `max work mcp serve --http --public-url https://<name>.ts.net`.
180
200
  - **Exceptions:** none.
181
- - **Applies to:** messenger CLIs. **Since:** cli-base 0.41.0. **Rulings:** NEED-594, NEED-774.
201
+ - **Applies to:** every CLI that offers `mcp serve --http`: the messenger CLIs, cli-template and every CLI made from
202
+ it. **Since:** cli-base 0.41.0. **Rulings:** NEED-594, NEED-774.
182
203
 
183
204
  ### WC-MCP-13 · A tool's hints and descriptions never stand in for the permission check
184
205
 
@@ -213,8 +234,11 @@ cli-core builds its personal and bot MCP catalogues from the command tree with `
213
234
  - **One server per subtree.** The personal server is built from the program without its `bot` group; the bot server
214
235
  from the `bot` group. Both keep three tools (WC-MCP-1).
215
236
  - **The profile still decides at run time** what is listed (WC-MCP-6).
216
- - **The whole catalogue is pinned** in each CLI's `contracts/mcp-tools.json`, so a command that becomes visible to
217
- agents shows up as a diff in its pull request.
237
+ - **Two snapshots pin what agents see.** `contracts/mcp-tools.json` pins the tools themselves: with three tools
238
+ (WC-MCP-1) that is three names and their schemas, which do not change when a command is added. So each CLI also
239
+ pins the command catalogue behind the tools, every command `tools_search` can return and whether it runs through
240
+ `read` or `write`, in `contracts/mcp-catalogue.json`. A command that becomes visible to agents then shows up as a diff in
241
+ its pull request. tg and max keep both; wa, zm, gcal and drive pin only the tools, a known gap as of 2026-10-27.
218
242
 
219
243
  ## Sources
220
244
 
@@ -12,7 +12,7 @@ built on are on the [Kit surface](kit-surface.md) page.
12
12
  ## Start with the template
13
13
 
14
14
  ```sh
15
- npx @wirecat/cli-template new cal-cli --command cal --package @wirecat/cal-cli --owner WireCatLabs \
15
+ npx @wirecat/cli-template new gcal-cli --command gcal --package @wirecat/gcal-cli --owner WireCatLabs \
16
16
  --category calendar --content-lang en,ru --sign-in oauth --api official-client --filters time,person,text \
17
17
  --never-store tokens,secret-links --untrusted names,links
18
18
  ```
@@ -24,7 +24,7 @@ the choices. Nothing is guessed.
24
24
  |---|---|---|
25
25
  | `<name>-cli` | the repository folder | any name |
26
26
  | `--command` | the command word people type | one word |
27
- | `--package` | the npm package name | a scoped name, such as `@wirecat/cal-cli` |
27
+ | `--package` | the npm package name | a scoped name, such as `@wirecat/gcal-cli` |
28
28
  | `--owner` | the GitHub owner of the repository | an owner |
29
29
  | `--category` | what the CLI reads | `messenger`, `mail`, `notes`, `files`, `calendar`, `meetings` |
30
30
  | `--content-lang` | the languages of the content | a list, such as `en,ru` |
@@ -42,17 +42,17 @@ a reference CLI for your category in `AGENTS.md`. It does not install packages,
42
42
  publish. Then:
43
43
 
44
44
  ```sh
45
- cd cal-cli && pnpm install && pnpm ci:quick
45
+ cd gcal-cli && pnpm install && pnpm ci:quick
46
46
  git add -A && git commit -m "chore: start from @wirecat/cli-template"
47
47
  ```
48
48
 
49
- Give the agent `docs/dev/DESIGN.md` next. Every example passes a profile explicitly, such as `cal personal doctor`.
49
+ Give the agent `docs/dev/DESIGN.md` next. Every example passes a profile explicitly, such as `gcal personal doctor`.
50
50
  The reference CLI is the one to compare with when a step is unclear.
51
51
 
52
52
  The copy already holds every file this handbook asks for: the required root commands (`commands`, `doctor`,
53
53
  `skill show`, `skill install`, `mcp`, `upgrade`) and `config` from cli-core, the run log and its `runs` commands
54
- ([WC-LIFE-15](lifecycle.md#wc-life-15--keep-a-run-log-through-cli-core-and-read-it-back-with-runs)), the MCP server with a tool for every
55
- command, writes included, the community presets in `config/wirecat/`, the TypeScript, Biome and vitest setup,
54
+ ([WC-LIFE-15](lifecycle.md#wc-life-15--keep-a-run-log-through-cli-core-and-read-it-back-with-runs)), the MCP server with its three tools
55
+ ([WC-MCP-1](mcp.md#wc-mcp-1--a-server-offers-three-tools-not-one-per-command)) that reach every command, writes included, the community presets in `config/wirecat/`, the TypeScript, Biome and vitest setup,
56
56
  `contracts/`, `lint/`, the test matrix, the user pages, `CHANGELOG.md`, `SECURITY.md`, `bin/release`, and the
57
57
  `pr.yml`, `ci.yml`, `main.yml`, `standards.yml`, `changelog-guard.yml`, `monthly.yml` and `release.yml` workflows.
58
58
  The rules they keep are on the area pages: [Commands](commands.md), [MCP](mcp.md), [Accounts](accounts.md),
@@ -61,6 +61,15 @@ The rules they keep are on the area pages: [Commands](commands.md), [MCP](mcp.md
61
61
  Later template changes come in with `npx @wirecat/cli-template upgrade`, file by file: apply, replace, skip or diff.
62
62
  See [Kit surface](kit-surface.md#how-your-cli-stays-current).
63
63
 
64
+ The template's `template.json` splits the copy in two. A **seed** (`DESIGN.md`, `CLAUDE.md`, `README.md`, the user
65
+ pages, `src/`) is the CLI's own from the first commit: rewrite it freely. A **template-owned** file (`AGENTS.md`,
66
+ `docs/dev/TESTING.md`, `docs/dev/reference-clis.md`, the skills under `docs/dev/skills/`, `.github/`) holds only what
67
+ is true for every CLI made from the template. Do not rewrite one to fit this CLI: put the CLI's own facts in a seed.
68
+ A template-owned page that is wrong for this CLI is a gap in cli-template, under
69
+ [WC-DOC-3](docs.md#wc-doc-3--every-document-states-current-facts-rewrite-a-wrong-sentence-in-place) like any other
70
+ wrong sentence: fix it there, and `upgrade` brings the fix. Change it in the CLI only where the CLI must differ for
71
+ good; `upgrade` then asks before it touches the change.
72
+
64
73
  ## Four steps that need a person
65
74
 
66
75
  `new` prints these at the end.
@@ -69,12 +78,14 @@ See [Kit surface](kit-surface.md#how-your-cli-stays-current).
69
78
  changes only through a pull request, required status checks, and no bypass. Add the ruleset after the first
70
79
  commit is on `main`, since a pull request rule leaves an empty repository no way to get one, and the required
71
80
  check names exist only once CI has run. See [Releases](releases.md).
72
- 2. **Set npm trusted publishing** for the new package, naming `release.yml`, and add any secrets the CI needs. See
81
+ 2. **Set npm trusted publishing** for the new package, naming `release.yml`, and add any secrets the CI needs. npm
82
+ offers this only once the package exists, so the owner publishes the first version with `bin/release --local`
83
+ and sets it right after. See
73
84
  [WC-REL-11](releases.md#wc-rel-11--publish-from-github-actions-with-npm-trusted-publishing).
74
- 3. **Pick a monthly cron minute** between 02:17 and 03:20 UTC that no other repository uses. The copy of
75
- `.github/workflows/monthly.yml` keeps the template's own minute, 03:02, and no check compares minutes across
76
- repositories, so change it by hand. See
85
+ 3. **Pick a monthly cron minute** between 02:17 and 03:20 UTC that is not in the list of taken minutes under
77
86
  [WC-CI-9](ci.md#wc-ci-9--run-full-validation-and-release-security-on-the-first-day-of-each-month-without-publishing).
87
+ The copy of `.github/workflows/monthly.yml` keeps the template's own minute, 03:02, and no check compares minutes
88
+ across repositories, so change it by hand, and add the new minute to the list in the step 4 pull request.
78
89
  4. **WireCat CLIs only: join the shared lists.** A pull request to cli-base's `PARITY_GROUPS` (at least the `all`
79
90
  group, which compares the shared root commands), a line in cli-core's `WIRECAT_TOOLS` and a line in cli-docs'
80
91
  `tools.json`. A messenger also joins cli-core's `parity.json`. A CLI outside WireCatLabs keeps its parity groups
@@ -89,17 +100,20 @@ All of these pass on `main`, before the first release:
89
100
  - `cli-dev lint` against the built CLI. It also makes the CLI fail once in an empty home and checks that
90
101
  `runs list` shows the failed run
91
102
  - `cli-dev audit --for <cli> --lint <cli>=lint` with no new finding, run with the command trees of the CLIs in
92
- the same parity groups. `--lint` judges the CLI's lint rules by its own `lint/` folder. What it shares with the
93
- other CLIs is judged by cli-base's `audit/baseline`; the pull request that adds the CLI to a parity group adds its
94
- shared findings there
103
+ the same parity groups (`pnpm audit:commands`). `--lint` judges the CLI's lint rules by its own `lint/` folder.
104
+ What it shares with the other CLIs has no baseline: `audit/baseline` does not accept a shared-contract finding
105
+ (cli-base 0.51.0). Fix each one, or, where the CLI must differ for good, the pull request that adds it to a parity
106
+ group declares the exact difference with its reason in that group's `differences`
95
107
  - `pnpm contracts:check`
96
108
  - `cli-dev test-matrix --check`
97
109
  - `pnpm docs:check`, with `--pages`, and the CLI's `generate:check`
98
110
  - the changelog guard on a pull request
99
111
  - `pnpm release:check`
100
112
 
101
- The last test is a fresh agent. Give it only this handbook's [index](README.md) and the new repository, and ask it to
102
- audit the CLI. Every gap it lists is either a gap in the CLI, to fix before the first release, or a hole in the
113
+ The last test is a fresh agent. Give it only this handbook's [index](README.md), the new repository and the cli-base
114
+ workflow files its own workflows pin
115
+ ([WC-CI-8](ci.md#wc-ci-8--call-cli-base-node-ciyml-from-ciyml-pinned-to-a-tag-or-commit)), and ask it to audit the
116
+ CLI. Every gap it lists is either a gap in the CLI, to fix before the first release, or a hole in the
103
117
  handbook, to fix here.
104
118
 
105
119
  ## The template repository itself
@@ -118,7 +132,7 @@ upgrade, and no script or agent reads its answers.
118
132
  | [WC-DOC-12](docs.md#wc-doc-12--a-runnable-command-in-a-document-has-no-placeholder-in-it) | Exempt only for the placeholder names `clidev`, `clidev-cli`, `@wirecat/clidev-cli` and `WireCatLabs/clidev-cli`, and the upper-case prefix `CLIDEV_` (as in `CLIDEV_PROFILE`). | `new` replaces each of them with the CLI's own name. Any other placeholder is still a gap. |
119
133
  | [WC-DOC-17](docs.md#wc-doc-17--each-entry-says-what-changed-for-the-caller-why-and-what-to-watch-for) | A changelog entry may name a file path. | The template's readers are CLI authors, who edit those files or take them with `upgrade`; for them a path is what they see. |
120
134
 
121
- Every other rule that applies to all CLIs, or to the ten repositories, applies to cli-template as it is written,
135
+ Every other rule that applies to all CLIs, or to the WireCat repositories, applies to cli-template as it is written,
122
136
  with one limit for the copy tool. It follows the rules for code and the repository: Toolchain, Layout, Style,
123
137
  Testing, Security and Documents. It has no profile, no machine mode, no MCP server, no store and no agent reading its
124
138
  answers, so the rules for a CLI's commands and runtime do not reach it: Commands, Output, MCP, Accounts, Permissions,
@@ -93,7 +93,11 @@ JSON Schema 2020-12.
93
93
  - **Why:** a caller that checks only the exit code must not take half an answer for a whole one.
94
94
  - **Enforced by:** review only.
95
95
  - **Example:** a search across accounts declares `annotate(search, { partial: true })` and exits 0 with
96
- `meta.complete: false`. Any other command prints what it read, then exits with the code of its first issue.
96
+ `meta.complete: false`. Any other command prints the whole envelope of what it read on stdout, with
97
+ `meta.complete: false` and its `issues`, then exits with the code of its first issue. Such an answer is an answer,
98
+ not a failure, so [WC-OUT-14](#wc-out-14--print-a-failure-in-machine-mode-as-one-json-object-on-stderr-with-stdout-empty)
99
+ does not apply to it: a caller reads stdout whenever it is not empty, also after a non-zero exit. cli-core's
100
+ `renderAnswer` does this.
97
101
  - **Exceptions:** none.
98
102
  - **Applies to:** all CLIs. **Since:** cli-base 0.41.0. **Rulings:** none.
99
103
 
@@ -103,7 +107,8 @@ JSON Schema 2020-12.
103
107
  - **Enforced by:** review only.
104
108
  - **Example:** `next` holds the step to take, then one alternative. Each has `kind` (`command`, or `user_action` with
105
109
  no command) and `safe_to_auto_execute`. A step that reads the service and writes only the local store (a sync) is
106
- safe. A step that reads nothing remotely and writes locally (`store reindex`) is not: it waits to be asked. A step
110
+ safe; it counts as a read, also over MCP
111
+ ([WC-CMD-17](commands.md#wc-cmd-17--declare-effects-on-every-command)). A step that reads nothing remotely and writes locally (`store reindex`) is not: it waits to be asked. A step
107
112
  is built from ids, counts and cursors the CLI holds, never from text a message, mail or document carried. A CLI
108
113
  answer's command step also has `line`, the whole command with the profile (`tg personal mcp config`), because
109
114
  `command` is the bare path an MCP router takes and a copied path without the profile fails.
@@ -160,6 +165,12 @@ JSON Schema 2020-12.
160
165
 
161
166
  ### WC-OUT-14 · Print a failure in machine mode as one JSON object on stderr, with stdout empty
162
167
 
168
+ A failure is a run that has no answer: the command threw. A command that answers with what it could read and
169
+ `meta.complete: false` has not failed, even when it exits non-zero; that case is
170
+ [WC-OUT-8](#wc-out-8--exit-0-on-a-partial-answer-only-when-the-command-declares-partial). So a non-zero exit does not
171
+ mean stdout is empty: stdout empty and one JSON object on stderr is a failure; an envelope on stdout is an answer,
172
+ whole or partial.
173
+
163
174
  - **Why:** a caller tells success from failure without parsing text, and stdout never holds half an answer and an
164
175
  error.
165
176
  - **Enforced by:** `src/failure.test.ts` · `cli-core: src/cli/agent-contract.test.ts`.
@@ -20,8 +20,9 @@ Rule ids on this page are `WC-PERM-<n>`. How to read a rule, how ids work and wh
20
20
  tg, max and wa use cli-core's permissions and write guard in full. memo uses them for the shared commands only. drive
21
21
  has its own policy and guard, with its own `--read-only` flag. zoom has per-profile `permissions` from zoom-cli 0.35.0,
22
22
  read with cli-core's `levelFor`; its writes change Zoom meetings, its built-in default is `remote-meetings: ask`, and
23
- it has no send journal or recipient list, since it sends no messages. Where this page says "every CLI that writes",
24
- drive is a known gap.
23
+ it has no send journal or recipient list, since it sends no messages. gcal declares its own keys through cli-core's
24
+ `commandPermissions` and runs its event writes through cli-core's write guard (WC-PERM-22). Where this page says
25
+ "every CLI that writes", drive is a known gap.
25
26
 
26
27
  WC-PERM-10 and WC-PERM-11 need cli-core 0.321.0, which tg, max and wa pin. WC-PERM-12 as written needs cli-core
27
28
  0.337.0; before it, the lock let through some local writes that overwrite what the owner saved. A command other
@@ -114,7 +115,8 @@ A default is never made stricter without the owner's word.
114
115
  - **Exceptions:** where the messenger has replies, `replies.send` is `deny`: a reply rule writes to people with nobody
115
116
  typing a command, so it waits for the owner's own `allow`. A messenger with no `replies` group, such as wa, has no
116
117
  use for the key.
117
- - **Applies to:** messenger CLIs. **Since:** cli-base 0.41.0. **Rulings:** NEED-568.
118
+ - **Applies to:** messenger CLIs. A CLI that is not a messenger sets its own defaults (WC-PERM-22). **Since:** cli-base
119
+ 0.41.0. **Rulings:** NEED-568.
118
120
 
119
121
  ### WC-PERM-7 · A built-in default only ever tightens
120
122
 
@@ -310,6 +312,47 @@ Moderation rules use the same four levels for each kind of action (delete, remov
310
312
  looser than the profile's `chats.moderate`; the code does not check that yet.
311
313
  - **Applies to:** messenger CLIs. **Since:** cli-base 0.41.0. **Rulings:** NEED-306, NEED-310.
312
314
 
315
+ ### WC-PERM-22 · A CLI that is not a messenger declares its own keys and defaults with `commandPermissions`
316
+
317
+ A messenger's keys come from its resources (`messages`, `chats`). Any other CLI names its own with cli-core's
318
+ `commandPermissions({ keyOf, extra, defaults })`, passed as `settingsFor(app, { permissions })` (cli-core 0.450.0):
319
+
320
+ - **Keys.** Each command is checked against its own path (`events.create`), the default `keyOf`. Housekeeping has no
321
+ key (WC-PERM-5). `extra` adds the keys a command checks inside itself, such as one per generated API method
322
+ (`api.calendar.events.insert`). A key that names no command is refused when it is set or saved.
323
+ - **Defaults.** `defaults` holds the CLI's built-in levels, beside cli-core's. They only ever tighten (WC-PERM-7).
324
+ Like WC-PERM-6 for the messengers, and as tg and drive do: an ordinary change is `allow`; a delete is `ask`; a
325
+ write that mails or notifies people other than the owner, or deletes a whole series from their calendars, is a key
326
+ of its own, `deny` until the owner allows it.
327
+ Sending a message is not this kind of write; a messenger CLI keeps WC-PERM-6.
328
+ - **Shared keys on every path.** A key a shared command checks inside itself, such as `events.notify` or
329
+ `events.delete.series`, is checked by every path that can do the same thing, the CLI's own generic API commands
330
+ included. A gcal `api` call that deletes an event with `sendUpdates: "all"` checks `events.notify` as well as
331
+ its own `api.calendar.events.delete`; otherwise the generic path is a way round the shared key.
332
+ - **Read-only lock and guard.** `<PREFIX>_READ_ONLY` refuses every write to the service, as in WC-PERM-12. Every
333
+ write runs through cli-core's write guard with `permissionDefaults: configuration.permissions.defaults`
334
+ (cli-core 0.455.0), so `ask` is a y/N question at a terminal (WC-PERM-9) and every attempt goes into the send journal
335
+ (WC-PERM-17). A CLI that sends no messages has no recipient list or hourly limit to set.
336
+
337
+ - **Why:** one permission model for every CLI; a CLI-own layer drifts from the shared one and is audited by nobody.
338
+ - **Enforced by:** `cli-core: src/cli/command-permissions.test.ts` · `cli-core: src/calendar/cli/writes.test.ts`.
339
+ - **Example:** a calendar CLI takes cli-core's `CALENDAR_WRITE_DEFAULTS` and `CALENDAR_WRITE_KEYS`
340
+ (`@wirecat/cli-core/calendar`, cli-core 0.458.0). By this rule `events create`, `update` and `respond` are `allow`
341
+ and `events delete` is `ask`. `events.delete.series` (every occurrence of a repeating series, in every attendee's
342
+ calendar) and `events.notify` (the provider mails the attendees) are `deny` until the owner allows them: over MCP
343
+ `ask` runs without a question, and each reaches people other than the owner. gcal adds its own keys for its generated
344
+ `api <method>` commands: sharing a calendar is `ask` (NEED-1857), and emptying the main calendar
345
+ (`api.calendar.calendars.clear`) is `deny`.
346
+ - **Exceptions:** known gaps, as of 2026-10-27:
347
+ - cli-core's `CALENDAR_WRITE_DEFAULTS` (`src/calendar/writer.ts`) still has `events create`, `update` and
348
+ `respond` at `ask`.
349
+ - gcal's generated `api` commands default every write to `ask`, ordinary changes included, and check neither
350
+ `events.notify` nor `events.delete.series`.
351
+ - drive keeps its own policy and `--read-only` flag, zoom reads its own `remote-meetings` key with `levelFor`, and
352
+ memo uses cli-core's permissions for the shared commands only. None of them declares `commandPermissions` yet.
353
+ - **Applies to:** every CLI that writes and is not a messenger. **Since:** cli-base 0.64.0 (the levels and the shared
354
+ keys on every path: cli-base 0.65.0). **Rulings:** NEED-1857, NEED-1883.
355
+
313
356
  ## Sources
314
357
 
315
358
  These documents explain the rules on this page in more detail. Where one of them disagrees with this page, this page
@@ -125,7 +125,11 @@ on the [CI](ci.md) page.
125
125
  - **Example:** `bin/release` starts `release.yml` and follows it. Only the `publish` job has the OIDC permission (the
126
126
  short-lived identity npm trusts), and it runs no project code: it only runs `npm publish` on the validated file.
127
127
  - **Exceptions:** npm trusts the workflow by file name: renaming `release.yml` stops publishing until npm's trusted
128
- publisher settings are updated.
128
+ publisher settings are updated. npm sets a trusted publisher on the package's settings page
129
+ ([npm docs](https://docs.npmjs.com/trusted-publishers)), which a package has only once it is published, so its
130
+ first version is published with `bin/release --local` by the owner, from a clean `main`. Right after it, the owner
131
+ adds the trusted publisher on npmjs.com (step 2 of [New CLI](new-cli.md#four-steps-that-need-a-person)), and every
132
+ later version goes through `release.yml`.
129
133
  - **Applies to:** all CLIs and shared libraries. **Since:** cli-base 0.41.0. **Rulings:** none.
130
134
 
131
135
  ### WC-REL-12 · Publish only after full validation and the release security checks pass
@@ -181,12 +185,24 @@ on the [CI](ci.md) page.
181
185
 
182
186
  ### WC-REL-16 · The package ships only the files it names
183
187
 
188
+ A CLI's `files` holds only what the installed CLI runs from or shows its user: `dist`, the agent skill (`skills`),
189
+ `README.md`, `LICENSE`, and files its code reads or runs after install, such as `scenarios` for `guide`, an `install`
190
+ script, third-party notices and licenses. `CHANGELOG.md` may ship too. It never holds `src/`, tests, fakes or
191
+ fixtures, `scripts/`, `.github/`, `bin/release`, `config/`, `lint/`, `contracts/`, `docs/dev/` or `template.json`.
192
+
184
193
  - **Why:** a stray file in the package can leak local data and grows every install.
185
194
  - **Enforced by:** `node-ci.yml:check` · `<app>: pnpm package:check` · `whatsapp-cli: pnpm release:check`.
186
- - **Example:** `package.json` `files` lists what ships. The CLI's `pnpm release:check` runs `packContents` from
187
- `@wirecat/cli-base/release` and fails on any other path in `npm pack --dry-run`.
188
- - **Exceptions:** none. Each CLI's `package:check` runs `packContents`; tg and max as
189
- `release-check.ts --package-only`.
195
+ - **Example:** `package.json` `files` lists what ships. `packContents(root, allowed, said)` from
196
+ `@wirecat/cli-base/release` fails on any path in `npm pack --dry-run` outside `allowed`. It checks the pack against
197
+ the list it is given, not against this rule: a CLI that passes its own `files` list as `allowed`, as the template's
198
+ `scripts/smoke.ts` does, checks only that npm packs what `files` says, and cannot fail on a wrong `files` list.
199
+ `npm pack --dry-run` in gcal-cli (2026-10-27) packs 256 files, `src/`, `scripts/`, `.github/`, `config/`,
200
+ `contracts/`, `lint/` and `template.json` among them, because its `files` list names them; its check cannot see it. The check this rule needs
201
+ refuses the forbidden paths above whatever `files` says; it belongs in cli-template, so every CLI gets it by
202
+ `upgrade` ([cli-template#77](https://github.com/WireCatLabs/cli-template/issues/77)).
203
+ - **Exceptions:** cli-template's own package ships the skeleton's sources and configs, since `new` copies them; the
204
+ `files` list `new` writes into a CLI follows this rule. Each CLI's `package:check` runs `packContents`; tg and max
205
+ as `release-check.ts --package-only`.
190
206
  - **Applies to:** all CLIs. **Since:** cli-base 0.41.0. **Rulings:** none.
191
207
 
192
208
  ## Sources
@@ -67,8 +67,9 @@ name. Any other top-level key is written as `[not allowed]`. A key that names co
67
67
  ### WC-SECR-4 · Secrets come from the environment, then the OS keyring, then a private file
68
68
 
69
69
  cli-base's `Credentials` reads the environment variable first (for CI), then the OS keyring, then a file with mode
70
- `0600`. The keyring service is the command's name. When the config directory comes from the environment, the service
71
- name is scoped to that directory (`isolated: true`).
70
+ `0600`. The keyring service is the CLI's app name, `appName` in its identity (`max-cli`, `gcal-cli`, `cli-memo`), not
71
+ the command word people type (`max`, `gcal`). When the config directory comes from the environment, the service name
72
+ is scoped to that directory (`isolated: true`).
72
73
 
73
74
  - **Why:** the keyring is the safest store on the computer, and a throwaway config directory must not overwrite the
74
75
  owner's real keyring entry.
@@ -274,7 +275,7 @@ general words.
274
275
  asks for it on that job only. The `zizmor` scan in `release-checks.yml` reports high findings at release, but does
275
276
  not fail on them.
276
277
  - **Exceptions:** none.
277
- - **Applies to:** the ten repositories: the six CLIs, cli-base, cli-core, cli-testing, cli-docs and community; and
278
+ - **Applies to:** the WireCat repositories: every CLI, cli-base, cli-core, cli-testing, cli-docs and community; and
278
279
  cli-template.
279
280
  **Since:** cli-base 0.41.0. **Rulings:** none.
280
281
 
@@ -309,16 +310,23 @@ store and runs no search. A CLI declares its provider and may replace any step.
309
310
  printed. A CLI that signs in another way (a bot token, a session) does not use the module.
310
311
  - **Applies to:** every CLI that signs in with OAuth. **Since:** cli-base 0.57.1. **Rulings:** NEED-1734.
311
312
 
312
- ### WC-SECR-23 · Meeting passcodes and the query of a join link never reach the store
313
+ ### WC-SECR-23 · Meeting passcodes and the query of a join link never reach the store, the output or an agent
313
314
 
314
- - **Why:** a passcode, PIN or join-link query lets anyone who reads the store join the meeting.
315
+ - **Why:** a passcode, PIN or join-link query lets anyone who reads the store, a terminal log or an agent's
316
+ transcript join the meeting.
315
317
  - **Enforced by:** `cli-core: src/store/calendar.test.ts`.
316
318
  - **Example:** a calendar entry's description loses each line that carries a passcode, password or PIN, and every
317
319
  link loses its query string, so `join_url` holds `https://zoom.us/j/123` without `?pwd=`. A meeting is matched by
318
320
  its room (`join_key`, such as `zoom:<number>`), never by a link that carries the passcode.
319
- - **Exceptions:** a title that names a PIN or a password is kept: it is the meeting's name.
320
- - **Applies to:** cli-core and every CLI that stores meetings or calendar entries. **Since:** cli-base 0.57.1.
321
- **Rulings:** none (Wave 3 requirements 3.4).
321
+ The same holds for what a command prints and what an MCP tool answers, also when the answer comes straight from
322
+ the provider and never touched the store: a generic API command removes the provider's passcode fields (Google's
323
+ `conferenceData.entryPoints[].passcode`, `pin`, `accessCode`, `password`), the query of every join link and the
324
+ dial sequence after a phone number, and the passcode lines of a description or a location.
325
+ - **Exceptions:** a title that names a PIN or a password is kept: it is the meeting's name. Known gap, as of
326
+ 2026-10-27: gcal's `api` answers drop the conference entry points' secrets (`src/conference.ts`) but keep the
327
+ description and location as Google sent them.
328
+ - **Applies to:** cli-core and every CLI that stores or shows meetings or calendar entries. **Since:** cli-base 0.57.1
329
+ (output and agent answers: cli-base 0.65.0). **Rulings:** none (Wave 3 requirements 3.4).
322
330
 
323
331
  ## Sources
324
332
 
@@ -30,7 +30,7 @@ Related pages: which account a read covers, and how a read across accounts is as
30
30
  `%LOCALAPPDATA%\WireCat\Data\` on Windows). Rows are keyed by provider and account. `storePath` is the only code
31
31
  that reads `MESSAGING_STORE` and `WIRECAT_STATE_DIR`.
32
32
  - **Exceptions:** none. Older documents that name `messages.db` describe a store that is no longer used.
33
- - **Applies to:** CLIs that keep local data through cli-core (tg, max, wa, memo, drive, zoom). **Since:** cli-base 0.41.0.
33
+ - **Applies to:** CLIs that keep local data through cli-core (tg, max, wa, memo, drive, zoom, gcal). **Since:** cli-base 0.41.0.
34
34
  **Rulings:** NEED-383.
35
35
 
36
36
  ### WC-STORE-2 · Reach the data through its area, and add a new kind of record as a new area
@@ -52,8 +52,9 @@ Related pages: which account a read covers, and how a read across accounts is as
52
52
  accounts on purpose asks for the whole store with `crossAccount(deps, reason)`; see [Accounts](accounts.md).
53
53
  - **Exceptions:** `forAccount` binds only the account areas (accounts, chats, sync, messages, attachments, roster,
54
54
  people, conversations, vectors, counters, tags, mentions). The `chats`, `conversations` and `attachments` services
55
- use the bound store; the other services still pair `deps.store()` with `deps.account()`. New code uses the bound
56
- store.
55
+ use the bound store; the other services still pair `deps.store()` with `deps.account()`. The calendar areas
56
+ (calendars, calendar entries, their sync cursors) are not bound either: cli-core's calendar commands pass
57
+ `accountId` to every call, a known gap as of 2026-10-27. New code uses the bound store.
57
58
  - **Applies to:** cli-core and every CLI that adds a service. **Since:** cli-base 0.41.0. **Rulings:** none.
58
59
 
59
60
  ### WC-STORE-4 · Reach the store through its public module, never a table module
@@ -44,7 +44,7 @@ and which run after merge is in [CI](ci.md).
44
44
  file, listed after the shared one, adds only what is its own, such as a default profile.
45
45
  - **Exceptions:** keep coverage reports and debug traces outside the sandbox, with bounded retention. Never clean
46
46
  another session's scratch directory as test teardown.
47
- - **Applies to:** all CLIs and the shared packages. cli-base, cli-core, the six CLIs and cli-template load the shared
47
+ - **Applies to:** all CLIs and the shared packages. cli-base, cli-core, every CLI and cli-template load the shared
48
48
  setup first in `setupFiles`; cli-testing does not use it yet. **Since:** cli-base 0.41.0. **Rulings:** none.
49
49
 
50
50
  ### WC-TEST-2 · No test opens a real credential store
@@ -94,7 +94,9 @@ and which run after merge is in [CI](ci.md).
94
94
  - **Enforced by:** review only.
95
95
  - **Example:** waits go through an injected `SleepLike`, and `fakeClock()` from `@wirecat/cli-base/testing` records
96
96
  them without waiting. A test harness passes `sleep: async () => {}`. `pnpm test:slow` lists the 20 slowest tests
97
- and the 10 slowest files.
97
+ and the 10 slowest files. A test may wait for a local process or server it started, for example polling its
98
+ status with `vi.waitFor` and a timeout shorter than the test's own; that waits for an event, not for time. A
99
+ fixed sleep or `setTimeout` is still a wait for real time.
98
100
  - **Exceptions:** none. A new wait in the code gets a seam, never a longer test timeout.
99
101
  - **Applies to:** all CLIs and the shared packages. **Since:** cli-base 0.41.0. **Rulings:** dev-process:4.
100
102
 
@@ -169,9 +171,14 @@ and which run after merge is in [CI](ci.md).
169
171
  - **Enforced by:** `node-ci.yml:check` · `<app>: pnpm test:matrix:check`.
170
172
  - **Example:** under vitest, `run()` appends each parsed command path and option name (never a value) to
171
173
  `coverage/argv.jsonl`. `pnpm test:matrix` turns that into `docs/dev/test-matrix.md` and fails on a command or
172
- option with neither a test nor a reason. A reason names the live scenario that covers it. A test that calls a
173
- function directly does not count.
174
- - **Exceptions:** none.
174
+ option with neither a test nor a reason. A test counts only when it runs the command through `run()`: a test that
175
+ calls a function directly records no command line, so the matrix does not see it. A reason says what covers the
176
+ command instead: the live scenario that runs it, or, for a generated group, the two tests below.
177
+ - **Exceptions:** commands generated from a service's API description, such as drive's and gcal's
178
+ `api <method>` from a Google Discovery snapshot, share one reason for the whole group (`api *` and its options).
179
+ It names two tests: one that checks every generated command's options against the snapshot, and one that drives a
180
+ generated command through `run()` into the executor every generated command shares. A hand-written command in the
181
+ same CLI still needs its own test or live scenario.
175
182
  - **Applies to:** all CLIs. memo's check fails only on a stale page, not yet on an option with no test.
176
183
  **Since:** cli-base 0.41.0. **Rulings:** NEED-359.
177
184
 
@@ -192,9 +199,13 @@ and which run after merge is in [CI](ci.md).
192
199
  - **Enforced by:** `node-ci.yml:check` · `<app>: pnpm contracts:upgrade`.
193
200
  - **Example:** `pnpm contracts:upgrade` runs the previous published package and this build on the same synthetic
194
201
  state and checks that the data survives. Each store migration also has its own upgrade test (WC-STORE-9).
195
- - **Exceptions:** a CLI with no local state, or with no published version yet. Local state is data the CLI's own
196
- code writes and reads back in a later run, such as a store, a session or a cache. A config file and run records
197
- that only cli-core writes do not count.
202
+ - **Exceptions:** a CLI with no local state, or with no published version yet. Local state is data the CLI writes
203
+ and reads back in a later run, such as a store, a session or a cache. The shared store counts, also when cli-core's
204
+ code writes it, because the CLI's release decides which cli-core and so which migrations run: gcal's calendars and
205
+ events in the store are its local state. A config file, run records, the
206
+ update check, MCP logins and lock files that only cli-core writes do not count. Tests still keep them in the
207
+ sandboxed home, like every other path
208
+ ([WC-TEST-1](#wc-test-1--point-every-path-a-test-could-write-at-a-temporary-directory)).
198
209
  - **Applies to:** all CLIs with local state. wa has no upgrade contract yet. **Since:** cli-base 0.41.0. **Rulings:** none.
199
210
 
200
211
  ### WC-TEST-15 · A refactor keeps `commands --json` and the MCP tool list as they were
@@ -76,7 +76,14 @@ local midnight. The answer gives the window it used, in UTC.
76
76
  - **Why:** "messages of 1 September" means the owner's day, not the UTC day, which starts hours earlier or later.
77
77
  - **Enforced by:** review only.
78
78
  - **Example:** in `Asia/Vladivostok`, `--since-time 2026-09-01` gives `meta.window.since` = `2026-08-31T14:00:00.000Z`.
79
- - **Exceptions:** none.
79
+ A thing that has only dates, such as an all-day event (`start_date` 2026-09-01, `end_date` 2026-09-02, the end
80
+ not included), matches by comparing local dates, never UTC moments: it is in the window when its dates overlap the
81
+ local dates the window covers. So in `Europe/Madrid` a filter for 1 September returns the all-day event of
82
+ 1 September and not the one of 31 August, whatever zone the event's calendar is in.
83
+ - **Exceptions:** none. Known gap, as of 2026-10-27, read from the code and not run: cli-core's event filter
84
+ (`src/store/sqlite/calendar.ts`, the `events` query) compares `starts_at` and `ends_at` with the UTC window for
85
+ all-day events too, and gcal stores an all-day event's start as midnight in its calendar's zone. The two agree
86
+ when the calendar's zone is the zone in use, and miss by a day at the edges when it is not.
80
87
  - **Applies to:** all CLIs. **Since:** cli-base 0.48.0. **Rulings:** none.
81
88
 
82
89
  ### WC-TIME-6 · Run the tests in a zone that is not UTC, and prove stored values do not depend on it