codex-plugin-doctor 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/dist/compatibility/claude-desktop-install-preview.d.ts +10 -0
- package/dist/compatibility/claude-desktop-install-preview.js +69 -0
- package/dist/compatibility/compatibility-matrix.d.ts +8 -1
- package/dist/compatibility/compatibility-matrix.js +123 -8
- package/dist/run-cli.js +28 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -160,6 +160,8 @@ codex-plugin-doctor init my-plugin
|
|
|
160
160
|
codex-plugin-doctor compat .
|
|
161
161
|
codex-plugin-doctor compat . --client codex
|
|
162
162
|
codex-plugin-doctor compat . --client generic-mcp
|
|
163
|
+
codex-plugin-doctor compat . --client claude-desktop
|
|
164
|
+
codex-plugin-doctor compat . --client claude-desktop --install-preview
|
|
163
165
|
codex-plugin-doctor compat . --json
|
|
164
166
|
codex-plugin-doctor compat . --json --output compatibility.json
|
|
165
167
|
codex-plugin-doctor check .
|
|
@@ -174,6 +176,8 @@ codex-plugin-doctor check . --config .codex-doctor.json
|
|
|
174
176
|
codex-plugin-doctor check . --json --runtime --verbose-runtime
|
|
175
177
|
```
|
|
176
178
|
|
|
179
|
+
`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.
|
|
180
|
+
|
|
177
181
|
Optional local policy file:
|
|
178
182
|
|
|
179
183
|
```json
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type CompatibilityEnvironment } from "./compatibility-matrix.js";
|
|
2
|
+
export interface ClaudeDesktopInstallPreview {
|
|
3
|
+
targetPath: string;
|
|
4
|
+
configPath: string;
|
|
5
|
+
snippet: {
|
|
6
|
+
mcpServers: Record<string, unknown>;
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
export declare function buildClaudeDesktopInstallPreview(targetPath: string, environment?: CompatibilityEnvironment): Promise<ClaudeDesktopInstallPreview>;
|
|
10
|
+
export declare function renderClaudeDesktopInstallPreview(preview: ClaudeDesktopInstallPreview): string;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { getClaudeDesktopConfigPath, 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 buildClaudeDesktopInstallPreview(targetPath, environment = {}) {
|
|
32
|
+
const rootPath = path.resolve(targetPath);
|
|
33
|
+
const configPath = getClaudeDesktopConfigPath(environment);
|
|
34
|
+
if (!configPath) {
|
|
35
|
+
throw new Error("Claude Desktop config path could not be resolved on this platform.");
|
|
36
|
+
}
|
|
37
|
+
const mcpConfigPath = await readMcpConfigPath(rootPath);
|
|
38
|
+
if (!mcpConfigPath) {
|
|
39
|
+
throw new Error("No MCP config found for install preview.");
|
|
40
|
+
}
|
|
41
|
+
const parsed = JSON.parse(await readFile(mcpConfigPath, "utf8"));
|
|
42
|
+
const servers = parsed.mcpServers;
|
|
43
|
+
if (!isRecord(servers) || Object.keys(servers).length === 0) {
|
|
44
|
+
throw new Error("MCP config does not contain a non-empty `mcpServers` object.");
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
targetPath: rootPath,
|
|
48
|
+
configPath,
|
|
49
|
+
snippet: {
|
|
50
|
+
mcpServers: Object.fromEntries(Object.entries(servers).map(([serverName, serverConfig]) => [
|
|
51
|
+
serverName,
|
|
52
|
+
normalizeServerConfig(serverConfig, rootPath)
|
|
53
|
+
]))
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
export function renderClaudeDesktopInstallPreview(preview) {
|
|
58
|
+
return [
|
|
59
|
+
"Claude Desktop Install Preview",
|
|
60
|
+
"==============================",
|
|
61
|
+
`Target: ${preview.targetPath}`,
|
|
62
|
+
`Config: ${preview.configPath}`,
|
|
63
|
+
"",
|
|
64
|
+
"Add or merge this snippet into `claude_desktop_config.json`:",
|
|
65
|
+
JSON.stringify(preview.snippet, null, 2),
|
|
66
|
+
"",
|
|
67
|
+
"No files were modified."
|
|
68
|
+
].join("\n");
|
|
69
|
+
}
|
|
@@ -9,5 +9,12 @@ export interface CompatibilityMatrix {
|
|
|
9
9
|
targetPath: string;
|
|
10
10
|
results: CompatibilityResult[];
|
|
11
11
|
}
|
|
12
|
-
export
|
|
12
|
+
export interface CompatibilityEnvironment {
|
|
13
|
+
env?: Record<string, string | undefined>;
|
|
14
|
+
platform?: NodeJS.Platform;
|
|
15
|
+
homedir?: string;
|
|
16
|
+
}
|
|
17
|
+
export declare function readMcpConfigPath(targetPath: string): Promise<string | null>;
|
|
18
|
+
export declare function getClaudeDesktopConfigPath(environment?: CompatibilityEnvironment): string | null;
|
|
19
|
+
export declare function buildCompatibilityMatrix(targetPath: string, environment?: CompatibilityEnvironment): Promise<CompatibilityMatrix>;
|
|
13
20
|
export declare function matrixExitCode(matrix: CompatibilityMatrix): 0 | 1;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { readFile, stat } from "node:fs/promises";
|
|
2
|
+
import os from "node:os";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
import { validatePlugin } from "../core/validate-plugin.js";
|
|
4
5
|
async function fileExists(targetPath) {
|
|
@@ -10,6 +11,15 @@ async function fileExists(targetPath) {
|
|
|
10
11
|
return false;
|
|
11
12
|
}
|
|
12
13
|
}
|
|
14
|
+
async function directoryExists(targetPath) {
|
|
15
|
+
try {
|
|
16
|
+
const details = await stat(targetPath);
|
|
17
|
+
return details.isDirectory();
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
13
23
|
function statusFromCheckResult(result) {
|
|
14
24
|
if (result.status === "fail") {
|
|
15
25
|
return "fail";
|
|
@@ -19,7 +29,7 @@ function statusFromCheckResult(result) {
|
|
|
19
29
|
}
|
|
20
30
|
return "pass";
|
|
21
31
|
}
|
|
22
|
-
async function readMcpConfigPath(targetPath) {
|
|
32
|
+
export async function readMcpConfigPath(targetPath) {
|
|
23
33
|
const rootPath = path.resolve(targetPath);
|
|
24
34
|
const directMcpPath = path.join(rootPath, ".mcp.json");
|
|
25
35
|
if (await fileExists(directMcpPath)) {
|
|
@@ -82,9 +92,119 @@ async function checkGenericMcp(targetPath) {
|
|
|
82
92
|
};
|
|
83
93
|
}
|
|
84
94
|
}
|
|
85
|
-
|
|
95
|
+
async function readMcpServerNames(targetPath) {
|
|
96
|
+
const mcpConfigPath = await readMcpConfigPath(targetPath);
|
|
97
|
+
if (!mcpConfigPath || !(await fileExists(mcpConfigPath))) {
|
|
98
|
+
return [];
|
|
99
|
+
}
|
|
100
|
+
try {
|
|
101
|
+
const parsed = JSON.parse(await readFile(mcpConfigPath, "utf8"));
|
|
102
|
+
const servers = parsed.mcpServers;
|
|
103
|
+
return typeof servers === "object" && servers !== null && !Array.isArray(servers)
|
|
104
|
+
? Object.keys(servers)
|
|
105
|
+
: [];
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
return [];
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
export function getClaudeDesktopConfigPath(environment = {}) {
|
|
112
|
+
const platform = environment.platform ?? process.platform;
|
|
113
|
+
const env = environment.env ?? process.env;
|
|
114
|
+
const homeDirectory = environment.homedir ?? os.homedir();
|
|
115
|
+
if (platform === "win32") {
|
|
116
|
+
const appData = env.APPDATA;
|
|
117
|
+
return appData ? path.join(appData, "Claude", "claude_desktop_config.json") : null;
|
|
118
|
+
}
|
|
119
|
+
if (platform === "darwin") {
|
|
120
|
+
return path.join(homeDirectory, "Library", "Application Support", "Claude", "claude_desktop_config.json");
|
|
121
|
+
}
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
async function checkClaudeDesktop(targetPath, genericMcpResult, environment = {}) {
|
|
125
|
+
if (genericMcpResult.status !== "pass") {
|
|
126
|
+
return {
|
|
127
|
+
client: "Claude Desktop",
|
|
128
|
+
status: "skipped",
|
|
129
|
+
summary: "No valid MCP package config is available for Claude Desktop.",
|
|
130
|
+
details: ["Add a valid `.mcp.json` with a non-empty `mcpServers` object first."]
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
const configPath = getClaudeDesktopConfigPath(environment);
|
|
134
|
+
if (!configPath) {
|
|
135
|
+
return {
|
|
136
|
+
client: "Claude Desktop",
|
|
137
|
+
status: "warn",
|
|
138
|
+
summary: "Claude Desktop config path could not be resolved on this platform.",
|
|
139
|
+
details: ["Claude Desktop local config detection currently supports Windows and macOS."]
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
if (!(await fileExists(configPath))) {
|
|
143
|
+
const configDirectory = path.dirname(configPath);
|
|
144
|
+
return {
|
|
145
|
+
client: "Claude Desktop",
|
|
146
|
+
status: await directoryExists(configDirectory) ? "pass" : "warn",
|
|
147
|
+
summary: await directoryExists(configDirectory)
|
|
148
|
+
? "Claude Desktop directory exists and a config file can be created."
|
|
149
|
+
: "Claude Desktop was not detected on this machine.",
|
|
150
|
+
details: [
|
|
151
|
+
configPath,
|
|
152
|
+
"Claude Desktop can add stdio MCP servers through `claude_desktop_config.json` when the app is installed."
|
|
153
|
+
]
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
try {
|
|
157
|
+
const parsed = JSON.parse(await readFile(configPath, "utf8"));
|
|
158
|
+
const servers = parsed.mcpServers;
|
|
159
|
+
if (servers !== undefined && (typeof servers !== "object" ||
|
|
160
|
+
servers === null ||
|
|
161
|
+
Array.isArray(servers))) {
|
|
162
|
+
return {
|
|
163
|
+
client: "Claude Desktop",
|
|
164
|
+
status: "fail",
|
|
165
|
+
summary: "Claude Desktop config has an invalid `mcpServers` shape.",
|
|
166
|
+
details: [configPath, "`mcpServers` must be an object before this package can be added safely."]
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
const packageServerNames = await readMcpServerNames(targetPath);
|
|
170
|
+
const existingServerNames = typeof servers === "object" && servers !== null
|
|
171
|
+
? Object.keys(servers)
|
|
172
|
+
: [];
|
|
173
|
+
const duplicateServerNames = packageServerNames.filter((serverName) => existingServerNames.includes(serverName));
|
|
174
|
+
if (duplicateServerNames.length > 0) {
|
|
175
|
+
return {
|
|
176
|
+
client: "Claude Desktop",
|
|
177
|
+
status: "warn",
|
|
178
|
+
summary: "Claude Desktop already has MCP server names from this package.",
|
|
179
|
+
details: [
|
|
180
|
+
configPath,
|
|
181
|
+
...duplicateServerNames.map((serverName) => `Duplicate server: ${serverName}`)
|
|
182
|
+
]
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
return {
|
|
186
|
+
client: "Claude Desktop",
|
|
187
|
+
status: "pass",
|
|
188
|
+
summary: "Claude Desktop config is valid and this MCP package can be added.",
|
|
189
|
+
details: [
|
|
190
|
+
configPath,
|
|
191
|
+
`Source package: ${path.resolve(targetPath)}`
|
|
192
|
+
]
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
return {
|
|
197
|
+
client: "Claude Desktop",
|
|
198
|
+
status: "fail",
|
|
199
|
+
summary: "Claude Desktop config is not valid JSON.",
|
|
200
|
+
details: [configPath, "Repair the local Claude Desktop config before adding new MCP servers."]
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
export async function buildCompatibilityMatrix(targetPath, environment = {}) {
|
|
86
205
|
const rootPath = path.resolve(targetPath);
|
|
87
206
|
const genericMcpResult = await checkGenericMcp(rootPath);
|
|
207
|
+
const claudeDesktopResult = await checkClaudeDesktop(rootPath, genericMcpResult, environment);
|
|
88
208
|
const codexResult = await validatePlugin(rootPath);
|
|
89
209
|
const codexStatus = statusFromCheckResult(codexResult);
|
|
90
210
|
const codexCompatibility = !await hasCodexManifest(rootPath)
|
|
@@ -106,12 +226,7 @@ export async function buildCompatibilityMatrix(targetPath) {
|
|
|
106
226
|
const results = [
|
|
107
227
|
codexCompatibility,
|
|
108
228
|
genericMcpResult,
|
|
109
|
-
|
|
110
|
-
client: "Claude Desktop",
|
|
111
|
-
status: "skipped",
|
|
112
|
-
summary: "Client-specific package adapter is not implemented yet.",
|
|
113
|
-
details: ["Planned adapter after generic MCP compatibility is stable."]
|
|
114
|
-
},
|
|
229
|
+
claudeDesktopResult,
|
|
115
230
|
{
|
|
116
231
|
client: "Cursor",
|
|
117
232
|
status: "skipped",
|
package/dist/run-cli.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { writeFile } from "node:fs/promises";
|
|
2
2
|
import { discoverInstalledPlugins, filterInstalledPlugins } from "./core/discover-installed-plugins.js";
|
|
3
3
|
import { buildCompatibilityMatrix, matrixExitCode } from "./compatibility/compatibility-matrix.js";
|
|
4
|
+
import { buildClaudeDesktopInstallPreview, renderClaudeDesktopInstallPreview } from "./compatibility/claude-desktop-install-preview.js";
|
|
4
5
|
import { applyDoctorConfig, loadDoctorConfig } from "./core/doctor-config.js";
|
|
5
6
|
import { initPluginPackage } from "./core/init-plugin.js";
|
|
6
7
|
import { runCheck } from "./index.js";
|
|
@@ -25,7 +26,7 @@ const defaultIo = {
|
|
|
25
26
|
}
|
|
26
27
|
};
|
|
27
28
|
function printUsage(io) {
|
|
28
|
-
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> [--json] [--output <path>]\n codex-plugin-doctor list --installed\n codex-plugin-doctor explain <finding-id>\n codex-plugin-doctor --version");
|
|
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");
|
|
29
30
|
}
|
|
30
31
|
function renderInstalledPlugins(plugins) {
|
|
31
32
|
const lines = [
|
|
@@ -113,6 +114,7 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
113
114
|
? [maybePath, ...remainingArgs]
|
|
114
115
|
: remainingArgs;
|
|
115
116
|
const jsonOutput = compatFlags.includes("--json");
|
|
117
|
+
const installPreview = compatFlags.includes("--install-preview");
|
|
116
118
|
const clientIndex = compatFlags.indexOf("--client");
|
|
117
119
|
const clientFilter = clientIndex === -1 ? null : compatFlags[clientIndex + 1];
|
|
118
120
|
const outputIndex = compatFlags.indexOf("--output");
|
|
@@ -125,7 +127,31 @@ export async function runCli(args, io = defaultIo, options = {}) {
|
|
|
125
127
|
io.writeStderr("Missing path after --output.");
|
|
126
128
|
return 2;
|
|
127
129
|
}
|
|
128
|
-
|
|
130
|
+
if (installPreview && clientFilter?.toLowerCase() !== "claude-desktop") {
|
|
131
|
+
io.writeStderr("--install-preview requires --client claude-desktop.");
|
|
132
|
+
return 2;
|
|
133
|
+
}
|
|
134
|
+
if (installPreview) {
|
|
135
|
+
try {
|
|
136
|
+
const preview = await buildClaudeDesktopInstallPreview(targetPath, {
|
|
137
|
+
env: terminalContext.env
|
|
138
|
+
});
|
|
139
|
+
const report = renderClaudeDesktopInstallPreview(preview);
|
|
140
|
+
if (outputPath) {
|
|
141
|
+
await writeFile(outputPath, report, "utf8");
|
|
142
|
+
}
|
|
143
|
+
io.writeStdout(report);
|
|
144
|
+
return 0;
|
|
145
|
+
}
|
|
146
|
+
catch (error) {
|
|
147
|
+
const message = error instanceof Error ? error.message : "Unknown install preview error.";
|
|
148
|
+
io.writeStderr(message);
|
|
149
|
+
return 1;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
let matrix = await buildCompatibilityMatrix(targetPath, {
|
|
153
|
+
env: terminalContext.env
|
|
154
|
+
});
|
|
129
155
|
if (clientFilter) {
|
|
130
156
|
const filteredMatrix = filterCompatibilityMatrix(matrix, clientFilter);
|
|
131
157
|
if (!filteredMatrix) {
|
package/package.json
CHANGED