codex-plugin-doctor 0.5.0 → 0.7.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
@@ -65,8 +65,9 @@ Output formats:
65
65
  - human text output
66
66
  - JSON reports
67
67
  - Markdown reports
68
- - Shields-compatible badge JSON and static badge Markdown
69
- - `--output` file writing
68
+ - Shields-compatible badge JSON and static badge Markdown
69
+ - validation history JSONL and trend summaries
70
+ - `--output` file writing
70
71
  - CI summary and artifact generation
71
72
 
72
73
  ## Quick Start
@@ -161,45 +162,67 @@ Run these from a Codex plugin package root:
161
162
 
162
163
  ```bash
163
164
  codex-plugin-doctor --version
164
- codex-plugin-doctor self-test
165
- codex-plugin-doctor init my-plugin
166
- codex-plugin-doctor compat .
167
- codex-plugin-doctor compat . --client codex
168
- codex-plugin-doctor compat . --client generic-mcp
165
+ codex-plugin-doctor self-test
166
+ codex-plugin-doctor doctor
167
+ codex-plugin-doctor init my-plugin
168
+ codex-plugin-doctor compat .
169
+ codex-plugin-doctor compat . --client codex
170
+ codex-plugin-doctor compat . --client generic-mcp
169
171
  codex-plugin-doctor compat . --client claude-desktop
170
172
  codex-plugin-doctor compat . --client claude-desktop --install-preview
171
173
  codex-plugin-doctor compat . --client claude-desktop --apply --backup
172
- codex-plugin-doctor compat . --client cursor
173
- codex-plugin-doctor compat . --client cursor --install-preview
174
- codex-plugin-doctor compat . --client cursor --apply --backup
175
- codex-plugin-doctor compat . --scorecard
176
- codex-plugin-doctor compat . --json
177
- codex-plugin-doctor compat . --json --output compatibility.json
178
- codex-plugin-doctor check .
179
- codex-plugin-doctor check . --json
180
- codex-plugin-doctor check . --json --output report.json
181
- codex-plugin-doctor check . --markdown --output report.md
174
+ codex-plugin-doctor compat . --client cursor
175
+ codex-plugin-doctor compat . --client cursor --install-preview
176
+ codex-plugin-doctor compat . --client cursor --apply --backup
177
+ codex-plugin-doctor compat . --client cline
178
+ codex-plugin-doctor compat . --client cline --install-preview
179
+ codex-plugin-doctor compat . --scorecard
180
+ codex-plugin-doctor compat . --json
181
+ codex-plugin-doctor compat . --json --output compatibility.json
182
+ codex-plugin-doctor check .
183
+ codex-plugin-doctor check . --profile ci
184
+ codex-plugin-doctor check . --profile strict
185
+ codex-plugin-doctor check . --profile publish
186
+ codex-plugin-doctor check . --json
187
+ codex-plugin-doctor check . --json --output report.json
188
+ codex-plugin-doctor check . --markdown --output report.md
182
189
  codex-plugin-doctor check . --badge-json --output doctor-badge.json
183
190
  codex-plugin-doctor check . --badge-markdown
184
191
  codex-plugin-doctor check . --sarif --output results.sarif
185
192
  codex-plugin-doctor check . --ascii
186
193
  codex-plugin-doctor check . --no-animations
187
- codex-plugin-doctor check . --runtime
188
- codex-plugin-doctor check . --config .codex-doctor.json
189
- codex-plugin-doctor check . --json --runtime --verbose-runtime
190
- ```
191
-
192
- `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`.
193
-
194
- `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.
195
-
196
- `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.
197
-
198
- `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`.
199
-
200
- `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`.
201
-
202
- Optional local policy file:
194
+ codex-plugin-doctor check . --runtime
195
+ codex-plugin-doctor check . --config .codex-doctor.json
196
+ codex-plugin-doctor check . --history validation-history.jsonl
197
+ codex-plugin-doctor history validation-history.jsonl
198
+ codex-plugin-doctor history validation-history.jsonl --json
199
+ codex-plugin-doctor history validation-history.jsonl --fail-on-regression
200
+ codex-plugin-doctor fix . --dry-run
201
+ codex-plugin-doctor fix . --apply --backup
202
+ codex-plugin-doctor check . --json --runtime --verbose-runtime
203
+ ```
204
+
205
+ `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`.
206
+
207
+ `doctor` checks the local environment, including package version, platform, Node version, npm global prefix, Codex home, and Codex plugin cache visibility.
208
+
209
+ `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.
210
+
211
+ `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.
212
+
213
+ `compat --client cline` checks whether the MCP package can be added to Cline. It uses `CLINE_DIR/data/settings/cline_mcp_settings.json` when `CLINE_DIR` is set, otherwise `~/.cline/data/settings/cline_mcp_settings.json`. Add `--install-preview` to print the JSON snippet that should be merged into `cline_mcp_settings.json`.
214
+
215
+ `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`.
216
+
217
+ `check --profile ci|strict|publish` applies named validation policies. `ci` keeps default behavior, `strict` fails on warnings, and `publish` fails on warnings while enabling runtime probing by default.
218
+
219
+ `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`.
220
+
221
+ `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.
222
+
223
+ `fix --dry-run` renders safe automatic fix plans without changing files. `fix --apply --backup` applies only supported safe fixes, such as manifest defaults and missing skills directories, after creating backups.
224
+
225
+ Optional local policy file:
203
226
 
204
227
  ```json
205
228
  {
@@ -232,11 +255,11 @@ jobs:
232
255
  runs-on: ubuntu-latest
233
256
  steps:
234
257
  - uses: actions/checkout@v4
235
- - uses: Esquetta/CodexPluginDoctor@v0.5.0
236
- with:
237
- version: "0.5.0"
238
- path: .
239
- runtime: "false"
258
+ - uses: Esquetta/CodexPluginDoctor@v0.7.0
259
+ with:
260
+ version: "0.7.0"
261
+ path: .
262
+ runtime: "false"
240
263
  ```
241
264
 
242
265
  For runtime probing, SARIF output, installed plugin cache checks, and pinned release examples, see [GitHub Action Usage](./docs/engineering/github-action-usage.md).
@@ -0,0 +1,10 @@
1
+ import { type CompatibilityEnvironment } from "./compatibility-matrix.js";
2
+ export interface ClineInstallPreview {
3
+ targetPath: string;
4
+ configPath: string;
5
+ snippet: {
6
+ mcpServers: Record<string, unknown>;
7
+ };
8
+ }
9
+ export declare function buildClineInstallPreview(targetPath: string, environment?: CompatibilityEnvironment): Promise<ClineInstallPreview>;
10
+ export declare function renderClineInstallPreview(preview: ClineInstallPreview): string;
@@ -0,0 +1,65 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { getClineMcpConfigPath, readMcpConfigPath } from "./compatibility-matrix.js";
4
+ function isRecord(value) {
5
+ return typeof value === "object" && value !== null && !Array.isArray(value);
6
+ }
7
+ function isRelativeLocalPath(value) {
8
+ return value.startsWith("./") ||
9
+ value.startsWith("../") ||
10
+ value.startsWith(".\\") ||
11
+ value.startsWith("..\\");
12
+ }
13
+ function normalizeLocalPathArgument(value, rootPath) {
14
+ return typeof value === "string" && isRelativeLocalPath(value)
15
+ ? path.resolve(rootPath, value)
16
+ : value;
17
+ }
18
+ function normalizeServerConfig(serverConfig, rootPath) {
19
+ if (!isRecord(serverConfig)) {
20
+ return serverConfig;
21
+ }
22
+ const normalized = { ...serverConfig };
23
+ if (typeof normalized.command === "string" && isRelativeLocalPath(normalized.command)) {
24
+ normalized.command = path.resolve(rootPath, normalized.command);
25
+ }
26
+ if (Array.isArray(normalized.args)) {
27
+ normalized.args = normalized.args.map((argument) => normalizeLocalPathArgument(argument, rootPath));
28
+ }
29
+ return normalized;
30
+ }
31
+ export async function buildClineInstallPreview(targetPath, environment = {}) {
32
+ const rootPath = path.resolve(targetPath);
33
+ const mcpConfigPath = await readMcpConfigPath(rootPath);
34
+ if (!mcpConfigPath) {
35
+ throw new Error("No MCP config found for install preview.");
36
+ }
37
+ const parsed = JSON.parse(await readFile(mcpConfigPath, "utf8"));
38
+ const servers = parsed.mcpServers;
39
+ if (!isRecord(servers) || Object.keys(servers).length === 0) {
40
+ throw new Error("MCP config does not contain a non-empty `mcpServers` object.");
41
+ }
42
+ return {
43
+ targetPath: rootPath,
44
+ configPath: getClineMcpConfigPath(environment),
45
+ snippet: {
46
+ mcpServers: Object.fromEntries(Object.entries(servers).map(([serverName, serverConfig]) => [
47
+ serverName,
48
+ normalizeServerConfig(serverConfig, rootPath)
49
+ ]))
50
+ }
51
+ };
52
+ }
53
+ export function renderClineInstallPreview(preview) {
54
+ return [
55
+ "Cline Install Preview",
56
+ "=====================",
57
+ `Target: ${preview.targetPath}`,
58
+ `Config: ${preview.configPath}`,
59
+ "",
60
+ "Add or merge this snippet into `cline_mcp_settings.json`:",
61
+ JSON.stringify(preview.snippet, null, 2),
62
+ "",
63
+ "No files were modified."
64
+ ].join("\n");
65
+ }
@@ -17,5 +17,6 @@ export interface CompatibilityEnvironment {
17
17
  export declare function readMcpConfigPath(targetPath: string): Promise<string | null>;
18
18
  export declare function getClaudeDesktopConfigPath(environment?: CompatibilityEnvironment): string | null;
19
19
  export declare function getCursorMcpConfigPath(targetPath: string, environment?: CompatibilityEnvironment): Promise<string>;
20
+ export declare function getClineMcpConfigPath(environment?: CompatibilityEnvironment): string;
20
21
  export declare function buildCompatibilityMatrix(targetPath: string, environment?: CompatibilityEnvironment): Promise<CompatibilityMatrix>;
21
22
  export declare function matrixExitCode(matrix: CompatibilityMatrix): 0 | 1;
@@ -134,6 +134,13 @@ export async function getCursorMcpConfigPath(targetPath, environment = {}) {
134
134
  }
135
135
  return path.join(getHomeDirectory(environment), ".cursor", "mcp.json");
136
136
  }
137
+ export function getClineMcpConfigPath(environment = {}) {
138
+ const env = environment.env ?? process.env;
139
+ const clineDirectory = env.CLINE_DIR
140
+ ? path.resolve(env.CLINE_DIR)
141
+ : path.join(getHomeDirectory(environment), ".cline");
142
+ return path.join(clineDirectory, "data", "settings", "cline_mcp_settings.json");
143
+ }
137
144
  async function checkClaudeDesktop(targetPath, genericMcpResult, environment = {}) {
138
145
  if (genericMcpResult.status !== "pass") {
139
146
  return {
@@ -286,11 +293,84 @@ async function checkCursor(targetPath, genericMcpResult, environment = {}) {
286
293
  };
287
294
  }
288
295
  }
296
+ async function checkCline(targetPath, genericMcpResult, environment = {}) {
297
+ if (genericMcpResult.status !== "pass") {
298
+ return {
299
+ client: "Cline",
300
+ status: "skipped",
301
+ summary: "No valid MCP package config is available for Cline.",
302
+ details: ["Add a valid `.mcp.json` with a non-empty `mcpServers` object first."]
303
+ };
304
+ }
305
+ const configPath = getClineMcpConfigPath(environment);
306
+ if (!(await fileExists(configPath))) {
307
+ const configDirectory = path.dirname(configPath);
308
+ return {
309
+ client: "Cline",
310
+ status: await directoryExists(configDirectory) ? "pass" : "warn",
311
+ summary: await directoryExists(configDirectory)
312
+ ? "Cline MCP settings directory exists and a config file can be created."
313
+ : "Cline was not detected on this machine.",
314
+ details: [
315
+ configPath,
316
+ "Cline stores MCP servers in `cline_mcp_settings.json` under its settings directory."
317
+ ]
318
+ };
319
+ }
320
+ try {
321
+ const parsed = await readJsonFile(configPath);
322
+ const servers = parsed.mcpServers;
323
+ if (servers !== undefined && (typeof servers !== "object" ||
324
+ servers === null ||
325
+ Array.isArray(servers))) {
326
+ return {
327
+ client: "Cline",
328
+ status: "fail",
329
+ summary: "Cline MCP config has an invalid `mcpServers` shape.",
330
+ details: [configPath, "`mcpServers` must be an object before this package can be added safely."]
331
+ };
332
+ }
333
+ const packageServerNames = await readMcpServerNames(targetPath);
334
+ const existingServerNames = typeof servers === "object" && servers !== null
335
+ ? Object.keys(servers)
336
+ : [];
337
+ const duplicateServerNames = packageServerNames.filter((serverName) => existingServerNames.includes(serverName));
338
+ if (duplicateServerNames.length > 0) {
339
+ return {
340
+ client: "Cline",
341
+ status: "warn",
342
+ summary: "Cline already has MCP server names from this package.",
343
+ details: [
344
+ configPath,
345
+ ...duplicateServerNames.map((serverName) => `Duplicate server: ${serverName}`)
346
+ ]
347
+ };
348
+ }
349
+ return {
350
+ client: "Cline",
351
+ status: "pass",
352
+ summary: "Cline MCP config is valid and this package can be added.",
353
+ details: [
354
+ configPath,
355
+ `Source package: ${path.resolve(targetPath)}`
356
+ ]
357
+ };
358
+ }
359
+ catch {
360
+ return {
361
+ client: "Cline",
362
+ status: "fail",
363
+ summary: "Cline MCP config is not valid JSON.",
364
+ details: [configPath, "Repair the local Cline MCP config before adding new MCP servers."]
365
+ };
366
+ }
367
+ }
289
368
  export async function buildCompatibilityMatrix(targetPath, environment = {}) {
290
369
  const rootPath = path.resolve(targetPath);
291
370
  const genericMcpResult = await checkGenericMcp(rootPath);
292
371
  const claudeDesktopResult = await checkClaudeDesktop(rootPath, genericMcpResult, environment);
293
372
  const cursorResult = await checkCursor(rootPath, genericMcpResult, environment);
373
+ const clineResult = await checkCline(rootPath, genericMcpResult, environment);
294
374
  const codexResult = await validatePlugin(rootPath);
295
375
  const codexStatus = statusFromCheckResult(codexResult);
296
376
  const codexCompatibility = !await hasCodexManifest(rootPath)
@@ -313,7 +393,8 @@ export async function buildCompatibilityMatrix(targetPath, environment = {}) {
313
393
  codexCompatibility,
314
394
  genericMcpResult,
315
395
  claudeDesktopResult,
316
- cursorResult
396
+ cursorResult,
397
+ clineResult
317
398
  ];
318
399
  return {
319
400
  targetPath: rootPath,
@@ -0,0 +1,2 @@
1
+ import type { CliTerminalContext } from "../run-cli.js";
2
+ export declare function renderEnvironmentDoctor(terminalContext: CliTerminalContext): Promise<string>;
@@ -0,0 +1,41 @@
1
+ import { access } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { packageVersion } from "../version.js";
4
+ async function pathExists(targetPath) {
5
+ try {
6
+ await access(targetPath);
7
+ return true;
8
+ }
9
+ catch {
10
+ return false;
11
+ }
12
+ }
13
+ function resolveCodexHome(env) {
14
+ if (env.CODEX_HOME) {
15
+ return path.resolve(env.CODEX_HOME);
16
+ }
17
+ if (env.USERPROFILE) {
18
+ return path.join(env.USERPROFILE, ".codex");
19
+ }
20
+ if (env.HOME) {
21
+ return path.join(env.HOME, ".codex");
22
+ }
23
+ return null;
24
+ }
25
+ export async function renderEnvironmentDoctor(terminalContext) {
26
+ const codexHome = resolveCodexHome(terminalContext.env);
27
+ const codexHomeExists = codexHome ? await pathExists(codexHome) : false;
28
+ const pluginCache = codexHome ? path.join(codexHome, "plugins", "cache") : null;
29
+ const pluginCacheExists = pluginCache ? await pathExists(pluginCache) : false;
30
+ const npmPrefix = terminalContext.env.npm_config_prefix ?? "(unknown)";
31
+ return [
32
+ "Codex Plugin Doctor Environment",
33
+ "===============================",
34
+ `Version: ${packageVersion}`,
35
+ `Platform: ${terminalContext.platform ?? process.platform}`,
36
+ `Node: ${process.version}`,
37
+ `npm global prefix: ${npmPrefix}`,
38
+ `Codex home: ${codexHomeExists ? "PASS" : "WARN"}${codexHome ? ` (${codexHome})` : ""}`,
39
+ `Codex plugin cache: ${pluginCacheExists ? "PASS" : "WARN"}${pluginCache ? ` (${pluginCache})` : ""}`
40
+ ].join("\n");
41
+ }
@@ -0,0 +1,20 @@
1
+ export interface FixPlanAction {
2
+ id: string;
3
+ title: string;
4
+ targetPath: string;
5
+ operation: "update-json" | "mkdir";
6
+ details: string;
7
+ }
8
+ export interface FixPlan {
9
+ targetPath: string;
10
+ actions: FixPlanAction[];
11
+ }
12
+ export interface ApplyFixPlanResult {
13
+ plan: FixPlan;
14
+ filesChanged: number;
15
+ backupDirectory: string;
16
+ }
17
+ export declare function buildFixPlan(targetPath: string): Promise<FixPlan>;
18
+ export declare function renderFixPlan(plan: FixPlan, mode: "dry-run"): string;
19
+ export declare function applyFixPlan(targetPath: string): Promise<ApplyFixPlanResult>;
20
+ export declare function renderApplyFixResult(result: ApplyFixPlanResult): string;
@@ -0,0 +1,114 @@
1
+ import { copyFile, mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { validatePlugin } from "./validate-plugin.js";
4
+ function relativeToTarget(targetPath, candidatePath) {
5
+ return path.relative(targetPath, candidatePath).replace(/\\/g, "/");
6
+ }
7
+ export async function buildFixPlan(targetPath) {
8
+ const result = await validatePlugin(targetPath);
9
+ const rootPath = result.targetPath;
10
+ const actions = [];
11
+ const findingIds = new Set(result.findings.map((finding) => finding.id));
12
+ const manifestPath = path.join(rootPath, ".codex-plugin", "plugin.json");
13
+ if (findingIds.has("plugin.manifest.version.missing") ||
14
+ findingIds.has("plugin.manifest.description.missing")) {
15
+ const fields = [
16
+ findingIds.has("plugin.manifest.version.missing") ? "`version`" : null,
17
+ findingIds.has("plugin.manifest.description.missing") ? "`description`" : null
18
+ ].filter(Boolean);
19
+ actions.push({
20
+ id: "manifest.safe_defaults",
21
+ title: "Add missing safe manifest defaults",
22
+ targetPath: manifestPath,
23
+ operation: "update-json",
24
+ details: `Set ${fields.join(" and ")} in ${relativeToTarget(rootPath, manifestPath)}.`
25
+ });
26
+ }
27
+ if (findingIds.has("plugin.skills.path.missing")) {
28
+ actions.push({
29
+ id: "skills.create_directory",
30
+ title: "Create missing skills directory",
31
+ targetPath: path.join(rootPath, "skills"),
32
+ operation: "mkdir",
33
+ details: "Create the skills directory referenced by the manifest."
34
+ });
35
+ }
36
+ return {
37
+ targetPath: rootPath,
38
+ actions
39
+ };
40
+ }
41
+ export function renderFixPlan(plan, mode) {
42
+ const lines = [
43
+ "Fix Plan",
44
+ "========",
45
+ `Mode: ${mode}`,
46
+ `Target: ${plan.targetPath}`
47
+ ];
48
+ if (plan.actions.length === 0) {
49
+ lines.push("", "No safe automatic fixes available.");
50
+ return lines.join("\n");
51
+ }
52
+ lines.push("", "Actions");
53
+ lines.push("-------");
54
+ plan.actions.forEach((action, index) => {
55
+ lines.push(`${index + 1}. ${action.title}`);
56
+ lines.push(` Path: ${relativeToTarget(plan.targetPath, action.targetPath)}`);
57
+ lines.push(` Operation: ${action.operation}`);
58
+ lines.push(` Details: ${action.details}`);
59
+ });
60
+ lines.push("", "No files changed.");
61
+ return lines.join("\n");
62
+ }
63
+ function timestampForPath() {
64
+ return new Date().toISOString().replace(/[:.]/g, "-");
65
+ }
66
+ function defaultManifestDescription() {
67
+ return "Codex plugin package.";
68
+ }
69
+ async function backupFile(rootPath, backupDirectory, filePath) {
70
+ const relativePath = path.relative(rootPath, filePath);
71
+ const backupPath = path.join(backupDirectory, relativePath);
72
+ await mkdir(path.dirname(backupPath), { recursive: true });
73
+ await copyFile(filePath, backupPath);
74
+ }
75
+ export async function applyFixPlan(targetPath) {
76
+ const plan = await buildFixPlan(targetPath);
77
+ const backupDirectory = path.join(plan.targetPath, ".codex-doctor-backups", timestampForPath());
78
+ let filesChanged = 0;
79
+ for (const action of plan.actions) {
80
+ if (action.operation === "update-json" && action.id === "manifest.safe_defaults") {
81
+ await backupFile(plan.targetPath, backupDirectory, action.targetPath);
82
+ const manifest = JSON.parse(await readFile(action.targetPath, "utf8"));
83
+ if (typeof manifest.version !== "string" || manifest.version.trim() === "") {
84
+ manifest.version = "0.1.0";
85
+ }
86
+ if (typeof manifest.description !== "string" ||
87
+ manifest.description.trim() === "") {
88
+ manifest.description = defaultManifestDescription();
89
+ }
90
+ await writeFile(action.targetPath, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
91
+ filesChanged += 1;
92
+ continue;
93
+ }
94
+ if (action.operation === "mkdir" && action.id === "skills.create_directory") {
95
+ await mkdir(action.targetPath, { recursive: true });
96
+ filesChanged += 1;
97
+ }
98
+ }
99
+ return {
100
+ plan,
101
+ filesChanged,
102
+ backupDirectory
103
+ };
104
+ }
105
+ export function renderApplyFixResult(result) {
106
+ return [
107
+ "Fix Plan",
108
+ "========",
109
+ "Mode: apply",
110
+ `Target: ${result.plan.targetPath}`,
111
+ `Files changed: ${result.filesChanged}`,
112
+ `Backup: ${relativeToTarget(result.plan.targetPath, result.backupDirectory)}`
113
+ ].join("\n");
114
+ }
@@ -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,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.js CHANGED
@@ -2,17 +2,22 @@ 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";
8
9
  import { buildCursorInstallPreview, renderCursorInstallPreview } from "./compatibility/cursor-install-preview.js";
10
+ import { buildClineInstallPreview, renderClineInstallPreview } from "./compatibility/cline-install-preview.js";
9
11
  import { applyDoctorConfig, loadDoctorConfig } from "./core/doctor-config.js";
12
+ import { applyFixPlan, buildFixPlan, renderApplyFixResult, renderFixPlan } from "./core/fix-plan.js";
13
+ import { renderEnvironmentDoctor } from "./core/environment-doctor.js";
10
14
  import { initPluginPackage } from "./core/init-plugin.js";
11
15
  import { runCheck } from "./index.js";
12
16
  import { renderInstalledSummary } from "./reporting/render-installed-summary.js";
13
17
  import { renderBadgeJson, renderBadgeMarkdown } from "./reporting/render-badge-report.js";
14
18
  import { renderCompatibilityScorecard } from "./reporting/render-compatibility-scorecard.js";
15
19
  import { renderCompatibilityReport } from "./reporting/render-compatibility-report.js";
20
+ import { renderHistorySummary } from "./reporting/render-history-summary.js";
16
21
  import { renderJsonReport } from "./reporting/render-json-report.js";
17
22
  import { buildMarkdownReport } from "./reporting/render-markdown-report.js";
18
23
  import { renderRuleExplanation } from "./reporting/render-rule-explanation.js";
@@ -32,7 +37,7 @@ const defaultIo = {
32
37
  }
33
38
  };
34
39
  function printUsage(io) {
35
- io.writeStderr("Usage: codex-plugin-doctor check <path|--installed> [filter] [--json|--markdown|--badge-json|--badge-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");
40
+ 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 fix <path> (--dry-run|--apply --backup)\n codex-plugin-doctor history <history.jsonl> [--json] [--fail-on-regression]\n codex-plugin-doctor doctor\n codex-plugin-doctor self-test\n codex-plugin-doctor list --installed\n codex-plugin-doctor explain <finding-id>\n codex-plugin-doctor --version");
36
41
  }
37
42
  function renderInstalledPlugins(plugins) {
38
43
  const lines = [
@@ -58,8 +63,27 @@ const compatibilityClientAliases = {
58
63
  mcp: "Generic MCP",
59
64
  "claude-desktop": "Claude Desktop",
60
65
  claude: "Claude Desktop",
61
- cursor: "Cursor"
66
+ cursor: "Cursor",
67
+ cline: "Cline"
62
68
  };
69
+ const checkProfiles = ["ci", "strict", "publish"];
70
+ function parseCheckProfile(value) {
71
+ if (!value) {
72
+ return null;
73
+ }
74
+ return checkProfiles.includes(value)
75
+ ? value
76
+ : null;
77
+ }
78
+ function applyCheckProfile(config, profile) {
79
+ if (profile === "strict" || profile === "publish") {
80
+ return {
81
+ ...config,
82
+ failOnWarnings: true
83
+ };
84
+ }
85
+ return config;
86
+ }
63
87
  function filterCompatibilityMatrix(matrix, clientFilter) {
64
88
  const client = compatibilityClientAliases[clientFilter.toLowerCase()];
65
89
  if (!client) {
@@ -105,6 +129,10 @@ export async function runCli(args, io = defaultIo, options = {}) {
105
129
  io.writeStdout(renderInstalledPlugins(installedPlugins));
106
130
  return 0;
107
131
  }
132
+ if (command === "doctor") {
133
+ io.writeStdout(await renderEnvironmentDoctor(terminalContext));
134
+ return 0;
135
+ }
108
136
  if (command === "explain") {
109
137
  if (!maybePath || maybePath.startsWith("--")) {
110
138
  io.writeStderr("Missing finding id. Usage: codex-plugin-doctor explain <finding-id>");
@@ -118,6 +146,31 @@ export async function runCli(args, io = defaultIo, options = {}) {
118
146
  io.writeStdout(renderRuleExplanation(rule));
119
147
  return 0;
120
148
  }
149
+ if (command === "history") {
150
+ if (!maybePath || maybePath.startsWith("--")) {
151
+ io.writeStderr("Missing history path. Usage: codex-plugin-doctor history <history.jsonl> [--json] [--fail-on-regression]");
152
+ return 2;
153
+ }
154
+ try {
155
+ const entries = await readValidationHistory(maybePath);
156
+ const summary = summarizeValidationHistory(entries);
157
+ const jsonOutput = remainingArgs.includes("--json");
158
+ const failOnRegression = remainingArgs.includes("--fail-on-regression");
159
+ io.writeStdout(jsonOutput
160
+ ? JSON.stringify(summary, null, 2)
161
+ : renderHistorySummary(entries));
162
+ if (failOnRegression && summary.regression) {
163
+ io.writeStderr("Validation history regression detected.");
164
+ return 1;
165
+ }
166
+ return 0;
167
+ }
168
+ catch (error) {
169
+ const message = error instanceof Error ? error.message : "Unable to read validation history.";
170
+ io.writeStderr(message);
171
+ return 1;
172
+ }
173
+ }
121
174
  if (command === "self-test" || command === "demo") {
122
175
  const targetPath = resolveBundledSelfTestTarget();
123
176
  const runCheckImpl = options.runCheckImpl ?? runCheck;
@@ -142,6 +195,27 @@ export async function runCli(args, io = defaultIo, options = {}) {
142
195
  ].join("\n"));
143
196
  return 0;
144
197
  }
198
+ if (command === "fix") {
199
+ if (!maybePath || maybePath.startsWith("--")) {
200
+ io.writeStderr("Missing target path. Usage: codex-plugin-doctor fix <path> (--dry-run|--apply --backup)");
201
+ return 2;
202
+ }
203
+ const dryRun = remainingArgs.includes("--dry-run");
204
+ const apply = remainingArgs.includes("--apply");
205
+ const backup = remainingArgs.includes("--backup");
206
+ if (apply && !backup) {
207
+ io.writeStderr("Fix apply requires --backup.");
208
+ return 2;
209
+ }
210
+ if (dryRun === apply) {
211
+ io.writeStderr("Choose exactly one fix mode: --dry-run or --apply --backup.");
212
+ return 2;
213
+ }
214
+ io.writeStdout(dryRun
215
+ ? renderFixPlan(await buildFixPlan(maybePath), "dry-run")
216
+ : renderApplyFixResult(await applyFixPlan(maybePath)));
217
+ return 0;
218
+ }
145
219
  if (command === "compat") {
146
220
  const targetPath = maybePath && !maybePath.startsWith("--") ? maybePath : ".";
147
221
  const compatFlags = maybePath && maybePath.startsWith("--")
@@ -166,8 +240,9 @@ export async function runCli(args, io = defaultIo, options = {}) {
166
240
  }
167
241
  if ((installPreview || applyInstall) &&
168
242
  clientFilter?.toLowerCase() !== "claude-desktop" &&
169
- clientFilter?.toLowerCase() !== "cursor") {
170
- io.writeStderr("--install-preview and --apply require --client claude-desktop or --client cursor.");
243
+ clientFilter?.toLowerCase() !== "cursor" &&
244
+ clientFilter?.toLowerCase() !== "cline") {
245
+ io.writeStderr("--install-preview and --apply require --client claude-desktop, cursor, or cline.");
171
246
  return 2;
172
247
  }
173
248
  if (installPreview && applyInstall) {
@@ -180,20 +255,32 @@ export async function runCli(args, io = defaultIo, options = {}) {
180
255
  }
181
256
  if (installPreview || applyInstall) {
182
257
  try {
183
- const preview = clientFilter?.toLowerCase() === "cursor"
258
+ const normalizedClient = clientFilter?.toLowerCase();
259
+ const preview = normalizedClient === "cursor"
184
260
  ? await buildCursorInstallPreview(targetPath, {
185
261
  env: terminalContext.env,
186
262
  platform: terminalContext.platform
187
263
  })
188
- : await buildClaudeDesktopInstallPreview(targetPath, {
189
- env: terminalContext.env,
190
- platform: terminalContext.platform
191
- });
264
+ : normalizedClient === "cline"
265
+ ? await buildClineInstallPreview(targetPath, {
266
+ env: terminalContext.env,
267
+ platform: terminalContext.platform
268
+ })
269
+ : await buildClaudeDesktopInstallPreview(targetPath, {
270
+ env: terminalContext.env,
271
+ platform: terminalContext.platform
272
+ });
192
273
  const report = applyInstall
193
- ? renderApplyInstallResult(await applyInstallPreview(clientFilter?.toLowerCase() === "cursor" ? "Cursor" : "Claude Desktop", preview))
194
- : clientFilter?.toLowerCase() === "cursor"
274
+ ? renderApplyInstallResult(await applyInstallPreview(normalizedClient === "cursor"
275
+ ? "Cursor"
276
+ : normalizedClient === "cline"
277
+ ? "Cline"
278
+ : "Claude Desktop", preview))
279
+ : normalizedClient === "cursor"
195
280
  ? renderCursorInstallPreview(preview)
196
- : renderClaudeDesktopInstallPreview(preview);
281
+ : normalizedClient === "cline"
282
+ ? renderClineInstallPreview(preview)
283
+ : renderClaudeDesktopInstallPreview(preview);
197
284
  if (outputPath) {
198
285
  await writeFile(outputPath, report, "utf8");
199
286
  }
@@ -260,6 +347,11 @@ export async function runCli(args, io = defaultIo, options = {}) {
260
347
  const outputPath = outputIndex === -1 ? null : normalizedFlags[outputIndex + 1];
261
348
  const configIndex = normalizedFlags.indexOf("--config");
262
349
  const configPath = configIndex === -1 ? null : normalizedFlags[configIndex + 1];
350
+ const profileIndex = normalizedFlags.indexOf("--profile");
351
+ const profileName = profileIndex === -1 ? null : normalizedFlags[profileIndex + 1];
352
+ const checkProfile = parseCheckProfile(profileName);
353
+ const historyIndex = normalizedFlags.indexOf("--history");
354
+ const historyPath = historyIndex === -1 ? null : normalizedFlags[historyIndex + 1];
263
355
  if (outputIndex !== -1 && (!outputPath || outputPath.startsWith("--"))) {
264
356
  io.writeStderr("Missing path after --output.");
265
357
  return 2;
@@ -268,10 +360,27 @@ export async function runCli(args, io = defaultIo, options = {}) {
268
360
  io.writeStderr("Missing path after --config.");
269
361
  return 2;
270
362
  }
363
+ if (profileIndex !== -1 && (!profileName || profileName.startsWith("--"))) {
364
+ io.writeStderr("Missing profile after --profile.");
365
+ return 2;
366
+ }
367
+ if (profileIndex !== -1 && !checkProfile) {
368
+ io.writeStderr("Unknown profile. Supported profiles: ci, strict, publish.");
369
+ return 2;
370
+ }
371
+ if (historyIndex !== -1 && (!historyPath || historyPath.startsWith("--"))) {
372
+ io.writeStderr("Missing path after --history.");
373
+ return 2;
374
+ }
271
375
  if (checkInstalled && (badgeJsonOutput || badgeMarkdownOutput)) {
272
376
  io.writeStderr("Badge output requires a single package target.");
273
377
  return 2;
274
378
  }
379
+ if (checkInstalled && historyPath) {
380
+ io.writeStderr("History output requires a single package target.");
381
+ return 2;
382
+ }
383
+ const effectiveRuntimeProbeEnabled = runtimeProbeEnabled || checkProfile === "publish";
275
384
  const outputPolicy = determineOutputPolicy({
276
385
  jsonOutput: jsonOutput || badgeJsonOutput,
277
386
  markdownOutput: markdownOutput || badgeMarkdownOutput,
@@ -297,11 +406,11 @@ export async function runCli(args, io = defaultIo, options = {}) {
297
406
  checkedPlugins.push({
298
407
  plugin,
299
408
  result: applyDoctorConfig(await runCheckImpl(plugin.rootPath, {
300
- runtime: runtimeProbeEnabled,
301
- runtimeTranscript: runtimeProbeEnabled && verboseRuntime
409
+ runtime: effectiveRuntimeProbeEnabled,
410
+ runtimeTranscript: effectiveRuntimeProbeEnabled && verboseRuntime
302
411
  ? (line) => io.writeStderr(line)
303
412
  : undefined
304
- }), config)
413
+ }), applyCheckProfile(config, checkProfile))
305
414
  });
306
415
  }
307
416
  const report = installedSummary
@@ -310,9 +419,9 @@ export async function runCli(args, io = defaultIo, options = {}) {
310
419
  .map((item) => sarifOutput
311
420
  ? renderSarifReport(item.result)
312
421
  : markdownOutput
313
- ? buildMarkdownReport(item.result, { runtimeProbeEnabled })
422
+ ? buildMarkdownReport(item.result, { runtimeProbeEnabled: effectiveRuntimeProbeEnabled })
314
423
  : jsonOutput
315
- ? renderJsonReport(item.result, { runtimeProbeEnabled })
424
+ ? renderJsonReport(item.result, { runtimeProbeEnabled: effectiveRuntimeProbeEnabled })
316
425
  : renderTextReport(item.result, { ascii: outputPolicy.style === "ascii" }))
317
426
  .join("\n\n");
318
427
  if (outputPath) {
@@ -327,11 +436,11 @@ export async function runCli(args, io = defaultIo, options = {}) {
327
436
  : null;
328
437
  renderer?.start("Validating package");
329
438
  const result = applyDoctorConfig(await runCheckImpl(targetPath, {
330
- runtime: runtimeProbeEnabled,
331
- runtimeTranscript: runtimeProbeEnabled && verboseRuntime
439
+ runtime: effectiveRuntimeProbeEnabled,
440
+ runtimeTranscript: effectiveRuntimeProbeEnabled && verboseRuntime
332
441
  ? (line) => io.writeStderr(line)
333
442
  : undefined
334
- }), await loadDoctorConfig(targetPath, configPath));
443
+ }), applyCheckProfile(await loadDoctorConfig(targetPath, configPath), checkProfile));
335
444
  if (renderer) {
336
445
  if (result.status === "fail") {
337
446
  renderer.stopFailure("Validation failed");
@@ -341,11 +450,11 @@ export async function runCli(args, io = defaultIo, options = {}) {
341
450
  }
342
451
  }
343
452
  const report = markdownOutput
344
- ? buildMarkdownReport(result, { runtimeProbeEnabled })
453
+ ? buildMarkdownReport(result, { runtimeProbeEnabled: effectiveRuntimeProbeEnabled })
345
454
  : sarifOutput
346
455
  ? renderSarifReport(result)
347
456
  : jsonOutput
348
- ? renderJsonReport(result, { runtimeProbeEnabled })
457
+ ? renderJsonReport(result, { runtimeProbeEnabled: effectiveRuntimeProbeEnabled })
349
458
  : badgeJsonOutput
350
459
  ? renderBadgeJson(result)
351
460
  : badgeMarkdownOutput
@@ -354,6 +463,11 @@ export async function runCli(args, io = defaultIo, options = {}) {
354
463
  if (outputPath) {
355
464
  await writeFile(outputPath, report, "utf8");
356
465
  }
466
+ if (historyPath) {
467
+ await appendValidationHistoryEntry(historyPath, result, {
468
+ runtimeProbeEnabled: effectiveRuntimeProbeEnabled
469
+ });
470
+ }
357
471
  io.writeStdout(report);
358
472
  return result.exitCode;
359
473
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-plugin-doctor",
3
- "version": "0.5.0",
3
+ "version": "0.7.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",