@wirecat/cli-template 0.8.0 → 0.10.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.
Files changed (125) hide show
  1. package/.github/workflows/ci.yml +3 -3
  2. package/.github/workflows/release.yml +63 -0
  3. package/.gitleaks.toml +18 -0
  4. package/AGENTS.md +9 -15
  5. package/CHANGELOG.md +87 -1
  6. package/README.md +4 -3
  7. package/bin/release +5 -3
  8. package/contracts/plan.json +2 -2
  9. package/dist/bin/clidev.js +34 -12
  10. package/dist/bundle/all-profiles-XHC7BG3P.js +24 -0
  11. package/dist/bundle/anthropic-N6B3ZZHQ.js +70 -0
  12. package/dist/bundle/bun-HUJLS7KP.js +169 -0
  13. package/dist/bundle/bun-sqlite-OWFO2TVR.js +69 -0
  14. package/dist/bundle/chunk-222FANS2.js +161 -0
  15. package/dist/bundle/chunk-2CFBHYFA.js +884 -0
  16. package/dist/bundle/chunk-2KQZ24GI.js +82 -0
  17. package/dist/bundle/chunk-2TBVT4SZ.js +31 -0
  18. package/dist/bundle/chunk-437W3Z2M.js +76 -0
  19. package/dist/bundle/chunk-4QRLPOJ7.js +18 -0
  20. package/dist/bundle/chunk-54AJONOM.js +54 -0
  21. package/dist/bundle/chunk-5D4AEUVA.js +24783 -0
  22. package/dist/bundle/chunk-6CBWO7OF.js +73 -0
  23. package/dist/bundle/chunk-75LVQQEK.js +35782 -0
  24. package/dist/bundle/chunk-7RB76E2L.js +264 -0
  25. package/dist/bundle/chunk-7RXBN7HP.js +63 -0
  26. package/dist/bundle/chunk-ABP7FDMJ.js +122 -0
  27. package/dist/bundle/chunk-ADLS6FVG.js +24600 -0
  28. package/dist/bundle/chunk-AP7QKYBG.js +3531 -0
  29. package/dist/bundle/chunk-ASMH7JWZ.js +288 -0
  30. package/dist/bundle/chunk-B4EED2RF.js +100 -0
  31. package/dist/bundle/chunk-B4LM2B6F.js +1900 -0
  32. package/dist/bundle/chunk-BDSDQCV4.js +281 -0
  33. package/dist/bundle/chunk-DFDN6AUB.js +21 -0
  34. package/dist/bundle/chunk-DMCN22H6.js +35 -0
  35. package/dist/bundle/chunk-E42W72PQ.js +151 -0
  36. package/dist/bundle/chunk-EFHLQXG6.js +5104 -0
  37. package/dist/bundle/chunk-FIHDGZIB.js +34352 -0
  38. package/dist/bundle/chunk-FWVKHD2L.js +48 -0
  39. package/dist/bundle/chunk-FXS3X76A.js +193 -0
  40. package/dist/bundle/chunk-HTQTIKWP.js +928 -0
  41. package/dist/bundle/chunk-IJ3IA7EK.js +24 -0
  42. package/dist/bundle/chunk-JEBSORH7.js +3379 -0
  43. package/dist/bundle/chunk-JTTW5ZKF.js +45 -0
  44. package/dist/bundle/chunk-LSEF4QC3.js +3075 -0
  45. package/dist/bundle/chunk-LY7GS2YI.js +58 -0
  46. package/dist/bundle/chunk-MFS52LTZ.js +570 -0
  47. package/dist/bundle/chunk-MNSD5BX2.js +78 -0
  48. package/dist/bundle/chunk-MTGLJCAZ.js +24 -0
  49. package/dist/bundle/chunk-MZVV2JZT.js +744 -0
  50. package/dist/bundle/chunk-NE3NSB5O.js +8341 -0
  51. package/dist/bundle/chunk-NTLPDH76.js +440 -0
  52. package/dist/bundle/chunk-O3PBQVAB.js +447 -0
  53. package/dist/bundle/chunk-O5EHUB2V.js +2 -0
  54. package/dist/bundle/chunk-O5LMDQTO.js +82 -0
  55. package/dist/bundle/chunk-OPJ24VGH.js +1028 -0
  56. package/dist/bundle/chunk-OPPMGPFW.js +164 -0
  57. package/dist/bundle/chunk-QGNNMPUB.js +158 -0
  58. package/dist/bundle/chunk-QVJLOUQB.js +2 -0
  59. package/dist/bundle/chunk-S57AMKLX.js +6848 -0
  60. package/dist/bundle/chunk-TARN7CHN.js +215 -0
  61. package/dist/bundle/chunk-TJLGI3N6.js +1095 -0
  62. package/dist/bundle/chunk-VMTM46FH.js +7532 -0
  63. package/dist/bundle/chunk-W2IDKBNB.js +2315 -0
  64. package/dist/bundle/chunk-XFXBXQBS.js +630 -0
  65. package/dist/bundle/chunk-Y5E2WNWB.js +35 -0
  66. package/dist/bundle/chunk-ZVVMGPBJ.js +50 -0
  67. package/dist/bundle/clidev.js +22 -0
  68. package/dist/bundle/command-contract-PEM3S634.js +29 -0
  69. package/dist/bundle/dist-ESHIQU4M.js +7577 -0
  70. package/dist/bundle/echarts-2QGL2JP5.js +42501 -0
  71. package/dist/bundle/embed-HHAAGHA6.js +35 -0
  72. package/dist/bundle/embedder-W2OGVVYH.js +108 -0
  73. package/dist/bundle/first-run-WCJUDUBU.js +23 -0
  74. package/dist/bundle/first-screen-U4DFUKUC.js +27 -0
  75. package/dist/bundle/instructions-OTZHT6NV.js +9 -0
  76. package/dist/bundle/mcp-OQBZXELQ.js +165 -0
  77. package/dist/bundle/mcp-VVF6J7SQ.js +151 -0
  78. package/dist/bundle/mcp-Y7VCIZ2J.js +78 -0
  79. package/dist/bundle/node-P4E63KXA.js +2340 -0
  80. package/dist/bundle/node-SYD4GX5O.js +176 -0
  81. package/dist/bundle/node-TLZSIHIJ.js +21 -0
  82. package/dist/bundle/node-sqlite-X2DGRFOZ.js +45 -0
  83. package/dist/bundle/oauth-42M34GQ5.js +17 -0
  84. package/dist/bundle/open-SWXRCDLN.js +19 -0
  85. package/dist/bundle/process-KAFGXZ7Q.js +91 -0
  86. package/dist/bundle/program-EHR4G7VN.js +16967 -0
  87. package/dist/bundle/prompt-ZQTMHRGX.js +12 -0
  88. package/dist/bundle/recognize-TTED4SE3.js +190 -0
  89. package/dist/bundle/remote-C37XJ4BP.js +26 -0
  90. package/dist/bundle/rerank-APFZAMQL.js +49 -0
  91. package/dist/bundle/serve-DDPIHDVZ.js +19 -0
  92. package/dist/bundle/server-L5GNG7KC.js +793 -0
  93. package/dist/bundle/server-LKVYA2AE.js +39 -0
  94. package/dist/bundle/state-WWPPHERF.js +33 -0
  95. package/dist/bundle/store-H6UIA4PK.js +120 -0
  96. package/dist/bundle/store-maintenance-command-EYBT64A2.js +40 -0
  97. package/dist/commands/mcp.d.ts.map +1 -1
  98. package/dist/commands/mcp.js +45 -30
  99. package/dist/commands/mcp.js.map +1 -1
  100. package/dist/version.d.ts +1 -1
  101. package/dist/version.d.ts.map +1 -1
  102. package/dist/version.js +1 -1
  103. package/dist/version.js.map +1 -1
  104. package/docs/commands.md +38 -22
  105. package/docs/dev/TESTING.md +14 -3
  106. package/docs/dev/reference-clis.md +17 -9
  107. package/docs/dev/skills/release/SKILL.md +9 -6
  108. package/docs/dev/skills/release/fresh-audit.md +4 -0
  109. package/docs/dev/test-matrix.md +10 -11
  110. package/docs/security.md +4 -0
  111. package/docs/usage.md +7 -6
  112. package/kit/dist/new.js +17 -2
  113. package/package.json +7 -5
  114. package/scripts/audit.sh +5 -3
  115. package/scripts/smoke-bundle.ts +63 -0
  116. package/scripts/test-matrix-untested.ts +4 -10
  117. package/scripts/version-level.test.ts +17 -1
  118. package/scripts/version-level.ts +35 -7
  119. package/skills/clidev-cli/SKILL.md +8 -0
  120. package/src/commands/mcp.ts +54 -33
  121. package/src/mcp-http.test.ts +20 -13
  122. package/src/mcp.test.ts +3 -3
  123. package/src/program.test.ts +8 -2
  124. package/src/roots.test.ts +2 -7
  125. package/src/version.ts +1 -1
@@ -1,13 +1,13 @@
1
1
  ---
2
2
  name: release
3
- description: Prepare a release of this CLI for the owner to publish — run release:check, write the changelog under the handbook's headings, check the docs against what changed, raise the version in a pull request; the owner runs bin/release. Use when asked to release, prepare a release, cut a version, «выпуск», «выпустить версию».
3
+ description: Release this CLI — run release:check, write the changelog under the handbook's headings, check the docs against what changed, raise the version in a pull request, merge it and publish with bin/release from GitHub Actions. Use when asked to release, prepare a release, cut a version, «выпуск», «выпустить версию».
4
4
  ---
5
5
 
6
6
  # Release
7
7
 
8
8
  The rules are the handbook's [Releases page](../../../../node_modules/@wirecat/cli-base/docs/standards/releases.md)
9
- (`WC-REL-<n>`), which wins over this skill. An agent prepares the release; the owner merges the release pull request
10
- and runs `bin/release`.
9
+ (`WC-REL-<n>`), which wins over this skill. An agent prepares the release, merges the release pull request and runs
10
+ `bin/release`, which publishes from GitHub Actions. Only the first release is the owner's, from this computer.
11
11
 
12
12
  1. **What changed.** In a worktree off `origin/main`: `git log --oneline "$(git describe --tags --abbrev=0 origin/main)..origin/main"`. The
13
13
  `docs/commands.md` diff shows what a cli-core or cli-base release changed in the commands.
@@ -24,10 +24,13 @@ and runs `bin/release`.
24
24
  before `1.0.0` this narrows `WC-REL-7`: a section with `Changed — may break callers` or `Removed` raises the minor
25
25
  (`0.1.8` to `0.2.0`), and so does one with `Added`; only a section of `Fixed`, `Security` or docs raises the patch.
26
26
  From `1.0.0`, a breaking change raises the major. `pnpm version:check` refuses a dated section whose number is
27
- raised less than its headings need. Set the next version in `package.json`, run `pnpm version:sync`, date the
27
+ raised less than its headings need, and when `bin/release` moves to the next free number on npm it raises that
28
+ number to the level the headings need. Set the next version in `package.json`, run `pnpm version:sync`, date the
28
29
  section as `## <version> — DD.MM.YYYY`, run `pnpm release:check` again, and open the pull request
29
30
  `chore(release): <version>`.
30
- 7. **The owner** merges it and runs `bin/release` on a clean `main`. `bin/release --local` publishes from this
31
- computer when GitHub Actions cannot, with `NPM_TOKEN`, the npm token in the keyring, or the owner's own npm login.
31
+ 7. **The publish.** Merge it when its checks pass, then run `bin/release` on a clean `main`: it starts
32
+ `release.yml` and follows it (`WC-REL-11`). A messenger CLI also needs the owner's signed release report
33
+ (`WC-REL-10`). Never `bin/release --local`: that is the owner's, for the first release before npm trusts
34
+ `release.yml`, with `NPM_TOKEN`, the npm token in the keyring, or the owner's own npm login.
32
35
  The keyring is read only when `NPM_KEYRING_ACCOUNT` names the account the token is stored under (service `npm`);
33
36
  there is no default account. When that lookup fails, `bin/release` stops; it does not fall back to the npm login.
@@ -9,6 +9,10 @@ You audit a WireCat CLI repository, in the current folder. Use only three source
9
9
  `node_modules/@wirecat/cli-base/docs/standards/README.md`, the repository itself, and the cli-base workflow files its
10
10
  own workflows pin (WC-CI-8). Do not read plans, journals or earlier reviews. Run `pnpm install` first if `node_modules` is missing.
11
11
 
12
+ Never use the owner's accounts. Do not sign in, and run the built CLI only with every config, state and cache folder
13
+ moved to a temporary folder and `DBUS_SESSION_BUS_ADDRESS=disabled:`, so it cannot reach the system keyring (removing
14
+ the variable is not enough). Run no command that writes to a service: no `--yes`, no `api` call that changes data.
15
+
12
16
  Check the repository against every handbook page that applies to it: the code, the docs, the configs and the
13
17
  workflows. You may run the package scripts. Change nothing.
14
18
 
@@ -7,7 +7,7 @@ ran it · ⛔ not tested offline, with the reason and where it is checked instea
7
7
  Measured from the test run (`coverage/argv.jsonl`), not searched for — see
8
8
  [TESTING.md](TESTING.md) for how, and for the states and failures that cut across commands.
9
9
 
10
- **50 ✅ · 7 ⛔ · 0 ❌** — 18 commands, 39 options.
10
+ **50 ✅ · 6 ⛔ · 0 ❌** — 19 commands, 37 options.
11
11
 
12
12
  | Command | Option | | Note |
13
13
  |---|---|---|---|
@@ -19,16 +19,15 @@ Measured from the test run (`coverage/argv.jsonl`), not searched for — see
19
19
  | `skill install` | | ✅ | |
20
20
  | `skill install` | `--for` | ✅ | |
21
21
  | `skill status` | | ✅ | |
22
- | `mcp` | | ✅ | |
23
- | `mcp` | `--http` | ✅ | |
24
- | `mcp` | `--port` | ✅ | |
25
- | `mcp` | `--public-url` | ✅ | |
26
- | `mcp` | `--revoke` | ✅ | |
27
- | `mcp server start` | | ⛔ | starts a detached `mcp --http` from the built entry; cli-core tests mcpServerCommand, and the background run was checked by hand against dist/ |
28
- | `mcp server start` | `--port` | ⛔ | starts a detached `mcp --http` from the built entry; cli-core tests mcpServerCommand, and the background run was checked by hand against dist/ |
29
- | `mcp server start` | `--public-url` | ⛔ | starts a detached `mcp --http` from the built entry; cli-core tests mcpServerCommand, and the background run was checked by hand against dist/ |
30
- | `mcp server start` | `--permission` | ⛔ | cli-core adds it and passes it to `mcp --http`, which no longer takes it, so the started server refuses it; checked by hand against dist/ |
31
- | `mcp server stop` | | ⛔ | starts a detached `mcp --http` from the built entry; cli-core tests mcpServerCommand, and the background run was checked by hand against dist/ |
22
+ | `mcp serve` | | ✅ | |
23
+ | `mcp serve` | `--http` | ✅ | |
24
+ | `mcp serve` | `--port` | ✅ | |
25
+ | `mcp serve` | `--public-url` | ✅ | |
26
+ | `mcp revoke` | | ✅ | |
27
+ | `mcp server start` | | ⛔ | starts a detached `mcp serve --http` from the built entry; cli-core tests mcpServerCommand, and the background run was checked by hand against dist/ |
28
+ | `mcp server start` | `--port` | ⛔ | starts a detached `mcp serve --http` from the built entry; cli-core tests mcpServerCommand, and the background run was checked by hand against dist/ |
29
+ | `mcp server start` | `--public-url` | ⛔ | starts a detached `mcp serve --http` from the built entry; cli-core tests mcpServerCommand, and the background run was checked by hand against dist/ |
30
+ | `mcp server stop` | | ⛔ | starts a detached `mcp serve --http` from the built entry; cli-core tests mcpServerCommand, and the background run was checked by hand against dist/ |
32
31
  | `mcp server status` | | ✅ | |
33
32
  | `upgrade` | | ✅ | |
34
33
  | `upgrade` | `--check` | ✅ | |
package/docs/security.md CHANGED
@@ -6,6 +6,10 @@ What WireCat CLIs share — the keyring, permissions, MCP and what never reaches
6
6
  `clidev` keeps its settings in `config.json` and its state in the folders `clidev doctor` shows. It sends nothing
7
7
  to any service yet.
8
8
 
9
+ `clidev personal mcp serve --http` listens on `127.0.0.1` only, for browser apps that reach it through your own
10
+ tunnel. Each app logs in with a one-time code, and the server keeps a hash of each login it gives, in the state
11
+ folder, readable by you only. `clidev personal mcp revoke` forgets every one of them.
12
+
9
13
  A run record keeps the command line as you typed it, so `clidev runs show` tells you what was run. A secret is still
10
14
  stored as `[redacted]`: an option named like a token or password, a password in a link, a long opaque string. A
11
15
  phone number is replaced by `[redacted]` inside the word you typed, message text included: one written with its "+" and
package/docs/usage.md CHANGED
@@ -15,24 +15,25 @@ command without one stops with exit code 2 and lists the profiles on this comput
15
15
 
16
16
  ## Output for scripts
17
17
 
18
- Every command except `mcp` on stdin and stdout takes `--json`, and prints one JSON value on stdout. Errors are JSON on stderr, with a code to branch
18
+ Every command except `mcp serve` on stdin and stdout takes `--json`, and prints one JSON value on stdout. Errors are JSON on stderr, with a code to branch
19
19
  on. The [command reference](commands.md) lists the codes.
20
20
 
21
21
  ## Agents
22
22
 
23
23
  `clidev skill install` puts the tool's skill where Claude Code, Codex, Gemini CLI and OpenClaw look for it.
24
24
  `clidev skill install --for hermes` also adds that folder to Hermes Agent's own settings, so a bare `skill install`
25
- leaves Hermes out. `clidev personal mcp` serves the profile's commands over MCP, on stdin and stdout:
25
+ leaves Hermes out. `clidev personal mcp serve` serves the profile's commands over MCP, on stdin and stdout:
26
26
 
27
27
  ```sh
28
- claude mcp add clidev -- clidev personal mcp
28
+ claude mcp add clidev -- clidev personal mcp serve
29
29
  ```
30
30
 
31
31
  The server offers every command, the ones that write included. The profile's permissions decide what runs; see
32
32
  [settings](configuration.md#permissions).
33
33
 
34
- For ChatGPT and Claude in the browser, `clidev personal mcp --http --public-url https://laptop.example.ts.net` serves on
35
- `127.0.0.1` (port 8765, or `--port`) behind your own tunnel. A browser app logs in with a one-time code that the
36
- command prints; `clidev personal mcp --revoke` ends every login. `clidev personal mcp server start` runs the same
34
+ For ChatGPT and Claude in the browser,
35
+ `clidev personal mcp serve --http --public-url https://laptop.example.ts.net` serves on `127.0.0.1` (port 8765, or
36
+ `--port`) behind your own tunnel. A browser app logs in with a one-time code that the command prints;
37
+ `clidev personal mcp revoke` ends every login. `clidev personal mcp server start` runs the same
37
38
  server in the background, `mcp server status` shows its port, the login code and the apps logged in, and
38
39
  `mcp server stop` ends it.
package/kit/dist/new.js CHANGED
@@ -5,11 +5,12 @@ import { createInterface } from "node:readline/promises";
5
5
  import { promisify } from "node:util";
6
6
  import { ask, choiceList, INTERVIEW, labelOf, NAME_QUESTIONS, parseAnswer } from "./interview.js";
7
7
  import { isRecord, MANIFEST, sha256 } from "./manifest.js";
8
- import { PACKED_AS, renderTemplate } from "./source.js";
8
+ import { renderTemplate } from "./source.js";
9
9
  const run = promisify(execFile);
10
10
  const REFERENCES = {
11
11
  messenger: ["whatsapp-cli", "tg-cli", "max-cli"],
12
12
  files: ["drive-cli"],
13
+ calendar: ["gcal-cli"],
13
14
  meetings: ["zoom-cli"],
14
15
  notes: ["cli-memo"],
15
16
  };
@@ -107,6 +108,18 @@ export const designDoc = (names, answers) => {
107
108
  `Written by \`npx @wirecat/cli-template new\`. The agent reads it first, asks the further questions the reference CLI`,
108
109
  "does not answer, and starts building only after the owner has read it.",
109
110
  "",
111
+ "## First task",
112
+ "",
113
+ "Delete this section once every step is done (WC-DOC-3).",
114
+ "",
115
+ "1. If an answer below is missing, ask the owner the [design interview](skills/design-interview/SKILL.md).",
116
+ "2. Learn from the reference CLI below: its provider, its store and its commands.",
117
+ "3. Ask the further questions in the interview that the reference CLI does not answer, and write the answers here.",
118
+ " Build only after the owner has read this page.",
119
+ "4. When the first command reaches the service, rewrite `README.md`, `docs/index.md` and the description in",
120
+ ` \`skills/${names.name}/SKILL.md\` to say what the tool does and who it is for. Rewrite the MCP server's`,
121
+ " instructions in `src/mcp.ts` too: name the kinds of outside text the CLI carries (WC-MCP-11).",
122
+ "",
110
123
  "## Answers",
111
124
  "",
112
125
  ...rows,
@@ -137,6 +150,7 @@ const removeKit = (command) => command
137
150
  .split(" && ")
138
151
  .filter((part) => !/\bkit[:/]/.test(part))
139
152
  .join(" && ");
153
+ const CLI_FILES = ["dist", "skills", "/README.md", "CHANGELOG.md", "LICENSE"];
140
154
  export const cliPackageJson = (text, names) => {
141
155
  const pkg = JSON.parse(text);
142
156
  if (!isRecord(pkg))
@@ -149,8 +163,9 @@ export const cliPackageJson = (text, names) => {
149
163
  ...Object.fromEntries(Object.entries(bin).filter(([, value]) => !isKit(value))),
150
164
  [names.command]: `dist/bin/${names.command}.js`,
151
165
  };
166
+ // The template's own list ships the skeleton's sources for `new` to copy; a CLI ships what runs (WC-REL-16).
152
167
  if (Array.isArray(pkg.files))
153
- next.files = pkg.files.filter((entry) => !isKit(entry) && !Object.values(PACKED_AS).includes(entry));
168
+ next.files = CLI_FILES;
154
169
  if (isRecord(pkg.scripts)) {
155
170
  next.scripts = Object.fromEntries(Object.entries(pkg.scripts)
156
171
  .filter((entry) => typeof entry[1] === "string" && !entry[0].startsWith("kit:"))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wirecat/cli-template",
3
- "version": "0.8.0",
3
+ "version": "0.10.0",
4
4
  "description": "Start a new WireCat CLI: a working skeleton on cli-base and cli-core, copied by new and kept current by upgrade",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Viacheslav Ptsarev",
@@ -50,7 +50,7 @@
50
50
  "gitignore"
51
51
  ],
52
52
  "scripts": {
53
- "build": "tsc --build --force && pnpm kit:build",
53
+ "build": "tsc --build --force && cli-dev bundle --bin dist/bin/clidev.js && pnpm kit:build",
54
54
  "typecheck": "tsc --build --force --emitDeclarationOnly && tsc -p tsconfig.test.json && tsc -p tsconfig.scripts.json && pnpm kit:typecheck",
55
55
  "lint": "biome check .",
56
56
  "lint:fix": "biome check --write .",
@@ -61,7 +61,8 @@
61
61
  "generate": "pnpm build && node --experimental-strip-types scripts/commands.ts",
62
62
  "generate:check": "pnpm build && node --experimental-strip-types scripts/commands.ts --check",
63
63
  "smoke": "node --experimental-strip-types scripts/smoke.ts",
64
- "smoke:bun": "bun scripts/smoke.ts",
64
+ "smoke:bun": "bun scripts/smoke.ts && bun scripts/smoke-bundle.ts",
65
+ "smoke:node": "node --experimental-strip-types scripts/smoke-bundle.ts",
65
66
  "package:check": "pnpm smoke",
66
67
  "docs:check": "cli-dev docs-check --rules scripts/docs-rules.ts --pages && pnpm docs:spell && rumdl check --config config/wirecat/rumdl.toml README.md SECURITY.md CHANGELOG.md CLAUDE.md AGENTS.md docs/*.md docs/dev/*.md && pnpm docs:lint && pnpm kit:docs",
67
68
  "docs:lint": "markdownlint-cli2",
@@ -84,8 +85,8 @@
84
85
  },
85
86
  "dependencies": {
86
87
  "@modelcontextprotocol/server": "2.4.0",
87
- "@wirecat/cli-base": "0.62.0",
88
- "@wirecat/cli-core": "0.449.0",
88
+ "@wirecat/cli-base": "0.65.0",
89
+ "@wirecat/cli-core": "0.469.0",
89
90
  "commander": "15.0.0"
90
91
  },
91
92
  "devDependencies": {
@@ -98,6 +99,7 @@
98
99
  "@vitest/coverage-v8": "5.0.5",
99
100
  "@wirecat/cli-testing": "0.5.3",
100
101
  "cspell": "10.4.0",
102
+ "esbuild": "0.28.2",
101
103
  "lefthook": "2.2.2",
102
104
  "markdownlint-cli2": "0.24.0",
103
105
  "rumdl": "0.3.0",
package/scripts/audit.sh CHANGED
@@ -11,14 +11,16 @@ trap 'rm -rf "$scratch"' EXIT
11
11
  export HOME="$scratch/home" MESSAGING_STORE="$scratch/store" WIRECAT_STATE_DIR="$scratch/state"
12
12
  mkdir -p "$HOME"
13
13
  npm install -g --prefix "$scratch/prefix" --ignore-scripts --no-audit --no-fund --loglevel error \
14
- @wirecat/tg-cli @wirecat/max-cli @wirecat/whatsapp-cli @wirecat/cli-memo @wirecat/drive-cli @wirecat/zoom-cli
14
+ @wirecat/tg-cli @wirecat/max-cli @wirecat/whatsapp-cli @wirecat/cli-memo @wirecat/drive-cli @wirecat/zoom-cli \
15
+ @wirecat/gcal-cli
15
16
  bin="$scratch/prefix/bin"
16
17
  others=()
17
18
  # A groups file replaces cli-base's PARITY_GROUPS. The template keeps one because it is in no group of cli-base's.
18
19
  [ -f "$root/config/parity-groups.json" ] && others+=(--groups "$root/config/parity-groups.json")
19
- for cli in tg max wa memo drive zm; do
20
+ for cli in tg max wa memo drive zm gcal; do
20
21
  [ "$cli" = "$command" ] || others+=(--cli "$bin/$cli")
21
22
  done
22
23
  "$root/node_modules/.bin/cli-dev" audit --cli "$root/dist/bin/$command.js" "${others[@]}" --for "$command" \
23
24
  --lint "$command=$root/lint" >"$scratch/scorecard.txt" || { cat "$scratch/scorecard.txt"; exit 1; }
24
- tail -n 1 "$scratch/scorecard.txt"
25
+ awk -v cli="$command" 'NR == 1 || $1 == cli' "$scratch/scorecard.txt"
26
+ grep -q "^$command *total " "$scratch/scorecard.txt" || { echo "audit: no scorecard row for $command" >&2; exit 1; }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * The published bin is a loader for `dist/bundle/` (cli-base's `cli-dev bundle`, WC-LAY-10). This runs it, with
3
+ * directories of its own, under Node (`pnpm smoke:node`) and Bun (`pnpm smoke:bun`): the vitest suite runs against
4
+ * `src/`, so this is the proof the bundle a user installs works.
5
+ *
6
+ * pnpm build && pnpm smoke:node
7
+ */
8
+ import { spawnSync } from "node:child_process"
9
+ import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs"
10
+ import { tmpdir } from "node:os"
11
+ import { join } from "node:path"
12
+
13
+ const COMMAND = "clidev"
14
+ const PREFIX = "CLIDEV"
15
+
16
+ const runtime = typeof (globalThis as { Bun?: unknown }).Bun === "undefined" ? "node" : "bun"
17
+ const failures: string[] = []
18
+ const check = (what: string, condition: boolean) => {
19
+ if (!condition) failures.push(what)
20
+ }
21
+
22
+ // Overridden directories also move the keyring entry, so nothing here reads the owner's accounts or store.
23
+ const isolated = mkdtempSync(join(tmpdir(), `${COMMAND}-bundle-smoke-`))
24
+ const run = (args: string[], env: Record<string, string> = {}) =>
25
+ spawnSync(process.execPath, [`dist/bin/${COMMAND}.js`, ...args], {
26
+ encoding: "utf8",
27
+ env: {
28
+ ...process.env,
29
+ [`${PREFIX}_CONFIG_DIR`]: join(isolated, "config"),
30
+ [`${PREFIX}_STATE_DIR`]: join(isolated, "state"),
31
+ [`${PREFIX}_CACHE_DIR`]: join(isolated, "cache"),
32
+ [`${PREFIX}_NO_UPDATE_CHECK`]: "1",
33
+ MESSAGING_STORE: join(isolated, "store.db"),
34
+ ...env,
35
+ },
36
+ })
37
+
38
+ const loader = readFileSync(`dist/bin/${COMMAND}.js`, "utf8")
39
+ check("the bin starts the bundle", loader.includes(`import("../bundle/${COMMAND}.js")`))
40
+ const preloaded = [...loader.matchAll(/"\.\.\/bundle\/([^"]+)"/g)].map(([, chunk]) => chunk ?? "")
41
+ check(
42
+ "a server preloads only chunks the bundle has",
43
+ preloaded.length > 1 && preloaded.every((chunk) => existsSync(join("dist/bundle", chunk))),
44
+ )
45
+
46
+ const version = run(["--version"])
47
+ check("--version prints a version and nothing else", /^\d+\.\d+\.\d+\n$/.test(version.stdout) && version.stderr === "")
48
+ const recorded = run(["--version"], { [`${PREFIX}_TEST_ARGV_LOG`]: join(isolated, "argv.jsonl") })
49
+ check("the loader's --version is the bundle's", recorded.stdout === version.stdout)
50
+ check("--help names the command", run(["--help"]).stdout.includes(`Usage: ${COMMAND}`))
51
+ check("skill show prints the SKILL.md the package ships", run(["skill", "show"]).stdout.startsWith("---"))
52
+ check(
53
+ "doctor finds the document readers",
54
+ run(["doctor", "--json"]).stdout.includes('"files":{"checked":true,"ok":true'),
55
+ )
56
+ rmSync(isolated, { recursive: true, force: true })
57
+
58
+ if (failures.length > 0) {
59
+ console.error(`${COMMAND} bundle smoke FAILED under ${runtime}:`)
60
+ for (const failure of failures) console.error(` - ${failure}`)
61
+ process.exit(1)
62
+ }
63
+ console.log(`${COMMAND} bundle smoke passed under ${runtime}`)
@@ -12,29 +12,23 @@ export const UNTESTED: { command: string; option?: string; reason: string }[] =
12
12
  {
13
13
  command: "mcp server start",
14
14
  reason:
15
- "starts a detached `mcp --http` from the built entry; cli-core tests mcpServerCommand, and the background run was checked by hand against dist/",
15
+ "starts a detached `mcp serve --http` from the built entry; cli-core tests mcpServerCommand, and the background run was checked by hand against dist/",
16
16
  },
17
17
  {
18
18
  command: "mcp server start",
19
19
  option: "--port",
20
20
  reason:
21
- "starts a detached `mcp --http` from the built entry; cli-core tests mcpServerCommand, and the background run was checked by hand against dist/",
21
+ "starts a detached `mcp serve --http` from the built entry; cli-core tests mcpServerCommand, and the background run was checked by hand against dist/",
22
22
  },
23
23
  {
24
24
  command: "mcp server start",
25
25
  option: "--public-url",
26
26
  reason:
27
- "starts a detached `mcp --http` from the built entry; cli-core tests mcpServerCommand, and the background run was checked by hand against dist/",
28
- },
29
- {
30
- command: "mcp server start",
31
- option: "--permission",
32
- reason:
33
- "cli-core adds it and passes it to `mcp --http`, which no longer takes it, so the started server refuses it; checked by hand against dist/",
27
+ "starts a detached `mcp serve --http` from the built entry; cli-core tests mcpServerCommand, and the background run was checked by hand against dist/",
34
28
  },
35
29
  {
36
30
  command: "mcp server stop",
37
31
  reason:
38
- "starts a detached `mcp --http` from the built entry; cli-core tests mcpServerCommand, and the background run was checked by hand against dist/",
32
+ "starts a detached `mcp serve --http` from the built entry; cli-core tests mcpServerCommand, and the background run was checked by hand against dist/",
39
33
  },
40
34
  ]
@@ -1,5 +1,5 @@
1
1
  import { expect, it } from "vitest"
2
- import { versionLevelProblem } from "./version-level.js"
2
+ import { nextVersion, versionLevelProblem } from "./version-level.js"
3
3
 
4
4
  const changelog = (version: string, headings: string[], previous = "0.1.8") =>
5
5
  [`## ${version} — 26.10.2026`, ...headings.flatMap((h) => [`### ${h}`, "- x"]), `## ${previous} — 25.10.2026`].join(
@@ -28,3 +28,19 @@ it("refuses a patch release that adds something", () => {
28
28
  it("checks nothing while the top section is Unreleased", () => {
29
29
  expect(versionLevelProblem("## Unreleased\n\n### Removed\n\n- x\n\n## 0.1.8 — 25.10.2026\n")).toBeUndefined()
30
30
  })
31
+
32
+ const unreleased = (headings: string[], previous = "0.1.1") =>
33
+ ["## Unreleased", ...headings.flatMap((h) => [`### ${h}`, "- x"]), `## ${previous} — 29.10.2026`].join("\n")
34
+
35
+ it.each([
36
+ [["Fixed"], "0.1.2", "0.1.2"],
37
+ [["Added"], "0.1.2", "0.2.0"],
38
+ [["Removed"], "0.1.2", "0.2.0"],
39
+ [["Added"], "0.3.1", "0.3.1"],
40
+ ])("raises the next free number for %j from %s to %s", (headings, next, expected) => {
41
+ expect(nextVersion(unreleased(headings), next)).toBe(expected)
42
+ })
43
+
44
+ it("raises a 1.x release that may break callers to the next major", () => {
45
+ expect(nextVersion(unreleased(["Changed — may break callers"], "1.4.2"), "1.4.3")).toBe("2.0.0")
46
+ })
@@ -21,13 +21,35 @@ const needed = (version: string, section: string): Level => {
21
21
  return /^### Added$/m.test(section) ? "minor" : "patch"
22
22
  }
23
23
 
24
+ const raise = (from: string, level: Level) => {
25
+ const [a = 0, b = 0, c = 0] = parse(from)
26
+ if (level === "major") return `${a + 1}.0.0`
27
+ return level === "minor" ? `${a}.${b + 1}.0` : `${a}.${b}.${c + 1}`
28
+ }
29
+
30
+ const later = (x: string, y: string) => {
31
+ const [a, b] = [parse(x), parse(y)]
32
+ const i = a.findIndex((n, k) => n !== b[k])
33
+ return i === -1 || (a[i] ?? 0) > (b[i] ?? 0) ? x : y
34
+ }
35
+
36
+ const sections = (changelog: string) => {
37
+ const [top, below] = [...changelog.matchAll(/^## (\S+).*$/gm)]
38
+ if (!top || !below?.[1]) return undefined
39
+ return { version: top[1], previous: below[1], section: changelog.slice(top.index, below.index) }
40
+ }
41
+
42
+ // The next free number on npm is a patch; the top section may need a minor or major over the version below it.
43
+ export const nextVersion = (changelog: string, next: string): string => {
44
+ const parts = sections(changelog)
45
+ if (!parts) return next
46
+ return later(next, raise(parts.previous, needed(next, parts.section)))
47
+ }
48
+
24
49
  export const versionLevelProblem = (changelog: string): string | undefined => {
25
- const headings = [...changelog.matchAll(/^## (\S+).*$/gm)]
26
- const [top, below] = headings
27
- const version = top?.[1]
28
- const previous = below?.[1]
29
- if (!top || !below || !version || !previous || !/^\d+\.\d+\.\d+$/.test(version)) return undefined
30
- const section = changelog.slice(top.index, below.index)
50
+ const parts = sections(changelog)
51
+ if (!parts?.version || !/^\d+\.\d+\.\d+$/.test(parts.version)) return undefined
52
+ const { version, previous, section } = parts
31
53
  const want = needed(version, section)
32
54
  const got = raised(previous, version)
33
55
  if (LEVELS.indexOf(got) >= LEVELS.indexOf(want)) return undefined
@@ -35,7 +57,13 @@ export const versionLevelProblem = (changelog: string): string | undefined => {
35
57
  }
36
58
 
37
59
  if (process.argv[1] === fileURLToPath(import.meta.url)) {
38
- const problem = versionLevelProblem(readFileSync(resolve(import.meta.dirname, "../CHANGELOG.md"), "utf8"))
60
+ const changelog = readFileSync(resolve(import.meta.dirname, "../CHANGELOG.md"), "utf8")
61
+ const [flag, next] = process.argv.slice(2)
62
+ if (flag === "--next" && next) {
63
+ console.log(nextVersion(changelog, next))
64
+ process.exit(0)
65
+ }
66
+ const problem = versionLevelProblem(changelog)
39
67
  if (problem) {
40
68
  console.error(`${problem} (docs/dev/skills/release/SKILL.md, step 6)`)
41
69
  process.exit(1)
@@ -1,6 +1,14 @@
1
1
  ---
2
2
  name: clidev-cli
3
3
  description: Use clidev, a WireCat CLI started from cli-template, from an agent — find a command, run it with the owner's profile, read JSON answers. Use when a task needs clidev. It does not connect to a service yet.
4
+ metadata:
5
+ openclaw:
6
+ requires:
7
+ bins: [clidev]
8
+ install:
9
+ - kind: node
10
+ package: "@wirecat/clidev-cli"
11
+ bins: [clidev]
4
12
  ---
5
13
 
6
14
  # clidev
@@ -6,6 +6,8 @@ import {
6
6
  type GlobalFlags,
7
7
  httpTokenFile,
8
8
  itemOf,
9
+ LOCAL_READ,
10
+ LOCAL_WRITE,
9
11
  MCP_HTTP_DEFAULT_PORT,
10
12
  mcpServerCommand,
11
13
  outputFor,
@@ -14,7 +16,6 @@ import {
14
16
  renderAnswer,
15
17
  rootOf,
16
18
  } from "@wirecat/cli-core/cli"
17
- import { portOf, publicUrlOf } from "@wirecat/cli-core/mcp"
18
19
  import { Command } from "commander"
19
20
  import { configuration } from "../config.js"
20
21
  import { IDENTITY } from "../identity.js"
@@ -23,9 +24,10 @@ interface McpFlags extends GlobalFlags {
23
24
  http?: boolean
24
25
  port?: string
25
26
  publicUrl?: string
26
- revoke?: boolean
27
27
  }
28
28
 
29
+ const SETUP = `claude mcp add ${IDENTITY.command} -- ${IDENTITY.command} personal mcp serve`
30
+
29
31
  const settingsOf = (command: Command) => {
30
32
  const flags = command.optsWithGlobals<McpFlags>()
31
33
  const env = environmentOf(command).env ?? process.env
@@ -33,16 +35,19 @@ const settingsOf = (command: Command) => {
33
35
  return { flags, env, profile, tokenFile: httpTokenFile(IDENTITY, profile, env) }
34
36
  }
35
37
 
36
- export const mcpCommand = (): Command =>
37
- annotate(new Command("mcp"), {
38
+ const serveCommand = (): Command =>
39
+ annotate(new Command("serve"), {
38
40
  effects: REMOTE_WRITE,
39
41
  lifetime: "persistent",
40
42
  mcp: false,
41
- examples: [`${IDENTITY.command} personal mcp`],
43
+ examples: [
44
+ `${IDENTITY.command} personal mcp serve`,
45
+ `${IDENTITY.command} personal mcp serve --http --public-url https://laptop.example.ts.net`,
46
+ ],
42
47
  })
43
48
  .summary("serve this profile's commands to an AI agent")
44
49
  .description(
45
- `serve this profile's commands to an agent over MCP, on stdin and stdout, writes included; the profile's permissions decide — \`claude mcp add ${IDENTITY.command} -- ${IDENTITY.command} personal mcp\``,
50
+ `serve this profile's commands to an agent over MCP, on stdin and stdout, writes included; the profile's permissions decide — \`${SETUP}\``,
46
51
  )
47
52
  .option(
48
53
  "--http",
@@ -50,39 +55,13 @@ export const mcpCommand = (): Command =>
50
55
  )
51
56
  .option("--port <port>", `the local port for --http (default ${MCP_HTTP_DEFAULT_PORT})`)
52
57
  .option("--public-url <url>", "the tunnel's https address the browser apps use, e.g. https://<name>.ts.net")
53
- .option("--revoke", "forget every login given to a browser app; each must log in again")
54
- .addCommand(
55
- mcpServerCommand({
56
- example: `${IDENTITY.command} personal`,
57
- target: (self) => {
58
- const { env, profile, tokenFile } = settingsOf(self)
59
- return {
60
- invocation: `${IDENTITY.command} ${profile}`,
61
- profile,
62
- envPrefix: IDENTITY.envPrefix,
63
- env,
64
- tokenFile,
65
- argv: [profile, "mcp", "--http"],
66
- version: IDENTITY.version,
67
- }
68
- },
69
- answer: (self, item, lines) => renderAnswer(self, outputFor(self), itemOf(item), () => lines.join("\n")),
70
- }),
71
- )
72
58
  .action(async function (this: Command) {
73
59
  const { flags, env, profile, tokenFile } = settingsOf(this)
74
- if (flags.revoke) {
75
- refuseUnderReadOnly(this, "mcp --revoke")
76
- const { revokeAll } = await import("@wirecat/cli-core/mcp")
77
- revokeAll(tokenFile)
78
- renderAnswer(this, outputFor(this), itemOf({ revoked: true, profile }))
79
- return
80
- }
81
60
  if (!flags.http && (flags.json || flags.jsonl))
82
61
  throw new CliError("validation_error", "MCP over stdio controls its own output; leave out --json and --jsonl")
83
62
  const environment = environmentOf(this)
84
63
  const run = environment.runCommand
85
- if (!run) throw new Error("mcp runs inside the program's run(), which provides runCommand")
64
+ if (!run) throw new Error("mcp serve runs inside the program's run(), which provides runCommand")
86
65
  const program = rootOf(this)
87
66
  const options = {
88
67
  profile,
@@ -95,6 +74,7 @@ export const mcpCommand = (): Command =>
95
74
  // Loaded here, not at the top: every other command would otherwise pay for the MCP SDK.
96
75
  const { serveMcp, serveMcpOverHttp } = await import("../mcp.js")
97
76
  if (!flags.http) return serveMcp(options)
77
+ const { portOf, publicUrlOf } = await import("@wirecat/cli-core/mcp")
98
78
  const { renderer } = outputFor(this)
99
79
  await serveMcpOverHttp(options, {
100
80
  publicUrl: publicUrlOf(flags.publicUrl, {
@@ -107,3 +87,44 @@ export const mcpCommand = (): Command =>
107
87
  note: (line) => renderer.note(line),
108
88
  })
109
89
  })
90
+
91
+ const revokeCommand = (): Command =>
92
+ annotate(new Command("revoke"), {
93
+ effects: LOCAL_WRITE,
94
+ mcp: false,
95
+ examples: [`${IDENTITY.command} personal mcp revoke`],
96
+ })
97
+ .summary("forget every login given to a browser app")
98
+ .description("forget every login `mcp serve --http` gave a browser app; each must log in again")
99
+ .action(async function (this: Command) {
100
+ refuseUnderReadOnly(this, "mcp revoke")
101
+ const { profile, tokenFile } = settingsOf(this)
102
+ const { revokeAll } = await import("@wirecat/cli-core/mcp")
103
+ revokeAll(tokenFile)
104
+ renderAnswer(this, outputFor(this), itemOf({ revoked: true, profile }))
105
+ })
106
+
107
+ export const mcpCommand = (): Command =>
108
+ annotate(new Command("mcp"), { effects: LOCAL_READ, mcp: false })
109
+ .summary("serve this profile's commands to an AI agent")
110
+ .description(`serve this profile's commands to an agent over MCP — \`${SETUP}\``)
111
+ .addCommand(serveCommand())
112
+ .addCommand(revokeCommand())
113
+ .addCommand(
114
+ mcpServerCommand({
115
+ example: `${IDENTITY.command} personal`,
116
+ target: (self) => {
117
+ const { env, profile, tokenFile } = settingsOf(self)
118
+ return {
119
+ invocation: `${IDENTITY.command} ${profile}`,
120
+ profile,
121
+ envPrefix: IDENTITY.envPrefix,
122
+ env,
123
+ tokenFile,
124
+ argv: [profile, "mcp", "serve", "--http"],
125
+ version: IDENTITY.version,
126
+ }
127
+ },
128
+ answer: (self, item, lines) => renderAnswer(self, outputFor(self), itemOf(item), () => lines.join("\n")),
129
+ }),
130
+ )