@wirecat/cli-base 0.63.0 → 0.64.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/dist/dev/main.js CHANGED
@@ -66,7 +66,15 @@ const USAGE = `usage: cli-dev <command> [options]
66
66
  rules by its own lint/ folder; the rest by --baseline
67
67
  (audit/baseline). --groups replaces PARITY_GROUPS with the
68
68
  JSON list of parity groups in that file; --differences adds
69
- to the groups in use`;
69
+ to the groups in use
70
+ bundle --bin <dist/bin/name.js> [--external <package>]... [--anchor <package>]... [--harmless <path>]...
71
+ bundles the compiled bin with esbuild (a devDependency) into
72
+ dist/bundle/ and turns the bin into its loader. --external keeps
73
+ a package outside (* matches anything); --anchor adds a package
74
+ whose modules keep their installed place, as cli-core's do;
75
+ --harmless names a third-party file whose import.meta or
76
+ __dirname was read and found harmless. Fails when a bundled
77
+ module would look for a file or package in the wrong place`;
70
78
  const load = async (cwd, path) => (await import(pathToFileURL(isAbsolute(path) ? path : join(cwd, path)).href));
71
79
  const report = (io, problems, ok) => {
72
80
  for (const problem of problems)
@@ -604,6 +612,43 @@ const auditRun = (args, io) => {
604
612
  io.err(problem);
605
613
  return problems.length > 0 ? 1 : 0;
606
614
  };
615
+ const bundleCommand = async (args, io) => {
616
+ const { values } = parseArgs({
617
+ args,
618
+ options: {
619
+ bin: { type: "string" },
620
+ external: { type: "string", multiple: true },
621
+ anchor: { type: "string", multiple: true },
622
+ harmless: { type: "string", multiple: true },
623
+ },
624
+ });
625
+ if (!values.bin) {
626
+ io.err("bundle needs --bin <dist/bin/name.js>");
627
+ return 2;
628
+ }
629
+ // esbuild is an optional peer: the other commands, and a CLI that does not bundle, never load it.
630
+ try {
631
+ await import("esbuild");
632
+ }
633
+ catch {
634
+ io.err("cli-dev bundle needs esbuild: add it as a devDependency, in cli-base's peer range");
635
+ return 1;
636
+ }
637
+ const { bundle } = await import("./bundle.js");
638
+ const result = await bundle({
639
+ root: io.cwd,
640
+ bin: values.bin,
641
+ external: values.external ?? [],
642
+ anchored: values.anchor ?? [],
643
+ harmless: values.harmless ?? [],
644
+ });
645
+ if (result.problems.length > 0) {
646
+ io.err(`the bundle would look for these in the wrong place, or is incomplete:\n${result.problems.join("\n")}`);
647
+ return 1;
648
+ }
649
+ io.out(`bundle: ${values.bin} loads dist/bundle/; a server preloads ${result.chunks.length} chunks`);
650
+ return 0;
651
+ };
607
652
  export const main = async (argv, io) => {
608
653
  const [command, ...args] = argv;
609
654
  if (args.includes("--help")) {
@@ -649,6 +694,8 @@ export const main = async (argv, io) => {
649
694
  return lint(args, io);
650
695
  case "audit":
651
696
  return audit(args, io);
697
+ case "bundle":
698
+ return bundleCommand(args, io);
652
699
  case "help":
653
700
  case "--help":
654
701
  case "-h":
@@ -11,7 +11,7 @@ export interface SkillApp {
11
11
  }
12
12
  export declare const SKILL_TARGETS: readonly ["claude", "agents"];
13
13
  export type SkillTarget = (typeof SKILL_TARGETS)[number];
14
- /** `~/.claude/skills` is Claude Code's; `~/.agents/skills` is read by Codex and Gemini CLI. */
14
+ /** `~/.claude/skills` is Claude Code's; `~/.agents/skills` is read by Codex, Gemini CLI and OpenClaw. */
15
15
  export declare const skillPath: (target: SkillTarget, appName: string, env: Env) => string;
16
16
  /**
17
17
  * `metadata.version` from the frontmatter — where the Agent Skills specification keeps fields of its
@@ -16,7 +16,7 @@ import { executableEnvironment, executableOnPath } from "../executable.js";
16
16
  import { CHECK_EVERY_MS, isNewer, readNoticeState, writeUpdateState } from "../update/index.js";
17
17
  export const SKILL_TARGETS = ["claude", "agents"];
18
18
  const homeOf = (env) => env.HOME || env.USERPROFILE || homedir();
19
- /** `~/.claude/skills` is Claude Code's; `~/.agents/skills` is read by Codex and Gemini CLI. */
19
+ /** `~/.claude/skills` is Claude Code's; `~/.agents/skills` is read by Codex, Gemini CLI and OpenClaw. */
20
20
  export const skillPath = (target, appName, env) => join(homeOf(env), target === "claude" ? ".claude" : ".agents", "skills", appName, "SKILL.md");
21
21
  const FRONTMATTER = /^---(\r?\n)([\s\S]*?\r?\n)?---(\r?\n|$)/;
22
22
  const scalar = (key, indent) => new RegExp(`^${indent}${key}:[ \\t]*["']?([^"'\\r\\n]*?)["']?[ \\t]*\\r?\\n?$`);
@@ -234,7 +234,7 @@ export const skillCommand = (app, skill, environment, more = {}) => {
234
234
  const named = Object.keys(more);
235
235
  const show = annotate(command.command("show"), { effects: { remote: "none", local: "read" } })
236
236
  .summary("print the tool's instructions for AI agents")
237
- .description(`print SKILL.md — \`${app.command} skill install\` puts it where Claude Code, Codex and Gemini CLI look for it; --for prints the copy already there`);
237
+ .description(`print SKILL.md — \`${app.command} skill install\` puts it where Claude Code, Codex, Gemini CLI and OpenClaw look for it, and where Hermes does on request; --for prints the copy already there`);
238
238
  if (named.length > 0)
239
239
  show.argument("[name]", `one of the skills shipped for a task: ${named.join(", ")}`);
240
240
  show.addOption(new Option("--for <agents>", "print the copy installed for these agents instead of this version's").choices(SKILL_TARGETS));
@@ -64,7 +64,7 @@ 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", "gcal"],
68
68
  covers: ["commands", "doctor", "skill", "mcp", "upgrade", "search", "notes", "meetings"],
69
69
  differences: [],
70
70
  },
@@ -0,0 +1,2 @@
1
+ import type { VocabularyFlag } from "./vocabulary.js";
2
+ export declare const CALENDAR_FLAGS: VocabularyFlag[];
@@ -0,0 +1,59 @@
1
+ export const CALENDAR_FLAGS = [
2
+ { name: "--end-time", value: "<time>", role: "value", type: "time", meaning: "when it ends" },
3
+ {
4
+ name: "--start-date",
5
+ value: "<date>",
6
+ role: "value",
7
+ type: "time",
8
+ meaning: "the first day of something that lasts whole days, such as an all-day event",
9
+ },
10
+ {
11
+ name: "--end-date",
12
+ value: "<date>",
13
+ role: "value",
14
+ type: "time",
15
+ meaning: "the last day of something that lasts whole days, such as an all-day event",
16
+ },
17
+ { name: "--location", value: "<text>", role: "value", type: "string", meaning: "where it takes place" },
18
+ {
19
+ name: "--attendee",
20
+ value: "<email>",
21
+ role: "value",
22
+ type: "string",
23
+ meaning: "a person invited to the event, by email address; repeat it for more",
24
+ },
25
+ {
26
+ name: "--id",
27
+ value: "<id>",
28
+ role: "value",
29
+ type: "id",
30
+ meaning: "the id to create it with, chosen beforehand, so a retry finds it instead of making a second one",
31
+ },
32
+ {
33
+ name: "--calendar",
34
+ value: "<id>",
35
+ role: "target",
36
+ type: "id",
37
+ meaning: "the calendar it acts on, when the thing is on more than one",
38
+ },
39
+ {
40
+ name: "--repeat",
41
+ value: "<rule>",
42
+ role: "value",
43
+ type: "string",
44
+ meaning: "how it repeats, as an RFC 5545 rule such as FREQ=WEEKLY;BYDAY=MO;COUNT=10",
45
+ },
46
+ {
47
+ name: "--hours",
48
+ value: "<from-to>",
49
+ role: "target",
50
+ type: "string",
51
+ meaning: "the working hours of each day to look in, such as 09:00-17:00, in the owner's time zone",
52
+ },
53
+ {
54
+ name: "--series",
55
+ role: "target",
56
+ type: "switch",
57
+ meaning: "act on the whole repeating event, not only this occurrence",
58
+ },
59
+ ];
@@ -308,7 +308,13 @@ export const HOW_FLAGS = [
308
308
  meaning: "do not keep this run, whatever the configuration says",
309
309
  },
310
310
  { name: "--no-registries", role: "mode", type: "switch", meaning: "do not ask the public ban lists" },
311
- { name: "--notify", role: "mode", type: "switch", meaning: "tell the chat's members about it" },
311
+ {
312
+ name: "--notify",
313
+ role: "mode",
314
+ type: "switch",
315
+ meaning: "tell the people it concerns about it: the chat's members, or an event's attendees",
316
+ },
317
+ { name: "--no-notify", role: "mode", type: "switch", meaning: "tell nobody about it" },
312
318
  { name: "--offline", role: "mode", type: "switch", meaning: "answer from what was recorded and never connect" },
313
319
  {
314
320
  name: "--operation-id",
@@ -1,4 +1,5 @@
1
1
  import * as v from "valibot";
2
+ import { CALENDAR_FLAGS } from "./flags-calendar.js";
2
3
  import { HOW_FLAGS } from "./flags-how.js";
3
4
  import { WHAT_FLAGS } from "./flags-what.js";
4
5
  import { duplicates, parseRegistry, text, word } from "./registry.js";
@@ -123,6 +124,7 @@ const verbs = [
123
124
  "bring the local store up to date from the source: only what changed, or the whole list where the source cannot say; a mirror syncs both ways",
124
125
  ],
125
126
  ["fetch", "one bounded pull of one named thing into the store, not a catch-up"],
127
+ ["unsync", "stop syncing a thing; what the store already holds stays"],
126
128
  ["export", "write from the store to a file"],
127
129
  ["import", "bring a file or list the user gives into the store or the messenger, once"],
128
130
  ["download", "save a file from the source to this machine"],
@@ -153,6 +155,7 @@ const verbs = [
153
155
  ["unstar", "take the owner's star off a message"],
154
156
  ["vote", "answer a poll"],
155
157
  ["answer", "reply to a button someone pressed under a bot's message"],
158
+ ["respond", "answer an invitation: accept, tentatively accept or decline it"],
156
159
  ["press", "press a button under a bot's message; the bot sees who pressed it"],
157
160
  ["action", "show what a bot is doing in a chat, such as typing, for a few seconds"],
158
161
  ["watch", "follow new events as they come, and keep them"],
@@ -175,6 +178,8 @@ const verbs = [
175
178
  ["acknowledge", "record successful delivery using the exact receipt issued when the item was claimed"],
176
179
  ["confirm", "the owner accepts what an agent proposed"],
177
180
  ["reject", "the owner refuses what an agent proposed"],
181
+ ["review", "list what waits for the owner's decision"],
182
+ ["resolve", "settle one thing that waits for the owner's decision, with the owner's choice"],
178
183
  ["move", "move a file in the source"],
179
184
  ["copy", "copy a file in the source"],
180
185
  ["run", "run a saved job once, now, as its timer would"],
@@ -241,6 +246,9 @@ const collections = [
241
246
  ["files", "files in the source, such as Google Drive, read and changed there"],
242
247
  ["comments", "comments people left on a thing in the source: a channel post or a file"],
243
248
  ["meetings", "stored meetings, from Zoom or another provider"],
249
+ ["calendars", "calendars of an account, each synced or not"],
250
+ ["events", "calendar entries: meetings, appointments and other things on a calendar"],
251
+ ["people", "persons the owner deals with, such as the attendees of events"],
244
252
  ["recordings", "the files a provider recorded of a meeting"],
245
253
  ["jobs", "work this tool runs in the background or on a timer"],
246
254
  ["actions", "what an agent proposes to do; the owner confirms or rejects each"],
@@ -302,6 +310,7 @@ const views = [
302
310
  ["evidence", "the stored messages or lines a result or a reference rests on"],
303
311
  ["passage", "a cited transcript line with bounded surrounding lines from the same revision"],
304
312
  ["agenda", "what needs a look today"],
313
+ ["busy", "when calendars or people are taken, without what takes them"],
305
314
  ["categories", "the categories a thing takes, with what each means"],
306
315
  ["domains", "the internet domains links point to, most linked first"],
307
316
  ["places", "the street addresses confirmed as places, most mentioned first"],
@@ -348,5 +357,5 @@ export const VOCABULARY = defineVocabulary({
348
357
  ],
349
358
  views: views.map(([entry, meaning]) => ({ word: entry, meaning })),
350
359
  roots: roots.map(([entry, meaning]) => ({ word: entry, meaning })),
351
- flags: [...HOW_FLAGS, ...WHAT_FLAGS],
360
+ flags: [...HOW_FLAGS, ...WHAT_FLAGS, ...CALENDAR_FLAGS],
352
361
  });
@@ -28,12 +28,12 @@ the four steps that need a person. A new CLI is ready for its first release when
28
28
  | Page | Area id | Rules | What it covers |
29
29
  |---|---|---:|---|
30
30
  | [Toolchain](toolchain.md) | `TOOL` | 20 | TypeScript, Biome, grit plugins, size caps, import cycles, spelling, Markdown, Go |
31
- | [Layout](layout.md) | `LAY` | 9 | Repository layout, entry points, exports, file size, one job per file, splitting a unit |
31
+ | [Layout](layout.md) | `LAY` | 10 | Repository layout, entry points, exports, file size, one job per file, splitting a unit, the bundled bin |
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
35
  | [Accounts](accounts.md) | `ACCT` | 11 | The explicit profile, reads across accounts, scope flags, linked identities |
36
- | [Permissions](permissions.md) | `PERM` | 21 | Levels, keys, defaults, hard limits, the write guard and its journal |
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
38
  | [Lifecycle](lifecycle.md) | `LIFE` | 14 | Opening late, who closes, one-shot exits, long-running commands, the `serve` relay, resumable syncs, the run log |
39
39
  | [Store](store.md) | `STORE` | 14 | Store areas, account scope, migrations, test isolation |
@@ -84,7 +84,7 @@ Every rule has the same shape. A rule may add a sentence or a short list under i
84
84
  CLI's `node_modules`;
85
85
  - a check in another repository, as `<repo>: <check>`, such as `cli-core: pnpm parity:check` or
86
86
  `cli-core: biome noRestrictedImports`. `<repo>` is the repository's name on GitHub: `cli-core`, `cli-testing`,
87
- `cli-docs`, `community`, `tg-cli`, `max-cli`, `cli-memo`, `drive-cli`, `zoom-cli` or `whatsapp-cli`. `<app>`
87
+ `cli-docs`, `community`, `tg-cli`, `max-cli`, `cli-memo`, `drive-cli`, `zoom-cli`, `whatsapp-cli` or `gcal-cli`. `<app>`
88
88
  stands for each CLI the rule applies to, when every one of them runs that check in its own repository.
89
89
  - **Since** is the cli-base release whose handbook first carried the rule. A feature can be older than its rule.
90
90
 
@@ -57,8 +57,8 @@ CLI with a `setup` command suggests it, and any other CLI suggests the command t
57
57
  - **Enforced by:** `cli-core: src/cli/settings.test.ts` · `cli-core: src/cli/program.test.ts`.
58
58
  - **Example:** ``name a profile: `tg <profile> …` — profiles here: personal, work (or set TG_PROFILE)``. With none,
59
59
  tg, which has `setup`, says to ``create one with `tg personal setup` (any name in place of personal)``. A CLI
60
- without `setup`, such as one made from cli-template, answers `cal events list` with ``name a profile: there is none
61
- on this computer yet — any name works, such as `cal personal events list` (or set CAL_PROFILE)`` (cli-core 0.424.0
60
+ without `setup`, such as one made from cli-template, answers `gcal events list` with ``name a profile: there is none
61
+ on this computer yet — any name works, such as `gcal personal events list` (or set GCAL_PROFILE)`` (cli-core 0.424.0
62
62
  and later).
63
63
  - **Exceptions:** none.
64
64
  - **Applies to:** all CLIs. **Since:** cli-base 0.41.0. **Rulings:** none.
@@ -10,8 +10,9 @@ Rule ids on this page are `WC-CI-<n>`. How to read a rule, how ids work and whic
10
10
  [Releases page](releases.md); which scanners must exist and how workflows are hardened is on the
11
11
  [Security page](security.md).
12
12
 
13
- "The ten repositories" on this page means the six CLIs (tg, max, memo, drive, zoom, wa), cli-base, cli-core,
14
- cli-testing, cli-docs, plus community itself. cli-template follows every rule here as a CLI does
13
+ "The WireCat repositories" on this page means every WireCat CLI (today tg, max, memo, drive, zoom, wa and gcal; a
14
+ CLI made with `npx @wirecat/cli-template new` counts from its first commit), cli-base, cli-core, cli-testing, cli-docs,
15
+ plus community itself. cli-template follows every rule here as a CLI does
15
16
  ([New CLI](new-cli.md#the-template-repository-itself)).
16
17
 
17
18
  ## Rules
@@ -24,7 +25,7 @@ cli-testing, cli-docs, plus community itself. cli-template follows every rule he
24
25
  `gitleaks git --staged --redact`. A clone without gitleaks gets a warning and can still commit; CI scans anyway.
25
26
  The `prepare` script installs the hooks; `LEFTHOOK=0 git commit` skips them for one commit.
26
27
  - **Exceptions:** none.
27
- - **Applies to:** the ten repositories. **Since:** cli-base 0.41.0. **Rulings:** none.
28
+ - **Applies to:** the WireCat repositories. **Since:** cli-base 0.41.0. **Rulings:** none.
28
29
 
29
30
  ### WC-CI-2 · Add no push hook, and no hook that installs, builds, checks types, generates or tests
30
31
 
@@ -34,7 +35,7 @@ cli-testing, cli-docs, plus community itself. cli-template follows every rule he
34
35
  does not run `standards:check`, the full lint, spelling, a build or the test suite on each commit or push; it runs a
35
36
  targeted check when it needs one.
36
37
  - **Exceptions:** none. cli-base `config/lefthook.yml` still has a pre-push typecheck, but no repository extends it.
37
- - **Applies to:** the ten repositories. **Since:** cli-base 0.41.0. **Rulings:** none.
38
+ - **Applies to:** the WireCat repositories. **Since:** cli-base 0.41.0. **Rulings:** none.
38
39
 
39
40
  ### WC-CI-3 · Run the fast standards job on every pull request and every push to `main`
40
41
 
@@ -45,7 +46,7 @@ cli-testing, cli-docs, plus community itself. cli-template follows every rule he
45
46
  commit cancels the run it replaces. The job times out after five minutes, a ceiling for a stalled setup, not an
46
47
  expected time.
47
48
  - **Exceptions:** none. Runner queues and cold downloads are outside the budget.
48
- - **Applies to:** the ten repositories. **Since:** cli-base 0.41.0. **Rulings:** none.
49
+ - **Applies to:** the WireCat repositories. **Since:** cli-base 0.41.0. **Rulings:** none.
49
50
 
50
51
  ### WC-CI-4 · Keep `ci:quick` to early bug checks that finish within two minutes
51
52
 
@@ -61,7 +62,7 @@ cli-testing, cli-docs, plus community itself. cli-template follows every rule he
61
62
  - **Exceptions:** never put live-account suites, agent suites or platform jobs here. Full tests and coverage run in
62
63
  the pull request validation job ([WC-CI-5](#wc-ci-5--run-the-full-linux-validation-on-every-pull-request-and-keep-platform-suites-out)),
63
64
  not in `ci:quick`.
64
- - **Applies to:** the ten repositories. **Since:** cli-base 0.41.0. **Rulings:** none.
65
+ - **Applies to:** the WireCat repositories. **Since:** cli-base 0.41.0. **Rulings:** none.
65
66
 
66
67
  ### WC-CI-5 · Run the full Linux validation on every pull request, and keep platform suites out
67
68
 
@@ -139,10 +140,13 @@ cannot pass yet; the `skip` input names it.
139
140
 
140
141
  The shared workflows are not in the npm package, because CI runs the pinned commit, not the installed copy. To see
141
142
  what CI runs, an audit reads the `uses: WireCatLabs/cli-base/.github/workflows/…@<commit>` lines in `ci.yml`,
142
- `release.yml` and `monthly.yml`, and opens each workflow file at that commit in cli-base.
143
+ `release.yml` and `monthly.yml`, and opens each workflow file at that commit in cli-base. The fresh-agent audit on the
144
+ [New CLI](new-cli.md#done-when) page is given these files for this reason, beside the handbook and the repository;
145
+ its prompt in cli-template says so from cli-template 0.8.0. A CLI on an older template takes the prompt with
146
+ `upgrade`.
143
147
 
144
148
  - **Why:** one shared workflow keeps the full suite the same everywhere, and a pin keeps a cli-base change from
145
- breaking ten repositories at once.
149
+ breaking every repository at once.
146
150
  - **Enforced by:** `ci.yml:ci`.
147
151
  - **Example:** `uses: WireCatLabs/cli-base/.github/workflows/node-ci.yml@<commit> # v0.31.0`, with `cli: true` for
148
152
  the CLI steps (WC-CI-7), the project's extra checks in the `checks` input and `bun-run` for the Bun job. A job that only one repository needs stays beside it in
@@ -158,12 +162,29 @@ what CI runs, an audit reads the `uses: WireCatLabs/cli-base/.github/workflows/
158
162
  - **Example:** `monthly.yml`, from the community template, calls the repository's `ci.yml` and the pinned cli-base
159
163
  `release-checks.yml`. Each repository has its own minute, between 02:17 and 03:20 UTC, so runs do not overlap. No
160
164
  check compares the minutes across repositories; review keeps them apart. A CLI made from cli-template starts with
161
- the template's own minute and changes it (see [New CLI](new-cli.md)).
165
+ the template's own minute and changes it (see [New CLI](new-cli.md)). Taken minutes, from each `monthly.yml` on
166
+ the default branch:
167
+
168
+ | UTC | Repository |
169
+ |---|---|
170
+ | 02:17 | cli-base |
171
+ | 02:24 | whatsapp-cli |
172
+ | 02:27 | gcal-cli |
173
+ | 02:31 | cli-memo |
174
+ | 02:38 | cli-core |
175
+ | 02:45 | drive-cli |
176
+ | 02:52 | cli-testing |
177
+ | 02:59 | max-cli |
178
+ | 03:02 | cli-template |
179
+ | 03:06 | tg-cli |
180
+ | 03:13 | zoom-cli |
181
+ | 03:20 | cli-docs |
182
+
162
183
  - **Exceptions:** cli-core also builds and probes its native packages in their own workflows, after the window:
163
184
  `onnx.yml` at 03:27 and `sqlite.yml` at 03:37 UTC. A scheduled run of either never publishes; only a manual run
164
185
  with `publish` does. community has no build, so it runs only the release security checks, at 03:47 UTC. A monthly
165
186
  run never publishes a package or deploys the site.
166
- - **Applies to:** the ten repositories. **Since:** cli-base 0.41.0. **Rulings:** none.
187
+ - **Applies to:** the WireCat repositories. **Since:** cli-base 0.41.0. **Rulings:** none.
167
188
 
168
189
  ### WC-CI-10 · Run heavy security checks only at release, monthly or on request
169
190
 
@@ -177,7 +198,7 @@ what CI runs, an audit reads the `uses: WireCatLabs/cli-base/.github/workflows/
177
198
  job ([WC-CI-5](#wc-ci-5--run-the-full-linux-validation-on-every-pull-request-and-keep-platform-suites-out)); it
178
199
  takes seconds. The dependency audit, `zizmor` and CodeQL stay out. Do not add a heavy scanner to an editor hook, a
179
200
  stop hook or an ordinary pull request job.
180
- - **Applies to:** the ten repositories. **Since:** cli-base 0.41.0. **Rulings:** sc:9.
201
+ - **Applies to:** the WireCat repositories. **Since:** cli-base 0.41.0. **Rulings:** sc:9.
181
202
 
182
203
  ### WC-CI-11 · Moved to [WC-SECR-20](security.md#wc-secr-20--pin-each-third-party-action-to-a-commit-and-give-each-workflow-read-only-defaults)
183
204
 
@@ -195,7 +216,7 @@ what CI runs, an audit reads the `uses: WireCatLabs/cli-base/.github/workflows/
195
216
  release changes `main` the same way
196
217
  ([WC-REL-2](releases.md#wc-rel-2--main-changes-only-through-a-pull-request-also-during-a-release)).
197
218
  - **Exceptions:** none.
198
- - **Applies to:** the ten repositories. **Since:** cli-base 0.41.0. **Rulings:** NEED-381.
219
+ - **Applies to:** the WireCat repositories. **Since:** cli-base 0.41.0. **Rulings:** NEED-381.
199
220
 
200
221
  ### WC-CI-13 · Start no live messenger or account test from a workflow
201
222
 
@@ -77,8 +77,9 @@ catalogue and parity manifest stay in cli-core
77
77
  - **Example:** ranking is `stats messages top`, not a ranking mode on `search messages`. Options choose the scope,
78
78
  measure, order or format of the same action. No command both shows and changes something.
79
79
  - **Exceptions:** a documented preparatory fetch such as `--sync-first`. It keeps its own permissions and reports
80
- its outcome.
81
- - **Applies to:** all CLIs. **Since:** cli-base 0.41.0. **Rulings:** none.
80
+ its outcome. `mcp --revoke` forgets every login given to a browser app; it stays an option on `mcp` because it
81
+ belongs with the HTTP server's login state.
82
+ - **Applies to:** all CLIs. **Since:** cli-base 0.41.0. **Rulings:** NEED-1864.
82
83
 
83
84
  ### WC-CMD-7 · Never let two commands overlap
84
85
 
@@ -18,11 +18,11 @@ nothing from either package.
18
18
  | Package and path | Exports |
19
19
  |---|---|
20
20
  | `@wirecat/cli-base` | `catchCrashes`, `CliError`, `configFilePath`, `mcpErrorResult`, `resolvePaths` |
21
- | `@wirecat/cli-base/commands` | `annotate`, `CommandInfo`, `describeProgram` |
21
+ | `@wirecat/cli-base/commands` | `annotate`, `CommandInfo`, `COMMANDS_PAGE_LABELS` (in `scripts/`), `commandsPage` (in `scripts/`), `describeOptions` (in `scripts/`), `describeProgram` |
22
22
  | `@wirecat/cli-base/codegen` | `writeArtifacts` (in `scripts/`) |
23
23
  | `@wirecat/cli-base/release` | `DocsRules`, `JOURNAL_IDS`, `markdownFiles`, `packContents` (in `scripts/`) |
24
- | `@wirecat/cli-core/cli` | `AppIdentity`, `changesAuthorityAt`, `commandsCommand`, `configCommand`, `Configuration`, `createProgram`, `environmentOf`, `GlobalFlags`, `itemOf`, `LOCAL_READ`, `outputFor`, `privateFiles`, `profileFree`, `ProgramDefinition`, `ProgramOptions`, `readOnlyLockOf`, `REMOTE_WRITE`, `renderAnswer`, `rootOf`, `run`, `RunOptions`, `runsCommand`, `settingsFor`, `skillCommand`, `upgradeCommand` |
25
- | `@wirecat/cli-core/mcp` | `McpCommand`, `registerCommands`, `runCommandForAgent`, `toolsFromCommands` |
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` |
26
26
  | `@wirecat/cli-core/sends` | `Level` |
27
27
  | `@wirecat/cli-core/update` | `appUpdate` |
28
28
  | `@wirecat/cli-core/testing` | `ARGV_LOG`, `argvLogSetup` |
@@ -122,6 +122,38 @@ Rule ids on this page are `WC-LAY-<n>`. How to read a rule, how ids work and whi
122
122
  - **Exceptions:** none.
123
123
  - **Applies to:** all CLIs and libraries. **Since:** cli-base 0.41.0. **Rulings:** none.
124
124
 
125
+ ### WC-LAY-10 · Ship the bin as a bundle that `cli-dev bundle` builds
126
+
127
+ A command that imports about a thousand module files spends most of its start resolving and reading them. tg's
128
+ bundle halved `--help` and every other short command
129
+ ([measurements](https://github.com/WireCatLabs/cli-testing/blob/main/performance/startup/tg-bundle/README.md)).
130
+
131
+ - **Why:** one shared build keeps what tg learned in one place: lazy imports stay lazy, files and packages are found
132
+ where they were installed, and a running server survives an in-place upgrade.
133
+ - **Enforced by:** `<app>: pnpm build` · `<app>: pnpm smoke:node` · `<app>: pnpm smoke:bun`.
134
+ - **Example:** `"build": "tsc --build --force && cli-dev bundle --bin dist/bin/tg.js --external '@mtcute/*' --external undici"`,
135
+ with `esbuild` as a devDependency in cli-base's peer range. `dist/bundle/` holds the bundle, split so every lazy
136
+ `import()` stays a chunk of its own; `dist/bin/tg.js` stays the path `"bin"` names and becomes a loader. What it
137
+ does and checks:
138
+ - The loader answers `--version` from the version built into it, turns the compile cache on, and imports the
139
+ bundle. Under `serve`, `watch` and `mcp` it loads every chunk 2 s after start and resolves imports between chunks
140
+ without looking on disk: an upgrade deletes the old chunks, whose names carry a hash.
141
+ - In the CLI's own modules and in cli-core's and cli-base's (`--anchor` adds a package), `import.meta.url` is the
142
+ module's place before bundling, so skills, fonts, scenarios, migrations and child scripts are found. A package
143
+ they load at run time resolves from that place too (a resolve hook under Node, `Bun.resolveSync` under Bun):
144
+ pnpm installs cli-core's dependencies beside cli-core.
145
+ - A dynamic import of CommonJS (`yaml`) keeps its named exports. Names are kept (`keepNames`), so `commands --json`
146
+ and errors read as before.
147
+ - Native modules, Bun's SQLite and the packages cli-core loads by name stay outside; `--external` adds the CLI's
148
+ own (`*` matches anything).
149
+ - The build fails when a bundled module would look for a file or package from the chunk's place, or when the loader
150
+ names a chunk that was not built. A third-party file read and found harmless is named with `--harmless`.
151
+ - The CLI's smoke test runs the built bin under Node and Bun and checks that the store opens and the document
152
+ readers resolve.
153
+ - **Exceptions:** a CLI moves when its outputs are byte-identical before and after and its start gains 15% or more;
154
+ until then it ships `tsc`'s output. cli-base, cli-core and cli-testing ship libraries, not a bin.
155
+ - **Applies to:** all CLIs. **Since:** cli-base 0.64.0. **Rulings:** none.
156
+
125
157
  ## Sources
126
158
 
127
159
  These documents explain the rules on this page in more detail. Where one of them disagrees with this page, this page
@@ -12,7 +12,7 @@ built on are on the [Kit surface](kit-surface.md) page.
12
12
  ## Start with the template
13
13
 
14
14
  ```sh
15
- npx @wirecat/cli-template new cal-cli --command cal --package @wirecat/cal-cli --owner WireCatLabs \
15
+ npx @wirecat/cli-template new gcal-cli --command gcal --package @wirecat/gcal-cli --owner WireCatLabs \
16
16
  --category calendar --content-lang en,ru --sign-in oauth --api official-client --filters time,person,text \
17
17
  --never-store tokens,secret-links --untrusted names,links
18
18
  ```
@@ -24,7 +24,7 @@ the choices. Nothing is guessed.
24
24
  |---|---|---|
25
25
  | `<name>-cli` | the repository folder | any name |
26
26
  | `--command` | the command word people type | one word |
27
- | `--package` | the npm package name | a scoped name, such as `@wirecat/cal-cli` |
27
+ | `--package` | the npm package name | a scoped name, such as `@wirecat/gcal-cli` |
28
28
  | `--owner` | the GitHub owner of the repository | an owner |
29
29
  | `--category` | what the CLI reads | `messenger`, `mail`, `notes`, `files`, `calendar`, `meetings` |
30
30
  | `--content-lang` | the languages of the content | a list, such as `en,ru` |
@@ -42,11 +42,11 @@ a reference CLI for your category in `AGENTS.md`. It does not install packages,
42
42
  publish. Then:
43
43
 
44
44
  ```sh
45
- cd cal-cli && pnpm install && pnpm ci:quick
45
+ cd gcal-cli && pnpm install && pnpm ci:quick
46
46
  git add -A && git commit -m "chore: start from @wirecat/cli-template"
47
47
  ```
48
48
 
49
- Give the agent `docs/dev/DESIGN.md` next. Every example passes a profile explicitly, such as `cal personal doctor`.
49
+ Give the agent `docs/dev/DESIGN.md` next. Every example passes a profile explicitly, such as `gcal personal doctor`.
50
50
  The reference CLI is the one to compare with when a step is unclear.
51
51
 
52
52
  The copy already holds every file this handbook asks for: the required root commands (`commands`, `doctor`,
@@ -61,6 +61,15 @@ The rules they keep are on the area pages: [Commands](commands.md), [MCP](mcp.md
61
61
  Later template changes come in with `npx @wirecat/cli-template upgrade`, file by file: apply, replace, skip or diff.
62
62
  See [Kit surface](kit-surface.md#how-your-cli-stays-current).
63
63
 
64
+ The template's `template.json` splits the copy in two. A **seed** (`DESIGN.md`, `CLAUDE.md`, `README.md`, the user
65
+ pages, `src/`) is the CLI's own from the first commit: rewrite it freely. A **template-owned** file (`AGENTS.md`,
66
+ `docs/dev/TESTING.md`, `docs/dev/reference-clis.md`, the skills under `docs/dev/skills/`, `.github/`) holds only what
67
+ is true for every CLI made from the template. Do not rewrite one to fit this CLI: put the CLI's own facts in a seed.
68
+ A template-owned page that is wrong for this CLI is a gap in cli-template, under
69
+ [WC-DOC-3](docs.md#wc-doc-3--every-document-states-current-facts-rewrite-a-wrong-sentence-in-place) like any other
70
+ wrong sentence: fix it there, and `upgrade` brings the fix. Change it in the CLI only where the CLI must differ for
71
+ good; `upgrade` then asks before it touches the change.
72
+
64
73
  ## Four steps that need a person
65
74
 
66
75
  `new` prints these at the end.
@@ -69,12 +78,14 @@ See [Kit surface](kit-surface.md#how-your-cli-stays-current).
69
78
  changes only through a pull request, required status checks, and no bypass. Add the ruleset after the first
70
79
  commit is on `main`, since a pull request rule leaves an empty repository no way to get one, and the required
71
80
  check names exist only once CI has run. See [Releases](releases.md).
72
- 2. **Set npm trusted publishing** for the new package, naming `release.yml`, and add any secrets the CI needs. See
81
+ 2. **Set npm trusted publishing** for the new package, naming `release.yml`, and add any secrets the CI needs. npm
82
+ offers this only once the package exists, so the owner publishes the first version with `bin/release --local`
83
+ and sets it right after. See
73
84
  [WC-REL-11](releases.md#wc-rel-11--publish-from-github-actions-with-npm-trusted-publishing).
74
- 3. **Pick a monthly cron minute** between 02:17 and 03:20 UTC that no other repository uses. The copy of
75
- `.github/workflows/monthly.yml` keeps the template's own minute, 03:02, and no check compares minutes across
76
- repositories, so change it by hand. See
85
+ 3. **Pick a monthly cron minute** between 02:17 and 03:20 UTC that is not in the list of taken minutes under
77
86
  [WC-CI-9](ci.md#wc-ci-9--run-full-validation-and-release-security-on-the-first-day-of-each-month-without-publishing).
87
+ The copy of `.github/workflows/monthly.yml` keeps the template's own minute, 03:02, and no check compares minutes
88
+ across repositories, so change it by hand, and add the new minute to the list in the step 4 pull request.
78
89
  4. **WireCat CLIs only: join the shared lists.** A pull request to cli-base's `PARITY_GROUPS` (at least the `all`
79
90
  group, which compares the shared root commands), a line in cli-core's `WIRECAT_TOOLS` and a line in cli-docs'
80
91
  `tools.json`. A messenger also joins cli-core's `parity.json`. A CLI outside WireCatLabs keeps its parity groups
@@ -89,17 +100,20 @@ All of these pass on `main`, before the first release:
89
100
  - `cli-dev lint` against the built CLI. It also makes the CLI fail once in an empty home and checks that
90
101
  `runs list` shows the failed run
91
102
  - `cli-dev audit --for <cli> --lint <cli>=lint` with no new finding, run with the command trees of the CLIs in
92
- the same parity groups. `--lint` judges the CLI's lint rules by its own `lint/` folder. What it shares with the
93
- other CLIs is judged by cli-base's `audit/baseline`; the pull request that adds the CLI to a parity group adds its
94
- shared findings there
103
+ the same parity groups (`pnpm audit:commands`). `--lint` judges the CLI's lint rules by its own `lint/` folder.
104
+ What it shares with the other CLIs has no baseline: `audit/baseline` does not accept a shared-contract finding
105
+ (cli-base 0.51.0). Fix each one, or, where the CLI must differ for good, the pull request that adds it to a parity
106
+ group declares the exact difference with its reason in that group's `differences`
95
107
  - `pnpm contracts:check`
96
108
  - `cli-dev test-matrix --check`
97
109
  - `pnpm docs:check`, with `--pages`, and the CLI's `generate:check`
98
110
  - the changelog guard on a pull request
99
111
  - `pnpm release:check`
100
112
 
101
- The last test is a fresh agent. Give it only this handbook's [index](README.md) and the new repository, and ask it to
102
- audit the CLI. Every gap it lists is either a gap in the CLI, to fix before the first release, or a hole in the
113
+ The last test is a fresh agent. Give it only this handbook's [index](README.md), the new repository and the cli-base
114
+ workflow files its own workflows pin
115
+ ([WC-CI-8](ci.md#wc-ci-8--call-cli-base-node-ciyml-from-ciyml-pinned-to-a-tag-or-commit)), and ask it to audit the
116
+ CLI. Every gap it lists is either a gap in the CLI, to fix before the first release, or a hole in the
103
117
  handbook, to fix here.
104
118
 
105
119
  ## The template repository itself
@@ -118,7 +132,7 @@ upgrade, and no script or agent reads its answers.
118
132
  | [WC-DOC-12](docs.md#wc-doc-12--a-runnable-command-in-a-document-has-no-placeholder-in-it) | Exempt only for the placeholder names `clidev`, `clidev-cli`, `@wirecat/clidev-cli` and `WireCatLabs/clidev-cli`, and the upper-case prefix `CLIDEV_` (as in `CLIDEV_PROFILE`). | `new` replaces each of them with the CLI's own name. Any other placeholder is still a gap. |
119
133
  | [WC-DOC-17](docs.md#wc-doc-17--each-entry-says-what-changed-for-the-caller-why-and-what-to-watch-for) | A changelog entry may name a file path. | The template's readers are CLI authors, who edit those files or take them with `upgrade`; for them a path is what they see. |
120
134
 
121
- Every other rule that applies to all CLIs, or to the ten repositories, applies to cli-template as it is written,
135
+ Every other rule that applies to all CLIs, or to the WireCat repositories, applies to cli-template as it is written,
122
136
  with one limit for the copy tool. It follows the rules for code and the repository: Toolchain, Layout, Style,
123
137
  Testing, Security and Documents. It has no profile, no machine mode, no MCP server, no store and no agent reading its
124
138
  answers, so the rules for a CLI's commands and runtime do not reach it: Commands, Output, MCP, Accounts, Permissions,
@@ -20,8 +20,9 @@ Rule ids on this page are `WC-PERM-<n>`. How to read a rule, how ids work and wh
20
20
  tg, max and wa use cli-core's permissions and write guard in full. memo uses them for the shared commands only. drive
21
21
  has its own policy and guard, with its own `--read-only` flag. zoom has per-profile `permissions` from zoom-cli 0.35.0,
22
22
  read with cli-core's `levelFor`; its writes change Zoom meetings, its built-in default is `remote-meetings: ask`, and
23
- it has no send journal or recipient list, since it sends no messages. Where this page says "every CLI that writes",
24
- drive is a known gap.
23
+ it has no send journal or recipient list, since it sends no messages. gcal declares its own keys through cli-core's
24
+ `commandPermissions` and runs its event writes through cli-core's write guard (WC-PERM-22). Where this page says
25
+ "every CLI that writes", drive is a known gap.
25
26
 
26
27
  WC-PERM-10 and WC-PERM-11 need cli-core 0.321.0, which tg, max and wa pin. WC-PERM-12 as written needs cli-core
27
28
  0.337.0; before it, the lock let through some local writes that overwrite what the owner saved. A command other
@@ -114,7 +115,8 @@ A default is never made stricter without the owner's word.
114
115
  - **Exceptions:** where the messenger has replies, `replies.send` is `deny`: a reply rule writes to people with nobody
115
116
  typing a command, so it waits for the owner's own `allow`. A messenger with no `replies` group, such as wa, has no
116
117
  use for the key.
117
- - **Applies to:** messenger CLIs. **Since:** cli-base 0.41.0. **Rulings:** NEED-568.
118
+ - **Applies to:** messenger CLIs. A CLI that is not a messenger sets its own defaults (WC-PERM-22). **Since:** cli-base
119
+ 0.41.0. **Rulings:** NEED-568.
118
120
 
119
121
  ### WC-PERM-7 · A built-in default only ever tightens
120
122
 
@@ -310,6 +312,34 @@ Moderation rules use the same four levels for each kind of action (delete, remov
310
312
  looser than the profile's `chats.moderate`; the code does not check that yet.
311
313
  - **Applies to:** messenger CLIs. **Since:** cli-base 0.41.0. **Rulings:** NEED-306, NEED-310.
312
314
 
315
+ ### WC-PERM-22 · A CLI that is not a messenger declares its own keys and defaults with `commandPermissions`
316
+
317
+ A messenger's keys come from its resources (`messages`, `chats`). Any other CLI names its own with cli-core's
318
+ `commandPermissions({ keyOf, extra, defaults })`, passed as `settingsFor(app, { permissions })` (cli-core 0.450.0):
319
+
320
+ - **Keys.** Each command is checked against its own path (`events.create`), the default `keyOf`. Housekeeping has no
321
+ key (WC-PERM-5). `extra` adds the keys a command checks inside itself, such as one per generated API method
322
+ (`api.calendar.events.insert`). A key that names no command is refused when it is set or saved.
323
+ - **Defaults.** `defaults` holds the CLI's built-in levels, beside cli-core's. They only ever tighten (WC-PERM-7). A
324
+ CLI picks them for what its writes do, not by WC-PERM-6's messenger list: they ask before a write people other
325
+ than the owner see or cannot easily undo, and deny what an agent must never do unasked.
326
+ - **Read-only lock and guard.** `<PREFIX>_READ_ONLY` refuses every write to the service, as in WC-PERM-12. Every
327
+ write runs through cli-core's write guard with `permissionDefaults: configuration.permissions.defaults`
328
+ (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
329
+ (WC-PERM-17). A CLI that sends no messages has no recipient list or hourly limit to set.
330
+
331
+ - **Why:** one permission model for every CLI; a CLI-own layer drifts from the shared one and is audited by nobody.
332
+ - **Enforced by:** `cli-core: src/cli/command-permissions.test.ts` · `cli-core: src/calendar/cli/writes.test.ts`.
333
+ - **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): `events create`, `update`, `respond` and `delete` ask, since each
335
+ changes a calendar other people may share or be invited from; `events.delete.series` (a whole repeating series) and
336
+ `events.notify` (mail the attendees) are `deny` until the owner allows them. gcal adds its own: a generated
337
+ `api <method>` that writes asks, and one that shares, empties or gives away a calendar is `deny`.
338
+ - **Exceptions:** known gaps: drive keeps its own policy and `--read-only` flag, zoom reads its own
339
+ `remote-meetings` key with `levelFor`, and memo uses cli-core's permissions for the shared commands only. None of
340
+ them declares `commandPermissions` yet.
341
+ - **Applies to:** every CLI that writes and is not a messenger. **Since:** cli-base 0.64.0. **Rulings:** none.
342
+
313
343
  ## Sources
314
344
 
315
345
  These documents explain the rules on this page in more detail. Where one of them disagrees with this page, this page