@luckychen1993/pm-mcp 0.4.0 → 0.5.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 CHANGED
@@ -33,7 +33,7 @@ node dist/cli.js doctor --root .
33
33
 
34
34
  ## 已有管理流程
35
35
 
36
- 用户仍然直接说“继续这个任务”“保存进度”“完成后帮我验收”。52 个工具全部保留,由 AI 组合调用,无需用户记工具名。
36
+ 用户仍然直接说“继续这个任务”“保存进度”“完成后帮我验收”。53 个工具全部保留,由 AI 组合调用,无需用户记工具名。
37
37
 
38
38
  - `update_task` 可同时更新进度、保存 `checkpoint:{note,next_step}` 并记录会话;完成时给 `record_session:true`,返回会话编号后不再另调 `log_session`。省略该参数仍兼容旧的手动记会话流程。同一幂等键重放不会重复落账;跨多个文件的异常仍遵守原有“不确定状态需核对”规则,不宣称事务原子性。
39
39
  - `feature/fix` 转 `done` 必须关联实际文件,并先通过 `run_quality_matrix` 执行相应测试。系统自动选择最新报告,核对执行成功、测试前后与当前源码摘要、测试单元目录和项目证据策略。`verification_run` 可明确指向最新报告,文字 `verification` 只作补充说明。报告存于 `.pm/quality-runs/`,任务保存报告与源码 SHA-256。目录范围匹配不等于代码覆盖率证明;第一方报告也不构成防篡改认证。
@@ -246,7 +246,7 @@ snapshot_codebase + audit_structure 对账;audit_security 安全体检;audit
246
246
 
247
247
  也可以直接用内置 prompts:`start-session` / `end-session` / `onboard` / `architecture-review` / `acceptance-review`;其中 `onboard` 会引导客户端读取状态并生成新人/AI 入职简报。
248
248
 
249
- ## 工具清单(52 个)
249
+ ## 工具清单(53 个)
250
250
 
251
251
  | 域 | 工具 | 说明 |
252
252
  |---|---|---|
@@ -259,7 +259,7 @@ snapshot_codebase + audit_structure 对账;audit_security 安全体检;audit
259
259
  | 调试 | `log_debug` | 调试知识账:症状/根因/修法/验证 |
260
260
  | 求证 | `search_code` / `search_knowledge` / `annotate_file` | 代码检索(file:line)、七类知识源检索(含模块/接口/仓库治理)、文件用途索引 |
261
261
  | 审计 | `snapshot_codebase` / `audit_structure` | 结构快照;完整性/增长/漂移/债务/churn/复杂度/索引/足迹/测试/Git 十节对账 |
262
- | 安全 | `audit_security` / `list_findings` / `resolve_finding` | 密钥+危险模式+新增/通配依赖提示;扫描命中原文不落盘,处置 note 拒绝内置密钥形态 |
262
+ | 安全 | `audit_security` / `list_findings` / `resolve_finding` / `search_code_public` | 密钥+危险模式+新增/通配依赖提示;扫描命中原文不落盘,处置 note 拒绝内置密钥形态;公开代码反查(联网默认关,confirm 门控,字面命中≠侵权结论) |
263
263
  | 法律 | `audit_license` | 依赖许可证清单、copyleft 冲突、GPL 头检测、LICENSE 检查、来源登记 |
264
264
  | 可观测 | `get_usage_log` / `get_runtime_log` | 使用日志(工具调用统计、错误率、耗时、输出与折叠省下的 token 估算——验证省不省 token)与运行日志(server/watcher/工具报错);只记计量不记参数内容 |
265
265
  | 注册表 | `list_projects` | 本机所有被管理项目 |
@@ -10,6 +10,7 @@ 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
+ import { renderPublicSearch, searchPublicCode } from "./public-search.js";
13
14
  export function registerAuditTools(server, root) {
14
15
  toolW(server, root, "snapshot_codebase", "给代码结构拍快照(文件数/行数/目录分布/测试数/skip 标记/依赖清单)。之后 audit_structure 会与上次快照 diff。建议每个会话或每个里程碑结束时拍一次。", {}, () => {
15
16
  requireInitialized(root);
@@ -36,6 +37,18 @@ export function registerAuditTools(server, root) {
36
37
  const scan = await auditOsv(root, { timeoutMs: args.timeout_ms });
37
38
  return renderOsv(scan, budgetLines(root));
38
39
  });
40
+ toolR(server, root, "search_code_public", "公开代码反查(联网!默认关):把一段本仓源码行作为精确短语发给 GitHub Code Search,列出包含相同代码的公开仓库;片段在参数里对用户全程可见,必须显式 confirm=true。", {
41
+ confirm: z.literal(true).describe("显式确认联网:本工具会把一段真实代码行发送到 GitHub"),
42
+ snippet: z.string().trim().min(8).max(400).optional().describe("要反查的单行代码片段(与 file 二选一)"),
43
+ file: z.string().trim().min(1).max(512).optional().describe("从该文件取一行作为片段(相对项目根;缺省取最长行)"),
44
+ line: z.number().int().min(1).optional().describe("指定取第几行(1-based)"),
45
+ max_results: z.number().int().min(1).max(30).optional().describe("列出的命中数上限,默认 10"),
46
+ timeout_ms: z.number().int().min(5000).max(120000).optional().describe("请求超时毫秒数,默认 30000"),
47
+ }, async (args) => {
48
+ requireInitialized(root);
49
+ const result = await searchPublicCode(root, { snippet: args.snippet, file: args.file, line: args.line, maxResults: args.max_results, timeoutMs: args.timeout_ms });
50
+ return renderPublicSearch(result, budgetLines(root));
51
+ });
39
52
  toolW(server, root, "audit_security", "安全体检(只扫描本地、不联网,结果写入安全台账):密钥泄露 / 危险模式(eval、SQL 拼接、禁用证书校验等)/ 依赖风险。修复后重扫自动关闭;接受风险必须留理由。", {}, (_args, prepared) => {
40
53
  requireInitialized(root);
41
54
  const report = auditSecurity(root, prepared);
@@ -0,0 +1,105 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { spawnSync } from "node:child_process";
4
+ import { foldLines } from "./budget.js";
5
+ /**
6
+ * 公开代码反查(GitHub Code Search,联网,显式确认后才会发起请求)。
7
+ *
8
+ * 与 audit_osv 的本质差异:OSV 只发包名/版本,本工具会把一段**真实代码行**发到
9
+ * GitHub——因此门控更强:confirm 必须显式为 true,且片段来自用户显式传入的
10
+ * snippet 参数或 file+line 选择(在客户端审批界面全程可见)。
11
+ * 仅做精确短语匹配:命中 ≠ 侵权结论,私有仓库不可见,配额 10 次/分钟。
12
+ */
13
+ const GITHUB_SEARCH_ENDPOINT = "https://api.github.com/search/code";
14
+ const MIN_SNIPPET = 8;
15
+ const MAX_QUERY_CHARS = 200;
16
+ /** 解析要外发的片段:显式 snippet 优先;否则 file(+可选 line,缺省取最长行)。 */
17
+ export function resolveSnippet(root, args) {
18
+ if (args.snippet !== undefined && args.snippet !== "") {
19
+ const snippet = args.snippet.replace(/\s+/g, " ").trim();
20
+ if (snippet.length < MIN_SNIPPET)
21
+ throw new Error(`片段太短(≥${MIN_SNIPPET} 字符),太短会命中海量无关仓库。`);
22
+ if (args.file !== undefined)
23
+ throw new Error("snippet 与 file 二选一。");
24
+ return snippet;
25
+ }
26
+ if (args.file !== undefined) {
27
+ const absolute = path.resolve(root, args.file);
28
+ if (absolute !== root && !absolute.startsWith(root + path.sep))
29
+ throw new Error("file 必须位于项目根内。");
30
+ const raw = fs.readFileSync(absolute, "utf8");
31
+ const lines = raw.split(/\r?\n/);
32
+ const index = args.line === undefined
33
+ ? lines.reduce((best, line, i) => (line.trim().length > (lines[best]?.trim().length ?? 0) ? i : best), 0)
34
+ : args.line - 1;
35
+ const line = lines[index];
36
+ if (line === undefined)
37
+ throw new Error(`行号越界:${args.file} 共 ${lines.length} 行,第 ${args.line ?? 0} 行不存在。`);
38
+ const snippet = line.replace(/\s+/g, " ").trim();
39
+ if (snippet.length < MIN_SNIPPET)
40
+ throw new Error(`选中的行太短(≥${MIN_SNIPPET} 字符):${snippet.slice(0, 40)}`);
41
+ return snippet.slice(0, MAX_QUERY_CHARS);
42
+ }
43
+ throw new Error("需要 snippet 或 file 之一:确认要外发到 GitHub 的代码片段。");
44
+ }
45
+ function resolveToken(explicit) {
46
+ if (explicit)
47
+ return explicit;
48
+ const fromEnv = process.env.GH_TOKEN ?? process.env.GITHUB_TOKEN;
49
+ if (fromEnv)
50
+ return fromEnv;
51
+ const gh = spawnSync("gh", ["auth", "token"], { encoding: "utf8", windowsHide: true });
52
+ const fromGh = gh.status === 0 ? gh.stdout.trim() : "";
53
+ if (fromGh)
54
+ return fromGh;
55
+ throw new Error("未找到 GitHub 凭据:设置 GH_TOKEN/GITHUB_TOKEN,或先 gh auth login。");
56
+ }
57
+ export async function searchPublicCode(root, args = {}) {
58
+ const started = Date.now();
59
+ const snippet = resolveSnippet(root, args);
60
+ const token = args.tokenResolver ? args.tokenResolver() : resolveToken(args.token);
61
+ const fetcher = args.fetcher ?? globalThis.fetch;
62
+ const maxResults = Math.min(30, Math.max(1, Math.trunc(args.maxResults ?? 10)));
63
+ // GitHub REST code search 是词项检索(不支持带引号短语):按空白拆词、去掉引号后以 AND 语义提交。
64
+ const query = snippet.replace(/"/g, " ").split(/\s+/).filter(Boolean).join(" ");
65
+ const response = await fetcher(`${GITHUB_SEARCH_ENDPOINT}?q=${encodeURIComponent(query)}&per_page=${maxResults}`, {
66
+ method: "GET",
67
+ headers: {
68
+ authorization: `Bearer ${token}`,
69
+ accept: "application/vnd.github+json",
70
+ "user-agent": "pm-mcp-public-search (single-line provenance lookup)",
71
+ "x-github-api-version": "2022-11-28",
72
+ },
73
+ signal: AbortSignal.timeout(args.timeoutMs ?? 30_000),
74
+ });
75
+ if (response.status === 401)
76
+ throw new Error("GitHub token 无效(401)。");
77
+ if (response.status === 403)
78
+ throw new Error("GitHub 拒绝(403):可能触发了 code search 限流(10 次/分钟)或 token 权限不足。");
79
+ if (response.status === 422)
80
+ throw new Error("GitHub 拒绝查询(422):短语可能不合法,换一段更普通的行试试。");
81
+ if (!response.ok)
82
+ throw new Error(`GitHub Code Search 失败:HTTP ${response.status}`);
83
+ const payload = JSON.parse(await response.text());
84
+ const items = Array.isArray(payload.items) ? payload.items : [];
85
+ const hits = items.slice(0, maxResults).map((item) => ({
86
+ repo: typeof item.repository?.full_name === "string" ? item.repository.full_name : "unknown",
87
+ path: typeof item.path === "string" ? item.path : "",
88
+ url: typeof item.html_url === "string" ? item.html_url : "",
89
+ }));
90
+ const totalCount = typeof payload.total_count === "number" ? payload.total_count : hits.length;
91
+ return { snippet: snippet.slice(0, MAX_QUERY_CHARS), totalCount, hits, truncated: totalCount > hits.length, durationMs: Date.now() - started };
92
+ }
93
+ export function renderPublicSearch(result, maxLines = 150) {
94
+ const lines = [
95
+ "## 公开代码反查(GitHub Code Search,联网完成)",
96
+ `- 外发片段(已确认): "${result.snippet}"`,
97
+ `- 命中: ${result.totalCount} 处(列出前 ${result.hits.length}${result.truncated ? ",已截断" : ""})· 耗时 ${result.durationMs}ms`,
98
+ ];
99
+ for (const hit of result.hits)
100
+ lines.push(`- 命中 ${hit.repo} · ${hit.path}${hit.url ? ` · ${hit.url}` : ""}`);
101
+ if (result.hits.length === 0)
102
+ lines.push("- ✅ 公开索引中未发现相同代码行");
103
+ lines.push("> 命中是字面相同,不是侵权结论:可能是同一开源出处、常见写法或独立实现。私有仓库不可见;配额 10 次/分钟。");
104
+ return foldLines(lines, { maxLines, hint: "换更长或更独特的行可提高区分度" });
105
+ }
package/dist/version.js CHANGED
@@ -7,4 +7,4 @@ export const PACKAGE_SPEC = `${metadata.name}@${VERSION}`;
7
7
  export const RUNTIME_CONTRACT = "pagination-evidence-v1";
8
8
  // 服务器工具目录契约(doctor 与独立巡检 health-check 都以此核对)。
9
9
  // 新增/移除 MCP 工具时必须同步更新这里,并用 `pm-mcp probe` 实测复核。
10
- export const TOOL_CATALOG_SIZE = 52;
10
+ export const TOOL_CATALOG_SIZE = 53;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@luckychen1993/pm-mcp",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "AI 编码项目的单一事实来源 + 健康台账 MCP 服务(事实来源 / 路线图 / 防幻觉 / 七本账 / 断点续做 / token 经济)",
5
5
  "keywords": [
6
6
  "mcp",