codex-plugin-doctor 1.0.0 → 1.0.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
@@ -196,9 +196,10 @@ codex-plugin-doctor doctor corpus
196
196
  codex-plugin-doctor doctor corpus --json --output validation-corpus.json
197
197
  codex-plugin-doctor doctor npm <published-plugin-package>
198
198
  codex-plugin-doctor doctor npm <published-plugin-package> --json --output npm-preinstall.json
199
- codex-plugin-doctor doctor attest .
200
- codex-plugin-doctor doctor attest . --json --output attestation.json
201
- codex-plugin-doctor doctor inspector .
199
+ codex-plugin-doctor doctor attest .
200
+ codex-plugin-doctor doctor attest . --json --output attestation.json
201
+ codex-plugin-doctor doctor attest . --json --sign-key-env CODEX_PLUGIN_DOCTOR_SIGNING_KEY --output attestation.json
202
+ codex-plugin-doctor doctor inspector .
202
203
  codex-plugin-doctor doctor inspector . --server context7 --json --output inspector-command.json
203
204
  codex-plugin-doctor doctor diff --before ./old-plugin --after ./new-plugin
204
205
  codex-plugin-doctor doctor diff --before ./old-plugin --after ./new-plugin --json --output risk-diff.json
@@ -206,9 +207,12 @@ codex-plugin-doctor doctor recommend .
206
207
  codex-plugin-doctor doctor recommend . --json --output recommendations.json
207
208
  codex-plugin-doctor doctor trust .
208
209
  codex-plugin-doctor doctor trust . --json --output trust-score.json
209
- codex-plugin-doctor doctor perf .
210
- codex-plugin-doctor doctor perf . --json --output perf.json
211
- codex-plugin-doctor doctor export --bundle .
210
+ codex-plugin-doctor doctor perf .
211
+ codex-plugin-doctor doctor perf . --json --output perf.json
212
+ codex-plugin-doctor doctor perf . --max-total-ms 2500 --max-stage-ms validation=500
213
+ codex-plugin-doctor doctor mcp .
214
+ codex-plugin-doctor doctor mcp . --json --output mcp-healthcheck.json
215
+ codex-plugin-doctor doctor export --bundle .
212
216
  codex-plugin-doctor doctor export --bundle . --output doctor-bundle.json
213
217
  codex-plugin-doctor doctor snapshot
214
218
  codex-plugin-doctor doctor snapshot --json
@@ -276,7 +280,7 @@ codex-plugin-doctor check . --json --runtime --verbose-runtime
276
280
 
277
281
  `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`.
278
282
 
279
- `doctor` checks the local environment, including package version, platform, Node version, npm global prefix, Codex home, and Codex plugin cache visibility. The text output also includes recommended next commands for self-test, installed plugin discovery, runtime checks, compatibility scoring, and CI setup. `doctor contract` publishes the machine-readable output contract, including public JSON schema surfaces, stable-through-1.0 compatibility metadata, and a frozen rule catalog digest. Add `--json` for automation or `--output output-contract.json` to write the contract to disk. `doctor corpus` runs the bundled validation corpus against healthy runtime, risky security, and starter skill packages, then reports whether each case matched its expected outcome. Add `--json` for automation or `--output validation-corpus.json` to write the corpus report to disk. `doctor npm <package>` runs a preinstall scan by packing the npm package with scripts disabled, extracting the publish tarball, and running validation, security, trust, and recommendation checks against the shipped contents. Use a published Codex plugin package as the target; scanning `codex-plugin-doctor` itself intentionally reports a missing plugin manifest because this CLI package is not a plugin package. Add `--json` for automation or `--output npm-preinstall.json` to write the report to disk. `doctor attest <path>` creates a deterministic local attestation with a package fingerprint, report digest, validation/security/compatibility/trust summary, and unsigned verification metadata. Add `--json` for automation or `--output attestation.json` to write the artifact to disk. `doctor inspector <path>` builds a safe MCP Inspector launch command from a packaged `.mcp.json` file without starting the Inspector proxy automatically. Use `--server <name>` when the package contains multiple MCP server entries. `doctor diff --before <path> --after <path>` compares two package roots and reports new findings, resolved findings, trust score delta, and whether risk increased. `doctor recommend <path>` turns validation, security, and compatibility signals into a prioritized action plan with blocker, high, medium, and info actions. Add `--json` for automation or `--output recommendations.json` to write the report to disk. `doctor trust <path>` creates a local trust score from package lifecycle scripts, dependency specs, and MCP security findings. Use it before release when you want supply-chain risks summarized as one score. `doctor perf <path>` profiles the shared package analysis pipeline and reports per-stage durations for validation, config, security, compatibility, trust, recommendations, and total runtime. `doctor export --bundle <path>` creates a redacted operator handoff bundle that includes validation JSON, security scorecard data, compatibility matrix, recommendations, and trust score in one file. `doctor snapshot` creates a redacted diagnostics bundle with environment health, client config readiness, installed plugin metadata, and next commands. Add `--json` for machine-readable output or `--output doctor-snapshot.json` to write the bundle to disk. `doctor clients` reports local Codex, Claude Desktop, Cursor, Cline, and Windsurf config readiness. `doctor --update-check` compares the installed CLI version with the latest npm version and prints the upgrade command when a newer release is available.
283
+ `doctor` checks the local environment, including package version, platform, Node version, npm global prefix, Codex home, and Codex plugin cache visibility. The text output also includes recommended next commands for self-test, installed plugin discovery, runtime checks, compatibility scoring, and CI setup. `doctor contract` publishes the machine-readable output contract, including public JSON schema surfaces, stable-through-1.0 compatibility metadata, and a frozen rule catalog digest. Add `--json` for automation or `--output output-contract.json` to write the contract to disk. `doctor corpus` runs the bundled validation corpus against healthy runtime, risky security, starter skill, and generic MCP packages, then reports whether each case matched its expected outcome. Add `--json` for automation or `--output validation-corpus.json` to write the corpus report to disk. `doctor npm <package>` runs a preinstall scan by packing the npm package with scripts disabled, extracting the publish tarball, and running validation, security, trust, and recommendation checks against the shipped contents. Use a published Codex plugin package as the target; scanning `codex-plugin-doctor` itself intentionally reports a missing plugin manifest because this CLI package is not a plugin package. Add `--json` for automation or `--output npm-preinstall.json` to write the report to disk. `doctor attest <path>` creates a deterministic local attestation with a package fingerprint, report digest, validation/security/compatibility/trust summary, and unsigned verification metadata. Add `--sign-key-env NAME` to attach a local HMAC-SHA256 signature without printing the secret, or `--json --output attestation.json` to write the artifact to disk. `doctor inspector <path>` builds a safe MCP Inspector launch command from a packaged `.mcp.json` file without starting the Inspector proxy automatically. Use `--server <name>` when the package contains multiple MCP server entries. `doctor diff --before <path> --after <path>` compares two package roots and reports new findings, resolved findings, trust score delta, and whether risk increased. `doctor recommend <path>` turns validation, security, and compatibility signals into a prioritized action plan with blocker, high, medium, and info actions. Add `--json` for automation or `--output recommendations.json` to write the report to disk. `doctor trust <path>` creates a local trust score from package lifecycle scripts, dependency specs, and MCP security findings. Use it before release when you want supply-chain risks summarized as one score. `doctor perf <path>` profiles the shared package analysis pipeline and reports per-stage durations for validation, config, security, compatibility, trust, recommendations, and total runtime. Add `--max-total-ms <ms>` or repeatable `--max-stage-ms stage=ms` to fail CI when a budget is exceeded. `doctor mcp <path>` exposes the generic MCP static health report under the doctor command family without starting local MCP servers. `doctor export --bundle <path>` creates a redacted operator handoff bundle that includes validation JSON, security scorecard data, compatibility matrix, recommendations, and trust score in one file. `doctor snapshot` creates a redacted diagnostics bundle with environment health, client config readiness, installed plugin metadata, and next commands. Add `--json` for machine-readable output or `--output doctor-snapshot.json` to write the bundle to disk. `doctor clients` reports local Codex, Claude Desktop, Cursor, Cline, and Windsurf config readiness. `doctor --update-check` compares the installed CLI version with the latest npm version and prints the upgrade command when a newer release is available.
280
284
 
281
285
  `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.
282
286
 
@@ -342,9 +346,9 @@ jobs:
342
346
  runs-on: ubuntu-latest
343
347
  steps:
344
348
  - uses: actions/checkout@v5
345
- - uses: Esquetta/CodexPluginDoctor@v1.0.0
346
- with:
347
- version: "1.0.0"
349
+ - uses: Esquetta/CodexPluginDoctor@v1.0.1
350
+ with:
351
+ version: "1.0.1"
348
352
  path: .
349
353
  runtime: "true"
350
354
  policy: codex-publish
@@ -50,12 +50,24 @@ export interface DoctorAttestation {
50
50
  recomputeCommand: string;
51
51
  notes: string[];
52
52
  };
53
- signature: {
54
- status: "unsigned";
55
- reason: string;
56
- };
53
+ signature: DoctorAttestationSignature;
54
+ }
55
+ export type DoctorAttestationSignature = {
56
+ status: "unsigned";
57
+ reason: string;
58
+ } | {
59
+ status: "signed";
60
+ algorithm: "hmac-sha256";
61
+ digest: string;
62
+ payloadDigest: string;
63
+ keyHint: string;
64
+ };
65
+ export interface BuildDoctorAttestationOptions {
66
+ signingKey?: string;
67
+ signingKeyHint?: string;
68
+ recomputeKeyEnv?: string;
57
69
  }
58
- export declare function buildDoctorAttestation(targetPath: string): Promise<DoctorAttestation>;
70
+ export declare function buildDoctorAttestation(targetPath: string, options?: BuildDoctorAttestationOptions): Promise<DoctorAttestation>;
59
71
  export declare function renderDoctorAttestationJson(attestation: DoctorAttestation): string;
60
72
  export declare function renderDoctorAttestation(attestation: DoctorAttestation, options?: {
61
73
  outputPath?: string | null;
@@ -1,4 +1,4 @@
1
- import { createHash } from "node:crypto";
1
+ import { createHash, createHmac } from "node:crypto";
2
2
  import { readdir, readFile, stat } from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { buildDoctorRecommendationsFromAnalysis, buildPackageAnalysis } from "./package-analysis.js";
@@ -181,7 +181,7 @@ function buildSummary(analysis) {
181
181
  }
182
182
  };
183
183
  }
184
- export async function buildDoctorAttestation(targetPath) {
184
+ export async function buildDoctorAttestation(targetPath, options = {}) {
185
185
  const analysis = await buildPackageAnalysis(targetPath);
186
186
  const [packageFingerprint, packageJson, manifest] = await Promise.all([
187
187
  buildPackageFingerprint(analysis.targetPath),
@@ -189,30 +189,58 @@ export async function buildDoctorAttestation(targetPath) {
189
189
  readManifestSubject(analysis.targetPath)
190
190
  ]);
191
191
  const reportDigest = sha256(stableStringify(buildReportDigestPayload(analysis, packageFingerprint)));
192
+ const subject = buildSubject(analysis, packageJson, manifest);
193
+ const summary = buildSummary(analysis);
194
+ const signingPayload = {
195
+ schemaVersion: "1.0.0",
196
+ kind: "doctor.attestation.signature.v1",
197
+ version: packageVersion,
198
+ subject,
199
+ packageFingerprint,
200
+ reportDigest,
201
+ summary
202
+ };
203
+ const signature = options.signingKey
204
+ ? {
205
+ status: "signed",
206
+ algorithm: "hmac-sha256",
207
+ digest: `sha256:${createHmac("sha256", options.signingKey)
208
+ .update(stableStringify(signingPayload))
209
+ .digest("hex")}`,
210
+ payloadDigest: sha256(stableStringify(signingPayload)),
211
+ keyHint: options.signingKeyHint ?? "inline"
212
+ }
213
+ : {
214
+ status: "unsigned",
215
+ reason: "No signing key was provided. Use --sign-key-env for reproducible local signing."
216
+ };
217
+ const recomputeCommand = signature.status === "signed"
218
+ ? `codex-plugin-doctor doctor attest ${analysis.targetPath} --json --sign-key-env ${options.recomputeKeyEnv ?? "CODEX_PLUGIN_DOCTOR_SIGNING_KEY"}`
219
+ : `codex-plugin-doctor doctor attest ${analysis.targetPath} --json`;
192
220
  return {
193
221
  schemaVersion: "1.0.0",
194
222
  kind: "doctor.attestation",
195
223
  generatedAt: analysis.generatedAt,
196
224
  version: packageVersion,
197
225
  targetPath: analysis.targetPath,
198
- subject: buildSubject(analysis, packageJson, manifest),
226
+ subject,
199
227
  packageFingerprint,
200
228
  reportDigest: {
201
229
  algorithm: "sha256",
202
230
  digest: reportDigest
203
231
  },
204
- summary: buildSummary(analysis),
232
+ summary,
205
233
  verification: {
206
- recomputeCommand: `codex-plugin-doctor doctor attest ${analysis.targetPath} --json`,
234
+ recomputeCommand,
207
235
  notes: [
208
236
  "Compare packageFingerprint.digest to confirm the same local package contents.",
209
- "Compare reportDigest.digest to confirm the same validation, security, compatibility, trust, and recommendation signals."
237
+ "Compare reportDigest.digest to confirm the same validation, security, compatibility, trust, and recommendation signals.",
238
+ signature.status === "signed"
239
+ ? "Recompute the signed attestation with the same HMAC key stored in an environment variable."
240
+ : "Signing is optional and local; unsigned attestations remain deterministic without key material."
210
241
  ]
211
242
  },
212
- signature: {
213
- status: "unsigned",
214
- reason: "v0.17 creates deterministic local attestations without key management or hosted signing."
215
- }
243
+ signature
216
244
  };
217
245
  }
218
246
  export function renderDoctorAttestationJson(attestation) {
@@ -227,7 +255,7 @@ export function renderDoctorAttestation(attestation, options = {}) {
227
255
  `Status: ${attestation.summary.status.toUpperCase()}`,
228
256
  `Package fingerprint: ${attestation.packageFingerprint.digest}`,
229
257
  `Report digest: ${attestation.reportDigest.digest}`,
230
- `Signature: ${attestation.signature.status}`
258
+ `Signature: ${attestation.signature.status}${attestation.signature.status === "signed" ? ` (${attestation.signature.algorithm})` : ""}`
231
259
  ];
232
260
  if (options.outputPath) {
233
261
  lines.push(`Output: ${options.outputPath}`);
@@ -26,7 +26,8 @@ const publicSchemaDefinitions = [
26
26
  {
27
27
  id: "doctor.mcp.json",
28
28
  command: "codex-plugin-doctor mcp <path> --json",
29
- required: ["schemaVersion", "targetPath", "status", "summary", "findings"]
29
+ outputKind: "doctor.mcp.healthcheck",
30
+ required: ["schemaVersion", "kind", "generatedAt", "targetPath", "status", "serverCount", "findings", "security", "compatibility"]
30
31
  },
31
32
  {
32
33
  id: "doctor.audit.json",
@@ -68,7 +69,7 @@ const publicSchemaDefinitions = [
68
69
  id: "doctor.performance.json",
69
70
  command: "codex-plugin-doctor doctor perf <path> --json",
70
71
  outputKind: "doctor.perf",
71
- required: ["schemaVersion", "kind", "generatedAt", "targetPath", "summary", "stages"]
72
+ required: ["schemaVersion", "kind", "generatedAt", "targetPath", "status", "exitCode", "summary", "stages", "thresholds"]
72
73
  },
73
74
  {
74
75
  id: "doctor.export.bundle.json",
@@ -13,8 +13,8 @@ export interface DoctorPerformanceReport {
13
13
  generatedAt: string;
14
14
  kind: "doctor.perf";
15
15
  targetPath: string;
16
- status: "pass";
17
- exitCode: 0;
16
+ status: "pass" | "fail";
17
+ exitCode: 0 | 1;
18
18
  summary: {
19
19
  stageCount: number;
20
20
  slowestStage: DoctorPerformanceStageName;
@@ -23,12 +23,25 @@ export interface DoctorPerformanceReport {
23
23
  securityStatus: "pass" | "warn" | "fail";
24
24
  trustScore: number;
25
25
  compatibilityFailures: number;
26
+ thresholdFailures: number;
26
27
  };
27
28
  stages: DoctorPerformanceStage[];
29
+ thresholds: DoctorPerformanceThresholdResult[];
28
30
  }
29
31
  export interface BuildDoctorPerformanceReportOptions {
30
32
  environment?: CompatibilityEnvironment;
31
33
  runCheck?: (targetPath: string) => Promise<CheckResult>;
34
+ thresholds?: DoctorPerformanceThresholdOptions;
35
+ }
36
+ export interface DoctorPerformanceThresholdOptions {
37
+ totalMs?: number;
38
+ stages?: Partial<Record<DoctorPerformanceStageName, number>>;
39
+ }
40
+ export interface DoctorPerformanceThresholdResult {
41
+ stage: DoctorPerformanceStageName;
42
+ limitMs: number;
43
+ actualMs: number;
44
+ status: "pass" | "fail";
32
45
  }
33
46
  export declare function buildDoctorPerformanceReport(targetPath: string, options?: BuildDoctorPerformanceReportOptions): Promise<DoctorPerformanceReport>;
34
47
  export declare function renderDoctorPerformanceReportJson(report: DoctorPerformanceReport): string;
@@ -24,6 +24,34 @@ function slowestStage(stages) {
24
24
  .filter((stage) => stage.name !== "total")
25
25
  .reduce((slowest, stage) => (stage.durationMs > slowest.durationMs ? stage : slowest), stages[0]).name;
26
26
  }
27
+ function evaluateThresholds(stages, thresholds = {}) {
28
+ const thresholdResults = [];
29
+ const stageByName = new Map(stages.map((stage) => [stage.name, stage]));
30
+ if (thresholds.totalMs !== undefined) {
31
+ const totalStage = stageByName.get("total");
32
+ if (totalStage) {
33
+ thresholdResults.push({
34
+ stage: "total",
35
+ limitMs: thresholds.totalMs,
36
+ actualMs: totalStage.durationMs,
37
+ status: totalStage.durationMs > thresholds.totalMs ? "fail" : "pass"
38
+ });
39
+ }
40
+ }
41
+ for (const [stageName, limitMs] of Object.entries(thresholds.stages ?? {})) {
42
+ const stage = stageByName.get(stageName);
43
+ if (!stage) {
44
+ continue;
45
+ }
46
+ thresholdResults.push({
47
+ stage: stageName,
48
+ limitMs,
49
+ actualMs: stage.durationMs,
50
+ status: stage.durationMs > limitMs ? "fail" : "pass"
51
+ });
52
+ }
53
+ return thresholdResults;
54
+ }
27
55
  export async function buildDoctorPerformanceReport(targetPath, options = {}) {
28
56
  const timings = [];
29
57
  const startedAt = performance.now();
@@ -94,13 +122,16 @@ export async function buildDoctorPerformanceReport(targetPath, options = {}) {
94
122
  durationMs: roundDuration(totalDurationMs)
95
123
  };
96
124
  });
125
+ const thresholdResults = evaluateThresholds(stages, options.thresholds);
126
+ const thresholdFailures = thresholdResults
127
+ .filter((threshold) => threshold.status === "fail").length;
97
128
  return {
98
129
  schemaVersion: "1.0.0",
99
130
  generatedAt: analysis.generatedAt,
100
131
  kind: "doctor.perf",
101
132
  targetPath: analysis.targetPath,
102
- status: "pass",
103
- exitCode: 0,
133
+ status: thresholdFailures > 0 ? "fail" : "pass",
134
+ exitCode: thresholdFailures > 0 ? 1 : 0,
104
135
  summary: {
105
136
  stageCount: stages.length,
106
137
  slowestStage: slowestStage(stages),
@@ -108,9 +139,11 @@ export async function buildDoctorPerformanceReport(targetPath, options = {}) {
108
139
  validationStatus: analysis.validation.status,
109
140
  securityStatus: analysis.security.status,
110
141
  trustScore: analysis.trust.score,
111
- compatibilityFailures
142
+ compatibilityFailures,
143
+ thresholdFailures
112
144
  },
113
- stages
145
+ stages,
146
+ thresholds: thresholdResults
114
147
  };
115
148
  }
116
149
  export function renderDoctorPerformanceReportJson(report) {
@@ -137,5 +170,12 @@ export function renderDoctorPerformanceReport(report, options = {}) {
137
170
  const count = stage.itemCount === undefined ? "" : `, items: ${stage.itemCount}`;
138
171
  lines.push(`${stage.name}: ${stage.durationMs}ms${status}${count}`);
139
172
  }
173
+ if (report.thresholds.length > 0) {
174
+ lines.push("", "Thresholds", "----------");
175
+ for (const threshold of report.thresholds) {
176
+ lines.push(`${threshold.stage}: ${threshold.actualMs}ms <= ${threshold.limitMs}ms ` +
177
+ `(${threshold.status.toUpperCase()})`);
178
+ }
179
+ }
140
180
  return lines.join("\n");
141
181
  }
@@ -7,6 +7,7 @@ export interface ValidationCorpusCaseDefinition {
7
7
  sourceType: "bundled-example";
8
8
  relativePath: string;
9
9
  runtimeEnabled: boolean;
10
+ mode?: "codex-plugin" | "generic-mcp";
10
11
  expected: {
11
12
  validationStatus: ValidationStatus;
12
13
  findingIds?: string[];
@@ -3,6 +3,7 @@ import { fileURLToPath } from "node:url";
3
3
  import { buildPackageAnalysis } from "./package-analysis.js";
4
4
  import { validatePlugin } from "./validate-plugin.js";
5
5
  import { matrixExitCode } from "../compatibility/compatibility-matrix.js";
6
+ import { buildGenericMcpDoctor } from "../mcp/generic-mcp-doctor.js";
6
7
  import { packageVersion } from "../version.js";
7
8
  const bundledCorpusCases = [
8
9
  {
@@ -38,6 +39,18 @@ const bundledCorpusCases = [
38
39
  expected: {
39
40
  validationStatus: "pass"
40
41
  }
42
+ },
43
+ {
44
+ id: "bundled-generic-mcp",
45
+ label: "Bundled generic MCP package",
46
+ profile: "generic-mcp",
47
+ sourceType: "bundled-example",
48
+ relativePath: "examples/codex-doctor-generic-mcp",
49
+ runtimeEnabled: false,
50
+ mode: "generic-mcp",
51
+ expected: {
52
+ validationStatus: "pass"
53
+ }
41
54
  }
42
55
  ];
43
56
  function resolvePackageRoot() {
@@ -48,6 +61,36 @@ function includesExpectedFindings(actualFindingIds, expectedFindingIds) {
48
61
  }
49
62
  async function runCorpusCase(caseDefinition, options) {
50
63
  const targetPath = path.resolve(resolvePackageRoot(), caseDefinition.relativePath);
64
+ const expectedFindingIds = [...(caseDefinition.expected.findingIds ?? [])].sort();
65
+ if (caseDefinition.mode === "generic-mcp") {
66
+ const report = await buildGenericMcpDoctor(targetPath, options.environment);
67
+ const findingIds = report.findings.map((finding) => finding.id).sort();
68
+ const compatibilityFailedClients = report.compatibility.results
69
+ .filter((result) => result.status === "fail")
70
+ .map((result) => result.client);
71
+ const expectationMatched = report.status === caseDefinition.expected.validationStatus &&
72
+ includesExpectedFindings(findingIds, expectedFindingIds);
73
+ return {
74
+ id: caseDefinition.id,
75
+ label: caseDefinition.label,
76
+ profile: caseDefinition.profile,
77
+ sourceType: caseDefinition.sourceType,
78
+ targetPath,
79
+ runtimeEnabled: caseDefinition.runtimeEnabled,
80
+ expected: {
81
+ validationStatus: caseDefinition.expected.validationStatus,
82
+ findingIds: expectedFindingIds
83
+ },
84
+ actual: {
85
+ validationStatus: report.status,
86
+ findingIds,
87
+ securityStatus: report.security.status,
88
+ trustStatus: "pass",
89
+ compatibilityFailedClients
90
+ },
91
+ expectationMatched
92
+ };
93
+ }
51
94
  const analysis = await buildPackageAnalysis(targetPath, {
52
95
  environment: options.environment,
53
96
  runCheck: (pathToCheck) => validatePlugin(pathToCheck, {
@@ -55,7 +98,6 @@ async function runCorpusCase(caseDefinition, options) {
55
98
  })
56
99
  });
57
100
  const findingIds = analysis.validation.findings.map((finding) => finding.id).sort();
58
- const expectedFindingIds = [...(caseDefinition.expected.findingIds ?? [])].sort();
59
101
  const compatibilityFailedClients = analysis.compatibility.results
60
102
  .filter((result) => result.status === "fail")
61
103
  .map((result) => result.client);
@@ -125,6 +125,7 @@ export async function buildGenericMcpDoctor(targetPath, environment = {}) {
125
125
  export function renderGenericMcpDoctorJson(report) {
126
126
  return JSON.stringify({
127
127
  schemaVersion: "1.0.0",
128
+ kind: "doctor.mcp.healthcheck",
128
129
  generatedAt: new Date().toISOString(),
129
130
  ...report
130
131
  }, null, 2);
package/dist/run-cli.js CHANGED
@@ -69,7 +69,71 @@ const defaultIo = {
69
69
  }
70
70
  };
71
71
  function printUsage(io) {
72
- 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] [--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 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 fix <path> (--dry-run|--interactive --backup|--apply --backup)\n codex-plugin-doctor history <history.jsonl> [--json] [--fail-on-regression]\n codex-plugin-doctor doctor [npm <package>|contract|corpus|attest <path>|inspector <path>|diff --before <path> --after <path>|recommend <path>|trust <path>|perf <path>|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 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");
72
+ 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] [--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 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 fix <path> (--dry-run|--interactive --backup|--apply --backup)\n codex-plugin-doctor history <history.jsonl> [--json] [--fail-on-regression]\n codex-plugin-doctor doctor [npm <package>|contract|corpus|attest <path> [--sign-key-env NAME]|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 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");
73
+ }
74
+ const performanceStageNames = new Set([
75
+ "validation",
76
+ "doctorConfig",
77
+ "security",
78
+ "compatibility",
79
+ "trust",
80
+ "recommendations",
81
+ "total"
82
+ ]);
83
+ function parseNonNegativeNumber(value) {
84
+ if (value === undefined || value.startsWith("--")) {
85
+ return null;
86
+ }
87
+ const parsed = Number(value);
88
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : null;
89
+ }
90
+ function buildGenericMcpDoctorCommandArgs(commandTarget, flags) {
91
+ if (!commandTarget || commandTarget.startsWith("--")) {
92
+ return "Missing target path. Usage: codex-plugin-doctor mcp <path> [--json] [--output <path>]";
93
+ }
94
+ const outputIndex = flags.indexOf("--output");
95
+ const outputPath = outputIndex === -1 ? null : flags[outputIndex + 1];
96
+ if (outputIndex !== -1 && (!outputPath || outputPath.startsWith("--"))) {
97
+ return "Missing path after --output.";
98
+ }
99
+ return {
100
+ targetPath: commandTarget,
101
+ jsonOutput: flags.includes("--json"),
102
+ outputPath
103
+ };
104
+ }
105
+ function parsePerformanceThresholds(flags) {
106
+ const thresholds = {};
107
+ const totalIndex = flags.indexOf("--max-total-ms");
108
+ if (totalIndex !== -1) {
109
+ const totalMs = parseNonNegativeNumber(flags[totalIndex + 1]);
110
+ if (totalMs === null) {
111
+ return "Missing or invalid number after --max-total-ms.";
112
+ }
113
+ thresholds.totalMs = totalMs;
114
+ }
115
+ for (let index = 0; index < flags.length; index += 1) {
116
+ if (flags[index] !== "--max-stage-ms") {
117
+ continue;
118
+ }
119
+ const value = flags[index + 1];
120
+ if (!value || value.startsWith("--") || !value.includes("=")) {
121
+ return "Missing or invalid stage threshold after --max-stage-ms. Use stage=milliseconds.";
122
+ }
123
+ const [stageName, rawLimit] = value.split("=", 2);
124
+ if (!performanceStageNames.has(stageName)) {
125
+ return `Unknown performance stage: ${stageName}.`;
126
+ }
127
+ const limitMs = parseNonNegativeNumber(rawLimit);
128
+ if (limitMs === null) {
129
+ return "Missing or invalid number after --max-stage-ms.";
130
+ }
131
+ thresholds.stages = {
132
+ ...thresholds.stages,
133
+ [stageName]: limitMs
134
+ };
135
+ }
136
+ return { thresholds };
73
137
  }
74
138
  function renderInstalledPlugins(plugins) {
75
139
  const lines = [
@@ -252,6 +316,29 @@ export async function runCli(args, io = defaultIo, options = {}) {
252
316
  : renderDoctorOutputContract(contract, { outputPath }));
253
317
  return 0;
254
318
  }
319
+ if (maybePath === "mcp") {
320
+ const targetPath = remainingArgs[0] && !remainingArgs[0].startsWith("--")
321
+ ? remainingArgs[0]
322
+ : "";
323
+ const mcpFlags = targetPath ? remainingArgs.slice(1) : remainingArgs;
324
+ const parsedMcpArgs = buildGenericMcpDoctorCommandArgs(targetPath, mcpFlags);
325
+ if (typeof parsedMcpArgs === "string") {
326
+ io.writeStderr(parsedMcpArgs);
327
+ return 2;
328
+ }
329
+ const report = await buildGenericMcpDoctor(parsedMcpArgs.targetPath, {
330
+ env: terminalContext.env,
331
+ platform: terminalContext.platform
332
+ });
333
+ const renderedReport = parsedMcpArgs.jsonOutput
334
+ ? renderGenericMcpDoctorJson(report)
335
+ : renderGenericMcpDoctor(report);
336
+ if (parsedMcpArgs.outputPath) {
337
+ await writeFile(parsedMcpArgs.outputPath, renderedReport, "utf8");
338
+ }
339
+ io.writeStdout(renderedReport);
340
+ return report.exitCode;
341
+ }
255
342
  if (maybePath === "corpus") {
256
343
  const jsonOutput = remainingArgs.includes("--json");
257
344
  const outputIndex = remainingArgs.indexOf("--output");
@@ -285,11 +372,36 @@ export async function runCli(args, io = defaultIo, options = {}) {
285
372
  const jsonOutput = attestFlags.includes("--json");
286
373
  const outputIndex = attestFlags.indexOf("--output");
287
374
  const outputPath = outputIndex === -1 ? null : attestFlags[outputIndex + 1];
375
+ const signKeyIndex = attestFlags.indexOf("--sign-key");
376
+ const signKeyEnvIndex = attestFlags.indexOf("--sign-key-env");
377
+ const signKey = signKeyIndex === -1 ? null : attestFlags[signKeyIndex + 1];
378
+ const signKeyEnv = signKeyEnvIndex === -1 ? null : attestFlags[signKeyEnvIndex + 1];
288
379
  if (outputIndex !== -1 && (!outputPath || outputPath.startsWith("--"))) {
289
380
  io.writeStderr("Missing path after --output.");
290
381
  return 2;
291
382
  }
292
- const attestation = await buildDoctorAttestation(targetPath);
383
+ if (signKeyIndex !== -1 && (!signKey || signKey.startsWith("--"))) {
384
+ io.writeStderr("Missing key after --sign-key.");
385
+ return 2;
386
+ }
387
+ if (signKeyEnvIndex !== -1 && (!signKeyEnv || signKeyEnv.startsWith("--"))) {
388
+ io.writeStderr("Missing environment variable name after --sign-key-env.");
389
+ return 2;
390
+ }
391
+ if (signKeyIndex !== -1 && signKeyEnvIndex !== -1) {
392
+ io.writeStderr("Use either --sign-key or --sign-key-env, not both.");
393
+ return 2;
394
+ }
395
+ const envSigningKey = signKeyEnv ? terminalContext.env[signKeyEnv] : undefined;
396
+ if (signKeyEnv && !envSigningKey) {
397
+ io.writeStderr(`Environment variable ${signKeyEnv} is not set.`);
398
+ return 2;
399
+ }
400
+ const attestation = await buildDoctorAttestation(targetPath, {
401
+ signingKey: signKey ?? envSigningKey,
402
+ signingKeyHint: signKeyEnv ? `env:${signKeyEnv}` : signKey ? "inline" : undefined,
403
+ recomputeKeyEnv: signKeyEnv ?? undefined
404
+ });
293
405
  const attestationJson = renderDoctorAttestationJson(attestation);
294
406
  if (outputPath) {
295
407
  await writeFile(outputPath, attestationJson, "utf8");
@@ -429,6 +541,11 @@ export async function runCli(args, io = defaultIo, options = {}) {
429
541
  io.writeStderr("Missing path after --output.");
430
542
  return 2;
431
543
  }
544
+ const parsedThresholds = parsePerformanceThresholds(perfFlags);
545
+ if (typeof parsedThresholds === "string") {
546
+ io.writeStderr(parsedThresholds);
547
+ return 2;
548
+ }
432
549
  const report = await buildDoctorPerformanceReport(targetPath, {
433
550
  environment: {
434
551
  env: terminalContext.env,
@@ -436,7 +553,8 @@ export async function runCli(args, io = defaultIo, options = {}) {
436
553
  },
437
554
  runCheck: options.runCheckImpl
438
555
  ? (pathToCheck) => options.runCheckImpl(pathToCheck)
439
- : undefined
556
+ : undefined,
557
+ thresholds: parsedThresholds.thresholds
440
558
  });
441
559
  const renderedReport = jsonOutput
442
560
  ? renderDoctorPerformanceReportJson(report)
@@ -689,26 +807,20 @@ export async function runCli(args, io = defaultIo, options = {}) {
689
807
  return audit.status === "fail" ? 1 : 0;
690
808
  }
691
809
  if (command === "mcp") {
692
- if (!maybePath || maybePath.startsWith("--")) {
693
- io.writeStderr("Missing target path. Usage: codex-plugin-doctor mcp <path> [--json] [--output <path>]");
694
- return 2;
695
- }
696
- const jsonOutput = remainingArgs.includes("--json");
697
- const outputIndex = remainingArgs.indexOf("--output");
698
- const outputPath = outputIndex === -1 ? null : remainingArgs[outputIndex + 1];
699
- if (outputIndex !== -1 && (!outputPath || outputPath.startsWith("--"))) {
700
- io.writeStderr("Missing path after --output.");
810
+ const parsedMcpArgs = buildGenericMcpDoctorCommandArgs(maybePath ?? "", remainingArgs);
811
+ if (typeof parsedMcpArgs === "string") {
812
+ io.writeStderr(parsedMcpArgs);
701
813
  return 2;
702
814
  }
703
- const report = await buildGenericMcpDoctor(maybePath, {
815
+ const report = await buildGenericMcpDoctor(parsedMcpArgs.targetPath, {
704
816
  env: terminalContext.env,
705
817
  platform: terminalContext.platform
706
818
  });
707
- const renderedReport = jsonOutput
819
+ const renderedReport = parsedMcpArgs.jsonOutput
708
820
  ? renderGenericMcpDoctorJson(report)
709
821
  : renderGenericMcpDoctor(report);
710
- if (outputPath) {
711
- await writeFile(outputPath, renderedReport, "utf8");
822
+ if (parsedMcpArgs.outputPath) {
823
+ await writeFile(parsedMcpArgs.outputPath, renderedReport, "utf8");
712
824
  }
713
825
  io.writeStdout(renderedReport);
714
826
  return report.exitCode;
@@ -0,0 +1,10 @@
1
+ {
2
+ "mcpServers": {
3
+ "genericRuntime": {
4
+ "command": "node",
5
+ "args": [
6
+ "server.js"
7
+ ]
8
+ }
9
+ }
10
+ }
@@ -0,0 +1 @@
1
+ process.stdin.resume();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-plugin-doctor",
3
- "version": "1.0.0",
3
+ "version": "1.0.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",