@wirecat/cli-template 0.1.8 → 0.3.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 +60 -1
- package/README.md +1 -1
- package/SECURITY.md +2 -2
- package/bin/release +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/docs/commands.md +5 -5
- package/docs/configuration.md +6 -0
- package/docs/dev/reference-clis.md +7 -6
- package/docs/dev/skills/design-interview/SKILL.md +3 -1
- package/docs/dev/skills/release/SKILL.md +7 -3
- package/docs/installation.md +1 -1
- package/docs/security.md +4 -0
- package/docs/usage.md +3 -2
- package/kit/dist/rename.js +1 -0
- package/package.json +5 -5
- package/scripts/version-level.test.ts +30 -0
- package/scripts/version-level.ts +43 -0
- package/src/mcp.test.ts +9 -4
- package/src/program.test.ts +28 -8
- package/src/version.ts +1 -1
- package/vitest.config.ts +5 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,7 +3,66 @@
|
|
|
3
3
|
Notable changes to `@wirecat/cli-template`. One section per version, newest first; versions follow
|
|
4
4
|
[semantic versioning](https://semver.org), so before `1.0.0` the skeleton may still change.
|
|
5
5
|
|
|
6
|
-
<!-- cspell:words katex smol -->
|
|
6
|
+
<!-- cspell:words katex smol whatsapp -->
|
|
7
|
+
|
|
8
|
+
## 0.3.0 — 26.10.2026
|
|
9
|
+
|
|
10
|
+
### Changed — may break callers
|
|
11
|
+
|
|
12
|
+
- **The skeleton runs on cli-core 0.439.0; cli-base stays at 0.59.0.** A run record now keeps the command line as
|
|
13
|
+
typed, so `runs show` tells what was run; a secret is still `[redacted]`. The new setting `record_input`
|
|
14
|
+
(`config set record_input false`) brings back the masking of every typed word. `config set` and `config unset` list
|
|
15
|
+
`record_input`, so a CLI's help snapshot changes once. A test or page of a CLI that says a run never holds what was
|
|
16
|
+
typed needs updating. An argument or option that carries a secret is marked with `secretInput()` from
|
|
17
|
+
`@wirecat/cli-core/cli`; the skeleton has none to mark.
|
|
18
|
+
|
|
19
|
+
### Security
|
|
20
|
+
|
|
21
|
+
- **A first word refused as a profile no longer reaches the run record** as the run's `profile`, and an error that
|
|
22
|
+
quotes `--token=…` or `password=…` no longer keeps the value (from cli-core 0.438.0 and 0.439.0).
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
|
|
26
|
+
- **`docs/dev/reference-clis.md` links the evidence for what it says about other repositories**, no longer calls
|
|
27
|
+
whatsapp-cli the handbook's worked example, points a mail CLI at cli-memo's mail import, and no longer says `config`
|
|
28
|
+
hides messenger settings when `messenger` is `false`.
|
|
29
|
+
- **`pnpm release:check` also runs `security:no-leak` and `package:check`**, which only `bin/release --local` ran.
|
|
30
|
+
`package.json` is copied once, so a CLI adds both to its own `release:check` by hand.
|
|
31
|
+
- **`SECURITY.md` asks never to send a session** beside a token, a password and private content.
|
|
32
|
+
|
|
33
|
+
## 0.2.0 — 26.10.2026
|
|
34
|
+
|
|
35
|
+
### Added
|
|
36
|
+
|
|
37
|
+
- **`skill install --for hermes` gives Hermes Agent the skill, and `skill status` reports the copy Hermes reads.**
|
|
38
|
+
A bare `skill install` still writes only Claude Code's folder and `~/.agents/skills`, which OpenClaw reads too.
|
|
39
|
+
- **`search_reranker` is a new setting in `config set`.** Only a CLI that adds cli-core's message search uses it.
|
|
40
|
+
|
|
41
|
+
### Changed — may break callers
|
|
42
|
+
|
|
43
|
+
- **The skeleton runs on cli-core 0.437.0 and cli-base 0.59.0.** cli-core 0.437 accepts only cli-base 0.59.x, so a
|
|
44
|
+
CLI that pins cli-base moves both. `--help-all` now gives `clidev personal mcp` as the profile example, not
|
|
45
|
+
`clidev personal doctor`, since `doctor` takes no profile; a CLI that keeps help snapshots refreshes them once.
|
|
46
|
+
`skill status --json` gains a `hermes` entry beside `targets`. A run started by OpenClaw has `agent: openclaw`,
|
|
47
|
+
and one started from an OpenClaw chat adds `agent.from_chat: true`: a reader of `run.json` that refuses unknown
|
|
48
|
+
keys must accept it.
|
|
49
|
+
- **`bin/release` runs `changelog restore` from the installed cli-base**, through `pnpm exec cli-dev` like the fold,
|
|
50
|
+
not from `npx -p @wirecat/cli-base@0.55.1` outside the lockfile. A CLI that takes this file with `upgrade` needs
|
|
51
|
+
cli-base 0.55.1 or later, which has `changelog restore`.
|
|
52
|
+
- **`pnpm version:check` refuses a release whose number is raised less than its changelog needs.** Before `1.0.0`, a
|
|
53
|
+
section with `Changed — may break callers`, `Removed` or `Added` raises the minor; only fixes, security and docs
|
|
54
|
+
raise the patch. The release skill says the same. `scripts/` and `package.json` are copied once, so a CLI started
|
|
55
|
+
from an older template copies `scripts/version-level.ts` and the `version:check` script to get the check.
|
|
56
|
+
- **A new CLI starts with coverage floors just under what the skeleton's tests reach**: lines 95, statements 92,
|
|
57
|
+
functions 85, branches 75, and still 50% lines per file. The template's kit has its own floor, which `new` leaves
|
|
58
|
+
out of the CLI's `vitest.config.ts`.
|
|
59
|
+
|
|
60
|
+
### Fixed
|
|
61
|
+
|
|
62
|
+
- **`new` drops a `kit/` key from a config,** not only a `kit/` entry in a list, so the CLI's `vitest.config.ts` has
|
|
63
|
+
no coverage floor for a folder it does not have.
|
|
64
|
+
- **`docs/installation.md` gives the Node.js versions `engines` allows:** 22.16 or a later 22.x, or 24 or later. It
|
|
65
|
+
read as if Node 23 worked.
|
|
7
66
|
|
|
8
67
|
## 0.1.8 — 26.10.2026
|
|
9
68
|
|
package/README.md
CHANGED
|
@@ -32,7 +32,7 @@ such as `personal` in `clidev personal config show`; none is ever assumed. `CLID
|
|
|
32
32
|
| `clidev doctor` | Checks this installation: version, paths, settings and profiles, without the network |
|
|
33
33
|
| `clidev commands --json` | Prints every command as JSON, for agents and scripts |
|
|
34
34
|
| `clidev personal config show` | Shows each setting and where it came from |
|
|
35
|
-
| `clidev skill install` | Installs the agent skill for Claude Code, Codex
|
|
35
|
+
| `clidev skill install` | Installs the agent skill for Claude Code, Codex, Gemini CLI and OpenClaw; `--for hermes` for Hermes Agent |
|
|
36
36
|
| `clidev personal mcp` | Serves the profile's commands to an AI agent over MCP |
|
|
37
37
|
| `clidev runs list` | Lists recorded runs, newest first: every command that failed is kept |
|
|
38
38
|
| `clidev upgrade --check` | Says whether a newer version is on npm |
|
package/SECURITY.md
CHANGED
|
@@ -5,8 +5,8 @@ Please report a security problem privately — never in a public issue:
|
|
|
5
5
|
- by email: [hello@wirecat.dev](mailto:hello@wirecat.dev);
|
|
6
6
|
- or through [GitHub's private vulnerability reporting](https://github.com/WireCatLabs/cli-template/security/advisories/new).
|
|
7
7
|
|
|
8
|
-
Say what you saw, how to repeat it, and the version (`clidev --version`). Never send a token, a password or
|
|
9
|
-
private content.
|
|
8
|
+
Say what you saw, how to repeat it, and the version (`clidev --version`). Never send a token, a password, a session or
|
|
9
|
+
anyone's private content.
|
|
10
10
|
|
|
11
11
|
What `clidev` keeps on your computer: the [shared security page](https://wirecat.dev/en/docs/security) and
|
|
12
12
|
[docs/security.md](docs/security.md).
|
package/bin/release
CHANGED
|
@@ -83,7 +83,7 @@ land() {
|
|
|
83
83
|
|
|
84
84
|
# A pull request checked before the last release and merged after it put its entry in that release's section;
|
|
85
85
|
# restore moves it to Unreleased, so the fold below brings it into this version.
|
|
86
|
-
restored=$(
|
|
86
|
+
restored=$(pnpm exec cli-dev changelog restore --land)
|
|
87
87
|
[ -z "$restored" ] || echo "$restored"
|
|
88
88
|
rerun_if_changed "$@"
|
|
89
89
|
|
package/dist/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const VERSION = "0.
|
|
1
|
+
export declare const VERSION = "0.3.0";
|
|
2
2
|
//# sourceMappingURL=version.d.ts.map
|
package/dist/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export const VERSION = "0.
|
|
1
|
+
export const VERSION = "0.3.0";
|
|
2
2
|
//# sourceMappingURL=version.js.map
|
package/docs/commands.md
CHANGED
|
@@ -90,7 +90,7 @@ clidev skill show [name] [options]
|
|
|
90
90
|
|
|
91
91
|
### `clidev skill install`
|
|
92
92
|
|
|
93
|
-
write SKILL.md to \~/.claude/skills/clidev-cli/ (Claude Code) and \~/.agents/skills/clidev-cli/ (Codex, Gemini CLI)
|
|
93
|
+
write SKILL.md to \~/.claude/skills/clidev-cli/ (Claude Code) and \~/.agents/skills/clidev-cli/ (Codex, Gemini CLI, OpenClaw); --for hermes also has Hermes read \~/.agents/skills, which changes Hermes's own settings, so all leaves it out
|
|
94
94
|
|
|
95
95
|
**Changes something on this computer only.**
|
|
96
96
|
|
|
@@ -100,7 +100,7 @@ clidev skill install [options]
|
|
|
100
100
|
|
|
101
101
|
| Option | What it does |
|
|
102
102
|
|---|---|
|
|
103
|
-
| `--for <agents>` | which agents to install for. One of: `claude`, `agents`, `all`. Default: `all`. |
|
|
103
|
+
| `--for <agents>` | which agents to install for. One of: `claude`, `agents`, `hermes`, `all`. Default: `all`. |
|
|
104
104
|
|
|
105
105
|
**Returns:** install the tool's instructions for AI agents
|
|
106
106
|
|
|
@@ -108,7 +108,7 @@ clidev skill install [options]
|
|
|
108
108
|
|
|
109
109
|
### `clidev skill status`
|
|
110
110
|
|
|
111
|
-
for each place `clidev skill install` writes to: the path, the version installed there, and whether it is current, stale, missing or unversioned
|
|
111
|
+
for each place `clidev skill install` writes to, and for the copy Hermes reads: the path, the version installed there, and whether it is current, stale, missing or unversioned
|
|
112
112
|
|
|
113
113
|
```sh
|
|
114
114
|
clidev skill status
|
|
@@ -182,7 +182,7 @@ clidev config set <setting> <value> [options]
|
|
|
182
182
|
|
|
183
183
|
| Argument | | What it is |
|
|
184
184
|
|---|---|---|
|
|
185
|
-
| `setting` | required | one of: limit, timeout_ms, color, record, keep_runs_for_days, permissions, sends_per_hour, requests_per_minute, transcribe_with, speech_model, search_catch_up, picture_search, embedding_provider, embedding_model, embedding_base_url, embedding_dims, models, read_other_bots, update_check, skill_hint, search_stemmers.cyrillic, search_stemmers.latin, phone_countries, task_categories.<name>, task_agent_close, task_agent_close_min, task_first_pass_per_chat, account_groups.<name>. |
|
|
185
|
+
| `setting` | required | one of: limit, timeout_ms, color, record, record_input, keep_runs_for_days, permissions, sends_per_hour, requests_per_minute, transcribe_with, speech_model, search_catch_up, picture_search, search_reranker, embedding_provider, embedding_model, embedding_base_url, embedding_dims, models, read_other_bots, update_check, skill_hint, search_stemmers.cyrillic, search_stemmers.latin, phone_countries, task_categories.<name>, task_agent_close, task_agent_close_min, task_first_pass_per_chat, account_groups.<name>. |
|
|
186
186
|
| `value` | required | a number, true or false; for a group tg:work,max:Alice,email:me@x. |
|
|
187
187
|
|
|
188
188
|
| Option | What it does |
|
|
@@ -207,7 +207,7 @@ clidev config unset <setting> [options]
|
|
|
207
207
|
|
|
208
208
|
| Argument | | What it is |
|
|
209
209
|
|---|---|---|
|
|
210
|
-
| `setting` | required | one of: limit, timeout_ms, color, record, keep_runs_for_days, permissions, sends_per_hour, requests_per_minute, transcribe_with, speech_model, search_catch_up, picture_search, embedding_provider, embedding_model, embedding_base_url, embedding_dims, models, read_other_bots, update_check, skill_hint, search_stemmers.cyrillic, search_stemmers.latin, phone_countries, task_categories.<name>, task_agent_close, task_agent_close_min, task_first_pass_per_chat, account_groups.<name>. |
|
|
210
|
+
| `setting` | required | one of: limit, timeout_ms, color, record, record_input, keep_runs_for_days, permissions, sends_per_hour, requests_per_minute, transcribe_with, speech_model, search_catch_up, picture_search, search_reranker, embedding_provider, embedding_model, embedding_base_url, embedding_dims, models, read_other_bots, update_check, skill_hint, search_stemmers.cyrillic, search_stemmers.latin, phone_countries, task_categories.<name>, task_agent_close, task_agent_close_min, task_first_pass_per_chat, account_groups.<name>. |
|
|
211
211
|
|
|
212
212
|
| Option | What it does |
|
|
213
213
|
|---|---|
|
package/docs/configuration.md
CHANGED
|
@@ -16,3 +16,9 @@ clidev personal config unset limit
|
|
|
16
16
|
`permissions` decides what a command may do for this profile: `allow`, `ask`, `readonly` or `deny`, per permission
|
|
17
17
|
key. Over MCP there is no one to ask, so `ask` lets the command run. `CLIDEV_READ_ONLY=1` refuses every write, whatever
|
|
18
18
|
the permissions say.
|
|
19
|
+
|
|
20
|
+
## Run records
|
|
21
|
+
|
|
22
|
+
A command that fails is kept as a run, for `clidev runs list` and `runs show`. `record_input` decides whether a run
|
|
23
|
+
keeps the words you typed; it is `true` unless you set it to `false`. Secrets are masked either way; see
|
|
24
|
+
[Security](security.md).
|
|
@@ -5,19 +5,20 @@ the first question of the [design interview](skills/design-interview/SKILL.md):
|
|
|
5
5
|
|
|
6
6
|
| Category | Learn from |
|
|
7
7
|
| --- | --- |
|
|
8
|
-
| messenger | [whatsapp-cli](https://github.com/WireCatLabs/whatsapp-cli),
|
|
8
|
+
| messenger | [whatsapp-cli](https://github.com/WireCatLabs/whatsapp-cli), then [tg-cli](https://github.com/WireCatLabs/tg-cli) and [max-cli](https://github.com/WireCatLabs/max-cli) |
|
|
9
9
|
| files and documents | [drive-cli](https://github.com/WireCatLabs/drive-cli) |
|
|
10
10
|
| meetings | [zoom-cli](https://github.com/WireCatLabs/zoom-cli) |
|
|
11
11
|
| notes | [cli-memo](https://github.com/WireCatLabs/cli-memo) |
|
|
12
|
-
| mail, calendar, other | none yet: pick the nearest one with the owner |
|
|
12
|
+
| mail, calendar, other | none of its own yet: pick the nearest one with the owner. cli-memo [imports mail](https://github.com/WireCatLabs/cli-memo/blob/c2d21254ab360aed7149a3250d17e879283aa603/docs/mail.md) |
|
|
13
13
|
|
|
14
14
|
A messenger CLI sets `messenger: true` in `src/identity.ts`, and `new --category messenger` writes it. Every other
|
|
15
|
-
category keeps the template's `messenger: false`, so the shared help
|
|
16
|
-
|
|
15
|
+
category keeps the template's `messenger: false`, so the shared help and `commands` do not speak of chats, messages
|
|
16
|
+
and senders. `config` still lists cli-core's messenger settings, such as `sends_per_hour` and `read_other_bots`.
|
|
17
17
|
|
|
18
18
|
cli-core keeps a provider registry (WC-ARCH-21). A CLI whose provider is not built into cli-core calls
|
|
19
19
|
`registerProvider(store, { name, kind, aliases, command })` from `@wirecat/cli-core/store` on every run that opens
|
|
20
20
|
the shared store. `kind` is one of `messages`, `mail`, `notes`, `documents`, `meetings` or `events`. Until the CLI
|
|
21
21
|
has run once on a computer, `search` there leaves its accounts out and lists them under `skipped`. The reference CLIs
|
|
22
|
-
do not show this call: their providers
|
|
23
|
-
it registers nothing; the call comes with
|
|
22
|
+
do not show this call: cli-core already knows their providers, from its [seed list](https://github.com/WireCatLabs/cli-core/blob/6d8f415a6ba3124679741f57d3c22ee0f236e8bf/src/domain/providers.ts#L17-L29) and, for Drive, its
|
|
23
|
+
[document providers](https://github.com/WireCatLabs/cli-core/blob/6d8f415a6ba3124679741f57d3c22ee0f236e8bf/src/domain/providers.ts#L50) (cli-core 0.439.0). `grep -rl registerProvider` over a clone of each of the six finds no call. The skeleton opens no store and has no provider, so it registers nothing; the call comes with
|
|
24
|
+
the CLI's first store code.
|
|
@@ -21,7 +21,9 @@ go to the further questions.
|
|
|
21
21
|
5. **How do people filter what they find?** by time · by person · by container (chat, folder, calendar) · by words in
|
|
22
22
|
the text · by status or label · other; several allowed.
|
|
23
23
|
6. **What must never reach the store, a log or the output?** tokens and passwords · one-time codes and passcodes ·
|
|
24
|
-
links that carry a secret · the full content, keep only metadata · other; several allowed.
|
|
24
|
+
links that carry a secret · the full content, keep only metadata · other; several allowed. A run record keeps
|
|
25
|
+
the words typed on the command line: an argument or option that carries such a value is marked with
|
|
26
|
+
`secretInput()` from `@wirecat/cli-core/cli`.
|
|
25
27
|
7. **What untrusted input arrives?** messages from other people · files and attachments · links · names and titles set
|
|
26
28
|
by others · none · other; several allowed.
|
|
27
29
|
|
|
@@ -20,9 +20,13 @@ and runs `bin/release`.
|
|
|
20
20
|
5. **The fresh audit.** Start a new subagent that has none of your context, and give it the prompt in
|
|
21
21
|
[fresh-audit.md](fresh-audit.md) as written. Put its report in the release pull request. Fix each gap in this
|
|
22
22
|
repository there, or list it as a follow-up; take a gap in the handbook to cli-base.
|
|
23
|
-
6. **The version.** `npm view @wirecat/clidev-cli version` is what is published.
|
|
24
|
-
`
|
|
25
|
-
`
|
|
23
|
+
6. **The version.** `npm view @wirecat/clidev-cli version` is what is published. The changelog picks the level, and
|
|
24
|
+
before `1.0.0` this narrows `WC-REL-7`: a section with `Changed — may break callers` or `Removed` raises the minor
|
|
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
|
+
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
|
|
28
|
+
section as `## <version> — DD.MM.YYYY`, run `pnpm release:check` again, and open the pull request
|
|
29
|
+
`chore(release): <version>`.
|
|
26
30
|
7. **The owner** merges it and runs `bin/release` on a clean `main`. `bin/release --local` publishes from this
|
|
27
31
|
computer when GitHub Actions cannot, with `NPM_TOKEN`, the npm token in the keyring, or the owner's own npm login.
|
|
28
32
|
The keyring is read only when `NPM_KEYRING_ACCOUNT` names the account the token is stored under (service `npm`);
|
package/docs/installation.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Install
|
|
2
2
|
|
|
3
|
-
Install `clidev` once on each computer where you or an agent use it. You need Node.js 22.16 or later, or 24.
|
|
3
|
+
Install `clidev` once on each computer where you or an agent use it. You need Node.js 22.16 or a later 22.x, or 24 or later; Node 23 is not supported.
|
|
4
4
|
|
|
5
5
|
```sh
|
|
6
6
|
npm install -g @wirecat/clidev-cli
|
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
|
+
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
|
+
stored as `[redacted]`: an option named like a token or password, a password in a link, a long opaque string. To
|
|
11
|
+
keep none of the words you type, run `clidev personal config set record_input false`.
|
|
12
|
+
|
|
9
13
|
`config.json` is saved readable by you only (`0600`), and its folder is created for you only (`0700`). A file or
|
|
10
14
|
folder made by an older version may be open to other users of the computer: `clidev doctor` lists each under
|
|
11
15
|
`files.problems` and prints the `chmod` that fixes it. Windows is not checked. Report a vulnerability as [SECURITY.md](https://github.com/WireCatLabs/cli-template/blob/main/SECURITY.md)
|
package/docs/usage.md
CHANGED
|
@@ -20,8 +20,9 @@ on. The [command reference](commands.md) lists the codes.
|
|
|
20
20
|
|
|
21
21
|
## Agents
|
|
22
22
|
|
|
23
|
-
`clidev skill install` puts the tool's skill where Claude Code, Codex
|
|
24
|
-
|
|
23
|
+
`clidev skill install` puts the tool's skill where Claude Code, Codex, Gemini CLI and OpenClaw look for it.
|
|
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
26
|
|
|
26
27
|
```sh
|
|
27
28
|
claude mcp add clidev -- clidev personal mcp
|
package/kit/dist/rename.js
CHANGED
|
@@ -30,6 +30,7 @@ export const stripKit = (path, content) => {
|
|
|
30
30
|
const text = content.toString("utf8");
|
|
31
31
|
const stripped = text
|
|
32
32
|
.replace(/^[ \t]*\/\/[^\n]*\n(?=[ \t]*"!?(\.\/)?kit\/)/gm, "")
|
|
33
|
+
.replace(/^[ \t]*"(\.\/)?kit\/[^"]*":[^\n]*\n/gm, "")
|
|
33
34
|
.replace(/\[\s*"!?(\.\/)?kit\/[^"]*"\s*\]/g, "[]")
|
|
34
35
|
.replace(/,\s*"!?(\.\/)?kit\/[^"]*"/g, "")
|
|
35
36
|
.replace(/"!?(\.\/)?kit\/[^"]*",\s*/g, "");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wirecat/cli-template",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.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",
|
|
@@ -67,13 +67,13 @@
|
|
|
67
67
|
"docs:lint": "markdownlint-cli2",
|
|
68
68
|
"contracts:check": "cli-contract contracts/plan.json",
|
|
69
69
|
"security:no-leak": "cli-testing security no-leak . contracts/no-leak.json",
|
|
70
|
-
"version:check": "cli-dev version",
|
|
70
|
+
"version:check": "cli-dev version && node --experimental-strip-types scripts/version-level.ts",
|
|
71
71
|
"version:sync": "cli-dev version --sync",
|
|
72
72
|
"standards:check": "node config/wirecat/check.mjs",
|
|
73
73
|
"lint:commands": "pnpm build && cli-dev lint --cli dist/bin/clidev.js",
|
|
74
74
|
"audit:commands": "pnpm build && bash scripts/audit.sh",
|
|
75
75
|
"ci:quick": "pnpm typecheck && pnpm build && pnpm lint:commands",
|
|
76
|
-
"release:check": "pnpm standards:check && pnpm lint && pnpm typecheck && pnpm test:coverage && pnpm lint:commands && pnpm audit:commands && pnpm contracts:check && pnpm test:matrix:check && pnpm docs:check && pnpm generate:check && pnpm version:check",
|
|
76
|
+
"release:check": "pnpm standards:check && pnpm lint && pnpm typecheck && pnpm test:coverage && pnpm lint:commands && pnpm audit:commands && pnpm contracts:check && pnpm test:matrix:check && pnpm docs:check && pnpm generate:check && pnpm version:check && pnpm security:no-leak && pnpm package:check",
|
|
77
77
|
"prepare": "lefthook install || true",
|
|
78
78
|
"prepack": "pnpm build",
|
|
79
79
|
"docs:spell": "cspell lint --config cspell.json --no-progress --no-summary README.md SECURITY.md CHANGELOG.md 'docs/*.md'",
|
|
@@ -84,8 +84,8 @@
|
|
|
84
84
|
},
|
|
85
85
|
"dependencies": {
|
|
86
86
|
"@modelcontextprotocol/server": "2.4.0",
|
|
87
|
-
"@wirecat/cli-base": "0.
|
|
88
|
-
"@wirecat/cli-core": "0.
|
|
87
|
+
"@wirecat/cli-base": "0.59.0",
|
|
88
|
+
"@wirecat/cli-core": "0.439.0",
|
|
89
89
|
"commander": "15.0.0"
|
|
90
90
|
},
|
|
91
91
|
"devDependencies": {
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { expect, it } from "vitest"
|
|
2
|
+
import { versionLevelProblem } from "./version-level.js"
|
|
3
|
+
|
|
4
|
+
const changelog = (version: string, headings: string[], previous = "0.1.8") =>
|
|
5
|
+
[`## ${version} — 26.10.2026`, ...headings.flatMap((h) => [`### ${h}`, "- x"]), `## ${previous} — 25.10.2026`].join(
|
|
6
|
+
"\n",
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
it("refuses 0.1.8 as it shipped: a patch release with changes that may break callers", () => {
|
|
10
|
+
expect(versionLevelProblem(changelog("0.1.8", ["Changed — may break callers", "Fixed"], "0.1.7"))).toBe(
|
|
11
|
+
"0.1.8 raises the patch version over 0.1.7, but its changelog needs a minor release",
|
|
12
|
+
)
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it.each([
|
|
16
|
+
["0.1.9", ["Fixed", "Security"]],
|
|
17
|
+
["0.2.0", ["Added", "Changed — may break callers"]],
|
|
18
|
+
["0.2.0", ["Removed"]],
|
|
19
|
+
["1.0.0", ["Changed — may break callers"]],
|
|
20
|
+
])("accepts %s with %j", (version, headings) => {
|
|
21
|
+
expect(versionLevelProblem(changelog(version, headings))).toBeUndefined()
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it("refuses a patch release that adds something", () => {
|
|
25
|
+
expect(versionLevelProblem(changelog("0.1.9", ["Added"]))).toMatch(/needs a minor release/)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it("checks nothing while the top section is Unreleased", () => {
|
|
29
|
+
expect(versionLevelProblem("## Unreleased\n\n### Removed\n\n- x\n\n## 0.1.8 — 25.10.2026\n")).toBeUndefined()
|
|
30
|
+
})
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs"
|
|
2
|
+
import { resolve } from "node:path"
|
|
3
|
+
import { fileURLToPath } from "node:url"
|
|
4
|
+
|
|
5
|
+
const LEVELS = ["patch", "minor", "major"] as const
|
|
6
|
+
type Level = (typeof LEVELS)[number]
|
|
7
|
+
|
|
8
|
+
const parse = (version: string) => version.split(".").map(Number)
|
|
9
|
+
|
|
10
|
+
const raised = (from: string, to: string): Level => {
|
|
11
|
+
const [a = 0, b = 0] = parse(from)
|
|
12
|
+
const [x = 0, y = 0] = parse(to)
|
|
13
|
+
if (x !== a) return "major"
|
|
14
|
+
return y !== b ? "minor" : "patch"
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Before 1.0.0 the minor carries what a major would; a fix-only release is the only patch.
|
|
18
|
+
const needed = (version: string, section: string): Level => {
|
|
19
|
+
const breaks = /^### (Changed — may break callers|Removed)$/m.test(section)
|
|
20
|
+
if (breaks) return (parse(version)[0] ?? 0) >= 1 ? "major" : "minor"
|
|
21
|
+
return /^### Added$/m.test(section) ? "minor" : "patch"
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
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)
|
|
31
|
+
const want = needed(version, section)
|
|
32
|
+
const got = raised(previous, version)
|
|
33
|
+
if (LEVELS.indexOf(got) >= LEVELS.indexOf(want)) return undefined
|
|
34
|
+
return `${version} raises the ${got} version over ${previous}, but its changelog needs a ${want} release`
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
38
|
+
const problem = versionLevelProblem(readFileSync(resolve(import.meta.dirname, "../CHANGELOG.md"), "utf8"))
|
|
39
|
+
if (problem) {
|
|
40
|
+
console.error(`${problem} (docs/dev/skills/release/SKILL.md, step 6)`)
|
|
41
|
+
process.exit(1)
|
|
42
|
+
}
|
|
43
|
+
}
|
package/src/mcp.test.ts
CHANGED
|
@@ -29,7 +29,13 @@ const connect = async () => {
|
|
|
29
29
|
return client
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
const
|
|
32
|
+
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
33
|
+
typeof value === "object" && value !== null && !Array.isArray(value)
|
|
34
|
+
const structured = (result: unknown) => {
|
|
35
|
+
if (!isRecord(result) || !isRecord(result.structuredContent))
|
|
36
|
+
throw new Error("the tool answered without structured content")
|
|
37
|
+
return result.structuredContent
|
|
38
|
+
}
|
|
33
39
|
|
|
34
40
|
it("offers search, read and write", async () => {
|
|
35
41
|
const client = await connect()
|
|
@@ -40,9 +46,8 @@ it("offers search, read and write", async () => {
|
|
|
40
46
|
it("finds writes as well as reads", async () => {
|
|
41
47
|
const client = await connect()
|
|
42
48
|
const found = structured(await client.callTool({ name: "clidev_tools_search", arguments: {} }))
|
|
43
|
-
|
|
44
|
-
expect(items.
|
|
45
|
-
expect(items.map((item) => item.command)).not.toContain("mcp")
|
|
49
|
+
expect(found.items).toContainEqual(expect.objectContaining({ writes: true }))
|
|
50
|
+
expect(found.items).not.toContainEqual(expect.objectContaining({ command: "mcp" }))
|
|
46
51
|
})
|
|
47
52
|
|
|
48
53
|
it("runs a command through the CLI and answers with its result", async () => {
|
package/src/program.test.ts
CHANGED
|
@@ -14,13 +14,21 @@ const ran = async (argv: string[]) => {
|
|
|
14
14
|
})
|
|
15
15
|
return { code, out: out.join("\n"), err: err.join("\n") }
|
|
16
16
|
}
|
|
17
|
-
const
|
|
17
|
+
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
18
|
+
typeof value === "object" && value !== null && !Array.isArray(value)
|
|
19
|
+
const json = (text: string) => {
|
|
20
|
+
const parsed: unknown = JSON.parse(text)
|
|
21
|
+
if (!isRecord(parsed) || !isRecord(parsed.item)) throw new Error(`not an answer with one item: ${text}`)
|
|
22
|
+
return { item: parsed.item }
|
|
23
|
+
}
|
|
18
24
|
|
|
19
25
|
it("lists the required root commands and config", async () => {
|
|
20
26
|
const { code, out } = await ran(["commands", "--json"])
|
|
21
27
|
expect(code).toBe(0)
|
|
22
|
-
const
|
|
23
|
-
expect(
|
|
28
|
+
const required = ["commands", "doctor", "skill", "mcp", "upgrade", "config", "runs"]
|
|
29
|
+
expect(JSON.parse(out).commands).toEqual(
|
|
30
|
+
expect.arrayContaining(required.map((name) => expect.objectContaining({ name }))),
|
|
31
|
+
)
|
|
24
32
|
})
|
|
25
33
|
|
|
26
34
|
it("doctor answers without a config file", async () => {
|
|
@@ -123,11 +131,9 @@ it("keeps a failed command as a run, and runs list, show and path read it back",
|
|
|
123
131
|
expect((await ran(["--no-such-flag"])).code).toBe(2)
|
|
124
132
|
const listed = await ran(["runs", "list", "--limit", "5", "--page", "1", "--json"])
|
|
125
133
|
expect(listed.code).toBe(0)
|
|
126
|
-
const failed =
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
expect(failed).toBeDefined()
|
|
130
|
-
const id = failed?.run_id ?? ""
|
|
134
|
+
const failed: unknown = JSON.parse(listed.out).items.find((run: { status: string }) => run.status === "failed")
|
|
135
|
+
if (!isRecord(failed) || typeof failed.run_id !== "string") throw new Error(`no failed run in ${listed.out}`)
|
|
136
|
+
const id = failed.run_id
|
|
131
137
|
const shown = await ran(["runs", "show", id, "--json"])
|
|
132
138
|
expect(shown.code).toBe(0)
|
|
133
139
|
expect(shown.out).toContain(id)
|
|
@@ -136,3 +142,17 @@ it("keeps a failed command as a run, and runs list, show and path read it back",
|
|
|
136
142
|
expect(path.code).toBe(0)
|
|
137
143
|
expect(path.out).toContain(id)
|
|
138
144
|
})
|
|
145
|
+
|
|
146
|
+
it("a failed run keeps the words typed, and record_input false masks them", async () => {
|
|
147
|
+
const keptArgv = async (words: string) => {
|
|
148
|
+
expect((await ran(["personal", "skill", "show", words])).code).toBe(2)
|
|
149
|
+
const { out } = await ran(["runs", "list", "--limit", "50", "--json"])
|
|
150
|
+
return JSON.parse(out).items.map((run: { argv: string[] }) => run.argv)
|
|
151
|
+
}
|
|
152
|
+
expect(await keptArgv("weekly report")).toContainEqual(["personal", "skill", "show", "weekly report"])
|
|
153
|
+
expect((await ran(["personal", "config", "set", "record_input", "false"])).code).toBe(0)
|
|
154
|
+
const masked = await keptArgv("monthly report")
|
|
155
|
+
expect(JSON.stringify(masked)).not.toContain("monthly report")
|
|
156
|
+
expect(masked).toContainEqual(["personal", "skill", "show", "[redacted]"])
|
|
157
|
+
expect((await ran(["personal", "config", "unset", "record_input"])).code).toBe(0)
|
|
158
|
+
})
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = "0.
|
|
1
|
+
export const VERSION = "0.3.0"
|
package/vitest.config.ts
CHANGED
|
@@ -21,7 +21,11 @@ export default defineConfig({
|
|
|
21
21
|
"kit/src/main.ts",
|
|
22
22
|
],
|
|
23
23
|
reporter: ["text-summary", "json-summary"],
|
|
24
|
-
thresholds: {
|
|
24
|
+
thresholds: {
|
|
25
|
+
"src/**": { lines: 95, statements: 92, functions: 85, branches: 75, perFile: { lines: 50 } },
|
|
26
|
+
// The kit is a separate program, copied by npx, so it keeps its own floor.
|
|
27
|
+
"kit/src/**": { lines: 95, statements: 91, functions: 90, branches: 78, perFile: { lines: 50 } },
|
|
28
|
+
},
|
|
25
29
|
},
|
|
26
30
|
},
|
|
27
31
|
})
|