codex-plugin-doctor 0.2.1 → 0.4.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 -1
- package/dist/compatibility/apply-install-preview.d.ts +15 -0
- package/dist/compatibility/apply-install-preview.js +71 -0
- package/dist/compatibility/compatibility-matrix.d.ts +1 -0
- package/dist/compatibility/compatibility-matrix.js +92 -11
- package/dist/compatibility/cursor-install-preview.d.ts +10 -0
- package/dist/compatibility/cursor-install-preview.js +65 -0
- package/dist/core/read-json-file.d.ts +2 -0
- package/dist/core/read-json-file.js +8 -0
- package/dist/reporting/render-compatibility-scorecard.d.ts +2 -0
- package/dist/reporting/render-compatibility-scorecard.js +21 -0
- package/dist/run-cli.js +63 -9
- package/examples/README.md +73 -0
- package/examples/codex-doctor-risky/.codex-plugin/plugin.json +7 -0
- package/examples/codex-doctor-risky/.mcp.json +12 -0
- package/examples/codex-doctor-risky/mock-server.js +1 -0
- package/examples/codex-doctor-runtime/.codex-plugin/plugin.json +8 -0
- package/examples/codex-doctor-runtime/.mcp.json +9 -0
- package/examples/codex-doctor-runtime/mock-server.js +237 -0
- package/examples/codex-doctor-runtime/skills/context-check/SKILL.md +7 -0
- package/examples/codex-doctor-starter/.codex-plugin/plugin.json +7 -0
- package/examples/codex-doctor-starter/skills/repo-scan/SKILL.md +7 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
# Codex Plugin Doctor
|
|
2
2
|
|
|
3
3
|
[](https://github.com/Esquetta/CodexPluginDoctor/actions/workflows/ci.yml)
|
|
4
|
+
[](https://www.npmjs.com/package/codex-plugin-doctor)
|
|
5
|
+
[](./LICENSE)
|
|
6
|
+
[](https://github.com/Esquetta/CodexPluginDoctor/releases)
|
|
4
7
|
|
|
5
8
|
Codex Plugin Doctor is a local CLI validator for Codex plugin packages, skills, and MCP server bundles.
|
|
6
9
|
|
|
@@ -72,6 +75,7 @@ Global install from npm:
|
|
|
72
75
|
```bash
|
|
73
76
|
npm install -g codex-plugin-doctor
|
|
74
77
|
codex-plugin-doctor --version
|
|
78
|
+
codex-plugin-doctor self-test
|
|
75
79
|
codex-plugin-doctor check path/to/plugin-package
|
|
76
80
|
```
|
|
77
81
|
|
|
@@ -156,12 +160,18 @@ Run these from a Codex plugin package root:
|
|
|
156
160
|
|
|
157
161
|
```bash
|
|
158
162
|
codex-plugin-doctor --version
|
|
163
|
+
codex-plugin-doctor self-test
|
|
159
164
|
codex-plugin-doctor init my-plugin
|
|
160
165
|
codex-plugin-doctor compat .
|
|
161
166
|
codex-plugin-doctor compat . --client codex
|
|
162
167
|
codex-plugin-doctor compat . --client generic-mcp
|
|
163
168
|
codex-plugin-doctor compat . --client claude-desktop
|
|
164
169
|
codex-plugin-doctor compat . --client claude-desktop --install-preview
|
|
170
|
+
codex-plugin-doctor compat . --client claude-desktop --apply --backup
|
|
171
|
+
codex-plugin-doctor compat . --client cursor
|
|
172
|
+
codex-plugin-doctor compat . --client cursor --install-preview
|
|
173
|
+
codex-plugin-doctor compat . --client cursor --apply --backup
|
|
174
|
+
codex-plugin-doctor compat . --scorecard
|
|
165
175
|
codex-plugin-doctor compat . --json
|
|
166
176
|
codex-plugin-doctor compat . --json --output compatibility.json
|
|
167
177
|
codex-plugin-doctor check .
|
|
@@ -176,7 +186,13 @@ codex-plugin-doctor check . --config .codex-doctor.json
|
|
|
176
186
|
codex-plugin-doctor check . --json --runtime --verbose-runtime
|
|
177
187
|
```
|
|
178
188
|
|
|
179
|
-
`
|
|
189
|
+
`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`.
|
|
190
|
+
|
|
191
|
+
`compat --client claude-desktop` checks whether the MCP package can be added to the local Claude Desktop setup. On Windows it looks for `%APPDATA%\Claude\claude_desktop_config.json`; on macOS it looks for `~/Library/Application Support/Claude/claude_desktop_config.json`. A valid existing config returns `PASS`, a missing Claude Desktop install returns `WARN`, and a malformed local config returns `FAIL` so you do not add new servers into a broken config file. If the package server name already exists in Claude Desktop, the command returns `WARN` with the duplicate server name. Add `--install-preview` to print the JSON snippet that should be merged into `claude_desktop_config.json`; it does not modify files. Use `--apply --backup` only when you want the CLI to create a timestamped backup and merge the server config. Apply mode refuses to overwrite duplicate server names.
|
|
192
|
+
|
|
193
|
+
`compat --client cursor` checks whether the MCP package can be added to Cursor. It prefers a project-level `.cursor/mcp.json` when one already exists in the target package, then falls back to the global `~/.cursor/mcp.json` path. A valid existing config returns `PASS`, a missing Cursor config returns `WARN`, malformed JSON returns `FAIL`, and duplicate MCP server names return `WARN`. Add `--install-preview` to print the JSON snippet that should be merged into Cursor's `mcp.json`; it does not modify files. Use `--apply --backup` only when you want the CLI to create a timestamped backup and merge the server config. Apply mode refuses to overwrite duplicate server names.
|
|
194
|
+
|
|
195
|
+
`compat --scorecard` turns the compatibility matrix into a compact score summary. `PASS` maps to `100`, `WARN` maps to `70`, and `FAIL` or `SKIPPED` maps to `0`.
|
|
180
196
|
|
|
181
197
|
Optional local policy file:
|
|
182
198
|
|
|
@@ -262,6 +278,9 @@ This runs tests, builds the TypeScript output, and performs `npm pack --dry-run`
|
|
|
262
278
|
Related docs:
|
|
263
279
|
|
|
264
280
|
- [Changelog](./CHANGELOG.md)
|
|
281
|
+
- [Contributing](./CONTRIBUTING.md)
|
|
282
|
+
- [Security Policy](./SECURITY.md)
|
|
283
|
+
- [Code of Conduct](./CODE_OF_CONDUCT.md)
|
|
265
284
|
- [NPM Release Checklist](./docs/engineering/npm-release-checklist.md)
|
|
266
285
|
- [Release Candidate Workflow](./docs/engineering/release-candidate-workflow.md)
|
|
267
286
|
- [v0.1.0 Release Notes](./docs/engineering/v0.1.0-final-release-notes.md)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface McpInstallPreviewLike {
|
|
2
|
+
targetPath: string;
|
|
3
|
+
configPath: string;
|
|
4
|
+
snippet: {
|
|
5
|
+
mcpServers: Record<string, unknown>;
|
|
6
|
+
};
|
|
7
|
+
}
|
|
8
|
+
export interface ApplyInstallResult {
|
|
9
|
+
client: string;
|
|
10
|
+
configPath: string;
|
|
11
|
+
backupPath: string | null;
|
|
12
|
+
appliedServers: string[];
|
|
13
|
+
}
|
|
14
|
+
export declare function applyInstallPreview(client: string, preview: McpInstallPreviewLike): Promise<ApplyInstallResult>;
|
|
15
|
+
export declare function renderApplyInstallResult(result: ApplyInstallResult): string;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { copyFile, mkdir, stat, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { readJsonFile } from "../core/read-json-file.js";
|
|
4
|
+
async function fileExists(targetPath) {
|
|
5
|
+
try {
|
|
6
|
+
const details = await stat(targetPath);
|
|
7
|
+
return details.isFile();
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
function isRecord(value) {
|
|
14
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
15
|
+
}
|
|
16
|
+
function buildBackupPath(configPath) {
|
|
17
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
18
|
+
return `${configPath}.${timestamp}.bak`;
|
|
19
|
+
}
|
|
20
|
+
export async function applyInstallPreview(client, preview) {
|
|
21
|
+
await mkdir(path.dirname(preview.configPath), { recursive: true });
|
|
22
|
+
const configExists = await fileExists(preview.configPath);
|
|
23
|
+
const currentConfig = configExists
|
|
24
|
+
? await readJsonFile(preview.configPath)
|
|
25
|
+
: {};
|
|
26
|
+
if (!isRecord(currentConfig)) {
|
|
27
|
+
throw new Error(`${client} MCP config must be a JSON object.`);
|
|
28
|
+
}
|
|
29
|
+
const currentServers = currentConfig.mcpServers;
|
|
30
|
+
if (currentServers !== undefined && !isRecord(currentServers)) {
|
|
31
|
+
throw new Error(`${client} MCP config has an invalid \`mcpServers\` shape.`);
|
|
32
|
+
}
|
|
33
|
+
const existingServers = currentServers ?? {};
|
|
34
|
+
const incomingServers = preview.snippet.mcpServers;
|
|
35
|
+
const duplicateServers = Object.keys(incomingServers).filter((serverName) => Object.prototype.hasOwnProperty.call(existingServers, serverName));
|
|
36
|
+
if (duplicateServers.length > 0) {
|
|
37
|
+
throw new Error(`Refusing to overwrite existing MCP server names: ${duplicateServers.join(", ")}`);
|
|
38
|
+
}
|
|
39
|
+
const backupPath = configExists ? buildBackupPath(preview.configPath) : null;
|
|
40
|
+
if (backupPath) {
|
|
41
|
+
await copyFile(preview.configPath, backupPath);
|
|
42
|
+
}
|
|
43
|
+
const nextConfig = {
|
|
44
|
+
...currentConfig,
|
|
45
|
+
mcpServers: {
|
|
46
|
+
...existingServers,
|
|
47
|
+
...incomingServers
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
await writeFile(preview.configPath, `${JSON.stringify(nextConfig, null, 2)}\n`, "utf8");
|
|
51
|
+
return {
|
|
52
|
+
client,
|
|
53
|
+
configPath: preview.configPath,
|
|
54
|
+
backupPath,
|
|
55
|
+
appliedServers: Object.keys(incomingServers)
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
export function renderApplyInstallResult(result) {
|
|
59
|
+
const lines = [
|
|
60
|
+
`Applied ${result.client} MCP config`,
|
|
61
|
+
"==============================",
|
|
62
|
+
`Config: ${result.configPath}`,
|
|
63
|
+
`Backup: ${result.backupPath ?? "No existing config file was present."}`,
|
|
64
|
+
"",
|
|
65
|
+
"Applied servers:"
|
|
66
|
+
];
|
|
67
|
+
for (const serverName of result.appliedServers) {
|
|
68
|
+
lines.push(`- ${serverName}`);
|
|
69
|
+
}
|
|
70
|
+
return lines.join("\n");
|
|
71
|
+
}
|
|
@@ -16,5 +16,6 @@ export interface CompatibilityEnvironment {
|
|
|
16
16
|
}
|
|
17
17
|
export declare function readMcpConfigPath(targetPath: string): Promise<string | null>;
|
|
18
18
|
export declare function getClaudeDesktopConfigPath(environment?: CompatibilityEnvironment): string | null;
|
|
19
|
+
export declare function getCursorMcpConfigPath(targetPath: string, environment?: CompatibilityEnvironment): Promise<string>;
|
|
19
20
|
export declare function buildCompatibilityMatrix(targetPath: string, environment?: CompatibilityEnvironment): Promise<CompatibilityMatrix>;
|
|
20
21
|
export declare function matrixExitCode(matrix: CompatibilityMatrix): 0 | 1;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { stat } from "node:fs/promises";
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { validatePlugin } from "../core/validate-plugin.js";
|
|
5
|
+
import { readJsonFile } from "../core/read-json-file.js";
|
|
5
6
|
async function fileExists(targetPath) {
|
|
6
7
|
try {
|
|
7
8
|
const details = await stat(targetPath);
|
|
@@ -40,7 +41,7 @@ export async function readMcpConfigPath(targetPath) {
|
|
|
40
41
|
return null;
|
|
41
42
|
}
|
|
42
43
|
try {
|
|
43
|
-
const manifest =
|
|
44
|
+
const manifest = await readJsonFile(manifestPath);
|
|
44
45
|
return typeof manifest.mcpServers === "string"
|
|
45
46
|
? path.resolve(rootPath, manifest.mcpServers)
|
|
46
47
|
: null;
|
|
@@ -63,7 +64,7 @@ async function checkGenericMcp(targetPath) {
|
|
|
63
64
|
};
|
|
64
65
|
}
|
|
65
66
|
try {
|
|
66
|
-
const parsed =
|
|
67
|
+
const parsed = await readJsonFile(mcpConfigPath);
|
|
67
68
|
const servers = parsed.mcpServers;
|
|
68
69
|
if (typeof servers !== "object" ||
|
|
69
70
|
servers === null ||
|
|
@@ -98,7 +99,7 @@ async function readMcpServerNames(targetPath) {
|
|
|
98
99
|
return [];
|
|
99
100
|
}
|
|
100
101
|
try {
|
|
101
|
-
const parsed =
|
|
102
|
+
const parsed = await readJsonFile(mcpConfigPath);
|
|
102
103
|
const servers = parsed.mcpServers;
|
|
103
104
|
return typeof servers === "object" && servers !== null && !Array.isArray(servers)
|
|
104
105
|
? Object.keys(servers)
|
|
@@ -121,6 +122,18 @@ export function getClaudeDesktopConfigPath(environment = {}) {
|
|
|
121
122
|
}
|
|
122
123
|
return null;
|
|
123
124
|
}
|
|
125
|
+
function getHomeDirectory(environment = {}) {
|
|
126
|
+
const env = environment.env ?? process.env;
|
|
127
|
+
return env.USERPROFILE ?? env.HOME ?? environment.homedir ?? os.homedir();
|
|
128
|
+
}
|
|
129
|
+
export async function getCursorMcpConfigPath(targetPath, environment = {}) {
|
|
130
|
+
const rootPath = path.resolve(targetPath);
|
|
131
|
+
const projectConfigPath = path.join(rootPath, ".cursor", "mcp.json");
|
|
132
|
+
if (await fileExists(projectConfigPath)) {
|
|
133
|
+
return projectConfigPath;
|
|
134
|
+
}
|
|
135
|
+
return path.join(getHomeDirectory(environment), ".cursor", "mcp.json");
|
|
136
|
+
}
|
|
124
137
|
async function checkClaudeDesktop(targetPath, genericMcpResult, environment = {}) {
|
|
125
138
|
if (genericMcpResult.status !== "pass") {
|
|
126
139
|
return {
|
|
@@ -154,7 +167,7 @@ async function checkClaudeDesktop(targetPath, genericMcpResult, environment = {}
|
|
|
154
167
|
};
|
|
155
168
|
}
|
|
156
169
|
try {
|
|
157
|
-
const parsed =
|
|
170
|
+
const parsed = await readJsonFile(configPath);
|
|
158
171
|
const servers = parsed.mcpServers;
|
|
159
172
|
if (servers !== undefined && (typeof servers !== "object" ||
|
|
160
173
|
servers === null ||
|
|
@@ -201,10 +214,83 @@ async function checkClaudeDesktop(targetPath, genericMcpResult, environment = {}
|
|
|
201
214
|
};
|
|
202
215
|
}
|
|
203
216
|
}
|
|
217
|
+
async function checkCursor(targetPath, genericMcpResult, environment = {}) {
|
|
218
|
+
if (genericMcpResult.status !== "pass") {
|
|
219
|
+
return {
|
|
220
|
+
client: "Cursor",
|
|
221
|
+
status: "skipped",
|
|
222
|
+
summary: "No valid MCP package config is available for Cursor.",
|
|
223
|
+
details: ["Add a valid `.mcp.json` with a non-empty `mcpServers` object first."]
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
const configPath = await getCursorMcpConfigPath(targetPath, environment);
|
|
227
|
+
if (!(await fileExists(configPath))) {
|
|
228
|
+
const configDirectory = path.dirname(configPath);
|
|
229
|
+
return {
|
|
230
|
+
client: "Cursor",
|
|
231
|
+
status: await directoryExists(configDirectory) ? "pass" : "warn",
|
|
232
|
+
summary: await directoryExists(configDirectory)
|
|
233
|
+
? "Cursor MCP config directory exists and a config file can be created."
|
|
234
|
+
: "Cursor was not detected on this machine.",
|
|
235
|
+
details: [
|
|
236
|
+
configPath,
|
|
237
|
+
"Cursor supports project `.cursor/mcp.json` and global `~/.cursor/mcp.json` MCP configs."
|
|
238
|
+
]
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
try {
|
|
242
|
+
const parsed = await readJsonFile(configPath);
|
|
243
|
+
const servers = parsed.mcpServers;
|
|
244
|
+
if (servers !== undefined && (typeof servers !== "object" ||
|
|
245
|
+
servers === null ||
|
|
246
|
+
Array.isArray(servers))) {
|
|
247
|
+
return {
|
|
248
|
+
client: "Cursor",
|
|
249
|
+
status: "fail",
|
|
250
|
+
summary: "Cursor MCP config has an invalid `mcpServers` shape.",
|
|
251
|
+
details: [configPath, "`mcpServers` must be an object before this package can be added safely."]
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
const packageServerNames = await readMcpServerNames(targetPath);
|
|
255
|
+
const existingServerNames = typeof servers === "object" && servers !== null
|
|
256
|
+
? Object.keys(servers)
|
|
257
|
+
: [];
|
|
258
|
+
const duplicateServerNames = packageServerNames.filter((serverName) => existingServerNames.includes(serverName));
|
|
259
|
+
if (duplicateServerNames.length > 0) {
|
|
260
|
+
return {
|
|
261
|
+
client: "Cursor",
|
|
262
|
+
status: "warn",
|
|
263
|
+
summary: "Cursor already has MCP server names from this package.",
|
|
264
|
+
details: [
|
|
265
|
+
configPath,
|
|
266
|
+
...duplicateServerNames.map((serverName) => `Duplicate server: ${serverName}`)
|
|
267
|
+
]
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
return {
|
|
271
|
+
client: "Cursor",
|
|
272
|
+
status: "pass",
|
|
273
|
+
summary: "Cursor global MCP config is valid and this package can be added.",
|
|
274
|
+
details: [
|
|
275
|
+
configPath,
|
|
276
|
+
`Source package: ${path.resolve(targetPath)}`
|
|
277
|
+
]
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
catch {
|
|
281
|
+
return {
|
|
282
|
+
client: "Cursor",
|
|
283
|
+
status: "fail",
|
|
284
|
+
summary: "Cursor MCP config is not valid JSON.",
|
|
285
|
+
details: [configPath, "Repair the local Cursor MCP config before adding new MCP servers."]
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
}
|
|
204
289
|
export async function buildCompatibilityMatrix(targetPath, environment = {}) {
|
|
205
290
|
const rootPath = path.resolve(targetPath);
|
|
206
291
|
const genericMcpResult = await checkGenericMcp(rootPath);
|
|
207
292
|
const claudeDesktopResult = await checkClaudeDesktop(rootPath, genericMcpResult, environment);
|
|
293
|
+
const cursorResult = await checkCursor(rootPath, genericMcpResult, environment);
|
|
208
294
|
const codexResult = await validatePlugin(rootPath);
|
|
209
295
|
const codexStatus = statusFromCheckResult(codexResult);
|
|
210
296
|
const codexCompatibility = !await hasCodexManifest(rootPath)
|
|
@@ -227,12 +313,7 @@ export async function buildCompatibilityMatrix(targetPath, environment = {}) {
|
|
|
227
313
|
codexCompatibility,
|
|
228
314
|
genericMcpResult,
|
|
229
315
|
claudeDesktopResult,
|
|
230
|
-
|
|
231
|
-
client: "Cursor",
|
|
232
|
-
status: "skipped",
|
|
233
|
-
summary: "Client-specific package adapter is not implemented yet.",
|
|
234
|
-
details: ["Planned adapter after generic MCP compatibility is stable."]
|
|
235
|
-
}
|
|
316
|
+
cursorResult
|
|
236
317
|
];
|
|
237
318
|
return {
|
|
238
319
|
targetPath: rootPath,
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type CompatibilityEnvironment } from "./compatibility-matrix.js";
|
|
2
|
+
export interface CursorInstallPreview {
|
|
3
|
+
targetPath: string;
|
|
4
|
+
configPath: string;
|
|
5
|
+
snippet: {
|
|
6
|
+
mcpServers: Record<string, unknown>;
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
export declare function buildCursorInstallPreview(targetPath: string, environment?: CompatibilityEnvironment): Promise<CursorInstallPreview>;
|
|
10
|
+
export declare function renderCursorInstallPreview(preview: CursorInstallPreview): string;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { getCursorMcpConfigPath, readMcpConfigPath } from "./compatibility-matrix.js";
|
|
4
|
+
function isRecord(value) {
|
|
5
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
6
|
+
}
|
|
7
|
+
function isRelativeLocalPath(value) {
|
|
8
|
+
return value.startsWith("./") ||
|
|
9
|
+
value.startsWith("../") ||
|
|
10
|
+
value.startsWith(".\\") ||
|
|
11
|
+
value.startsWith("..\\");
|
|
12
|
+
}
|
|
13
|
+
function normalizeLocalPathArgument(value, rootPath) {
|
|
14
|
+
return typeof value === "string" && isRelativeLocalPath(value)
|
|
15
|
+
? path.resolve(rootPath, value)
|
|
16
|
+
: value;
|
|
17
|
+
}
|
|
18
|
+
function normalizeServerConfig(serverConfig, rootPath) {
|
|
19
|
+
if (!isRecord(serverConfig)) {
|
|
20
|
+
return serverConfig;
|
|
21
|
+
}
|
|
22
|
+
const normalized = { ...serverConfig };
|
|
23
|
+
if (typeof normalized.command === "string" && isRelativeLocalPath(normalized.command)) {
|
|
24
|
+
normalized.command = path.resolve(rootPath, normalized.command);
|
|
25
|
+
}
|
|
26
|
+
if (Array.isArray(normalized.args)) {
|
|
27
|
+
normalized.args = normalized.args.map((argument) => normalizeLocalPathArgument(argument, rootPath));
|
|
28
|
+
}
|
|
29
|
+
return normalized;
|
|
30
|
+
}
|
|
31
|
+
export async function buildCursorInstallPreview(targetPath, environment = {}) {
|
|
32
|
+
const rootPath = path.resolve(targetPath);
|
|
33
|
+
const mcpConfigPath = await readMcpConfigPath(rootPath);
|
|
34
|
+
if (!mcpConfigPath) {
|
|
35
|
+
throw new Error("No MCP config found for install preview.");
|
|
36
|
+
}
|
|
37
|
+
const parsed = JSON.parse(await readFile(mcpConfigPath, "utf8"));
|
|
38
|
+
const servers = parsed.mcpServers;
|
|
39
|
+
if (!isRecord(servers) || Object.keys(servers).length === 0) {
|
|
40
|
+
throw new Error("MCP config does not contain a non-empty `mcpServers` object.");
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
targetPath: rootPath,
|
|
44
|
+
configPath: await getCursorMcpConfigPath(rootPath, environment),
|
|
45
|
+
snippet: {
|
|
46
|
+
mcpServers: Object.fromEntries(Object.entries(servers).map(([serverName, serverConfig]) => [
|
|
47
|
+
serverName,
|
|
48
|
+
normalizeServerConfig(serverConfig, rootPath)
|
|
49
|
+
]))
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
export function renderCursorInstallPreview(preview) {
|
|
54
|
+
return [
|
|
55
|
+
"Cursor Install Preview",
|
|
56
|
+
"======================",
|
|
57
|
+
`Target: ${preview.targetPath}`,
|
|
58
|
+
`Config: ${preview.configPath}`,
|
|
59
|
+
"",
|
|
60
|
+
"Add or merge this snippet into `mcp.json`:",
|
|
61
|
+
JSON.stringify(preview.snippet, null, 2),
|
|
62
|
+
"",
|
|
63
|
+
"No files were modified."
|
|
64
|
+
].join("\n");
|
|
65
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
export function parseJsonText(text) {
|
|
3
|
+
const normalizedText = text.startsWith("\uFEFF") ? text.slice(1) : text;
|
|
4
|
+
return JSON.parse(normalizedText);
|
|
5
|
+
}
|
|
6
|
+
export async function readJsonFile(filePath) {
|
|
7
|
+
return parseJsonText(await readFile(filePath, "utf8"));
|
|
8
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
function scoreForStatus(status) {
|
|
2
|
+
if (status === "pass") {
|
|
3
|
+
return 100;
|
|
4
|
+
}
|
|
5
|
+
if (status === "warn") {
|
|
6
|
+
return 70;
|
|
7
|
+
}
|
|
8
|
+
return 0;
|
|
9
|
+
}
|
|
10
|
+
export function renderCompatibilityScorecard(matrix) {
|
|
11
|
+
const lines = [
|
|
12
|
+
"Compatibility Scorecard",
|
|
13
|
+
"=======================",
|
|
14
|
+
`Target: ${matrix.targetPath}`,
|
|
15
|
+
""
|
|
16
|
+
];
|
|
17
|
+
for (const result of matrix.results) {
|
|
18
|
+
lines.push(`${result.client}: ${scoreForStatus(result.status)} (${result.status.toUpperCase()})`);
|
|
19
|
+
}
|
|
20
|
+
return lines.join("\n");
|
|
21
|
+
}
|
package/dist/run-cli.js
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
import { writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
2
4
|
import { discoverInstalledPlugins, filterInstalledPlugins } from "./core/discover-installed-plugins.js";
|
|
3
5
|
import { buildCompatibilityMatrix, matrixExitCode } from "./compatibility/compatibility-matrix.js";
|
|
6
|
+
import { applyInstallPreview, renderApplyInstallResult } from "./compatibility/apply-install-preview.js";
|
|
4
7
|
import { buildClaudeDesktopInstallPreview, renderClaudeDesktopInstallPreview } from "./compatibility/claude-desktop-install-preview.js";
|
|
8
|
+
import { buildCursorInstallPreview, renderCursorInstallPreview } from "./compatibility/cursor-install-preview.js";
|
|
5
9
|
import { applyDoctorConfig, loadDoctorConfig } from "./core/doctor-config.js";
|
|
6
10
|
import { initPluginPackage } from "./core/init-plugin.js";
|
|
7
11
|
import { runCheck } from "./index.js";
|
|
8
12
|
import { renderInstalledSummary } from "./reporting/render-installed-summary.js";
|
|
13
|
+
import { renderCompatibilityScorecard } from "./reporting/render-compatibility-scorecard.js";
|
|
9
14
|
import { renderCompatibilityReport } from "./reporting/render-compatibility-report.js";
|
|
10
15
|
import { renderJsonReport } from "./reporting/render-json-report.js";
|
|
11
16
|
import { buildMarkdownReport } from "./reporting/render-markdown-report.js";
|
|
@@ -26,7 +31,7 @@ const defaultIo = {
|
|
|
26
31
|
}
|
|
27
32
|
};
|
|
28
33
|
function printUsage(io) {
|
|
29
|
-
io.writeStderr("Usage: codex-plugin-doctor check <path|--installed> [filter] [--json|--markdown] [--output <path>] [--runtime] [--verbose-runtime] [--no-animations] [--ascii]\n codex-plugin-doctor compat <path> [--client <client>] [--json] [--output <path>] [--install-preview]\n codex-plugin-doctor list --installed\n codex-plugin-doctor explain <finding-id>\n codex-plugin-doctor --version");
|
|
34
|
+
io.writeStderr("Usage: codex-plugin-doctor check <path|--installed> [filter] [--json|--markdown] [--output <path>] [--runtime] [--verbose-runtime] [--no-animations] [--ascii]\n codex-plugin-doctor compat <path> [--client <client>] [--json] [--scorecard] [--output <path>] [--install-preview|--apply --backup]\n codex-plugin-doctor self-test\n codex-plugin-doctor list --installed\n codex-plugin-doctor explain <finding-id>\n codex-plugin-doctor --version");
|
|
30
35
|
}
|
|
31
36
|
function renderInstalledPlugins(plugins) {
|
|
32
37
|
const lines = [
|
|
@@ -64,6 +69,22 @@ function filterCompatibilityMatrix(matrix, clientFilter) {
|
|
|
64
69
|
results: matrix.results.filter((result) => result.client === client)
|
|
65
70
|
};
|
|
66
71
|
}
|
|
72
|
+
function resolveBundledSelfTestTarget() {
|
|
73
|
+
return path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "examples", "codex-doctor-runtime");
|
|
74
|
+
}
|
|
75
|
+
function renderSelfTestReport(targetPath, validationStatus, findingsCount, compatibilityMatrix) {
|
|
76
|
+
return [
|
|
77
|
+
"Codex Plugin Doctor Self-Test",
|
|
78
|
+
"=============================",
|
|
79
|
+
`Version: ${packageVersion}`,
|
|
80
|
+
`Sample: ${targetPath}`,
|
|
81
|
+
`Validation: ${validationStatus.toUpperCase()}`,
|
|
82
|
+
"Runtime probes: enabled",
|
|
83
|
+
`Findings: ${findingsCount}`,
|
|
84
|
+
"",
|
|
85
|
+
renderCompatibilityScorecard(compatibilityMatrix)
|
|
86
|
+
].join("\n");
|
|
87
|
+
}
|
|
67
88
|
export async function runCli(args, io = defaultIo, options = {}) {
|
|
68
89
|
const [command, maybePath, ...remainingArgs] = args;
|
|
69
90
|
if (command === "--version" || command === "-v" || command === "version") {
|
|
@@ -95,6 +116,16 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
95
116
|
io.writeStdout(renderRuleExplanation(rule));
|
|
96
117
|
return 0;
|
|
97
118
|
}
|
|
119
|
+
if (command === "self-test" || command === "demo") {
|
|
120
|
+
const targetPath = resolveBundledSelfTestTarget();
|
|
121
|
+
const runCheckImpl = options.runCheckImpl ?? runCheck;
|
|
122
|
+
const result = applyDoctorConfig(await runCheckImpl(targetPath, { runtime: true }), await loadDoctorConfig(targetPath));
|
|
123
|
+
const compatibilityMatrix = await buildCompatibilityMatrix(targetPath, {
|
|
124
|
+
env: terminalContext.env
|
|
125
|
+
});
|
|
126
|
+
io.writeStdout(renderSelfTestReport(targetPath, result.status, result.findings.length, compatibilityMatrix));
|
|
127
|
+
return result.exitCode === 1 || matrixExitCode(compatibilityMatrix) === 1 ? 1 : 0;
|
|
128
|
+
}
|
|
98
129
|
if (command === "init") {
|
|
99
130
|
const targetPath = maybePath && !maybePath.startsWith("--") ? maybePath : ".";
|
|
100
131
|
const result = await initPluginPackage(targetPath);
|
|
@@ -114,7 +145,10 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
114
145
|
? [maybePath, ...remainingArgs]
|
|
115
146
|
: remainingArgs;
|
|
116
147
|
const jsonOutput = compatFlags.includes("--json");
|
|
148
|
+
const scorecardOutput = compatFlags.includes("--scorecard");
|
|
117
149
|
const installPreview = compatFlags.includes("--install-preview");
|
|
150
|
+
const applyInstall = compatFlags.includes("--apply");
|
|
151
|
+
const backupInstall = compatFlags.includes("--backup");
|
|
118
152
|
const clientIndex = compatFlags.indexOf("--client");
|
|
119
153
|
const clientFilter = clientIndex === -1 ? null : compatFlags[clientIndex + 1];
|
|
120
154
|
const outputIndex = compatFlags.indexOf("--output");
|
|
@@ -127,16 +161,34 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
127
161
|
io.writeStderr("Missing path after --output.");
|
|
128
162
|
return 2;
|
|
129
163
|
}
|
|
130
|
-
if (installPreview
|
|
131
|
-
|
|
164
|
+
if ((installPreview || applyInstall) &&
|
|
165
|
+
clientFilter?.toLowerCase() !== "claude-desktop" &&
|
|
166
|
+
clientFilter?.toLowerCase() !== "cursor") {
|
|
167
|
+
io.writeStderr("--install-preview and --apply require --client claude-desktop or --client cursor.");
|
|
168
|
+
return 2;
|
|
169
|
+
}
|
|
170
|
+
if (installPreview && applyInstall) {
|
|
171
|
+
io.writeStderr("Use either --install-preview or --apply, not both.");
|
|
172
|
+
return 2;
|
|
173
|
+
}
|
|
174
|
+
if (applyInstall && !backupInstall) {
|
|
175
|
+
io.writeStderr("--apply requires --backup.");
|
|
132
176
|
return 2;
|
|
133
177
|
}
|
|
134
|
-
if (installPreview) {
|
|
178
|
+
if (installPreview || applyInstall) {
|
|
135
179
|
try {
|
|
136
|
-
const preview =
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
180
|
+
const preview = clientFilter?.toLowerCase() === "cursor"
|
|
181
|
+
? await buildCursorInstallPreview(targetPath, {
|
|
182
|
+
env: terminalContext.env
|
|
183
|
+
})
|
|
184
|
+
: await buildClaudeDesktopInstallPreview(targetPath, {
|
|
185
|
+
env: terminalContext.env
|
|
186
|
+
});
|
|
187
|
+
const report = applyInstall
|
|
188
|
+
? renderApplyInstallResult(await applyInstallPreview(clientFilter?.toLowerCase() === "cursor" ? "Cursor" : "Claude Desktop", preview))
|
|
189
|
+
: clientFilter?.toLowerCase() === "cursor"
|
|
190
|
+
? renderCursorInstallPreview(preview)
|
|
191
|
+
: renderClaudeDesktopInstallPreview(preview);
|
|
140
192
|
if (outputPath) {
|
|
141
193
|
await writeFile(outputPath, report, "utf8");
|
|
142
194
|
}
|
|
@@ -162,7 +214,9 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
162
214
|
}
|
|
163
215
|
const report = jsonOutput
|
|
164
216
|
? JSON.stringify({ schemaVersion: "1.0.0", ...matrix }, null, 2)
|
|
165
|
-
:
|
|
217
|
+
: scorecardOutput
|
|
218
|
+
? renderCompatibilityScorecard(matrix)
|
|
219
|
+
: renderCompatibilityReport(matrix);
|
|
166
220
|
if (outputPath) {
|
|
167
221
|
await writeFile(outputPath, report, "utf8");
|
|
168
222
|
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# Examples
|
|
2
|
+
|
|
3
|
+
This folder contains manual example packs for local testing. Unlike `tests/fixtures`, these examples are meant for humans to run directly against the CLI.
|
|
4
|
+
|
|
5
|
+
## Example Packs
|
|
6
|
+
|
|
7
|
+
### `codex-doctor-starter`
|
|
8
|
+
|
|
9
|
+
Minimal valid Codex plugin package with one skill and no runtime MCP server.
|
|
10
|
+
|
|
11
|
+
Expected result:
|
|
12
|
+
|
|
13
|
+
- static validation passes
|
|
14
|
+
- no runtime probing needed
|
|
15
|
+
|
|
16
|
+
Command:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
codex-plugin-doctor check examples/codex-doctor-starter
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### `codex-doctor-runtime`
|
|
23
|
+
|
|
24
|
+
Valid Codex plugin package with:
|
|
25
|
+
|
|
26
|
+
- skill metadata
|
|
27
|
+
- `.mcp.json`
|
|
28
|
+
- mock MCP stdio server
|
|
29
|
+
- `tools/list`
|
|
30
|
+
- `tools/call`
|
|
31
|
+
- `resources/list`
|
|
32
|
+
- `resources/read`
|
|
33
|
+
- `resources/templates/list`
|
|
34
|
+
- `prompts/list`
|
|
35
|
+
- `prompts/get`
|
|
36
|
+
|
|
37
|
+
Expected result:
|
|
38
|
+
|
|
39
|
+
- static validation passes
|
|
40
|
+
- runtime validation passes
|
|
41
|
+
- runtime scorecard shows all supported runtime capabilities as `pass`
|
|
42
|
+
|
|
43
|
+
Command:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
codex-plugin-doctor check examples/codex-doctor-runtime --json --runtime --verbose-runtime
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### `codex-doctor-risky`
|
|
50
|
+
|
|
51
|
+
Intentionally flawed package for showing failure output.
|
|
52
|
+
|
|
53
|
+
Expected result:
|
|
54
|
+
|
|
55
|
+
- security finding for hard-coded secret
|
|
56
|
+
|
|
57
|
+
Command:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
codex-plugin-doctor check examples/codex-doctor-risky --ascii
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Suggested Local Flow
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
npm install
|
|
67
|
+
npm run build
|
|
68
|
+
npm link
|
|
69
|
+
codex-plugin-doctor check examples/codex-doctor-starter
|
|
70
|
+
codex-plugin-doctor check examples/codex-doctor-runtime --json --runtime --verbose-runtime
|
|
71
|
+
codex-plugin-doctor check examples/codex-doctor-risky --ascii
|
|
72
|
+
```
|
|
73
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
process.stdin.resume();
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import readline from "node:readline";
|
|
2
|
+
|
|
3
|
+
const rl = readline.createInterface({
|
|
4
|
+
input: process.stdin,
|
|
5
|
+
crlfDelay: Infinity
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
rl.on("line", (line) => {
|
|
9
|
+
const message = JSON.parse(line);
|
|
10
|
+
const cursor = message.params?.cursor;
|
|
11
|
+
|
|
12
|
+
if (message.method === "initialize") {
|
|
13
|
+
process.stdout.write(
|
|
14
|
+
`${JSON.stringify({
|
|
15
|
+
jsonrpc: "2.0",
|
|
16
|
+
id: message.id,
|
|
17
|
+
result: {
|
|
18
|
+
protocolVersion: "2025-11-25",
|
|
19
|
+
capabilities: {
|
|
20
|
+
tools: {},
|
|
21
|
+
resources: {},
|
|
22
|
+
prompts: {}
|
|
23
|
+
},
|
|
24
|
+
serverInfo: {
|
|
25
|
+
name: "doctor-runtime-server",
|
|
26
|
+
version: "1.0.0"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
})}\n`
|
|
30
|
+
);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (message.method === "tools/list") {
|
|
35
|
+
process.stdout.write(
|
|
36
|
+
`${JSON.stringify({
|
|
37
|
+
jsonrpc: "2.0",
|
|
38
|
+
id: message.id,
|
|
39
|
+
result:
|
|
40
|
+
cursor === "tools-page-2"
|
|
41
|
+
? {
|
|
42
|
+
tools: [
|
|
43
|
+
{
|
|
44
|
+
name: "format_status",
|
|
45
|
+
description: "Return a formatted health status.",
|
|
46
|
+
inputSchema: {
|
|
47
|
+
type: "object",
|
|
48
|
+
properties: {},
|
|
49
|
+
required: []
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
]
|
|
53
|
+
}
|
|
54
|
+
: {
|
|
55
|
+
tools: [
|
|
56
|
+
{
|
|
57
|
+
name: "ping",
|
|
58
|
+
description: "Return a healthcheck response.",
|
|
59
|
+
inputSchema: {
|
|
60
|
+
type: "object",
|
|
61
|
+
properties: {},
|
|
62
|
+
required: []
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
],
|
|
66
|
+
nextCursor: "tools-page-2"
|
|
67
|
+
}
|
|
68
|
+
})}\n`
|
|
69
|
+
);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (message.method === "tools/call") {
|
|
74
|
+
process.stdout.write(
|
|
75
|
+
`${JSON.stringify({
|
|
76
|
+
jsonrpc: "2.0",
|
|
77
|
+
id: message.id,
|
|
78
|
+
result: {
|
|
79
|
+
content: [
|
|
80
|
+
{
|
|
81
|
+
type: "text",
|
|
82
|
+
text: "doctor-runtime-ok"
|
|
83
|
+
}
|
|
84
|
+
]
|
|
85
|
+
}
|
|
86
|
+
})}\n`
|
|
87
|
+
);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (message.method === "resources/list") {
|
|
92
|
+
process.stdout.write(
|
|
93
|
+
`${JSON.stringify({
|
|
94
|
+
jsonrpc: "2.0",
|
|
95
|
+
id: message.id,
|
|
96
|
+
result:
|
|
97
|
+
cursor === "resources-page-2"
|
|
98
|
+
? {
|
|
99
|
+
resources: [
|
|
100
|
+
{
|
|
101
|
+
name: "workspace-license",
|
|
102
|
+
uri: "file:///workspace/LICENSE"
|
|
103
|
+
}
|
|
104
|
+
]
|
|
105
|
+
}
|
|
106
|
+
: {
|
|
107
|
+
resources: [
|
|
108
|
+
{
|
|
109
|
+
name: "workspace-readme",
|
|
110
|
+
uri: "file:///workspace/README.md"
|
|
111
|
+
}
|
|
112
|
+
],
|
|
113
|
+
nextCursor: "resources-page-2"
|
|
114
|
+
}
|
|
115
|
+
})}\n`
|
|
116
|
+
);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (message.method === "resources/read") {
|
|
121
|
+
process.stdout.write(
|
|
122
|
+
`${JSON.stringify({
|
|
123
|
+
jsonrpc: "2.0",
|
|
124
|
+
id: message.id,
|
|
125
|
+
result: {
|
|
126
|
+
contents: [
|
|
127
|
+
{
|
|
128
|
+
uri: "file:///workspace/README.md",
|
|
129
|
+
text: "# Workspace README"
|
|
130
|
+
}
|
|
131
|
+
]
|
|
132
|
+
}
|
|
133
|
+
})}\n`
|
|
134
|
+
);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (message.method === "resources/templates/list") {
|
|
139
|
+
process.stdout.write(
|
|
140
|
+
`${JSON.stringify({
|
|
141
|
+
jsonrpc: "2.0",
|
|
142
|
+
id: message.id,
|
|
143
|
+
result:
|
|
144
|
+
cursor === "templates-page-2"
|
|
145
|
+
? {
|
|
146
|
+
resourceTemplates: [
|
|
147
|
+
{
|
|
148
|
+
name: "log",
|
|
149
|
+
uriTemplate: "file:///workspace/logs/{name}.log"
|
|
150
|
+
}
|
|
151
|
+
]
|
|
152
|
+
}
|
|
153
|
+
: {
|
|
154
|
+
resourceTemplates: [
|
|
155
|
+
{
|
|
156
|
+
name: "doc",
|
|
157
|
+
uriTemplate: "file:///workspace/docs/{name}.md"
|
|
158
|
+
}
|
|
159
|
+
],
|
|
160
|
+
nextCursor: "templates-page-2"
|
|
161
|
+
}
|
|
162
|
+
})}\n`
|
|
163
|
+
);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (message.method === "prompts/list") {
|
|
168
|
+
process.stdout.write(
|
|
169
|
+
`${JSON.stringify({
|
|
170
|
+
jsonrpc: "2.0",
|
|
171
|
+
id: message.id,
|
|
172
|
+
result:
|
|
173
|
+
cursor === "prompts-page-2"
|
|
174
|
+
? {
|
|
175
|
+
prompts: [
|
|
176
|
+
{
|
|
177
|
+
name: "summary",
|
|
178
|
+
description: "Summarize the current change."
|
|
179
|
+
}
|
|
180
|
+
]
|
|
181
|
+
}
|
|
182
|
+
: {
|
|
183
|
+
prompts: [
|
|
184
|
+
{
|
|
185
|
+
name: "code_review",
|
|
186
|
+
description: "Review code for bugs.",
|
|
187
|
+
arguments: [
|
|
188
|
+
{
|
|
189
|
+
name: "diff",
|
|
190
|
+
required: true
|
|
191
|
+
}
|
|
192
|
+
]
|
|
193
|
+
}
|
|
194
|
+
],
|
|
195
|
+
nextCursor: "prompts-page-2"
|
|
196
|
+
}
|
|
197
|
+
})}\n`
|
|
198
|
+
);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (message.method === "prompts/get") {
|
|
203
|
+
if (message.params?.arguments?.diff !== "codex-plugin-doctor-probe") {
|
|
204
|
+
process.stdout.write(
|
|
205
|
+
`${JSON.stringify({
|
|
206
|
+
jsonrpc: "2.0",
|
|
207
|
+
id: message.id,
|
|
208
|
+
error: {
|
|
209
|
+
code: -32602,
|
|
210
|
+
message: "Missing required diff argument"
|
|
211
|
+
}
|
|
212
|
+
})}\n`
|
|
213
|
+
);
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
process.stdout.write(
|
|
218
|
+
`${JSON.stringify({
|
|
219
|
+
jsonrpc: "2.0",
|
|
220
|
+
id: message.id,
|
|
221
|
+
result: {
|
|
222
|
+
description: "Prompt for code review",
|
|
223
|
+
messages: [
|
|
224
|
+
{
|
|
225
|
+
role: "user",
|
|
226
|
+
content: {
|
|
227
|
+
type: "text",
|
|
228
|
+
text: "Review this diff for bugs."
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
]
|
|
232
|
+
}
|
|
233
|
+
})}\n`
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codex-plugin-doctor",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.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",
|
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
"./package.json": "./package.json"
|
|
17
17
|
},
|
|
18
18
|
"files": [
|
|
19
|
-
"dist"
|
|
19
|
+
"dist",
|
|
20
|
+
"examples"
|
|
20
21
|
],
|
|
21
22
|
"scripts": {
|
|
22
23
|
"clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"",
|