codex-plugin-doctor 1.33.0 → 1.34.1

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
@@ -234,10 +234,12 @@ codex-plugin-doctor audit --installed
234
234
  codex-plugin-doctor audit --installed --security --compat
235
235
  codex-plugin-doctor audit --installed --security --compat --json --output local-audit.json
236
236
  codex-plugin-doctor audit --installed --security --compat --cache
237
- codex-plugin-doctor audit --installed --changed --cache
238
- codex-plugin-doctor audit deps .
239
- codex-plugin-doctor audit deps . --json --output dependency-audit.json
240
- codex-plugin-doctor watch .
237
+ codex-plugin-doctor audit --installed --changed --cache
238
+ codex-plugin-doctor audit deps .
239
+ codex-plugin-doctor audit deps . --json --output dependency-audit.json
240
+ codex-plugin-doctor audit deps . --recommend
241
+ codex-plugin-doctor audit deps . --recommend --json --output dependency-remediation.json
242
+ codex-plugin-doctor watch .
241
243
  codex-plugin-doctor watch . --json --output watch-latest.json --debounce-ms 500
242
244
  codex-plugin-doctor init-git-hooks .
243
245
  codex-plugin-doctor init-git-hooks . --json
@@ -302,7 +304,7 @@ codex-plugin-doctor check . --json --runtime --verbose-runtime
302
304
 
303
305
  `audit --installed` runs a local ecosystem audit against every discovered Codex plugin in the installed plugin cache. Add `--security` to include security scorecards, `--compat` to include the all-client compatibility matrix, and `--json --output local-audit.json` when you want a shareable machine-readable report. Add `--cache` to reuse unchanged plugin results between runs; add `--changed` to only report plugins whose fingerprint changed since the last cached audit. Use `--cache-file path/to/audit-cache.json` when CI or scripted runs need an explicit cache location.
304
306
 
305
- `audit deps <path>` runs `npm audit --json` for the target package and normalizes dependency vulnerability results into a stable `schemaVersion: "1.0.0"` report. Use `--json --output dependency-audit.json` when CI needs machine-readable status, vulnerability counts, and the raw npm audit payload.
307
+ `audit deps <path>` runs `npm audit --json` for the target package and normalizes dependency vulnerability results into a stable `schemaVersion: "1.0.0"` report. Use `--json --output dependency-audit.json` when CI needs machine-readable status, vulnerability counts, and the raw npm audit payload. Add `--recommend` to include prioritized remediation actions for direct upgrades, direct no-fix packages, transitive parent updates, and manual transitive review.
306
308
 
307
309
  `watch <path>` continuously validates a plugin package after file changes. Use `--debounce-ms <ms>` to tune noisy repositories, `--runtime` when runtime probes are required, and `--json --output watch-latest.json` when another process should consume the latest validation snapshot.
308
310
 
@@ -400,9 +402,9 @@ jobs:
400
402
  runs-on: ubuntu-latest
401
403
  steps:
402
404
  - uses: actions/checkout@v5
403
- - uses: Esquetta/CodexPluginDoctor@v1.33.0
404
- with:
405
- version: "1.33.0"
405
+ - uses: Esquetta/CodexPluginDoctor@v1.34.1
406
+ with:
407
+ version: "1.34.1"
406
408
  path: .
407
409
  runtime: "true"
408
410
  policy: codex-publish
@@ -30,6 +30,11 @@ function statusFromCheckResult(result) {
30
30
  }
31
31
  return "pass";
32
32
  }
33
+ function isPathWithinRoot(rootPath, candidatePath) {
34
+ const relativePath = path.relative(rootPath, candidatePath);
35
+ return (relativePath === "" ||
36
+ (!relativePath.startsWith("..") && !path.isAbsolute(relativePath)));
37
+ }
33
38
  export async function readMcpConfigPath(targetPath) {
34
39
  const rootPath = path.resolve(targetPath);
35
40
  const directMcpPath = path.join(rootPath, ".mcp.json");
@@ -40,21 +45,38 @@ export async function readMcpConfigPath(targetPath) {
40
45
  if (!(await fileExists(manifestPath))) {
41
46
  return null;
42
47
  }
48
+ let manifest;
43
49
  try {
44
- const manifest = await readJsonFile(manifestPath);
45
- return typeof manifest.mcpServers === "string"
46
- ? path.resolve(rootPath, manifest.mcpServers)
47
- : null;
50
+ manifest = await readJsonFile(manifestPath);
48
51
  }
49
52
  catch {
50
53
  return null;
51
54
  }
55
+ if (typeof manifest.mcpServers !== "string") {
56
+ return null;
57
+ }
58
+ const mcpConfigPath = path.resolve(rootPath, manifest.mcpServers);
59
+ if (!isPathWithinRoot(rootPath, mcpConfigPath)) {
60
+ throw new Error("Manifest MCP config path resolves outside the package root.");
61
+ }
62
+ return mcpConfigPath;
52
63
  }
53
64
  async function hasCodexManifest(targetPath) {
54
65
  return fileExists(path.join(path.resolve(targetPath), ".codex-plugin", "plugin.json"));
55
66
  }
56
67
  async function checkGenericMcp(targetPath) {
57
- const mcpConfigPath = await readMcpConfigPath(targetPath);
68
+ let mcpConfigPath;
69
+ try {
70
+ mcpConfigPath = await readMcpConfigPath(targetPath);
71
+ }
72
+ catch (error) {
73
+ return {
74
+ client: "Generic MCP",
75
+ status: "fail",
76
+ summary: error instanceof Error ? error.message : "MCP config path is unsafe.",
77
+ details: ["Keep `.mcp.json` or manifest `mcpServers` references inside the package root."]
78
+ };
79
+ }
58
80
  if (!mcpConfigPath || !(await fileExists(mcpConfigPath))) {
59
81
  return {
60
82
  client: "Generic MCP",
@@ -12,7 +12,19 @@ export interface DepAuditReport {
12
12
  totalVulnerabilities: number;
13
13
  auditJson: unknown;
14
14
  }
15
+ export interface DepAuditRecommendation {
16
+ packageName: string;
17
+ priority: "critical" | "high" | "moderate" | "low";
18
+ action: "upgrade_direct" | "replace_direct" | "update_parent" | "review_transitive";
19
+ summary: string;
20
+ breakingChangeRisk: "low" | "medium" | "high" | "unknown";
21
+ }
22
+ export declare function buildDepAuditRecommendations(vulnerabilities: DepAuditVulnerability[]): DepAuditRecommendation[];
15
23
  export declare function buildDepAudit(targetPath: string): Promise<DepAuditReport>;
16
- export declare function renderDepAudit(report: DepAuditReport): string;
17
- export declare function renderDepAuditJson(report: DepAuditReport): string;
24
+ export declare function renderDepAudit(report: DepAuditReport, options?: {
25
+ recommendations?: boolean;
26
+ }): string;
27
+ export declare function renderDepAuditJson(report: DepAuditReport, options?: {
28
+ recommendations?: boolean;
29
+ }): string;
18
30
  export declare function renderDepAuditSarif(report: DepAuditReport): string;
@@ -82,6 +82,66 @@ function computeStatus(vulnerabilities) {
82
82
  }
83
83
  return "warn";
84
84
  }
85
+ function severityRank(severity) {
86
+ return severity === "critical" ? 0 :
87
+ severity === "high" ? 1 :
88
+ severity === "moderate" ? 2 : 3;
89
+ }
90
+ function remediationRank(vulnerability) {
91
+ if (vulnerability.isDirect && vulnerability.fixAvailable) {
92
+ return 0;
93
+ }
94
+ if (vulnerability.isDirect) {
95
+ return 1;
96
+ }
97
+ return vulnerability.fixAvailable ? 2 : 3;
98
+ }
99
+ export function buildDepAuditRecommendations(vulnerabilities) {
100
+ return [...vulnerabilities]
101
+ .sort((left, right) => {
102
+ const remediationDelta = remediationRank(left) - remediationRank(right);
103
+ if (remediationDelta !== 0) {
104
+ return remediationDelta;
105
+ }
106
+ return severityRank(left.severity) - severityRank(right.severity);
107
+ })
108
+ .map((vuln) => {
109
+ if (vuln.isDirect && vuln.fixAvailable) {
110
+ return {
111
+ packageName: vuln.name,
112
+ priority: vuln.severity,
113
+ action: "upgrade_direct",
114
+ summary: `Upgrade direct dependency \`${vuln.name}\` with \`npm update ${vuln.name}\` or an explicit package upgrade.`,
115
+ breakingChangeRisk: vuln.severity === "critical" ? "high" : "medium"
116
+ };
117
+ }
118
+ if (vuln.isDirect) {
119
+ return {
120
+ packageName: vuln.name,
121
+ priority: vuln.severity,
122
+ action: "replace_direct",
123
+ summary: `Replace or pin direct dependency \`${vuln.name}\`; npm did not report an automatic fix.`,
124
+ breakingChangeRisk: "high"
125
+ };
126
+ }
127
+ if (vuln.fixAvailable) {
128
+ return {
129
+ packageName: vuln.name,
130
+ priority: vuln.severity,
131
+ action: "update_parent",
132
+ summary: `Update the parent dependency for transitive package \`${vuln.name}\`.`,
133
+ breakingChangeRisk: "medium"
134
+ };
135
+ }
136
+ return {
137
+ packageName: vuln.name,
138
+ priority: vuln.severity,
139
+ action: "review_transitive",
140
+ summary: `Review transitive package \`${vuln.name}\`; npm did not report an automatic fix.`,
141
+ breakingChangeRisk: "unknown"
142
+ };
143
+ });
144
+ }
85
145
  export async function buildDepAudit(targetPath) {
86
146
  const resolvedPath = path.resolve(targetPath);
87
147
  const packageJsonPath = resolvePackageJson(resolvedPath);
@@ -140,7 +200,7 @@ export async function buildDepAudit(targetPath) {
140
200
  auditJson
141
201
  };
142
202
  }
143
- export function renderDepAudit(report) {
203
+ export function renderDepAudit(report, options = {}) {
144
204
  const lines = [
145
205
  "Dependency Vulnerability Audit",
146
206
  "=============================",
@@ -159,15 +219,28 @@ export function renderDepAudit(report) {
159
219
  vuln.severity === "moderate" ? "MODERATE" : "LOW";
160
220
  lines.push(`${tag.padEnd(10)} ${vuln.name}`, ` Direct: ${vuln.isDirect ? "yes" : "no"}`, ` Fix available: ${vuln.fixAvailable ? "yes" : "no"}`, vuln.via.length > 0 ? ` Via: ${vuln.via.join(", ")}` : "", "");
161
221
  }
222
+ if (options.recommendations) {
223
+ const recommendations = buildDepAuditRecommendations(report.vulnerabilities);
224
+ lines.push("Next actions", "------------");
225
+ if (recommendations.length === 0) {
226
+ lines.push("No remediation actions needed.");
227
+ }
228
+ for (const [index, recommendation] of recommendations.entries()) {
229
+ lines.push(`${index + 1}. ${recommendation.summary}`);
230
+ }
231
+ }
162
232
  return lines.join("\n");
163
233
  }
164
- export function renderDepAuditJson(report) {
234
+ export function renderDepAuditJson(report, options = {}) {
165
235
  return JSON.stringify({
166
236
  schemaVersion: "1.0.0",
167
237
  targetPath: report.targetPath,
168
238
  status: report.status,
169
239
  totalVulnerabilities: report.totalVulnerabilities,
170
240
  vulnerabilities: report.vulnerabilities,
241
+ ...(options.recommendations
242
+ ? { recommendations: buildDepAuditRecommendations(report.vulnerabilities) }
243
+ : {}),
171
244
  audit: report.auditJson
172
245
  }, null, 2);
173
246
  }
@@ -193,12 +193,20 @@ async function fileExists(targetPath) {
193
193
  return false;
194
194
  }
195
195
  }
196
+ function isPathWithinRoot(rootPath, candidatePath) {
197
+ const relativePath = path.relative(rootPath, candidatePath);
198
+ return (relativePath === "" ||
199
+ (!relativePath.startsWith("..") && !path.isAbsolute(relativePath)));
200
+ }
196
201
  async function loadMcpServers(discoveredPackage) {
197
202
  const { manifest, rootPath } = discoveredPackage;
198
203
  if (!manifest.mcpServers) {
199
204
  return null;
200
205
  }
201
206
  const mcpConfigPath = path.resolve(rootPath, manifest.mcpServers);
207
+ if (!isPathWithinRoot(rootPath, mcpConfigPath)) {
208
+ return null;
209
+ }
202
210
  const exists = await fileExists(mcpConfigPath);
203
211
  if (!exists) {
204
212
  return null;
package/dist/index.d.ts CHANGED
@@ -25,6 +25,6 @@ export { buildEcosystemAudit, renderEcosystemAudit, renderEcosystemAuditJson, ty
25
25
  export { applyPolicyToDepAudit, applyPolicyToDoctorConfig, applyPolicyToSecurityAudit, parsePolicyPack, policyEnablesRuntime, policyFailsOnWarnings, policyPackNames, type PolicyPackName } from "./policy/policy-packs.js";
26
26
  export { buildGenericMcpDoctor, renderGenericMcpDoctor, renderGenericMcpDoctorJson, type GenericMcpDoctorReport } from "./mcp/generic-mcp-doctor.js";
27
27
  export { watchPlugin, type WatchPluginOptions, type WatchPluginResult } from "./core/watch-plugin.js";
28
- export { buildDepAudit, renderDepAudit, renderDepAuditJson, renderDepAuditSarif, type DepAuditReport, type DepAuditVulnerability } from "./core/dep-audit.js";
28
+ export { buildDepAudit, buildDepAuditRecommendations, renderDepAudit, renderDepAuditJson, renderDepAuditSarif, type DepAuditRecommendation, type DepAuditReport, type DepAuditVulnerability } from "./core/dep-audit.js";
29
29
  export { initGitHooks, removeGitHooks, type InitGitHooksResult, type RemoveGitHooksResult } from "./core/init-git-hooks.js";
30
30
  export declare function runCheck(targetPath: string, options?: CheckOptions): Promise<CheckResult>;
package/dist/index.js CHANGED
@@ -25,7 +25,7 @@ export { buildEcosystemAudit, renderEcosystemAudit, renderEcosystemAuditJson } f
25
25
  export { applyPolicyToDepAudit, applyPolicyToDoctorConfig, applyPolicyToSecurityAudit, parsePolicyPack, policyEnablesRuntime, policyFailsOnWarnings, policyPackNames } from "./policy/policy-packs.js";
26
26
  export { buildGenericMcpDoctor, renderGenericMcpDoctor, renderGenericMcpDoctorJson } from "./mcp/generic-mcp-doctor.js";
27
27
  export { watchPlugin } from "./core/watch-plugin.js";
28
- export { buildDepAudit, renderDepAudit, renderDepAuditJson, renderDepAuditSarif } from "./core/dep-audit.js";
28
+ export { buildDepAudit, buildDepAuditRecommendations, renderDepAudit, renderDepAuditJson, renderDepAuditSarif } from "./core/dep-audit.js";
29
29
  export { initGitHooks, removeGitHooks } from "./core/init-git-hooks.js";
30
30
  export async function runCheck(targetPath, options = {}) {
31
31
  return validatePlugin(targetPath, options);
package/dist/run-cli.js CHANGED
@@ -78,7 +78,7 @@ const defaultIo = {
78
78
  }
79
79
  };
80
80
  function printUsage(io) {
81
- io.writeStderr("Usage: codex-plugin-doctor check <path|--installed> [filter] [--policy codex-publish|mcp-strict|security] [--compat] [--json|--markdown|--badge-json|--badge-markdown] [--output <path>] [--history <path>] [--runtime] [--require-runtime-approval --runtime-approval-digest <digest>] [--verbose-runtime] [--explain] [--no-animations] [--ascii]\n codex-plugin-doctor audit --installed [filter] [--policy codex-publish|mcp-strict|security] [--security] [--compat] [--json] [--output <path>] [--cache] [--changed]\n codex-plugin-doctor audit deps <path> [--policy codex-publish|mcp-strict|security] [--json|--sarif] [--output <path>]\n codex-plugin-doctor mcp <path> [--json] [--output <path>]\n codex-plugin-doctor security <path> [--policy security] [--json|--scorecard]\n codex-plugin-doctor compat <path> [--all|--client <client>] [--json] [--scorecard] [--output <path>] [--install-preview|--apply --backup]\n codex-plugin-doctor suppress add <path> [--fingerprint <sha256> --reason <text> --expires-at YYYY-MM-DD] [--config <path>] [--json]\n codex-plugin-doctor suppress list <path> [--config <path>] [--json]\n codex-plugin-doctor suppress remove <path> [--fingerprint <sha256>|--index <n>] [--config <path>] [--json]\n codex-plugin-doctor fix <path> (--dry-run|--interactive --backup|--apply --backup)\n codex-plugin-doctor history <history.jsonl> [--json] [--fail-on-regression]\n codex-plugin-doctor watch <path> [--runtime] [--json] [--output <path>] [--debounce-ms <ms>] [--max-iterations <n>] [--fail-fast] [--accumulate-json <path>]\n codex-plugin-doctor doctor [npm <package>|contract|corpus|runtime-plan <path> [--json|--markdown] [--output <path>]|runtime-policy <path> [--json] [--output <path>]|review-bundle <path> --output <dir> --sign-key-env NAME [--json] [--allow-dirty] [--allow-untagged]|review-bundle verify <bundle-dir> --target <path> --sign-key-env NAME [--json] [--output <path>] [--failures-only]|review-bundle diff --before <dir> --after <dir> [--json]|attest <path> [--sign-key-env NAME]|attest verify <attestation.json> --target <path> --sign-key-env NAME|release-evidence <path> --sign-key-env NAME [--allow-dirty] [--allow-untagged] [--require-runtime-approval --runtime-approval-digest <digest>]|release-evidence verify <evidence.json> --target <path> --sign-key-env NAME|release-evidence asset <path> --tag <tag> --output <evidence.json> --sign-key-env NAME [--upload]|mcp <path>|inspector <path>|diff --before <path> --after <path>|recommend <path>|trust <path>|perf <path> [--max-total-ms <ms>] [--max-stage-ms stage=ms]|export --bundle <path>|snapshot|clients|--json|--update-check]\n codex-plugin-doctor init [path] [--template skill-only|mcp-stdio|mcp-http|full-runtime]\n codex-plugin-doctor init-ci [path]\n codex-plugin-doctor init-git-hooks [path] [--force] [--json]\n codex-plugin-doctor init-git-hooks [path] --remove [--json]\n codex-plugin-doctor self-test\n codex-plugin-doctor list --installed\n codex-plugin-doctor explain <finding-id>\n codex-plugin-doctor --version\n\nFirst run:\n codex-plugin-doctor doctor\n codex-plugin-doctor self-test\n codex-plugin-doctor init my-plugin\n codex-plugin-doctor check . --runtime --explain");
81
+ io.writeStderr("Usage: codex-plugin-doctor check <path|--installed> [filter] [--policy codex-publish|mcp-strict|security] [--compat] [--json|--markdown|--badge-json|--badge-markdown] [--output <path>] [--history <path>] [--runtime] [--require-runtime-approval --runtime-approval-digest <digest>] [--verbose-runtime] [--explain] [--no-animations] [--ascii]\n codex-plugin-doctor audit --installed [filter] [--policy codex-publish|mcp-strict|security] [--security] [--compat] [--json] [--output <path>] [--cache] [--changed]\n codex-plugin-doctor audit deps <path> [--policy codex-publish|mcp-strict|security] [--recommend] [--json|--sarif] [--output <path>]\n codex-plugin-doctor mcp <path> [--json] [--output <path>]\n codex-plugin-doctor security <path> [--policy security] [--json|--scorecard]\n codex-plugin-doctor compat <path> [--all|--client <client>] [--json] [--scorecard] [--output <path>] [--install-preview|--apply --backup]\n codex-plugin-doctor suppress add <path> [--fingerprint <sha256> --reason <text> --expires-at YYYY-MM-DD] [--config <path>] [--json]\n codex-plugin-doctor suppress list <path> [--config <path>] [--json]\n codex-plugin-doctor suppress remove <path> [--fingerprint <sha256>|--index <n>] [--config <path>] [--json]\n codex-plugin-doctor fix <path> (--dry-run|--interactive --backup|--apply --backup)\n codex-plugin-doctor history <history.jsonl> [--json] [--fail-on-regression]\n codex-plugin-doctor watch <path> [--runtime] [--json] [--output <path>] [--debounce-ms <ms>] [--max-iterations <n>] [--fail-fast] [--accumulate-json <path>]\n codex-plugin-doctor doctor [npm <package>|contract|corpus|runtime-plan <path> [--json|--markdown] [--output <path>]|runtime-policy <path> [--json] [--output <path>]|review-bundle <path> --output <dir> --sign-key-env NAME [--json] [--allow-dirty] [--allow-untagged]|review-bundle verify <bundle-dir> --target <path> --sign-key-env NAME [--json] [--output <path>] [--failures-only]|review-bundle diff --before <dir> --after <dir> [--json]|attest <path> [--sign-key-env NAME]|attest verify <attestation.json> --target <path> --sign-key-env NAME|release-evidence <path> --sign-key-env NAME [--allow-dirty] [--allow-untagged] [--require-runtime-approval --runtime-approval-digest <digest>]|release-evidence verify <evidence.json> --target <path> --sign-key-env NAME|release-evidence asset <path> --tag <tag> --output <evidence.json> --sign-key-env NAME [--upload]|mcp <path>|inspector <path>|diff --before <path> --after <path>|recommend <path>|trust <path>|perf <path> [--max-total-ms <ms>] [--max-stage-ms stage=ms]|export --bundle <path>|snapshot|clients|--json|--update-check]\n codex-plugin-doctor init [path] [--template skill-only|mcp-stdio|mcp-http|full-runtime]\n codex-plugin-doctor init-ci [path]\n codex-plugin-doctor init-git-hooks [path] [--force] [--json]\n codex-plugin-doctor init-git-hooks [path] --remove [--json]\n codex-plugin-doctor self-test\n codex-plugin-doctor list --installed\n codex-plugin-doctor explain <finding-id>\n codex-plugin-doctor --version\n\nFirst run:\n codex-plugin-doctor doctor\n codex-plugin-doctor self-test\n codex-plugin-doctor init my-plugin\n codex-plugin-doctor check . --runtime --explain");
82
82
  io.writeStderr("Suppression governance: codex-plugin-doctor suppress list <path> [--fail-on-expired] [--fail-on-invalid] [--warn-expiring-within-days <days>]\n codex-plugin-doctor suppress prune <path> [--apply] [--json]");
83
83
  }
84
84
  const suppressUsageText = [
@@ -1964,6 +1964,7 @@ export async function runCli(args, io = defaultIo, options = {}) {
1964
1964
  : remainingArgs.slice(1);
1965
1965
  const jsonOutput = depsFlags.includes("--json");
1966
1966
  const sarifOutput = depsFlags.includes("--sarif");
1967
+ const recommendationsOutput = depsFlags.includes("--recommend");
1967
1968
  const outputIndex = depsFlags.indexOf("--output");
1968
1969
  const outputPath = outputIndex === -1 ? null : depsFlags[outputIndex + 1];
1969
1970
  const policyIndex = depsFlags.indexOf("--policy");
@@ -1992,8 +1993,8 @@ export async function runCli(args, io = defaultIo, options = {}) {
1992
1993
  const renderedReport = sarifOutput
1993
1994
  ? renderDepAuditSarif(report)
1994
1995
  : jsonOutput
1995
- ? renderDepAuditJson(report)
1996
- : renderDepAudit(report);
1996
+ ? renderDepAuditJson(report, { recommendations: recommendationsOutput })
1997
+ : renderDepAudit(report, { recommendations: recommendationsOutput });
1997
1998
  if (outputPath) {
1998
1999
  await writeFile(outputPath, renderedReport, "utf8");
1999
2000
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-plugin-doctor",
3
- "version": "1.33.0",
3
+ "version": "1.34.1",
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",