codex-plugin-doctor 0.4.0 → 0.6.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/README.md CHANGED
@@ -1,308 +1,323 @@
1
- # Codex Plugin Doctor
2
-
3
- [![CI](https://github.com/Esquetta/CodexPluginDoctor/actions/workflows/ci.yml/badge.svg)](https://github.com/Esquetta/CodexPluginDoctor/actions/workflows/ci.yml)
4
- [![npm version](https://img.shields.io/npm/v/codex-plugin-doctor.svg)](https://www.npmjs.com/package/codex-plugin-doctor)
5
- [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](./LICENSE)
6
- [![GitHub release](https://img.shields.io/github/v/release/Esquetta/CodexPluginDoctor)](https://github.com/Esquetta/CodexPluginDoctor/releases)
7
-
8
- Codex Plugin Doctor is a local CLI validator for Codex plugin packages, skills, and MCP server bundles.
9
-
10
- It catches packaging, metadata, security, and runtime protocol problems before a plugin reaches users, teammates, or release workflows.
11
-
12
- ## Status
13
-
14
- Codex Plugin Doctor is an early public CLI release.
15
-
16
- - Primary surface: GitHub repository and npm package
17
- - Distribution today: `npm install -g`, local source install, `npm link`, `npm pack`, GitHub Releases
18
- - Public npm package: `codex-plugin-doctor`
19
- - License: [MIT](./LICENSE)
20
-
21
- ## Why This Exists
22
-
23
- Codex plugin packages can fail in several places:
24
-
25
- - the package manifest is missing or points outside the package root
26
- - skills exist but do not expose valid `SKILL.md` metadata
27
- - `.mcp.json` is malformed or references unsafe secrets
28
- - an MCP server starts but does not complete the protocol handshake
29
- - tools, resources, or prompts list successfully but fail deeper runtime checks
30
- - verbose metadata creates noisy matching and unnecessary context cost
31
-
32
- This tool gives plugin authors a repeatable preflight check before distribution.
33
-
34
- ## What It Checks
35
-
36
- Static validation:
37
-
38
- - required `.codex-plugin/plugin.json`
39
- - manifest fields: `name`, `version`, `description`
40
- - skill directory wiring
41
- - `SKILL.md` presence and frontmatter fields
42
- - YAML single-line and block-scalar skill descriptions
43
- - `.mcp.json` structure
44
- - path traversal risks
45
- - hard-coded secret-like env values
46
- - description quality heuristics tuned against real plugin packages
47
-
48
- Runtime MCP validation with `--runtime`:
49
-
50
- - `initialize`
51
- - `notifications/initialized`
52
- - `tools/list`
53
- - `tools/call`
54
- - `resources/list`
55
- - `resources/read`
56
- - `resources/templates/list`
57
- - `prompts/list`
58
- - `prompts/get`
59
- - paginated list responses
60
- - runtime capability scorecard
61
- - redacted verbose transcript with `--verbose-runtime`
62
-
63
- Output formats:
64
-
65
- - human text output
66
- - JSON reports
67
- - Markdown reports
1
+ # Codex Plugin Doctor
2
+
3
+ [![CI](https://github.com/Esquetta/CodexPluginDoctor/actions/workflows/ci.yml/badge.svg)](https://github.com/Esquetta/CodexPluginDoctor/actions/workflows/ci.yml)
4
+ [![npm version](https://img.shields.io/npm/v/codex-plugin-doctor.svg)](https://www.npmjs.com/package/codex-plugin-doctor)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](./LICENSE)
6
+ [![GitHub release](https://img.shields.io/github/v/release/Esquetta/CodexPluginDoctor)](https://github.com/Esquetta/CodexPluginDoctor/releases)
7
+
8
+ Codex Plugin Doctor is a local CLI validator for Codex plugin packages, skills, and MCP server bundles.
9
+
10
+ It catches packaging, metadata, security, and runtime protocol problems before a plugin reaches users, teammates, or release workflows.
11
+
12
+ ## Status
13
+
14
+ Codex Plugin Doctor is an early public CLI release.
15
+
16
+ - Primary surface: GitHub repository and npm package
17
+ - Distribution today: `npm install -g`, local source install, `npm link`, `npm pack`, GitHub Releases
18
+ - Public npm package: `codex-plugin-doctor`
19
+ - License: [MIT](./LICENSE)
20
+
21
+ ## Why This Exists
22
+
23
+ Codex plugin packages can fail in several places:
24
+
25
+ - the package manifest is missing or points outside the package root
26
+ - skills exist but do not expose valid `SKILL.md` metadata
27
+ - `.mcp.json` is malformed or references unsafe secrets
28
+ - an MCP server starts but does not complete the protocol handshake
29
+ - tools, resources, or prompts list successfully but fail deeper runtime checks
30
+ - verbose metadata creates noisy matching and unnecessary context cost
31
+
32
+ This tool gives plugin authors a repeatable preflight check before distribution.
33
+
34
+ ## What It Checks
35
+
36
+ Static validation:
37
+
38
+ - required `.codex-plugin/plugin.json`
39
+ - manifest fields: `name`, `version`, `description`
40
+ - skill directory wiring
41
+ - `SKILL.md` presence and frontmatter fields
42
+ - YAML single-line and block-scalar skill descriptions
43
+ - `.mcp.json` structure
44
+ - path traversal risks
45
+ - hard-coded secret-like env values
46
+ - description quality heuristics tuned against real plugin packages
47
+
48
+ Runtime MCP validation with `--runtime`:
49
+
50
+ - `initialize`
51
+ - `notifications/initialized`
52
+ - `tools/list`
53
+ - `tools/call`
54
+ - `resources/list`
55
+ - `resources/read`
56
+ - `resources/templates/list`
57
+ - `prompts/list`
58
+ - `prompts/get`
59
+ - paginated list responses
60
+ - runtime capability scorecard
61
+ - redacted verbose transcript with `--verbose-runtime`
62
+
63
+ Output formats:
64
+
65
+ - human text output
66
+ - JSON reports
67
+ - Markdown reports
68
+ - Shields-compatible badge JSON and static badge Markdown
69
+ - validation history JSONL and trend summaries
68
70
  - `--output` file writing
69
- - CI summary and artifact generation
70
-
71
- ## Quick Start
72
-
73
- Global install from npm:
74
-
75
- ```bash
76
- npm install -g codex-plugin-doctor
77
- codex-plugin-doctor --version
78
- codex-plugin-doctor self-test
79
- codex-plugin-doctor check path/to/plugin-package
80
- ```
81
-
82
- Run `codex-plugin-doctor check .` from the root of a Codex plugin package that contains `.codex-plugin/plugin.json`. The Codex Plugin Doctor source repository is not itself a plugin package.
83
-
84
- If you already have Codex installed locally and do not know plugin paths, discover the installed plugin cache:
85
-
86
- ```bash
87
- codex-plugin-doctor list --installed
88
- codex-plugin-doctor check --installed
89
- codex-plugin-doctor check --installed --all-summary
90
- codex-plugin-doctor check --installed github
91
- codex-plugin-doctor explain plugin.manifest.missing
92
- ```
93
-
94
- Run from source:
95
-
96
- ```bash
97
- npm install
98
- npm run build
99
- node dist/cli.js check examples/codex-doctor-runtime --runtime
100
- ```
101
-
102
- For local global usage:
103
-
104
- ```bash
105
- npm link
106
- codex-plugin-doctor check examples/codex-doctor-runtime --runtime
107
- ```
108
-
109
- Generate validation artifacts locally:
110
-
111
- ```bash
112
- npm run generate-validation-artifacts -- --target examples/codex-doctor-runtime --runtime-target examples/codex-doctor-runtime --out-dir validation-artifacts-local
113
- ```
114
-
115
- ## Example Output
116
-
117
- Passing runtime package:
118
-
119
- ```text
120
- Codex Plugin Doctor
121
- ===================
122
- Status: PASS
123
- Target: <repo>\examples\codex-doctor-runtime
124
- Summary: 0 fail, 0 warn, 0 total
125
-
126
- Runtime Scorecard
127
- ----------------
128
- initialize: pass
129
- tools/list: pass
130
- tools/call: pass
131
- resources/list: pass
132
- resources/read: pass
133
- resources/templates/list: pass
134
- prompts/list: pass
135
- prompts/get: pass
136
-
137
- No findings.
138
- ```
139
-
140
- Risky package:
141
-
142
- ```text
143
- Codex Plugin Doctor
144
- ===================
145
- Status: FAIL
146
- Target: <repo>\examples\codex-doctor-risky
147
- Summary: 1 fail, 0 warn, 1 total
148
-
149
- Failures
150
- --------
151
- x plugin.security.hard_coded_secret
152
- Message: The MCP server `dangerServer` contains a hard-coded secret-like env value for `OPENAI_API_KEY`.
153
- Impact: Hard-coded credentials inside plugin bundles increase leakage risk and make secure rotation difficult.
154
- Suggested fix: Replace the literal value for `OPENAI_API_KEY` with an environment reference or injected secret outside the package.
155
- ```
156
-
157
- ## Useful Commands
158
-
159
- Run these from a Codex plugin package root:
160
-
161
- ```bash
162
- codex-plugin-doctor --version
163
- codex-plugin-doctor self-test
164
- codex-plugin-doctor init my-plugin
165
- codex-plugin-doctor compat .
166
- codex-plugin-doctor compat . --client codex
167
- codex-plugin-doctor compat . --client generic-mcp
168
- codex-plugin-doctor compat . --client claude-desktop
169
- codex-plugin-doctor compat . --client claude-desktop --install-preview
170
- codex-plugin-doctor compat . --client claude-desktop --apply --backup
171
- codex-plugin-doctor compat . --client cursor
172
- codex-plugin-doctor compat . --client cursor --install-preview
173
- codex-plugin-doctor compat . --client cursor --apply --backup
174
- codex-plugin-doctor compat . --scorecard
175
- codex-plugin-doctor compat . --json
176
- codex-plugin-doctor compat . --json --output compatibility.json
177
- codex-plugin-doctor check .
178
- codex-plugin-doctor check . --json
179
- codex-plugin-doctor check . --json --output report.json
180
- codex-plugin-doctor check . --markdown --output report.md
181
- codex-plugin-doctor check . --sarif --output results.sarif
182
- codex-plugin-doctor check . --ascii
183
- codex-plugin-doctor check . --no-animations
71
+ - CI summary and artifact generation
72
+
73
+ ## Quick Start
74
+
75
+ Global install from npm:
76
+
77
+ ```bash
78
+ npm install -g codex-plugin-doctor
79
+ codex-plugin-doctor --version
80
+ codex-plugin-doctor self-test
81
+ codex-plugin-doctor check path/to/plugin-package
82
+ ```
83
+
84
+ Run `codex-plugin-doctor check .` from the root of a Codex plugin package that contains `.codex-plugin/plugin.json`. The Codex Plugin Doctor source repository is not itself a plugin package.
85
+
86
+ If you already have Codex installed locally and do not know plugin paths, discover the installed plugin cache:
87
+
88
+ ```bash
89
+ codex-plugin-doctor list --installed
90
+ codex-plugin-doctor check --installed
91
+ codex-plugin-doctor check --installed --all-summary
92
+ codex-plugin-doctor check --installed github
93
+ codex-plugin-doctor explain plugin.manifest.missing
94
+ ```
95
+
96
+ Run from source:
97
+
98
+ ```bash
99
+ npm install
100
+ npm run build
101
+ node dist/cli.js check examples/codex-doctor-runtime --runtime
102
+ ```
103
+
104
+ For local global usage:
105
+
106
+ ```bash
107
+ npm link
108
+ codex-plugin-doctor check examples/codex-doctor-runtime --runtime
109
+ ```
110
+
111
+ Generate validation artifacts locally:
112
+
113
+ ```bash
114
+ npm run generate-validation-artifacts -- --target examples/codex-doctor-runtime --runtime-target examples/codex-doctor-runtime --out-dir validation-artifacts-local
115
+ ```
116
+
117
+ ## Example Output
118
+
119
+ Passing runtime package:
120
+
121
+ ```text
122
+ Codex Plugin Doctor
123
+ ===================
124
+ Status: PASS
125
+ Target: <repo>\examples\codex-doctor-runtime
126
+ Summary: 0 fail, 0 warn, 0 total
127
+
128
+ Runtime Scorecard
129
+ ----------------
130
+ initialize: pass
131
+ tools/list: pass
132
+ tools/call: pass
133
+ resources/list: pass
134
+ resources/read: pass
135
+ resources/templates/list: pass
136
+ prompts/list: pass
137
+ prompts/get: pass
138
+
139
+ No findings.
140
+ ```
141
+
142
+ Risky package:
143
+
144
+ ```text
145
+ Codex Plugin Doctor
146
+ ===================
147
+ Status: FAIL
148
+ Target: <repo>\examples\codex-doctor-risky
149
+ Summary: 1 fail, 0 warn, 1 total
150
+
151
+ Failures
152
+ --------
153
+ x plugin.security.hard_coded_secret
154
+ Message: The MCP server `dangerServer` contains a hard-coded secret-like env value for `OPENAI_API_KEY`.
155
+ Impact: Hard-coded credentials inside plugin bundles increase leakage risk and make secure rotation difficult.
156
+ Suggested fix: Replace the literal value for `OPENAI_API_KEY` with an environment reference or injected secret outside the package.
157
+ ```
158
+
159
+ ## Useful Commands
160
+
161
+ Run these from a Codex plugin package root:
162
+
163
+ ```bash
164
+ codex-plugin-doctor --version
165
+ codex-plugin-doctor self-test
166
+ codex-plugin-doctor init my-plugin
167
+ codex-plugin-doctor compat .
168
+ codex-plugin-doctor compat . --client codex
169
+ codex-plugin-doctor compat . --client generic-mcp
170
+ codex-plugin-doctor compat . --client claude-desktop
171
+ codex-plugin-doctor compat . --client claude-desktop --install-preview
172
+ codex-plugin-doctor compat . --client claude-desktop --apply --backup
173
+ codex-plugin-doctor compat . --client cursor
174
+ codex-plugin-doctor compat . --client cursor --install-preview
175
+ codex-plugin-doctor compat . --client cursor --apply --backup
176
+ codex-plugin-doctor compat . --scorecard
177
+ codex-plugin-doctor compat . --json
178
+ codex-plugin-doctor compat . --json --output compatibility.json
179
+ codex-plugin-doctor check .
180
+ codex-plugin-doctor check . --json
181
+ codex-plugin-doctor check . --json --output report.json
182
+ codex-plugin-doctor check . --markdown --output report.md
183
+ codex-plugin-doctor check . --badge-json --output doctor-badge.json
184
+ codex-plugin-doctor check . --badge-markdown
185
+ codex-plugin-doctor check . --sarif --output results.sarif
186
+ codex-plugin-doctor check . --ascii
187
+ codex-plugin-doctor check . --no-animations
184
188
  codex-plugin-doctor check . --runtime
185
189
  codex-plugin-doctor check . --config .codex-doctor.json
190
+ codex-plugin-doctor check . --history validation-history.jsonl
191
+ codex-plugin-doctor history validation-history.jsonl
192
+ codex-plugin-doctor history validation-history.jsonl --json
193
+ codex-plugin-doctor history validation-history.jsonl --fail-on-regression
186
194
  codex-plugin-doctor check . --json --runtime --verbose-runtime
187
195
  ```
188
-
189
- `self-test` runs the bundled runtime-complete sample through static validation, runtime MCP probes, and the compatibility scorecard. It is the fastest post-install check after `npm install -g codex-plugin-doctor`.
190
-
191
- `compat --client claude-desktop` checks whether the MCP package can be added to the local Claude Desktop setup. On Windows it looks for `%APPDATA%\Claude\claude_desktop_config.json`; on macOS it looks for `~/Library/Application Support/Claude/claude_desktop_config.json`. A valid existing config returns `PASS`, a missing Claude Desktop install returns `WARN`, and a malformed local config returns `FAIL` so you do not add new servers into a broken config file. If the package server name already exists in Claude Desktop, the command returns `WARN` with the duplicate server name. Add `--install-preview` to print the JSON snippet that should be merged into `claude_desktop_config.json`; it does not modify files. Use `--apply --backup` only when you want the CLI to create a timestamped backup and merge the server config. Apply mode refuses to overwrite duplicate server names.
192
-
193
- `compat --client cursor` checks whether the MCP package can be added to Cursor. It prefers a project-level `.cursor/mcp.json` when one already exists in the target package, then falls back to the global `~/.cursor/mcp.json` path. A valid existing config returns `PASS`, a missing Cursor config returns `WARN`, malformed JSON returns `FAIL`, and duplicate MCP server names return `WARN`. Add `--install-preview` to print the JSON snippet that should be merged into Cursor's `mcp.json`; it does not modify files. Use `--apply --backup` only when you want the CLI to create a timestamped backup and merge the server config. Apply mode refuses to overwrite duplicate server names.
194
-
195
- `compat --scorecard` turns the compatibility matrix into a compact score summary. `PASS` maps to `100`, `WARN` maps to `70`, and `FAIL` or `SKIPPED` maps to `0`.
196
-
197
- Optional local policy file:
198
-
199
- ```json
200
- {
201
- "ignoreRules": ["plugin.heuristic.description.too_long"],
202
- "failOnWarnings": true
203
- }
204
- ```
205
-
206
- Run these when you want Codex Plugin Doctor to find plugins from the local Codex installation:
207
-
208
- ```bash
209
- codex-plugin-doctor list --installed
210
- codex-plugin-doctor check --installed
211
- codex-plugin-doctor check --installed --all-summary
212
- codex-plugin-doctor check --installed github
213
- codex-plugin-doctor check --installed github --runtime --no-animations
214
- codex-plugin-doctor explain plugin.security.hard_coded_secret
215
- ```
216
-
217
- ## GitHub Action
218
-
219
- ```yaml
220
- name: Validate Codex plugin
221
-
222
- on:
223
- pull_request:
224
-
225
- jobs:
226
- doctor:
227
- runs-on: ubuntu-latest
228
- steps:
229
- - uses: actions/checkout@v4
230
- - uses: Esquetta/CodexPluginDoctor@v0.2.0
196
+
197
+ `self-test` runs the bundled runtime-complete sample through static validation, runtime MCP probes, and the compatibility scorecard. It is the fastest post-install check after `npm install -g codex-plugin-doctor`.
198
+
199
+ `compat --client claude-desktop` checks whether the MCP package can be added to the local Claude Desktop setup. On Windows it looks for `%APPDATA%\Claude\claude_desktop_config.json`; on macOS it looks for `~/Library/Application Support/Claude/claude_desktop_config.json`. A valid existing config returns `PASS`, a missing Claude Desktop install returns `WARN`, and a malformed local config returns `FAIL` so you do not add new servers into a broken config file. If the package server name already exists in Claude Desktop, the command returns `WARN` with the duplicate server name. Add `--install-preview` to print the JSON snippet that should be merged into `claude_desktop_config.json`; it does not modify files. Use `--apply --backup` only when you want the CLI to create a timestamped backup and merge the server config. Apply mode refuses to overwrite duplicate server names.
200
+
201
+ `compat --client cursor` checks whether the MCP package can be added to Cursor. It prefers a project-level `.cursor/mcp.json` when one already exists in the target package, then falls back to the global `~/.cursor/mcp.json` path. A valid existing config returns `PASS`, a missing Cursor config returns `WARN`, malformed JSON returns `FAIL`, and duplicate MCP server names return `WARN`. Add `--install-preview` to print the JSON snippet that should be merged into Cursor's `mcp.json`; it does not modify files. Use `--apply --backup` only when you want the CLI to create a timestamped backup and merge the server config. Apply mode refuses to overwrite duplicate server names.
202
+
203
+ `compat --scorecard` turns the compatibility matrix into a compact score summary. `PASS` maps to `100`, `WARN` maps to `70`, and `FAIL` or `SKIPPED` maps to `0`.
204
+
205
+ `check --badge-json` emits Shields endpoint-compatible JSON such as `{"schemaVersion":1,"label":"doctor","message":"PASS","color":"brightgreen"}`. `check --badge-markdown` emits a static shields.io Markdown badge for README or release notes. Badge output is intentionally limited to single package checks, not `check --installed`.
206
+
207
+ `check --history <path>` appends a compact JSONL validation snapshot after a single package check. `history <path>` reads the JSONL file and compares the latest run to the previous run, including status, finding-count deltas, and whether the latest run regressed. Add `history --json` for automation output or `history --fail-on-regression` when CI should fail after a worse latest run.
208
+
209
+ Optional local policy file:
210
+
211
+ ```json
212
+ {
213
+ "ignoreRules": ["plugin.heuristic.description.too_long"],
214
+ "failOnWarnings": true
215
+ }
216
+ ```
217
+
218
+ Run these when you want Codex Plugin Doctor to find plugins from the local Codex installation:
219
+
220
+ ```bash
221
+ codex-plugin-doctor list --installed
222
+ codex-plugin-doctor check --installed
223
+ codex-plugin-doctor check --installed --all-summary
224
+ codex-plugin-doctor check --installed github
225
+ codex-plugin-doctor check --installed github --runtime --no-animations
226
+ codex-plugin-doctor explain plugin.security.hard_coded_secret
227
+ ```
228
+
229
+ ## GitHub Action
230
+
231
+ ```yaml
232
+ name: Validate Codex plugin
233
+
234
+ on:
235
+ pull_request:
236
+
237
+ jobs:
238
+ doctor:
239
+ runs-on: ubuntu-latest
240
+ steps:
241
+ - uses: actions/checkout@v4
242
+ - uses: Esquetta/CodexPluginDoctor@v0.6.0
231
243
  with:
244
+ version: "0.6.0"
232
245
  path: .
233
246
  runtime: "false"
234
- ```
235
-
236
- To self-test this repository after cloning it:
237
-
238
- ```bash
239
- codex-plugin-doctor check examples/codex-doctor-runtime --runtime --no-animations
240
- ```
241
-
242
- ## Repository Layout
243
-
244
- ```text
245
- docs/ Product, engineering, security, and release docs
246
- examples/ Manual plugin packs for local CLI testing
247
- src/ CLI, validation logic, runtime probing, reports
248
- tests/ Fixture-based regression tests
249
- validation-sessions/ Real-world validation waves and tuning notes
250
- ```
251
-
252
- ## Validation Evidence
253
-
254
- The validator is tuned against local fixtures and real marketplace-style plugin packages. See:
255
-
256
- - [Real-World Validation Workflow](./docs/engineering/real-world-validation-workflow.md)
257
- - [Validation Sessions](./validation-sessions/README.md)
258
- - [Examples](./examples/README.md)
259
- - [Rule Catalog](./docs/rules/catalog.md)
260
-
261
- Recent validation waves covered:
262
-
263
- - curated Codex plugin cache packages
264
- - marketplace-style plugin snapshots
265
- - YAML block-scalar skill metadata
266
- - media and visual workflow metadata
267
-
268
- ## Release Readiness
269
-
270
- Release preparation is reproducible from the repository:
271
-
272
- ```bash
273
- npm run prepare-release
274
- ```
275
-
276
- This runs tests, builds the TypeScript output, and performs `npm pack --dry-run`.
277
-
278
- Related docs:
279
-
280
- - [Changelog](./CHANGELOG.md)
281
- - [Contributing](./CONTRIBUTING.md)
282
- - [Security Policy](./SECURITY.md)
283
- - [Code of Conduct](./CODE_OF_CONDUCT.md)
284
- - [NPM Release Checklist](./docs/engineering/npm-release-checklist.md)
285
- - [Release Candidate Workflow](./docs/engineering/release-candidate-workflow.md)
286
- - [v0.1.0 Release Notes](./docs/engineering/v0.1.0-final-release-notes.md)
287
-
288
- ## Contributing
289
-
290
- Contributions are welcome once the repository is public. Start with:
291
-
292
- - [Contributing](./CONTRIBUTING.md)
293
- - [Security Policy](./SECURITY.md)
294
- - [Validation tuning issue template](./.github/ISSUE_TEMPLATE/validation_tuning.yml)
295
-
296
- ## Support
297
-
298
- If this tool saves you time, GitHub stars and sponsorship help signal that the project is worth continuing.
299
-
300
- - Star the repository on GitHub.
301
- - Use GitHub Sponsors through the repository funding link.
302
- - Open validation tuning issues for false positives or false negatives.
303
-
304
- ## Product Direction
305
-
306
- Codex Plugin Doctor starts as a Codex-specific validator and can grow into a broader MCP Doctor over time.
307
-
308
- The immediate goal is not a marketplace, dashboard, or hosted website. The immediate goal is a trustworthy local preflight check for Codex-compatible plugin bundles.
247
+ ```
248
+
249
+ For runtime probing, SARIF output, installed plugin cache checks, and pinned release examples, see [GitHub Action Usage](./docs/engineering/github-action-usage.md).
250
+
251
+ To self-test this repository after cloning it:
252
+
253
+ ```bash
254
+ codex-plugin-doctor check examples/codex-doctor-runtime --runtime --no-animations
255
+ ```
256
+
257
+ ## Repository Layout
258
+
259
+ ```text
260
+ docs/ Product, engineering, security, and release docs
261
+ examples/ Manual plugin packs for local CLI testing
262
+ src/ CLI, validation logic, runtime probing, reports
263
+ tests/ Fixture-based regression tests
264
+ validation-sessions/ Real-world validation waves and tuning notes
265
+ ```
266
+
267
+ ## Validation Evidence
268
+
269
+ The validator is tuned against local fixtures and real marketplace-style plugin packages. See:
270
+
271
+ - [Real-World Validation Workflow](./docs/engineering/real-world-validation-workflow.md)
272
+ - [Validation Sessions](./validation-sessions/README.md)
273
+ - [Examples](./examples/README.md)
274
+ - [Rule Catalog](./docs/rules/catalog.md)
275
+
276
+ Recent validation waves covered:
277
+
278
+ - curated Codex plugin cache packages
279
+ - marketplace-style plugin snapshots
280
+ - YAML block-scalar skill metadata
281
+ - media and visual workflow metadata
282
+
283
+ ## Release Readiness
284
+
285
+ Release preparation is reproducible from the repository:
286
+
287
+ ```bash
288
+ npm run prepare-release
289
+ ```
290
+
291
+ This runs tests, builds the TypeScript output, and performs `npm pack --dry-run`.
292
+
293
+ Related docs:
294
+
295
+ - [Changelog](./CHANGELOG.md)
296
+ - [Contributing](./CONTRIBUTING.md)
297
+ - [Security Policy](./SECURITY.md)
298
+ - [Code of Conduct](./CODE_OF_CONDUCT.md)
299
+ - [NPM Release Checklist](./docs/engineering/npm-release-checklist.md)
300
+ - [Release Candidate Workflow](./docs/engineering/release-candidate-workflow.md)
301
+ - [v0.1.0 Release Notes](./docs/engineering/v0.1.0-final-release-notes.md)
302
+
303
+ ## Contributing
304
+
305
+ Contributions are welcome once the repository is public. Start with:
306
+
307
+ - [Contributing](./CONTRIBUTING.md)
308
+ - [Security Policy](./SECURITY.md)
309
+ - [Validation tuning issue template](./.github/ISSUE_TEMPLATE/validation_tuning.yml)
310
+
311
+ ## Support
312
+
313
+ If this tool saves you time, GitHub stars and sponsorship help signal that the project is worth continuing.
314
+
315
+ - Star the repository on GitHub.
316
+ - Use GitHub Sponsors through the repository funding link.
317
+ - Open validation tuning issues for false positives or false negatives.
318
+
319
+ ## Product Direction
320
+
321
+ Codex Plugin Doctor starts as a Codex-specific validator and can grow into a broader MCP Doctor over time.
322
+
323
+ The immediate goal is not a marketplace, dashboard, or hosted website. The immediate goal is a trustworthy local preflight check for Codex-compatible plugin bundles.
@@ -0,0 +1,33 @@
1
+ import type { CheckResult } from "../domain/types.js";
2
+ export interface ValidationHistoryEntry {
3
+ schemaVersion: "1.0.0";
4
+ generatedAt: string;
5
+ targetPath: string;
6
+ status: CheckResult["status"];
7
+ runtimeProbeEnabled: boolean;
8
+ findingCounts: {
9
+ fail: number;
10
+ warn: number;
11
+ total: number;
12
+ };
13
+ }
14
+ export interface ValidationHistorySummary {
15
+ schemaVersion: "1.0.0";
16
+ runs: number;
17
+ latest: ValidationHistoryEntry;
18
+ previous: ValidationHistoryEntry | null;
19
+ delta: {
20
+ fail: number;
21
+ warn: number;
22
+ total: number;
23
+ };
24
+ regression: boolean;
25
+ }
26
+ export declare function buildValidationHistoryEntry(result: CheckResult, options: {
27
+ runtimeProbeEnabled: boolean;
28
+ }): ValidationHistoryEntry;
29
+ export declare function appendValidationHistoryEntry(historyPath: string, result: CheckResult, options: {
30
+ runtimeProbeEnabled: boolean;
31
+ }): Promise<void>;
32
+ export declare function readValidationHistory(historyPath: string): Promise<ValidationHistoryEntry[]>;
33
+ export declare function summarizeValidationHistory(entries: ValidationHistoryEntry[]): ValidationHistorySummary;
@@ -0,0 +1,64 @@
1
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ const statusRank = {
4
+ pass: 0,
5
+ warn: 1,
6
+ fail: 2
7
+ };
8
+ function countFindings(result) {
9
+ return {
10
+ fail: result.findings.filter((finding) => finding.severity === "fail").length,
11
+ warn: result.findings.filter((finding) => finding.severity === "warn").length,
12
+ total: result.findings.length
13
+ };
14
+ }
15
+ export function buildValidationHistoryEntry(result, options) {
16
+ return {
17
+ schemaVersion: "1.0.0",
18
+ generatedAt: new Date().toISOString(),
19
+ targetPath: result.targetPath,
20
+ status: result.status,
21
+ runtimeProbeEnabled: options.runtimeProbeEnabled,
22
+ findingCounts: countFindings(result)
23
+ };
24
+ }
25
+ export async function appendValidationHistoryEntry(historyPath, result, options) {
26
+ const absoluteHistoryPath = path.resolve(historyPath);
27
+ await mkdir(path.dirname(absoluteHistoryPath), { recursive: true });
28
+ await writeFile(absoluteHistoryPath, `${JSON.stringify(buildValidationHistoryEntry(result, options))}\n`, { encoding: "utf8", flag: "a" });
29
+ }
30
+ export async function readValidationHistory(historyPath) {
31
+ const content = await readFile(path.resolve(historyPath), "utf8");
32
+ return content
33
+ .split(/\r?\n/)
34
+ .map((line) => line.trim())
35
+ .filter(Boolean)
36
+ .map((line) => JSON.parse(line));
37
+ }
38
+ export function summarizeValidationHistory(entries) {
39
+ if (entries.length === 0) {
40
+ throw new Error("No validation history entries found.");
41
+ }
42
+ const latest = entries[entries.length - 1];
43
+ const previous = entries.length > 1 ? entries[entries.length - 2] : null;
44
+ const delta = previous
45
+ ? {
46
+ fail: latest.findingCounts.fail - previous.findingCounts.fail,
47
+ warn: latest.findingCounts.warn - previous.findingCounts.warn,
48
+ total: latest.findingCounts.total - previous.findingCounts.total
49
+ }
50
+ : { fail: 0, warn: 0, total: 0 };
51
+ const regression = previous
52
+ ? statusRank[latest.status] > statusRank[previous.status]
53
+ || delta.fail > 0
54
+ || delta.warn > 0
55
+ : false;
56
+ return {
57
+ schemaVersion: "1.0.0",
58
+ runs: entries.length,
59
+ latest,
60
+ previous,
61
+ delta,
62
+ regression
63
+ };
64
+ }
@@ -0,0 +1,10 @@
1
+ import type { CheckResult } from "../domain/types.js";
2
+ export interface BadgeReport {
3
+ schemaVersion: 1;
4
+ label: "doctor";
5
+ message: "PASS" | "WARN" | "FAIL";
6
+ color: "brightgreen" | "yellow" | "red";
7
+ }
8
+ export declare function buildBadgeReport(result: CheckResult): BadgeReport;
9
+ export declare function renderBadgeJson(result: CheckResult): string;
10
+ export declare function renderBadgeMarkdown(result: CheckResult): string;
@@ -0,0 +1,32 @@
1
+ function badgeForStatus(status) {
2
+ if (status === "pass") {
3
+ return {
4
+ message: "PASS",
5
+ color: "brightgreen"
6
+ };
7
+ }
8
+ if (status === "warn") {
9
+ return {
10
+ message: "WARN",
11
+ color: "yellow"
12
+ };
13
+ }
14
+ return {
15
+ message: "FAIL",
16
+ color: "red"
17
+ };
18
+ }
19
+ export function buildBadgeReport(result) {
20
+ return {
21
+ schemaVersion: 1,
22
+ label: "doctor",
23
+ ...badgeForStatus(result.status)
24
+ };
25
+ }
26
+ export function renderBadgeJson(result) {
27
+ return JSON.stringify(buildBadgeReport(result), null, 2);
28
+ }
29
+ export function renderBadgeMarkdown(result) {
30
+ const badge = buildBadgeReport(result);
31
+ return `![Codex Plugin Doctor](https://img.shields.io/badge/${badge.label}-${badge.message}-${badge.color})`;
32
+ }
@@ -0,0 +1,2 @@
1
+ import { type ValidationHistoryEntry } from "../core/validation-history.js";
2
+ export declare function renderHistorySummary(entries: ValidationHistoryEntry[]): string;
@@ -0,0 +1,23 @@
1
+ import { summarizeValidationHistory } from "../core/validation-history.js";
2
+ function formatDelta(value) {
3
+ return value > 0 ? `+${value}` : String(value);
4
+ }
5
+ export function renderHistorySummary(entries) {
6
+ const summary = summarizeValidationHistory(entries);
7
+ const { latest, previous } = summary;
8
+ const lines = [
9
+ "Validation History",
10
+ "==================",
11
+ `Runs: ${summary.runs}`,
12
+ `Latest: ${latest.status.toUpperCase()}`,
13
+ `Target: ${latest.targetPath}`,
14
+ `Generated: ${latest.generatedAt}`,
15
+ `Fail findings: ${latest.findingCounts.fail}`,
16
+ `Warn findings: ${latest.findingCounts.warn}`,
17
+ `Regression: ${summary.regression ? "YES" : "NO"}`
18
+ ];
19
+ if (previous) {
20
+ lines.push("", `Previous: ${previous.status.toUpperCase()}`, `Fail findings: ${formatDelta(summary.delta.fail)}`, `Warn findings: ${formatDelta(summary.delta.warn)}`);
21
+ }
22
+ return lines.join("\n");
23
+ }
package/dist/run-cli.d.ts CHANGED
@@ -7,6 +7,7 @@ export interface CliTerminalContext {
7
7
  stdoutIsTTY: boolean;
8
8
  stderrIsTTY: boolean;
9
9
  env: Record<string, string | undefined>;
10
+ platform?: NodeJS.Platform;
10
11
  }
11
12
  export interface RunCliOptions {
12
13
  terminalContext?: CliTerminalContext;
package/dist/run-cli.js CHANGED
@@ -2,6 +2,7 @@ import { writeFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
4
  import { discoverInstalledPlugins, filterInstalledPlugins } from "./core/discover-installed-plugins.js";
5
+ import { appendValidationHistoryEntry, readValidationHistory, summarizeValidationHistory } from "./core/validation-history.js";
5
6
  import { buildCompatibilityMatrix, matrixExitCode } from "./compatibility/compatibility-matrix.js";
6
7
  import { applyInstallPreview, renderApplyInstallResult } from "./compatibility/apply-install-preview.js";
7
8
  import { buildClaudeDesktopInstallPreview, renderClaudeDesktopInstallPreview } from "./compatibility/claude-desktop-install-preview.js";
@@ -10,8 +11,10 @@ import { applyDoctorConfig, loadDoctorConfig } from "./core/doctor-config.js";
10
11
  import { initPluginPackage } from "./core/init-plugin.js";
11
12
  import { runCheck } from "./index.js";
12
13
  import { renderInstalledSummary } from "./reporting/render-installed-summary.js";
14
+ import { renderBadgeJson, renderBadgeMarkdown } from "./reporting/render-badge-report.js";
13
15
  import { renderCompatibilityScorecard } from "./reporting/render-compatibility-scorecard.js";
14
16
  import { renderCompatibilityReport } from "./reporting/render-compatibility-report.js";
17
+ import { renderHistorySummary } from "./reporting/render-history-summary.js";
15
18
  import { renderJsonReport } from "./reporting/render-json-report.js";
16
19
  import { buildMarkdownReport } from "./reporting/render-markdown-report.js";
17
20
  import { renderRuleExplanation } from "./reporting/render-rule-explanation.js";
@@ -31,7 +34,7 @@ const defaultIo = {
31
34
  }
32
35
  };
33
36
  function printUsage(io) {
34
- io.writeStderr("Usage: codex-plugin-doctor check <path|--installed> [filter] [--json|--markdown] [--output <path>] [--runtime] [--verbose-runtime] [--no-animations] [--ascii]\n codex-plugin-doctor compat <path> [--client <client>] [--json] [--scorecard] [--output <path>] [--install-preview|--apply --backup]\n codex-plugin-doctor self-test\n codex-plugin-doctor list --installed\n codex-plugin-doctor explain <finding-id>\n codex-plugin-doctor --version");
37
+ io.writeStderr("Usage: codex-plugin-doctor check <path|--installed> [filter] [--json|--markdown|--badge-json|--badge-markdown] [--output <path>] [--history <path>] [--runtime] [--verbose-runtime] [--no-animations] [--ascii]\n codex-plugin-doctor compat <path> [--client <client>] [--json] [--scorecard] [--output <path>] [--install-preview|--apply --backup]\n codex-plugin-doctor history <history.jsonl> [--json] [--fail-on-regression]\n codex-plugin-doctor self-test\n codex-plugin-doctor list --installed\n codex-plugin-doctor explain <finding-id>\n codex-plugin-doctor --version");
35
38
  }
36
39
  function renderInstalledPlugins(plugins) {
37
40
  const lines = [
@@ -94,7 +97,8 @@ export async function runCli(args, io = defaultIo, options = {}) {
94
97
  const terminalContext = options.terminalContext ?? {
95
98
  stdoutIsTTY: Boolean(process.stdout.isTTY),
96
99
  stderrIsTTY: Boolean(process.stderr.isTTY),
97
- env: process.env
100
+ env: process.env,
101
+ platform: process.platform
98
102
  };
99
103
  if (command === "list" && maybePath === "--installed") {
100
104
  const installedPlugins = await discoverInstalledPlugins({
@@ -116,12 +120,38 @@ export async function runCli(args, io = defaultIo, options = {}) {
116
120
  io.writeStdout(renderRuleExplanation(rule));
117
121
  return 0;
118
122
  }
123
+ if (command === "history") {
124
+ if (!maybePath || maybePath.startsWith("--")) {
125
+ io.writeStderr("Missing history path. Usage: codex-plugin-doctor history <history.jsonl> [--json] [--fail-on-regression]");
126
+ return 2;
127
+ }
128
+ try {
129
+ const entries = await readValidationHistory(maybePath);
130
+ const summary = summarizeValidationHistory(entries);
131
+ const jsonOutput = remainingArgs.includes("--json");
132
+ const failOnRegression = remainingArgs.includes("--fail-on-regression");
133
+ io.writeStdout(jsonOutput
134
+ ? JSON.stringify(summary, null, 2)
135
+ : renderHistorySummary(entries));
136
+ if (failOnRegression && summary.regression) {
137
+ io.writeStderr("Validation history regression detected.");
138
+ return 1;
139
+ }
140
+ return 0;
141
+ }
142
+ catch (error) {
143
+ const message = error instanceof Error ? error.message : "Unable to read validation history.";
144
+ io.writeStderr(message);
145
+ return 1;
146
+ }
147
+ }
119
148
  if (command === "self-test" || command === "demo") {
120
149
  const targetPath = resolveBundledSelfTestTarget();
121
150
  const runCheckImpl = options.runCheckImpl ?? runCheck;
122
151
  const result = applyDoctorConfig(await runCheckImpl(targetPath, { runtime: true }), await loadDoctorConfig(targetPath));
123
152
  const compatibilityMatrix = await buildCompatibilityMatrix(targetPath, {
124
- env: terminalContext.env
153
+ env: terminalContext.env,
154
+ platform: terminalContext.platform
125
155
  });
126
156
  io.writeStdout(renderSelfTestReport(targetPath, result.status, result.findings.length, compatibilityMatrix));
127
157
  return result.exitCode === 1 || matrixExitCode(compatibilityMatrix) === 1 ? 1 : 0;
@@ -179,10 +209,12 @@ export async function runCli(args, io = defaultIo, options = {}) {
179
209
  try {
180
210
  const preview = clientFilter?.toLowerCase() === "cursor"
181
211
  ? await buildCursorInstallPreview(targetPath, {
182
- env: terminalContext.env
212
+ env: terminalContext.env,
213
+ platform: terminalContext.platform
183
214
  })
184
215
  : await buildClaudeDesktopInstallPreview(targetPath, {
185
- env: terminalContext.env
216
+ env: terminalContext.env,
217
+ platform: terminalContext.platform
186
218
  });
187
219
  const report = applyInstall
188
220
  ? renderApplyInstallResult(await applyInstallPreview(clientFilter?.toLowerCase() === "cursor" ? "Cursor" : "Claude Desktop", preview))
@@ -202,7 +234,8 @@ export async function runCli(args, io = defaultIo, options = {}) {
202
234
  }
203
235
  }
204
236
  let matrix = await buildCompatibilityMatrix(targetPath, {
205
- env: terminalContext.env
237
+ env: terminalContext.env,
238
+ platform: terminalContext.platform
206
239
  });
207
240
  if (clientFilter) {
208
241
  const filteredMatrix = filterCompatibilityMatrix(matrix, clientFilter);
@@ -242,6 +275,8 @@ export async function runCli(args, io = defaultIo, options = {}) {
242
275
  : remainingArgs;
243
276
  const jsonOutput = normalizedFlags.includes("--json");
244
277
  const markdownOutput = normalizedFlags.includes("--markdown");
278
+ const badgeJsonOutput = normalizedFlags.includes("--badge-json");
279
+ const badgeMarkdownOutput = normalizedFlags.includes("--badge-markdown");
245
280
  const sarifOutput = normalizedFlags.includes("--sarif");
246
281
  const runtimeProbeEnabled = normalizedFlags.includes("--runtime");
247
282
  const verboseRuntime = normalizedFlags.includes("--verbose-runtime");
@@ -252,6 +287,8 @@ export async function runCli(args, io = defaultIo, options = {}) {
252
287
  const outputPath = outputIndex === -1 ? null : normalizedFlags[outputIndex + 1];
253
288
  const configIndex = normalizedFlags.indexOf("--config");
254
289
  const configPath = configIndex === -1 ? null : normalizedFlags[configIndex + 1];
290
+ const historyIndex = normalizedFlags.indexOf("--history");
291
+ const historyPath = historyIndex === -1 ? null : normalizedFlags[historyIndex + 1];
255
292
  if (outputIndex !== -1 && (!outputPath || outputPath.startsWith("--"))) {
256
293
  io.writeStderr("Missing path after --output.");
257
294
  return 2;
@@ -260,9 +297,21 @@ export async function runCli(args, io = defaultIo, options = {}) {
260
297
  io.writeStderr("Missing path after --config.");
261
298
  return 2;
262
299
  }
300
+ if (historyIndex !== -1 && (!historyPath || historyPath.startsWith("--"))) {
301
+ io.writeStderr("Missing path after --history.");
302
+ return 2;
303
+ }
304
+ if (checkInstalled && (badgeJsonOutput || badgeMarkdownOutput)) {
305
+ io.writeStderr("Badge output requires a single package target.");
306
+ return 2;
307
+ }
308
+ if (checkInstalled && historyPath) {
309
+ io.writeStderr("History output requires a single package target.");
310
+ return 2;
311
+ }
263
312
  const outputPolicy = determineOutputPolicy({
264
- jsonOutput,
265
- markdownOutput,
313
+ jsonOutput: jsonOutput || badgeJsonOutput,
314
+ markdownOutput: markdownOutput || badgeMarkdownOutput,
266
315
  outputPath,
267
316
  noAnimations,
268
317
  asciiMode,
@@ -334,10 +383,17 @@ export async function runCli(args, io = defaultIo, options = {}) {
334
383
  ? renderSarifReport(result)
335
384
  : jsonOutput
336
385
  ? renderJsonReport(result, { runtimeProbeEnabled })
337
- : renderTextReport(result, { ascii: outputPolicy.style === "ascii" });
386
+ : badgeJsonOutput
387
+ ? renderBadgeJson(result)
388
+ : badgeMarkdownOutput
389
+ ? renderBadgeMarkdown(result)
390
+ : renderTextReport(result, { ascii: outputPolicy.style === "ascii" });
338
391
  if (outputPath) {
339
392
  await writeFile(outputPath, report, "utf8");
340
393
  }
394
+ if (historyPath) {
395
+ await appendValidationHistoryEntry(historyPath, result, { runtimeProbeEnabled });
396
+ }
341
397
  io.writeStdout(report);
342
398
  return result.exitCode;
343
399
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-plugin-doctor",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "description": "CLI-first validator for Codex plugins, skills, and MCP package surfaces with runtime MCP protocol validation.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",