codex-plugin-doctor 1.77.0 → 1.79.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.
Files changed (32) hide show
  1. package/README.md +18 -2
  2. package/dist/core/init-ci.d.ts +1 -0
  3. package/dist/core/init-ci.js +16 -2
  4. package/dist/core/mcp-prompt-catalog.js +2 -0
  5. package/dist/core/mcp-resource-template-catalog.d.ts +36 -0
  6. package/dist/core/mcp-resource-template-catalog.js +55 -0
  7. package/dist/core/mcp-resource-template-definition.d.ts +28 -0
  8. package/dist/core/mcp-resource-template-definition.js +116 -0
  9. package/dist/core/mcp-resource-template-diff.d.ts +43 -0
  10. package/dist/core/mcp-resource-template-diff.js +136 -0
  11. package/dist/core/mcp-resource-template-file.d.ts +31 -0
  12. package/dist/core/mcp-resource-template-file.js +118 -0
  13. package/dist/core/mcp-resource-template-impact.d.ts +21 -0
  14. package/dist/core/mcp-resource-template-impact.js +74 -0
  15. package/dist/core/mcp-stdio-tool-catalog.d.ts +1 -1
  16. package/dist/core/mcp-stdio-tool-catalog.js +6 -6
  17. package/dist/core/mcp-tool-catalog.d.ts +8 -6
  18. package/dist/core/mcp-tool-catalog.js +7 -5
  19. package/dist/core/mcp-tool-response-file.d.ts +1 -1
  20. package/dist/core/mcp-tool-response-file.js +3 -2
  21. package/dist/core/output-contract.js +172 -1
  22. package/dist/reporting/render-mcp-resource-template-catalog-report.d.ts +2 -0
  23. package/dist/reporting/render-mcp-resource-template-catalog-report.js +33 -0
  24. package/dist/reporting/render-mcp-resource-template-diff-markdown.d.ts +2 -0
  25. package/dist/reporting/render-mcp-resource-template-diff-markdown.js +39 -0
  26. package/dist/reporting/render-mcp-resource-template-diff-report.d.ts +2 -0
  27. package/dist/reporting/render-mcp-resource-template-diff-report.js +48 -0
  28. package/dist/reporting/render-mcp-resource-template-file-report.d.ts +5 -0
  29. package/dist/reporting/render-mcp-resource-template-file-report.js +31 -0
  30. package/dist/run-cli.d.ts +7 -0
  31. package/dist/run-cli.js +80 -16
  32. package/package.json +1 -1
@@ -0,0 +1,39 @@
1
+ import { markdownCodeSpan } from "./quote-server-text.js";
2
+ import { describeResourceTemplateLocation } from "./render-mcp-resource-template-file-report.js";
3
+ function count(value) {
4
+ return value === null || value === undefined ? "not compared" : String(value);
5
+ }
6
+ function templateIndex(value) {
7
+ return value === null ? "-" : String(value);
8
+ }
9
+ export function renderMcpResourceTemplateDiffMarkdown(report) {
10
+ const comparison = report.comparison;
11
+ const named = report.changes.some((change) => change.name !== undefined);
12
+ const lines = [
13
+ "## MCP Resource Template Definition Diff",
14
+ "",
15
+ `**Status:** \`${report.status}\` · **Comparison complete:** ${comparison.complete ? "yes" : "no"}${comparison.reason === null ? "" : ` · **Reason:** \`${comparison.reason}\``}`,
16
+ "",
17
+ "| Added | Removed | Changed | Unchanged | Breaking | Unclassified |",
18
+ "| --- | --- | --- | --- | --- | --- |",
19
+ `| ${count(comparison.added)} | ${count(comparison.removed)} | ${count(comparison.changed)} | ${count(comparison.unchanged)} | ${count(comparison.breaking)} | ${count(comparison.unclassified)} |`
20
+ ];
21
+ if (report.changes.length > 0) {
22
+ lines.push("", "### Changes", "", named ? "| Change | Template | Before template | After template | Fields | Impact | Reasons |" : "| Change | Before template | After template | Fields | Impact | Reasons |", named ? "| --- | --- | --- | --- | --- | --- | --- |" : "| --- | --- | --- | --- | --- | --- |", ...report.changes.map((change) => `| ${change.kind}${named ? ` | ${change.name === undefined ? "-" : markdownCodeSpan(change.name)}` : ""} | ${templateIndex(change.beforeTemplateIndex)} | ${templateIndex(change.afterTemplateIndex)} | ${change.fields.map((field) => `\`${field}\``).join(", ") || "-"} | **${change.impact}** | ${change.reasons.map((reason) => `\`${reason}\``).join(", ") || "-"} |`));
23
+ }
24
+ const located = report.changes.flatMap((change) => (change.locations ?? []).map((location) => `| ${change.name === undefined ? "-" : markdownCodeSpan(change.name)} | \`${location.reason}\` | ${location.variable === null ? "-" : markdownCodeSpan(location.variable)} |`));
25
+ if (located.length > 0) {
26
+ lines.push("", "### Locations", "", "| Template | Reason | Variable |", "| --- | --- | --- |", ...located);
27
+ }
28
+ const findings = ["before", "after"].flatMap((side) => report[side].findings.map((finding) => {
29
+ const locator = finding.location ? `${side} ${describeResourceTemplateLocation(finding.location)}` : side;
30
+ return `| ${finding.severity} | \`${finding.id}\` | ${locator} | ${finding.message} |`;
31
+ }));
32
+ if (findings.length > 0) {
33
+ lines.push("", "### Input Findings", "", "| Severity | Finding | Location | Message |", "| --- | --- | --- | --- |", ...findings);
34
+ }
35
+ lines.push("", named
36
+ ? `Template and variable names are shown because --show-names was used; URI templates, descriptions, and other values are not reported. Impact labels are ${report.coverage.impactClassification} URI template checks, not proof of compatibility.`
37
+ : `Template numbers are one-based positions in the before and after files; names, URI templates, and descriptions are not reported. Impact labels are ${report.coverage.impactClassification} URI template checks, not proof of compatibility.`);
38
+ return lines.join("\n");
39
+ }
@@ -0,0 +1,2 @@
1
+ import type { McpResourceTemplateDiffReport } from "../core/mcp-resource-template-diff.js";
2
+ export declare function renderMcpResourceTemplateDiffReport(report: McpResourceTemplateDiffReport): string;
@@ -0,0 +1,48 @@
1
+ import { describeResourceTemplateLocation } from "./render-mcp-resource-template-file-report.js";
2
+ import { quoteServerText } from "./quote-server-text.js";
3
+ export function renderMcpResourceTemplateDiffReport(report) {
4
+ const comparison = report.comparison;
5
+ return [
6
+ "Offline MCP Resource Template Diff",
7
+ "==================================",
8
+ `Status: ${report.status}`,
9
+ `Scope: ${report.scope}`,
10
+ `Comparison complete: ${comparison.complete}`,
11
+ ...(comparison.reason === null ? [] : [`Reason: ${comparison.reason}`]),
12
+ `Added: ${comparison.added ?? "not compared"}`,
13
+ `Removed: ${comparison.removed ?? "not compared"}`,
14
+ `Changed: ${comparison.changed ?? "not compared"}`,
15
+ `Unchanged: ${comparison.unchanged ?? "not compared"}`,
16
+ `Breaking: ${comparison.breaking ?? "not compared"}`,
17
+ `Unclassified: ${comparison.unclassified ?? "not compared"}`,
18
+ `Comparison coverage: ${report.coverage.comparison}`,
19
+ `Definition coverage: ${report.coverage.definitions}`,
20
+ `Compatibility: ${report.coverage.compatibility}`,
21
+ `Server catalog: ${report.coverage.serverCatalog}`,
22
+ `Resource read: ${report.coverage.resourceRead}`,
23
+ `Impact classification: ${report.coverage.impactClassification}`,
24
+ "Only the supplied responses are compared. Impact labels are URI template heuristics: a compatible label or an unchanged result does not establish compatibility.",
25
+ ...["before", "after"].flatMap((side) => {
26
+ const source = report[side];
27
+ const label = side === "before" ? "Before" : "After";
28
+ return [
29
+ `${label} input: ${source.status}; templates checked: ${source.inspection.templatesChecked}; bytes read: ${source.source.bytesRead}`,
30
+ ...(source.inspection.reason === null ? [] : [`${label} reason: ${source.inspection.reason}`]),
31
+ ...source.findings.map((finding) => {
32
+ const locator = finding.location ? ` [${side} ${describeResourceTemplateLocation(finding.location)}]` : "";
33
+ return `${label} ${finding.severity.toUpperCase()} ${finding.id}${locator}: ${finding.message}\n ${finding.suggestedFix}`;
34
+ })
35
+ ];
36
+ }),
37
+ ...report.changes.map((change) => {
38
+ const locations = [
39
+ ...(change.beforeTemplateIndex === null ? [] : [`before template ${change.beforeTemplateIndex}`]),
40
+ ...(change.afterTemplateIndex === null ? [] : [`after template ${change.afterTemplateIndex}`])
41
+ ];
42
+ const impact = `${change.impact}${change.reasons.length ? ` (${change.reasons.join(", ")})` : ""}`;
43
+ const name = change.name === undefined ? "" : ` ${quoteServerText(change.name)}`;
44
+ const at = (change.locations ?? []).map((location) => `\n At ${location.variable === null ? "(template)" : `variable ${quoteServerText(location.variable)}`}: ${location.reason}`).join("");
45
+ return `${change.kind.toUpperCase()}${name} [${locations.join(" -> ")}]${change.fields.length ? `: ${change.fields.join(", ")}` : ""}\n Impact: ${impact}${at}`;
46
+ })
47
+ ].join("\n");
48
+ }
@@ -0,0 +1,5 @@
1
+ import type { ResourceTemplateCatalogLocation } from "../core/mcp-resource-template-definition.js";
2
+ import type { McpResourceTemplateFileReport } from "../core/mcp-resource-template-file.js";
3
+ /** Describes a finding location by one-based positions only, such as "template 2, related template 1". */
4
+ export declare function describeResourceTemplateLocation(location: ResourceTemplateCatalogLocation): string;
5
+ export declare function renderMcpResourceTemplateFileReport(report: McpResourceTemplateFileReport): string;
@@ -0,0 +1,31 @@
1
+ /** Describes a finding location by one-based positions only, such as "template 2, related template 1". */
2
+ export function describeResourceTemplateLocation(location) {
3
+ return [
4
+ `template ${location.templateIndex}`,
5
+ ...(location.relatedTemplateIndex === undefined ? [] : [`related template ${location.relatedTemplateIndex}`])
6
+ ].join(", ");
7
+ }
8
+ export function renderMcpResourceTemplateFileReport(report) {
9
+ return [
10
+ "Offline MCP Resource Template Definitions",
11
+ "=========================================",
12
+ `Status: ${report.status}`,
13
+ `Scope: ${report.scope}`,
14
+ `Source: ${report.source.kind} (${report.source.format})`,
15
+ `Bytes read: ${report.source.bytesRead}`,
16
+ `Inspection complete: ${report.inspection.complete}`,
17
+ `Templates checked: ${report.inspection.templatesChecked}`,
18
+ `More pages advertised: ${report.inspection.hasNextCursor ?? "unknown"}`,
19
+ ...(report.inspection.reason === null ? [] : [`Reason: ${report.inspection.reason}`]),
20
+ `Definition coverage: ${report.coverage.definitions}`,
21
+ `Discovery: ${report.coverage.discovery}`,
22
+ `Server catalog: ${report.coverage.serverCatalog}`,
23
+ `Resource read: ${report.coverage.resourceRead}`,
24
+ `Full conformance: ${report.coverage.fullConformance}`,
25
+ "Only the supplied file was inspected. Its provenance and server completeness are not established.",
26
+ ...report.findings.map((finding) => {
27
+ const locator = finding.location ? ` [${describeResourceTemplateLocation(finding.location)}]` : "";
28
+ return `${finding.severity.toUpperCase()} ${finding.id}${locator}: ${finding.message}\n ${finding.suggestedFix}`;
29
+ })
30
+ ].join("\n");
31
+ }
package/dist/run-cli.d.ts CHANGED
@@ -2,8 +2,11 @@ import { discoverMcpServer } from "./core/mcp-discovery.js";
2
2
  import { captureMcpToolCatalog, inspectMcpToolCatalog } from "./core/mcp-tool-catalog.js";
3
3
  import { captureStdioMcpToolCatalog } from "./core/mcp-stdio-tool-catalog.js";
4
4
  import { captureMcpPromptCatalog, captureStdioMcpPromptCatalog } from "./core/mcp-prompt-catalog.js";
5
+ import { captureMcpResourceTemplateCatalog, captureStdioMcpResourceTemplateCatalog } from "./core/mcp-resource-template-catalog.js";
5
6
  import { compareMcpPromptFiles } from "./core/mcp-prompt-diff.js";
6
7
  import { inspectMcpPromptFile } from "./core/mcp-prompt-file.js";
8
+ import { compareMcpResourceTemplateFiles } from "./core/mcp-resource-template-diff.js";
9
+ import { inspectMcpResourceTemplateFile } from "./core/mcp-resource-template-file.js";
7
10
  import { inspectMcpToolFile } from "./core/mcp-tool-file.js";
8
11
  import { compareMcpToolFiles } from "./core/mcp-tool-diff.js";
9
12
  import { buildCorpusQualityMetricsDiffReport, buildCorpusQualityMetricsReport } from "./core/corpus-quality-metrics.js";
@@ -27,9 +30,13 @@ export interface RunCliOptions {
27
30
  captureMcpToolCatalogImpl?: typeof captureMcpToolCatalog;
28
31
  captureStdioMcpToolCatalogImpl?: typeof captureStdioMcpToolCatalog;
29
32
  compareMcpPromptFilesImpl?: typeof compareMcpPromptFiles;
33
+ compareMcpResourceTemplateFilesImpl?: typeof compareMcpResourceTemplateFiles;
34
+ inspectMcpResourceTemplateFileImpl?: typeof inspectMcpResourceTemplateFile;
30
35
  inspectMcpPromptFileImpl?: typeof inspectMcpPromptFile;
31
36
  captureMcpPromptCatalogImpl?: typeof captureMcpPromptCatalog;
32
37
  captureStdioMcpPromptCatalogImpl?: typeof captureStdioMcpPromptCatalog;
38
+ captureMcpResourceTemplateCatalogImpl?: typeof captureMcpResourceTemplateCatalog;
39
+ captureStdioMcpResourceTemplateCatalogImpl?: typeof captureStdioMcpResourceTemplateCatalog;
33
40
  discoverMcpServerImpl?: typeof discoverMcpServer;
34
41
  terminalContext?: CliTerminalContext;
35
42
  runCheckImpl?: typeof runCheck;
package/dist/run-cli.js CHANGED
@@ -26,8 +26,11 @@ import { discoverMcpServer } from "./core/mcp-discovery.js";
26
26
  import { captureMcpToolCatalog, inspectMcpToolCatalog } from "./core/mcp-tool-catalog.js";
27
27
  import { captureStdioMcpToolCatalog, resolveMcpStdioServer } from "./core/mcp-stdio-tool-catalog.js";
28
28
  import { captureMcpPromptCatalog, captureStdioMcpPromptCatalog } from "./core/mcp-prompt-catalog.js";
29
+ import { captureMcpResourceTemplateCatalog, captureStdioMcpResourceTemplateCatalog } from "./core/mcp-resource-template-catalog.js";
29
30
  import { compareMcpPromptFiles } from "./core/mcp-prompt-diff.js";
30
31
  import { inspectMcpPromptFile } from "./core/mcp-prompt-file.js";
32
+ import { compareMcpResourceTemplateFiles } from "./core/mcp-resource-template-diff.js";
33
+ import { inspectMcpResourceTemplateFile } from "./core/mcp-resource-template-file.js";
31
34
  import { inspectMcpToolFile, isBlockedOfflinePath } from "./core/mcp-tool-file.js";
32
35
  import { saveMcpListResponse } from "./core/mcp-tool-response-file.js";
33
36
  import { compareMcpToolFiles } from "./core/mcp-tool-diff.js";
@@ -36,9 +39,13 @@ import { renderMcpToolDiffReport } from "./reporting/render-mcp-tool-diff-report
36
39
  import { renderMcpToolFileReport } from "./reporting/render-mcp-tool-file-report.js";
37
40
  import { renderMcpToolCatalogReport } from "./reporting/render-mcp-tool-catalog-report.js";
38
41
  import { renderMcpPromptCatalogReport } from "./reporting/render-mcp-prompt-catalog-report.js";
42
+ import { renderMcpResourceTemplateCatalogReport } from "./reporting/render-mcp-resource-template-catalog-report.js";
39
43
  import { renderMcpPromptDiffMarkdown } from "./reporting/render-mcp-prompt-diff-markdown.js";
40
44
  import { renderMcpPromptDiffReport } from "./reporting/render-mcp-prompt-diff-report.js";
41
45
  import { renderMcpPromptFileReport } from "./reporting/render-mcp-prompt-file-report.js";
46
+ import { renderMcpResourceTemplateDiffMarkdown } from "./reporting/render-mcp-resource-template-diff-markdown.js";
47
+ import { renderMcpResourceTemplateDiffReport } from "./reporting/render-mcp-resource-template-diff-report.js";
48
+ import { renderMcpResourceTemplateFileReport } from "./reporting/render-mcp-resource-template-file-report.js";
42
49
  import { renderMcpDiscoveryReport } from "./reporting/render-mcp-discovery-report.js";
43
50
  import { buildDoctorValidationCorpusReport, renderDoctorValidationCorpusJson, renderDoctorValidationCorpusReport } from "./core/validation-corpus.js";
44
51
  import { buildExternalValidationCorpusReport, ExternalCorpusManifestError } from "./core/external-validation-corpus.js";
@@ -122,18 +129,25 @@ const toolsDiffUsage = "Usage: codex-plugin-doctor doctor tools-diff --before <p
122
129
  const toolsFileUsage = "Usage: codex-plugin-doctor doctor tools-file <path> [--json]";
123
130
  const promptsDiffUsage = "Usage: codex-plugin-doctor doctor prompts-diff --before <path> --after <path> [--json|--markdown] [--fail-on any|breaking] [--show-names]";
124
131
  const promptsFileUsage = "Usage: codex-plugin-doctor doctor prompts-file <path> [--json]";
132
+ const resourceTemplatesDiffUsage = "Usage: codex-plugin-doctor doctor resource-templates-diff --before <path> --after <path> [--json|--markdown] [--fail-on any|breaking] [--show-names]";
133
+ const resourceTemplatesFileUsage = "Usage: codex-plugin-doctor doctor resource-templates-file <path> [--json]";
125
134
  const toolsUsage = "Usage: codex-plugin-doctor doctor tools <url> --allow-network [--allow-local-network] [--json] [--save-response <path>] [--bearer-token-env <NAME>]\n codex-plugin-doctor doctor tools <path> --server <name> --runtime [--sandbox docker [--env <NAME>]...] [--require-runtime-approval [--runtime-approval-digest <digest>]] [--json] [--save-response <path>]";
135
+ const resourceTemplatesUsage = "Usage: codex-plugin-doctor doctor resource-templates <url> --allow-network [--allow-local-network] [--json] [--save-response <path>] [--bearer-token-env <NAME>]\n codex-plugin-doctor doctor resource-templates <path> --server <name> --runtime [--sandbox docker [--env <NAME>]...] [--require-runtime-approval [--runtime-approval-digest <digest>]] [--json] [--save-response <path>]";
126
136
  const promptsUsage = "Usage: codex-plugin-doctor doctor prompts <url> --allow-network [--allow-local-network] [--json] [--save-response <path>] [--bearer-token-env <NAME>]\n codex-plugin-doctor doctor prompts <path> --server <name> --runtime [--sandbox docker [--env <NAME>]...] [--require-runtime-approval [--runtime-approval-digest <digest>]] [--json] [--save-response <path>]";
127
- const TOOL_CATALOG_COMMAND = { name: "tools", noun: "Tool", method: "tools/list", usage: toolsUsage };
128
- const PROMPT_CATALOG_COMMAND = { name: "prompts", noun: "Prompt", method: "prompts/list", usage: promptsUsage };
137
+ const TOOL_CATALOG_COMMAND = { name: "tools", noun: "Tool", plural: "tools", method: "tools/list", resultKey: "tools", usage: toolsUsage };
138
+ const PROMPT_CATALOG_COMMAND = { name: "prompts", noun: "Prompt", plural: "prompts", method: "prompts/list", resultKey: "prompts", usage: promptsUsage };
139
+ const RESOURCE_TEMPLATE_CATALOG_COMMAND = {
140
+ name: "resource-templates", noun: "Resource template", plural: "resource templates",
141
+ method: "resources/templates/list", resultKey: "resourceTemplates", usage: resourceTemplatesUsage
142
+ };
129
143
  const discoveryUsage = "Usage: codex-plugin-doctor doctor discover <url> --allow-network [--allow-local-network] [--json] [--bearer-token-env <NAME>]";
130
144
  const bearerTokenHelp = "--bearer-token-env reads a bearer token from the named environment variable and sends it only over HTTPS or to an approved loopback target; the token is never printed or saved.";
131
145
  const ENVIRONMENT_VARIABLE_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/u;
132
- /** Reads init-ci --tools-diff-server, --baseline, and --sandbox; options is null when none is given. */
146
+ /** Reads init-ci --tools-diff-server, --baseline, --prompts-baseline, and --sandbox; options is null when none is given. */
133
147
  function parseToolsDiffWorkflowOptions(flags) {
134
148
  const values = new Map();
135
149
  for (let index = 0; index < flags.length; index += 1) {
136
- if (!["--tools-diff-server", "--baseline", "--sandbox"].includes(flags[index]))
150
+ if (!["--tools-diff-server", "--baseline", "--prompts-baseline", "--sandbox"].includes(flags[index]))
137
151
  continue;
138
152
  const value = flags[index + 1];
139
153
  const present = value !== undefined && !value.startsWith("--");
@@ -144,7 +158,7 @@ function parseToolsDiffWorkflowOptions(flags) {
144
158
  if (values.size === 0)
145
159
  return { options: null };
146
160
  if (!values.has("--tools-diff-server"))
147
- return { error: "--baseline and --sandbox require --tools-diff-server." };
161
+ return { error: "--baseline, --prompts-baseline, and --sandbox require --tools-diff-server." };
148
162
  // Control characters would break the generated workflow lines, GitHub evaluates "${{" in any
149
163
  // workflow value, and the Action rejects values that start with "-".
150
164
  const usable = (value) => value !== undefined && value.trim() !== "" && !value.startsWith("-") && !value.includes("${{")
@@ -152,16 +166,23 @@ function parseToolsDiffWorkflowOptions(flags) {
152
166
  const server = values.get("--tools-diff-server");
153
167
  if (!usable(server))
154
168
  return { error: "--tools-diff-server requires a server name." };
155
- // The workflow runs on Linux from the repository root, so the baseline is a relative POSIX path.
169
+ // The workflow runs on Linux from the repository root, so baselines are relative POSIX paths.
170
+ const relative = (value) => usable(value) && !path.posix.isAbsolute(value)
171
+ && !/^[A-Za-z]:/u.test(value) && !path.posix.normalize(value).split("/").includes("..");
156
172
  const baseline = (values.has("--baseline") ? values.get("--baseline") : "mcp/tools-baseline.json")?.replaceAll("\\", "/");
157
- if (!usable(baseline) || path.posix.isAbsolute(baseline) || /^[A-Za-z]:/u.test(baseline)
158
- || path.posix.normalize(baseline).split("/").includes("..")) {
173
+ if (!relative(baseline))
159
174
  return { error: "--baseline requires a relative path inside the repository." };
175
+ const promptsBaseline = values.has("--prompts-baseline") ? values.get("--prompts-baseline")?.replaceAll("\\", "/") : null;
176
+ if (promptsBaseline !== null && !relative(promptsBaseline)) {
177
+ return { error: "--prompts-baseline requires a relative path inside the repository." };
178
+ }
179
+ if (promptsBaseline !== null && path.posix.normalize(promptsBaseline) === path.posix.normalize(baseline)) {
180
+ return { error: "--prompts-baseline must differ from --baseline." };
160
181
  }
161
182
  const sandbox = values.get("--sandbox");
162
183
  if (values.has("--sandbox") && sandbox !== "docker")
163
184
  return { error: "--sandbox must be docker." };
164
- return { options: { server, baseline, sandbox: sandbox === "docker" ? "docker" : null } };
185
+ return { options: { server, baseline, sandbox: sandbox === "docker" ? "docker" : null, promptsBaseline } };
165
186
  }
166
187
  const MAX_FORWARDED_ENVIRONMENT_VARIABLES = 32;
167
188
  const forwardedEnvironmentHelp = "--env forwards the named host environment variable into the Docker sandbox by name; Doctor never prints its value, writes it to a report, or passes it as a command argument. Native servers already inherit Doctor's environment.";
@@ -188,7 +209,7 @@ async function finishCatalogCommand(io, kind, status, output, items, cache, save
188
209
  io.writeStderr(`${kind.noun} catalog response was not saved because enumeration did not complete.`);
189
210
  return 2;
190
211
  }
191
- const saved = await saveMcpListResponse(savePath, kind.name, items, cache);
212
+ const saved = await saveMcpListResponse(savePath, kind.resultKey, items, cache);
192
213
  if (saved.kind !== "saved") {
193
214
  io.writeStderr({
194
215
  "path-blocked": `${kind.noun} catalog response was not saved because the path is not a local file path.`,
@@ -198,7 +219,7 @@ async function finishCatalogCommand(io, kind, status, output, items, cache, save
198
219
  }[saved.kind]);
199
220
  return 2;
200
221
  }
201
- io.writeStderr(`Saved a complete ${kind.method} response with ${saved.count} ${kind.name} (${saved.bytes} bytes).`);
222
+ io.writeStderr(`Saved a complete ${kind.method} response with ${saved.count} ${kind.plural} (${saved.bytes} bytes).`);
202
223
  return exitCode;
203
224
  }
204
225
  function finishToolCatalogCommand(io, snapshot, json, savePath) {
@@ -209,6 +230,10 @@ function finishPromptCatalogCommand(io, snapshot, json, savePath) {
209
230
  const report = snapshot.report;
210
231
  return finishCatalogCommand(io, PROMPT_CATALOG_COMMAND, report.status, json ? JSON.stringify(report, null, 2) : renderMcpPromptCatalogReport(report), snapshot.prompts, snapshot.cache, savePath);
211
232
  }
233
+ function finishResourceTemplateCatalogCommand(io, snapshot, json, savePath) {
234
+ const report = snapshot.report;
235
+ return finishCatalogCommand(io, RESOURCE_TEMPLATE_CATALOG_COMMAND, report.status, json ? JSON.stringify(report, null, 2) : renderMcpResourceTemplateCatalogReport(report), snapshot.templates, snapshot.cache, savePath);
236
+ }
212
237
  async function runHttpCatalogCommand(url, flags, io, env, kind, run) {
213
238
  const allowedFlags = new Set(["--allow-network", "--allow-local-network", "--json"]);
214
239
  const switches = [];
@@ -509,14 +534,17 @@ function parseRemoteNetworkFlags(flags, runtime) {
509
534
  return { allowNetwork, allowLocalNetwork, allowSessionLifecycle, requireRemoteReliability };
510
535
  }
511
536
  function printUsage(io) {
512
- 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 [--allow-network [--allow-local-network]]] [--sandbox docker] [--require-runtime-approval --runtime-approval-digest <digest>] [--verbose-runtime] [--explain] [--no-animations] [--ascii] [--changed-since <ref>] [--fail-on <rule-id>]\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> [--runtime [--allow-network [--allow-local-network]]] [--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 [--manifest <corpus.json>] [--json] [--output <path>]|corpus metrics --manifest <corpus.json> [--json|--markdown] [--output <path>] [--min-precision <0..1>] [--min-recall <0..1>] [--max-false-positive-rate <0..1>]|runtime-plan <path> [--sandbox docker] [--json|--markdown] [--output <path>]|runtime-policy <path> [--sandbox docker] [--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 [--runtime [--allow-network [--allow-local-network]]] [--sandbox docker] [--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 [--runtime [--allow-network [--allow-local-network]]] [--sandbox docker] [--allow-dirty] [--allow-untagged] [--require-runtime-approval --runtime-approval-digest <digest>] [--upload]|mcp <path> [--runtime [--allow-network [--allow-local-network]]]|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] [--tools-diff-server <name> [--baseline <path>] [--sandbox docker]]\n codex-plugin-doctor init-git-hooks [path] [--force] [--json]\n codex-plugin-doctor init-git-hooks [path] --remove [--json]\n codex-plugin-doctor completion bash|zsh|fish\n codex-plugin-doctor config validate <path> [--json]\n codex-plugin-doctor release check <path> [--json] [--runtime [--allow-network [--allow-local-network]]] [--sandbox docker]\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");
537
+ 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 [--allow-network [--allow-local-network]]] [--sandbox docker] [--require-runtime-approval --runtime-approval-digest <digest>] [--verbose-runtime] [--explain] [--no-animations] [--ascii] [--changed-since <ref>] [--fail-on <rule-id>]\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> [--runtime [--allow-network [--allow-local-network]]] [--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 [--manifest <corpus.json>] [--json] [--output <path>]|corpus metrics --manifest <corpus.json> [--json|--markdown] [--output <path>] [--min-precision <0..1>] [--min-recall <0..1>] [--max-false-positive-rate <0..1>]|runtime-plan <path> [--sandbox docker] [--json|--markdown] [--output <path>]|runtime-policy <path> [--sandbox docker] [--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 [--runtime [--allow-network [--allow-local-network]]] [--sandbox docker] [--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 [--runtime [--allow-network [--allow-local-network]]] [--sandbox docker] [--allow-dirty] [--allow-untagged] [--require-runtime-approval --runtime-approval-digest <digest>] [--upload]|mcp <path> [--runtime [--allow-network [--allow-local-network]]]|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] [--tools-diff-server <name> [--baseline <path>] [--prompts-baseline <path>] [--sandbox docker]]\n codex-plugin-doctor init-git-hooks [path] [--force] [--json]\n codex-plugin-doctor init-git-hooks [path] --remove [--json]\n codex-plugin-doctor completion bash|zsh|fish\n codex-plugin-doctor config validate <path> [--json]\n codex-plugin-doctor release check <path> [--json] [--runtime [--allow-network [--allow-local-network]]] [--sandbox docker]\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");
513
538
  io.writeStderr(discoveryUsage);
514
539
  io.writeStderr(toolsUsage);
515
540
  io.writeStderr(promptsUsage);
541
+ io.writeStderr(resourceTemplatesUsage);
516
542
  io.writeStderr(toolsFileUsage);
517
543
  io.writeStderr(toolsDiffUsage);
518
544
  io.writeStderr(promptsFileUsage);
519
545
  io.writeStderr(promptsDiffUsage);
546
+ io.writeStderr(resourceTemplatesFileUsage);
547
+ io.writeStderr(resourceTemplatesDiffUsage);
520
548
  io.writeStderr("Registry readiness: codex-plugin-doctor registry check <server.json|directory> [--json] [--output <path>] [--require-registry-readiness]\n"
521
549
  + " codex-plugin-doctor registry inspect <server-name> --allow-network [--json] [--output <path>] [--require-registry-readiness]\n"
522
550
  + "Registry publication preflight: codex-plugin-doctor registry preflight <server.json|directory> [--allow-network] [--json] [--output <path>] [--require-publish-ready]");
@@ -1480,10 +1508,10 @@ export async function runCli(args, io = defaultIo, options = {}) {
1480
1508
  (command === "doctor" &&
1481
1509
  (["runtime-plan", "runtime-policy"].includes(maybePath ?? "") ||
1482
1510
  (maybePath === "release-evidence" && remainingArgs[0] !== "verify") ||
1483
- ((maybePath === "tools" || maybePath === "prompts") && remainingArgs.includes("--server")))) ||
1511
+ (["tools", "prompts", "resource-templates"].includes(maybePath ?? "") && remainingArgs.includes("--server")))) ||
1484
1512
  (command === "init-ci" && args.includes("--tools-diff-server"));
1485
1513
  if (args.includes("--sandbox") && !runtimeSandboxSupported) {
1486
- io.writeStderr("--sandbox is supported only by single-package check, release check, runtime-plan, runtime-policy, release-evidence, doctor tools --server, doctor prompts --server, and init-ci --tools-diff-server.");
1514
+ io.writeStderr("--sandbox is supported only by single-package check, release check, runtime-plan, runtime-policy, release-evidence, doctor tools --server, doctor prompts --server, doctor resource-templates --server, and init-ci --tools-diff-server.");
1487
1515
  return 2;
1488
1516
  }
1489
1517
  if (command === "--version" || command === "-v" || command === "version") {
@@ -1565,6 +1593,24 @@ export async function runCli(args, io = defaultIo, options = {}) {
1565
1593
  }
1566
1594
  return runOfflineFileCommand(remainingArgs, io, promptsFileUsage, options.inspectMcpPromptFileImpl ?? inspectMcpPromptFile, renderMcpPromptFileReport);
1567
1595
  }
1596
+ if (maybePath === "resource-templates-diff") {
1597
+ if (remainingArgs.length === 1 && remainingArgs[0] === "--help") {
1598
+ io.writeStdout(`${resourceTemplatesDiffUsage}\nCompares two saved MCP resources/templates/list responses offline. Changes are classified heuristically as breaking, compatible, or unclassified; no resource is read and compatibility is not tested.\n--fail-on breaking exits 1 only for breaking or unclassified changes (default: any change or input warning).
1599
+ --show-names adds template names and the variable behind each reason; reports omit both, and never include URI templates, so keep the flag out of public output such as pull request comments.`);
1600
+ return 0;
1601
+ }
1602
+ return runOfflineDiffCommand(remainingArgs, io, resourceTemplatesDiffUsage, (beforePath, afterPath, showNames) => {
1603
+ const compare = options.compareMcpResourceTemplateFilesImpl ?? compareMcpResourceTemplateFiles;
1604
+ return showNames ? compare(beforePath, afterPath, { showNames: true }) : compare(beforePath, afterPath);
1605
+ }, (report, markdown) => markdown ? renderMcpResourceTemplateDiffMarkdown(report) : renderMcpResourceTemplateDiffReport(report));
1606
+ }
1607
+ if (maybePath === "resource-templates-file") {
1608
+ if (remainingArgs.length === 1 && remainingArgs[0] === "--help") {
1609
+ io.writeStdout(`${resourceTemplatesFileUsage}\nInspects one saved UTF-8 MCP resources/templates/list response. No server connection or resource read.`);
1610
+ return 0;
1611
+ }
1612
+ return runOfflineFileCommand(remainingArgs, io, resourceTemplatesFileUsage, options.inspectMcpResourceTemplateFileImpl ?? inspectMcpResourceTemplateFile, renderMcpResourceTemplateFileReport);
1613
+ }
1568
1614
  if (maybePath === "tools") {
1569
1615
  if (remainingArgs.length === 1 && remainingArgs[0] === "--help") {
1570
1616
  io.writeStdout(`${toolsUsage}\nTool catalog structure only; tools are not executed.\n--server starts the named stdio server from the package MCP configuration after explicit --runtime consent and stops it after reading the tool list.\n${forwardedEnvironmentHelp}\n--save-response writes a complete enumeration as one tools/list response for doctor tools-file and tools-diff; nothing is written when enumeration is incomplete.\n${bearerTokenHelp}`);
@@ -1600,6 +1646,21 @@ export async function runCli(args, io = defaultIo, options = {}) {
1600
1646
  }
1601
1647
  return runHttpCatalogCommand(target, flags, io, terminalContext.env, PROMPT_CATALOG_COMMAND, async (url, catalogOptions, json, savePath) => finishPromptCatalogCommand(io, await (options.captureMcpPromptCatalogImpl ?? captureMcpPromptCatalog)(url, catalogOptions), json, savePath));
1602
1648
  }
1649
+ if (maybePath === "resource-templates") {
1650
+ if (remainingArgs.length === 1 && remainingArgs[0] === "--help") {
1651
+ io.writeStdout(`${resourceTemplatesUsage}\nResource template catalog structure only; no resource is read.\n--server starts the named stdio server from the package MCP configuration after explicit --runtime consent and stops it after reading the resource template list.\n${forwardedEnvironmentHelp}\n--save-response writes a complete enumeration as one resources/templates/list response for doctor resource-templates-file and resource-templates-diff; nothing is written when enumeration is incomplete.\n${bearerTokenHelp}`);
1652
+ return 0;
1653
+ }
1654
+ const [target, ...flags] = remainingArgs;
1655
+ if (!target || target.startsWith("-")) {
1656
+ io.writeStderr(resourceTemplatesUsage);
1657
+ return 2;
1658
+ }
1659
+ if (flags.includes("--server")) {
1660
+ return runStdioCatalogCommand(target, flags, io, terminalContext.env, RESOURCE_TEMPLATE_CATALOG_COMMAND, async (server, captureOptions, json, savePath) => finishResourceTemplateCatalogCommand(io, await (options.captureStdioMcpResourceTemplateCatalogImpl ?? captureStdioMcpResourceTemplateCatalog)(server, captureOptions), json, savePath));
1661
+ }
1662
+ return runHttpCatalogCommand(target, flags, io, terminalContext.env, RESOURCE_TEMPLATE_CATALOG_COMMAND, async (url, catalogOptions, json, savePath) => finishResourceTemplateCatalogCommand(io, await (options.captureMcpResourceTemplateCatalogImpl ?? captureMcpResourceTemplateCatalog)(url, catalogOptions), json, savePath));
1663
+ }
1603
1664
  if (maybePath === "discover") {
1604
1665
  if (remainingArgs.length === 1 && remainingArgs[0] === "--help") {
1605
1666
  io.writeStdout(`${discoveryUsage}\nHTTP discovery only; runtime behavior is not tested.\n${bearerTokenHelp}`);
@@ -2882,13 +2943,16 @@ export async function runCli(args, io = defaultIo, options = {}) {
2882
2943
  return 2;
2883
2944
  }
2884
2945
  if (toolsDiff.options !== null) {
2885
- const { server, baseline, sandbox } = toolsDiff.options;
2946
+ const { server, baseline, sandbox, promptsBaseline } = toolsDiff.options;
2886
2947
  const toolsResult = await initToolsDiffWorkflow(targetPath, toolsDiff.options);
2887
2948
  io.writeStdout([
2888
2949
  "Initialized Codex Plugin Doctor tool diff workflow",
2889
2950
  `Root: ${toolsResult.rootPath}`,
2890
2951
  `Workflow: ${toolsResult.workflowPath}`,
2891
- `Capture and commit a baseline at ${baseline}, for example with: codex-plugin-doctor doctor tools ${JSON.stringify(targetPath)} --server ${JSON.stringify(server)} --runtime${sandbox === "docker" ? " --sandbox docker" : ""} --save-response ${JSON.stringify(path.join(targetPath, baseline))}`
2952
+ `Capture and commit a baseline at ${baseline}, for example with: codex-plugin-doctor doctor tools ${JSON.stringify(targetPath)} --server ${JSON.stringify(server)} --runtime${sandbox === "docker" ? " --sandbox docker" : ""} --save-response ${JSON.stringify(path.join(targetPath, baseline))}`,
2953
+ ...(promptsBaseline === null ? [] : [
2954
+ `Capture and commit a prompt baseline at ${promptsBaseline}, for example with: codex-plugin-doctor doctor prompts ${JSON.stringify(targetPath)} --server ${JSON.stringify(server)} --runtime${sandbox === "docker" ? " --sandbox docker" : ""} --save-response ${JSON.stringify(path.join(targetPath, promptsBaseline))}`
2955
+ ])
2892
2956
  ].join("\n"));
2893
2957
  return 0;
2894
2958
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-plugin-doctor",
3
- "version": "1.77.0",
3
+ "version": "1.79.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",