codex-plugin-doctor 1.75.0 → 1.77.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -3
- package/dist/core/mcp-prompt-catalog.d.ts +36 -0
- package/dist/core/mcp-prompt-catalog.js +53 -0
- package/dist/core/mcp-prompt-definition.d.ts +24 -0
- package/dist/core/mcp-prompt-definition.js +107 -0
- package/dist/core/mcp-prompt-diff.d.ts +43 -0
- package/dist/core/mcp-prompt-diff.js +132 -0
- package/dist/core/mcp-prompt-file.d.ts +31 -0
- package/dist/core/mcp-prompt-file.js +118 -0
- package/dist/core/mcp-prompt-impact.d.ts +21 -0
- package/dist/core/mcp-prompt-impact.js +93 -0
- package/dist/core/mcp-stdio-tool-catalog.d.ts +9 -3
- package/dist/core/mcp-stdio-tool-catalog.js +74 -55
- package/dist/core/mcp-tool-catalog.d.ts +44 -5
- package/dist/core/mcp-tool-catalog.js +76 -31
- package/dist/core/mcp-tool-diff.d.ts +8 -2
- package/dist/core/mcp-tool-diff.js +7 -5
- package/dist/core/mcp-tool-file.d.ts +20 -0
- package/dist/core/mcp-tool-file.js +1 -1
- package/dist/core/mcp-tool-impact.d.ts +10 -1
- package/dist/core/mcp-tool-impact.js +74 -17
- package/dist/core/mcp-tool-response-file.d.ts +6 -4
- package/dist/core/mcp-tool-response-file.js +8 -8
- package/dist/core/output-contract.js +189 -2
- package/dist/reporting/quote-server-text.d.ts +3 -0
- package/dist/reporting/quote-server-text.js +20 -0
- package/dist/reporting/render-mcp-prompt-catalog-report.d.ts +2 -0
- package/dist/reporting/render-mcp-prompt-catalog-report.js +31 -0
- package/dist/reporting/render-mcp-prompt-diff-markdown.d.ts +2 -0
- package/dist/reporting/render-mcp-prompt-diff-markdown.js +39 -0
- package/dist/reporting/render-mcp-prompt-diff-report.d.ts +2 -0
- package/dist/reporting/render-mcp-prompt-diff-report.js +48 -0
- package/dist/reporting/render-mcp-prompt-file-report.d.ts +5 -0
- package/dist/reporting/render-mcp-prompt-file-report.js +33 -0
- package/dist/reporting/render-mcp-tool-diff-markdown.js +10 -2
- package/dist/reporting/render-mcp-tool-diff-report.js +4 -1
- package/dist/run-cli.d.ts +7 -0
- package/dist/run-cli.js +206 -137
- package/package.json +1 -1
package/dist/run-cli.js
CHANGED
|
@@ -25,13 +25,20 @@ import { isValidBearerToken } from "./core/bounded-http-client.js";
|
|
|
25
25
|
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
|
+
import { captureMcpPromptCatalog, captureStdioMcpPromptCatalog } from "./core/mcp-prompt-catalog.js";
|
|
29
|
+
import { compareMcpPromptFiles } from "./core/mcp-prompt-diff.js";
|
|
30
|
+
import { inspectMcpPromptFile } from "./core/mcp-prompt-file.js";
|
|
28
31
|
import { inspectMcpToolFile, isBlockedOfflinePath } from "./core/mcp-tool-file.js";
|
|
29
|
-
import {
|
|
32
|
+
import { saveMcpListResponse } from "./core/mcp-tool-response-file.js";
|
|
30
33
|
import { compareMcpToolFiles } from "./core/mcp-tool-diff.js";
|
|
31
34
|
import { renderMcpToolDiffMarkdown } from "./reporting/render-mcp-tool-diff-markdown.js";
|
|
32
35
|
import { renderMcpToolDiffReport } from "./reporting/render-mcp-tool-diff-report.js";
|
|
33
36
|
import { renderMcpToolFileReport } from "./reporting/render-mcp-tool-file-report.js";
|
|
34
37
|
import { renderMcpToolCatalogReport } from "./reporting/render-mcp-tool-catalog-report.js";
|
|
38
|
+
import { renderMcpPromptCatalogReport } from "./reporting/render-mcp-prompt-catalog-report.js";
|
|
39
|
+
import { renderMcpPromptDiffMarkdown } from "./reporting/render-mcp-prompt-diff-markdown.js";
|
|
40
|
+
import { renderMcpPromptDiffReport } from "./reporting/render-mcp-prompt-diff-report.js";
|
|
41
|
+
import { renderMcpPromptFileReport } from "./reporting/render-mcp-prompt-file-report.js";
|
|
35
42
|
import { renderMcpDiscoveryReport } from "./reporting/render-mcp-discovery-report.js";
|
|
36
43
|
import { buildDoctorValidationCorpusReport, renderDoctorValidationCorpusJson, renderDoctorValidationCorpusReport } from "./core/validation-corpus.js";
|
|
37
44
|
import { buildExternalValidationCorpusReport, ExternalCorpusManifestError } from "./core/external-validation-corpus.js";
|
|
@@ -111,9 +118,14 @@ function writeExactStdout(io, message) {
|
|
|
111
118
|
}
|
|
112
119
|
class CliUsageError extends Error {
|
|
113
120
|
}
|
|
114
|
-
const toolsDiffUsage = "Usage: codex-plugin-doctor doctor tools-diff --before <path> --after <path> [--json|--markdown] [--fail-on any|breaking]";
|
|
121
|
+
const toolsDiffUsage = "Usage: codex-plugin-doctor doctor tools-diff --before <path> --after <path> [--json|--markdown] [--fail-on any|breaking] [--show-names]";
|
|
115
122
|
const toolsFileUsage = "Usage: codex-plugin-doctor doctor tools-file <path> [--json]";
|
|
123
|
+
const promptsDiffUsage = "Usage: codex-plugin-doctor doctor prompts-diff --before <path> --after <path> [--json|--markdown] [--fail-on any|breaking] [--show-names]";
|
|
124
|
+
const promptsFileUsage = "Usage: codex-plugin-doctor doctor prompts-file <path> [--json]";
|
|
116
125
|
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>]";
|
|
126
|
+
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 };
|
|
117
129
|
const discoveryUsage = "Usage: codex-plugin-doctor doctor discover <url> --allow-network [--allow-local-network] [--json] [--bearer-token-env <NAME>]";
|
|
118
130
|
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.";
|
|
119
131
|
const ENVIRONMENT_VARIABLE_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/u;
|
|
@@ -165,33 +177,90 @@ function resolveBearerToken(name, env) {
|
|
|
165
177
|
return { error: `Bearer token environment variable does not hold a valid bearer token: ${name}` };
|
|
166
178
|
return { token };
|
|
167
179
|
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
io.writeStdout(
|
|
171
|
-
|
|
172
|
-
:
|
|
173
|
-
const exitCode = report.status === "pass" ? 0
|
|
174
|
-
: report.status === "incomplete" || report.status === "blocked" ? 2 : 1;
|
|
180
|
+
// Writes the catalog report and, when asked, saves a complete enumeration as one list response.
|
|
181
|
+
async function finishCatalogCommand(io, kind, status, output, items, cache, savePath) {
|
|
182
|
+
io.writeStdout(output);
|
|
183
|
+
const exitCode = status === "pass" ? 0
|
|
184
|
+
: status === "incomplete" || status === "blocked" ? 2 : 1;
|
|
175
185
|
if (savePath === undefined)
|
|
176
186
|
return exitCode;
|
|
177
|
-
if (
|
|
178
|
-
io.writeStderr(
|
|
187
|
+
if (items === null || cache === null) {
|
|
188
|
+
io.writeStderr(`${kind.noun} catalog response was not saved because enumeration did not complete.`);
|
|
179
189
|
return 2;
|
|
180
190
|
}
|
|
181
|
-
const saved = await
|
|
191
|
+
const saved = await saveMcpListResponse(savePath, kind.name, items, cache);
|
|
182
192
|
if (saved.kind !== "saved") {
|
|
183
193
|
io.writeStderr({
|
|
184
|
-
"path-blocked":
|
|
185
|
-
"not-regular":
|
|
186
|
-
"too-large":
|
|
187
|
-
"write-failed":
|
|
194
|
+
"path-blocked": `${kind.noun} catalog response was not saved because the path is not a local file path.`,
|
|
195
|
+
"not-regular": `${kind.noun} catalog response was not saved because the existing target is not a regular file.`,
|
|
196
|
+
"too-large": `${kind.noun} catalog response was not saved because it exceeds the one MiB ${kind.name}-file limit.`,
|
|
197
|
+
"write-failed": `${kind.noun} catalog response could not be written.`
|
|
188
198
|
}[saved.kind]);
|
|
189
199
|
return 2;
|
|
190
200
|
}
|
|
191
|
-
io.writeStderr(`Saved a complete
|
|
201
|
+
io.writeStderr(`Saved a complete ${kind.method} response with ${saved.count} ${kind.name} (${saved.bytes} bytes).`);
|
|
192
202
|
return exitCode;
|
|
193
203
|
}
|
|
194
|
-
|
|
204
|
+
function finishToolCatalogCommand(io, snapshot, json, savePath) {
|
|
205
|
+
const report = snapshot.report;
|
|
206
|
+
return finishCatalogCommand(io, TOOL_CATALOG_COMMAND, report.status, json ? JSON.stringify(report, null, 2) : renderMcpToolCatalogReport(report), snapshot.tools, snapshot.cache, savePath);
|
|
207
|
+
}
|
|
208
|
+
function finishPromptCatalogCommand(io, snapshot, json, savePath) {
|
|
209
|
+
const report = snapshot.report;
|
|
210
|
+
return finishCatalogCommand(io, PROMPT_CATALOG_COMMAND, report.status, json ? JSON.stringify(report, null, 2) : renderMcpPromptCatalogReport(report), snapshot.prompts, snapshot.cache, savePath);
|
|
211
|
+
}
|
|
212
|
+
async function runHttpCatalogCommand(url, flags, io, env, kind, run) {
|
|
213
|
+
const allowedFlags = new Set(["--allow-network", "--allow-local-network", "--json"]);
|
|
214
|
+
const switches = [];
|
|
215
|
+
let savePath;
|
|
216
|
+
let tokenEnv;
|
|
217
|
+
let invalid = false;
|
|
218
|
+
for (let index = 0; index < flags.length; index += 1) {
|
|
219
|
+
const flag = flags[index];
|
|
220
|
+
if ((flag === "--save-response" && savePath === undefined) || (flag === "--bearer-token-env" && tokenEnv === undefined)) {
|
|
221
|
+
const value = flags[index + 1];
|
|
222
|
+
if (!value || value.startsWith("-")) {
|
|
223
|
+
invalid = true;
|
|
224
|
+
break;
|
|
225
|
+
}
|
|
226
|
+
if (flag === "--save-response")
|
|
227
|
+
savePath = value;
|
|
228
|
+
else
|
|
229
|
+
tokenEnv = value;
|
|
230
|
+
index += 1;
|
|
231
|
+
}
|
|
232
|
+
else if (allowedFlags.has(flag) && !switches.includes(flag)) {
|
|
233
|
+
switches.push(flag);
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
invalid = true;
|
|
237
|
+
break;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
if (invalid) {
|
|
241
|
+
io.writeStderr(`Invalid or duplicate ${kind.noun.toLowerCase()} catalog arguments. ${kind.usage}`);
|
|
242
|
+
return 2;
|
|
243
|
+
}
|
|
244
|
+
if (!switches.includes("--allow-network")) {
|
|
245
|
+
io.writeStderr(`doctor ${kind.name} requires explicit --allow-network consent.`);
|
|
246
|
+
return 2;
|
|
247
|
+
}
|
|
248
|
+
if (savePath !== undefined && isBlockedOfflinePath(savePath)) {
|
|
249
|
+
io.writeStderr(`doctor ${kind.name} --save-response requires a local file path.`);
|
|
250
|
+
return 2;
|
|
251
|
+
}
|
|
252
|
+
const bearer = resolveBearerToken(tokenEnv, env);
|
|
253
|
+
if ("error" in bearer) {
|
|
254
|
+
io.writeStderr(bearer.error);
|
|
255
|
+
return 2;
|
|
256
|
+
}
|
|
257
|
+
return run(url, {
|
|
258
|
+
allowNetwork: true,
|
|
259
|
+
allowLocalNetwork: switches.includes("--allow-local-network"),
|
|
260
|
+
...(bearer.token === undefined ? {} : { bearerToken: bearer.token })
|
|
261
|
+
}, switches.includes("--json"), savePath);
|
|
262
|
+
}
|
|
263
|
+
async function runStdioCatalogCommand(targetPath, flags, io, env, kind, run) {
|
|
195
264
|
const allowedSwitches = new Set(["--runtime", "--json", "--require-runtime-approval"]);
|
|
196
265
|
const switches = [];
|
|
197
266
|
const values = new Map();
|
|
@@ -231,11 +300,11 @@ async function runStdioToolCatalogCommand(targetPath, flags, io, options, env) {
|
|
|
231
300
|
const approvalDigest = values.get("--runtime-approval-digest");
|
|
232
301
|
const requireApproval = switches.includes("--require-runtime-approval");
|
|
233
302
|
if (invalid || serverName === undefined || (sandbox !== undefined && sandbox !== "docker")) {
|
|
234
|
-
io.writeStderr(`Invalid or duplicate
|
|
303
|
+
io.writeStderr(`Invalid or duplicate ${kind.noun.toLowerCase()} catalog arguments. ${kind.usage}`);
|
|
235
304
|
return 2;
|
|
236
305
|
}
|
|
237
306
|
if (!switches.includes("--runtime")) {
|
|
238
|
-
io.writeStderr(
|
|
307
|
+
io.writeStderr(`doctor ${kind.name} --server starts a local MCP server and requires explicit --runtime consent.`);
|
|
239
308
|
return 2;
|
|
240
309
|
}
|
|
241
310
|
if (approvalDigest !== undefined && !requireApproval) {
|
|
@@ -262,14 +331,14 @@ async function runStdioToolCatalogCommand(targetPath, flags, io, options, env) {
|
|
|
262
331
|
}
|
|
263
332
|
}
|
|
264
333
|
if (isBlockedOfflinePath(targetPath)) {
|
|
265
|
-
io.writeStderr(
|
|
334
|
+
io.writeStderr(`doctor ${kind.name} --server requires a local package path.`);
|
|
266
335
|
return 2;
|
|
267
336
|
}
|
|
268
337
|
if (savePath !== undefined && isBlockedOfflinePath(savePath)) {
|
|
269
|
-
io.writeStderr(
|
|
338
|
+
io.writeStderr(`doctor ${kind.name} --save-response requires a local file path.`);
|
|
270
339
|
return 2;
|
|
271
340
|
}
|
|
272
|
-
const resolved = await resolveMcpStdioServer(targetPath, serverName);
|
|
341
|
+
const resolved = await resolveMcpStdioServer(targetPath, serverName, kind.name);
|
|
273
342
|
if (!resolved.ok) {
|
|
274
343
|
io.writeStderr(resolved.message);
|
|
275
344
|
return 2;
|
|
@@ -290,8 +359,74 @@ async function runStdioToolCatalogCommand(targetPath, flags, io, options, env) {
|
|
|
290
359
|
return 1;
|
|
291
360
|
}
|
|
292
361
|
}
|
|
293
|
-
|
|
294
|
-
|
|
362
|
+
return run(resolved.server, sandbox !== "docker" ? {} : environment.length > 0 ? { sandbox: "docker", environment } : { sandbox: "docker" }, switches.includes("--json"), savePath);
|
|
363
|
+
}
|
|
364
|
+
// Runs tools-diff or prompts-diff, which share their options and exit codes.
|
|
365
|
+
async function runOfflineDiffCommand(args, io, usage, compare, render) {
|
|
366
|
+
let beforePath;
|
|
367
|
+
let afterPath;
|
|
368
|
+
let format;
|
|
369
|
+
let failOn;
|
|
370
|
+
let showNames = false;
|
|
371
|
+
let invalid = false;
|
|
372
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
373
|
+
const flag = args[index];
|
|
374
|
+
if (flag === "--show-names" && !showNames) {
|
|
375
|
+
showNames = true;
|
|
376
|
+
}
|
|
377
|
+
else if ((flag === "--json" || flag === "--markdown") && format === undefined) {
|
|
378
|
+
format = flag === "--json" ? "json" : "markdown";
|
|
379
|
+
}
|
|
380
|
+
else if (flag === "--fail-on" && failOn === undefined) {
|
|
381
|
+
const value = args[index + 1];
|
|
382
|
+
if (value !== "any" && value !== "breaking") {
|
|
383
|
+
invalid = true;
|
|
384
|
+
break;
|
|
385
|
+
}
|
|
386
|
+
failOn = value;
|
|
387
|
+
index += 1;
|
|
388
|
+
}
|
|
389
|
+
else if ((flag === "--before" && beforePath === undefined) || (flag === "--after" && afterPath === undefined)) {
|
|
390
|
+
const value = args[index + 1];
|
|
391
|
+
if (!value || value.startsWith("-")) {
|
|
392
|
+
invalid = true;
|
|
393
|
+
break;
|
|
394
|
+
}
|
|
395
|
+
if (flag === "--before")
|
|
396
|
+
beforePath = value;
|
|
397
|
+
else
|
|
398
|
+
afterPath = value;
|
|
399
|
+
index += 1;
|
|
400
|
+
}
|
|
401
|
+
else {
|
|
402
|
+
invalid = true;
|
|
403
|
+
break;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
if (invalid || !beforePath || !afterPath) {
|
|
407
|
+
io.writeStderr(`Invalid comparison arguments. ${usage}`);
|
|
408
|
+
return 2;
|
|
409
|
+
}
|
|
410
|
+
const report = await compare(beforePath, afterPath, showNames);
|
|
411
|
+
io.writeStdout(format === "json" ? JSON.stringify(report, null, 2) : render(report, format === "markdown"));
|
|
412
|
+
if (report.status !== "pass" && report.status !== "warn")
|
|
413
|
+
return 2;
|
|
414
|
+
if (failOn === "breaking") {
|
|
415
|
+
return (report.comparison.breaking ?? 0) + (report.comparison.unclassified ?? 0) > 0 ? 1 : 0;
|
|
416
|
+
}
|
|
417
|
+
return report.status === "pass" ? 0 : 1;
|
|
418
|
+
}
|
|
419
|
+
// Runs tools-file or prompts-file, which share their options and exit codes.
|
|
420
|
+
async function runOfflineFileCommand(args, io, usage, inspect, render) {
|
|
421
|
+
const [filePath, ...flags] = args;
|
|
422
|
+
if (!filePath || filePath.startsWith("-") || flags.some((flag) => flag !== "--json") || flags.length > 1) {
|
|
423
|
+
io.writeStderr(`Invalid file inspection arguments. ${usage}`);
|
|
424
|
+
return 2;
|
|
425
|
+
}
|
|
426
|
+
const report = await inspect(filePath);
|
|
427
|
+
io.writeStdout(flags.includes("--json") ? JSON.stringify(report, null, 2) : render(report));
|
|
428
|
+
return report.status === "pass" ? 0
|
|
429
|
+
: report.status === "warn" || report.status === "fail" ? 1 : 2;
|
|
295
430
|
}
|
|
296
431
|
function parseRuntimeSandbox(flags, options = { requireRuntime: true }) {
|
|
297
432
|
const index = flags.indexOf("--sandbox");
|
|
@@ -377,8 +512,11 @@ function printUsage(io) {
|
|
|
377
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");
|
|
378
513
|
io.writeStderr(discoveryUsage);
|
|
379
514
|
io.writeStderr(toolsUsage);
|
|
515
|
+
io.writeStderr(promptsUsage);
|
|
380
516
|
io.writeStderr(toolsFileUsage);
|
|
381
517
|
io.writeStderr(toolsDiffUsage);
|
|
518
|
+
io.writeStderr(promptsFileUsage);
|
|
519
|
+
io.writeStderr(promptsDiffUsage);
|
|
382
520
|
io.writeStderr("Registry readiness: codex-plugin-doctor registry check <server.json|directory> [--json] [--output <path>] [--require-registry-readiness]\n"
|
|
383
521
|
+ " codex-plugin-doctor registry inspect <server-name> --allow-network [--json] [--output <path>] [--require-registry-readiness]\n"
|
|
384
522
|
+ "Registry publication preflight: codex-plugin-doctor registry preflight <server.json|directory> [--allow-network] [--json] [--output <path>] [--require-publish-ready]");
|
|
@@ -1342,10 +1480,10 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
1342
1480
|
(command === "doctor" &&
|
|
1343
1481
|
(["runtime-plan", "runtime-policy"].includes(maybePath ?? "") ||
|
|
1344
1482
|
(maybePath === "release-evidence" && remainingArgs[0] !== "verify") ||
|
|
1345
|
-
(maybePath === "tools" && remainingArgs.includes("--server")))) ||
|
|
1483
|
+
((maybePath === "tools" || maybePath === "prompts") && remainingArgs.includes("--server")))) ||
|
|
1346
1484
|
(command === "init-ci" && args.includes("--tools-diff-server"));
|
|
1347
1485
|
if (args.includes("--sandbox") && !runtimeSandboxSupported) {
|
|
1348
|
-
io.writeStderr("--sandbox is supported only by single-package check, release check, runtime-plan, runtime-policy, release-evidence, doctor tools --server, and init-ci --tools-diff-server.");
|
|
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.");
|
|
1349
1487
|
return 2;
|
|
1350
1488
|
}
|
|
1351
1489
|
if (command === "--version" || command === "-v" || command === "version") {
|
|
@@ -1393,76 +1531,39 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
1393
1531
|
if (command === "doctor") {
|
|
1394
1532
|
if (maybePath === "tools-diff") {
|
|
1395
1533
|
if (remainingArgs.length === 1 && remainingArgs[0] === "--help") {
|
|
1396
|
-
io.writeStdout(`${toolsDiffUsage}\nCompares two saved MCP tools/list responses offline. Changes are classified heuristically as breaking, compatible, or unclassified; compatibility is not tested.\n--fail-on breaking exits 1 only for breaking or unclassified changes (default: any change or input warning)
|
|
1534
|
+
io.writeStdout(`${toolsDiffUsage}\nCompares two saved MCP tools/list responses offline. Changes are classified heuristically as breaking, compatible, or unclassified; compatibility is not tested.\n--fail-on breaking exits 1 only for breaking or unclassified changes (default: any change or input warning).
|
|
1535
|
+
--show-names adds tool names and the JSON Pointer of each reason; reports omit both by default, so keep it out of public output such as pull request comments.`);
|
|
1397
1536
|
return 0;
|
|
1398
1537
|
}
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
else if (flag === "--fail-on" && failOn === undefined) {
|
|
1410
|
-
const value = remainingArgs[index + 1];
|
|
1411
|
-
if (value !== "any" && value !== "breaking") {
|
|
1412
|
-
invalid = true;
|
|
1413
|
-
break;
|
|
1414
|
-
}
|
|
1415
|
-
failOn = value;
|
|
1416
|
-
index += 1;
|
|
1417
|
-
}
|
|
1418
|
-
else if ((flag === "--before" && beforePath === undefined) || (flag === "--after" && afterPath === undefined)) {
|
|
1419
|
-
const value = remainingArgs[index + 1];
|
|
1420
|
-
if (!value || value.startsWith("-")) {
|
|
1421
|
-
invalid = true;
|
|
1422
|
-
break;
|
|
1423
|
-
}
|
|
1424
|
-
if (flag === "--before")
|
|
1425
|
-
beforePath = value;
|
|
1426
|
-
else
|
|
1427
|
-
afterPath = value;
|
|
1428
|
-
index += 1;
|
|
1429
|
-
}
|
|
1430
|
-
else {
|
|
1431
|
-
invalid = true;
|
|
1432
|
-
break;
|
|
1433
|
-
}
|
|
1434
|
-
}
|
|
1435
|
-
if (invalid || !beforePath || !afterPath) {
|
|
1436
|
-
io.writeStderr(`Invalid comparison arguments. ${toolsDiffUsage}`);
|
|
1437
|
-
return 2;
|
|
1438
|
-
}
|
|
1439
|
-
const report = await (options.compareMcpToolFilesImpl ?? compareMcpToolFiles)(beforePath, afterPath);
|
|
1440
|
-
io.writeStdout(format === "json" ? JSON.stringify(report, null, 2)
|
|
1441
|
-
: format === "markdown" ? renderMcpToolDiffMarkdown(report)
|
|
1442
|
-
: renderMcpToolDiffReport(report));
|
|
1443
|
-
if (report.status !== "pass" && report.status !== "warn")
|
|
1444
|
-
return 2;
|
|
1445
|
-
if (failOn === "breaking") {
|
|
1446
|
-
return (report.comparison.breaking ?? 0) + (report.comparison.unclassified ?? 0) > 0 ? 1 : 0;
|
|
1538
|
+
return runOfflineDiffCommand(remainingArgs, io, toolsDiffUsage, (beforePath, afterPath, showNames) => {
|
|
1539
|
+
const compare = options.compareMcpToolFilesImpl ?? compareMcpToolFiles;
|
|
1540
|
+
return showNames ? compare(beforePath, afterPath, { showNames: true }) : compare(beforePath, afterPath);
|
|
1541
|
+
}, (report, markdown) => markdown ? renderMcpToolDiffMarkdown(report) : renderMcpToolDiffReport(report));
|
|
1542
|
+
}
|
|
1543
|
+
if (maybePath === "prompts-diff") {
|
|
1544
|
+
if (remainingArgs.length === 1 && remainingArgs[0] === "--help") {
|
|
1545
|
+
io.writeStdout(`${promptsDiffUsage}\nCompares two saved MCP prompts/list responses offline. Changes are classified heuristically as breaking, compatible, or unclassified; prompts are not retrieved and compatibility is not tested.\n--fail-on breaking exits 1 only for breaking or unclassified changes (default: any change or input warning).
|
|
1546
|
+
--show-names adds prompt names and the argument behind each reason; reports omit both by default, so keep it out of public output such as pull request comments.`);
|
|
1547
|
+
return 0;
|
|
1447
1548
|
}
|
|
1448
|
-
return
|
|
1549
|
+
return runOfflineDiffCommand(remainingArgs, io, promptsDiffUsage, (beforePath, afterPath, showNames) => {
|
|
1550
|
+
const compare = options.compareMcpPromptFilesImpl ?? compareMcpPromptFiles;
|
|
1551
|
+
return showNames ? compare(beforePath, afterPath, { showNames: true }) : compare(beforePath, afterPath);
|
|
1552
|
+
}, (report, markdown) => markdown ? renderMcpPromptDiffMarkdown(report) : renderMcpPromptDiffReport(report));
|
|
1449
1553
|
}
|
|
1450
1554
|
if (maybePath === "tools-file") {
|
|
1451
1555
|
if (remainingArgs.length === 1 && remainingArgs[0] === "--help") {
|
|
1452
1556
|
io.writeStdout(`${toolsFileUsage}\nInspects one saved UTF-8 MCP tools/list response. No server connection or tool execution.`);
|
|
1453
1557
|
return 0;
|
|
1454
1558
|
}
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1559
|
+
return runOfflineFileCommand(remainingArgs, io, toolsFileUsage, options.inspectMcpToolFileImpl ?? inspectMcpToolFile, renderMcpToolFileReport);
|
|
1560
|
+
}
|
|
1561
|
+
if (maybePath === "prompts-file") {
|
|
1562
|
+
if (remainingArgs.length === 1 && remainingArgs[0] === "--help") {
|
|
1563
|
+
io.writeStdout(`${promptsFileUsage}\nInspects one saved UTF-8 MCP prompts/list response. No server connection or prompt retrieval.`);
|
|
1564
|
+
return 0;
|
|
1459
1565
|
}
|
|
1460
|
-
|
|
1461
|
-
io.writeStdout(flags.includes("--json")
|
|
1462
|
-
? JSON.stringify(report, null, 2)
|
|
1463
|
-
: renderMcpToolFileReport(report));
|
|
1464
|
-
return report.status === "pass" ? 0
|
|
1465
|
-
: report.status === "warn" || report.status === "fail" ? 1 : 2;
|
|
1566
|
+
return runOfflineFileCommand(remainingArgs, io, promptsFileUsage, options.inspectMcpPromptFileImpl ?? inspectMcpPromptFile, renderMcpPromptFileReport);
|
|
1466
1567
|
}
|
|
1467
1568
|
if (maybePath === "tools") {
|
|
1468
1569
|
if (remainingArgs.length === 1 && remainingArgs[0] === "--help") {
|
|
@@ -1475,61 +1576,29 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
1475
1576
|
return 2;
|
|
1476
1577
|
}
|
|
1477
1578
|
if (flags.includes("--server")) {
|
|
1478
|
-
return
|
|
1479
|
-
}
|
|
1480
|
-
const allowedFlags = new Set(["--allow-network", "--allow-local-network", "--json"]);
|
|
1481
|
-
const switches = [];
|
|
1482
|
-
let savePath;
|
|
1483
|
-
let tokenEnv;
|
|
1484
|
-
let invalid = false;
|
|
1485
|
-
for (let index = 0; index < flags.length; index += 1) {
|
|
1486
|
-
const flag = flags[index];
|
|
1487
|
-
if ((flag === "--save-response" && savePath === undefined) || (flag === "--bearer-token-env" && tokenEnv === undefined)) {
|
|
1488
|
-
const value = flags[index + 1];
|
|
1489
|
-
if (!value || value.startsWith("-")) {
|
|
1490
|
-
invalid = true;
|
|
1491
|
-
break;
|
|
1492
|
-
}
|
|
1493
|
-
if (flag === "--save-response")
|
|
1494
|
-
savePath = value;
|
|
1495
|
-
else
|
|
1496
|
-
tokenEnv = value;
|
|
1497
|
-
index += 1;
|
|
1498
|
-
}
|
|
1499
|
-
else if (allowedFlags.has(flag) && !switches.includes(flag)) {
|
|
1500
|
-
switches.push(flag);
|
|
1501
|
-
}
|
|
1502
|
-
else {
|
|
1503
|
-
invalid = true;
|
|
1504
|
-
break;
|
|
1505
|
-
}
|
|
1579
|
+
return runStdioCatalogCommand(url, flags, io, terminalContext.env, TOOL_CATALOG_COMMAND, async (server, captureOptions, json, savePath) => finishToolCatalogCommand(io, await (options.captureStdioMcpToolCatalogImpl ?? captureStdioMcpToolCatalog)(server, captureOptions), json, savePath));
|
|
1506
1580
|
}
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1581
|
+
return runHttpCatalogCommand(url, flags, io, terminalContext.env, TOOL_CATALOG_COMMAND, async (target, catalogOptions, json, savePath) => {
|
|
1582
|
+
const snapshot = savePath === undefined
|
|
1583
|
+
? { report: await (options.inspectMcpToolCatalogImpl ?? inspectMcpToolCatalog)(target, catalogOptions), tools: null, cache: null }
|
|
1584
|
+
: await (options.captureMcpToolCatalogImpl ?? captureMcpToolCatalog)(target, catalogOptions);
|
|
1585
|
+
return finishToolCatalogCommand(io, snapshot, json, savePath);
|
|
1586
|
+
});
|
|
1587
|
+
}
|
|
1588
|
+
if (maybePath === "prompts") {
|
|
1589
|
+
if (remainingArgs.length === 1 && remainingArgs[0] === "--help") {
|
|
1590
|
+
io.writeStdout(`${promptsUsage}\nPrompt catalog structure only; prompts are not retrieved.\n--server starts the named stdio server from the package MCP configuration after explicit --runtime consent and stops it after reading the prompt list.\n${forwardedEnvironmentHelp}\n--save-response writes a complete enumeration as one prompts/list response for doctor prompts-file and prompts-diff; nothing is written when enumeration is incomplete.\n${bearerTokenHelp}`);
|
|
1591
|
+
return 0;
|
|
1514
1592
|
}
|
|
1515
|
-
|
|
1516
|
-
|
|
1593
|
+
const [target, ...flags] = remainingArgs;
|
|
1594
|
+
if (!target || target.startsWith("-")) {
|
|
1595
|
+
io.writeStderr(promptsUsage);
|
|
1517
1596
|
return 2;
|
|
1518
1597
|
}
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
io.writeStderr(bearer.error);
|
|
1522
|
-
return 2;
|
|
1598
|
+
if (flags.includes("--server")) {
|
|
1599
|
+
return runStdioCatalogCommand(target, flags, io, terminalContext.env, PROMPT_CATALOG_COMMAND, async (server, captureOptions, json, savePath) => finishPromptCatalogCommand(io, await (options.captureStdioMcpPromptCatalogImpl ?? captureStdioMcpPromptCatalog)(server, captureOptions), json, savePath));
|
|
1523
1600
|
}
|
|
1524
|
-
|
|
1525
|
-
allowNetwork: true,
|
|
1526
|
-
allowLocalNetwork: switches.includes("--allow-local-network"),
|
|
1527
|
-
...(bearer.token === undefined ? {} : { bearerToken: bearer.token })
|
|
1528
|
-
};
|
|
1529
|
-
const snapshot = savePath === undefined
|
|
1530
|
-
? { report: await (options.inspectMcpToolCatalogImpl ?? inspectMcpToolCatalog)(url, catalogOptions), tools: null, cache: null }
|
|
1531
|
-
: await (options.captureMcpToolCatalogImpl ?? captureMcpToolCatalog)(url, catalogOptions);
|
|
1532
|
-
return finishToolCatalogCommand(io, snapshot, switches.includes("--json"), savePath);
|
|
1601
|
+
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));
|
|
1533
1602
|
}
|
|
1534
1603
|
if (maybePath === "discover") {
|
|
1535
1604
|
if (remainingArgs.length === 1 && remainingArgs[0] === "--help") {
|
package/package.json
CHANGED