@wirecat/cli-base 0.64.0 → 0.65.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +119 -0
- package/dist/dev/bundle.d.ts +6 -0
- package/dist/dev/bundle.js +12 -3
- package/dist/standards/audit.js +9 -1
- package/dist/standards/flags-calendar.js +13 -0
- package/dist/standards/flags-what.js +7 -0
- package/dist/standards/vocabulary.js +3 -1
- package/docs/standards/README.md +2 -2
- package/docs/standards/accounts.md +42 -9
- package/docs/standards/architecture.md +19 -12
- package/docs/standards/commands.md +36 -12
- package/docs/standards/data-model.md +5 -1
- package/docs/standards/docs.md +4 -1
- package/docs/standards/kit-surface.md +5 -4
- package/docs/standards/lifecycle.md +69 -8
- package/docs/standards/mcp.md +48 -24
- package/docs/standards/new-cli.md +2 -2
- package/docs/standards/output.md +13 -2
- package/docs/standards/permissions.md +24 -11
- package/docs/standards/releases.md +8 -2
- package/docs/standards/security.md +12 -5
- package/docs/standards/store.md +3 -2
- package/docs/standards/testing.md +10 -5
- package/docs/standards/time.md +8 -1
- package/docs/standards/toolchain.md +7 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,125 @@ 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.65.0 — 27.10.2026
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- **Calendar words for moving an event and for "this and all following".** Flag `--following` (`target`, switch):
|
|
16
|
+
this occurrence and every later one of a repeating event (`events update|delete --following`), beside `--series`.
|
|
17
|
+
`--scope` already means personal or work, so it is not reused. Flag `--to-calendar <id>` (`target`, id): the
|
|
18
|
+
calendar an event moves to (`events move --to-calendar`); `--calendar` keeps its meaning, the calendar the event is
|
|
19
|
+
on. The verb `move` now says what moves in general: a file to a folder, an event to a calendar. cli-core's shared
|
|
20
|
+
calendar commands use all three; gcal-cli carries `lint/overrides.json` entries for them until this release.
|
|
21
|
+
- **Every CLI that signs in has `session start`, `session status`, `session end` and a profile-free `session list`.**
|
|
22
|
+
[WC-ACCT-12](docs/standards/accounts.md#wc-acct-12--sign-in-is-session-start-status-and-end-and-session-list-names-the-profiles)
|
|
23
|
+
is the owner's ruling: `status` says whether one profile is signed in, `list` which profiles exist. Each
|
|
24
|
+
CLI's missing commands are listed as known gaps: tg, max and wa lack `session status` and list profiles with
|
|
25
|
+
`account list`; drive lacks `session status`; gcal lacks `session list`; zm lacks `session end` and `session list`;
|
|
26
|
+
memo has no `session` group.
|
|
27
|
+
- **A sync spends a request budget sized to its deadline and says how to continue.**
|
|
28
|
+
[WC-LIFE-16](docs/standards/lifecycle.md#wc-life-16--a-sync-spends-a-request-budget-sized-to-its-deadline-and-its-answer-says-how-to-continue)
|
|
29
|
+
describes what cli-core 0.464.0 (cli-core #2118) builds for the calendar sync: `floor((deadline − margin) /
|
|
30
|
+
requestMs)` requests, one `requestMs` per CLI, `--max-pages` only lowering it, a partial stop that exits 0 with the
|
|
31
|
+
exact command to continue, and progress in the sync's status command. The other syncs and gcal's own `requestMs`
|
|
32
|
+
are listed as gaps.
|
|
33
|
+
|
|
34
|
+
- **The command vocabulary has the verbs `serve` (run a server in this process and answer its clients until stopped)
|
|
35
|
+
and `revoke` (take back what was granted, so it stops working).** cli-core 0.463.0 names the MCP server
|
|
36
|
+
`mcp serve` and gives forgetting browser logins its own command, `mcp revoke`; `revoke` also covers
|
|
37
|
+
`chats link revoke` and the `consents revoke` commands that were baselined as unknown verbs.
|
|
38
|
+
|
|
39
|
+
### Changed — may break callers
|
|
40
|
+
|
|
41
|
+
- **A command that reads the service and only saves what it read is a read, also over MCP.**
|
|
42
|
+
[WC-CMD-17](docs/standards/commands.md#wc-cmd-17--declare-effects-on-every-command) defines a write, and WC-CMD-6,
|
|
43
|
+
WC-CMD-28, [WC-MCP-4](docs/standards/mcp.md#wc-mcp-4--read-runs-only-reading-commands-and-write-only-writing-ones)
|
|
44
|
+
and WC-OUT-9 point at it: `{ remote: "read", local: "write" }` (`sync`, `calendars list`) runs through the `read`
|
|
45
|
+
tool; `{ remote: "none", local: "write" }` (`store reindex`) stays a write. cli-base's `writes()` still counts every
|
|
46
|
+
local write, so cli-core serves `sync` through `write` until a follow-up changes it; watch for that release, which
|
|
47
|
+
moves those commands between tools.
|
|
48
|
+
- **Deadlines: 30 s for a one-shot command, 5 min for a cooperative one at a terminal and 45 s over MCP.**
|
|
49
|
+
[WC-LIFE-5](docs/standards/lifecycle.md#wc-life-5--bound-every-one-shot-command-by-a-deadline) and WC-LIFE-10 say
|
|
50
|
+
so, as cli-core 0.464.0 builds it, because MCP clients cut a tool call at about 60 s.
|
|
51
|
+
- **A non-messenger CLI allows ordinary writes, asks before deletes, and denies mail to other people and deleting
|
|
52
|
+
a whole series.**
|
|
53
|
+
[WC-PERM-22](docs/standards/permissions.md#wc-perm-22--a-cli-that-is-not-a-messenger-declares-its-own-keys-and-defaults-with-commandpermissions)
|
|
54
|
+
follows the owner's ruling, and a CLI's generic API path must check the shared keys (`events.notify`,
|
|
55
|
+
`events.delete.series`) too. Its example said sharing a calendar is `deny`; the owner made it `ask`. cli-core's
|
|
56
|
+
`CALENDAR_WRITE_DEFAULTS` and gcal's `api` defaults do not follow it yet; both are listed as gaps.
|
|
57
|
+
- **Meeting passcodes stay out of command output and agent answers, not only out of the store.**
|
|
58
|
+
[WC-SECR-23](docs/standards/security.md) covers answers that come straight from the provider: gcal's `api` answers
|
|
59
|
+
still keep a description's passcode lines, a listed gap.
|
|
60
|
+
- **Each CLI pins the command catalogue behind its MCP tools.** The [MCP](docs/standards/mcp.md) page no longer
|
|
61
|
+
claims `mcp-tools.json` pins the catalogue: with three tools it does not. `contracts/mcp-catalogue.json` does; tg and
|
|
62
|
+
max have it, and wa, zm, gcal and drive do not yet.
|
|
63
|
+
|
|
64
|
+
- **An MCP server serves the profiles it is told, and refuses without them.**
|
|
65
|
+
[WC-MCP-9](docs/standards/mcp.md#wc-mcp-9--a-server-serves-the-profiles-it-is-told-with-one-connection-for-each)
|
|
66
|
+
now allows one named profile (`<cli> <profile> mcp serve`), a list (`<cli> mcp serve --profiles work,personal`) or
|
|
67
|
+
every profile (`<cli> mcp serve --profiles all`), with one messenger connection per served profile. With no profile
|
|
68
|
+
and no `--profiles`, `mcp serve` fails with `validation_error` and lists the profiles; it never serves every profile
|
|
69
|
+
by default. With several profiles, each call names its profile. WC-ACCT-1, WC-ACCT-2 and WC-ACCT-5 say how
|
|
70
|
+
`--profiles` fits the profile rules, and a profile lock refuses `--profiles all`. The flag registry has `--profiles`.
|
|
71
|
+
The owner ruled it; cli-core does not build `--profiles` yet.
|
|
72
|
+
- **The handbook names the MCP server `mcp serve` and has no `mcp --revoke` exception.** The
|
|
73
|
+
[MCP](docs/standards/mcp.md) and [Lifecycle](docs/standards/lifecycle.md) pages say `mcp serve`, `mcp serve --http`,
|
|
74
|
+
`mcp serve --permission`, `bot mcp serve` and `mcp revoke`, as cli-core 0.463.0 builds them.
|
|
75
|
+
[WC-CMD-6](docs/standards/commands.md#wc-cmd-6--give-one-action-or-one-report-one-leaf-command) no longer lets
|
|
76
|
+
`--revoke` stay an option: forgetting every browser login is the `mcp revoke` command. A CLI with its own server
|
|
77
|
+
(drive, memo, zoom) moves to the same shape.
|
|
78
|
+
- **A lint baseline entry for `serve` or `revoke` goes stale, and `cli-dev lint` fails on it.** cli-core lists
|
|
79
|
+
`bot mcp serve`, `mcp serve`, `mcp revoke` and `replies consents revoke` in `lint/baseline/unknown-verb.json`;
|
|
80
|
+
tg-cli and max-cli list `replies consents revoke`. Remove those entries in the same change that takes this version.
|
|
81
|
+
- **The HTTP MCP server rule covers every CLI that offers `mcp serve --http`, not only the messenger CLIs.**
|
|
82
|
+
[WC-MCP-12](docs/standards/mcp.md#wc-mcp-12--an-mcp-server-over-http-follows-the-same-permissions-and-short-lived-logins)
|
|
83
|
+
now applies to cli-template and every CLI made from it, which have offered `mcp serve --http` with no rule over it.
|
|
84
|
+
The template already follows it; a CLI that changed its copy of the HTTP server checks it against the rule.
|
|
85
|
+
|
|
86
|
+
### Fixed
|
|
87
|
+
|
|
88
|
+
- **`cli-dev audit` compares gcal only on the commands it already shares with every CLI, for now.** cli-base 0.64.0
|
|
89
|
+
put gcal into the `all` parity group, and gcal's release audit then blocked on 20 `same-path-contract` differences
|
|
90
|
+
in `doctor`, `mcp`, `search` and `skill` that already exist among the other six published CLIs and that gcal
|
|
91
|
+
cannot remove alone (gcal-cli#13). gcal leaves `all` and has its own group, `gcal-shared`, with every CLI over
|
|
92
|
+
`commands`, `upgrade`, `notes` and `meetings`. It rejoins `all` once tg, max, wa, drive, zoom and memo release on
|
|
93
|
+
cli-core's shared `mcp serve`, `doctor` and `skill` ([#390](https://github.com/WireCatLabs/cli-base/issues/390)).
|
|
94
|
+
The other CLIs' audits are unchanged: a path is compared across every member of every group that covers it.
|
|
95
|
+
|
|
96
|
+
- **`cli-dev bundle`'s loader finds a server after a flag: `<cli> personal --read-only mcp serve`.** It looked for
|
|
97
|
+
`serve`, `watch` or `mcp` in the first three words of the command, and since cli-core 0.463 named the server
|
|
98
|
+
`mcp serve`, a flag before it pushed `serve` to the fourth word. Such a server loaded no chunks ahead and could fail
|
|
99
|
+
after an in-place upgrade (measured on drive: 91 of 138 chunks loaded, instead of 138). The loader now skips flags
|
|
100
|
+
and looks at four words. Rebuild a CLI with this release to take it.
|
|
101
|
+
- **The handbook no longer contradicts itself on gcal's audit findings.**
|
|
102
|
+
- WC-OUT-8 and WC-OUT-14: an answer with `meta.complete: false` is an answer on stdout, also after a non-zero
|
|
103
|
+
exit; only a run with no answer prints one JSON error with stdout empty.
|
|
104
|
+
- WC-CMD-10 names `--max-pages` a `budget` option, not a paging one, and says why the calendar `--id` exists beside
|
|
105
|
+
`--operation-id`; gcal's `events create`, with `--id` and no `--operation-id`, is a listed gap. WC-CMD-11 lists `--duration-minutes` as a known gap until the vocabulary has
|
|
106
|
+
`--duration <duration>`.
|
|
107
|
+
- WC-REL-16 says `packContents` checks the pack against the list it is given, so a check that passes the `files`
|
|
108
|
+
list itself cannot fail on a wrong list; the fix belongs in cli-template.
|
|
109
|
+
- WC-TIME-5 matches an all-day event by local dates, not UTC moments; cli-core's event filter is a listed gap.
|
|
110
|
+
- WC-ARCH-3 counts generated API types as vendor types, WC-ARCH-5 and WC-ARCH-6 cover calendar CLIs, and
|
|
111
|
+
WC-ARCH-21's example is gcal's real provider entry.
|
|
112
|
+
- WC-TOOL-16 puts a word waiting for the vocabulary in `lint/overrides.json` with a link and a date, never in the
|
|
113
|
+
baseline. The [Documents](docs/standards/docs.md#wc-doc-3--every-document-states-current-facts-rewrite-a-wrong-sentence-in-place)
|
|
114
|
+
rule on current facts sends a false template-owned file to cli-template.
|
|
115
|
+
- WC-DATA-3 makes a calendar entry's `external_id` unique per calendar, WC-STORE-3 lists the calendar areas as not
|
|
116
|
+
bound, WC-TEST-12 says what a reason names, and WC-TEST-14 counts the shared store as local state.
|
|
117
|
+
- The "who follows" lists name gcal.
|
|
118
|
+
|
|
119
|
+
- **The handbook's pages on the template match cli-template 0.9.0.** The [Kit surface](docs/standards/kit-surface.md)
|
|
120
|
+
table lists `LOCAL_WRITE` and `revokeAll`, which the template now imports, and counts imports loaded on demand.
|
|
121
|
+
[New CLI](docs/standards/new-cli.md) says the MCP server has three tools that reach every command, not a tool for
|
|
122
|
+
every command.
|
|
123
|
+
- **The handbook says where a lint violation that a shared package forces goes, and which cli-core files are not a
|
|
124
|
+
CLI's local state.** [WC-TOOL-16](docs/standards/toolchain.md) puts such a violation in the CLI's lint baseline
|
|
125
|
+
until the shared package catches up.
|
|
126
|
+
[WC-TEST-14](docs/standards/testing.md#wc-test-14--check-the-upgrade-from-the-previous-published-version) names
|
|
127
|
+
the update check, MCP logins and lock files that only cli-core writes as outside the upgrade contract; tests still
|
|
128
|
+
keep them in the sandboxed home.
|
|
129
|
+
|
|
11
130
|
## 0.64.0 — 27.10.2026
|
|
12
131
|
|
|
13
132
|
### Added
|
package/dist/dev/bundle.d.ts
CHANGED
|
@@ -20,6 +20,12 @@ export declare const DEFAULT_EXTERNAL: string[];
|
|
|
20
20
|
export declare const DEFAULT_ANCHORED: string[];
|
|
21
21
|
export declare const DEFAULT_HARMLESS: string[];
|
|
22
22
|
export declare const LOADER_MARK = "// cli-dev bundle loader\n";
|
|
23
|
+
/**
|
|
24
|
+
* Whether the loader runs a server: `serve`, `watch` or `mcp serve`, or `mcp` alone as before cli-core 0.463. Flags
|
|
25
|
+
* are skipped, so `<cli> personal --read-only mcp serve` counts; four words leave room for one flag's value. It is
|
|
26
|
+
* written into the loader as source, so it uses nothing from outside.
|
|
27
|
+
*/
|
|
28
|
+
export declare const isServer: (words: readonly string[]) => boolean;
|
|
23
29
|
/** The bin that replaces the compiled one: `name` is the bundle's entry, `chunks` what a server preloads. */
|
|
24
30
|
export declare const loaderSource: (name: string, version: string, chunks: readonly string[]) => string;
|
|
25
31
|
export declare const bundle: (options: BundleOptions) => Promise<BundleResult>;
|
package/dist/dev/bundle.js
CHANGED
|
@@ -166,6 +166,17 @@ const commonJsNames = {
|
|
|
166
166
|
});
|
|
167
167
|
},
|
|
168
168
|
};
|
|
169
|
+
/**
|
|
170
|
+
* Whether the loader runs a server: `serve`, `watch` or `mcp serve`, or `mcp` alone as before cli-core 0.463. Flags
|
|
171
|
+
* are skipped, so `<cli> personal --read-only mcp serve` counts; four words leave room for one flag's value. It is
|
|
172
|
+
* written into the loader as source, so it uses nothing from outside.
|
|
173
|
+
*/
|
|
174
|
+
export const isServer = (words) => {
|
|
175
|
+
const plain = words.filter((word) => !word.startsWith("-"));
|
|
176
|
+
return plain
|
|
177
|
+
.slice(0, 4)
|
|
178
|
+
.some((word, at) => word === "serve" || word === "watch" || (word === "mcp" && plain[at + 1] === undefined));
|
|
179
|
+
};
|
|
169
180
|
/** The bin that replaces the compiled one: `name` is the bundle's entry, `chunks` what a server preloads. */
|
|
170
181
|
export const loaderSource = (name, version, chunks) => `#!/usr/bin/env node
|
|
171
182
|
${LOADER_MARK}import * as module from "node:module"
|
|
@@ -181,9 +192,7 @@ if (
|
|
|
181
192
|
} else {
|
|
182
193
|
// Before the bundle compiles: the bundle's own call comes after its chunks.
|
|
183
194
|
module.enableCompileCache()
|
|
184
|
-
const serves = words
|
|
185
|
-
.slice(0, 3)
|
|
186
|
-
.some((word, at) => ["serve", "watch"].includes(word) || (word === "mcp" && !/^[^-]/.test(words[at + 1] ?? "")))
|
|
195
|
+
const serves = (${isServer.toString()})(words)
|
|
187
196
|
if (serves && module.registerHooks) {
|
|
188
197
|
const bundle = new URL("../bundle/", import.meta.url).href
|
|
189
198
|
module.registerHooks({
|
package/dist/standards/audit.js
CHANGED
|
@@ -64,10 +64,18 @@ export const PARITY_GROUPS = defineParityGroups([
|
|
|
64
64
|
{ name: "messengers", members: ["tg", "max"], covers: "tree", differences: [] },
|
|
65
65
|
{
|
|
66
66
|
name: "all",
|
|
67
|
-
members: ["tg", "max", "memo", "drive", "zm", "wa"
|
|
67
|
+
members: ["tg", "max", "memo", "drive", "zm", "wa"],
|
|
68
68
|
covers: ["commands", "doctor", "skill", "mcp", "upgrade", "search", "notes", "meetings"],
|
|
69
69
|
differences: [],
|
|
70
70
|
},
|
|
71
|
+
// gcal is compared on what it already shares with every CLI; doctor, mcp, search and skill wait until the published
|
|
72
|
+
// CLIs agree there (https://github.com/WireCatLabs/cli-base/issues/390), then gcal moves back into "all".
|
|
73
|
+
{
|
|
74
|
+
name: "gcal-shared",
|
|
75
|
+
members: ["tg", "max", "memo", "drive", "zm", "wa", "gcal"],
|
|
76
|
+
covers: ["commands", "upgrade", "notes", "meetings"],
|
|
77
|
+
differences: [],
|
|
78
|
+
},
|
|
71
79
|
]);
|
|
72
80
|
const requiredRootSchema = v.strictObject({ path: v.pipe(v.array(segment), v.minLength(1)), meaning: text });
|
|
73
81
|
export const defineRequiredRoots = (input) => parseRegistry("required roots", v.array(requiredRootSchema), input, (roots) => duplicates(roots.map((root) => root.path.join(" "))).map((path) => `"${path}" is listed twice`));
|
|
@@ -56,4 +56,17 @@ export const CALENDAR_FLAGS = [
|
|
|
56
56
|
type: "switch",
|
|
57
57
|
meaning: "act on the whole repeating event, not only this occurrence",
|
|
58
58
|
},
|
|
59
|
+
{
|
|
60
|
+
name: "--following",
|
|
61
|
+
role: "target",
|
|
62
|
+
type: "switch",
|
|
63
|
+
meaning: "act on this occurrence and every later one of the repeating event",
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
name: "--to-calendar",
|
|
67
|
+
value: "<id>",
|
|
68
|
+
role: "target",
|
|
69
|
+
type: "id",
|
|
70
|
+
meaning: "the calendar it moves to, of the same account",
|
|
71
|
+
},
|
|
59
72
|
];
|
|
@@ -187,6 +187,13 @@ export const WHAT_FLAGS = [
|
|
|
187
187
|
type: "string",
|
|
188
188
|
meaning: "the profile to use, instead of the first word",
|
|
189
189
|
},
|
|
190
|
+
{
|
|
191
|
+
name: "--profiles",
|
|
192
|
+
value: "<names|all>",
|
|
193
|
+
role: "scope",
|
|
194
|
+
type: "list",
|
|
195
|
+
meaning: "the profiles an MCP server serves, comma-separated, or all of them",
|
|
196
|
+
},
|
|
190
197
|
{
|
|
191
198
|
name: "--history-since-time",
|
|
192
199
|
value: "<time>",
|
|
@@ -115,7 +115,9 @@ const verbs = [
|
|
|
115
115
|
["start", "start a running process, or a login session"],
|
|
116
116
|
["stop", "stop a running process"],
|
|
117
117
|
["restart", "stop and start a running process"],
|
|
118
|
+
["serve", "run a server in this process and answer its clients until stopped"],
|
|
118
119
|
["end", "end a login session"],
|
|
120
|
+
["revoke", "take back what was granted, so it stops working: a login, an invite link, a consent"],
|
|
119
121
|
["install", "install a system unit"],
|
|
120
122
|
["uninstall", "remove a system unit"],
|
|
121
123
|
["cancel", "stop a job or a pending thing"],
|
|
@@ -180,7 +182,7 @@ const verbs = [
|
|
|
180
182
|
["reject", "the owner refuses what an agent proposed"],
|
|
181
183
|
["review", "list what waits for the owner's decision"],
|
|
182
184
|
["resolve", "settle one thing that waits for the owner's decision, with the owner's choice"],
|
|
183
|
-
["move", "move a
|
|
185
|
+
["move", "move a thing to another place in the source: a file to a folder, an event to a calendar"],
|
|
184
186
|
["copy", "copy a file in the source"],
|
|
185
187
|
["run", "run a saved job once, now, as its timer would"],
|
|
186
188
|
["claim", "take a thing for a while so no other agent works it at once; the claim lapses by itself"],
|
package/docs/standards/README.md
CHANGED
|
@@ -32,10 +32,10 @@ the four steps that need a person. A new CLI is ready for its first release when
|
|
|
32
32
|
| [Commands](commands.md) | `CMD` | 28 | Paths, vocabulary, flags, arguments, effects, lifetime, query, help, external references |
|
|
33
33
|
| [Output](output.md) | `OUT` | 23 | Data on stdout only, envelopes, errors, exit codes, partial answers, projection |
|
|
34
34
|
| [MCP](mcp.md) | `MCP` | 13 | The MCP tools, how they come from commands, what each permission shows |
|
|
35
|
-
| [Accounts](accounts.md) | `ACCT` |
|
|
35
|
+
| [Accounts](accounts.md) | `ACCT` | 12 | The explicit profile, reads across accounts, scope flags, linked identities, sign-in commands |
|
|
36
36
|
| [Permissions](permissions.md) | `PERM` | 22 | Levels, keys, defaults, hard limits, the write guard and its journal |
|
|
37
37
|
| [Architecture](architecture.md) | `ARCH` | 17 | Layers, dependency direction, vendor types, what goes in which package |
|
|
38
|
-
| [Lifecycle](lifecycle.md) | `LIFE` |
|
|
38
|
+
| [Lifecycle](lifecycle.md) | `LIFE` | 15 | Opening late, who closes, deadlines, long-running commands, the `serve` relay, resumable syncs and their request budget, the run log |
|
|
39
39
|
| [Store](store.md) | `STORE` | 14 | Store areas, account scope, migrations, test isolation |
|
|
40
40
|
| [Data model](data-model.md) | `DATA` | 9 | Core entities, entities and source records, `external_id`, field names, links and duplicates, files, calendar entries |
|
|
41
41
|
| [Testing](testing.md) | `TEST` | 17 | Sandbox, no network, no real waits, leak tests, coverage, contracts, test matrix, parity |
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
## Scope
|
|
4
4
|
|
|
5
5
|
How a CLI chooses an account: the profile is always explicit, a read across accounts covers all of them and says so,
|
|
6
|
-
|
|
6
|
+
scope flags work the same everywhere, and sign-in has the same four commands in every CLI.
|
|
7
7
|
|
|
8
8
|
A **profile** is the name of one login of a CLI on one computer, for example `personal` or `work`. One profile can
|
|
9
9
|
hold a personal session, a bot token, or both.
|
|
@@ -16,7 +16,8 @@ Rule ids on this page are `WC-ACCT-<n>`. How to read a rule, how ids work and wh
|
|
|
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
17
|
and `--account` (WC-ACCT-7). zoom resolves its own profile word by the same rules (`zm <profile> …`, `ZM_PROFILE`,
|
|
18
18
|
`ZM_PROFILE_LOCK`) from zoom-cli 0.35.0, with cli-core's `liftProfile` and `usableProfileName`. drive still has its own
|
|
19
|
-
profile code: it registers its own profiles. That is a known gap, not an exception.
|
|
19
|
+
profile code: it registers its own profiles. That is a known gap, not an exception. gcal resolves the profile through
|
|
20
|
+
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.
|
|
20
21
|
|
|
21
22
|
## Rules
|
|
22
23
|
|
|
@@ -31,9 +32,11 @@ on the computer.
|
|
|
31
32
|
- **Example:** `tg personal chats list`, or `TG_PROFILE=personal tg chats list`. When no profile is named and the
|
|
32
33
|
config file still has `default_profile`, cli-core stops and says which profile to type. `config set default_profile`
|
|
33
34
|
is refused.
|
|
34
|
-
- **Exceptions:** a command that touches no account runs without a profile, for example `
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
- **Exceptions:** a command that touches no account runs without a profile, for example `session list`
|
|
36
|
+
([WC-ACCT-12](#wc-acct-12--sign-in-is-session-start-status-and-end-and-session-list-names-the-profiles)),
|
|
37
|
+
`config show` and `commands`; tg, max and wa still name their list `account list`. `mcp serve` may name its profiles with `--profiles <names|all>` instead
|
|
38
|
+
([WC-MCP-9](mcp.md#wc-mcp-9--a-server-serves-the-profiles-it-is-told-with-one-connection-for-each)); `all` is the owner's explicit choice, never a fallback.
|
|
39
|
+
- **Applies to:** all CLIs. **Since:** cli-base 0.41.0. **Rulings:** NEED-84, NEED-1881.
|
|
37
40
|
|
|
38
41
|
### WC-ACCT-2 · The profile is a word, never a flag
|
|
39
42
|
|
|
@@ -44,8 +47,9 @@ profile.
|
|
|
44
47
|
- **Enforced by:** `cli-core: src/cli/profile.test.ts`.
|
|
45
48
|
- **Example:** `max work bot messages send …` uses the same first word as `max work chats list`. `tg --json chats list`
|
|
46
49
|
still works, because `--json` is an option.
|
|
47
|
-
- **Exceptions:**
|
|
48
|
-
|
|
50
|
+
- **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.
|
|
52
|
+
- **Applies to:** all CLIs. **Since:** cli-base 0.41.0. **Rulings:** NEED-84, NEED-294, NEED-1881.
|
|
49
53
|
|
|
50
54
|
### WC-ACCT-3 · A missing profile is an error that lists the choices
|
|
51
55
|
|
|
@@ -78,8 +82,8 @@ top-level command, such as `chats` or `bot`, is refused when the profile is crea
|
|
|
78
82
|
|
|
79
83
|
### WC-ACCT-5 · A profile lock pins a process to one profile
|
|
80
84
|
|
|
81
|
-
When `<PREFIX>_PROFILE_LOCK` is set, a first word
|
|
82
|
-
`permission_error`.
|
|
85
|
+
When `<PREFIX>_PROFILE_LOCK` is set, a first word, `<PREFIX>_PROFILE` or `mcp serve --profiles` that names any other
|
|
86
|
+
profile is refused with `permission_error`. `--profiles all` names every profile, so it is refused too.
|
|
83
87
|
|
|
84
88
|
- **Why:** an agent must not escape its guards by naming a profile with fewer of them.
|
|
85
89
|
- **Enforced by:** `cli-core: src/cli/settings.test.ts`.
|
|
@@ -155,6 +159,35 @@ inferred from a matching name, and `unlink` undoes it.
|
|
|
155
159
|
- **Exceptions:** none.
|
|
156
160
|
- **Applies to:** all CLIs. **Since:** cli-base 0.43.1. **Rulings:** none.
|
|
157
161
|
|
|
162
|
+
### WC-ACCT-12 · Sign-in is `session start`, `status` and `end`, and `session list` names the profiles
|
|
163
|
+
|
|
164
|
+
Every CLI that signs in has four commands under `session`:
|
|
165
|
+
|
|
166
|
+
- `<cli> <profile> session start` signs the profile in.
|
|
167
|
+
- `<cli> <profile> session status` says whether this profile is signed in, and as whom.
|
|
168
|
+
- `<cli> <profile> session end` signs the profile out and forgets its login.
|
|
169
|
+
- `<cli> session list` takes no profile and lists the profiles on this computer.
|
|
170
|
+
|
|
171
|
+
`status` answers about one profile; `list` answers which profiles exist. Neither one connects to more than it must:
|
|
172
|
+
`list` reads only what the computer holds.
|
|
173
|
+
|
|
174
|
+
- **Why:** the owner and an agent look for sign-in in the same place in every CLI. Today each CLI has a different
|
|
175
|
+
part of the set, so an agent that learned one CLI guesses wrong in the next.
|
|
176
|
+
- **Enforced by:** review only.
|
|
177
|
+
- **Example:** `gcal personal session status` answers whether the `personal` profile holds a Google login;
|
|
178
|
+
`gcal session list` lists `personal` and `work` with no profile named.
|
|
179
|
+
- **Exceptions:** known gaps, as of 2026-10-27:
|
|
180
|
+
- tg, max and wa have `session start` and `session end`, no `session status`, and list their profiles with
|
|
181
|
+
`account list` instead of `session list`.
|
|
182
|
+
- drive has `session start`, `session list` and `session end`, and no `session status`.
|
|
183
|
+
- gcal has `session start`, `session status` and `session end`, and no `session list`.
|
|
184
|
+
- zm has `session start` and `session status`, and no `session end` or `session list`.
|
|
185
|
+
- memo signs in per mail account with `mail add` and has no `session` group.
|
|
186
|
+
|
|
187
|
+
A CLI that adds a missing command adds it under `session`; a renamed path keeps no alias
|
|
188
|
+
([WC-CMD-8](commands.md#wc-cmd-8--keep-no-aliases-a-renamed-path-stops-working-in-the-release-that-renames-it)).
|
|
189
|
+
- **Applies to:** every CLI that signs in. **Since:** cli-base 0.65.0. **Rulings:** NEED-1884.
|
|
190
|
+
|
|
158
191
|
## Sources
|
|
159
192
|
|
|
160
193
|
These documents explain the rules on this page in more detail. Where one of them disagrees with this page, this page
|
|
@@ -20,7 +20,9 @@ at start-up and the `serve` relay are in [Lifecycle](lifecycle.md); the store's
|
|
|
20
20
|
- **Service:** a use case, written once, that both a command and an MCP tool call.
|
|
21
21
|
- **Composition root:** the place in a CLI where real implementations are chosen and connected, usually
|
|
22
22
|
`program.ts` and the command context.
|
|
23
|
-
- **Vendor type:** a type from a provider library, such as `@mtcute/*` or `ws
|
|
23
|
+
- **Vendor type:** a type from a provider library, such as `@mtcute/*` or `ws`, and a type generated from a
|
|
24
|
+
provider's API description, such as gcal's and drive's types under `src/generated/` from a Google Discovery
|
|
25
|
+
document: both change when the provider changes, not when we do.
|
|
24
26
|
|
|
25
27
|
## Rules
|
|
26
28
|
|
|
@@ -57,7 +59,9 @@ at start-up and the `serve` relay are in [Lifecycle](lifecycle.md); the store's
|
|
|
57
59
|
`src/adapter/` maps the client onto the shared messenger port. A field that only one provider has travels in
|
|
58
60
|
`providerMetadata`.
|
|
59
61
|
- **Exceptions:** a bot slice that talks to an official HTTP API keeps its own transport, beside the personal
|
|
60
|
-
account's adapter, never above it.
|
|
62
|
+
account's adapter, never above it. Generated API types may also be used by the generated `api <method>` commands,
|
|
63
|
+
which are the provider's own API by design; the shared ports and services never see them. In gcal,
|
|
64
|
+
`calendar-source.ts` and `calendar-writer.ts` translate the generated event type into cli-core's calendar entry.
|
|
61
65
|
- **Applies to:** all CLIs. **Since:** cli-base 0.41.0. **Rulings:** NEED-12.
|
|
62
66
|
|
|
63
67
|
### WC-ARCH-4 · Put code in the lowest shared package that can hold it
|
|
@@ -77,14 +81,16 @@ at start-up and the `serve` relay are in [Lifecycle](lifecycle.md); the store's
|
|
|
77
81
|
|
|
78
82
|
### WC-ARCH-5 · Build a feature that another provider could have as a shared command behind a capability
|
|
79
83
|
|
|
80
|
-
- **Why:** a command written once in cli-core gives every
|
|
84
|
+
- **Why:** a command written once in cli-core gives every provider of a kind the same path, flags and output.
|
|
81
85
|
- **Enforced by:** review only.
|
|
82
86
|
- **Example:** topics and polls are port groups and shared commands in cli-core. A messenger that lacks a capability
|
|
83
87
|
refuses the command; it does not silently do less. A bot is another messenger with fewer capabilities, and tg and
|
|
84
|
-
max share the bot code.
|
|
85
|
-
|
|
86
|
-
or
|
|
87
|
-
- **
|
|
88
|
+
max share the bot code. Calendars have the same shape: cli-core's `addCalendarCommands` builds `calendars`,
|
|
89
|
+
`sync`, `status`, `events` and `search` over the `CalendarSource` and `CalendarWriter` ports, and mounts
|
|
90
|
+
`calendars busy` or the event writes only when the provider has them.
|
|
91
|
+
- **Exceptions:** a feature stays in one CLI only when the other providers cannot have it, such as MAX's socket server,
|
|
92
|
+
Telegram's app registration, or gcal's generated `api` commands.
|
|
93
|
+
- **Applies to:** messenger CLIs (tg, max, wa) and calendar CLIs (gcal). **Since:** cli-base 0.41.0. **Rulings:** NEED-820, NEED-440,
|
|
88
94
|
NEED-301, NEED-444.
|
|
89
95
|
|
|
90
96
|
### WC-ARCH-6 · Replace a use case, not a command
|
|
@@ -93,9 +99,10 @@ at start-up and the `serve` relay are in [Lifecycle](lifecycle.md); the store's
|
|
|
93
99
|
- **Enforced by:** review only.
|
|
94
100
|
- **Example:** `Messenger.services` receives the shared services and returns only the ones it changes:
|
|
95
101
|
`services: (base) => ({ messages: { ...base.messages, list: maxList } })`. To add a subcommand, a CLI calls
|
|
96
|
-
`addCommand` on the command a factory returns.
|
|
102
|
+
`addCommand` on the command a factory returns. A calendar CLI changes what the shared calendar commands do through
|
|
103
|
+
the source and writer it passes to `addCalendarCommands`, never by writing its own `sync` or `events list`.
|
|
97
104
|
- **Exceptions:** none.
|
|
98
|
-
- **Applies to:** messenger CLIs. **Since:** cli-base 0.41.0. **Rulings:** none.
|
|
105
|
+
- **Applies to:** messenger CLIs and calendar CLIs. **Since:** cli-base 0.41.0. **Rulings:** none.
|
|
99
106
|
|
|
100
107
|
### WC-ARCH-7 · Keep policy in the service; a command or MCP handler only parses input and presents the result
|
|
101
108
|
|
|
@@ -215,9 +222,9 @@ out of search and listed under `skipped`, with the hint to run that CLI once.
|
|
|
215
222
|
- **Why:** a new source joins search and memo without editing cli-core, and a provider nobody described is never read
|
|
216
223
|
as something it is not (until this rule, an unlisted provider was searched as a messenger).
|
|
217
224
|
- **Enforced by:** `cli-core: src/store/sqlite/providers.test.ts`.
|
|
218
|
-
- **Example:**
|
|
219
|
-
time it
|
|
220
|
-
`skipped` instead of reading their rows as messages.
|
|
225
|
+
- **Example:** gcal registers `{ name: "google-calendar", kind: "events", aliases: ["gcal", "calendar"], command:
|
|
226
|
+
"gcal" }` each time it opens the store. On a machine where gcal has never run, a search across accounts lists
|
|
227
|
+
gcal's accounts under `skipped` instead of reading their rows as messages.
|
|
221
228
|
- **Exceptions:** the built-in providers of cli-core need no entry. Two entries that disagree on a kind are an error
|
|
222
229
|
naming both.
|
|
223
230
|
- **Applies to:** cli-core and every CLI with a provider of its own. **Since:** cli-base 0.53.0. **Rulings:** none
|
|
@@ -75,11 +75,12 @@ catalogue and parity manifest stay in cli-core
|
|
|
75
75
|
- **Why:** an option that turns a command into a different report hides that report from anyone reading the tree.
|
|
76
76
|
- **Enforced by:** review only.
|
|
77
77
|
- **Example:** ranking is `stats messages top`, not a ranking mode on `search messages`. Options choose the scope,
|
|
78
|
-
measure, order or format of the same action. No command both shows and changes something
|
|
78
|
+
measure, order or format of the same action. No command both shows and changes something; saving what it read
|
|
79
|
+
into the local store is not a change ([WC-CMD-17](#wc-cmd-17--declare-effects-on-every-command)).
|
|
79
80
|
- **Exceptions:** a documented preparatory fetch such as `--sync-first`. It keeps its own permissions and reports
|
|
80
|
-
its outcome.
|
|
81
|
-
|
|
82
|
-
-
|
|
81
|
+
its outcome.
|
|
82
|
+
- **Applies to:** all CLIs. **Since:** cli-base 0.41.0. **Rulings:** NEED-1880 (`mcp revoke` is its own command; it
|
|
83
|
+
replaces NEED-1864, which kept `--revoke` as an option on `mcp`).
|
|
83
84
|
|
|
84
85
|
### WC-CMD-7 · Never let two commands overlap
|
|
85
86
|
|
|
@@ -117,9 +118,16 @@ catalogue and parity manifest stay in cli-core
|
|
|
117
118
|
- **Why:** `--limit` must mean "how many items" in every command of every CLI.
|
|
118
119
|
- **Enforced by:** `flag-synonym` · `flag-two-meanings` · `same-flag-meaning` (weekly audit; it does not block a pull
|
|
119
120
|
request yet).
|
|
120
|
-
- **Example:** `--limit <n>`, never `--max`, `--take` or `--count`. `--cursor <cursor>`, never `--offset`. How
|
|
121
|
-
|
|
122
|
-
|
|
121
|
+
- **Example:** `--limit <n>`, never `--max`, `--take` or `--count`. `--cursor <cursor>`, never `--offset`. How big
|
|
122
|
+
the answer is: `--limit`, `--page` and `--page-size`; a count of pages is never a paging option. How much work a
|
|
123
|
+
command may do is a `budget` option: `--max-pages <pages>` caps the pages a sync reads from the source, and
|
|
124
|
+
`--max-output-bytes` the size of the answer. A budget changes how far the command gets, never which items one page
|
|
125
|
+
of the answer holds. A retry after `outcome_unknown` is `--operation-id`: the attempt in the send journal. The
|
|
126
|
+
calendar vocabulary's `--id <id>` is a different value: the provider's own id for a thing being created, chosen
|
|
127
|
+
before the create, so a retry finds it at the provider. A CLI whose provider takes such an id may offer both.
|
|
128
|
+
- **Exceptions:** generated commands (`bot api`) are exempt from the naming rules. Known gap, as of 2026-10-27: gcal's
|
|
129
|
+
`events create` offers `--id` and no `--operation-id`, so its `outcome_unknown` names the event id instead of an
|
|
130
|
+
`operation_id` (WC-LIFE-9, WC-OUT-10).
|
|
123
131
|
- **Applies to:** all CLIs. **Since:** cli-base 0.41.0. **Rulings:** none.
|
|
124
132
|
|
|
125
133
|
### WC-CMD-11 · Name an option in plain words, with the kind of value in the name
|
|
@@ -129,7 +137,10 @@ catalogue and parity manifest stay in cli-core
|
|
|
129
137
|
- **Example:** `--operation-id`, not `--cid`. `--before-id` takes a message id, `--after-time` a time, `--before-n` a
|
|
130
138
|
count: two kinds are two options. A length of time is a `<duration>` with a unit (`500ms`, `30s`, `2m`, `4h`, `1d`),
|
|
131
139
|
never a bare number.
|
|
132
|
-
- **Exceptions:**
|
|
140
|
+
- **Exceptions:** `--duration-minutes <minutes>` (zm's meeting length, gcal's `calendars busy`) takes a bare count of
|
|
141
|
+
minutes. It is a known gap in the vocabulary, as of 2026-10-27: it stays until the vocabulary has
|
|
142
|
+
`--duration <duration>`, and then moves in the release that adds it, under "Changed — may break callers". No new
|
|
143
|
+
option follows it.
|
|
133
144
|
- **Applies to:** all CLIs. **Since:** cli-base 0.41.0. **Rulings:** none.
|
|
134
145
|
|
|
135
146
|
### WC-CMD-12 · Give every option a role from the vocabulary, and filter a list with its query
|
|
@@ -188,13 +199,25 @@ Each of these is set with `annotate(command, { … })` from `@wirecat/cli-base/c
|
|
|
188
199
|
|
|
189
200
|
### WC-CMD-17 · Declare `effects` on every command
|
|
190
201
|
|
|
202
|
+
A command **writes** when it changes something on the service (`remote: "write"`), or when it changes the local
|
|
203
|
+
store or files without reading the service first (`remote: "none"`, `local: "write"`, such as `store reindex` or
|
|
204
|
+
`config set`). A command that reads the service and only saves what it read into the owner's local store
|
|
205
|
+
(`{ remote: "read", local: "write" }`, such as `sync` or `calendars list`) is a **read**: on the command line, in
|
|
206
|
+
[WC-CMD-6](#wc-cmd-6--give-one-action-or-one-report-one-leaf-command), over MCP
|
|
207
|
+
([WC-MCP-4](mcp.md#wc-mcp-4--read-runs-only-reading-commands-and-write-only-writing-ones)) and as a next step
|
|
208
|
+
([WC-OUT-9](output.md#wc-out-9--offer-at-most-two-next-steps-and-mark-a-step-safe-only-when-it-changes-nothing-on-the-service)).
|
|
209
|
+
|
|
191
210
|
- **Why:** retries and unasked next steps are judged on effects, and a command without them counts as a write.
|
|
192
211
|
- **Enforced by:** `missing-command-effects` · `cli-core: src/cli/effects.test.ts` · `cli-memo: src/effects.test.ts`.
|
|
193
212
|
- **Example:** `annotate(send, { effects: { remote: "write", local: "write" } })`. Each side is `none`, `read` or
|
|
194
213
|
`write`. A sync that reads the service and writes the local store is `{ remote: "read", local: "write" }`.
|
|
195
214
|
- **Exceptions:** none. Without `effects`, the error handler never marks a failure safe to retry, and a next step
|
|
196
|
-
that runs the command is never safe to run unasked.
|
|
197
|
-
|
|
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`.
|
|
219
|
+
- **Applies to:** all CLIs. **Since:** cli-base 0.41.0 (what counts as a read: cli-base 0.65.0). **Rulings:** the
|
|
220
|
+
owner's ruling on what counts as a read over MCP (2026-10-27).
|
|
198
221
|
|
|
199
222
|
### WC-CMD-18 · Declare `lifetime` and `cooperative` instead of matching command names
|
|
200
223
|
|
|
@@ -328,8 +351,9 @@ The references, each for its own part:
|
|
|
328
351
|
[WC-OUT-18](output.md#wc-out-18--exit-with-cli-bases-code-for-each-kind-of-failure), prompts follow
|
|
329
352
|
[WC-CMD-15](#wc-cmd-15--never-prompt-when-no-person-can-answer), and a renamed path keeps no alias
|
|
330
353
|
([WC-CMD-8](#wc-cmd-8--keep-no-aliases-a-renamed-path-stops-working-in-the-release-that-renames-it)), unlike the
|
|
331
|
-
deprecation advice of the Command Line Interface Guidelines. A read may have documented local effects, such as
|
|
332
|
-
run, so a failed command does not promise that it wrote nothing
|
|
354
|
+
deprecation advice of the Command Line Interface Guidelines. A read may have documented local effects, such as
|
|
355
|
+
storing what it read or recording a failed run, so a failed command does not promise that it wrote nothing; what
|
|
356
|
+
counts as a read is in [WC-CMD-17](#wc-cmd-17--declare-effects-on-every-command). A write is never retried on its own, and a tool's
|
|
333
357
|
whole payload is never logged. The `stats` root and its resources are the project's own choice.
|
|
334
358
|
- **Exceptions:** the CLI Agent Spec's rule that help goes to stderr when stdout is not a terminal is not adopted. The
|
|
335
359
|
Agent Client Protocol links an editor to a coding agent; a WireCat CLI is a tool an agent calls, so it adds no
|
|
@@ -37,6 +37,8 @@ is rewritten only to follow it:
|
|
|
37
37
|
entity's own fields (WC-DATA-2).
|
|
38
38
|
- `meeting_participants` is unique on the meeting and the contact; its `external_id` is in no unique key (WC-DATA-3).
|
|
39
39
|
|
|
40
|
+
The calendar tables that gcal fills through cli-core (`calendars`, `calendar_entries`) follow them from the start.
|
|
41
|
+
|
|
40
42
|
## Rules
|
|
41
43
|
|
|
42
44
|
### WC-DATA-1 · A record more than one tool needs is a core entity in cli-core; a CLI's own tables stay private
|
|
@@ -73,7 +75,9 @@ to its entity. One entity may combine source records from several sources and ac
|
|
|
73
75
|
### WC-DATA-3 · A provider's id goes in `external_id`
|
|
74
76
|
|
|
75
77
|
The id a provider gives a record is stored in `external_id`, unique with `account_id` (or with `provider` where the
|
|
76
|
-
record has no account).
|
|
78
|
+
record has no account). Where the provider's id is unique only inside a container the record belongs to, it is unique
|
|
79
|
+
with that container instead: a calendar entry's `external_id` is unique with its `calendar_id`, because the same event
|
|
80
|
+
id appears in each calendar that holds a copy of the event, and the calendar is unique with `account_id`. Never in a column named after the provider (`google_id`, `file_id`) and never only in
|
|
77
81
|
`metadata`. A second id the provider also uses gets a named column of its own (`ical_uid`).
|
|
78
82
|
|
|
79
83
|
- **Why:** one place to look for every provider's id, and one unique key to update a record by.
|
package/docs/standards/docs.md
CHANGED
|
@@ -37,7 +37,10 @@ Rule ids on this page are `WC-DOC-<n>`. How to read a rule, how ids work and whi
|
|
|
37
37
|
- **Enforced by:** review only.
|
|
38
38
|
- **Example:** a claim that turns out wrong is rewritten to the current fact, with no "correction" note and no dated
|
|
39
39
|
"measured on" trail. A finished plan or handoff is deleted. A done backlog or cleanup line is deleted.
|
|
40
|
-
- **Exceptions:** `CHANGELOG.md` is the one place with version history.
|
|
40
|
+
- **Exceptions:** `CHANGELOG.md` is the one place with version history. A template-owned file (`AGENTS.md`,
|
|
41
|
+
`docs/dev/TESTING.md`, the skills under `docs/dev/skills/` and the others
|
|
42
|
+
[New CLI](new-cli.md) lists) that has turned false is fixed in cli-template, never only in the CLI: `upgrade` brings
|
|
43
|
+
the fix to every CLI, and a local edit would make `upgrade` ask about it each time.
|
|
41
44
|
- **Applies to:** all CLIs, user and developer pages. **Since:** cli-base 0.41.0. **Rulings:** none.
|
|
42
45
|
|
|
43
46
|
### WC-DOC-4 · A generated page is never edited by hand
|
|
@@ -12,7 +12,8 @@ This page is a reference, not a rule page: it assigns no rule ids. How to start
|
|
|
12
12
|
|
|
13
13
|
## What the template imports
|
|
14
14
|
|
|
15
|
-
The list is what the template's `src/` and `scripts/` import today, type imports
|
|
15
|
+
The list is what the template's `src/` and `scripts/` import today, type imports, re-exports and imports loaded on
|
|
16
|
+
demand included. `kit/` imports
|
|
16
17
|
nothing from either package.
|
|
17
18
|
|
|
18
19
|
| Package and path | Exports |
|
|
@@ -20,9 +21,9 @@ nothing from either package.
|
|
|
20
21
|
| `@wirecat/cli-base` | `catchCrashes`, `CliError`, `configFilePath`, `mcpErrorResult`, `resolvePaths` |
|
|
21
22
|
| `@wirecat/cli-base/commands` | `annotate`, `CommandInfo`, `COMMANDS_PAGE_LABELS` (in `scripts/`), `commandsPage` (in `scripts/`), `describeOptions` (in `scripts/`), `describeProgram` |
|
|
22
23
|
| `@wirecat/cli-base/codegen` | `writeArtifacts` (in `scripts/`) |
|
|
23
|
-
| `@wirecat/cli-base/release` | `DocsRules`, `JOURNAL_IDS`, `markdownFiles`, `packContents` (in `scripts/`) |
|
|
24
|
-
| `@wirecat/cli-core/cli` | `AppIdentity`, `changesAuthorityAt`, `commandHelpMarkdown` (in `scripts/`), `commandsCommand`, `configCommand`, `Configuration`, `createProgram`, `environmentOf`, `GlobalFlags`, `httpTokenFile`, `itemOf`, `LOCAL_READ`, `MCP_HTTP_DEFAULT_PORT`, `mcpServerCommand`, `outputFor`, `privateFiles`, `profileFree`, `ProgramDefinition`, `ProgramOptions`, `readOnlyLockOf`, `refuseUnderReadOnly`, `REMOTE_WRITE`, `renderAnswer`, `rootOf`, `run`, `RunOptions`, `runsCommand`, `settingsFor`, `skillCommand`, `upgradeCommand` |
|
|
25
|
-
| `@wirecat/cli-core/mcp` | `httpLockFile`, `MCP_PATH`, `McpCommand`, `portOf`, `publicUrlOf`, `registerCommands`, `runCommandForAgent`, `serveOverHttp`, `toolsFromCommands` |
|
|
24
|
+
| `@wirecat/cli-base/release` | `DocsRules`, `JOURNAL_IDS`, `markdownFiles`, `packContents` (all in `scripts/`) |
|
|
25
|
+
| `@wirecat/cli-core/cli` | `AppIdentity`, `changesAuthorityAt`, `commandHelpMarkdown` (in `scripts/`), `commandsCommand`, `configCommand`, `Configuration`, `createProgram`, `environmentOf`, `GlobalFlags`, `httpTokenFile`, `itemOf`, `LOCAL_READ`, `LOCAL_WRITE`, `MCP_HTTP_DEFAULT_PORT`, `mcpServerCommand`, `outputFor`, `privateFiles`, `profileFree`, `ProgramDefinition`, `ProgramOptions`, `readOnlyLockOf`, `refuseUnderReadOnly`, `REMOTE_WRITE`, `renderAnswer`, `rootOf`, `run`, `RunOptions`, `runsCommand`, `settingsFor`, `skillCommand`, `upgradeCommand` |
|
|
26
|
+
| `@wirecat/cli-core/mcp` | `httpLockFile`, `MCP_PATH`, `McpCommand`, `portOf`, `publicUrlOf`, `registerCommands`, `revokeAll`, `runCommandForAgent`, `serveOverHttp`, `toolsFromCommands` |
|
|
26
27
|
| `@wirecat/cli-core/sends` | `Level` |
|
|
27
28
|
| `@wirecat/cli-core/update` | `appUpdate` |
|
|
28
29
|
| `@wirecat/cli-core/testing` | `ARGV_LOG`, `argvLogSetup` |
|
|
@@ -20,9 +20,13 @@ and [WC-SECR-2](security.md#wc-secr-2--a-log-keeps-only-known-attribute-keys-and
|
|
|
20
20
|
|
|
21
21
|
### Who follows these rules today
|
|
22
22
|
|
|
23
|
-
- **WC-LIFE-15:** tg, max, wa and the template. drive, memo and zm do not keep runs yet; each is adding them on a
|
|
23
|
+
- **WC-LIFE-15:** tg, max, wa, gcal and the template. drive, memo and zm do not keep runs yet; each is adding them on a
|
|
24
24
|
`feat/runs` branch (2026-10-25). zm's `resolveSettings` also returns `keepFailedRuns: false`, which turns off the
|
|
25
25
|
failure log cli-core's `run` would keep.
|
|
26
|
+
- **WC-LIFE-16:** cli-core's shared calendar `sync`, from cli-core 0.464.0 (cli-core #2118); gcal gets it when it
|
|
27
|
+
moves up from cli-core 0.459.0. The deadlines of WC-LIFE-5 come with the same release. The other syncs
|
|
28
|
+
(`messages sync` in tg, max and wa, drive's `sync`, memo's mail import, zm's `recordings sync`) are cooperative but
|
|
29
|
+
spend no request budget yet, and gcal sets no `requestMs` of its own, so it uses the 2 s default.
|
|
26
30
|
|
|
27
31
|
## One-shot commands
|
|
28
32
|
|
|
@@ -74,15 +78,31 @@ and [WC-SECR-2](security.md#wc-secr-2--a-log-keeps-only-known-attribute-keys-and
|
|
|
74
78
|
|
|
75
79
|
### WC-LIFE-5 · Bound every one-shot command by a deadline
|
|
76
80
|
|
|
77
|
-
|
|
81
|
+
| Command | At a terminal or in a script | Called over MCP |
|
|
82
|
+
|---|---|---|
|
|
83
|
+
| one-shot | 30 s | 30 s |
|
|
84
|
+
| `cooperative` ([WC-LIFE-10](#wc-life-10--mark-a-long-command-cooperative-when-it-should-stop-at-its-own-pace-and-keep-its-work)) | 5 min | 45 s |
|
|
85
|
+
|
|
86
|
+
`--timeout 2m` or the `<PREFIX>_TIMEOUT` variable sets the deadline, higher or lower. Past the deadline a one-shot
|
|
87
|
+
command fails with `timeout` (exit 9).
|
|
88
|
+
|
|
89
|
+
- **Why:** a command that waits for ever on a dead connection never fails, so nobody learns why it hangs. Over MCP
|
|
90
|
+
the client cuts a tool call at about 60 s: the MCP TypeScript SDK's `DEFAULT_REQUEST_TIMEOUT_MSEC` is 60000
|
|
91
|
+
(`@modelcontextprotocol/sdk` 1.32.1, `dist/esm/shared/protocol.js:8`), and users report the same for Claude Desktop
|
|
92
|
+
and ChatGPT. A cooperative command answers at 45 s, so the agent gets the answer and the way to continue instead of
|
|
93
|
+
a cut call. Claude Code waits longer ([its MCP page](https://code.claude.com/docs/en/mcp)), but a CLI does not know
|
|
94
|
+
its client, so it takes the shortest.
|
|
78
95
|
- **Enforced by:** `cli-core: src/cli/deadline.test.ts` · `cli-core: src/cli/program.test.ts`.
|
|
79
|
-
- **Example:**
|
|
80
|
-
|
|
96
|
+
- **Example:** from cli-core 0.464.0, a calendar `sync` in a terminal may run 5 minutes; the same command called over
|
|
97
|
+
MCP stops at 45 seconds with what it saved. cli-core's `DEFAULT_COMMAND_MS`, `DEFAULT_COOPERATIVE_MS` and
|
|
98
|
+
`DEFAULT_AGENT_COOPERATIVE_MS` (`src/cli/execution.ts`) hold the three values, and `run()` picks one when it starts
|
|
99
|
+
the command.
|
|
81
100
|
- **Exceptions:** a `persistent` command has no deadline and ignores `--timeout`. An `interactive` command has none
|
|
82
101
|
while a person is at the terminal; run headless, it keeps the deadline. `--help` and `--version` need none. A
|
|
83
102
|
background fetch started with `<PREFIX>_BACKFILL_JOB` is bounded only by an explicit `--timeout`.
|
|
84
103
|
- **Applies to:** CLIs that run on cli-core's `run()`; other CLIs use cli-base's `createDeadline`. **Since:** cli-base
|
|
85
|
-
0.41.0. **Rulings:**
|
|
104
|
+
0.41.0 (the cooperative deadlines: cli-base 0.65.0, cli-core 0.464.0). **Rulings:** the owner's deadlines and
|
|
105
|
+
request budgets (2026-10-27).
|
|
86
106
|
|
|
87
107
|
### WC-LIFE-6 · Open nothing until an action needs it
|
|
88
108
|
|
|
@@ -131,7 +151,9 @@ and [WC-SECR-2](security.md#wc-secr-2--a-log-keeps-only-known-attribute-keys-and
|
|
|
131
151
|
- **Enforced by:** review only.
|
|
132
152
|
- **Example:** `annotate(fetch, { cooperative: true })`. The runner then does not cut the command when the run is
|
|
133
153
|
aborted from outside. The command passes the signal to every operation that can be cancelled, checks it before
|
|
134
|
-
each write, and stops after the current step. `persistent` commands count as cooperative.
|
|
154
|
+
each write, and stops after the current step. `persistent` commands count as cooperative. A cooperative command
|
|
155
|
+
gets the longer deadline of WC-LIFE-5, and a sync among them spends a request budget (WC-LIFE-16), so it stops
|
|
156
|
+
before the deadline instead of at it.
|
|
135
157
|
- **Exceptions:** none.
|
|
136
158
|
- **Applies to:** all CLIs. **Since:** cli-base 0.41.0. **Rulings:** none.
|
|
137
159
|
|
|
@@ -147,14 +169,15 @@ and [WC-SECR-2](security.md#wc-secr-2--a-log-keeps-only-known-attribute-keys-and
|
|
|
147
169
|
a CLI's own commands included, connects through `connectFor`: a relayed run gets serve's own adapter, and a run for
|
|
148
170
|
another profile is refused. With no live `serve`, the command connects on its own. A `serve` that offers the socket
|
|
149
171
|
but does not answer gives `provider_unavailable`, never a second connection.
|
|
150
|
-
- **Exceptions:** `serve`, `server`, `bot`, logins, `--offline`, `--dry-run` and `mcp --http` are not relayed.
|
|
172
|
+
- **Exceptions:** `serve`, `server`, `bot`, logins, `--offline`, `--dry-run` and `mcp serve --http` are not relayed.
|
|
151
173
|
- **Applies to:** messenger CLIs with an exclusive connection (wa today). **Since:** cli-base 0.41.0.
|
|
152
174
|
**Rulings:** NEED-1389.
|
|
153
175
|
|
|
154
176
|
## Resumable syncs
|
|
155
177
|
|
|
156
178
|
A long sync — a mailbox, a chat history, a Drive folder, a recording archive — is stopped by a kill, a reboot or a
|
|
157
|
-
provider's limit as often as it ends. These three rules make the next run carry on
|
|
179
|
+
provider's limit as often as it ends. These three rules make the next run carry on, and WC-LIFE-16 stops a run
|
|
180
|
+
before its deadline.
|
|
158
181
|
|
|
159
182
|
### WC-LIFE-12 · Save a long sync's work per item or per page, so a kill loses nothing saved
|
|
160
183
|
|
|
@@ -221,6 +244,44 @@ every value that may be content
|
|
|
221
244
|
- **Exceptions:** none.
|
|
222
245
|
- **Applies to:** all CLIs. **Since:** cli-base 0.56.0. **Rulings:** none.
|
|
223
246
|
|
|
247
|
+
## Request budgets
|
|
248
|
+
|
|
249
|
+
A sync that resumes (WC-LIFE-12 to WC-LIFE-14) also stops on its own before its deadline.
|
|
250
|
+
|
|
251
|
+
### WC-LIFE-16 · A sync spends a request budget sized to its deadline, and its answer says how to continue
|
|
252
|
+
|
|
253
|
+
Before each request to the provider, a sync asks its budget whether one more may go. The budget is
|
|
254
|
+
|
|
255
|
+
`requests = floor((deadline − margin) / requestMs)`, at least one,
|
|
256
|
+
|
|
257
|
+
where `margin` is the larger of 5 s and 15 % of the deadline, and `requestMs` is how long one request of this CLI is
|
|
258
|
+
expected to take: one constant per CLI, `AppIdentity.requestMs` (2 s when it is not set). `--timeout` changes the
|
|
259
|
+
deadline, and the budget is computed again from it; `--max-pages <pages>` can only lower it. The budget also stops
|
|
260
|
+
when the deadline is closer than the margin plus the slowest request so far, and when the run is cancelled.
|
|
261
|
+
|
|
262
|
+
When the budget stops the sync before its end, the sync saves where it is (WC-LIFE-12), exits 0 as a declared
|
|
263
|
+
`partial` answer ([WC-OUT-8](output.md#wc-out-8--exit-0-on-a-partial-answer-only-when-the-command-declares-partial))
|
|
264
|
+
and says in the answer why it stopped and how far it got. Its `next` holds the exact command that continues it, the
|
|
265
|
+
same `sync` with the same budget and timeout, marked safe to run unasked; over MCP the step carries that command and
|
|
266
|
+
its arguments for the `read` tool. A run that moved nothing on offers no step, so an agent cannot loop. The sync's
|
|
267
|
+
status command answers how far it is
|
|
268
|
+
([WC-LIFE-14](#wc-life-14--a-syncs-status-command-says-how-far-it-is-in-one-shape)).
|
|
269
|
+
|
|
270
|
+
- **Why:** a first sync of a large account does not fit one deadline. Without a budget the deadline cuts it mid-page
|
|
271
|
+
and the run loses its round; with one, every run ends with an answer, saves its work and says how to go on. One
|
|
272
|
+
`requestMs` per CLI, in one place, keeps the arithmetic the same in every sync of that CLI.
|
|
273
|
+
- **Enforced by:** `cli-core: src/background/budget.test.ts` · `cli-core: src/calendar/testing/sync-contract.ts`.
|
|
274
|
+
- **Example:** `gcal personal sync` over MCP has 45 s: margin 6.75 s, so with the 2 s default about 19 requests. It
|
|
275
|
+
answers `state: "partial"`, `stopped: "requests"`, `progress: { calendars, calendarsDone, requests, saved }`, and
|
|
276
|
+
`next` with `gcal personal sync`; `gcal personal status` shows each calendar's progress. cli-core builds the budget
|
|
277
|
+
with `runBudgetOf(command, maxPages)` from `@wirecat/cli-core/cli` over `runBudget` in
|
|
278
|
+
`@wirecat/cli-core/background`.
|
|
279
|
+
- **Exceptions:** known gaps, as of 2026-10-27: only the calendar sync spends a budget (see "Who follows"). The
|
|
280
|
+
continuing step names the command, not the MCP tool, and until the WC-CMD-17 follow-up the router sends that
|
|
281
|
+
command to `write`, not `read`.
|
|
282
|
+
- **Applies to:** every CLI with a sync. **Since:** cli-base 0.65.0. **Rulings:** the owner's deadlines and request
|
|
283
|
+
budgets (2026-10-27).
|
|
284
|
+
|
|
224
285
|
## Sources
|
|
225
286
|
|
|
226
287
|
These documents explain the rules on this page in more detail. Where one of them disagrees with this page, this page
|
package/docs/standards/mcp.md
CHANGED
|
@@ -15,9 +15,12 @@ Rule ids on this page are `WC-MCP-<n>`. How to read a rule, how ids work and whi
|
|
|
15
15
|
|
|
16
16
|
### Who follows these rules today
|
|
17
17
|
|
|
18
|
-
tg, max and wa serve cli-core's personal server (`<cli> <profile> mcp`) and bot server (`<cli> <profile> bot mcp`).
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
tg, max and wa serve cli-core's personal server (`<cli> <profile> mcp serve`) and bot server (`<cli> <profile> bot mcp serve`).
|
|
19
|
+
`mcp serve --profiles` ([WC-MCP-9](#wc-mcp-9--a-server-serves-the-profiles-it-is-told-with-one-connection-for-each)) is not
|
|
20
|
+
built yet: cli-core serves one named profile.
|
|
21
|
+
zoom serves the three shared tools over its own commands from zoom-cli 0.35.0 (`zm <profile> mcp serve`, cli-core's
|
|
22
|
+
`toolsFromCommands` and `runCommandForAgent`), and gcal does the same (`gcal <profile> mcp`; it moves to `mcp serve`
|
|
23
|
+
when it takes cli-core 0.463.0). memo and drive have their own servers: memo has hand-written tools, and
|
|
21
24
|
drive builds one tool per command from its command tree and skips commands through its own list. Those two are known
|
|
22
25
|
gaps against these rules.
|
|
23
26
|
|
|
@@ -66,12 +69,22 @@ Every entry in the catalogue must be a command the CLI has.
|
|
|
66
69
|
|
|
67
70
|
Every command says whether it writes. `read` refuses a command that writes, and `write` refuses one that only reads.
|
|
68
71
|
Both declare the answer envelope (a list `{ items, page, … }` or one thing `{ item, … }`) as their `outputSchema`.
|
|
69
|
-
|
|
70
|
-
|
|
72
|
+
A command that reads the service and only saves what it read into the owner's local store, such as `sync` or
|
|
73
|
+
`calendars list`, is a read and runs through `read`
|
|
74
|
+
([WC-CMD-17](commands.md#wc-cmd-17--declare-effects-on-every-command) defines it).
|
|
75
|
+
|
|
76
|
+
- **Why:** an agent host can let `read` run freely and ask a person before `write`. `read` carries the MCP
|
|
77
|
+
`readOnlyHint`, which the [MCP schema](https://modelcontextprotocol.io/specification/2025-11-25/schema#toolannotations)
|
|
78
|
+
describes as "the tool does not modify its environment". A local copy of what the service already holds changes
|
|
79
|
+
nothing the owner would have to undo, so a sync stays under it; sending it through `write` would make a host ask a
|
|
80
|
+
person before every refresh.
|
|
71
81
|
- **Enforced by:** `cli-core: src/mcp/mcp.test.ts` · `cli-core: src/mcp/output-schema.test.ts`.
|
|
72
|
-
- **Example:** `max_read { "command": "messages send", … }` is refused.
|
|
73
|
-
|
|
74
|
-
- **
|
|
82
|
+
- **Example:** `max_read { "command": "messages send", … }` is refused. Once the gap below is closed,
|
|
83
|
+
`gcal_read { "command": "sync" }` runs.
|
|
84
|
+
- **Exceptions:** none. Known gap, as of 2026-10-27: cli-core decides with cli-base's `writes()`, which still counts
|
|
85
|
+
the local write, so `sync` runs through `write` until that follow-up lands (WC-CMD-17).
|
|
86
|
+
- **Applies to:** all CLIs. **Since:** cli-base 0.41.0 (what counts as a read: cli-base 0.65.0). **Rulings:** the
|
|
87
|
+
owner's ruling on what counts as a read over MCP (2026-10-27).
|
|
75
88
|
|
|
76
89
|
### WC-MCP-5 · Unknown arguments are refused before anything runs
|
|
77
90
|
|
|
@@ -116,29 +129,36 @@ profile's permissions, the recipient list and the hourly limit decide.
|
|
|
116
129
|
|
|
117
130
|
### WC-MCP-8 · No `mcp` flag grants access
|
|
118
131
|
|
|
119
|
-
`mcp --permission <key=level>` may only make a level stricter for that server. An override that would loosen the
|
|
132
|
+
`mcp serve --permission <key=level>` may only make a level stricter for that server. An override that would loosen the
|
|
120
133
|
profile is refused at start-up with `validation_error` naming the key. This includes a parent key that would hide a
|
|
121
|
-
stricter key under it, and lifting a built-in `ask`. `mcp
|
|
134
|
+
stricter key under it, and lifting a built-in `ask`. `mcp add <client> --allow-writes` only confirms, at install time, that
|
|
122
135
|
the owner saw the profile offers writes.
|
|
123
136
|
|
|
124
137
|
- **Why:** whoever writes the agent's MCP config must not get more than the profile allows.
|
|
125
138
|
- **Enforced by:** `cli-core: src/cli/messenger/mcp-command-http.test.ts`.
|
|
126
|
-
- **Example:** `max work mcp --permission messages=readonly` works. `max work mcp --permission messages.delete=allow`
|
|
139
|
+
- **Example:** `max work mcp serve --permission messages=readonly` works. `max work mcp serve --permission messages.delete=allow`
|
|
127
140
|
is refused, because the built-in level is `ask`.
|
|
128
|
-
- **Exceptions:** `bot mcp` takes no `--permission` at all. cli-base's flag registry still describes `--allow-writes`
|
|
141
|
+
- **Exceptions:** `bot mcp serve` takes no `--permission` at all. cli-base's flag registry still describes `--allow-writes`
|
|
129
142
|
as "permit writing"; that wording is a gap.
|
|
130
143
|
- **Applies to:** all CLIs. **Since:** cli-base 0.41.0. **Rulings:** NEED-1423.
|
|
131
144
|
|
|
132
|
-
### WC-MCP-9 · A server
|
|
145
|
+
### WC-MCP-9 · A server serves the profiles it is told, with one connection for each
|
|
133
146
|
|
|
134
|
-
`mcp`
|
|
135
|
-
every
|
|
147
|
+
`mcp serve` serves one named profile (`<cli> <profile> mcp serve`), or the profiles that `--profiles` lists:
|
|
148
|
+
`--profiles work,personal`, or `--profiles all` for every profile on this computer. With neither, it refuses with
|
|
149
|
+
`validation_error` and lists the profiles, like any missing profile ([WC-ACCT-3](accounts.md)). It never serves every
|
|
150
|
+
profile by default. The server opens one messenger connection for each profile it serves, and every call for that
|
|
151
|
+
profile shares it. When it serves more than one profile, each call names its profile; a call without one is refused
|
|
152
|
+
and lists the profiles served. Each call follows its own profile's permissions.
|
|
136
153
|
|
|
137
|
-
- **Why:**
|
|
154
|
+
- **Why:** which accounts an agent can reach is the owner's choice, written in the agent's config. A connection per
|
|
155
|
+
call would break the messenger's pace limits and look unlike the official client.
|
|
138
156
|
- **Enforced by:** review only.
|
|
139
|
-
- **Example:** `max work mcp
|
|
157
|
+
- **Example:** `max work mcp serve`, `max mcp serve --profiles work,personal` or `max mcp serve --profiles all`.
|
|
158
|
+
Under the last two, a call reads `max_read { "profile": "work", "command": "inbox", "arguments": {} }`.
|
|
159
|
+
`max mcp serve` alone is refused, and its message lists `personal` and `work`.
|
|
140
160
|
- **Exceptions:** the bot server needs no held connection: each Bot API call is plain HTTPS.
|
|
141
|
-
- **Applies to:** messenger CLIs. **Since:** cli-base 0.41.0. **Rulings:**
|
|
161
|
+
- **Applies to:** messenger CLIs. **Since:** cli-base 0.41.0. **Rulings:** NEED-1881.
|
|
142
162
|
|
|
143
163
|
### WC-MCP-10 · A tool and its command give the same answer and the same error
|
|
144
164
|
|
|
@@ -170,15 +190,16 @@ it carries.
|
|
|
170
190
|
|
|
171
191
|
### WC-MCP-12 · An MCP server over HTTP follows the same permissions and short-lived logins
|
|
172
192
|
|
|
173
|
-
`mcp --http` serves on `127.0.0.1` for agents in a browser, behind the owner's tunnel. Writes follow the profile's
|
|
193
|
+
`mcp serve --http` serves on `127.0.0.1` for agents in a browser, behind the owner's tunnel. Writes follow the profile's
|
|
174
194
|
permissions. An access token lives one hour and a refresh token 30 days, replaced on each use. To log in, a browser app
|
|
175
|
-
needs a one-time code that the owner reads in a terminal.
|
|
195
|
+
needs a one-time code that the owner reads in a terminal. `mcp revoke` ends every login.
|
|
176
196
|
|
|
177
197
|
- **Why:** a server reachable from a browser must not be a softer way in than the local one.
|
|
178
198
|
- **Enforced by:** review only.
|
|
179
|
-
- **Example:** `max work mcp --http --public-url https://<name>.ts.net`.
|
|
199
|
+
- **Example:** `max work mcp serve --http --public-url https://<name>.ts.net`.
|
|
180
200
|
- **Exceptions:** none.
|
|
181
|
-
- **Applies to:** messenger CLIs
|
|
201
|
+
- **Applies to:** every CLI that offers `mcp serve --http`: the messenger CLIs, cli-template and every CLI made from
|
|
202
|
+
it. **Since:** cli-base 0.41.0. **Rulings:** NEED-594, NEED-774.
|
|
182
203
|
|
|
183
204
|
### WC-MCP-13 · A tool's hints and descriptions never stand in for the permission check
|
|
184
205
|
|
|
@@ -213,8 +234,11 @@ cli-core builds its personal and bot MCP catalogues from the command tree with `
|
|
|
213
234
|
- **One server per subtree.** The personal server is built from the program without its `bot` group; the bot server
|
|
214
235
|
from the `bot` group. Both keep three tools (WC-MCP-1).
|
|
215
236
|
- **The profile still decides at run time** what is listed (WC-MCP-6).
|
|
216
|
-
- **
|
|
217
|
-
|
|
237
|
+
- **Two snapshots pin what agents see.** `contracts/mcp-tools.json` pins the tools themselves: with three tools
|
|
238
|
+
(WC-MCP-1) that is three names and their schemas, which do not change when a command is added. So each CLI also
|
|
239
|
+
pins the command catalogue behind the tools, every command `tools_search` can return and whether it runs through
|
|
240
|
+
`read` or `write`, in `contracts/mcp-catalogue.json`. A command that becomes visible to agents then shows up as a diff in
|
|
241
|
+
its pull request. tg and max keep both; wa, zm, gcal and drive pin only the tools, a known gap as of 2026-10-27.
|
|
218
242
|
|
|
219
243
|
## Sources
|
|
220
244
|
|
|
@@ -51,8 +51,8 @@ The reference CLI is the one to compare with when a step is unclear.
|
|
|
51
51
|
|
|
52
52
|
The copy already holds every file this handbook asks for: the required root commands (`commands`, `doctor`,
|
|
53
53
|
`skill show`, `skill install`, `mcp`, `upgrade`) and `config` from cli-core, the run log and its `runs` commands
|
|
54
|
-
([WC-LIFE-15](lifecycle.md#wc-life-15--keep-a-run-log-through-cli-core-and-read-it-back-with-runs)), the MCP server with
|
|
55
|
-
command, writes included, the community presets in `config/wirecat/`, the TypeScript, Biome and vitest setup,
|
|
54
|
+
([WC-LIFE-15](lifecycle.md#wc-life-15--keep-a-run-log-through-cli-core-and-read-it-back-with-runs)), the MCP server with its three tools
|
|
55
|
+
([WC-MCP-1](mcp.md#wc-mcp-1--a-server-offers-three-tools-not-one-per-command)) that reach every command, writes included, the community presets in `config/wirecat/`, the TypeScript, Biome and vitest setup,
|
|
56
56
|
`contracts/`, `lint/`, the test matrix, the user pages, `CHANGELOG.md`, `SECURITY.md`, `bin/release`, and the
|
|
57
57
|
`pr.yml`, `ci.yml`, `main.yml`, `standards.yml`, `changelog-guard.yml`, `monthly.yml` and `release.yml` workflows.
|
|
58
58
|
The rules they keep are on the area pages: [Commands](commands.md), [MCP](mcp.md), [Accounts](accounts.md),
|
package/docs/standards/output.md
CHANGED
|
@@ -93,7 +93,11 @@ JSON Schema 2020-12.
|
|
|
93
93
|
- **Why:** a caller that checks only the exit code must not take half an answer for a whole one.
|
|
94
94
|
- **Enforced by:** review only.
|
|
95
95
|
- **Example:** a search across accounts declares `annotate(search, { partial: true })` and exits 0 with
|
|
96
|
-
`meta.complete: false`. Any other command prints what it read
|
|
96
|
+
`meta.complete: false`. Any other command prints the whole envelope of what it read on stdout, with
|
|
97
|
+
`meta.complete: false` and its `issues`, then exits with the code of its first issue. Such an answer is an answer,
|
|
98
|
+
not a failure, so [WC-OUT-14](#wc-out-14--print-a-failure-in-machine-mode-as-one-json-object-on-stderr-with-stdout-empty)
|
|
99
|
+
does not apply to it: a caller reads stdout whenever it is not empty, also after a non-zero exit. cli-core's
|
|
100
|
+
`renderAnswer` does this.
|
|
97
101
|
- **Exceptions:** none.
|
|
98
102
|
- **Applies to:** all CLIs. **Since:** cli-base 0.41.0. **Rulings:** none.
|
|
99
103
|
|
|
@@ -103,7 +107,8 @@ JSON Schema 2020-12.
|
|
|
103
107
|
- **Enforced by:** review only.
|
|
104
108
|
- **Example:** `next` holds the step to take, then one alternative. Each has `kind` (`command`, or `user_action` with
|
|
105
109
|
no command) and `safe_to_auto_execute`. A step that reads the service and writes only the local store (a sync) is
|
|
106
|
-
safe
|
|
110
|
+
safe; it counts as a read, also over MCP
|
|
111
|
+
([WC-CMD-17](commands.md#wc-cmd-17--declare-effects-on-every-command)). A step that reads nothing remotely and writes locally (`store reindex`) is not: it waits to be asked. A step
|
|
107
112
|
is built from ids, counts and cursors the CLI holds, never from text a message, mail or document carried. A CLI
|
|
108
113
|
answer's command step also has `line`, the whole command with the profile (`tg personal mcp config`), because
|
|
109
114
|
`command` is the bare path an MCP router takes and a copied path without the profile fails.
|
|
@@ -160,6 +165,12 @@ JSON Schema 2020-12.
|
|
|
160
165
|
|
|
161
166
|
### WC-OUT-14 · Print a failure in machine mode as one JSON object on stderr, with stdout empty
|
|
162
167
|
|
|
168
|
+
A failure is a run that has no answer: the command threw. A command that answers with what it could read and
|
|
169
|
+
`meta.complete: false` has not failed, even when it exits non-zero; that case is
|
|
170
|
+
[WC-OUT-8](#wc-out-8--exit-0-on-a-partial-answer-only-when-the-command-declares-partial). So a non-zero exit does not
|
|
171
|
+
mean stdout is empty: stdout empty and one JSON object on stderr is a failure; an envelope on stdout is an answer,
|
|
172
|
+
whole or partial.
|
|
173
|
+
|
|
163
174
|
- **Why:** a caller tells success from failure without parsing text, and stdout never holds half an answer and an
|
|
164
175
|
error.
|
|
165
176
|
- **Enforced by:** `src/failure.test.ts` · `cli-core: src/cli/agent-contract.test.ts`.
|
|
@@ -320,9 +320,15 @@ A messenger's keys come from its resources (`messages`, `chats`). Any other CLI
|
|
|
320
320
|
- **Keys.** Each command is checked against its own path (`events.create`), the default `keyOf`. Housekeeping has no
|
|
321
321
|
key (WC-PERM-5). `extra` adds the keys a command checks inside itself, such as one per generated API method
|
|
322
322
|
(`api.calendar.events.insert`). A key that names no command is refused when it is set or saved.
|
|
323
|
-
- **Defaults.** `defaults` holds the CLI's built-in levels, beside cli-core's. They only ever tighten (WC-PERM-7).
|
|
324
|
-
|
|
325
|
-
than the owner
|
|
323
|
+
- **Defaults.** `defaults` holds the CLI's built-in levels, beside cli-core's. They only ever tighten (WC-PERM-7).
|
|
324
|
+
Like WC-PERM-6 for the messengers, and as tg and drive do: an ordinary change is `allow`; a delete is `ask`; a
|
|
325
|
+
write that mails or notifies people other than the owner, or deletes a whole series from their calendars, is a key
|
|
326
|
+
of its own, `deny` until the owner allows it.
|
|
327
|
+
Sending a message is not this kind of write; a messenger CLI keeps WC-PERM-6.
|
|
328
|
+
- **Shared keys on every path.** A key a shared command checks inside itself, such as `events.notify` or
|
|
329
|
+
`events.delete.series`, is checked by every path that can do the same thing, the CLI's own generic API commands
|
|
330
|
+
included. A gcal `api` call that deletes an event with `sendUpdates: "all"` checks `events.notify` as well as
|
|
331
|
+
its own `api.calendar.events.delete`; otherwise the generic path is a way round the shared key.
|
|
326
332
|
- **Read-only lock and guard.** `<PREFIX>_READ_ONLY` refuses every write to the service, as in WC-PERM-12. Every
|
|
327
333
|
write runs through cli-core's write guard with `permissionDefaults: configuration.permissions.defaults`
|
|
328
334
|
(cli-core 0.455.0), so `ask` is a y/N question at a terminal (WC-PERM-9) and every attempt goes into the send journal
|
|
@@ -331,14 +337,21 @@ A messenger's keys come from its resources (`messages`, `chats`). Any other CLI
|
|
|
331
337
|
- **Why:** one permission model for every CLI; a CLI-own layer drifts from the shared one and is audited by nobody.
|
|
332
338
|
- **Enforced by:** `cli-core: src/cli/command-permissions.test.ts` · `cli-core: src/calendar/cli/writes.test.ts`.
|
|
333
339
|
- **Example:** a calendar CLI takes cli-core's `CALENDAR_WRITE_DEFAULTS` and `CALENDAR_WRITE_KEYS`
|
|
334
|
-
(`@wirecat/cli-core/calendar`, cli-core 0.458.0)
|
|
335
|
-
|
|
336
|
-
`events.notify` (
|
|
337
|
-
`
|
|
338
|
-
|
|
339
|
-
`
|
|
340
|
-
|
|
341
|
-
-
|
|
340
|
+
(`@wirecat/cli-core/calendar`, cli-core 0.458.0). By this rule `events create`, `update` and `respond` are `allow`
|
|
341
|
+
and `events delete` is `ask`. `events.delete.series` (every occurrence of a repeating series, in every attendee's
|
|
342
|
+
calendar) and `events.notify` (the provider mails the attendees) are `deny` until the owner allows them: over MCP
|
|
343
|
+
`ask` runs without a question, and each reaches people other than the owner. gcal adds its own keys for its generated
|
|
344
|
+
`api <method>` commands: sharing a calendar is `ask` (NEED-1857), and emptying the main calendar
|
|
345
|
+
(`api.calendar.calendars.clear`) is `deny`.
|
|
346
|
+
- **Exceptions:** known gaps, as of 2026-10-27:
|
|
347
|
+
- cli-core's `CALENDAR_WRITE_DEFAULTS` (`src/calendar/writer.ts`) still has `events create`, `update` and
|
|
348
|
+
`respond` at `ask`.
|
|
349
|
+
- gcal's generated `api` commands default every write to `ask`, ordinary changes included, and check neither
|
|
350
|
+
`events.notify` nor `events.delete.series`.
|
|
351
|
+
- drive keeps its own policy and `--read-only` flag, zoom reads its own `remote-meetings` key with `levelFor`, and
|
|
352
|
+
memo uses cli-core's permissions for the shared commands only. None of them declares `commandPermissions` yet.
|
|
353
|
+
- **Applies to:** every CLI that writes and is not a messenger. **Since:** cli-base 0.64.0 (the levels and the shared
|
|
354
|
+
keys on every path: cli-base 0.65.0). **Rulings:** NEED-1857, NEED-1883.
|
|
342
355
|
|
|
343
356
|
## Sources
|
|
344
357
|
|
|
@@ -192,8 +192,14 @@ fixtures, `scripts/`, `.github/`, `bin/release`, `config/`, `lint/`, `contracts/
|
|
|
192
192
|
|
|
193
193
|
- **Why:** a stray file in the package can leak local data and grows every install.
|
|
194
194
|
- **Enforced by:** `node-ci.yml:check` · `<app>: pnpm package:check` · `whatsapp-cli: pnpm release:check`.
|
|
195
|
-
- **Example:** `package.json` `files` lists what ships.
|
|
196
|
-
`@wirecat/cli-base/release`
|
|
195
|
+
- **Example:** `package.json` `files` lists what ships. `packContents(root, allowed, said)` from
|
|
196
|
+
`@wirecat/cli-base/release` fails on any path in `npm pack --dry-run` outside `allowed`. It checks the pack against
|
|
197
|
+
the list it is given, not against this rule: a CLI that passes its own `files` list as `allowed`, as the template's
|
|
198
|
+
`scripts/smoke.ts` does, checks only that npm packs what `files` says, and cannot fail on a wrong `files` list.
|
|
199
|
+
`npm pack --dry-run` in gcal-cli (2026-10-27) packs 256 files, `src/`, `scripts/`, `.github/`, `config/`,
|
|
200
|
+
`contracts/`, `lint/` and `template.json` among them, because its `files` list names them; its check cannot see it. The check this rule needs
|
|
201
|
+
refuses the forbidden paths above whatever `files` says; it belongs in cli-template, so every CLI gets it by
|
|
202
|
+
`upgrade` ([cli-template#77](https://github.com/WireCatLabs/cli-template/issues/77)).
|
|
197
203
|
- **Exceptions:** cli-template's own package ships the skeleton's sources and configs, since `new` copies them; the
|
|
198
204
|
`files` list `new` writes into a CLI follows this rule. Each CLI's `package:check` runs `packContents`; tg and max
|
|
199
205
|
as `release-check.ts --package-only`.
|
|
@@ -310,16 +310,23 @@ store and runs no search. A CLI declares its provider and may replace any step.
|
|
|
310
310
|
printed. A CLI that signs in another way (a bot token, a session) does not use the module.
|
|
311
311
|
- **Applies to:** every CLI that signs in with OAuth. **Since:** cli-base 0.57.1. **Rulings:** NEED-1734.
|
|
312
312
|
|
|
313
|
-
### WC-SECR-23 · Meeting passcodes and the query of a join link never reach the store
|
|
313
|
+
### WC-SECR-23 · Meeting passcodes and the query of a join link never reach the store, the output or an agent
|
|
314
314
|
|
|
315
|
-
- **Why:** a passcode, PIN or join-link query lets anyone who reads the store
|
|
315
|
+
- **Why:** a passcode, PIN or join-link query lets anyone who reads the store, a terminal log or an agent's
|
|
316
|
+
transcript join the meeting.
|
|
316
317
|
- **Enforced by:** `cli-core: src/store/calendar.test.ts`.
|
|
317
318
|
- **Example:** a calendar entry's description loses each line that carries a passcode, password or PIN, and every
|
|
318
319
|
link loses its query string, so `join_url` holds `https://zoom.us/j/123` without `?pwd=`. A meeting is matched by
|
|
319
320
|
its room (`join_key`, such as `zoom:<number>`), never by a link that carries the passcode.
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
321
|
+
The same holds for what a command prints and what an MCP tool answers, also when the answer comes straight from
|
|
322
|
+
the provider and never touched the store: a generic API command removes the provider's passcode fields (Google's
|
|
323
|
+
`conferenceData.entryPoints[].passcode`, `pin`, `accessCode`, `password`), the query of every join link and the
|
|
324
|
+
dial sequence after a phone number, and the passcode lines of a description or a location.
|
|
325
|
+
- **Exceptions:** a title that names a PIN or a password is kept: it is the meeting's name. Known gap, as of
|
|
326
|
+
2026-10-27: gcal's `api` answers drop the conference entry points' secrets (`src/conference.ts`) but keep the
|
|
327
|
+
description and location as Google sent them.
|
|
328
|
+
- **Applies to:** cli-core and every CLI that stores or shows meetings or calendar entries. **Since:** cli-base 0.57.1
|
|
329
|
+
(output and agent answers: cli-base 0.65.0). **Rulings:** none (Wave 3 requirements 3.4).
|
|
323
330
|
|
|
324
331
|
## Sources
|
|
325
332
|
|
package/docs/standards/store.md
CHANGED
|
@@ -52,8 +52,9 @@ Related pages: which account a read covers, and how a read across accounts is as
|
|
|
52
52
|
accounts on purpose asks for the whole store with `crossAccount(deps, reason)`; see [Accounts](accounts.md).
|
|
53
53
|
- **Exceptions:** `forAccount` binds only the account areas (accounts, chats, sync, messages, attachments, roster,
|
|
54
54
|
people, conversations, vectors, counters, tags, mentions). The `chats`, `conversations` and `attachments` services
|
|
55
|
-
use the bound store; the other services still pair `deps.store()` with `deps.account()`.
|
|
56
|
-
|
|
55
|
+
use the bound store; the other services still pair `deps.store()` with `deps.account()`. The calendar areas
|
|
56
|
+
(calendars, calendar entries, their sync cursors) are not bound either: cli-core's calendar commands pass
|
|
57
|
+
`accountId` to every call, a known gap as of 2026-10-27. New code uses the bound store.
|
|
57
58
|
- **Applies to:** cli-core and every CLI that adds a service. **Since:** cli-base 0.41.0. **Rulings:** none.
|
|
58
59
|
|
|
59
60
|
### WC-STORE-4 · Reach the store through its public module, never a table module
|
|
@@ -171,8 +171,9 @@ and which run after merge is in [CI](ci.md).
|
|
|
171
171
|
- **Enforced by:** `node-ci.yml:check` · `<app>: pnpm test:matrix:check`.
|
|
172
172
|
- **Example:** under vitest, `run()` appends each parsed command path and option name (never a value) to
|
|
173
173
|
`coverage/argv.jsonl`. `pnpm test:matrix` turns that into `docs/dev/test-matrix.md` and fails on a command or
|
|
174
|
-
option with neither a test nor a reason. A
|
|
175
|
-
function directly does not
|
|
174
|
+
option with neither a test nor a reason. A test counts only when it runs the command through `run()`: a test that
|
|
175
|
+
calls a function directly records no command line, so the matrix does not see it. A reason says what covers the
|
|
176
|
+
command instead: the live scenario that runs it, or, for a generated group, the two tests below.
|
|
176
177
|
- **Exceptions:** commands generated from a service's API description, such as drive's and gcal's
|
|
177
178
|
`api <method>` from a Google Discovery snapshot, share one reason for the whole group (`api *` and its options).
|
|
178
179
|
It names two tests: one that checks every generated command's options against the snapshot, and one that drives a
|
|
@@ -198,9 +199,13 @@ and which run after merge is in [CI](ci.md).
|
|
|
198
199
|
- **Enforced by:** `node-ci.yml:check` · `<app>: pnpm contracts:upgrade`.
|
|
199
200
|
- **Example:** `pnpm contracts:upgrade` runs the previous published package and this build on the same synthetic
|
|
200
201
|
state and checks that the data survives. Each store migration also has its own upgrade test (WC-STORE-9).
|
|
201
|
-
- **Exceptions:** a CLI with no local state, or with no published version yet. Local state is data the CLI
|
|
202
|
-
|
|
203
|
-
|
|
202
|
+
- **Exceptions:** a CLI with no local state, or with no published version yet. Local state is data the CLI writes
|
|
203
|
+
and reads back in a later run, such as a store, a session or a cache. The shared store counts, also when cli-core's
|
|
204
|
+
code writes it, because the CLI's release decides which cli-core and so which migrations run: gcal's calendars and
|
|
205
|
+
events in the store are its local state. A config file, run records, the
|
|
206
|
+
update check, MCP logins and lock files that only cli-core writes do not count. Tests still keep them in the
|
|
207
|
+
sandboxed home, like every other path
|
|
208
|
+
([WC-TEST-1](#wc-test-1--point-every-path-a-test-could-write-at-a-temporary-directory)).
|
|
204
209
|
- **Applies to:** all CLIs with local state. wa has no upgrade contract yet. **Since:** cli-base 0.41.0. **Rulings:** none.
|
|
205
210
|
|
|
206
211
|
### WC-TEST-15 · A refactor keeps `commands --json` and the MCP tool list as they were
|
package/docs/standards/time.md
CHANGED
|
@@ -76,7 +76,14 @@ local midnight. The answer gives the window it used, in UTC.
|
|
|
76
76
|
- **Why:** "messages of 1 September" means the owner's day, not the UTC day, which starts hours earlier or later.
|
|
77
77
|
- **Enforced by:** review only.
|
|
78
78
|
- **Example:** in `Asia/Vladivostok`, `--since-time 2026-09-01` gives `meta.window.since` = `2026-08-31T14:00:00.000Z`.
|
|
79
|
-
-
|
|
79
|
+
A thing that has only dates, such as an all-day event (`start_date` 2026-09-01, `end_date` 2026-09-02, the end
|
|
80
|
+
not included), matches by comparing local dates, never UTC moments: it is in the window when its dates overlap the
|
|
81
|
+
local dates the window covers. So in `Europe/Madrid` a filter for 1 September returns the all-day event of
|
|
82
|
+
1 September and not the one of 31 August, whatever zone the event's calendar is in.
|
|
83
|
+
- **Exceptions:** none. Known gap, as of 2026-10-27, read from the code and not run: cli-core's event filter
|
|
84
|
+
(`src/store/sqlite/calendar.ts`, the `events` query) compares `starts_at` and `ends_at` with the UTC window for
|
|
85
|
+
all-day events too, and gcal stores an all-day event's start as midnight in its calendar's zone. The two agree
|
|
86
|
+
when the calendar's zone is the zone in use, and miss by a day at the edges when it is not.
|
|
80
87
|
- **Applies to:** all CLIs. **Since:** cli-base 0.48.0. **Rulings:** none.
|
|
81
88
|
|
|
82
89
|
### WC-TIME-6 · Run the tests in a zone that is not UTC, and prove stored values do not depend on it
|
|
@@ -200,8 +200,13 @@ no-unbounded-spread ([WC-TOOL-12](#wc-tool-12--do-not-spread-a-list-of-user-data
|
|
|
200
200
|
request.
|
|
201
201
|
- **Enforced by:** `<app>: pnpm lint:commands`.
|
|
202
202
|
- **Example:** `"lint:commands": "cli-dev lint --cli dist/bin/max.js"`, run by `ci:quick` after the build. Known
|
|
203
|
-
violations live in `lint/baseline/<rule-id>.json`; an entry with no violation left fails the run. A
|
|
204
|
-
|
|
203
|
+
violations live in `lint/baseline/<rule-id>.json`; an entry with no violation left fails the run. A violation that a
|
|
204
|
+
shared package forces, such as a verb cli-core names before the vocabulary has it, goes in the baseline too and
|
|
205
|
+
comes out when the shared package catches up. A word the CLI needs that the vocabulary does not have yet goes in
|
|
206
|
+
`lint/overrides.json`, never in the baseline: its `reason` links the cli-base pull request or issue that adds the
|
|
207
|
+
word, and it comes out in the change that takes the cli-base release with the word. The shared package's words
|
|
208
|
+
go in the baseline, as above; the CLI's own go in the overrides, where the count and the date keep them few. A permanent exception also goes in `lint/overrides.json`, with `rule`, `target`, `reason`, `owner`
|
|
209
|
+
and `review_after`. More than 10
|
|
205
210
|
overrides fail the run, and so does one past its date. The rules themselves are on the
|
|
206
211
|
[Commands page](commands.md).
|
|
207
212
|
- **Exceptions:** zoom runs `lint:commands` in full validation only. cli-core runs its own command lint test in
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wirecat/cli-base",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.65.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",
|