@wirecat/cli-base 0.67.0 → 0.68.1
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 +43 -0
- package/dist/credentials.d.ts +11 -3
- package/dist/credentials.js +27 -15
- package/dist/dev/audit.d.ts +2 -0
- package/dist/dev/audit.js +26 -8
- package/dist/dev/main.js +2 -1
- package/dist/index.d.ts +1 -1
- package/dist/standards/audit.d.ts +3 -0
- package/dist/standards/audit.js +16 -2
- package/dist/standards/flags-how.js +7 -0
- package/docs/standards/README.md +1 -1
- package/docs/standards/accounts.md +5 -2
- package/docs/standards/mcp.md +5 -1
- package/docs/standards/releases.md +11 -12
- package/docs/standards/security.md +19 -7
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,49 @@ 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.1 — 31.10.2026
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- **The audit accepts a flag that one CLI lacks by design, by name.** A parity difference may carry `absent: "<cli>"`
|
|
16
|
+
with a target of `<path> <flag>` and no CLI list; it covers every finding where only that CLI lacks the flag and the
|
|
17
|
+
others agree, so another CLI joining the group does not make it stale. `PARITY_GROUPS` uses it to declare that memo
|
|
18
|
+
has no profiles of its own (no `--profiles` on `mcp serve`, `mcp revoke`, `mcp server start|status|stop`); memo
|
|
19
|
+
selects other CLIs' accounts with `--source` and `--account`. A run without the absent CLI does not call it stale.
|
|
20
|
+
WC-MCP-9 and WC-ACCT-1 say so. See [#390](https://github.com/WireCatLabs/cli-base/issues/390).
|
|
21
|
+
|
|
22
|
+
## 0.68.0 — 31.10.2026
|
|
23
|
+
|
|
24
|
+
### Removed
|
|
25
|
+
|
|
26
|
+
- **WC-REL-10: a messenger CLI is released after the owner's yes in the conversation, with no signed report.**
|
|
27
|
+
`bin/release` in max-cli and whatsapp-cli no longer reads a release report or a `Signed off:` line. The agent asks
|
|
28
|
+
the owner before it runs `bin/release`; one yes covers one version.
|
|
29
|
+
|
|
30
|
+
### Changed — may break callers
|
|
31
|
+
|
|
32
|
+
- **`Credentials` in `auto` does the whole file fallback itself (WC-SECR-4).** A secret the file holds is read from the
|
|
33
|
+
file before the keyring, so a locked keyring is not tried for it, and a refresh `write` keeps it there. A sign-in
|
|
34
|
+
passes `write(account, secret, { signIn: true })`: it tries the keyring and, once the keyring takes it, removes the
|
|
35
|
+
file copy. The "could not reach the OS keyring" warning comes once per run (per `env` object), not once per
|
|
36
|
+
`Credentials`. Watch for: a file copy left next to a newer keyring entry by an older CLI now wins the read; sign in
|
|
37
|
+
again to move it back. CLIs can drop their own file-first read, "remove the file copy" step and `warnOnce`.
|
|
38
|
+
- **WC-SECR-4 is the one credential-storage standard.** The keyring by default; when it fails, the file without
|
|
39
|
+
asking, one warning on stderr, and the answer that saved the secret says where it went;
|
|
40
|
+
`--credential-storage file` on the sign-in command chooses the file and is discouraged; a secret in the file is
|
|
41
|
+
read from there until a sign-in reaches the keyring again. No CLI asks, none is keyring-only. `Credentials` already
|
|
42
|
+
falls back this way in `auto`; the rule now says every CLI must use it.
|
|
43
|
+
|
|
44
|
+
### Fixed
|
|
45
|
+
|
|
46
|
+
- **`Credentials.remove` with `storage: "keyring"` leaves the file alone.** It also deleted the account from the
|
|
47
|
+
credentials file, so a CLI that cleared the keyring copy after a file sign-in lost the sign-in it had just saved.
|
|
48
|
+
|
|
49
|
+
### Added
|
|
50
|
+
|
|
51
|
+
- **`--credential-storage <storage>` in the flag vocabulary** (role `mode`), so `cli-dev lint` accepts it on a sign-in
|
|
52
|
+
command without a per-CLI override.
|
|
53
|
+
|
|
11
54
|
## 0.67.0 — 31.10.2026
|
|
12
55
|
|
|
13
56
|
### Added
|
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) {
|
|
@@ -91,14 +99,18 @@ export class Credentials {
|
|
|
91
99
|
"moved folders never removes it");
|
|
92
100
|
}
|
|
93
101
|
}
|
|
94
|
-
|
|
95
|
-
if (store?.[account] !== undefined) {
|
|
96
|
-
delete store[account];
|
|
97
|
-
this.#writeFile(store);
|
|
102
|
+
if (this.#storage !== "keyring" && this.#removeFromFile(account))
|
|
98
103
|
removed.push("file");
|
|
99
|
-
}
|
|
100
104
|
return removed;
|
|
101
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
|
+
}
|
|
102
114
|
/** Only for the warning: a keyring that fails here must not fail a removal that already happened. */
|
|
103
115
|
#ownersEntryExists(account) {
|
|
104
116
|
try {
|
|
@@ -141,8 +153,8 @@ export class Credentials {
|
|
|
141
153
|
return operation();
|
|
142
154
|
}
|
|
143
155
|
catch (error) {
|
|
144
|
-
if (!this.#
|
|
145
|
-
this.#
|
|
156
|
+
if (!warnedRuns.has(this.#env)) {
|
|
157
|
+
warnedRuns.add(this.#env);
|
|
146
158
|
this.#warn(`could not reach the OS keyring — it may be locked, or no keyring service is running ` +
|
|
147
159
|
`(${error instanceof Error ? error.message : String(error)}); ` +
|
|
148
160
|
`using ${this.#path()} instead, readable only by you`);
|
package/dist/dev/audit.d.ts
CHANGED
|
@@ -26,6 +26,8 @@ export interface Finding {
|
|
|
26
26
|
/** `<clis> <what>`: `max+tg chats list --limit`, `zm upgrade`, `tg: chats grab` for a lint violation. */
|
|
27
27
|
target: string;
|
|
28
28
|
message: string;
|
|
29
|
+
/** What each CLI showed, for a finding about one flag or output; `absent` when it lacks the flag. */
|
|
30
|
+
values?: Record<string, string>;
|
|
29
31
|
}
|
|
30
32
|
export interface ScoreRow {
|
|
31
33
|
cli: string;
|
package/dist/dev/audit.js
CHANGED
|
@@ -74,6 +74,7 @@ const samePathContract = (inputs, groups) => {
|
|
|
74
74
|
clis: having,
|
|
75
75
|
target: `${named(having)} ${path}${what}`,
|
|
76
76
|
message: describe(values),
|
|
77
|
+
values: Object.fromEntries(values),
|
|
77
78
|
});
|
|
78
79
|
};
|
|
79
80
|
const kinds = new Map([...commands].map(([cli, command]) => [
|
|
@@ -253,18 +254,35 @@ export const auditClis = (inputs, options, rules = AUDIT_RULES) => {
|
|
|
253
254
|
options.capabilities &&
|
|
254
255
|
capabilityDifference(finding.target, finding.clis, inputs.map((input) => input.document), options.capabilities))
|
|
255
256
|
.map((finding) => `${finding.rule} ${finding.target}`));
|
|
256
|
-
|
|
257
|
+
// An `absent` difference names no CLI list: it stands for every finding where only that CLI lacks the flag.
|
|
258
|
+
const differences = options.groups.flatMap((group) => group.differences.flatMap((entry) => {
|
|
259
|
+
if (!entry.absent)
|
|
260
|
+
return [{ group: group.name, ...entry }];
|
|
261
|
+
const matching = findings.filter((finding) => {
|
|
262
|
+
if (finding.rule !== entry.rule || finding.target !== `${named(finding.clis)} ${entry.target}`)
|
|
263
|
+
return false;
|
|
264
|
+
const others = Object.entries(finding.values ?? {}).filter(([cli]) => cli !== entry.absent);
|
|
265
|
+
return (finding.values?.[entry.absent] === "absent" && new Set(others.map(([, value]) => value)).size === 1);
|
|
266
|
+
});
|
|
267
|
+
if (matching.length === 0 && clis.includes(entry.absent))
|
|
268
|
+
problems.push(`stale difference in ${group.name}, no such divergence — remove it: ${entry.rule} ${entry.target} (absent in ${entry.absent})`);
|
|
269
|
+
return matching.map((finding) => ({
|
|
270
|
+
...entry,
|
|
271
|
+
group: group.name,
|
|
272
|
+
target: finding.target,
|
|
273
|
+
expected: finding.message,
|
|
274
|
+
}));
|
|
275
|
+
}));
|
|
276
|
+
const declared = new Map(differences.map((entry) => [`${entry.rule} ${entry.target}`, entry.group]));
|
|
257
277
|
const found = new Set(findings.map((finding) => `${finding.rule} ${finding.target}`));
|
|
258
278
|
for (const [key, group] of declared)
|
|
259
279
|
if (!found.has(key))
|
|
260
280
|
problems.push(`stale difference in ${group}, no such divergence — remove it: ${key}`);
|
|
261
|
-
for (const
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
problems.push(`difference in ${group.name} must match the exact current values: ${difference.rule} ${difference.target}`);
|
|
267
|
-
}
|
|
281
|
+
for (const difference of differences) {
|
|
282
|
+
const finding = findings.find((one) => one.rule === difference.rule && one.target === difference.target);
|
|
283
|
+
if (finding && difference.expected !== finding.message) {
|
|
284
|
+
declared.delete(`${difference.rule} ${difference.target}`);
|
|
285
|
+
problems.push(`difference in ${difference.group} must match the exact current values: ${difference.rule} ${difference.target}`);
|
|
268
286
|
}
|
|
269
287
|
}
|
|
270
288
|
for (const group of options.groups)
|
package/dist/dev/main.js
CHANGED
|
@@ -575,7 +575,8 @@ const auditRun = (args, io) => {
|
|
|
575
575
|
group: v.string(),
|
|
576
576
|
rule: v.string(),
|
|
577
577
|
target: v.string(),
|
|
578
|
-
expected: v.string(),
|
|
578
|
+
expected: v.optional(v.string()),
|
|
579
|
+
absent: v.optional(v.string()),
|
|
579
580
|
reason: v.string(),
|
|
580
581
|
}));
|
|
581
582
|
const parsed = v.safeParse(schema, readJson(at(values.differences)));
|
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";
|
|
@@ -22,8 +22,11 @@ declare const parityGroupSchema: v.StrictObjectSchema<{
|
|
|
22
22
|
/** Permanent differences, each with its reason; they count against the override budget of the group. */
|
|
23
23
|
readonly differences: v.ArraySchema<v.StrictObjectSchema<{
|
|
24
24
|
readonly rule: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a lower-case word, words joined by -">]>;
|
|
25
|
+
/** `<clis> <path> <flag>`; with `absent`, only `<path> <flag>`: the CLI list is whoever else has it. */
|
|
25
26
|
readonly target: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "must not be empty">]>;
|
|
26
27
|
readonly expected: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "must not be empty">]>, undefined>;
|
|
28
|
+
/** The CLI that lacks the path or flag by design; the rest must agree, so other CLIs joining do not stale it. */
|
|
29
|
+
readonly absent: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a lower-case word, words joined by -">]>, undefined>;
|
|
27
30
|
readonly reason: v.SchemaWithPipe<readonly [v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "must not be empty">]>, v.MinLengthAction<string, 10, "say why, in a sentence">]>;
|
|
28
31
|
}, undefined>, undefined>;
|
|
29
32
|
}, undefined>;
|
package/dist/standards/audit.js
CHANGED
|
@@ -51,14 +51,22 @@ const parityGroupSchema = v.strictObject({
|
|
|
51
51
|
/** Permanent differences, each with its reason; they count against the override budget of the group. */
|
|
52
52
|
differences: v.array(v.strictObject({
|
|
53
53
|
rule: word,
|
|
54
|
+
/** `<clis> <path> <flag>`; with `absent`, only `<path> <flag>`: the CLI list is whoever else has it. */
|
|
54
55
|
target: text,
|
|
55
56
|
expected: v.optional(text),
|
|
57
|
+
/** The CLI that lacks the path or flag by design; the rest must agree, so other CLIs joining do not stale it. */
|
|
58
|
+
absent: v.optional(word),
|
|
56
59
|
reason: v.pipe(text, v.minLength(10, "say why, in a sentence")),
|
|
57
60
|
})),
|
|
58
61
|
});
|
|
59
62
|
export const defineParityGroups = (input) => parseRegistry("parity groups", v.array(parityGroupSchema), input, (groups) => [
|
|
60
63
|
...duplicates(groups.map((group) => group.name)).map((name) => `"${name}" is listed twice`),
|
|
61
|
-
...groups.flatMap((group) =>
|
|
64
|
+
...groups.flatMap((group) => [
|
|
65
|
+
...duplicates(group.differences.map((entry) => `${entry.rule} ${entry.target}`)).map((both) => `${group.name}: "${both}" is listed twice`),
|
|
66
|
+
...group.differences
|
|
67
|
+
.filter((entry) => entry.absent && entry.expected)
|
|
68
|
+
.map((entry) => `${group.name}: "${entry.target}" has \`absent\`, which measures the values itself; drop \`expected\``),
|
|
69
|
+
]),
|
|
62
70
|
]);
|
|
63
71
|
export const PARITY_GROUPS = defineParityGroups([
|
|
64
72
|
{ name: "messengers", members: ["tg", "max"], covers: "tree", differences: [] },
|
|
@@ -66,7 +74,13 @@ export const PARITY_GROUPS = defineParityGroups([
|
|
|
66
74
|
name: "all",
|
|
67
75
|
members: ["tg", "max", "memo", "drive", "zm", "wa", "mail"],
|
|
68
76
|
covers: ["commands", "doctor", "skill", "mcp", "upgrade", "search", "notes", "meetings"],
|
|
69
|
-
differences: []
|
|
77
|
+
differences: ["mcp serve", "mcp revoke", "mcp server start", "mcp server status", "mcp server stop"].map((path) => ({
|
|
78
|
+
rule: "same-path-contract",
|
|
79
|
+
target: `${path} --profiles`,
|
|
80
|
+
absent: "memo",
|
|
81
|
+
reason: "memo has no profiles: it reads other CLIs' stores and selects their accounts with --source and --account " +
|
|
82
|
+
"(https://github.com/WireCatLabs/cli-base/issues/390)",
|
|
83
|
+
})),
|
|
70
84
|
},
|
|
71
85
|
// gcal is compared on what it already shares with every CLI; doctor, mcp, search and skill wait until the published
|
|
72
86
|
// CLIs agree there (https://github.com/WireCatLabs/cli-base/issues/390), then gcal moves back into "all".
|
|
@@ -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",
|
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 |
|
|
@@ -14,7 +14,9 @@ Rule ids on this page are `WC-ACCT-<n>`. How to read a rule, how ids work and wh
|
|
|
14
14
|
### Who follows these rules today
|
|
15
15
|
|
|
16
16
|
cli-core resolves the profile for tg, max and wa. WC-ACCT-1 needs cli-core 0.319.0 or newer. memo has no messenger profile: its shared commands run without one, and they pick accounts with `--provider`
|
|
17
|
-
and `--account` (WC-ACCT-7).
|
|
17
|
+
and `--account` (WC-ACCT-7). memo never creates profiles of its own: it reads the stores of other CLIs and selects
|
|
18
|
+
their accounts with `--source email:<name>` or `--account telegram:1`, so it has no `--profiles` either
|
|
19
|
+
([cli-base#390](https://github.com/WireCatLabs/cli-base/issues/390), owner decision 2026-10-31). zoom resolves its own profile word by the same rules (`zm <profile> …`, `ZM_PROFILE`,
|
|
18
20
|
`ZM_PROFILE_LOCK`) from zoom-cli 0.35.0, with cli-core's `liftProfile` and `usableProfileName`. drive still has its own
|
|
19
21
|
profile code: it registers its own profiles. That is a known gap, not an exception. gcal resolves the profile through
|
|
20
22
|
cli-core's `run()`, like tg, max and wa. Which CLIs lack a sign-in command of WC-ACCT-12 is listed in that rule.
|
|
@@ -48,7 +50,8 @@ profile.
|
|
|
48
50
|
- **Example:** `max work bot messages send …` uses the same first word as `max work chats list`. `tg --json chats list`
|
|
49
51
|
still works, because `--json` is an option.
|
|
50
52
|
- **Exceptions:** `mcp serve --profiles <names|all>` names the profiles one server serves ([WC-MCP-9](mcp.md#wc-mcp-9--a-server-serves-the-profiles-it-is-told-with-one-connection-for-each)). It is used in
|
|
51
|
-
place of the first word, never with it.
|
|
53
|
+
place of the first word, never with it. memo has no profiles of its own, so it has neither the first word nor
|
|
54
|
+
`--profiles`; it selects the accounts of other CLIs with `--source` and `--account`.
|
|
52
55
|
- **Applies to:** all CLIs. **Since:** cli-base 0.41.0. **Rulings:** NEED-84, NEED-294, NEED-1881.
|
|
53
56
|
|
|
54
57
|
### WC-ACCT-3 · A missing profile is an error that lists the choices
|
package/docs/standards/mcp.md
CHANGED
|
@@ -155,7 +155,11 @@ and lists the profiles served. Each call follows its own profile's permissions.
|
|
|
155
155
|
- **Example:** `max work mcp serve`, `max mcp serve --profiles work,personal` or `max mcp serve --profiles all`.
|
|
156
156
|
Under the last two, a call reads `max_read { "profile": "work", "command": "inbox", "arguments": {} }`.
|
|
157
157
|
`max mcp serve` alone is refused, and its message lists `personal` and `work`.
|
|
158
|
-
- **Exceptions:** the bot server needs no held connection: each Bot API call is plain HTTPS.
|
|
158
|
+
- **Exceptions:** the bot server needs no held connection: each Bot API call is plain HTTPS. memo has no profiles of
|
|
159
|
+
its own, so `--profiles` stays absent on its `mcp serve`, `mcp revoke` and `mcp server start`, `status` and `stop`;
|
|
160
|
+
it reads other CLIs' stores and selects their accounts with `--source email:<name>` or `--account telegram:1`
|
|
161
|
+
([cli-base#390](https://github.com/WireCatLabs/cli-base/issues/390), owner decision 2026-10-31). The audit declares
|
|
162
|
+
this once, as a difference of memo, not of the other CLIs.
|
|
159
163
|
- **Applies to:** messenger CLIs. **Since:** cli-base 0.41.0. **Rulings:** NEED-1881.
|
|
160
164
|
|
|
161
165
|
### WC-MCP-10 · A tool and its command give the same answer and the same error
|
|
@@ -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
|
|
@@ -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
|
|
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.
|
|
109
|
+
### WC-REL-10 · A messenger CLI is released only after the owner says yes in the conversation
|
|
110
|
+
|
|
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
|
|
|
@@ -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
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wirecat/cli-base",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.68.1",
|
|
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",
|