@luckychen1993/pm-mcp 0.5.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.
@@ -19,8 +19,8 @@ function taskLine(t, withNext = false) {
19
19
  }
20
20
  return parts.join(" ");
21
21
  }
22
- export function registerProjectTools(server, root) {
23
- toolW(server, root, "init_project", "初始化当前项目的 .pm/ 状态目录(单一事实来源),并自动写入 AGENTS.md 工作规矩(已存在则增量合并,不覆盖你的内容)。每个新项目只调用一次;已初始化请用 update_project。", {
22
+ export function registerProjectTools(server, startupRoot) {
23
+ toolW(server, startupRoot, "init_project", "初始化当前项目的 .pm/ 状态目录(单一事实来源),并自动写入 AGENTS.md 工作规矩(已存在则增量合并,不覆盖你的内容)。每个新项目只调用一次;已初始化请用 update_project。", {
24
24
  name: z.string().min(1).describe("项目名"),
25
25
  description: z.string().optional().describe("一句话描述"),
26
26
  stack: z.array(z.string()).optional().describe("技术栈,如 [\"TypeScript\",\"React\"]"),
@@ -29,7 +29,8 @@ export function registerProjectTools(server, root) {
29
29
  exposure: z.enum(["local", "network", "public"]).optional().describe("暴露面,影响安全告警力度"),
30
30
  modules: z.array(z.string()).optional().describe("初始模块登记"),
31
31
  agents_md: z.boolean().optional().describe("默认 true:自动写入/合并 AGENTS.md 工作规矩(false 关闭)"),
32
- }, (args) => {
32
+ }, (args, ctx) => {
33
+ const root = ctx.root;
33
34
  const project = initProject(root, { ...args, agentsMd: args.agents_md });
34
35
  refreshDerived(root);
35
36
  return [
@@ -40,7 +41,8 @@ export function registerProjectTools(server, root) {
40
41
  "建议顺序: upsert_module 声明模块/owner/语言 → add_milestone 建里程碑 → add_task 拆任务 → 开工前 get_status / 收工后 log_session。",
41
42
  ].join("\n");
42
43
  });
43
- toolR(server, root, "get_status", "一站式「我在哪」:项目阶段 + 里程碑进度 + 进行中任务(含断点下一步)+ 健康摘要 + 最近变更。会话开工第一件事。since 可只看某日期以来的任务/会话变化。", { since: z.string().optional().describe("ISO 日期,如 2026-09-01:只看该日期以来的任务/会话变化") }, (args) => {
44
+ toolR(server, startupRoot, "get_status", "一站式「我在哪」:项目阶段 + 里程碑进度 + 进行中任务(含断点下一步)+ 健康摘要 + 最近变更。会话开工第一件事。since 可只看某日期以来的任务/会话变化。", { since: z.string().optional().describe("ISO 日期,如 2026-09-01:只看该日期以来的任务/会话变化") }, (args, ctx) => {
45
+ const root = ctx.root;
44
46
  requireInitialized(root);
45
47
  const project = loadProject(root);
46
48
  const { milestones } = loadRoadmap(root);
@@ -49,7 +51,9 @@ export function registerProjectTools(server, root) {
49
51
  const security = loadSecurity(root);
50
52
  const L = [];
51
53
  L.push(`# ${project.name}${project.phase ? ` — ${project.phase}` : ""}`);
52
- L.push(`运行版本: pm-mcp ${VERSION} · 项目根: ${root}`);
54
+ L.push(`运行版本: pm-mcp ${VERSION} · 项目根: ${root}(依据 ${ctx.rootSource})`);
55
+ if (ctx.rootCandidates?.length)
56
+ L.push(`⚠️ 未能确定调用方工作区,候选: ${ctx.rootCandidates.join(" | ")}——需要精确时给本次调用传 root。`);
53
57
  if (project.description)
54
58
  L.push(project.description);
55
59
  L.push("");
@@ -72,8 +76,15 @@ export function registerProjectTools(server, root) {
72
76
  const openDebt = tasks.filter((t) => t.type === "debt" && t.status !== "done" && t.status !== "cancelled").length;
73
77
  const openFindings = security.findings.filter((f) => f.status === "open");
74
78
  L.push(`债务未清 ${openDebt} 条 · 安全未处理 ${openFindings.length} 个(高危 ${openFindings.filter((f) => f.severity === "high").length})· 会话 ${sessions.length} 次。详细对账用 audit_structure / audit_security。`);
79
+ let governanceSummary = { modules: 0, interfaces: 0, repositories: 0, initialized: false };
75
80
  try {
76
81
  const governance = loadGovernance(root);
82
+ governanceSummary = {
83
+ modules: governance.modules.length,
84
+ interfaces: governance.interfaces.length,
85
+ repositories: governance.repositories.length,
86
+ initialized: true,
87
+ };
77
88
  L.push(`治理模型 ${governance.modules.length} 模块 / ${governance.interfaces.length} 接口 / ${governance.repositories.length} 仓库;实时语义门禁用 audit_governance。`);
78
89
  }
79
90
  catch {
@@ -87,6 +98,7 @@ export function registerProjectTools(server, root) {
87
98
  if (last.next_steps.length > 0)
88
99
  L.push(`上次留下的下一步: ${last.next_steps.join(";")}`);
89
100
  }
101
+ let sinceSummary;
90
102
  if (args.since) {
91
103
  const parsed = Date.parse(args.since);
92
104
  const since = Number.isNaN(parsed) ? args.since : new Date(parsed).toISOString();
@@ -95,33 +107,94 @@ export function registerProjectTools(server, root) {
95
107
  L.push("");
96
108
  L.push(`## 自 ${since.slice(0, 10)} 以来`);
97
109
  L.push(`新增/更新任务 ${newTasks.length} 个,会话 ${newSessions.length} 次。`);
98
- for (const t of newTasks.slice(-8).reverse())
99
- L.push(`- ${taskLine(t)}`);
110
+ const items = newTasks.slice(-8).reverse().map((t) => taskLine(t));
111
+ for (const line of items)
112
+ L.push(`- ${line}`);
113
+ sinceSummary = { since, tasks: newTasks.length, sessions: newSessions.length, items };
100
114
  }
101
115
  L.push("");
102
116
  L.push("> 三段式定位:本状态 → search_code 精确到行 → 才读具体文件。别全量读代码。");
103
- return JSON.stringify({ text: foldLines(L, { maxLines: budgetLines(root) }), runtime: { version: VERSION, root, contract: RUNTIME_CONTRACT } });
104
- }, { cacheVersion: `runtime:${VERSION}`, outputSchema: { runtime: z.object({ version: z.string(), root: z.string(), contract: z.string() }) },
105
- decode: (value) => { const result = JSON.parse(value); return { text: result.text, data: { runtime: result.runtime } }; } });
106
- toolR(server, root, "get_usage_log", "使用日志:pm-mcp 被用了哪些工具、成败、耗时,以及输出 token 估算与「折叠省下的 token」——回答“用它到底省不省 token”。只记工具名与计量,不记参数内容。日志滚动保留约 2MB。", {
117
+ // 结构化载荷必须自带全部状态:部分客户端(如 Qoder 的 mcp_call)只呈现 structuredContent,
118
+ // 只放 runtime 会让调用方看不到阶段/里程碑/进行中任务。
119
+ return JSON.stringify({
120
+ text: foldLines(L, { maxLines: budgetLines(root) }),
121
+ runtime: {
122
+ version: VERSION,
123
+ root,
124
+ contract: RUNTIME_CONTRACT,
125
+ root_source: ctx.rootSource,
126
+ ...(ctx.rootCandidates?.length ? { root_candidates: ctx.rootCandidates } : {}),
127
+ },
128
+ project: { name: project.name, phase: project.phase || null, description: project.description || null },
129
+ milestones: milestones.map((m) => ({ id: m.id, title: m.title, status: m.status, ...milestoneStats(tasks, m.id) })),
130
+ focus: active.slice(0, 10).map((t) => ({
131
+ id: t.id,
132
+ status: t.status,
133
+ title: t.title,
134
+ type: t.type,
135
+ milestone: t.milestone || null,
136
+ next_step: t.checkpoint?.next_step ?? null,
137
+ steps_done: t.steps.filter((s) => s.done).length,
138
+ steps_total: t.steps.length,
139
+ })),
140
+ health: {
141
+ open_debt: openDebt,
142
+ open_findings: openFindings.length,
143
+ high_findings: openFindings.filter((f) => f.severity === "high").length,
144
+ sessions: sessions.length,
145
+ governance: governanceSummary,
146
+ },
147
+ last_session: last ? { date: last.date, author: last.author || null, summary: last.summary, next_steps: last.next_steps } : null,
148
+ ...(sinceSummary ? { since: sinceSummary } : {}),
149
+ });
150
+ }, { cacheVersion: `runtime:${VERSION}`,
151
+ outputSchema: {
152
+ runtime: z.object({
153
+ version: z.string(),
154
+ root: z.string(),
155
+ contract: z.string(),
156
+ root_source: z.string(),
157
+ root_candidates: z.array(z.string()).optional(),
158
+ }),
159
+ project: z.object({ name: z.string(), phase: z.string().nullable(), description: z.string().nullable() }),
160
+ milestones: z.array(z.object({ id: z.string(), title: z.string(), status: z.string(), total: z.number(), done: z.number(), pct: z.number(), refactorish: z.number(), refactorPct: z.number() })),
161
+ focus: z.array(z.object({ id: z.string(), status: z.string(), title: z.string(), type: z.string(), milestone: z.string().nullable(), next_step: z.string().nullable(), steps_done: z.number(), steps_total: z.number() })),
162
+ health: z.object({
163
+ open_debt: z.number(),
164
+ open_findings: z.number(),
165
+ high_findings: z.number(),
166
+ sessions: z.number(),
167
+ governance: z.object({ modules: z.number(), interfaces: z.number(), repositories: z.number(), initialized: z.boolean() }),
168
+ }),
169
+ last_session: z.object({ date: z.string(), author: z.string().nullable(), summary: z.string(), next_steps: z.array(z.string()) }).nullable(),
170
+ since: z.object({ since: z.string(), tasks: z.number(), sessions: z.number(), items: z.array(z.string()) }).optional(),
171
+ },
172
+ decode: (value) => {
173
+ const result = JSON.parse(value);
174
+ const { text, ...data } = result;
175
+ return { text, data };
176
+ } });
177
+ toolR(server, startupRoot, "get_usage_log", "使用日志:pm-mcp 被用了哪些工具、成败、耗时,以及输出 token 估算与「折叠省下的 token」——回答“用它到底省不省 token”。只记工具名与计量,不记参数内容。日志滚动保留约 2MB。", {
107
178
  last: z.number().int().min(1).max(200).optional().describe("返回最近 N 条明细(默认 20;汇总始终覆盖全部保留日志)"),
108
179
  tool: z.string().optional().describe("只看某工具,如 get_status"),
109
180
  ok: z.boolean().optional().describe("true 只看成,false 只看败"),
110
181
  since: z.string().optional().describe("ISO 日期/时间,只看该时间之后的明细"),
111
- }, (args) => {
182
+ }, (args, ctx) => {
183
+ const root = ctx.root;
112
184
  requireInitialized(root);
113
185
  return foldLines(renderUsageLogLines(root, args), { maxLines: budgetLines(root) });
114
186
  });
115
- toolR(server, root, "get_runtime_log", "运行日志:server 就绪、watcher 状态、工具报错等服务级事件(info/warn/error),判断 pm-mcp 运行是否健康、哪里在反复出错。", {
187
+ toolR(server, startupRoot, "get_runtime_log", "运行日志:server 就绪、watcher 状态、工具报错等服务级事件(info/warn/error),判断 pm-mcp 运行是否健康、哪里在反复出错。", {
116
188
  last: z.number().int().min(1).max(200).optional().describe("返回最近 N 条明细(默认 20)"),
117
189
  level: z.enum(["info", "warn", "error"]).optional().describe("只看某级别"),
118
190
  event: z.string().optional().describe("只看某事件,如 tool.error / server.ready"),
119
191
  since: z.string().optional().describe("ISO 日期/时间,只看该时间之后的明细"),
120
- }, (args) => {
192
+ }, (args, ctx) => {
193
+ const root = ctx.root;
121
194
  requireInitialized(root);
122
195
  return foldLines(renderRuntimeLogLines(root, args), { maxLines: budgetLines(root) });
123
196
  });
124
- toolW(server, root, "update_project", "更新项目元信息/预算(描述、技术栈、目标、阶段、模块登记、许可证、暴露面、复杂度与输出预算)。", {
197
+ toolW(server, startupRoot, "update_project", "更新项目元信息/预算(描述、技术栈、目标、阶段、模块登记、许可证、暴露面、复杂度与输出预算)。", {
125
198
  description: z.string().optional(),
126
199
  stack: z.array(z.string()).optional(),
127
200
  goals: z.array(z.string()).optional(),
@@ -135,7 +208,8 @@ export function registerProjectTools(server, root) {
135
208
  refactor_quota_pct: z.number().min(0).max(100).optional().describe("里程碑重构类任务最低占比%"),
136
209
  session_blast_radius: z.number().int().min(1).optional().describe("单会话波及文件阈值"),
137
210
  output_budget_lines: z.number().int().min(20).optional().describe("读类工具输出行数预算"),
138
- }, (args) => {
211
+ }, (args, ctx) => {
212
+ const root = ctx.root;
139
213
  requireInitialized(root);
140
214
  const project = loadProject(root);
141
215
  if (args.description !== undefined)
@@ -168,14 +242,16 @@ export function registerProjectTools(server, root) {
168
242
  refreshDerived(root);
169
243
  return `✅ 项目信息已更新(阶段: ${project.phase || "—"},模块: ${project.modules.join(", ") || "—"})。仪表盘已刷新。`;
170
244
  });
171
- toolI(server, root, "regenerate_dashboard", "手动重新生成 PROJECT.md 仪表盘与 changelog.md(正常情况下每次写操作已自动刷新)。", {}, () => {
245
+ toolI(server, startupRoot, "regenerate_dashboard", "手动重新生成 PROJECT.md 仪表盘与 changelog.md(正常情况下每次写操作已自动刷新)。", {}, (_args, ctx) => {
246
+ const root = ctx.root;
172
247
  requireInitialized(root);
173
248
  refreshDerived(root);
174
249
  return "✅ PROJECT.md 与 changelog.md 已重新生成。";
175
250
  });
176
251
  }
177
- export function registerRoadmapTools(server, root) {
178
- toolW(server, root, "add_milestone", "添加里程碑(M1/M2…,按顺序自动编号)。路线图的骨架;任务挂到里程碑上。", { title: z.string().min(1), goal: z.string().optional().describe("这个里程碑要达成什么") }, (args) => {
252
+ export function registerRoadmapTools(server, startupRoot) {
253
+ toolW(server, startupRoot, "add_milestone", "添加里程碑(M1/M2…,按顺序自动编号)。路线图的骨架;任务挂到里程碑上。", { title: z.string().min(1), goal: z.string().optional().describe("这个里程碑要达成什么") }, (args, ctx) => {
254
+ const root = ctx.root;
179
255
  requireInitialized(root);
180
256
  const data = loadRoadmap(root);
181
257
  const id = `M${data.seq + 1}`;
@@ -194,13 +270,14 @@ export function registerRoadmapTools(server, root) {
194
270
  refreshDerived(root);
195
271
  return `✅ 里程碑 ${id}「${args.title}」已创建(planned)。开始推进时用 update_milestone 置为 active。`;
196
272
  });
197
- toolW(server, root, "update_milestone", "更新里程碑(状态流转 planned/active/done/paused、标题、目标、顺序)。", {
273
+ toolW(server, startupRoot, "update_milestone", "更新里程碑(状态流转 planned/active/done/paused、标题、目标、顺序)。", {
198
274
  id: z.string().describe("里程碑 id,如 M1"),
199
275
  status: z.enum(["planned", "active", "done", "paused"]).optional(),
200
276
  title: z.string().optional(),
201
277
  goal: z.string().optional(),
202
278
  order: z.number().int().optional(),
203
- }, (args) => {
279
+ }, (args, ctx) => {
280
+ const root = ctx.root;
204
281
  requireInitialized(root);
205
282
  const data = loadRoadmap(root);
206
283
  const m = data.milestones.find((x) => x.id === args.id);
@@ -228,7 +305,8 @@ export function registerRoadmapTools(server, root) {
228
305
  const s = milestoneStats(tasks, m.id);
229
306
  return [`✅ ${m.id} 已更新(${m.status})。进度: ${s.done}/${s.total}(${s.pct}%)。`, ...warnings].join("\n");
230
307
  });
231
- toolR(server, root, "get_roadmap", "查看路线图。depth=1 每个里程碑一行摘要(功能再多也不乱);depth=2 额外展开活跃里程碑的任务明细与断点。", { depth: z.number().int().min(1).max(2).optional().describe("默认 1") }, (args) => {
308
+ toolR(server, startupRoot, "get_roadmap", "查看路线图。depth=1 每个里程碑一行摘要(功能再多也不乱);depth=2 额外展开活跃里程碑的任务明细与断点。", { depth: z.number().int().min(1).max(2).optional().describe("默认 1") }, (args, ctx) => {
309
+ const root = ctx.root;
232
310
  requireInitialized(root);
233
311
  const { milestones } = loadRoadmap(root);
234
312
  const { tasks } = loadTasks(root);
@@ -0,0 +1,91 @@
1
+ #!/usr/bin/env node
2
+ export function registerPrompts(server) {
3
+ server.registerPrompt("start-session", { description: "开工仪式:恢复项目上下文,从断点续做,不靠回忆。" }, () => ({
4
+ messages: [
5
+ {
6
+ role: "user",
7
+ content: {
8
+ type: "text",
9
+ text: [
10
+ "按以下顺序恢复项目上下文(本项目由 pm-mcp 管理,状态以 .pm/ 为准,不要凭记忆陈述):",
11
+ "1. 调用 get_status:了解阶段、里程碑进度、进行中任务及其 checkpoint 下一步。",
12
+ "2. 若要动的模块不清楚,先 search_code 定位到具体文件与行,再读文件——三段式定位,不要全量读代码。",
13
+ "3. 陈述本次计划:要做哪个任务、动哪些文件、验收标准是什么。",
14
+ "4. 过程纪律:走捷径就 add_task(type=debt) 登记债务;修 bug 完成后 log_debug 记根因;长任务随手 checkpoint。",
15
+ ].join("\n"),
16
+ },
17
+ },
18
+ ],
19
+ }));
20
+ server.registerPrompt("end-session", { description: "收工仪式:更新任务与里程碑、留断点、记会话——把本次上下文写回仓库。" }, () => ({
21
+ messages: [
22
+ {
23
+ role: "user",
24
+ content: {
25
+ type: "text",
26
+ text: [
27
+ "收工前依次完成(顺序执行,缺一不可):",
28
+ "1. feature/fix 先执行适用测试,生成最新质量报告;update_task 置 done,给 result_note 与实际 files,record_session=true 可一并归档。",
29
+ "2. 未完成的任务:checkpoint 保存「进展 + 下一步具体动作」。",
30
+ "3. 本次落地的功能:register_feature 登记(入口文件 + 测试文件)。",
31
+ "4. 修过的 bug:log_debug 记录症状/根因/修法/验证。",
32
+ "5. 走过的捷径:add_task(type=debt) 登记债务,别让它在沉默中腐烂。",
33
+ "6. update_task 已返回自动会话编号时,不重复 log_session;否则用 log_session 记录真实改动清单。纯问答不需要另建任务或反复写会话。",
34
+ "7. 若到达里程碑节点:update_milestone 流转状态;可 snapshot_codebase 拍快照 + audit_structure 对账。",
35
+ ].join("\n"),
36
+ },
37
+ },
38
+ ],
39
+ }));
40
+ server.registerPrompt("onboard", { description: "入职简报工作流:引导客户端为新成员(人或 AI)生成项目现状一页纸。" }, () => ({
41
+ messages: [
42
+ {
43
+ role: "user",
44
+ content: {
45
+ type: "text",
46
+ text: [
47
+ "为新加入的成员(人或 AI)生成入职简报,数据全部来自 .pm/ 状态,不要编造:",
48
+ "1. 读 pm://dashboard(或 PROJECT.md):项目是什么、路线图到哪、健康状态如何。",
49
+ "2. list_decisions + 阅读最近 3 条 ADR:理解关键取舍的「为什么」。",
50
+ "3. list_features:了解已有功能地图与各功能入口文件。",
51
+ "4. get_status:当前焦点与断点。",
52
+ "5. 读 pm://architecture 与 pm://portfolio:模块 owner、公开接口、跨语言质量矩阵和跨仓版本风险。",
53
+ "输出一页纸简报:项目一句话 / 技术栈与模块 / 路线图现状 / 关键决策与红线 / 模块与接口边界 / 当前焦点与下一步 / 哪里有坑(债务、安全、语义与跨仓风险)。",
54
+ ].join("\n"),
55
+ },
56
+ },
57
+ ],
58
+ }));
59
+ server.registerPrompt("architecture-review", { description: "跨文件/模块/语言架构评审:先取证,再判断影响和门禁。" }, () => ({
60
+ messages: [{
61
+ role: "user",
62
+ content: {
63
+ type: "text",
64
+ text: [
65
+ "按证据完成跨模块/跨语言架构评审:",
66
+ "1. 读取 pm://architecture 或调用 audit_governance,先检查语义覆盖、unresolved、owner、私有接口、循环和边界违规。",
67
+ "2. 对本次变更调用 impact_analysis,列出反向依赖文件与模块;未知文件不得忽略。",
68
+ "3. 调用 plan_quality_matrix;这一步只是计划,不能声称测试通过。",
69
+ "4. 只有用户明确要求执行时才调用 run_quality_matrix(confirm_execute=true),并分别报告每种语言的真实结果。",
70
+ "5. 跨仓变更读取 pm://portfolio,核对依赖目标、版本约束、加载失败与仓库循环。",
71
+ ].join("\n"),
72
+ },
73
+ }],
74
+ }));
75
+ server.registerPrompt("acceptance-review", { description: "标准化终验:冻结质量基线后执行证据核验,不能把开发者自测冒充认证。" }, () => ({
76
+ messages: [{
77
+ role: "user",
78
+ content: {
79
+ type: "text",
80
+ text: [
81
+ "按 pm-mcp 的标准化验收闭环执行,不得事后调低阈值:",
82
+ "1. list_acceptance_baselines 并读取目标基线,确认 ISO/IEC 25010 九特性适用性、量化指标、风险和测试双向追踪。",
83
+ "2. 只有产品负责人审阅 draft fingerprint 后,才能 approve_acceptance_baseline;批准后的同版本不可修改。",
84
+ "3. 执行基线定义的质量、治理、性能与安全测试;失败、跳过、缺工具和过期证据都不得写成通过。",
85
+ "4. evaluate_acceptance 会重算项目内证据 SHA-256,并由机器生成不可覆盖的 JSON/Markdown 报告。",
86
+ "5. 分开陈述:第一方范围内验收、独立评价、托管 CI、生产证据和 ISO 认证;任何一层都不能冒充另一层。",
87
+ ].join("\n"),
88
+ },
89
+ }],
90
+ }));
91
+ }
@@ -0,0 +1,131 @@
1
+ #!/usr/bin/env node
2
+ /* Qoder 客户端适配器。实测事实:Qoder 主进程只派生一个常驻 stdio 子进程,并用本地路由把
3
+ 所有窗口/会话复用到它上面(同一 server uuid、不同 session token),子进程 cwd 是安装目录。
4
+ 因此启动期解析出的工作区只能是「默认值」,绝不能当成 --root 钉死,否则别的窗口会读到错项目。
5
+ 本适配器的职责:把客户端标记与启动期提示如实交给服务器,由服务器在每次调用时反查真正的调用方工作区。 */
6
+ import { spawn } from "node:child_process";
7
+ import fs from "node:fs";
8
+ import os from "node:os";
9
+ import path from "node:path";
10
+ import { fileURLToPath } from "node:url";
11
+ import { CLIENT_ADAPTER_ENV, QODER_ADAPTER, ROOT_HINT_ENV, ROOT_HINT_SOURCE_ENV } from "./caller-workspace.js";
12
+ const PLACEHOLDER = /\$\{[^}]*\}/;
13
+ export function usableDir(value) {
14
+ const trimmed = value?.trim() ?? "";
15
+ if (!trimmed || PLACEHOLDER.test(trimmed))
16
+ return null;
17
+ const resolved = path.resolve(trimmed);
18
+ try {
19
+ return fs.statSync(resolved).isDirectory() ? resolved : null;
20
+ }
21
+ catch {
22
+ return null;
23
+ }
24
+ }
25
+ /** 显式给了 QODER_CONFIG_DIR 就只看它,避免真实 ~/.qoder-cn 混进隔离环境 */
26
+ function configDirs(env) {
27
+ const fromEnv = usableDir(env.QODER_CONFIG_DIR);
28
+ if (fromEnv)
29
+ return [fromEnv];
30
+ const home = os.homedir();
31
+ return [path.join(home, ".qoder-cn"), path.join(home, ".qoder")];
32
+ }
33
+ /** 运行日志目录名虽然以 `-p<pid>` 结尾,但 Qoder 所有会话共用主进程 pid,ppid 无法区分会话;
34
+ 启动期只能取「最近在写」的那份日志作为默认提示,真正的认领交给服务器按请求指纹完成。 */
35
+ export function workspaceFromRunLog(options = {}) {
36
+ const env = options.env ?? process.env;
37
+ const candidates = [];
38
+ for (const dir of configDirs(env)) {
39
+ const runs = path.join(dir, "logs", "runs");
40
+ let names;
41
+ try {
42
+ names = fs.readdirSync(runs);
43
+ }
44
+ catch {
45
+ continue;
46
+ }
47
+ for (const name of names) {
48
+ const file = path.join(runs, name, "qodercli.log");
49
+ try {
50
+ const stat = fs.statSync(file);
51
+ if (stat.isFile())
52
+ candidates.push({ file, mtime: stat.mtimeMs });
53
+ }
54
+ catch {
55
+ continue;
56
+ }
57
+ }
58
+ }
59
+ const limit = options.limit ?? 8;
60
+ for (const candidate of candidates.sort((a, b) => b.mtime - a.mtime).slice(0, limit)) {
61
+ let head = "";
62
+ try {
63
+ head = fs.readFileSync(candidate.file, "utf8").slice(0, 8192);
64
+ }
65
+ catch {
66
+ continue;
67
+ }
68
+ const match = /initial_cwd=([\s\S]*?)\s+restore_cwd=/.exec(head);
69
+ const root = usableDir(match?.[1]);
70
+ if (root)
71
+ return root;
72
+ }
73
+ return null;
74
+ }
75
+ export function resolveQoderWorkspace(env = process.env) {
76
+ const fromEnv = usableDir(env.QODER_WORKER_CWD);
77
+ if (fromEnv)
78
+ return { root: fromEnv, source: "QODER_WORKER_CWD" };
79
+ const fromLog = workspaceFromRunLog({ env });
80
+ if (fromLog)
81
+ return { root: fromLog, source: "qodercli run log" };
82
+ return null;
83
+ }
84
+ /** 调用方自己钉了 --root 时完全尊重它:不标记适配器,服务器就不会按请求改根 */
85
+ export function qoderLaunchEnv(argv = process.argv.slice(2), env = process.env) {
86
+ if (argv.includes("--root"))
87
+ return env;
88
+ const resolved = resolveQoderWorkspace(env);
89
+ return {
90
+ ...env,
91
+ [CLIENT_ADAPTER_ENV]: QODER_ADAPTER,
92
+ ...(resolved ? { [ROOT_HINT_ENV]: resolved.root, [ROOT_HINT_SOURCE_ENV]: resolved.source } : {}),
93
+ };
94
+ }
95
+ /** 参数原样透传:适配器不再伪造 --root,免得服务器把动态解析的依据报成「启动参数写死」 */
96
+ export function qoderLaunchArgs(argv = process.argv.slice(2)) {
97
+ return argv;
98
+ }
99
+ export function qoderServerEntry() {
100
+ const here = path.dirname(fileURLToPath(import.meta.url));
101
+ const candidates = ["index.js", "index.ts"].map((name) => path.resolve(here, name));
102
+ return candidates.find((file) => fs.existsSync(file)) ?? candidates[0];
103
+ }
104
+ export function runQoderLaunch(argv = process.argv.slice(2)) {
105
+ const env = qoderLaunchEnv(argv);
106
+ const pinned = argv.includes("--root");
107
+ if (pinned) {
108
+ console.error("[pm-mcp qoder-launch] 调用方已钉 --root,按固定项目根运行(不做按请求认领)");
109
+ }
110
+ else if (env[ROOT_HINT_ENV]) {
111
+ console.error(`[pm-mcp qoder-launch] 启动期默认根: ${env[ROOT_HINT_ENV]} (source: ${env[ROOT_HINT_SOURCE_ENV]});每次调用会按会话日志重新认领工作区`);
112
+ }
113
+ else {
114
+ console.error("[pm-mcp qoder-launch] 启动期未取到 Qoder 工作区,交给服务器按请求认领(get_status 会报告依据)");
115
+ }
116
+ const child = spawn(process.execPath, [qoderServerEntry(), ...qoderLaunchArgs(argv)], { stdio: "inherit", env });
117
+ child.on("exit", (code, signal) => {
118
+ if (signal)
119
+ process.kill(process.pid, signal);
120
+ else
121
+ process.exit(code ?? 1);
122
+ });
123
+ child.on("error", (error) => {
124
+ console.error(`[pm-mcp qoder-launch] 启动失败: ${error.message}`);
125
+ process.exit(1);
126
+ });
127
+ for (const name of ["SIGINT", "SIGTERM"])
128
+ process.on(name, () => child.kill(name));
129
+ }
130
+ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url))
131
+ runQoderLaunch();
@@ -0,0 +1,116 @@
1
+ #!/usr/bin/env node
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { dashboardFile, pmPath } from "./paths.js";
5
+ import { loadRoadmap, loadTasks } from "./store.js";
6
+ import { renderRoadmap } from "./roadmap.js";
7
+ import { buildChangelog } from "./dashboard.js";
8
+ import { auditGovernance } from "./governance-audit.js";
9
+ import { buildPortfolioReport, loadPortfolioProject } from "./portfolio.js";
10
+ import { listAcceptanceBaselines } from "./acceptance-tools.js";
11
+ import { AcceptanceReportSchema } from "./acceptance-report.js";
12
+ import { runCoalescedRead } from "./idempotency.js";
13
+ import { budgetLines, recordUsage, requestContext } from "./tool-base.js";
14
+ import { taskPage } from "./task-pagination.js";
15
+ export function registerResources(server, root) {
16
+ function safeText(fn) {
17
+ try {
18
+ return fn();
19
+ }
20
+ catch (e) {
21
+ return `(不可用: ${e.message})`;
22
+ }
23
+ }
24
+ async function resourceText(name, fn) {
25
+ const started = Date.now();
26
+ const target = requestContext(name, {}, root).root;
27
+ try {
28
+ const text = (await runCoalescedRead(target, `resource:${name}`, {}, () => safeText(() => fn(target)))).text;
29
+ recordUsage(target, `resource:${name}`, "read", started, { ok: true, text });
30
+ return text;
31
+ }
32
+ catch (error) {
33
+ recordUsage(target, `resource:${name}`, "read", started, { ok: false, error: error.message });
34
+ throw error;
35
+ }
36
+ }
37
+ server.registerResource("dashboard", "pm://dashboard", { description: "项目仪表盘(与仓库根 PROJECT.md 同源),路线图/健康摘要/任务/功能清单" }, async (uri) => ({
38
+ contents: [
39
+ {
40
+ uri: uri.href,
41
+ mimeType: "text/markdown",
42
+ text: await resourceText("dashboard", (r) => fs.readFileSync(dashboardFile(r), "utf8")),
43
+ },
44
+ ],
45
+ }));
46
+ server.registerResource("roadmap", "pm://roadmap", { description: "路线图(里程碑进度 + 活跃里程碑任务明细)" }, async (uri) => ({
47
+ contents: [
48
+ {
49
+ uri: uri.href,
50
+ mimeType: "text/markdown",
51
+ text: await resourceText("roadmap", (r) => renderRoadmap(loadRoadmap(r).milestones, loadTasks(r).tasks, 2).join("\n")),
52
+ },
53
+ ],
54
+ }));
55
+ server.registerResource("tasks", "pm://tasks", { description: "任务原始数据;大列表末尾返回 continuation,可通过 list_tasks 保持 include_done:true 连续取回后续任务" }, async (uri) => ({
56
+ contents: [
57
+ {
58
+ uri: uri.href,
59
+ mimeType: "application/json",
60
+ text: await resourceText("tasks", (r) => {
61
+ const data = loadTasks(r);
62
+ const tasks = data.tasks;
63
+ // 资源也要守 token 预算:全量 dump 大账本会吃爆上下文(截断后仍是合法 JSON)
64
+ if (tasks.length <= 100)
65
+ return JSON.stringify(tasks, null, 2);
66
+ const page = taskPage(r, { include_done: true, page_size: 100 }, budgetLines(r) - 4, data);
67
+ const shown = tasks.slice(0, page.returned);
68
+ shown.push({
69
+ id: `_TRUNCATED_${tasks.length - page.returned}`,
70
+ title: `…本次输出已截断,另有 ${tasks.length - page.returned} 个任务,用 continuation 连续读取`,
71
+ continuation: { tool: "list_tasks", arguments: { include_done: true, cursor: page.next_cursor } },
72
+ });
73
+ return JSON.stringify(shown, null, 2);
74
+ }),
75
+ },
76
+ ],
77
+ }));
78
+ server.registerResource("changelog", "pm://changelog", { description: "变更日志(由 sessions.json 生成的人类可读版本)" }, async (uri) => ({
79
+ contents: [
80
+ {
81
+ uri: uri.href,
82
+ mimeType: "text/markdown",
83
+ text: await resourceText("changelog", (r) => buildChangelog(r)),
84
+ },
85
+ ],
86
+ }));
87
+ server.registerResource("architecture", "pm://architecture", { description: "跨文件/模块/语言治理审计:语义覆盖、依赖边界、接口、循环与质量矩阵" }, async (uri) => ({
88
+ contents: [{ uri: uri.href, mimeType: "text/markdown", text: await resourceText("architecture", (r) => auditGovernance(r, 150).report) }],
89
+ }));
90
+ server.registerResource("portfolio", "pm://portfolio", { description: "当前项目的组合视图与跨仓依赖/版本风险" }, async (uri) => ({
91
+ contents: [{
92
+ uri: uri.href,
93
+ mimeType: "application/json",
94
+ text: await resourceText("portfolio", (r) => JSON.stringify(buildPortfolioReport({ projects: [loadPortfolioProject(r)], projectsRequested: 1 }), null, 2)),
95
+ }],
96
+ }));
97
+ server.registerResource("acceptance", "pm://acceptance", { description: "标准化产品验收概览:版本化预批准基线与最近一次机器判定报告" }, async (uri) => ({
98
+ contents: [{
99
+ uri: uri.href,
100
+ mimeType: "application/json",
101
+ text: await resourceText("acceptance", (r) => {
102
+ const reportDir = pmPath(r, "acceptance", "reports");
103
+ let latestReport = null;
104
+ if (fs.existsSync(reportDir)) {
105
+ const candidates = fs.readdirSync(reportDir, { withFileTypes: true })
106
+ .filter((entry) => entry.isFile() && entry.name.endsWith(".json"))
107
+ .map((entry) => ({ name: entry.name, mtime: fs.statSync(path.join(reportDir, entry.name)).mtimeMs }))
108
+ .sort((a, b) => b.mtime - a.mtime || a.name.localeCompare(b.name));
109
+ if (candidates[0])
110
+ latestReport = AcceptanceReportSchema.parse(JSON.parse(fs.readFileSync(path.join(reportDir, candidates[0].name), "utf8")));
111
+ }
112
+ return JSON.stringify({ baselines: listAcceptanceBaselines(r), latest_report: latestReport }, null, 2);
113
+ }),
114
+ }],
115
+ }));
116
+ }
package/dist/scan.js CHANGED
@@ -78,6 +78,10 @@ export function looksTrivialTest(content) {
78
78
  export function countSkipLines(content) {
79
79
  let n = 0;
80
80
  for (const line of content.split("\n")) {
81
+ // 排除两类误报:正则模式定义行(本扫描器源码自身)与夹具字符串模板行(含转义 \n 的内嵌代码)
82
+ const trimmed = line.trim();
83
+ if (trimmed.startsWith("/") || line.includes("\\n"))
84
+ continue;
81
85
  if (SKIP_MARKER_PATTERNS.some((re) => re.test(line)))
82
86
  n += 1;
83
87
  }
@@ -90,7 +90,7 @@ export function isLocalServerEntry(entry) {
90
90
  if (!/^node(?:\.exe)?$/i.test(path.basename(entry.command)) || entry.args.length < 1 || !path.isAbsolute(entry.args[0]))
91
91
  return false;
92
92
  const file = path.resolve(entry.args[0]);
93
- if (!/^(?:index|cli)\.(?:js|ts)$/.test(path.basename(file)))
93
+ if (!/^(?:index|cli|qoder-launch)\.(?:js|ts)$/.test(path.basename(file)))
94
94
  return false;
95
95
  try {
96
96
  const stat = fs.lstatSync(file);