@wirecat/cli-base 0.66.1 → 0.67.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,48 @@ 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.67.0 — 31.10.2026
12
+
13
+ ### Added
14
+
15
+ - **`page.total`: how many match, every page together** (WC-OUT-5). An optional `{ value, relation }`, shaped as
16
+ Elasticsearch's `hits.total`: `eq` is the exact count, `gte` a lower bound. A search answer can now say "20 of 144"
17
+ without the caller paging to the end. `totalSchema` and the `Total` type are exported from
18
+ `@wirecat/cli-base/standards`. Optional, so every existing answer stays valid.
19
+
20
+ ### Fixed
21
+
22
+ - **WC-LIFE-15 says a success is kept by default.** It described `--record` as the way to keep a success and said
23
+ `record: false` also drops failures. cli-core keeps every run of a command that works through it; `record: false`
24
+ keeps failures only, and `--no-record` keeps nothing.
25
+
26
+ - **`Credentials.remove` in a moved run says "kept" only when the owner's entry is there and was kept.** It printed
27
+ "kept X in the OS keyring: it is the owner's sign-in" on every removal in a moved run, also when it had just removed
28
+ the run's own temporary entry and when the owner had no entry at all. Now it looks the owner's entry up after the
29
+ removal: with the temporary entry removed it says so and that the owner's was kept; with nothing removed it says the
30
+ owner's was kept; with no owner's entry it prints nothing. A keyring that fails on that look-up does not fail the
31
+ removal. The return value is unchanged (WC-SECR-24).
32
+ - **WC-SECR-24 says how to hide the keyring from a run.** Set `DBUS_SESSION_BUS_ADDRESS=disabled:`. Removing the
33
+ variable, as the rule said before, still reaches the keyring through the default session bus.
34
+ - **WC-TOOL-6 counts cli-template's Node 22 job.** cli-template 0.11.0 runs its tests on Node 22.16.0, and a CLI made
35
+ from it gets the job with `upgrade`; only CLIs made from an older template still lack it.
36
+ - **Five rules answer what cli-template's 0.10.0 audit found unclear.** WC-SECR-20 names the release `tag` job,
37
+ which keeps its credentials to push the tag. WC-TOOL-6 lists cli-template and its CLIs among those that do not run
38
+ Node 22 yet. WC-PERM-22 says a template-made CLI calls `commandPermissions` with its first command that writes to a
39
+ service. WC-REL-11 covers ClawHub's stored `CLAWHUB_TOKEN` and a ClawHub failure after the tag. WC-REL-16 says
40
+ where the forbidden-path check lives now, and that `upgrade` does not deliver it.
41
+ - **The handbook catches up with cli-core 0.491.0 and gcal-cli 0.4.0.** WC-CMD-17 and WC-MCP-4 no longer list
42
+ `sync` and `calendars list` as running through `write`: cli-core's `fetchesOnly` sends them through `read`.
43
+ WC-PERM-22 no longer says `CALENDAR_WRITE_DEFAULTS` asks before `events create`, `update` and `respond`; those and
44
+ `move` are `allow`. "Tools generated from commands" (mcp.md) counts gcal among the CLIs that pin
45
+ `contracts/mcp-catalogue.json`.
46
+ - **WC-PERM-6 lists all of cli-core's built-in defaults.** It showed the 8 messenger keys; `DEFAULT_PERMISSIONS` has
47
+ 25, including `mcp.authority: deny` and `ask` for `store.*`, `server.*`, `models.download` and `schedule.*`.
48
+ - **WC-CMD-23 exempts generated commands,** as the naming and example rules do: their help is the API description's
49
+ own text.
50
+ - **gcal-cli is named where it meets a rule:** no correction marks on user pages, `rumdl` on user pages, pull
51
+ request validation, required checks on `main`, the free version number and `bin/release --local`.
52
+
11
53
  ## 0.66.1 — 31.10.2026
12
54
 
13
55
  ### Fixed
@@ -83,9 +83,12 @@ export class Credentials {
83
83
  if (this.#tryKeyring(() => this.#keyring.delete(this.#temporaryService ?? this.#service, account))) {
84
84
  removed.push("keyring");
85
85
  }
86
- if (this.#temporaryService) {
87
- this.#warn(`kept ${account} in the OS keyring: it is the owner's sign-in, shared with every run, and a run with moved ` +
88
- "folders never removes it");
86
+ if (this.#temporaryService && this.#ownersEntryExists(account)) {
87
+ this.#warn(removed.includes("keyring")
88
+ ? `removed this run's own sign-in of ${account} from the OS keyring and kept the owner's: a run with ` +
89
+ "moved folders never removes it"
90
+ : `kept ${account} in the OS keyring: it is the owner's sign-in, shared with every run, and a run with ` +
91
+ "moved folders never removes it");
89
92
  }
90
93
  }
91
94
  const store = this.#readFile();
@@ -96,6 +99,15 @@ export class Credentials {
96
99
  }
97
100
  return removed;
98
101
  }
102
+ /** Only for the warning: a keyring that fails here must not fail a removal that already happened. */
103
+ #ownersEntryExists(account) {
104
+ try {
105
+ return this.#keyring.get(this.#service, account) !== null;
106
+ }
107
+ catch {
108
+ return false;
109
+ }
110
+ }
99
111
  #path() {
100
112
  return join(this.#configDir, this.#fileName);
101
113
  }
@@ -1,6 +1,6 @@
1
1
  export { EFFECT_LEVELS, type EffectLevel, type Effects, effectsSchema, writes } from "./effects.js";
2
2
  export { BASE_REASONS, COMMON_DETAILS, CRASH_CODE, CRASH_REASON, defineReasons, type ErrorObject, type ErrorReason, errorObjectSchema, } from "./errors.js";
3
3
  export { defineLogEvents, isContentKey, LOG_ATTRIBUTE_KEYS, LOG_EVENTS, LOG_SEVERITIES, type LogEvent, type LogRecord, type LogSeverity, logAttributesJsonSchema, logEventProblems, logRecordSchema, traceIdOf, } from "./log.js";
4
- export { type Issue, type ItemEnvelope, isoTime, issueSchema, itemEnvelopeSchema, jsonlEndSchema, jsonlItemSchema, jsonlOf, type ListEnvelope, listEnvelopeSchema, MAX_NEXT_STEPS, type Meta, type Missing, metaSchema, missingSchema, type NextStep, nextStepSchema, type Page, pageSchema, reference, } from "./output.js";
4
+ export { type Issue, type ItemEnvelope, isoTime, issueSchema, itemEnvelopeSchema, jsonlEndSchema, jsonlItemSchema, jsonlOf, type ListEnvelope, listEnvelopeSchema, MAX_NEXT_STEPS, type Meta, type Missing, metaSchema, missingSchema, type NextStep, nextStepSchema, type Page, pageSchema, reference, type Total, totalSchema, } from "./output.js";
5
5
  export { fill, jsonSchemaOf, RegistryError } from "./registry.js";
6
6
  export { defineSteps, SAME_COMMAND, STEPS, type StepDefinition, type StepInput, type StepTarget, stepOf, } from "./steps.js";
@@ -1,6 +1,6 @@
1
1
  export { EFFECT_LEVELS, effectsSchema, writes } from "./effects.js";
2
2
  export { BASE_REASONS, COMMON_DETAILS, CRASH_CODE, CRASH_REASON, defineReasons, errorObjectSchema, } from "./errors.js";
3
3
  export { defineLogEvents, isContentKey, LOG_ATTRIBUTE_KEYS, LOG_EVENTS, LOG_SEVERITIES, logAttributesJsonSchema, logEventProblems, logRecordSchema, traceIdOf, } from "./log.js";
4
- export { isoTime, issueSchema, itemEnvelopeSchema, jsonlEndSchema, jsonlItemSchema, jsonlOf, listEnvelopeSchema, MAX_NEXT_STEPS, metaSchema, missingSchema, nextStepSchema, pageSchema, reference, } from "./output.js";
4
+ export { isoTime, issueSchema, itemEnvelopeSchema, jsonlEndSchema, jsonlItemSchema, jsonlOf, listEnvelopeSchema, MAX_NEXT_STEPS, metaSchema, missingSchema, nextStepSchema, pageSchema, reference, totalSchema, } from "./output.js";
5
5
  export { fill, jsonSchemaOf, RegistryError } from "./registry.js";
6
6
  export { defineSteps, SAME_COMMAND, STEPS, stepOf, } from "./steps.js";
@@ -30,18 +30,35 @@ export declare const missingSchema: v.StrictObjectSchema<{
30
30
  /** Left out when there is nothing to run that would fill the field. */
31
31
  readonly step: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "dotted lower-case words, such as `page.next`">]>, undefined>;
32
32
  }, undefined>;
33
+ /**
34
+ * How many match, every page together, as Elasticsearch's `hits.total`: `eq` is the exact count, `gte` a lower bound
35
+ * (counting stopped at a cap, or could not be done cheaply).
36
+ */
37
+ export declare const totalSchema: v.StrictObjectSchema<{
38
+ readonly value: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
39
+ readonly relation: v.PicklistSchema<["eq", "gte"], undefined>;
40
+ }, undefined>;
33
41
  /** `has_more`: another page exists. A cursor source never pretends to have page numbers. */
34
42
  export declare const pageSchema: v.VariantSchema<"mode", [v.StrictObjectSchema<{
35
43
  readonly mode: v.LiteralSchema<"page", undefined>;
36
44
  readonly limit: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 1, undefined>]>;
37
45
  readonly has_more: v.BooleanSchema<undefined>;
38
46
  readonly next_page: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 2, undefined>]>, undefined>;
47
+ readonly total: v.OptionalSchema<v.StrictObjectSchema<{
48
+ readonly value: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
49
+ readonly relation: v.PicklistSchema<["eq", "gte"], undefined>;
50
+ }, undefined>, undefined>;
39
51
  }, undefined>, v.StrictObjectSchema<{
40
52
  readonly mode: v.LiteralSchema<"cursor", undefined>;
41
53
  readonly limit: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 1, undefined>]>;
42
54
  readonly has_more: v.BooleanSchema<undefined>;
43
55
  readonly next_cursor: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "must not be empty">]>, undefined>;
56
+ readonly total: v.OptionalSchema<v.StrictObjectSchema<{
57
+ readonly value: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
58
+ readonly relation: v.PicklistSchema<["eq", "gte"], undefined>;
59
+ }, undefined>, undefined>;
44
60
  }, undefined>], undefined>;
61
+ export type Total = v.InferOutput<typeof totalSchema>;
45
62
  /** A source that failed or was skipped: the answer holds what the others gave. */
46
63
  export declare const issueSchema: v.StrictObjectSchema<{
47
64
  readonly code: v.PicklistSchema<readonly ["validation_error", "configuration_error", "authentication_error", "permission_error", "not_found", "confirmation_required", "rate_limited", "timeout", "network_error", "provider_error", "provider_unavailable", "invalid_response", "outcome_unknown", "cancelled"], undefined>;
@@ -78,11 +95,19 @@ export declare const listEnvelopeSchema: <T extends v.GenericSchema>(item: T) =>
78
95
  readonly limit: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 1, undefined>]>;
79
96
  readonly has_more: v.BooleanSchema<undefined>;
80
97
  readonly next_page: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 2, undefined>]>, undefined>;
98
+ readonly total: v.OptionalSchema<v.StrictObjectSchema<{
99
+ readonly value: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
100
+ readonly relation: v.PicklistSchema<["eq", "gte"], undefined>;
101
+ }, undefined>, undefined>;
81
102
  }, undefined>, v.StrictObjectSchema<{
82
103
  readonly mode: v.LiteralSchema<"cursor", undefined>;
83
104
  readonly limit: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 1, undefined>]>;
84
105
  readonly has_more: v.BooleanSchema<undefined>;
85
106
  readonly next_cursor: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "must not be empty">]>, undefined>;
107
+ readonly total: v.OptionalSchema<v.StrictObjectSchema<{
108
+ readonly value: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
109
+ readonly relation: v.PicklistSchema<["eq", "gte"], undefined>;
110
+ }, undefined>, undefined>;
86
111
  }, undefined>], undefined>;
87
112
  readonly next: v.OptionalSchema<v.SchemaWithPipe<readonly [v.ArraySchema<v.VariantSchema<"kind", [v.StrictObjectSchema<{
88
113
  readonly kind: v.LiteralSchema<"command", undefined>;
@@ -243,11 +268,19 @@ export declare const jsonlEndSchema: v.StrictObjectSchema<{
243
268
  readonly limit: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 1, undefined>]>;
244
269
  readonly has_more: v.BooleanSchema<undefined>;
245
270
  readonly next_page: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 2, undefined>]>, undefined>;
271
+ readonly total: v.OptionalSchema<v.StrictObjectSchema<{
272
+ readonly value: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
273
+ readonly relation: v.PicklistSchema<["eq", "gte"], undefined>;
274
+ }, undefined>, undefined>;
246
275
  }, undefined>, v.StrictObjectSchema<{
247
276
  readonly mode: v.LiteralSchema<"cursor", undefined>;
248
277
  readonly limit: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 1, undefined>]>;
249
278
  readonly has_more: v.BooleanSchema<undefined>;
250
279
  readonly next_cursor: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "must not be empty">]>, undefined>;
280
+ readonly total: v.OptionalSchema<v.StrictObjectSchema<{
281
+ readonly value: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>]>;
282
+ readonly relation: v.PicklistSchema<["eq", "gte"], undefined>;
283
+ }, undefined>, undefined>;
251
284
  }, undefined>], undefined>;
252
285
  }, undefined>;
253
286
  /** A list answer as JSON lines, the end line last. */
@@ -37,6 +37,14 @@ export const missingSchema = v.strictObject({
37
37
  step: v.optional(dottedId),
38
38
  });
39
39
  const limit = v.pipe(v.number(), v.integer(), v.minValue(1));
40
+ /**
41
+ * How many match, every page together, as Elasticsearch's `hits.total`: `eq` is the exact count, `gte` a lower bound
42
+ * (counting stopped at a cap, or could not be done cheaply).
43
+ */
44
+ export const totalSchema = v.strictObject({
45
+ value: v.pipe(v.number(), v.integer(), v.minValue(0)),
46
+ relation: v.picklist(["eq", "gte"]),
47
+ });
40
48
  /** `has_more`: another page exists. A cursor source never pretends to have page numbers. */
41
49
  export const pageSchema = v.variant("mode", [
42
50
  v.strictObject({
@@ -44,8 +52,15 @@ export const pageSchema = v.variant("mode", [
44
52
  limit,
45
53
  has_more: v.boolean(),
46
54
  next_page: v.optional(v.pipe(v.number(), v.integer(), v.minValue(2))),
55
+ total: v.optional(totalSchema),
56
+ }),
57
+ v.strictObject({
58
+ mode: v.literal("cursor"),
59
+ limit,
60
+ has_more: v.boolean(),
61
+ next_cursor: v.optional(text),
62
+ total: v.optional(totalSchema),
47
63
  }),
48
- v.strictObject({ mode: v.literal("cursor"), limit, has_more: v.boolean(), next_cursor: v.optional(text) }),
49
64
  ]);
50
65
  /** A source that failed or was skipped: the answer holds what the others gave. */
51
66
  export const issueSchema = v.strictObject({
@@ -98,7 +98,7 @@ plus community itself. cli-template follows every rule here as a CLI does
98
98
  cli-docs validates its site before each deployment instead, and community has no build. Third-party security apps
99
99
  may post their own checks.
100
100
  - **Applies to:** the CLIs, cli-base, cli-core and cli-testing. cli-base, whatsapp-cli, tg-cli, max-cli, cli-core,
101
- cli-memo, zoom-cli, drive-cli and cli-template have `pr.yml`; cli-testing does not have it yet. **Since:**
101
+ cli-memo, zoom-cli, drive-cli, gcal-cli and cli-template have `pr.yml`; cli-testing does not have it yet. **Since:**
102
102
  cli-base 0.41.0 (pull request validation since cli-base 0.45.0). **Rulings:** none.
103
103
 
104
104
  ### WC-CI-6 · Run the changelog guard on every pull request
@@ -208,7 +208,8 @@ its prompt in cli-template says so from cli-template 0.8.0. A CLI on an older te
208
208
  - **Why:** a red merge breaks `main` for every parallel branch, and an admin merge skips the only gate there is.
209
209
  - **Enforced by:** `whatsapp-cli: main ruleset required checks` · `cli-core: main ruleset required checks` ·
210
210
  `tg-cli: main ruleset required checks` · `max-cli: main ruleset required checks` ·
211
- `cli-memo: main ruleset required checks` · `drive-cli: main ruleset required checks`, plus the main rulesets of
211
+ `cli-memo: main ruleset required checks` · `drive-cli: main ruleset required checks` ·
212
+ `gcal-cli: main ruleset required checks`, plus the main rulesets of
212
213
  cli-base and cli-template. In each repository with pr.yml, the ruleset requires the pull request validation jobs besides the
213
214
  standards job. cli-testing and cli-docs have no pr.yml, so their rulesets require only the standards job, and
214
215
  review covers the rest. zoom-cli has pr.yml,
@@ -212,10 +212,9 @@ store or files without reading the service first (`remote: "none"`, `local: "wri
212
212
  - **Example:** `annotate(send, { effects: { remote: "write", local: "write" } })`. Each side is `none`, `read` or
213
213
  `write`. A sync that reads the service and writes the local store is `{ remote: "read", local: "write" }`.
214
214
  - **Exceptions:** none. Without `effects`, the error handler never marks a failure safe to retry, and a next step
215
- that runs the command is never safe to run unasked. Known gap, as of 2026-10-27: cli-base's `writes()`
216
- (`src/standards/effects.ts`) still counts every local write as a write, so cli-core's MCP router serves `sync` and
217
- `calendars list` through the `write` tool. The follow-up makes `writes()` true for `remote: "write"`, or for
218
- `local: "write"` when `remote` is `none`.
215
+ that runs the command is never safe to run unasked. cli-base's `writes()` (`src/standards/effects.ts`) still
216
+ counts every local write as a write; cli-core's MCP router adds its own `fetchesOnly` test (cli-core 0.468.0), so
217
+ `sync` and `calendars list` run through `read`.
219
218
  - **Applies to:** all CLIs. **Since:** cli-base 0.41.0 (what counts as a read: cli-base 0.65.0). **Rulings:** the
220
219
  owner's ruling on what counts as a read over MCP (2026-10-27).
221
220
 
@@ -277,7 +276,9 @@ store or files without reading the service first (`remote: "none"`, `local: "wri
277
276
  - **Why:** a wire field, a port or an adapter name means nothing to the person reading help.
278
277
  - **Enforced by:** review only.
279
278
  - **Example:** an option says what it changes and its unit: `--pause <duration>` "wait this long between pages".
280
- - **Exceptions:** none.
279
+ - **Exceptions:** generated commands (`bot api`, gcal's and drive's `api`) keep the help their API description
280
+ gives, such as Google's full sentences for `gcal api calendar-acl-delete --calendar-id`; rewriting it by hand would
281
+ drift from the API on the next regeneration.
281
282
  - **Applies to:** all CLIs. **Since:** cli-base 0.41.0. **Rulings:** none.
282
283
 
283
284
  ### WC-CMD-24 · Give every leaf command one to three examples that run as written
@@ -25,10 +25,10 @@ Rule ids on this page are `WC-DOC-<n>`. How to read a rule, how ids work and whi
25
25
 
26
26
  - **Why:** the reader wants the current behaviour, not the history of a page or a journal number they cannot open.
27
27
  - **Enforced by:** `tg-cli: pnpm docs:check` · `max-cli: pnpm docs:check` · `whatsapp-cli: pnpm docs:check` ·
28
- `zoom-cli: pnpm docs:check`.
28
+ `zoom-cli: pnpm docs:check` · `gcal-cli: pnpm docs:check`.
29
29
  - **Example:** write "`--limit` defaults to 50", not "~~20~~ 50 (correction 2026-10-01, NEED-12)".
30
30
  - **Exceptions:** none. `cli-dev docs-check` checks this when the CLI's rules module sets `userPage`. tg, max, wa,
31
- zoom and cli-template set it; memo, drive and cli-base's default check do not.
31
+ zoom, gcal and cli-template set it; memo, drive and cli-base's default check do not.
32
32
  - **Applies to:** all CLIs. **Since:** cli-base 0.41.0. **Rulings:** none.
33
33
 
34
34
  ### WC-DOC-3 · Every document states current facts; rewrite a wrong sentence in place
@@ -220,12 +220,13 @@ before its deadline.
220
220
 
221
221
  ### WC-LIFE-15 · Keep a run log through cli-core, and read it back with `runs`
222
222
 
223
- A failed command is always kept as a run, unless the person turned recording off with `--no-record` or
224
- `record: false` in the config. A run keeps the command line as typed, secrets hidden; `record_input: false` masks
225
- every value that may be content
223
+ Every failed command is kept as a run, and so is every command that does its work inside cli-core's run
224
+ (`context.run`, or `startRecording` in a program of its own). `record: false` in the config keeps failures only;
225
+ `--no-record` keeps nothing; `--record` keeps a success the config, WireCat Desktop or a command that runs until
226
+ stopped would not. A run keeps the command line as typed, secrets hidden; `record_input: false` masks every value
227
+ that may be content
226
228
  ([WC-SECR-1](security.md#wc-secr-1--private-data-and-secrets-never-reach-a-log-a-fixture-or-a-committed-document)).
227
- `--record` keeps every run of a command that does its work inside cli-core's run
228
- (`context.run`, or `startRecording` in a program of its own). A run is a folder `<state>/runs/<day>/<run_id>/` with
229
+ A run is a folder `<state>/runs/<day>/<run_id>/` with
229
230
  `run.json` and `events.jsonl`, one log record per line. `runs list` and `runs show` read it.
230
231
 
231
232
  - A CLI that runs its program through cli-core's `run` gets the failure log and `--record`/`--no-record` from it,
@@ -78,10 +78,9 @@ A command that reads the service and only saves what it read into the owner's lo
78
78
  nothing the owner would have to undo, so a sync stays under it; sending it through `write` would make a host ask a
79
79
  person before every refresh.
80
80
  - **Enforced by:** `cli-core: src/mcp/mcp.test.ts` · `cli-core: src/mcp/output-schema.test.ts`.
81
- - **Example:** `max_read { "command": "messages send", … }` is refused. Once the gap below is closed,
82
- `gcal_read { "command": "sync" }` runs.
83
- - **Exceptions:** none. Known gap, as of 2026-10-27: cli-core decides with cli-base's `writes()`, which still counts
84
- the local write, so `sync` runs through `write` until that follow-up lands (WC-CMD-17).
81
+ - **Example:** `max_read { "command": "messages send", … }` is refused. `gcal_read { "command": "sync" }` runs:
82
+ cli-core counts a command that only fetches into the local store as a read (`fetchesOnly`, cli-core 0.468.0).
83
+ - **Exceptions:** none.
85
84
  - **Applies to:** all CLIs. **Since:** cli-base 0.41.0 (what counts as a read: cli-base 0.65.0). **Rulings:** the
86
85
  owner's ruling on what counts as a read over MCP (2026-10-27).
87
86
 
@@ -237,7 +236,7 @@ cli-core builds its personal and bot MCP catalogues from the command tree with `
237
236
  (WC-MCP-1) that is three names and their schemas, which do not change when a command is added. So each CLI also
238
237
  pins the command catalogue behind the tools, every command `tools_search` can return and whether it runs through
239
238
  `read` or `write`, in `contracts/mcp-catalogue.json`. A command that becomes visible to agents then shows up as a diff in
240
- 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.
239
+ its pull request. tg, max and gcal keep both; wa, zm and drive pin only the tools, a known gap as of 2026-10-27.
241
240
 
242
241
  ## Sources
243
242
 
@@ -63,7 +63,9 @@ JSON Schema 2020-12.
63
63
  - **Why:** a cursor source has no page numbers, and pretending it has them sends the caller to the wrong place.
64
64
  - **Enforced by:** review only.
65
65
  - **Example:** `{ mode: "page", limit, has_more, next_page? }`, or `{ mode: "cursor", limit, has_more,
66
- next_cursor? }` for a source with its own position, such as a message history.
66
+ next_cursor? }` for a source with its own position, such as a message history. A search that can count its
67
+ matches adds `total: { value, relation }`, shaped as Elasticsearch's `hits.total`: `relation` is `eq` for an exact
68
+ count and `gte` for a lower bound, when counting stopped at a cap or could not be done cheaply.
67
69
  - **Exceptions:** none.
68
70
  - **Applies to:** all CLIs. **Since:** cli-base 0.41.0. **Rulings:** none.
69
71
 
@@ -97,26 +97,39 @@ Commands that look after the tool and its files, and never show a message, have
97
97
 
98
98
  ### WC-PERM-6 · The built-in defaults ask only before what cannot be undone
99
99
 
100
- With no `permissions` set, a profile may do almost everything. The built-in defaults are:
100
+ With no `permissions` set, a profile may do almost everything. The built-in defaults, cli-core's
101
+ `DEFAULT_PERMISSIONS` (`src/sends/permissions.ts`), are:
101
102
 
102
103
  ```json
103
104
  {
104
105
  "messages.delete": "ask", "bot.messages.delete": "ask", "account.sessions.end": "ask",
105
106
  "topics.enable": "ask", "topics.delete": "ask", "chats.delete": "ask", "chats.clear": "ask",
106
- "replies.send": "deny"
107
+ "replies.send": "deny",
108
+ "mcp.authority": "deny",
109
+ "store.backup": "ask", "store.restore": "ask", "store.migrate": "ask", "store.repair": "ask",
110
+ "store.reset": "ask", "store.clear": "ask", "store.decrypt": "ask", "store.copies.delete": "ask",
111
+ "server.start": "ask", "server.stop": "ask", "server.restart": "ask",
112
+ "models.download": "ask", "images.models.download": "ask",
113
+ "schedule.run": "ask", "schedule.exec": "ask", "doctor.reports.create": "ask"
107
114
  }
108
115
  ```
109
116
 
117
+ `mcp.authority` keeps an agent from running the commands that change what a later run may do, such as settings,
118
+ schedules or recipient lists (NEED-1662, cli-core 0.400.0). The `store`, `server`, `models`, `schedule` and `doctor`
119
+ keys give a key to writes that had none, so an MCP server can offer them (NEED-1663, cli-core 0.401.0). A CLI that
120
+ declares `commandPermissions` (WC-PERM-22) gets these defaults beneath its own.
121
+
110
122
  A default is never made stricter without the owner's word.
111
123
 
112
- - **Why:** the tool should work without questions; only a deletion, a logout or an automatic reply needs one.
124
+ - **Why:** the tool should work without questions; only a deletion, a logout, an automatic reply, a change to the
125
+ store or this machine, or a change to what an agent may do needs one.
113
126
  - **Enforced by:** `cli-core: src/sends/permissions.test.ts`.
114
127
  - **Example:** a new profile sends at once, and asks before `messages delete`.
115
128
  - **Exceptions:** where the messenger has replies, `replies.send` is `deny`: a reply rule writes to people with nobody
116
129
  typing a command, so it waits for the owner's own `allow`. A messenger with no `replies` group, such as wa, has no
117
130
  use for the key.
118
131
  - **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.
132
+ 0.41.0. **Rulings:** NEED-568, NEED-1662, NEED-1663.
120
133
 
121
134
  ### WC-PERM-7 · A built-in default only ever tightens
122
135
 
@@ -333,19 +346,19 @@ A messenger's keys come from its resources (`messages`, `chats`). Any other CLI
333
346
  write runs through cli-core's write guard with `permissionDefaults: configuration.permissions.defaults`
334
347
  (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
348
  (WC-PERM-17). A CLI that sends no messages has no recipient list or hourly limit to set.
349
+ - **When.** cli-template's skeleton has no command of its own that writes to a service, so it does not call
350
+ `commandPermissions`; a CLI made from it adds the call with its first such command.
336
351
 
337
352
  - **Why:** one permission model for every CLI; a CLI-own layer drifts from the shared one and is audited by nobody.
338
353
  - **Enforced by:** `cli-core: src/cli/command-permissions.test.ts` · `cli-core: src/calendar/cli/writes.test.ts`.
339
354
  - **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
355
+ (`@wirecat/cli-core/calendar`, cli-core 0.458.0). By this rule `events create`, `update`, `respond` and `move` are
356
+ `allow` and `events delete` is `ask`. `events.delete.series` (every occurrence of a repeating series, in every
357
+ attendee's calendar) and `events.notify` (the provider mails the attendees) are `deny` until the owner allows them: over MCP
343
358
  `ask` runs without a question, and each reaches people other than the owner. gcal adds its own keys for its generated
344
359
  `api <method>` commands: sharing a calendar is `ask` (NEED-1857), and emptying the main calendar
345
360
  (`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`.
361
+ - **Exceptions:** known gaps, as of 2026-10-31:
349
362
  - gcal's generated `api` commands default every write to `ask`, ordinary changes included.
350
363
  - drive keeps its own policy and `--read-only` flag, zoom reads its own `remote-meetings` key with `levelFor`, and
351
364
  memo uses cli-core's permissions for the shared commands only. None of them declares `commandPermissions` yet.
@@ -65,7 +65,7 @@ on the [CI](ci.md) page.
65
65
 
66
66
  - **Why:** two sessions often prepare the same number; the second must not publish over the first or fail late.
67
67
  - **Enforced by:** `tg-cli: bin/release` · `max-cli: bin/release` · `whatsapp-cli: bin/release` ·
68
- `cli-memo: bin/release`.
68
+ `cli-memo: bin/release` · `gcal-cli: bin/release`.
69
69
  - **Example:** before it publishes, `bin/release` asks npm. When npm already has the version, or a higher one, it
70
70
  takes the next free number at the same level (after `x.y.0` the next minor, else the next patch). It lands that
71
71
  number through a pull request and publishes it.
@@ -129,7 +129,9 @@ on the [CI](ci.md) page.
129
129
  ([npm docs](https://docs.npmjs.com/trusted-publishers)), which a package has only once it is published, so its
130
130
  first version is published with `bin/release --local` by the owner, from a clean `main`. Right after it, the owner
131
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`.
132
+ later version goes through `release.yml`. ClawHub has no trusted publishing: the `clawhub` job publishes the agent
133
+ skill with the organization secret `CLAWHUB_TOKEN`. It runs after the tag, so a ClawHub failure never holds back
134
+ npm; the run then fails although npm and the tag are done, and only that job is run again.
133
135
  - **Applies to:** all CLIs and shared libraries. **Since:** cli-base 0.41.0. **Rulings:** none.
134
136
 
135
137
  ### WC-REL-12 · Publish only after full validation and the release security checks pass
@@ -157,7 +159,7 @@ on the [CI](ci.md) page.
157
159
 
158
160
  - **Why:** a release must still be possible when GitHub Actions is down, without leaking the token.
159
161
  - **Enforced by:** `tg-cli: bin/release` · `max-cli: bin/release` · `whatsapp-cli: bin/release` ·
160
- `cli-memo: bin/release`.
162
+ `cli-memo: bin/release` · `gcal-cli: bin/release`.
161
163
  - **Example:** `bin/release --local` runs the same checks on this machine and reads the npm token from the keyring
162
164
  into the publish command's environment. The checks include the test suite with its coverage floor,
163
165
  any shipped non-JavaScript engine's tests, offline contracts, leak and secret scans, and dependency
@@ -199,8 +201,9 @@ fixtures, `scripts/`, `.github/`, `bin/release`, `config/`, `lint/`, `contracts/
199
201
  `npm pack --dry-run` in gcal-cli packed 256 files on 2026-10-27, `src/`, `scripts/`, `.github/`, `config/`,
200
202
  `contracts/`, `lint/` and `template.json` among them, because its `files` list named them; its check could not see
201
203
  it. On 2026-10-31 (gcal-cli 0.2.0) it packs 144. The check this rule needs
202
- refuses the forbidden paths above whatever `files` says; it belongs in cli-template, so every CLI gets it by
203
- `upgrade` ([cli-template#77](https://github.com/WireCatLabs/cli-template/issues/77)).
204
+ refuses the forbidden paths above whatever `files` says: cli-template's `scripts/smoke.ts` does since
205
+ [cli-template#85](https://github.com/WireCatLabs/cli-template/pull/85). `scripts/` is a seed that `upgrade` never
206
+ changes, so a CLI made before it copies the check by hand.
204
207
  - **Exceptions:** cli-template's own package ships the skeleton's sources and configs, since `new` copies them; the
205
208
  `files` list `new` writes into a CLI follows this rule. Each CLI's `package:check` runs `packContents`; tg and max
206
209
  as `release-check.ts --package-only`.
@@ -274,7 +274,8 @@ general words.
274
274
  `persist-credentials: false`, and `permissions: contents: read` at the top of the workflow. A job that needs more
275
275
  asks for it on that job only. The `zizmor` scan in `release-checks.yml` reports high findings at release, but does
276
276
  not fail on them.
277
- - **Exceptions:** none.
277
+ - **Exceptions:** the `tag` job of `release.yml` keeps its checkout's credentials (no `persist-credentials: false`),
278
+ because it pushes `v<version>`; it runs no project code.
278
279
  - **Applies to:** the WireCat repositories: every CLI, cli-base, cli-core, cli-testing, cli-docs and community; and
279
280
  cli-template.
280
281
  **Since:** cli-base 0.41.0. **Rulings:** none.
@@ -347,7 +348,8 @@ store and runs no search. A CLI declares its provider and may replace any step.
347
348
 
348
349
  A CLI never revokes a sign-in at the provider in a moved run, even one read from the temporary entry: a refreshed
349
350
  token is still the owner's grant. A test passes `memoryKeyring()`; a run that must not see the keyring at all also
350
- runs with `DBUS_SESSION_BUS_ADDRESS` removed, since the keyring is reached over D-Bus.
351
+ runs with `DBUS_SESSION_BUS_ADDRESS=disabled:`, since the keyring is reached over D-Bus. Removing the variable is not
352
+ enough: D-Bus then connects to `$XDG_RUNTIME_DIR/bus`, the default session bus, and reaches the keyring anyway.
351
353
  - **Cleanup:** a moved run that never reaches `session end` leaves its temporary entry, and so does one that refreshes
352
354
  a token, since the refreshed token is written there. `bin/keyring-dedupe --delete` in the projects folder removes
353
355
  temporary entries that sit beside the owner's real one; a test or agent can also delete one by name with
@@ -89,10 +89,12 @@ plus community itself. cli-template follows every rule here as a CLI does
89
89
  Node 22 from 22.16.0, then 24 and later. Full validation runs on Node 24. `bun test` cannot run the vitest suite, so
90
90
  `scripts/smoke.ts` runs the built exports with plain assertions, and the Bun job runs it
91
91
  (`pnpm build && pnpm smoke:bun`). A new entry point gets a line there. Node 22 runs in the Windows install matrix of
92
- tg, max and wa, in zoom's full validation and in cli-core's old-Node job.
93
- - **Exceptions:** cli-docs and community publish no runtime package. tg needs `^22.19.0`. memo, drive and cli-base
94
- do not run Node 22 in any workflow yet, and cli-base's own `engines` is still `>=22`. The unit suite runs on Windows
95
- and macOS for tg, max and drive (`platforms: true`); zoom and memo still fail tests there, so theirs is off.
92
+ tg, max and wa, in zoom's full validation, in cli-core's old-Node job and in cli-template's `node-22` job (Node
93
+ 22.16.0, from cli-template 0.11.0), which a CLI made from the template gets with `upgrade`.
94
+ - **Exceptions:** cli-docs and community publish no runtime package. tg needs `^22.19.0`. memo, drive, cli-base and
95
+ the CLIs made from a template before 0.11.0 do not run Node 22 in any workflow yet; gcal is adding its own job.
96
+ cli-base's own `engines` is still `>=22`. The unit suite runs on Windows and macOS for tg, max and drive
97
+ (`platforms: true`); zoom and memo still fail tests there, so theirs is off.
96
98
  - **Applies to:** the CLIs, cli-core and cli-base. **Since:** cli-base 0.41.0. **Rulings:** NEED-11.
97
99
 
98
100
  ### WC-TOOL-7 · Let Biome format the code, and do not format by hand
@@ -239,7 +241,7 @@ no-unbounded-spread ([WC-TOOL-12](#wc-tool-12--do-not-spread-a-list-of-user-data
239
241
 
240
242
  - **Why:** user pages feed the docs site, so their Markdown form is checked more closely than developer notes.
241
243
  - **Enforced by:** `tg-cli: pnpm docs:check` · `max-cli: pnpm docs:check` · `whatsapp-cli: pnpm docs:check` ·
242
- `zoom-cli: pnpm docs:check` · `cli-testing: pnpm docs:check`.
244
+ `zoom-cli: pnpm docs:check` · `gcal-cli: pnpm docs:check` · `cli-testing: pnpm docs:check`.
243
245
  - **Example:** `rumdl check --config config/wirecat/rumdl.toml README.md docs/*.md`, inside `docs:check`. The preset
244
246
  turns off line length (MD013), first-line heading (MD041) and relative links (MD057), because other checks cover
245
247
  them.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wirecat/cli-base",
3
- "version": "0.66.1",
3
+ "version": "0.67.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",