@luckychen1993/pm-mcp 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -29,44 +29,58 @@ AI 编码项目的典型失控(本工具逐一给出机制):
29
29
  | 法律知识产权 | `audit_license` 依赖许可证/copyleft 冲突 + 来源登记(provenance) |
30
30
  | 测试投机(删测试、skip 凑绿) | 禁用/蒸发/空测试检测 + 功能测试背书占比 + done 必附验证 |
31
31
 
32
- ## 一键添加到 AI 编程助手
32
+ ## 一个命令自动添加到 AI 编程助手
33
33
 
34
- 要求:Node.js ≥ 22.18。下面的配置固定到 npm 版本 `0.1.1`;首次启动由 `npx` 下载并在本机运行,pm-mcp 本身不需要 API Key,也不会调用远程模型。
34
+ 要求:Node.js ≥ 22.18。Windows、macOS 和 Linux 使用同一条命令:
35
35
 
36
- ### Codex / ChatGPT 桌面版 / Codex IDE 扩展
36
+ ```bash
37
+ npx -y @luckychen1993/pm-mcp@latest setup
38
+ ```
39
+
40
+ 安装器自动检测本机的 Codex、Claude Code、ZCode、Cursor 和 VS Code,并配置所有检测到的客户端。JSON 配置会先备份;已经存在的 Codex/Claude 配置默认保留,用 `setup --force` 才替换。先预览、不写配置:
41
+
42
+ ```bash
43
+ npx -y @luckychen1993/pm-mcp@latest setup --dry-run
44
+ ```
45
+
46
+ 非标准安装路径无法自动发现时,可在同一命令末尾指定 `--client codex|claude|zcode|cursor|vscode`;`--client print` 输出通用 MCP JSON。首次启动由 `npx` 下载并在本机运行,pm-mcp 本身不需要 API Key,也不会调用远程模型。
47
+
48
+ ### 客户端官方命令(自动检测失败时备用)
49
+
50
+ #### Codex / ChatGPT 桌面版 / Codex IDE 扩展
37
51
 
38
52
  Codex 的本地客户端共享同一份 MCP 配置。复制执行:
39
53
 
40
54
  ```bash
41
- codex mcp add pm-mcp -- npx -y @luckychen1993/pm-mcp@0.1.1
55
+ codex mcp add pm-mcp -- npx -y @luckychen1993/pm-mcp@0.1.2
42
56
  ```
43
57
 
44
58
  重启客户端后可用 `codex mcp list` 检查。该命令遵循 [OpenAI 官方 MCP CLI 格式](https://learn.chatgpt.com/docs/extend/mcp?surface=cli)。
45
59
 
46
- ### Claude Code
60
+ #### Claude Code
47
61
 
48
62
  ```bash
49
- claude mcp add pm-mcp --scope user -- npx -y @luckychen1993/pm-mcp@0.1.1
63
+ claude mcp add pm-mcp --scope user -- npx -y @luckychen1993/pm-mcp@0.1.2
50
64
  ```
51
65
 
52
- ### ZCode、Cursor、VS Code(Windows 一键脚本)
66
+ #### ZCode、Cursor、VS Code(Windows 备用脚本)
53
67
 
54
68
  将 `<client>` 替换为 `zcode`、`cursor` 或 `vscode`,整行复制到 PowerShell:
55
69
 
56
70
  ```powershell
57
- $u='https://raw.githubusercontent.com/happy520ai/pm-mcp/v0.1.1/install.ps1'; $f=Join-Path $env:TEMP 'install-pm-mcp.ps1'; Invoke-WebRequest $u -OutFile $f; & $f -Client <client>
71
+ $u='https://raw.githubusercontent.com/happy520ai/pm-mcp/v0.1.2/install.ps1'; $f=Join-Path $env:TEMP 'install-pm-mcp.ps1'; Invoke-WebRequest $u -OutFile $f; & $f -Client <client>
58
72
  ```
59
73
 
60
74
  脚本只写对应客户端的 MCP 配置;已有 JSON 配置会先生成带时间戳的备份。建议执行前先打开 [`install.ps1`](install.ps1) 审阅。ZCode 默认写入 `$ZCODE_HOME/cli/config.json` 或 `~/.zcode/cli/config.json`,Cursor 默认写入 `~/.cursor/mcp.json`;也可用 `-ConfigPath` 指定路径。
61
75
 
62
- ### 其他支持 stdio MCP 的客户端
76
+ #### 其他支持 stdio MCP 的客户端
63
77
 
64
78
  ```json
65
79
  {
66
80
  "mcpServers": {
67
81
  "pm-mcp": {
68
82
  "command": "npx",
69
- "args": ["-y", "@luckychen1993/pm-mcp@0.1.1"],
83
+ "args": ["-y", "@luckychen1993/pm-mcp@0.1.2"],
70
84
  "env": {}
71
85
  }
72
86
  }
package/dist/cli.js ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env node
2
+ if (process.argv[2] === "setup") {
3
+ try {
4
+ const { runSetup } = await import("./setup.js");
5
+ process.exitCode = runSetup(process.argv.slice(3));
6
+ }
7
+ catch (error) {
8
+ console.error(`[pm-mcp setup] ERROR: ${error instanceof Error ? error.message : String(error)}`);
9
+ process.exitCode = 1;
10
+ }
11
+ }
12
+ else {
13
+ await import("./index.js");
14
+ }
15
+ export {};
package/dist/index.js CHANGED
@@ -22,7 +22,7 @@ for (let i = 0; i < argv.length; i++) {
22
22
  explicitRoot = argv[i + 1];
23
23
  }
24
24
  const root = resolveRoot(explicitRoot);
25
- const server = new McpServer({ name: "pm-mcp", version: "0.1.1" });
25
+ const server = new McpServer({ name: "pm-mcp", version: "0.1.2" });
26
26
  registerAllTools(server, root);
27
27
  /* -------------------------------- Resources ------------------------------- */
28
28
  function safeText(fn) {
package/dist/setup.js ADDED
@@ -0,0 +1,257 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { spawnSync } from "node:child_process";
5
+ export const PACKAGE_SPEC = "@luckychen1993/pm-mcp@0.1.2";
6
+ export const SERVER_NAME = "pm-mcp";
7
+ const CLIENTS = ["codex", "claude", "zcode", "cursor", "vscode"];
8
+ export function parseSetupArgs(argv) {
9
+ const options = { client: "auto", force: false, dryRun: false, help: false };
10
+ for (let index = 0; index < argv.length; index += 1) {
11
+ const arg = argv[index];
12
+ if (arg === "--client") {
13
+ const value = argv[index + 1];
14
+ if (!value || !["auto", "all", "codex", "claude", "zcode", "cursor", "vscode", "print"].includes(value)) {
15
+ throw new Error("--client must be auto, all, codex, claude, zcode, cursor, vscode, or print");
16
+ }
17
+ options.client = value;
18
+ index += 1;
19
+ }
20
+ else if (arg === "--force") {
21
+ options.force = true;
22
+ }
23
+ else if (arg === "--dry-run") {
24
+ options.dryRun = true;
25
+ }
26
+ else if (arg === "--help" || arg === "-h") {
27
+ options.help = true;
28
+ }
29
+ else {
30
+ throw new Error(`unknown setup argument: ${arg}`);
31
+ }
32
+ }
33
+ return options;
34
+ }
35
+ export function commandExists(command, env = process.env, platform = process.platform) {
36
+ const searchPath = env.PATH ?? env.Path ?? "";
37
+ const suffixes = platform === "win32" ? [".exe", ".cmd", ".bat", ""] : [""];
38
+ return searchPath.split(path.delimiter).filter(Boolean).some((directory) => suffixes.some((suffix) => {
39
+ try {
40
+ return fs.statSync(path.join(directory, command + suffix)).isFile();
41
+ }
42
+ catch {
43
+ return false;
44
+ }
45
+ }));
46
+ }
47
+ export function detectClients(context) {
48
+ const found = [];
49
+ const { env, home, exists } = context;
50
+ if (context.commandExists("codex") || Boolean(env.CODEX_HOME) || exists(path.join(home, ".codex")))
51
+ found.push("codex");
52
+ if (context.commandExists("claude"))
53
+ found.push("claude");
54
+ if (context.commandExists("zcode") || Boolean(env.ZCODE_HOME) || exists(path.join(home, ".zcode")))
55
+ found.push("zcode");
56
+ if (context.commandExists("cursor") || exists(path.join(home, ".cursor")))
57
+ found.push("cursor");
58
+ if (context.commandExists("code"))
59
+ found.push("vscode");
60
+ return found;
61
+ }
62
+ function asObject(value, label) {
63
+ if (!value || typeof value !== "object" || Array.isArray(value))
64
+ throw new Error(`${label} must be a JSON object`);
65
+ return value;
66
+ }
67
+ export function mergeJsonServer(document, shape) {
68
+ const root = asObject(document, "configuration root");
69
+ let servers;
70
+ if (shape === "zcode") {
71
+ const mcp = root.mcp === undefined ? (root.mcp = {}) : root.mcp;
72
+ const mcpObject = asObject(mcp, "mcp");
73
+ const currentServers = mcpObject.servers === undefined ? (mcpObject.servers = {}) : mcpObject.servers;
74
+ servers = asObject(currentServers, "mcp.servers");
75
+ }
76
+ else {
77
+ const currentServers = root.mcpServers === undefined ? (root.mcpServers = {}) : root.mcpServers;
78
+ servers = asObject(currentServers, "mcpServers");
79
+ }
80
+ servers[SERVER_NAME] = { command: "npx", args: ["-y", PACKAGE_SPEC], env: {} };
81
+ return root;
82
+ }
83
+ function backupName(file) {
84
+ const stamp = new Date().toISOString().replace(/[-:.TZ]/g, "");
85
+ return `${file}.backup-${stamp}-${process.pid}`;
86
+ }
87
+ export function writeJsonClientConfig(file, shape, dryRun = false) {
88
+ const absolute = path.resolve(file);
89
+ const exists = fs.existsSync(absolute);
90
+ let document = {};
91
+ if (exists) {
92
+ const stat = fs.lstatSync(absolute);
93
+ if (!stat.isFile() || stat.isSymbolicLink())
94
+ throw new Error(`refusing non-regular config file: ${absolute}`);
95
+ try {
96
+ document = JSON.parse(fs.readFileSync(absolute, "utf8"));
97
+ }
98
+ catch (error) {
99
+ throw new Error(`invalid JSON in ${absolute}; no changes made: ${error instanceof Error ? error.message : String(error)}`);
100
+ }
101
+ }
102
+ const merged = mergeJsonServer(document, shape);
103
+ if (dryRun)
104
+ return null;
105
+ fs.mkdirSync(path.dirname(absolute), { recursive: true });
106
+ const backup = exists ? backupName(absolute) : null;
107
+ if (backup)
108
+ fs.copyFileSync(absolute, backup, fs.constants.COPYFILE_EXCL);
109
+ const temporary = `${absolute}.tmp-${process.pid}`;
110
+ try {
111
+ fs.writeFileSync(temporary, JSON.stringify(merged, null, 2) + "\n", { encoding: "utf8", flag: "wx" });
112
+ fs.renameSync(temporary, absolute);
113
+ }
114
+ catch (error) {
115
+ try {
116
+ fs.unlinkSync(temporary);
117
+ }
118
+ catch { /* nothing to clean */ }
119
+ throw error;
120
+ }
121
+ return backup;
122
+ }
123
+ function codexConfigPath(env, home) {
124
+ return path.join(env.CODEX_HOME || path.join(home, ".codex"), "config.toml");
125
+ }
126
+ function zcodeConfigPath(env, home) {
127
+ return path.join(env.ZCODE_HOME || path.join(home, ".zcode"), "cli", "config.json");
128
+ }
129
+ function hasCodexServer(text) {
130
+ return /^\s*\[mcp_servers\.(?:pm-mcp|"pm-mcp")\]\s*$/m.test(text);
131
+ }
132
+ function appendCodexConfig(file, dryRun, log) {
133
+ const absolute = path.resolve(file);
134
+ const existing = fs.existsSync(absolute) ? fs.readFileSync(absolute, "utf8") : "";
135
+ if (hasCodexServer(existing)) {
136
+ log(`[skip] Codex already has ${SERVER_NAME}; use --force with the Codex CLI to replace it.`);
137
+ return;
138
+ }
139
+ log(`[${dryRun ? "plan" : "write"}] Codex: ${absolute}`);
140
+ if (dryRun)
141
+ return;
142
+ fs.mkdirSync(path.dirname(absolute), { recursive: true });
143
+ if (fs.existsSync(absolute))
144
+ fs.copyFileSync(absolute, backupName(absolute), fs.constants.COPYFILE_EXCL);
145
+ const separator = existing.length === 0 || existing.endsWith("\n") ? "" : "\n";
146
+ const block = `${separator}\n[mcp_servers.${SERVER_NAME}]\ncommand = "npx"\nargs = ["-y", "${PACKAGE_SPEC}"]\n`;
147
+ fs.appendFileSync(absolute, block, "utf8");
148
+ }
149
+ function run(command, args, visible) {
150
+ const result = spawnSync(command, args, {
151
+ shell: false,
152
+ windowsHide: true,
153
+ stdio: visible ? "inherit" : "ignore",
154
+ });
155
+ if (result.error)
156
+ return -1;
157
+ return result.status ?? -1;
158
+ }
159
+ function configureCliClient(client, options, available, log) {
160
+ if (!available)
161
+ return false;
162
+ const getArgs = ["mcp", "get", SERVER_NAME];
163
+ const exists = run(client, getArgs, false) === 0;
164
+ if (exists && !options.force) {
165
+ log(`[skip] ${client} already has ${SERVER_NAME}; run setup --force to replace it.`);
166
+ return true;
167
+ }
168
+ const addArgs = client === "codex"
169
+ ? ["mcp", "add", SERVER_NAME, "--", "npx", "-y", PACKAGE_SPEC]
170
+ : ["mcp", "add", SERVER_NAME, "--scope", "user", "--", "npx", "-y", PACKAGE_SPEC];
171
+ log(`[${options.dryRun ? "plan" : "run"}] ${client} ${addArgs.join(" ")}`);
172
+ if (options.dryRun)
173
+ return true;
174
+ if (exists && run(client, ["mcp", "remove", SERVER_NAME], true) !== 0)
175
+ throw new Error(`${client} could not remove the existing server`);
176
+ if (run(client, addArgs, true) !== 0)
177
+ throw new Error(`${client} could not add ${SERVER_NAME}`);
178
+ return true;
179
+ }
180
+ function configureClient(client, options, context, log) {
181
+ if (client === "codex") {
182
+ if (configureCliClient("codex", options, context.commandExists("codex"), log))
183
+ return;
184
+ appendCodexConfig(codexConfigPath(context.env, context.home), options.dryRun, log);
185
+ return;
186
+ }
187
+ if (client === "claude") {
188
+ if (!configureCliClient("claude", options, context.commandExists("claude"), log))
189
+ throw new Error("claude was not found in PATH");
190
+ return;
191
+ }
192
+ if (client === "zcode" || client === "cursor") {
193
+ const file = client === "zcode" ? zcodeConfigPath(context.env, context.home) : path.join(context.home, ".cursor", "mcp.json");
194
+ log(`[${options.dryRun ? "plan" : "write"}] ${client}: ${file}`);
195
+ const backup = writeJsonClientConfig(file, client === "zcode" ? "zcode" : "standard", options.dryRun);
196
+ if (backup)
197
+ log(`[backup] ${backup}`);
198
+ return;
199
+ }
200
+ if (!context.commandExists("code"))
201
+ throw new Error("code was not found in PATH");
202
+ const definition = JSON.stringify({ name: SERVER_NAME, command: "npx", args: ["-y", PACKAGE_SPEC] });
203
+ log(`[${options.dryRun ? "plan" : "run"}] code --add-mcp ${definition}`);
204
+ if (!options.dryRun && run("code", ["--add-mcp", definition], true) !== 0)
205
+ throw new Error("VS Code could not add pm-mcp");
206
+ }
207
+ function printHelp(log) {
208
+ log("pm-mcp setup — configure local AI coding clients");
209
+ log("");
210
+ log(`Usage: npx -y ${PACKAGE_SPEC} setup [--client auto|all|codex|claude|zcode|cursor|vscode|print] [--force] [--dry-run]`);
211
+ log("Default auto mode configures every detected supported client.");
212
+ }
213
+ export function runSetup(argv, overrides = {}) {
214
+ const options = parseSetupArgs(argv);
215
+ const log = overrides.log ?? console.log;
216
+ if (options.help) {
217
+ printHelp(log);
218
+ return 0;
219
+ }
220
+ if (options.client === "print") {
221
+ log(JSON.stringify({ mcpServers: { [SERVER_NAME]: { command: "npx", args: ["-y", PACKAGE_SPEC], env: {} } } }, null, 2));
222
+ return 0;
223
+ }
224
+ const env = overrides.env ?? process.env;
225
+ const home = overrides.home ?? os.homedir();
226
+ const context = {
227
+ env,
228
+ home,
229
+ commandExists: overrides.commandExists ?? ((command) => commandExists(command, env)),
230
+ exists: overrides.exists ?? fs.existsSync,
231
+ };
232
+ const selected = options.client === "all"
233
+ ? CLIENTS
234
+ : options.client === "auto"
235
+ ? detectClients(context)
236
+ : [options.client];
237
+ if (selected.length === 0) {
238
+ log("No supported client was detected. Use --client <name>, or --client print for generic JSON.");
239
+ return 1;
240
+ }
241
+ log(`pm-mcp setup ${PACKAGE_SPEC}`);
242
+ log(`clients: ${selected.join(", ")}`);
243
+ let failures = 0;
244
+ for (const client of selected) {
245
+ try {
246
+ configureClient(client, options, context, log);
247
+ }
248
+ catch (error) {
249
+ failures += 1;
250
+ console.error(`[failed] ${client}: ${error instanceof Error ? error.message : String(error)}`);
251
+ }
252
+ }
253
+ if (failures > 0)
254
+ return 1;
255
+ log("Done. Restart the configured client and ask it to call pm-mcp get_status.");
256
+ return 0;
257
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@luckychen1993/pm-mcp",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "AI 编码项目的单一事实来源 + 健康台账 MCP 服务(事实来源 / 路线图 / 防幻觉 / 七本账 / 断点续做 / token 经济)",
5
5
  "keywords": [
6
6
  "mcp",
@@ -26,7 +26,7 @@
26
26
  "dist/"
27
27
  ],
28
28
  "bin": {
29
- "pm-mcp": "dist/index.js"
29
+ "pm-mcp": "dist/cli.js"
30
30
  },
31
31
  "main": "dist/index.js",
32
32
  "engines": {