@wirecat/cli-base 0.66.1 → 0.68.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 +74 -0
- package/dist/credentials.d.ts +11 -3
- package/dist/credentials.js +42 -18
- package/dist/index.d.ts +1 -1
- package/dist/standards/flags-how.js +7 -0
- package/dist/standards/index.d.ts +1 -1
- package/dist/standards/index.js +1 -1
- package/dist/standards/output.d.ts +33 -0
- package/dist/standards/output.js +16 -1
- package/docs/standards/README.md +1 -1
- package/docs/standards/ci.md +3 -2
- package/docs/standards/commands.md +6 -5
- package/docs/standards/docs.md +2 -2
- package/docs/standards/lifecycle.md +6 -5
- package/docs/standards/mcp.md +4 -5
- package/docs/standards/output.md +3 -1
- package/docs/standards/permissions.md +23 -10
- package/docs/standards/releases.md +18 -16
- package/docs/standards/security.md +23 -9
- package/docs/standards/toolchain.md +7 -5
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,80 @@ 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.68.0 — 31.10.2026
|
|
12
|
+
|
|
13
|
+
### Removed
|
|
14
|
+
|
|
15
|
+
- **WC-REL-10: a messenger CLI is released after the owner's yes in the conversation, with no signed report.**
|
|
16
|
+
`bin/release` in max-cli and whatsapp-cli no longer reads a release report or a `Signed off:` line. The agent asks
|
|
17
|
+
the owner before it runs `bin/release`; one yes covers one version.
|
|
18
|
+
|
|
19
|
+
### Changed — may break callers
|
|
20
|
+
|
|
21
|
+
- **`Credentials` in `auto` does the whole file fallback itself (WC-SECR-4).** A secret the file holds is read from the
|
|
22
|
+
file before the keyring, so a locked keyring is not tried for it, and a refresh `write` keeps it there. A sign-in
|
|
23
|
+
passes `write(account, secret, { signIn: true })`: it tries the keyring and, once the keyring takes it, removes the
|
|
24
|
+
file copy. The "could not reach the OS keyring" warning comes once per run (per `env` object), not once per
|
|
25
|
+
`Credentials`. Watch for: a file copy left next to a newer keyring entry by an older CLI now wins the read; sign in
|
|
26
|
+
again to move it back. CLIs can drop their own file-first read, "remove the file copy" step and `warnOnce`.
|
|
27
|
+
- **WC-SECR-4 is the one credential-storage standard.** The keyring by default; when it fails, the file without
|
|
28
|
+
asking, one warning on stderr, and the answer that saved the secret says where it went;
|
|
29
|
+
`--credential-storage file` on the sign-in command chooses the file and is discouraged; a secret in the file is
|
|
30
|
+
read from there until a sign-in reaches the keyring again. No CLI asks, none is keyring-only. `Credentials` already
|
|
31
|
+
falls back this way in `auto`; the rule now says every CLI must use it.
|
|
32
|
+
|
|
33
|
+
### Fixed
|
|
34
|
+
|
|
35
|
+
- **`Credentials.remove` with `storage: "keyring"` leaves the file alone.** It also deleted the account from the
|
|
36
|
+
credentials file, so a CLI that cleared the keyring copy after a file sign-in lost the sign-in it had just saved.
|
|
37
|
+
|
|
38
|
+
### Added
|
|
39
|
+
|
|
40
|
+
- **`--credential-storage <storage>` in the flag vocabulary** (role `mode`), so `cli-dev lint` accepts it on a sign-in
|
|
41
|
+
command without a per-CLI override.
|
|
42
|
+
|
|
43
|
+
## 0.67.0 — 31.10.2026
|
|
44
|
+
|
|
45
|
+
### Added
|
|
46
|
+
|
|
47
|
+
- **`page.total`: how many match, every page together** (WC-OUT-5). An optional `{ value, relation }`, shaped as
|
|
48
|
+
Elasticsearch's `hits.total`: `eq` is the exact count, `gte` a lower bound. A search answer can now say "20 of 144"
|
|
49
|
+
without the caller paging to the end. `totalSchema` and the `Total` type are exported from
|
|
50
|
+
`@wirecat/cli-base/standards`. Optional, so every existing answer stays valid.
|
|
51
|
+
|
|
52
|
+
### Fixed
|
|
53
|
+
|
|
54
|
+
- **WC-LIFE-15 says a success is kept by default.** It described `--record` as the way to keep a success and said
|
|
55
|
+
`record: false` also drops failures. cli-core keeps every run of a command that works through it; `record: false`
|
|
56
|
+
keeps failures only, and `--no-record` keeps nothing.
|
|
57
|
+
|
|
58
|
+
- **`Credentials.remove` in a moved run says "kept" only when the owner's entry is there and was kept.** It printed
|
|
59
|
+
"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
|
|
60
|
+
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
|
|
61
|
+
removal: with the temporary entry removed it says so and that the owner's was kept; with nothing removed it says the
|
|
62
|
+
owner's was kept; with no owner's entry it prints nothing. A keyring that fails on that look-up does not fail the
|
|
63
|
+
removal. The return value is unchanged (WC-SECR-24).
|
|
64
|
+
- **WC-SECR-24 says how to hide the keyring from a run.** Set `DBUS_SESSION_BUS_ADDRESS=disabled:`. Removing the
|
|
65
|
+
variable, as the rule said before, still reaches the keyring through the default session bus.
|
|
66
|
+
- **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
|
|
67
|
+
from it gets the job with `upgrade`; only CLIs made from an older template still lack it.
|
|
68
|
+
- **Five rules answer what cli-template's 0.10.0 audit found unclear.** WC-SECR-20 names the release `tag` job,
|
|
69
|
+
which keeps its credentials to push the tag. WC-TOOL-6 lists cli-template and its CLIs among those that do not run
|
|
70
|
+
Node 22 yet. WC-PERM-22 says a template-made CLI calls `commandPermissions` with its first command that writes to a
|
|
71
|
+
service. WC-REL-11 covers ClawHub's stored `CLAWHUB_TOKEN` and a ClawHub failure after the tag. WC-REL-16 says
|
|
72
|
+
where the forbidden-path check lives now, and that `upgrade` does not deliver it.
|
|
73
|
+
- **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
|
|
74
|
+
`sync` and `calendars list` as running through `write`: cli-core's `fetchesOnly` sends them through `read`.
|
|
75
|
+
WC-PERM-22 no longer says `CALENDAR_WRITE_DEFAULTS` asks before `events create`, `update` and `respond`; those and
|
|
76
|
+
`move` are `allow`. "Tools generated from commands" (mcp.md) counts gcal among the CLIs that pin
|
|
77
|
+
`contracts/mcp-catalogue.json`.
|
|
78
|
+
- **WC-PERM-6 lists all of cli-core's built-in defaults.** It showed the 8 messenger keys; `DEFAULT_PERMISSIONS` has
|
|
79
|
+
25, including `mcp.authority: deny` and `ask` for `store.*`, `server.*`, `models.download` and `schedule.*`.
|
|
80
|
+
- **WC-CMD-23 exempts generated commands,** as the naming and example rules do: their help is the API description's
|
|
81
|
+
own text.
|
|
82
|
+
- **gcal-cli is named where it meets a rule:** no correction marks on user pages, `rumdl` on user pages, pull
|
|
83
|
+
request validation, required checks on `main`, the free version number and `bin/release --local`.
|
|
84
|
+
|
|
11
85
|
## 0.66.1 — 31.10.2026
|
|
12
86
|
|
|
13
87
|
### Fixed
|
package/dist/credentials.d.ts
CHANGED
|
@@ -38,17 +38,25 @@ export declare const keyringService: (service: string, _configDir?: string, _iso
|
|
|
38
38
|
* shape `bin/keyring-dedupe` already finds and removes.
|
|
39
39
|
*/
|
|
40
40
|
export declare const movedRunService: (service: string, configDir: string) => string;
|
|
41
|
+
export interface WriteOptions {
|
|
42
|
+
/**
|
|
43
|
+
* A new sign-in rather than a refresh: it tries the keyring even when the file holds the account, and once the
|
|
44
|
+
* keyring takes it, removes the file copy (WC-SECR-4).
|
|
45
|
+
*/
|
|
46
|
+
signIn?: boolean;
|
|
47
|
+
}
|
|
41
48
|
/**
|
|
42
|
-
* Environment first, then the OS keyring, then a file.
|
|
49
|
+
* Environment first, then the OS keyring, then a file. In `auto`, a file that holds the account is read before the
|
|
50
|
+
* keyring and keeps its refreshes, so a locked keyring is not tried again for it; a sign-in moves it back.
|
|
43
51
|
*
|
|
44
52
|
* `auto` does not probe whether a keyring exists: it attempts the operation, and on failure warns
|
|
45
|
-
* once and falls through. A probe would touch the user's keychain for nothing and could still
|
|
53
|
+
* once per run and falls through. A probe would touch the user's keychain for nothing and could still
|
|
46
54
|
* succeed where the real operation fails.
|
|
47
55
|
*/
|
|
48
56
|
export declare class Credentials {
|
|
49
57
|
#private;
|
|
50
58
|
constructor(options: CredentialsOptions);
|
|
51
59
|
read(account: string): StoredCredential | undefined;
|
|
52
|
-
write(account: string, secret: string): CredentialSource;
|
|
60
|
+
write(account: string, secret: string, options?: WriteOptions): CredentialSource;
|
|
53
61
|
remove(account: string): CredentialSource[];
|
|
54
62
|
}
|
package/dist/credentials.js
CHANGED
|
@@ -15,11 +15,14 @@ export const keyringService = (service, _configDir, _isolated) => service;
|
|
|
15
15
|
* shape `bin/keyring-dedupe` already finds and removes.
|
|
16
16
|
*/
|
|
17
17
|
export const movedRunService = (service, configDir) => `${service}:${resolve(configDir)}`;
|
|
18
|
+
// One warning per run, not per instance: a run over several profiles builds one Credentials for each.
|
|
19
|
+
const warnedRuns = new WeakSet();
|
|
18
20
|
/**
|
|
19
|
-
* Environment first, then the OS keyring, then a file.
|
|
21
|
+
* Environment first, then the OS keyring, then a file. In `auto`, a file that holds the account is read before the
|
|
22
|
+
* keyring and keeps its refreshes, so a locked keyring is not tried again for it; a sign-in moves it back.
|
|
20
23
|
*
|
|
21
24
|
* `auto` does not probe whether a keyring exists: it attempts the operation, and on failure warns
|
|
22
|
-
* once and falls through. A probe would touch the user's keychain for nothing and could still
|
|
25
|
+
* once per run and falls through. A probe would touch the user's keychain for nothing and could still
|
|
23
26
|
* succeed where the real operation fails.
|
|
24
27
|
*/
|
|
25
28
|
export class Credentials {
|
|
@@ -33,7 +36,6 @@ export class Credentials {
|
|
|
33
36
|
#warn;
|
|
34
37
|
/** Set only for a moved run with the system keyring. */
|
|
35
38
|
#temporaryService;
|
|
36
|
-
#warned = false;
|
|
37
39
|
constructor(options) {
|
|
38
40
|
this.#configDir = options.configDir;
|
|
39
41
|
this.#storage = options.storage ?? "auto";
|
|
@@ -50,23 +52,29 @@ export class Credentials {
|
|
|
50
52
|
const fromEnv = this.#envVar ? this.#env[this.#envVar]?.trim() : undefined;
|
|
51
53
|
if (fromEnv)
|
|
52
54
|
return { secret: fromEnv, source: "environment" };
|
|
55
|
+
const fromFile = this.#storage === "keyring" ? undefined : this.#readFile()?.[account]?.secret;
|
|
56
|
+
if (fromFile)
|
|
57
|
+
return { secret: fromFile, source: "file" };
|
|
53
58
|
if (this.#storage !== "file") {
|
|
54
59
|
const temporary = this.#temporaryService;
|
|
55
60
|
const fromKeyring = this.#tryKeyring(() => (temporary && this.#keyring.get(temporary, account)) || this.#keyring.get(this.#service, account));
|
|
56
61
|
if (fromKeyring)
|
|
57
62
|
return { secret: fromKeyring, source: "keyring" };
|
|
58
63
|
}
|
|
59
|
-
|
|
60
|
-
return secret ? { secret, source: "file" } : undefined;
|
|
64
|
+
return undefined;
|
|
61
65
|
}
|
|
62
|
-
write(account, secret) {
|
|
63
|
-
|
|
66
|
+
write(account, secret, options = {}) {
|
|
67
|
+
const inFile = this.#storage === "auto" && this.#readFile()?.[account] !== undefined;
|
|
68
|
+
if (this.#storage === "keyring" || (this.#storage === "auto" && (options.signIn || !inFile))) {
|
|
64
69
|
const stored = this.#tryKeyring(() => {
|
|
65
70
|
this.#keyring.set(this.#temporaryService ?? this.#service, account, secret);
|
|
66
71
|
return true;
|
|
67
72
|
});
|
|
68
|
-
if (stored)
|
|
73
|
+
if (stored) {
|
|
74
|
+
if (inFile)
|
|
75
|
+
this.#removeFromFile(account);
|
|
69
76
|
return "keyring";
|
|
77
|
+
}
|
|
70
78
|
}
|
|
71
79
|
const store = this.#readFile();
|
|
72
80
|
if (!store) {
|
|
@@ -83,19 +91,35 @@ export class Credentials {
|
|
|
83
91
|
if (this.#tryKeyring(() => this.#keyring.delete(this.#temporaryService ?? this.#service, account))) {
|
|
84
92
|
removed.push("keyring");
|
|
85
93
|
}
|
|
86
|
-
if (this.#temporaryService) {
|
|
87
|
-
this.#warn(
|
|
88
|
-
|
|
94
|
+
if (this.#temporaryService && this.#ownersEntryExists(account)) {
|
|
95
|
+
this.#warn(removed.includes("keyring")
|
|
96
|
+
? `removed this run's own sign-in of ${account} from the OS keyring and kept the owner's: a run with ` +
|
|
97
|
+
"moved folders never removes it"
|
|
98
|
+
: `kept ${account} in the OS keyring: it is the owner's sign-in, shared with every run, and a run with ` +
|
|
99
|
+
"moved folders never removes it");
|
|
89
100
|
}
|
|
90
101
|
}
|
|
91
|
-
|
|
92
|
-
if (store?.[account] !== undefined) {
|
|
93
|
-
delete store[account];
|
|
94
|
-
this.#writeFile(store);
|
|
102
|
+
if (this.#storage !== "keyring" && this.#removeFromFile(account))
|
|
95
103
|
removed.push("file");
|
|
96
|
-
}
|
|
97
104
|
return removed;
|
|
98
105
|
}
|
|
106
|
+
#removeFromFile(account) {
|
|
107
|
+
const store = this.#readFile();
|
|
108
|
+
if (store?.[account] === undefined)
|
|
109
|
+
return false;
|
|
110
|
+
delete store[account];
|
|
111
|
+
this.#writeFile(store);
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
/** Only for the warning: a keyring that fails here must not fail a removal that already happened. */
|
|
115
|
+
#ownersEntryExists(account) {
|
|
116
|
+
try {
|
|
117
|
+
return this.#keyring.get(this.#service, account) !== null;
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
99
123
|
#path() {
|
|
100
124
|
return join(this.#configDir, this.#fileName);
|
|
101
125
|
}
|
|
@@ -129,8 +153,8 @@ export class Credentials {
|
|
|
129
153
|
return operation();
|
|
130
154
|
}
|
|
131
155
|
catch (error) {
|
|
132
|
-
if (!this.#
|
|
133
|
-
this.#
|
|
156
|
+
if (!warnedRuns.has(this.#env)) {
|
|
157
|
+
warnedRuns.add(this.#env);
|
|
134
158
|
this.#warn(`could not reach the OS keyring — it may be locked, or no keyring service is running ` +
|
|
135
159
|
`(${error instanceof Error ? error.message : String(error)}); ` +
|
|
136
160
|
`using ${this.#path()} instead, readable only by you`);
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { BATCH_LIMITS, type BatchContext, type BatchHandler, type BatchOperation, type BatchReport, type BatchResult, type PreparedBatchRequest, prepareBatch, projectBatchAnswers, readBatchInput, runBatch, } from "./batch.js";
|
|
2
2
|
export { loadConfigFile, saveConfigFile, writeSecurely } from "./config.js";
|
|
3
|
-
export type { CredentialSource, CredentialStorage, CredentialsOptions, StoredCredential } from "./credentials.js";
|
|
3
|
+
export type { CredentialSource, CredentialStorage, CredentialsOptions, StoredCredential, WriteOptions, } from "./credentials.js";
|
|
4
4
|
export { CREDENTIAL_STORAGE, Credentials, keyringService, movedRunService } from "./credentials.js";
|
|
5
5
|
export { createDeadline, type Deadline, type DeadlineOptions } from "./deadline.js";
|
|
6
6
|
export { CliError, type CliErrorDetails, type ErrorCode, errorCodes, isCliError } from "./errors.js";
|
|
@@ -375,6 +375,13 @@ export const HOW_FLAGS = [
|
|
|
375
375
|
type: "string",
|
|
376
376
|
meaning: "the browser program to open for sign-in",
|
|
377
377
|
},
|
|
378
|
+
{
|
|
379
|
+
name: "--credential-storage",
|
|
380
|
+
value: "<storage>",
|
|
381
|
+
role: "mode",
|
|
382
|
+
type: "choice",
|
|
383
|
+
meaning: "where a sign-in is kept: keyring, which falls back to a private file by itself, or file (WC-SECR-4)",
|
|
384
|
+
},
|
|
378
385
|
{ name: "--checkpoint", value: "<file>", role: "mode", type: "path", meaning: "keep resume progress in this file" },
|
|
379
386
|
{
|
|
380
387
|
name: "--expected-event-id",
|
|
@@ -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";
|
package/dist/standards/index.js
CHANGED
|
@@ -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. */
|
package/dist/standards/output.js
CHANGED
|
@@ -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({
|
package/docs/standards/README.md
CHANGED
|
@@ -42,7 +42,7 @@ the four steps that need a person. A new CLI is ready for its first release when
|
|
|
42
42
|
| [Security](security.md) | `SECR` | 23 | Secrets, logs, keyring, argv, error text, untrusted text, scanners, workflow hardening, reporting, OAuth sign-in, meeting secrets, keyring in moved-folder runs |
|
|
43
43
|
| [Documents](docs.md) | `DOC` | 20 | User pages and developer pages, generated pages, current facts, changelog |
|
|
44
44
|
| [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 |
|
|
45
|
-
| [Releases](releases.md) | `REL` | 15 | `bin/release`, the release lock, versions,
|
|
45
|
+
| [Releases](releases.md) | `REL` | 15 | `bin/release`, the release lock, versions, the owner's yes, trusted publishing |
|
|
46
46
|
| [CI](ci.md) | `CI` | 12 | Hooks, the pull request budget, full validation, monthly runs |
|
|
47
47
|
| [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 |
|
|
48
48
|
| [Style](style.md) | `STYLE` | 10 | Comments, TypeScript habits, commit messages |
|
package/docs/standards/ci.md
CHANGED
|
@@ -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
|
|
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.
|
|
216
|
-
|
|
217
|
-
`
|
|
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:**
|
|
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
|
package/docs/standards/docs.md
CHANGED
|
@@ -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
|
-
|
|
224
|
-
`
|
|
225
|
-
|
|
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
|
-
|
|
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,
|
package/docs/standards/mcp.md
CHANGED
|
@@ -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.
|
|
82
|
-
|
|
83
|
-
- **Exceptions:** none.
|
|
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
|
|
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
|
|
package/docs/standards/output.md
CHANGED
|
@@ -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
|
|
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
|
|
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 `
|
|
341
|
-
and `events delete` is `ask`. `events.delete.series` (every occurrence of a repeating series, in every
|
|
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-
|
|
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.
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
## Scope
|
|
6
6
|
|
|
7
|
-
How a CLI is released: `bin/release`, the release lock, version numbers, the
|
|
7
|
+
How a CLI is released: `bin/release`, the release lock, version numbers, the owner's yes and trusted publishing.
|
|
8
8
|
|
|
9
9
|
Rule ids on this page are `WC-REL-<n>`. How to read a rule, how ids work and which rule wins are in the
|
|
10
10
|
[index](README.md). The changelog a release dates is on the [Documents](docs.md) page. The checks a release runs are
|
|
@@ -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.
|
|
@@ -106,17 +106,16 @@ on the [CI](ci.md) page.
|
|
|
106
106
|
|
|
107
107
|
### WC-REL-9 · Merged into [WC-REL-8](#wc-rel-8--any-package-may-release-at-any-time)
|
|
108
108
|
|
|
109
|
-
### WC-REL-10 · A messenger CLI is released only
|
|
109
|
+
### WC-REL-10 · A messenger CLI is released only after the owner says yes in the conversation
|
|
110
110
|
|
|
111
|
-
- **Why:**
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
- **
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
- **Exceptions:** none.
|
|
118
|
-
|
|
119
|
-
- **Applies to:** messenger CLIs. **Since:** cli-base 0.41.0. **Rulings:** none.
|
|
111
|
+
- **Why:** a messenger CLI acts on the owner's own accounts, so the owner decides when a new version ships. A signed
|
|
112
|
+
release report added a step without adding a check: the owner removed it on 2026-10-31 ("remove the signing
|
|
113
|
+
requirements instead just ask").
|
|
114
|
+
- **Enforced by:** review only.
|
|
115
|
+
- **Example:** the agent finishes the release skill, then asks "release max 0.91.0?". It runs `bin/release` only after
|
|
116
|
+
the owner says yes for that version. One yes covers one version.
|
|
117
|
+
- **Exceptions:** none.
|
|
118
|
+
- **Applies to:** messenger CLIs. **Since:** cli-base 0.41.0. **Rulings:** owner, 2026-10-31.
|
|
120
119
|
|
|
121
120
|
### WC-REL-11 · Publish from GitHub Actions with npm trusted publishing
|
|
122
121
|
|
|
@@ -129,7 +128,9 @@ on the [CI](ci.md) page.
|
|
|
129
128
|
([npm docs](https://docs.npmjs.com/trusted-publishers)), which a package has only once it is published, so its
|
|
130
129
|
first version is published with `bin/release --local` by the owner, from a clean `main`. Right after it, the owner
|
|
131
130
|
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`.
|
|
131
|
+
later version goes through `release.yml`. ClawHub has no trusted publishing: the `clawhub` job publishes the agent
|
|
132
|
+
skill with the organization secret `CLAWHUB_TOKEN`. It runs after the tag, so a ClawHub failure never holds back
|
|
133
|
+
npm; the run then fails although npm and the tag are done, and only that job is run again.
|
|
133
134
|
- **Applies to:** all CLIs and shared libraries. **Since:** cli-base 0.41.0. **Rulings:** none.
|
|
134
135
|
|
|
135
136
|
### WC-REL-12 · Publish only after full validation and the release security checks pass
|
|
@@ -157,7 +158,7 @@ on the [CI](ci.md) page.
|
|
|
157
158
|
|
|
158
159
|
- **Why:** a release must still be possible when GitHub Actions is down, without leaking the token.
|
|
159
160
|
- **Enforced by:** `tg-cli: bin/release` · `max-cli: bin/release` · `whatsapp-cli: bin/release` ·
|
|
160
|
-
`cli-memo: bin/release`.
|
|
161
|
+
`cli-memo: bin/release` · `gcal-cli: bin/release`.
|
|
161
162
|
- **Example:** `bin/release --local` runs the same checks on this machine and reads the npm token from the keyring
|
|
162
163
|
into the publish command's environment. The checks include the test suite with its coverage floor,
|
|
163
164
|
any shipped non-JavaScript engine's tests, offline contracts, leak and secret scans, and dependency
|
|
@@ -199,8 +200,9 @@ fixtures, `scripts/`, `.github/`, `bin/release`, `config/`, `lint/`, `contracts/
|
|
|
199
200
|
`npm pack --dry-run` in gcal-cli packed 256 files on 2026-10-27, `src/`, `scripts/`, `.github/`, `config/`,
|
|
200
201
|
`contracts/`, `lint/` and `template.json` among them, because its `files` list named them; its check could not see
|
|
201
202
|
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
|
|
203
|
-
|
|
203
|
+
refuses the forbidden paths above whatever `files` says: cli-template's `scripts/smoke.ts` does since
|
|
204
|
+
[cli-template#85](https://github.com/WireCatLabs/cli-template/pull/85). `scripts/` is a seed that `upgrade` never
|
|
205
|
+
changes, so a CLI made before it copies the check by hand.
|
|
204
206
|
- **Exceptions:** cli-template's own package ships the skeleton's sources and configs, since `new` copies them; the
|
|
205
207
|
`files` list `new` writes into a CLI follows this rule. Each CLI's `package:check` runs `packContents`; tg and max
|
|
206
208
|
as `release-check.ts --package-only`.
|
|
@@ -67,19 +67,31 @@ 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
|
|
70
|
+
`0600` in the CLI's own folder. Storage is the keyring by default (`auto`): when the keyring fails (locked, not
|
|
71
|
+
running, no D-Bus), the CLI uses the file without asking, warns once on stderr, and the answer that saved the secret
|
|
72
|
+
says where it went (gcal: `credential_storage: "file"`). `--credential-storage file` on the sign-in command chooses
|
|
73
|
+
the file outright; help and docs discourage it. Once the file holds a secret, it is read from the file, so a locked
|
|
74
|
+
keyring is not tried again for it; the next sign-in that reaches the keyring moves it back and removes the file copy.
|
|
75
|
+
`Credentials` in `auto` does all of this: the CLI passes `{ signIn: true }` to `write` for a new sign-in and nothing
|
|
76
|
+
for a refresh.
|
|
77
|
+
No CLI asks which store to use, and none is keyring-only.
|
|
78
|
+
The keyring service is the CLI's app name, `appName` in its identity (`max-cli`, `gcal-cli`, `cli-memo`), not
|
|
71
79
|
the command word people type (`max`, `gcal`). A run with moved folders reads the same entry, writes a temporary one,
|
|
72
80
|
and never replaces or deletes the owner's
|
|
73
81
|
([WC-SECR-24](#wc-secr-24--a-run-with-moved-folders-shares-the-owners-keyring-entry-and-never-deletes-it)).
|
|
74
82
|
|
|
75
|
-
- **Why:** the keyring is the safest store on the computer
|
|
83
|
+
- **Why:** the keyring is the safest store on the computer, and a sign-in that fails on a server or in a container
|
|
84
|
+
because no keyring runs there is a broken CLI, not a safe one.
|
|
76
85
|
- **Enforced by:** `src/credentials.test.ts`.
|
|
77
86
|
- **Example:** a bot token is read from `MAX_BOT_TOKEN`, then the keyring entry `bot:<profile>`, then the file.
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
-
|
|
87
|
+
`gcal personal session start` on a computer without a keyring signs in, warns once, and answers
|
|
88
|
+
`credential_storage: "file"`.
|
|
89
|
+
- **Exceptions:** a messenger library's own device store, which the library opens as a file and which cannot live in
|
|
90
|
+
the keyring (WhatsApp's `private.db`, tg's session file), is an owner-only file under
|
|
91
|
+
[WC-SECR-10](#wc-secr-10--private-files-are-written-atomically-with-private-modes) instead. A copy between two
|
|
92
|
+
profiles that must land in the keyring (cli-core's bot transfer) may use `storage: "keyring"`.
|
|
93
|
+
- **Applies to:** all CLIs. **Since:** cli-base 0.41.0. **Rulings:** NEED-295; owner, 2026-10-31 (never ask, keyring
|
|
94
|
+
by default, fall back to a file on error, a discouraged flag for the file).
|
|
83
95
|
|
|
84
96
|
### WC-SECR-5 · A secret is never taken on the command line
|
|
85
97
|
|
|
@@ -274,7 +286,8 @@ general words.
|
|
|
274
286
|
`persist-credentials: false`, and `permissions: contents: read` at the top of the workflow. A job that needs more
|
|
275
287
|
asks for it on that job only. The `zizmor` scan in `release-checks.yml` reports high findings at release, but does
|
|
276
288
|
not fail on them.
|
|
277
|
-
- **Exceptions:**
|
|
289
|
+
- **Exceptions:** the `tag` job of `release.yml` keeps its checkout's credentials (no `persist-credentials: false`),
|
|
290
|
+
because it pushes `v<version>`; it runs no project code.
|
|
278
291
|
- **Applies to:** the WireCat repositories: every CLI, cli-base, cli-core, cli-testing, cli-docs and community; and
|
|
279
292
|
cli-template.
|
|
280
293
|
**Since:** cli-base 0.41.0. **Rulings:** none.
|
|
@@ -347,7 +360,8 @@ store and runs no search. A CLI declares its provider and may replace any step.
|
|
|
347
360
|
|
|
348
361
|
A CLI never revokes a sign-in at the provider in a moved run, even one read from the temporary entry: a refreshed
|
|
349
362
|
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
|
|
363
|
+
runs with `DBUS_SESSION_BUS_ADDRESS=disabled:`, since the keyring is reached over D-Bus. Removing the variable is not
|
|
364
|
+
enough: D-Bus then connects to `$XDG_RUNTIME_DIR/bus`, the default session bus, and reaches the keyring anyway.
|
|
351
365
|
- **Cleanup:** a moved run that never reaches `session end` leaves its temporary entry, and so does one that refreshes
|
|
352
366
|
a token, since the refreshed token is written there. `bin/keyring-dedupe --delete` in the projects folder removes
|
|
353
367
|
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
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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.
|
|
3
|
+
"version": "0.68.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",
|