@luckychen1993/pm-mcp 0.4.0 → 0.6.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 +58 -19
- package/dist/acceptance-tools.js +15 -10
- package/dist/audit-tools.js +39 -16
- package/dist/caller-workspace.js +244 -0
- package/dist/cli.js +11 -1
- package/dist/doctor.js +19 -16
- package/dist/git-state.js +41 -2
- package/dist/governance-audit.js +10 -0
- package/dist/governance-tools.js +54 -28
- package/dist/index-store.js +2 -1
- package/dist/index.js +12 -207
- package/dist/knowledge-tools.js +29 -20
- package/dist/paths.js +61 -3
- package/dist/project-tools.js +100 -22
- package/dist/prompts.js +91 -0
- package/dist/public-search.js +105 -0
- package/dist/qoder-launch.js +131 -0
- package/dist/resources.js +116 -0
- package/dist/scan.js +4 -0
- package/dist/setup-project.js +1 -1
- package/dist/setup.js +49 -6
- package/dist/task-evidence.js +9 -9
- package/dist/task-tools.js +15 -10
- package/dist/tool-base.js +32 -7
- package/dist/typescript-semantic.js +28 -10
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,7 +8,16 @@
|
|
|
8
8
|
|
|
9
9
|
> 核心理念:**把恶化从「无声发生」变成「显式记账」,把上下文从「模型记忆」搬到「仓库文件」。**
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
当前源码与 npm 最新发布为 **0.6.0**。公共安装示例使用 `@latest`;`setup --local` 可绑定已构建的本地入口。
|
|
12
|
+
|
|
13
|
+
## 0.6.0:Qoder 接入发布、git 对账兜底与完成门禁指路
|
|
14
|
+
|
|
15
|
+
- **发布 `launch` 子命令与 `--client qoder`**:npm 版可直接 `setup --client qoder`(条目写 `<QODER_CONFIG_DIR|~/.qoder-cn>/settings.json`,走 `pm-mcp launch` 适配器);`install.ps1` 新增 `-Client qoder` 分支,修复其硬编码版本号。
|
|
16
|
+
- **Qoder 多窗口按请求认领工作区**:Qoder 只派生一个常驻子进程复用所有窗口,项目根不再于启动期钉死——服务端用「工具名 + 参数 + 紧时间窗」指纹倒扫会话运行日志,命中调用方工作区(`root_source=qoder-session-log(request-match)`)。
|
|
17
|
+
- **git 对账健壮性**:宿主进程 PATH 缺 git 时按 `where git` 与常见安装路径兜底解析并缓存,`git status` 对账不再恒红 fail-closed。
|
|
18
|
+
- **完成门禁报错带下一步指引**:`update_task` 的证据过期/最新报告/范围校验拦截都附可执行的重试路径(先跑 `run_quality_matrix` 全绿再置 done),减少 AI 重试碰壁。
|
|
19
|
+
- **审计误报修复**:skip/only 标记计数不再统计扫描器自身的正则模式定义与夹具字符串模板;fixtures 目录不参与空测试嫌疑。
|
|
20
|
+
- **入口拆分**:`index.ts` 只留装配与启动,resources/prompts 注册拆到 `src/resources.ts`、`src/prompts.ts`。
|
|
12
21
|
|
|
13
22
|
## 0.3.0:升级生效、任务取全与证据分级
|
|
14
23
|
|
|
@@ -33,7 +42,7 @@ node dist/cli.js doctor --root .
|
|
|
33
42
|
|
|
34
43
|
## 已有管理流程
|
|
35
44
|
|
|
36
|
-
用户仍然直接说“继续这个任务”“保存进度”“完成后帮我验收”。
|
|
45
|
+
用户仍然直接说“继续这个任务”“保存进度”“完成后帮我验收”。54 个工具全部保留,由 AI 组合调用,无需用户记工具名。
|
|
37
46
|
|
|
38
47
|
- `update_task` 可同时更新进度、保存 `checkpoint:{note,next_step}` 并记录会话;完成时给 `record_session:true`,返回会话编号后不再另调 `log_session`。省略该参数仍兼容旧的手动记会话流程。同一幂等键重放不会重复落账;跨多个文件的异常仍遵守原有“不确定状态需核对”规则,不宣称事务原子性。
|
|
39
48
|
- `feature/fix` 转 `done` 必须关联实际文件,并先通过 `run_quality_matrix` 执行相应测试。系统自动选择最新报告,核对执行成功、测试前后与当前源码摘要、测试单元目录和项目证据策略。`verification_run` 可明确指向最新报告,文字 `verification` 只作补充说明。报告存于 `.pm/quality-runs/`,任务保存报告与源码 SHA-256。目录范围匹配不等于代码覆盖率证明;第一方报告也不构成防篡改认证。
|
|
@@ -68,7 +77,7 @@ AI 编码项目的典型失控(本工具逐一给出机制):
|
|
|
68
77
|
|
|
69
78
|
## 一个命令自动添加到 AI 编程助手
|
|
70
79
|
|
|
71
|
-
本页安装示例使用 npm 上最新的**已发布**版本(当前为 **0.
|
|
80
|
+
本页安装示例使用 npm 上最新的**已发布**版本(当前为 **0.6.0**)。仓库源码与发布版本同步;需要本地验证时可用 `setup --local` 从源码构建后接入。
|
|
72
81
|
|
|
73
82
|
要求:Node.js ≥ 22.18。Windows、macOS 和 Linux 使用同一条命令:
|
|
74
83
|
|
|
@@ -82,7 +91,7 @@ npx -y @luckychen1993/pm-mcp@latest setup
|
|
|
82
91
|
npx -y @luckychen1993/pm-mcp@latest setup --dry-run
|
|
83
92
|
```
|
|
84
93
|
|
|
85
|
-
非标准安装路径无法自动发现时,可在同一命令末尾指定 `--client codex|claude|zcode|cursor|vscode|workbuddy`;`--client print` 输出通用 MCP JSON。首次启动由 `npx` 下载并在本机运行,pm-mcp 本身不需要 API Key,也不会调用远程模型。
|
|
94
|
+
非标准安装路径无法自动发现时,可在同一命令末尾指定 `--client codex|claude|zcode|cursor|vscode|workbuddy|qoder`;`--client print` 输出通用 MCP JSON。首次启动由 `npx` 下载并在本机运行,pm-mcp 本身不需要 API Key,也不会调用远程模型。
|
|
86
95
|
|
|
87
96
|
### 新项目一条命令上手(Codex 必用)
|
|
88
97
|
|
|
@@ -134,21 +143,12 @@ $u='https://raw.githubusercontent.com/happy520ai/pm-mcp/v0.1.5/install.ps1'; $f=
|
|
|
134
143
|
|
|
135
144
|
#### WorkBuddy 桌面版
|
|
136
145
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
0.3.0 发布后:
|
|
146
|
+
要求 pm-mcp ≥ 0.3.0(`--client workbuddy` 为 0.3.0 新增)。一条命令接入:
|
|
140
147
|
|
|
141
148
|
```bash
|
|
142
149
|
npx -y @luckychen1993/pm-mcp@latest setup --client workbuddy
|
|
143
150
|
```
|
|
144
151
|
|
|
145
|
-
0.3.0 发布前,从源码构建后用本地入口接入(`--local` 绑定本包已构建的 `dist/index.js`):
|
|
146
|
-
|
|
147
|
-
```bash
|
|
148
|
-
npm ci && npm run build
|
|
149
|
-
node dist/cli.js setup --client workbuddy --local
|
|
150
|
-
```
|
|
151
|
-
|
|
152
152
|
写入 `<WorkBuddy 数据目录>/mcp.json`,结构与下方通用 JSON 一致。数据目录按 `WORKBUDDY_CONFIG_DIR` → `~/.workbuddy-ai`(当前版本)→ `~/.workbuddy`(旧版)顺序解析,写入前会生成带时间戳的备份,未知顶层字段与既有 server 全部保留。
|
|
153
153
|
|
|
154
154
|
与其他客户端的关键差别:**WorkBuddy 的条目刻意不写 `--root`**。它的 MCP 子进程由会话工作区拉起,cwd 即当前项目,服务端按 cwd 解析项目根——因此一条配置同时服务所有工作区,不需要逐项目钉根;未初始化的目录只返回「未初始化」,不会写入任何文件。
|
|
@@ -179,14 +179,53 @@ node dist/cli.js doctor --root . --client workbuddy # 核验 48 工具
|
|
|
179
179
|
|
|
180
180
|
服务默认以客户端启动它时的工作目录作为项目根。若客户端不从项目目录启动,可在 `args` 末尾增加 `"--root", "项目绝对路径"`,或设置 `PM_ROOT`。第一次使用未初始化的项目时,让 AI 调用 `init_project`;MCP 工具可能读写目标项目的 `.pm/`,请按客户端提示确认信任。
|
|
181
181
|
|
|
182
|
+
#### Qoder CLI / Qoder CN
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
npx -y @luckychen1993/pm-mcp setup --client qoder # 发布版:条目写 ~/.qoder-cn/settings.json,走 `pm-mcp launch`
|
|
186
|
+
node dist/cli.js setup --client qoder --local # 本仓库:绑定已构建的 dist/qoder-launch.js
|
|
187
|
+
node dist/cli.js doctor --root . --client qoder # 真实握手核验(配置改完先跑这个)
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Qoder 拉起 MCP 子进程时 cwd 是它的安装目录(worker cwd shim),且 1.1.57 实测不加载 `.qoder/settings.local.json` 的 `mcpServers`。**更关键的是:Qoder 只派生一个常驻 stdio 子进程,再用本地路由把所有窗口/会话复用到它上面**(实测同一 server uuid 被多个不同 session token 共用,进程可存活数天)。因此任何「启动时解析一次项目根」的做法对其它窗口必然是错的——运行日志目录名结尾的 `-p<pid>` 也是主进程 pid,`process.ppid` 无法区分会话。
|
|
191
|
+
|
|
192
|
+
所以项目根是**每次调用重新认领**的:Qoder 在派发前会把 `tool.requested`(含工具名与参数原文)写进本会话运行日志,服务端用「工具名 + 参数 + 紧时间窗」做请求指纹倒扫最近日志,命中那份日志头部的 `initial_cwd` 就是调用方工作区。适配器 `dist/qoder-launch.js` 只负责打标记与给出启动期默认值:继承到的 `QODER_WORKER_CWD`,否则取最近在写的那份运行日志的 `initial_cwd`,通过 `PM_MCP_ROOT_HINT(_SOURCE)` 交给服务端,并**不再伪造 `--root`**,所以 `get_status` 报出的依据是真实来历而不是清一色的 `--root`。认领顺序:调用时显式传的 `root` 参数 > 请求指纹命中 > 唯一活跃会话 > 启动期提示(标 `spawn-time`)> 启动解析。指纹命中多个会话、或多个会话同时活跃又无指纹时**一律不猜**:退回启动根,在 `root_source` 里写明「未猜测」并列出候选,此时给本次调用传 `root` 即可确定。
|
|
193
|
+
|
|
194
|
+
你在 `args` 里显式写 `--root` 时适配器完全尊重它:不打适配器标记,服务端也就不会按请求改根(适合「一个固定账本」的用法)。
|
|
195
|
+
|
|
196
|
+
等价的手写配置(`setup --client qoder --local` 写的就是这个形状):
|
|
197
|
+
|
|
198
|
+
```json
|
|
199
|
+
{
|
|
200
|
+
"mcpServers": {
|
|
201
|
+
"pm-mcp": {
|
|
202
|
+
"command": "node",
|
|
203
|
+
"args": ["<本仓库绝对路径>/dist/qoder-launch.js"]
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
也可以跳过适配器直接指服务器:`["<绝对路径>/dist/index.js", "--root", "${QODER_WORKER_CWD}"]`。客户端支持 args 插值时直接生效;不支持时留在 `args` 里的字面量会被 pm-mcp 判为无效值忽略,转而采用继承到的 `QODER_WORKER_CWD` 本身。**但这条写法把项目根钉在派生那一刻**:进程被所有窗口共用时,后开的窗口会读到先开窗口的项目,只适合单工作区或你确实想要一个固定账本的场景。
|
|
210
|
+
|
|
211
|
+
推荐用适配器写法:一条配置服务所有工作区,且每个窗口各归各的账本。核验:重连(`/mcp reload` 或重开会话)后调用 `get_status`,「项目根」后面会标出本次认领依据(正常应是 `qoder-session-log(request-match)`);`structuredContent` 里同时带 `project`/`milestones`/`focus`/`health`/`last_session`,只呈现结构化载荷的客户端也能看到完整状态。
|
|
212
|
+
|
|
213
|
+
> `launch` 子命令与 `--client qoder` 自 **0.6.0** 起随 npm 发布;旧版本请升级,或用 `setup --client qoder --local` 从源码构建后的入口接入。
|
|
214
|
+
|
|
182
215
|
### 环境变量与参数
|
|
183
216
|
|
|
184
217
|
| 变量/参数 | 作用 |
|
|
185
218
|
|---|---|
|
|
186
|
-
|
|
|
187
|
-
|
|
|
219
|
+
| `root`(工具参数) | 本次调用的项目根,优先级最高;仅在服务端报告「未猜测」时才需要传 |
|
|
220
|
+
| `--root <path>` | 启动期项目根(未插值的 `${...}` 字面量视为未提供)。显式给出时**关闭按请求认领**,全程固定该根 |
|
|
221
|
+
| `PM_ROOT` | 启动期项目根(次优先级) |
|
|
222
|
+
| `PM_MCP_CLIENT_ADAPTER` | 由适配器设为 `qoder`:开启按请求认领调用方工作区;未设置时行为与从前完全一致(CLI/ZCode/直接 stdio 不受影响) |
|
|
223
|
+
| `PM_MCP_ROOT_HINT` / `PM_MCP_ROOT_HINT_SOURCE` | 适配器给出的启动期默认根及其真实来历,`get_status` 会标成 `<来历>(spawn-time)` |
|
|
224
|
+
| `QODER_WORKER_CWD` | 客户端工作区提示:仅当启动目录不是已初始化项目时采用,优先取已初始化的提示目录,其次取存在的目录 |
|
|
188
225
|
| `PM_MCP_HOME` | 全局注册表重定向(默认 `~`;多环境/测试隔离用) |
|
|
189
226
|
|
|
227
|
+
启动期解析顺序:`--root` > `PM_ROOT` > 适配器提示 > 已初始化的启动目录 > 客户端工作区提示 > 启动目录。**每次调用**再按「`root` 参数 > 请求指纹 > 唯一活跃会话 > 启动期提示 > 启动解析」重新认领(仅在 `PM_MCP_CLIENT_ADAPTER=qoder` 时生效)。`get_status` 与启动横幅都会回报本次采用的依据,依据里出现「未猜测」就说明证据不足、需要显式传 `root`。
|
|
228
|
+
|
|
190
229
|
### pm-mcp probe:标准 raw tools/list 直连探针
|
|
191
230
|
|
|
192
231
|
宿主(ZCode/Codex/Cursor 等)是唯一 MCP client 持有者,不会向模型透传原始 `tools/list`;要拿服务器在协议线上的真实工具清单,用 `probe` 绕过宿主直连:
|
|
@@ -246,7 +285,7 @@ snapshot_codebase + audit_structure 对账;audit_security 安全体检;audit
|
|
|
246
285
|
|
|
247
286
|
也可以直接用内置 prompts:`start-session` / `end-session` / `onboard` / `architecture-review` / `acceptance-review`;其中 `onboard` 会引导客户端读取状态并生成新人/AI 入职简报。
|
|
248
287
|
|
|
249
|
-
## 工具清单(
|
|
288
|
+
## 工具清单(54 个)
|
|
250
289
|
|
|
251
290
|
| 域 | 工具 | 说明 |
|
|
252
291
|
|---|---|---|
|
|
@@ -259,12 +298,12 @@ snapshot_codebase + audit_structure 对账;audit_security 安全体检;audit
|
|
|
259
298
|
| 调试 | `log_debug` | 调试知识账:症状/根因/修法/验证 |
|
|
260
299
|
| 求证 | `search_code` / `search_knowledge` / `annotate_file` | 代码检索(file:line)、七类知识源检索(含模块/接口/仓库治理)、文件用途索引 |
|
|
261
300
|
| 审计 | `snapshot_codebase` / `audit_structure` | 结构快照;完整性/增长/漂移/债务/churn/复杂度/索引/足迹/测试/Git 十节对账 |
|
|
262
|
-
| 安全 | `audit_security` / `list_findings` / `resolve_finding` | 密钥+危险模式+新增/通配依赖提示;扫描命中原文不落盘,处置 note
|
|
301
|
+
| 安全 | `audit_security` / `list_findings` / `resolve_finding` / `search_code_public` | 密钥+危险模式+新增/通配依赖提示;扫描命中原文不落盘,处置 note 拒绝内置密钥形态;公开代码反查(联网默认关,confirm 门控,字面命中≠侵权结论) |
|
|
263
302
|
| 法律 | `audit_license` | 依赖许可证清单、copyleft 冲突、GPL 头检测、LICENSE 检查、来源登记 |
|
|
264
303
|
| 可观测 | `get_usage_log` / `get_runtime_log` | 使用日志(工具调用统计、错误率、耗时、输出与折叠省下的 token 估算——验证省不省 token)与运行日志(server/watcher/工具报错);只记计量不记参数内容 |
|
|
265
304
|
| 注册表 | `list_projects` | 本机所有被管理项目 |
|
|
266
305
|
| 治理模型 | `init_governance` / `get_governance` / `upsert_module` / `upsert_interface` / `upsert_repository` / `set_governance_policies` | 结构化模块根、owner、语言、公开接口、允许/禁止依赖、跨仓版本约束与强制策略 |
|
|
267
|
-
| 语义治理 | `discover_languages` / `audit_governance` / `dependency_graph` / `impact_analysis` / `list_semantic_evidence` / `save_semantic_evidence` / `replace_semantic_evidence_for_file` | 编译器/Tree-sitter AST 关系、hash-bound
|
|
306
|
+
| 语义治理 | `discover_languages` / `audit_governance` / `list_governance_issues` / `dependency_graph` / `impact_analysis` / `list_semantic_evidence` / `save_semantic_evidence` / `replace_semantic_evidence_for_file` | 编译器/Tree-sitter AST 关系、hash-bound 原生分析器/运行时证据、模块与文件级循环、完整治理问题分页、依赖邻域/枢纽/unresolved/覆盖率与变更反向闭包 |
|
|
268
307
|
| 质量与组合 | `plan_quality_matrix` / `run_quality_matrix` / `get_portfolio` / `find_duplicates` / `audit_osv` | plan 与真实执行严格分离;shell=false;跨仓阶段/债务/安全/版本/cycle 聚合;行指纹聚类跨文件重复块;OSV 联网漏洞比对(默认关,须显式 confirm=true) |
|
|
269
308
|
| 标准化验收 | `list_acceptance_baselines` / `get_acceptance_baseline` / `save_acceptance_baseline_draft` / `approve_acceptance_baseline` / `evaluate_acceptance` | ISO/SQuaRE 对齐的版本化质量基线、冻结证据指针、需求—风险—测试追踪、机器判定报告与 SHA-256 manifest |
|
|
270
309
|
|
package/dist/acceptance-tools.js
CHANGED
|
@@ -268,46 +268,51 @@ export function evaluateAcceptanceFile(root, input) {
|
|
|
268
268
|
refreshDerived(root);
|
|
269
269
|
return { report, ...files };
|
|
270
270
|
}
|
|
271
|
-
export function registerAcceptanceTools(server,
|
|
272
|
-
toolR(server,
|
|
271
|
+
export function registerAcceptanceTools(server, startupRoot) {
|
|
272
|
+
toolR(server, startupRoot, "list_acceptance_baselines", "列出版本化验收基线、审批状态、当前 fingerprint 和规模。", {}, (_args, ctx) => {
|
|
273
|
+
const root = ctx.root;
|
|
273
274
|
requireInitialized(root);
|
|
274
275
|
return JSON.stringify(listAcceptanceBaselines(root), null, 2);
|
|
275
276
|
});
|
|
276
|
-
toolR(server,
|
|
277
|
+
toolR(server, startupRoot, "get_acceptance_baseline", "读取一个验收基线;默认返回摘要,full=true 返回严格 JSON。", {
|
|
277
278
|
baseline_id: IdSchema,
|
|
278
279
|
baseline_version: SemVerSchema,
|
|
279
280
|
full: z.boolean().optional(),
|
|
280
|
-
}, (args) => {
|
|
281
|
+
}, (args, ctx) => {
|
|
282
|
+
const root = ctx.root;
|
|
281
283
|
requireInitialized(root);
|
|
282
284
|
const baseline = loadAcceptanceBaseline(root, args.baseline_id, args.baseline_version);
|
|
283
285
|
if (args.full)
|
|
284
286
|
return JSON.stringify(baseline, null, 2);
|
|
285
287
|
return JSON.stringify(listAcceptanceBaselines(root).find((item) => item.id === args.baseline_id && item.version === args.baseline_version), null, 2);
|
|
286
288
|
});
|
|
287
|
-
toolI(server,
|
|
289
|
+
toolI(server, startupRoot, "save_acceptance_baseline_draft", "从项目内 .pm/acceptance 下的严格 JSON 保存 draft 基线;批准后的同版本不可覆盖。", {
|
|
288
290
|
baseline_file: JsonPathSchema,
|
|
289
|
-
}, (args) => {
|
|
291
|
+
}, (args, ctx) => {
|
|
292
|
+
const root = ctx.root;
|
|
290
293
|
requireInitialized(root);
|
|
291
294
|
const baseline = saveDraftAcceptanceBaselineFile(root, args.baseline_file);
|
|
292
295
|
return `✅ draft ${baseline.baseline_id}@${baseline.baseline_version} 已保存;fingerprint=${acceptanceBaselineFingerprint(baseline)}`;
|
|
293
296
|
});
|
|
294
|
-
toolI(server,
|
|
297
|
+
toolI(server, startupRoot, "approve_acceptance_baseline", "按当前 SHA-256 fingerprint 原子批准基线;fingerprint 变化即拒绝,批准后修改必须升版。", {
|
|
295
298
|
baseline_id: IdSchema,
|
|
296
299
|
baseline_version: SemVerSchema,
|
|
297
300
|
expected_fingerprint_sha256: Sha256Schema,
|
|
298
301
|
approved_by: z.string().trim().min(1),
|
|
299
302
|
approval_rationale: z.string().trim().min(8),
|
|
300
303
|
approved_at: z.string().datetime({ offset: true }).optional(),
|
|
301
|
-
}, (args) => {
|
|
304
|
+
}, (args, ctx) => {
|
|
305
|
+
const root = ctx.root;
|
|
302
306
|
requireInitialized(root);
|
|
303
307
|
const approved = approveAcceptanceBaseline(root, args);
|
|
304
308
|
return `✅ baseline ${approved.baseline_id}@${approved.baseline_version} 已批准;approved fingerprint=${acceptanceBaselineFingerprint(approved)}`;
|
|
305
309
|
});
|
|
306
|
-
toolI(server,
|
|
310
|
+
toolI(server, startupRoot, "evaluate_acceptance", "读取项目内 .pm/acceptance evaluation JSON,逐项复算证据 SHA-256,再生成不可手填、不可覆盖的正式报告。", {
|
|
307
311
|
baseline_id: IdSchema,
|
|
308
312
|
baseline_version: SemVerSchema,
|
|
309
313
|
evaluation_file: JsonPathSchema,
|
|
310
|
-
}, (args) => {
|
|
314
|
+
}, (args, ctx) => {
|
|
315
|
+
const root = ctx.root;
|
|
311
316
|
requireInitialized(root);
|
|
312
317
|
const result = evaluateAcceptanceFile(root, args);
|
|
313
318
|
const relativeJson = path.relative(path.resolve(root), result.json_file).replace(/\\/g, "/");
|
package/dist/audit-tools.js
CHANGED
|
@@ -10,39 +10,59 @@ import { budgetLines, toolR, toolW } from "./tool-base.js";
|
|
|
10
10
|
import { walkRefresh } from "./index-store.js";
|
|
11
11
|
import { renderDuplicates } from "./duplicates.js";
|
|
12
12
|
import { renderOsv, auditOsv } from "./osv.js";
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
import { renderPublicSearch, searchPublicCode } from "./public-search.js";
|
|
14
|
+
export function registerAuditTools(server, startupRoot) {
|
|
15
|
+
toolW(server, startupRoot, "snapshot_codebase", "给代码结构拍快照(文件数/行数/目录分布/测试数/skip 标记/依赖清单)。之后 audit_structure 会与上次快照 diff。建议每个会话或每个里程碑结束时拍一次。", {}, (_args, ctx) => {
|
|
16
|
+
const root = ctx.root;
|
|
15
17
|
requireInitialized(root);
|
|
16
18
|
const { snapshot, summary } = snapshotCodebase(root);
|
|
17
19
|
refreshDerived(root);
|
|
18
20
|
return [...summary, `下次 audit_structure 将以此为基线(${snapshot.file})。`].join("\n");
|
|
19
|
-
}, () => { requireInitialized(root); walkRefresh(root, { forceContent: true }); });
|
|
20
|
-
toolR(server,
|
|
21
|
+
}, (_args, ctx) => { const root = ctx.root; requireInitialized(root); walkRefresh(root, { forceContent: true }); });
|
|
22
|
+
toolR(server, startupRoot, "audit_structure", "八项结构对账(定期做):①增长与新增依赖 ②漂移对账(功能↔文件,防幻觉)③债务与重构配额 ④churn 热点 ⑤复杂度预算 ⑥索引覆盖率 ⑦足迹/产出 ⑧测试健康(禁用/蒸发/空测试/背书占比)。", {}, (_args, ctx) => {
|
|
23
|
+
const root = ctx.root;
|
|
21
24
|
requireInitialized(root);
|
|
22
25
|
return auditStructure(root, budgetLines(root), true);
|
|
23
26
|
});
|
|
24
|
-
toolR(server,
|
|
27
|
+
toolR(server, startupRoot, "find_duplicates", "重复代码检测:注释剥离/字符串占位/空白归一化的行指纹滑动窗口聚类,跨文件重复块按长度降序列出。", {
|
|
25
28
|
min_lines: z.number().int().min(4).max(20).optional().describe("最小重复行数(有效代码行),默认 6"),
|
|
26
29
|
limit: z.number().int().min(1).max(100).optional().describe("最多列出的重复组数,默认 20"),
|
|
27
|
-
}, (args) => {
|
|
30
|
+
}, (args, ctx) => {
|
|
31
|
+
const root = ctx.root;
|
|
28
32
|
requireInitialized(root);
|
|
29
33
|
return renderDuplicates(root, args ?? {}, budgetLines(root));
|
|
30
34
|
});
|
|
31
|
-
toolR(server,
|
|
35
|
+
toolR(server, startupRoot, "audit_osv", "OSV.dev 已知漏洞查询(联网!默认关):把 manifest 声明的依赖(仅包名/版本/生态)批量发给 api.osv.dev 比对;必须显式 confirm=true 才发起网络请求。", {
|
|
32
36
|
confirm: z.literal(true).describe("显式确认联网:本工具会把依赖名与版本发送到 api.osv.dev"),
|
|
33
37
|
timeout_ms: z.number().int().min(5000).max(120000).optional().describe("每批请求超时毫秒数,默认 30000"),
|
|
34
|
-
}, async (args) => {
|
|
38
|
+
}, async (args, ctx) => {
|
|
39
|
+
const root = ctx.root;
|
|
35
40
|
requireInitialized(root);
|
|
36
41
|
const scan = await auditOsv(root, { timeoutMs: args.timeout_ms });
|
|
37
42
|
return renderOsv(scan, budgetLines(root));
|
|
38
43
|
});
|
|
39
|
-
|
|
44
|
+
toolR(server, startupRoot, "search_code_public", "公开代码反查(联网!默认关):把一段本仓源码行作为精确短语发给 GitHub Code Search,列出包含相同代码的公开仓库;片段在参数里对用户全程可见,必须显式 confirm=true。", {
|
|
45
|
+
confirm: z.literal(true).describe("显式确认联网:本工具会把一段真实代码行发送到 GitHub"),
|
|
46
|
+
snippet: z.string().trim().min(8).max(400).optional().describe("要反查的单行代码片段(与 file 二选一)"),
|
|
47
|
+
file: z.string().trim().min(1).max(512).optional().describe("从该文件取一行作为片段(相对项目根;缺省取最长行)"),
|
|
48
|
+
line: z.number().int().min(1).optional().describe("指定取第几行(1-based)"),
|
|
49
|
+
max_results: z.number().int().min(1).max(30).optional().describe("列出的命中数上限,默认 10"),
|
|
50
|
+
timeout_ms: z.number().int().min(5000).max(120000).optional().describe("请求超时毫秒数,默认 30000"),
|
|
51
|
+
}, async (args, ctx) => {
|
|
52
|
+
const root = ctx.root;
|
|
53
|
+
requireInitialized(root);
|
|
54
|
+
const result = await searchPublicCode(root, { snippet: args.snippet, file: args.file, line: args.line, maxResults: args.max_results, timeoutMs: args.timeout_ms });
|
|
55
|
+
return renderPublicSearch(result, budgetLines(root));
|
|
56
|
+
});
|
|
57
|
+
toolW(server, startupRoot, "audit_security", "安全体检(只扫描本地、不联网,结果写入安全台账):密钥泄露 / 危险模式(eval、SQL 拼接、禁用证书校验等)/ 依赖风险。修复后重扫自动关闭;接受风险必须留理由。", {}, (_args, ctx, prepared) => {
|
|
58
|
+
const root = ctx.root;
|
|
40
59
|
requireInitialized(root);
|
|
41
60
|
const report = auditSecurity(root, prepared);
|
|
42
61
|
refreshDerived(root);
|
|
43
62
|
return report.text.join("\n");
|
|
44
|
-
}, () => { requireInitialized(root); return prepareSecurityAudit(root); });
|
|
45
|
-
toolR(server,
|
|
63
|
+
}, (_args, ctx) => { const root = ctx.root; requireInitialized(root); return prepareSecurityAudit(root); });
|
|
64
|
+
toolR(server, startupRoot, "list_findings", "查看安全台账(可按状态过滤)。", { status: z.enum(["open", "fixed", "accepted"]).optional() }, (args, ctx) => {
|
|
65
|
+
const root = ctx.root;
|
|
46
66
|
requireInitialized(root);
|
|
47
67
|
const list = listFindings(root, args.status);
|
|
48
68
|
if (list.length === 0)
|
|
@@ -50,23 +70,26 @@ export function registerAuditTools(server, root) {
|
|
|
50
70
|
const L = list.map((f) => `[${f.status}/${f.severity}] ${f.id} ${f.file}:${f.line} ${f.rule} — ${f.note.slice(0, 60)}`);
|
|
51
71
|
return foldLines([`共 ${list.length} 条:`, ...L], { maxLines: budgetLines(root) });
|
|
52
72
|
});
|
|
53
|
-
toolW(server,
|
|
73
|
+
toolW(server, startupRoot, "resolve_finding", "处置安全发现:修复后标 fixed;带理由接受风险标 accepted(必须填 note——接受是显式选择,要留痕)。", {
|
|
54
74
|
id: z.string().describe("如 SEC-001"),
|
|
55
75
|
status: z.enum(["fixed", "accepted"]),
|
|
56
76
|
note: z.string().optional().describe("accepted 时必填:接受理由"),
|
|
57
|
-
}, (args) => {
|
|
77
|
+
}, (args, ctx) => {
|
|
78
|
+
const root = ctx.root;
|
|
58
79
|
requireInitialized(root);
|
|
59
80
|
const f = resolveFinding(root, args.id, args.status, args.note ?? "");
|
|
60
81
|
refreshDerived(root);
|
|
61
82
|
return `✅ ${f.id} 已置为 ${f.status}${f.note ? `(${f.note})` : ""}。`;
|
|
62
83
|
});
|
|
63
|
-
toolR(server,
|
|
84
|
+
toolR(server, startupRoot, "audit_license", "许可证审计(法律账):依赖许可证清单与 copyleft 冲突、源码中的 GPL 家族许可证头、LICENSE 文件检查、来源登记清单。离线启发式,不替代法务。", {}, (_args, ctx) => {
|
|
85
|
+
const root = ctx.root;
|
|
64
86
|
requireInitialized(root);
|
|
65
87
|
return auditLicense(root, budgetLines(root), true);
|
|
66
88
|
});
|
|
67
89
|
}
|
|
68
|
-
export function registerRegistryTools(server,
|
|
69
|
-
toolR(server,
|
|
90
|
+
export function registerRegistryTools(server, startupRoot) {
|
|
91
|
+
toolR(server, startupRoot, "list_projects", "列出本机所有被 pm-mcp 管理的项目(全局注册表)。", {}, (_args, ctx) => {
|
|
92
|
+
const root = ctx.root;
|
|
70
93
|
const list = listRegistry();
|
|
71
94
|
if (list.length === 0)
|
|
72
95
|
return "(注册表为空。在任何项目里 init_project 后会自动登记。)";
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
/* Qoder 只派生一个常驻 stdio 子进程服务所有窗口(实测:同一 server uuid 被三个不同 session token 复用),
|
|
5
|
+
所以项目根不能在启动时定格。本模块在每次工具调用时反查「是谁在调我」:客户端运行日志会先记下
|
|
6
|
+
tool.requested(含工具名与参数原文),再发起调用,于是用本次请求做指纹就能锁定调用方那个会话的
|
|
7
|
+
initial_cwd。取不到证据时一律如实降级并说明依据,绝不猜。 */
|
|
8
|
+
export const CLIENT_ADAPTER_ENV = "PM_MCP_CLIENT_ADAPTER";
|
|
9
|
+
export const ROOT_HINT_ENV = "PM_MCP_ROOT_HINT";
|
|
10
|
+
export const ROOT_HINT_SOURCE_ENV = "PM_MCP_ROOT_HINT_SOURCE";
|
|
11
|
+
export const QODER_ADAPTER = "qoder";
|
|
12
|
+
/** 服务端工具名前缀:客户端把工具暴露成 mcp__<server>__<tool>,服务名可能是 pm_mcp 或 pm-mcp */
|
|
13
|
+
const TOOL_NAME_PATTERN = /mcp__pm[_-]mcp__/;
|
|
14
|
+
function isDir(p) {
|
|
15
|
+
try {
|
|
16
|
+
return fs.statSync(p).isDirectory();
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/** 显式给了 QODER_CONFIG_DIR 就只看它:否则本机的真实 ~/.qoder-cn 会混进测试与隔离环境 */
|
|
23
|
+
function configDirs(env) {
|
|
24
|
+
const fromEnv = env.QODER_CONFIG_DIR?.trim();
|
|
25
|
+
if (fromEnv && isDir(fromEnv))
|
|
26
|
+
return [path.resolve(fromEnv)];
|
|
27
|
+
const home = os.homedir();
|
|
28
|
+
return [".qoder-cn", ".qoder"].map((name) => path.join(home, name)).filter(isDir);
|
|
29
|
+
}
|
|
30
|
+
/** 日志目录名以 -p<pid> 结尾,但 Qoder 全部会话共用主进程 pid,因此 ppid 不能用来区分会话 */
|
|
31
|
+
function runLogDirs(env) {
|
|
32
|
+
const out = [];
|
|
33
|
+
for (const dir of configDirs(env)) {
|
|
34
|
+
const runs = path.join(dir, "logs", "runs");
|
|
35
|
+
let names;
|
|
36
|
+
try {
|
|
37
|
+
names = fs.readdirSync(runs);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
for (const name of names)
|
|
43
|
+
out.push(path.join(runs, name, "qodercli.log"));
|
|
44
|
+
}
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
function workspaceOfLog(file) {
|
|
48
|
+
let head;
|
|
49
|
+
try {
|
|
50
|
+
const fd = fs.openSync(file, "r");
|
|
51
|
+
try {
|
|
52
|
+
const buffer = Buffer.alloc(8192);
|
|
53
|
+
const bytes = fs.readSync(fd, buffer, 0, 8192, 0);
|
|
54
|
+
head = buffer.toString("utf8", 0, bytes);
|
|
55
|
+
}
|
|
56
|
+
finally {
|
|
57
|
+
fs.closeSync(fd);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
const match = /initial_cwd=([\s\S]*?)\s+restore_cwd=/.exec(head);
|
|
64
|
+
const value = match?.[1]?.trim();
|
|
65
|
+
return value && isDir(value) ? path.resolve(value) : null;
|
|
66
|
+
}
|
|
67
|
+
function recentRunLogs(options) {
|
|
68
|
+
const logs = [];
|
|
69
|
+
for (const file of runLogDirs(options.env)) {
|
|
70
|
+
let stat;
|
|
71
|
+
try {
|
|
72
|
+
stat = fs.statSync(file);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (!stat.isFile() || options.now - stat.mtimeMs > options.windowMs)
|
|
78
|
+
continue;
|
|
79
|
+
logs.push({ file, mtime: stat.mtimeMs, size: stat.size, workspace: workspaceOfLog(file) });
|
|
80
|
+
}
|
|
81
|
+
return logs.sort((a, b) => b.mtime - a.mtime);
|
|
82
|
+
}
|
|
83
|
+
/** 从日志尾部按块往前扫,扫到比本次请求还早就停:活跃会话日志可能几 MB,但相关行永远在最后几秒 */
|
|
84
|
+
const CHUNK_BYTES = 256 * 1024;
|
|
85
|
+
const MAX_SCAN_BYTES = 8 * 1024 * 1024;
|
|
86
|
+
/** 日志记的是客户端发出的原始参数,服务端拿到的是 zod 解析后的(可能补了默认值):只要求日志参数被本次参数覆盖 */
|
|
87
|
+
function argsCovered(logged, received) {
|
|
88
|
+
if (logged === undefined || logged === null)
|
|
89
|
+
return true;
|
|
90
|
+
if (typeof logged !== "object" || typeof received !== "object" || received === null)
|
|
91
|
+
return logged === received;
|
|
92
|
+
if (Array.isArray(logged) !== Array.isArray(received))
|
|
93
|
+
return false;
|
|
94
|
+
const loggedRecord = logged;
|
|
95
|
+
const receivedRecord = received;
|
|
96
|
+
return Object.keys(loggedRecord).every((key) => argsCovered(loggedRecord[key], receivedRecord[key]));
|
|
97
|
+
}
|
|
98
|
+
function extractArgsJson(line) {
|
|
99
|
+
const marker = line.indexOf(" args=");
|
|
100
|
+
if (marker < 0)
|
|
101
|
+
return undefined;
|
|
102
|
+
try {
|
|
103
|
+
return JSON.parse(line.slice(marker + " args=".length));
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
function lineMatchesRequest(line, tool, args) {
|
|
110
|
+
if (!TOOL_NAME_PATTERN.test(line))
|
|
111
|
+
return false;
|
|
112
|
+
const payload = extractArgsJson(line);
|
|
113
|
+
if (payload === undefined || payload === null || typeof payload !== "object")
|
|
114
|
+
return false;
|
|
115
|
+
const record = payload;
|
|
116
|
+
if (typeof record.toolName === "string") {
|
|
117
|
+
return record.toolName.endsWith(`__${tool}`) && argsCovered(record.arguments, args);
|
|
118
|
+
}
|
|
119
|
+
const name = /tool_name="([^"]+)"/.exec(line)?.[1];
|
|
120
|
+
return name === `mcp__pm_mcp__${tool}` && argsCovered(record, args);
|
|
121
|
+
}
|
|
122
|
+
function lineTimestamp(line) {
|
|
123
|
+
const stamp = /^\s*(\d{4}-\d{2}-\d{2}T[\d:.]+(?:[+-]\d{2}:\d{2}|Z))/.exec(line)?.[1];
|
|
124
|
+
if (!stamp)
|
|
125
|
+
return null;
|
|
126
|
+
const parsed = Date.parse(stamp);
|
|
127
|
+
return Number.isNaN(parsed) ? null : parsed;
|
|
128
|
+
}
|
|
129
|
+
function inWindow(at, window) {
|
|
130
|
+
return at !== null && at <= window.now + window.tieMs && window.now - at <= window.maxAgeMs;
|
|
131
|
+
}
|
|
132
|
+
/** 倒着按块扫,按字节拼接后再解码,避免把多字节字符从中间截断成乱码 */
|
|
133
|
+
function scanLogForRequest(log, tool, args, window) {
|
|
134
|
+
let fd;
|
|
135
|
+
try {
|
|
136
|
+
fd = fs.openSync(log.file, "r");
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
let best = null;
|
|
142
|
+
try {
|
|
143
|
+
let offset = log.size;
|
|
144
|
+
let scanned = 0;
|
|
145
|
+
let carry = Buffer.alloc(0);
|
|
146
|
+
while (offset > 0 && scanned < MAX_SCAN_BYTES) {
|
|
147
|
+
const length = Math.min(CHUNK_BYTES, offset);
|
|
148
|
+
offset -= length;
|
|
149
|
+
scanned += length;
|
|
150
|
+
const chunk = Buffer.alloc(length);
|
|
151
|
+
const bytes = fs.readSync(fd, chunk, 0, length, offset);
|
|
152
|
+
const buffer = Buffer.concat([bytes === length ? chunk : chunk.subarray(0, bytes), carry]);
|
|
153
|
+
const firstNewline = buffer.indexOf(0x0a);
|
|
154
|
+
if (firstNewline === -1) {
|
|
155
|
+
carry = buffer;
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
carry = buffer.subarray(0, firstNewline + 1);
|
|
159
|
+
const lines = buffer.subarray(firstNewline + 1).toString("utf8").split(/\r?\n/).filter((line) => line.length > 0);
|
|
160
|
+
let oldest = null;
|
|
161
|
+
for (let i = lines.length - 1; i >= 0; i -= 1) {
|
|
162
|
+
const line = lines[i];
|
|
163
|
+
const at = lineTimestamp(line);
|
|
164
|
+
if (at !== null)
|
|
165
|
+
oldest = at;
|
|
166
|
+
if (!inWindow(at, window))
|
|
167
|
+
continue;
|
|
168
|
+
if (!line.includes(tool) || !lineMatchesRequest(line, tool, args))
|
|
169
|
+
continue;
|
|
170
|
+
if (!best || at > best.at)
|
|
171
|
+
best = { workspace: log.workspace, at: at };
|
|
172
|
+
}
|
|
173
|
+
if (oldest !== null && window.now - oldest > window.maxAgeMs)
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
return best;
|
|
179
|
+
}
|
|
180
|
+
finally {
|
|
181
|
+
fs.closeSync(fd);
|
|
182
|
+
}
|
|
183
|
+
return best;
|
|
184
|
+
}
|
|
185
|
+
/** 找出本次请求的指纹落在哪个会话日志里。
|
|
186
|
+
必须按时间收紧:客户端日志会长期留着「工具名+参数」,两个窗口先后发同一个无参调用会留下同样的指纹,
|
|
187
|
+
只有「刚刚写下」的那条才可能是本次调用方。 */
|
|
188
|
+
export function matchCallerWorkspaces(options) {
|
|
189
|
+
const window = { now: options.now, maxAgeMs: options.maxAgeMs ?? 30_000, tieMs: options.tieMs ?? 2_000 };
|
|
190
|
+
const hits = [];
|
|
191
|
+
for (const log of options.logs) {
|
|
192
|
+
if (!log.workspace)
|
|
193
|
+
continue;
|
|
194
|
+
const hit = scanLogForRequest(log, options.tool, options.args, window);
|
|
195
|
+
if (hit)
|
|
196
|
+
hits.push(hit);
|
|
197
|
+
}
|
|
198
|
+
if (hits.length === 0)
|
|
199
|
+
return [];
|
|
200
|
+
hits.sort((a, b) => b.at - a.at);
|
|
201
|
+
const newest = hits[0];
|
|
202
|
+
return hits
|
|
203
|
+
.filter((hit) => newest.at - hit.at <= window.tieMs)
|
|
204
|
+
.filter((hit, index, all) => all.findIndex((other) => other.workspace === hit.workspace) === index);
|
|
205
|
+
}
|
|
206
|
+
/* 不缓存解析结果:两个窗口在几百毫秒内发同一个无参调用时,缓存会把前一个窗口的项目根串给后一个。
|
|
207
|
+
单次解析只扫最近几秒的日志尾部,代价可以接受,正确性优先。 */
|
|
208
|
+
/** 每次工具调用解析项目根:显式 root 参数 > 请求指纹命中的会话工作区 > 唯一活跃会话 > 启动期提示 > 启动解析 */
|
|
209
|
+
export function resolveRequestRoot(options) {
|
|
210
|
+
const env = options.env ?? process.env;
|
|
211
|
+
const now = options.now ?? Date.now();
|
|
212
|
+
const startup = { root: options.startupRoot, source: options.startupSource };
|
|
213
|
+
if (env[CLIENT_ADAPTER_ENV] !== QODER_ADAPTER)
|
|
214
|
+
return startup;
|
|
215
|
+
const explicit = typeof options.args.root === "string" ? options.args.root.trim() : "";
|
|
216
|
+
if (explicit && isDir(explicit))
|
|
217
|
+
return { root: path.resolve(explicit), source: "argument" };
|
|
218
|
+
const logs = recentRunLogs({ env, now, windowMs: options.scanWindowMs ?? 60_000 });
|
|
219
|
+
const matched = matchCallerWorkspaces({ tool: options.tool, args: options.args, logs, now });
|
|
220
|
+
if (matched.length === 1)
|
|
221
|
+
return { root: matched[0].workspace, source: "qoder-session-log(request-match)" };
|
|
222
|
+
if (matched.length > 1) {
|
|
223
|
+
return {
|
|
224
|
+
...startup,
|
|
225
|
+
source: `startup(${options.startupSource});请求指纹同时命中 ${matched.length} 个会话,未猜测`,
|
|
226
|
+
candidates: matched.map((hit) => hit.workspace),
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
const hot = logs.filter((log) => log.workspace && now - log.mtime <= (options.hotWindowMs ?? 10_000));
|
|
230
|
+
const hotWorkspaces = [...new Set(hot.map((log) => log.workspace))];
|
|
231
|
+
if (hotWorkspaces.length === 1)
|
|
232
|
+
return { root: hotWorkspaces[0], source: "qoder-session-log(only-active)" };
|
|
233
|
+
// 多个会话同时活跃又没指纹:宁可如实报歧义,也不拿启动期那个可能早就过期的根去猜
|
|
234
|
+
if (hotWorkspaces.length > 1) {
|
|
235
|
+
return { ...startup, source: `startup(${options.startupSource});多个 Qoder 会话同时活跃,未猜测`, candidates: hotWorkspaces };
|
|
236
|
+
}
|
|
237
|
+
const hint = env[ROOT_HINT_ENV]?.trim();
|
|
238
|
+
if (hint && isDir(hint)) {
|
|
239
|
+
const basis = env[ROOT_HINT_SOURCE_ENV]?.trim() || "hint";
|
|
240
|
+
return { root: path.resolve(hint), source: `${basis}(spawn-time)` };
|
|
241
|
+
}
|
|
242
|
+
return startup;
|
|
243
|
+
}
|
|
244
|
+
export const __test = { recentRunLogs, runLogDirs, configDirs, scanLogForRequest };
|
package/dist/cli.js
CHANGED
|
@@ -13,7 +13,7 @@ else if (process.argv[2] === "doctor") {
|
|
|
13
13
|
try {
|
|
14
14
|
const { parseDoctorArgs, runDoctor } = await import("./doctor.js");
|
|
15
15
|
if (process.argv.includes("--help") || process.argv.includes("-h"))
|
|
16
|
-
console.log("pm-mcp doctor --root <项目> [--client codex|workbuddy] [--config <配置文件> | --entry <本地入口>]");
|
|
16
|
+
console.log("pm-mcp doctor --root <项目> [--client codex|workbuddy|qoder] [--config <配置文件> | --entry <本地入口>]");
|
|
17
17
|
else {
|
|
18
18
|
const result = await runDoctor(parseDoctorArgs(process.argv.slice(3)));
|
|
19
19
|
console.log(JSON.stringify(result, null, 2));
|
|
@@ -63,6 +63,16 @@ else if (process.argv[2] === "probe") {
|
|
|
63
63
|
process.exitCode = 1;
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
|
+
else if (process.argv[2] === "launch") {
|
|
67
|
+
try {
|
|
68
|
+
const { runQoderLaunch } = await import("./qoder-launch.js");
|
|
69
|
+
runQoderLaunch(process.argv.slice(3));
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
console.error(`[pm-mcp launch] ERROR: ${error.message}`);
|
|
73
|
+
process.exitCode = 1;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
66
76
|
else {
|
|
67
77
|
await import("./index.js");
|
|
68
78
|
}
|