@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/dist/index.js
CHANGED
|
@@ -1,33 +1,25 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
/** 入口与装配:解析项目根、创建 server、按域注册 tools/resources/prompts、启动 watcher。 */
|
|
2
3
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
4
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
-
import {
|
|
5
|
+
import { resolveRootWithSource, recordRootResolution, isInitialized, ensurePmRuntimeIgnored } from "./paths.js";
|
|
5
6
|
import { registerAllTools } from "./tools.js";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import { buildChangelog } from "./dashboard.js";
|
|
9
|
-
import { dashboardFile, pmPath } from "./paths.js";
|
|
7
|
+
import { registerResources } from "./resources.js";
|
|
8
|
+
import { registerPrompts } from "./prompts.js";
|
|
10
9
|
import { startWatcher } from "./index-store.js";
|
|
11
|
-
import { auditGovernance } from "./governance-audit.js";
|
|
12
|
-
import { buildPortfolioReport, loadPortfolioProject } from "./portfolio.js";
|
|
13
|
-
import fs from "node:fs";
|
|
14
|
-
import path from "node:path";
|
|
15
|
-
import { listAcceptanceBaselines } from "./acceptance-tools.js";
|
|
16
|
-
import { AcceptanceReportSchema } from "./acceptance-report.js";
|
|
17
|
-
import { runCoalescedRead } from "./idempotency.js";
|
|
18
|
-
import { recordUsage } from "./tool-base.js";
|
|
19
10
|
import { logRuntime } from "./usage-log.js";
|
|
20
11
|
import { VERSION } from "./version.js";
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
/* --root 参数 > PM_ROOT 环境变量 > 启动时工作目录 */
|
|
12
|
+
/* 启动根只是默认值:--root 参数 > PM_ROOT > 适配器提示 > 已初始化的启动目录 > 客户端工作区提示 > 启动时工作目录。
|
|
13
|
+
客户端(如 Qoder)会把一个常驻子进程复用给所有窗口,真正的调用方工作区由 caller-workspace 按请求认领。 */
|
|
24
14
|
const argv = process.argv.slice(2);
|
|
25
15
|
let explicitRoot;
|
|
26
16
|
for (let i = 0; i < argv.length; i++) {
|
|
27
17
|
if (argv[i] === "--root" && argv[i + 1])
|
|
28
18
|
explicitRoot = argv[i + 1];
|
|
29
19
|
}
|
|
30
|
-
const
|
|
20
|
+
const resolution = resolveRootWithSource(explicitRoot);
|
|
21
|
+
recordRootResolution(resolution);
|
|
22
|
+
const root = resolution.root;
|
|
31
23
|
const server = new McpServer({ name: "pm-mcp", version: VERSION }, {
|
|
32
24
|
instructions: [
|
|
33
25
|
`pm-mcp ${VERSION}。get_status 返回实际运行版本与项目根;配置修改后须重连核验。list_tasks 有 has_more 时保持过滤条件并使用 next_cursor 续取,不能把一页当全量。feature/fix 默认要求 tests_observed,未知计数不冒充测试通过。`,
|
|
@@ -37,195 +29,8 @@ const server = new McpServer({ name: "pm-mcp", version: VERSION }, {
|
|
|
37
29
|
].join(" "),
|
|
38
30
|
});
|
|
39
31
|
registerAllTools(server, root);
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
try {
|
|
43
|
-
return fn();
|
|
44
|
-
}
|
|
45
|
-
catch (e) {
|
|
46
|
-
return `(不可用: ${e.message})`;
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
async function resourceText(name, fn) {
|
|
50
|
-
const started = Date.now();
|
|
51
|
-
try {
|
|
52
|
-
const text = (await runCoalescedRead(root, `resource:${name}`, {}, () => safeText(fn))).text;
|
|
53
|
-
recordUsage(root, `resource:${name}`, "read", started, { ok: true, text });
|
|
54
|
-
return text;
|
|
55
|
-
}
|
|
56
|
-
catch (error) {
|
|
57
|
-
recordUsage(root, `resource:${name}`, "read", started, { ok: false, error: error.message });
|
|
58
|
-
throw error;
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
server.registerResource("dashboard", "pm://dashboard", { description: "项目仪表盘(与仓库根 PROJECT.md 同源),路线图/健康摘要/任务/功能清单" }, async (uri) => ({
|
|
62
|
-
contents: [
|
|
63
|
-
{
|
|
64
|
-
uri: uri.href,
|
|
65
|
-
mimeType: "text/markdown",
|
|
66
|
-
text: await resourceText("dashboard", () => fs.readFileSync(dashboardFile(root), "utf8")),
|
|
67
|
-
},
|
|
68
|
-
],
|
|
69
|
-
}));
|
|
70
|
-
server.registerResource("roadmap", "pm://roadmap", { description: "路线图(里程碑进度 + 活跃里程碑任务明细)" }, async (uri) => ({
|
|
71
|
-
contents: [
|
|
72
|
-
{
|
|
73
|
-
uri: uri.href,
|
|
74
|
-
mimeType: "text/markdown",
|
|
75
|
-
text: await resourceText("roadmap", () => renderRoadmap(loadRoadmap(root).milestones, loadTasks(root).tasks, 2).join("\n")),
|
|
76
|
-
},
|
|
77
|
-
],
|
|
78
|
-
}));
|
|
79
|
-
server.registerResource("tasks", "pm://tasks", { description: "任务原始数据;大列表末尾返回 continuation,可通过 list_tasks 保持 include_done:true 连续取回后续任务" }, async (uri) => ({
|
|
80
|
-
contents: [
|
|
81
|
-
{
|
|
82
|
-
uri: uri.href,
|
|
83
|
-
mimeType: "application/json",
|
|
84
|
-
text: await resourceText("tasks", () => {
|
|
85
|
-
const data = loadTasks(root);
|
|
86
|
-
const tasks = data.tasks;
|
|
87
|
-
// 资源也要守 token 预算:全量 dump 大账本会吃爆上下文(截断后仍是合法 JSON)
|
|
88
|
-
if (tasks.length <= 100)
|
|
89
|
-
return JSON.stringify(tasks, null, 2);
|
|
90
|
-
const page = taskPage(root, { include_done: true, page_size: 100 }, budgetLines(root) - 4, data);
|
|
91
|
-
const shown = tasks.slice(0, page.returned);
|
|
92
|
-
shown.push({
|
|
93
|
-
id: `_TRUNCATED_${tasks.length - page.returned}`,
|
|
94
|
-
title: `…本次输出已截断,另有 ${tasks.length - page.returned} 个任务,用 continuation 连续读取`,
|
|
95
|
-
continuation: { tool: "list_tasks", arguments: { include_done: true, cursor: page.next_cursor } },
|
|
96
|
-
});
|
|
97
|
-
return JSON.stringify(shown, null, 2);
|
|
98
|
-
}),
|
|
99
|
-
},
|
|
100
|
-
],
|
|
101
|
-
}));
|
|
102
|
-
server.registerResource("changelog", "pm://changelog", { description: "变更日志(由 sessions.json 生成的人类可读版本)" }, async (uri) => ({
|
|
103
|
-
contents: [
|
|
104
|
-
{
|
|
105
|
-
uri: uri.href,
|
|
106
|
-
mimeType: "text/markdown",
|
|
107
|
-
text: await resourceText("changelog", () => buildChangelog(root)),
|
|
108
|
-
},
|
|
109
|
-
],
|
|
110
|
-
}));
|
|
111
|
-
server.registerResource("architecture", "pm://architecture", { description: "跨文件/模块/语言治理审计:语义覆盖、依赖边界、接口、循环与质量矩阵" }, async (uri) => ({
|
|
112
|
-
contents: [{ uri: uri.href, mimeType: "text/markdown", text: await resourceText("architecture", () => auditGovernance(root, 150).report) }],
|
|
113
|
-
}));
|
|
114
|
-
server.registerResource("portfolio", "pm://portfolio", { description: "当前项目的组合视图与跨仓依赖/版本风险" }, async (uri) => ({
|
|
115
|
-
contents: [{
|
|
116
|
-
uri: uri.href,
|
|
117
|
-
mimeType: "application/json",
|
|
118
|
-
text: await resourceText("portfolio", () => JSON.stringify(buildPortfolioReport({ projects: [loadPortfolioProject(root)], projectsRequested: 1 }), null, 2)),
|
|
119
|
-
}],
|
|
120
|
-
}));
|
|
121
|
-
server.registerResource("acceptance", "pm://acceptance", { description: "标准化产品验收概览:版本化预批准基线与最近一次机器判定报告" }, async (uri) => ({
|
|
122
|
-
contents: [{
|
|
123
|
-
uri: uri.href,
|
|
124
|
-
mimeType: "application/json",
|
|
125
|
-
text: await resourceText("acceptance", () => {
|
|
126
|
-
const reportDir = pmPath(root, "acceptance", "reports");
|
|
127
|
-
let latestReport = null;
|
|
128
|
-
if (fs.existsSync(reportDir)) {
|
|
129
|
-
const candidates = fs.readdirSync(reportDir, { withFileTypes: true })
|
|
130
|
-
.filter((entry) => entry.isFile() && entry.name.endsWith(".json"))
|
|
131
|
-
.map((entry) => ({ name: entry.name, mtime: fs.statSync(path.join(reportDir, entry.name)).mtimeMs }))
|
|
132
|
-
.sort((a, b) => b.mtime - a.mtime || a.name.localeCompare(b.name));
|
|
133
|
-
if (candidates[0])
|
|
134
|
-
latestReport = AcceptanceReportSchema.parse(JSON.parse(fs.readFileSync(path.join(reportDir, candidates[0].name), "utf8")));
|
|
135
|
-
}
|
|
136
|
-
return JSON.stringify({ baselines: listAcceptanceBaselines(root), latest_report: latestReport }, null, 2);
|
|
137
|
-
}),
|
|
138
|
-
}],
|
|
139
|
-
}));
|
|
140
|
-
/* --------------------------------- Prompts -------------------------------- */
|
|
141
|
-
server.registerPrompt("start-session", { description: "开工仪式:恢复项目上下文,从断点续做,不靠回忆。" }, () => ({
|
|
142
|
-
messages: [
|
|
143
|
-
{
|
|
144
|
-
role: "user",
|
|
145
|
-
content: {
|
|
146
|
-
type: "text",
|
|
147
|
-
text: [
|
|
148
|
-
"按以下顺序恢复项目上下文(本项目由 pm-mcp 管理,状态以 .pm/ 为准,不要凭记忆陈述):",
|
|
149
|
-
"1. 调用 get_status:了解阶段、里程碑进度、进行中任务及其 checkpoint 下一步。",
|
|
150
|
-
"2. 若要动的模块不清楚,先 search_code 定位到具体文件与行,再读文件——三段式定位,不要全量读代码。",
|
|
151
|
-
"3. 陈述本次计划:要做哪个任务、动哪些文件、验收标准是什么。",
|
|
152
|
-
"4. 过程纪律:走捷径就 add_task(type=debt) 登记债务;修 bug 完成后 log_debug 记根因;长任务随手 checkpoint。",
|
|
153
|
-
].join("\n"),
|
|
154
|
-
},
|
|
155
|
-
},
|
|
156
|
-
],
|
|
157
|
-
}));
|
|
158
|
-
server.registerPrompt("end-session", { description: "收工仪式:更新任务与里程碑、留断点、记会话——把本次上下文写回仓库。" }, () => ({
|
|
159
|
-
messages: [
|
|
160
|
-
{
|
|
161
|
-
role: "user",
|
|
162
|
-
content: {
|
|
163
|
-
type: "text",
|
|
164
|
-
text: [
|
|
165
|
-
"收工前依次完成(顺序执行,缺一不可):",
|
|
166
|
-
"1. feature/fix 先执行适用测试,生成最新质量报告;update_task 置 done,给 result_note 与实际 files,record_session=true 可一并归档。",
|
|
167
|
-
"2. 未完成的任务:checkpoint 保存「进展 + 下一步具体动作」。",
|
|
168
|
-
"3. 本次落地的功能:register_feature 登记(入口文件 + 测试文件)。",
|
|
169
|
-
"4. 修过的 bug:log_debug 记录症状/根因/修法/验证。",
|
|
170
|
-
"5. 走过的捷径:add_task(type=debt) 登记债务,别让它在沉默中腐烂。",
|
|
171
|
-
"6. update_task 已返回自动会话编号时,不重复 log_session;否则用 log_session 记录真实改动清单。纯问答不需要另建任务或反复写会话。",
|
|
172
|
-
"7. 若到达里程碑节点:update_milestone 流转状态;可 snapshot_codebase 拍快照 + audit_structure 对账。",
|
|
173
|
-
].join("\n"),
|
|
174
|
-
},
|
|
175
|
-
},
|
|
176
|
-
],
|
|
177
|
-
}));
|
|
178
|
-
server.registerPrompt("onboard", { description: "入职简报工作流:引导客户端为新成员(人或 AI)生成项目现状一页纸。" }, () => ({
|
|
179
|
-
messages: [
|
|
180
|
-
{
|
|
181
|
-
role: "user",
|
|
182
|
-
content: {
|
|
183
|
-
type: "text",
|
|
184
|
-
text: [
|
|
185
|
-
"为新加入的成员(人或 AI)生成入职简报,数据全部来自 .pm/ 状态,不要编造:",
|
|
186
|
-
"1. 读 pm://dashboard(或 PROJECT.md):项目是什么、路线图到哪、健康状态如何。",
|
|
187
|
-
"2. list_decisions + 阅读最近 3 条 ADR:理解关键取舍的「为什么」。",
|
|
188
|
-
"3. list_features:了解已有功能地图与各功能入口文件。",
|
|
189
|
-
"4. get_status:当前焦点与断点。",
|
|
190
|
-
"5. 读 pm://architecture 与 pm://portfolio:模块 owner、公开接口、跨语言质量矩阵和跨仓版本风险。",
|
|
191
|
-
"输出一页纸简报:项目一句话 / 技术栈与模块 / 路线图现状 / 关键决策与红线 / 模块与接口边界 / 当前焦点与下一步 / 哪里有坑(债务、安全、语义与跨仓风险)。",
|
|
192
|
-
].join("\n"),
|
|
193
|
-
},
|
|
194
|
-
},
|
|
195
|
-
],
|
|
196
|
-
}));
|
|
197
|
-
server.registerPrompt("architecture-review", { description: "跨文件/模块/语言架构评审:先取证,再判断影响和门禁。" }, () => ({
|
|
198
|
-
messages: [{
|
|
199
|
-
role: "user",
|
|
200
|
-
content: {
|
|
201
|
-
type: "text",
|
|
202
|
-
text: [
|
|
203
|
-
"按证据完成跨模块/跨语言架构评审:",
|
|
204
|
-
"1. 读取 pm://architecture 或调用 audit_governance,先检查语义覆盖、unresolved、owner、私有接口、循环和边界违规。",
|
|
205
|
-
"2. 对本次变更调用 impact_analysis,列出反向依赖文件与模块;未知文件不得忽略。",
|
|
206
|
-
"3. 调用 plan_quality_matrix;这一步只是计划,不能声称测试通过。",
|
|
207
|
-
"4. 只有用户明确要求执行时才调用 run_quality_matrix(confirm_execute=true),并分别报告每种语言的真实结果。",
|
|
208
|
-
"5. 跨仓变更读取 pm://portfolio,核对依赖目标、版本约束、加载失败与仓库循环。",
|
|
209
|
-
].join("\n"),
|
|
210
|
-
},
|
|
211
|
-
}],
|
|
212
|
-
}));
|
|
213
|
-
server.registerPrompt("acceptance-review", { description: "标准化终验:冻结质量基线后执行证据核验,不能把开发者自测冒充认证。" }, () => ({
|
|
214
|
-
messages: [{
|
|
215
|
-
role: "user",
|
|
216
|
-
content: {
|
|
217
|
-
type: "text",
|
|
218
|
-
text: [
|
|
219
|
-
"按 pm-mcp 的标准化验收闭环执行,不得事后调低阈值:",
|
|
220
|
-
"1. list_acceptance_baselines 并读取目标基线,确认 ISO/IEC 25010 九特性适用性、量化指标、风险和测试双向追踪。",
|
|
221
|
-
"2. 只有产品负责人审阅 draft fingerprint 后,才能 approve_acceptance_baseline;批准后的同版本不可修改。",
|
|
222
|
-
"3. 执行基线定义的质量、治理、性能与安全测试;失败、跳过、缺工具和过期证据都不得写成通过。",
|
|
223
|
-
"4. evaluate_acceptance 会重算项目内证据 SHA-256,并由机器生成不可覆盖的 JSON/Markdown 报告。",
|
|
224
|
-
"5. 分开陈述:第一方范围内验收、独立评价、托管 CI、生产证据和 ISO 认证;任何一层都不能冒充另一层。",
|
|
225
|
-
].join("\n"),
|
|
226
|
-
},
|
|
227
|
-
}],
|
|
228
|
-
}));
|
|
32
|
+
registerResources(server, root);
|
|
33
|
+
registerPrompts(server);
|
|
229
34
|
/* ---------------------------------- 启动 ---------------------------------- */
|
|
230
35
|
await server.connect(new StdioServerTransport());
|
|
231
36
|
// 超大项目核心:server 常驻期间用递归 watcher 持续保鲜 SQLite 索引,
|
|
@@ -238,4 +43,4 @@ if (isInitialized(root)) {
|
|
|
238
43
|
}
|
|
239
44
|
logRuntime(root, "info", "server.ready", `pid=${process.pid} node=${process.versions.node}`);
|
|
240
45
|
// stdio 是协议通道,日志只能走 stderr
|
|
241
|
-
console.error(`[pm-mcp] ready. project root: ${root}`);
|
|
46
|
+
console.error(`[pm-mcp] ready. default project root: ${root} (source: ${resolution.source})`);
|
package/dist/knowledge-tools.js
CHANGED
|
@@ -10,15 +10,16 @@ import { budgetLines, toolR, toolW } from "./tool-base.js";
|
|
|
10
10
|
import { now } from "./types.js";
|
|
11
11
|
import { appendSession } from "./session-log.js";
|
|
12
12
|
import { captureFileHashes } from "./git-state.js";
|
|
13
|
-
export function registerFeatureTools(server,
|
|
14
|
-
toolW(server,
|
|
13
|
+
export function registerFeatureTools(server, startupRoot) {
|
|
14
|
+
toolW(server, startupRoot, "register_feature", "登记功能(回答「这个项目有哪些功能」)。entry_files=入口文件(漂移对账锚点),test_files=测试文件(测试背书)。", {
|
|
15
15
|
name: z.string().min(1),
|
|
16
16
|
description: z.string().optional(),
|
|
17
17
|
entry_files: z.array(z.string()).optional().describe("入口/实现文件相对路径"),
|
|
18
18
|
module: z.string().optional().describe("所属模块"),
|
|
19
19
|
test_files: z.array(z.string()).optional().describe("对应测试文件"),
|
|
20
20
|
status: z.enum(["planned", "implemented", "deprecated"]).optional().describe("默认 implemented"),
|
|
21
|
-
}, (args) => {
|
|
21
|
+
}, (args, ctx) => {
|
|
22
|
+
const root = ctx.root;
|
|
22
23
|
requireInitialized(root);
|
|
23
24
|
const data = loadFeatures(root);
|
|
24
25
|
const id = nextId("F", data.seq);
|
|
@@ -43,7 +44,8 @@ export function registerFeatureTools(server, root) {
|
|
|
43
44
|
: "";
|
|
44
45
|
return `✅ 功能 ${id}「${args.name}」已登记${args.module ? `(${args.module})` : ""}。audit_structure 会核对其入口文件是否真实存在(防幻觉对账)。${hint}`;
|
|
45
46
|
});
|
|
46
|
-
toolR(server,
|
|
47
|
+
toolR(server, startupRoot, "list_features", "列出功能清单(可按模块/状态过滤)。", { module: z.string().optional(), status: z.enum(["planned", "implemented", "deprecated"]).optional() }, (args, ctx) => {
|
|
48
|
+
const root = ctx.root;
|
|
47
49
|
requireInitialized(root);
|
|
48
50
|
let list = loadFeatures(root).features;
|
|
49
51
|
if (args.module)
|
|
@@ -58,13 +60,14 @@ export function registerFeatureTools(server, root) {
|
|
|
58
60
|
return foldLines([`共 ${list.length} 个功能:`, ...L], { maxLines: budgetLines(root), hint: "用 module/status 过滤" });
|
|
59
61
|
});
|
|
60
62
|
}
|
|
61
|
-
export function registerDecisionTools(server,
|
|
62
|
-
toolW(server,
|
|
63
|
+
export function registerDecisionTools(server, startupRoot) {
|
|
64
|
+
toolW(server, startupRoot, "record_decision", "记录架构决策(ADR):为什么这么选、决定了什么、有什么后果。认知债务的解药——半年后还能回答「当时为什么」。", {
|
|
63
65
|
title: z.string().min(1),
|
|
64
66
|
context: z.string().describe("背景与约束(为什么需要做选择)"),
|
|
65
67
|
decision: z.string().describe("决定与理由"),
|
|
66
68
|
consequences: z.string().optional().describe("后果/代价/被放弃的方案"),
|
|
67
|
-
}, (args) => {
|
|
69
|
+
}, (args, ctx) => {
|
|
70
|
+
const root = ctx.root;
|
|
68
71
|
requireInitialized(root);
|
|
69
72
|
ensureDecisionDir(root);
|
|
70
73
|
const existing = fs.readdirSync(decisionsDir(root)).filter((n) => n.endsWith(".md"));
|
|
@@ -97,7 +100,8 @@ export function registerDecisionTools(server, root) {
|
|
|
97
100
|
refreshDerived(root);
|
|
98
101
|
return `✅ 决策已记录: .pm/decisions/${file}(不可变编号,git 友好)。search_knowledge 可检索。`;
|
|
99
102
|
});
|
|
100
|
-
toolR(server,
|
|
103
|
+
toolR(server, startupRoot, "list_decisions", "列出全部架构决策(ADR)。", {}, (_args, ctx) => {
|
|
104
|
+
const root = ctx.root;
|
|
101
105
|
requireInitialized(root);
|
|
102
106
|
const dir = decisionsDir(root);
|
|
103
107
|
const names = fs.existsSync(dir) ? fs.readdirSync(dir).filter((n) => n.endsWith(".md")).sort() : [];
|
|
@@ -109,13 +113,14 @@ export function registerDecisionTools(server, root) {
|
|
|
109
113
|
function ensureDecisionDir(root) {
|
|
110
114
|
fs.mkdirSync(decisionsDir(root), { recursive: true });
|
|
111
115
|
}
|
|
112
|
-
export function registerSessionTools(server,
|
|
113
|
-
toolW(server,
|
|
116
|
+
export function registerSessionTools(server, startupRoot) {
|
|
117
|
+
toolW(server, startupRoot, "log_session", "收工仪式:记录本次会话(做了什么、改了哪些文件、下一步)。files 是变更足迹(churn 账与波及面告警的数据源),务必如实列出。", {
|
|
114
118
|
summary: z.string().min(1).describe("本次做了什么(一两句)"),
|
|
115
119
|
files: z.array(z.string()).optional().describe("本次改动/新增/删除的文件(相对路径)"),
|
|
116
120
|
next_steps: z.array(z.string()).optional().describe("留给下次的下一步"),
|
|
117
121
|
author: z.string().optional().describe("署名:zcode / codex / human:名字"),
|
|
118
|
-
}, (args, hashes) => {
|
|
122
|
+
}, (args, ctx, hashes) => {
|
|
123
|
+
const root = ctx.root;
|
|
119
124
|
requireInitialized(root);
|
|
120
125
|
const project = loadProject(root);
|
|
121
126
|
const id = appendSession(root, args, hashes);
|
|
@@ -125,15 +130,16 @@ export function registerSessionTools(server, root) {
|
|
|
125
130
|
L.push(`⚠️ 本次波及 ${(args.files ?? []).length} 个文件,超过阈值 ${project.budgets.sessionBlastRadius}——波及面过大,考虑拆任务或立重构任务。`);
|
|
126
131
|
}
|
|
127
132
|
return L.join("\n");
|
|
128
|
-
}, (args) => { requireInitialized(root); return captureFileHashes(root, args.files ?? []); });
|
|
129
|
-
toolW(server,
|
|
133
|
+
}, (args, ctx) => { const root = ctx.root; requireInitialized(root); return captureFileHashes(root, args.files ?? []); });
|
|
134
|
+
toolW(server, startupRoot, "log_debug", "记录调试过程:症状 / 根因 / 修法 / 怎么验证的。调试知识账——下次同样的错不用从零猜,search_knowledge 可查。", {
|
|
130
135
|
symptom: z.string().describe("症状:什么现象、报什么错"),
|
|
131
136
|
root_cause: z.string().describe("根因(诊断出的真正原因,不是表面现象)"),
|
|
132
137
|
fix: z.string().describe("修法"),
|
|
133
138
|
verified_how: z.string().optional().describe("怎么确认修好了"),
|
|
134
139
|
files: z.array(z.string()).optional(),
|
|
135
140
|
task_id: z.string().optional().describe("关联任务 id"),
|
|
136
|
-
}, (args) => {
|
|
141
|
+
}, (args, ctx) => {
|
|
142
|
+
const root = ctx.root;
|
|
137
143
|
requireInitialized(root);
|
|
138
144
|
const data = loadDebugLog(root);
|
|
139
145
|
const id = nextId("D", data.seq, 4);
|
|
@@ -154,27 +160,30 @@ export function registerSessionTools(server, root) {
|
|
|
154
160
|
return `✅ 调试记录 ${id} 已存档。这份诊断经验已进入项目知识库。`;
|
|
155
161
|
});
|
|
156
162
|
}
|
|
157
|
-
export function registerSearchTools(server,
|
|
158
|
-
toolR(server,
|
|
163
|
+
export function registerSearchTools(server, startupRoot) {
|
|
164
|
+
toolR(server, startupRoot, "search_code", "在真实代码里检索(防幻觉事中防线):谈某个功能/文件之前先搜实码。返回 file:line 片段而非整文件(token 经济)。默认按字面量匹配;regex=true 时按 JavaScript 正则(仅在你确需时开启)。", {
|
|
159
165
|
query: z.string().min(1).describe("关键词(默认字面量匹配)"),
|
|
160
166
|
glob: z.string().optional().describe("限定文件范围,如 src/**/*.ts"),
|
|
161
167
|
max_results: z.number().int().min(1).max(100).optional().describe("默认 30"),
|
|
162
168
|
regex: z.boolean().optional().describe("默认 false;true 时 query 按正则解释"),
|
|
163
|
-
}, (args) => {
|
|
169
|
+
}, (args, ctx) => {
|
|
170
|
+
const root = ctx.root;
|
|
164
171
|
requireInitialized(root);
|
|
165
172
|
const res = searchCode(root, args.query, args.glob, args.max_results ?? 30, args.regex ?? false);
|
|
166
173
|
return formatCodeSearch(res, args.query);
|
|
167
174
|
});
|
|
168
|
-
toolR(server,
|
|
175
|
+
toolR(server, startupRoot, "search_knowledge", "检索六类项目知识源(ADR/任务/功能/会话/调试记录/文件索引)——回答「之前为什么这么做」「上次这个错怎么解的」,用历史结论代替重新推理。代码内容请用 search_code。", { query: z.string().min(1) }, (args, ctx) => {
|
|
176
|
+
const root = ctx.root;
|
|
169
177
|
requireInitialized(root);
|
|
170
178
|
return searchKnowledge(root, args.query);
|
|
171
179
|
});
|
|
172
|
-
toolW(server,
|
|
180
|
+
toolW(server, startupRoot, "annotate_file", "给文件登记一行用途(文件用途索引):之后任何会话靠索引定位,不用打开文件。引用外部代码时可登记 source/license(provenance)。", {
|
|
173
181
|
path: z.string().min(1).describe("相对路径"),
|
|
174
182
|
purpose: z.string().min(1).describe("一句话用途"),
|
|
175
183
|
source: z.string().optional().describe("代码来源(URL/仓库),如引用自外部"),
|
|
176
184
|
license: z.string().optional().describe("该文件引用代码的许可证"),
|
|
177
|
-
}, (args) => {
|
|
185
|
+
}, (args, ctx) => {
|
|
186
|
+
const root = ctx.root;
|
|
178
187
|
requireInitialized(root);
|
|
179
188
|
const rel = normSep(args.path);
|
|
180
189
|
if (!fs.existsSync(path.join(root, rel))) {
|
package/dist/paths.js
CHANGED
|
@@ -2,10 +2,68 @@ import path from "node:path";
|
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import { randomUUID } from "node:crypto";
|
|
5
|
-
/**
|
|
5
|
+
/** 客户端注入的工作区提示:部分客户端(如 Qoder)拉起 MCP 子进程时 cwd 是安装目录,工作区只存在于环境变量 */
|
|
6
|
+
export const WORKSPACE_HINT_ENV_VARS = ["QODER_WORKER_CWD"];
|
|
7
|
+
/** 启动期适配器交下来的工作区提示与它的真实来历(适配器自己解析的,不能再报成 --root) */
|
|
8
|
+
export const ROOT_HINT_ENV_VAR = "PM_MCP_ROOT_HINT";
|
|
9
|
+
export const ROOT_HINT_SOURCE_ENV_VAR = "PM_MCP_ROOT_HINT_SOURCE";
|
|
10
|
+
/** 配置里写了 ${VAR} 但客户端没有插值时,这种字面量不能当路径用 */
|
|
11
|
+
function unexpanded(value) {
|
|
12
|
+
return value.includes("${") || value.includes("}");
|
|
13
|
+
}
|
|
14
|
+
function isDir(p) {
|
|
15
|
+
try {
|
|
16
|
+
return fs.statSync(p).isDirectory();
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function usable(value) {
|
|
23
|
+
return !!value && value.trim().length > 0 && !unexpanded(value.trim());
|
|
24
|
+
}
|
|
25
|
+
function workspaceHints() {
|
|
26
|
+
const out = [];
|
|
27
|
+
for (const name of WORKSPACE_HINT_ENV_VARS) {
|
|
28
|
+
const value = process.env[name]?.trim();
|
|
29
|
+
if (value && !unexpanded(value))
|
|
30
|
+
out.push({ name, value });
|
|
31
|
+
}
|
|
32
|
+
return out;
|
|
33
|
+
}
|
|
34
|
+
/** 解析项目根并报告依据,供启动横幅与 get_status 核验 */
|
|
35
|
+
export function resolveRootWithSource(explicit) {
|
|
36
|
+
const flag = explicit?.trim();
|
|
37
|
+
if (usable(flag))
|
|
38
|
+
return { root: path.resolve(flag), source: "--root" };
|
|
39
|
+
const pmRoot = process.env.PM_ROOT?.trim();
|
|
40
|
+
if (usable(pmRoot))
|
|
41
|
+
return { root: path.resolve(pmRoot), source: "PM_ROOT" };
|
|
42
|
+
const hint = process.env[ROOT_HINT_ENV_VAR]?.trim();
|
|
43
|
+
if (usable(hint) && isDir(hint)) {
|
|
44
|
+
const basis = process.env[ROOT_HINT_SOURCE_ENV_VAR]?.trim() || ROOT_HINT_ENV_VAR;
|
|
45
|
+
return { root: path.resolve(hint), source: `${basis}(spawn-time)` };
|
|
46
|
+
}
|
|
47
|
+
const cwd = process.cwd();
|
|
48
|
+
if (isInitialized(cwd))
|
|
49
|
+
return { root: path.resolve(cwd), source: "cwd" };
|
|
50
|
+
const hints = workspaceHints();
|
|
51
|
+
const chosen = hints.find((h) => isInitialized(h.value)) ?? hints.find((h) => isDir(h.value));
|
|
52
|
+
if (chosen)
|
|
53
|
+
return { root: path.resolve(chosen.value), source: chosen.name };
|
|
54
|
+
return { root: path.resolve(cwd), source: "cwd" };
|
|
55
|
+
}
|
|
56
|
+
/** 服务端启动时定格的一次解析;get_status 用它自证项目根从哪来 */
|
|
57
|
+
let startupResolution = null;
|
|
58
|
+
export function recordRootResolution(resolution) {
|
|
59
|
+
startupResolution = resolution;
|
|
60
|
+
}
|
|
61
|
+
export function rootResolution() {
|
|
62
|
+
return startupResolution;
|
|
63
|
+
}
|
|
64
|
+
/** 项目根解析顺序:--root 参数 > PM_ROOT > 已初始化的启动目录 > 客户端工作区提示 > 当前工作目录 */
|
|
6
65
|
export function resolveRoot(explicit) {
|
|
7
|
-
|
|
8
|
-
return path.resolve(raw);
|
|
66
|
+
return resolveRootWithSource(explicit).root;
|
|
9
67
|
}
|
|
10
68
|
export const PM_DIR = ".pm";
|
|
11
69
|
export function pmPath(root, ...segs) {
|