add-coder 0.2.8 → 0.2.10

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.
Files changed (57) hide show
  1. package/README.md +22 -17
  2. package/dist/index.js +10 -0
  3. package/package.json +4 -11
  4. package/templates/adapters/claude/hooks/doc-format-guard.sh +17 -0
  5. package/templates/adapters/claude/hooks/lib/notify.sh +34 -0
  6. package/templates/adapters/claude/hooks/pre-tool-use.sh +26 -7
  7. package/templates/adapters/claude/hooks/prompt-submit.sh +16 -0
  8. package/templates/adapters/codex/hooks/doc-format-guard.sh +17 -0
  9. package/templates/adapters/codex/hooks/lib/notify.sh +34 -0
  10. package/templates/adapters/codex/hooks/pre-tool-use.sh +48 -18
  11. package/templates/adapters/codex/hooks/prompt-submit.sh +16 -0
  12. package/templates/adapters/qoder/hooks/doc-format-guard.sh +17 -0
  13. package/templates/adapters/qoder/hooks/lib/notify.sh +34 -0
  14. package/templates/adapters/qoder/hooks/pre-tool-use.sh +28 -9
  15. package/templates/adapters/qoder/hooks/prompt-submit.sh +18 -1
  16. package/templates/adapters/trae/hooks/doc-format-guard.sh +17 -0
  17. package/templates/adapters/trae/hooks/lib/notify.sh +34 -0
  18. package/templates/adapters/trae/hooks/pre-tool-use.sh +48 -18
  19. package/templates/adapters/trae/hooks/prompt-submit.sh +16 -0
  20. package/templates/adapters/vscode/hooks/doc-format-guard.sh +16 -0
  21. package/templates/adapters/vscode/hooks/lib/notify.sh +34 -0
  22. package/templates/adapters/vscode/hooks/pre-tool-use.sh +26 -7
  23. package/templates/adapters/vscode/hooks/prompt-submit.sh +16 -0
  24. package/templates/core/hooks/doc-format-guard.sh +17 -0
  25. package/templates/core/hooks/lib/notify.sh +34 -0
  26. package/templates/core/hooks/pre-tool-use.sh +48 -18
  27. package/templates/core/hooks/prompt-submit.sh +16 -0
  28. package/templates/core/reviews/.gitkeep +0 -0
  29. package/templates/core/scripts/mcp-server/elicitation/confirm.ts +30 -0
  30. package/templates/core/scripts/mcp-server/elicitation/index.ts +1 -0
  31. package/templates/core/scripts/mcp-server/index.ts +15 -0
  32. package/templates/core/scripts/mcp-server/notifications/hitl.ts +49 -0
  33. package/templates/core/scripts/mcp-server/notifications/hook.ts +268 -0
  34. package/templates/core/scripts/mcp-server/notifications/index.ts +8 -0
  35. package/templates/core/scripts/mcp-server/resources/add-coder-version.ts +25 -0
  36. package/templates/core/scripts/mcp-server/resources/add-state.ts +44 -0
  37. package/templates/core/scripts/mcp-server/resources/hook-events-report.ts +104 -0
  38. package/templates/core/scripts/mcp-server/resources/index.ts +12 -0
  39. package/templates/core/scripts/mcp-server/resources/round-task.ts +22 -0
  40. package/templates/core/scripts/mcp-server/sampling/index.ts +1 -0
  41. package/templates/core/scripts/mcp-server/sampling/review.ts +47 -0
  42. package/templates/core/scripts/mcp-server/shared/env.ts +26 -0
  43. package/templates/core/scripts/mcp-server/shared/fs.ts +40 -0
  44. package/templates/core/scripts/mcp-server/shared/prisma.ts +35 -0
  45. package/templates/core/scripts/mcp-server/shared/response.ts +9 -0
  46. package/templates/core/scripts/mcp-server/tasks/index.ts +2 -0
  47. package/templates/core/scripts/mcp-server/tasks/runner.ts +20 -0
  48. package/templates/core/scripts/mcp-server/tasks/store.ts +20 -0
  49. package/templates/core/scripts/mcp-server/tools/audit.ts +73 -0
  50. package/templates/core/scripts/mcp-server/tools/context.ts +337 -0
  51. package/templates/core/scripts/mcp-server/tools/docs.ts +42 -0
  52. package/templates/core/scripts/mcp-server/tools/gateway.ts +135 -0
  53. package/templates/core/scripts/mcp-server/tools/hook-event-report.ts +92 -0
  54. package/templates/core/scripts/mcp-server/tools/index.ts +17 -0
  55. package/templates/core/scripts/mcp-server/tools/quality.ts +93 -0
  56. package/templates/core/scripts/mcp-server/types.ts +7 -0
  57. package/templates/core/scripts/mcp-server.ts +8 -3455
@@ -0,0 +1,92 @@
1
+ import * as z from "zod/v4"
2
+ import type { McpServer } from "@modelcontextprotocol/server"
3
+ import { textResponse, errorResponse } from "../shared/response.js"
4
+ import { prisma } from "../shared/prisma.js"
5
+
6
+ export function registerHookEventTools(server: McpServer) {
7
+
8
+ server.registerTool("get_hook_events", {
9
+ description:
10
+ "查询 Hook 拦截事件(HOOK_INTERCEPT)。支持按 planKeyword/hook/时间区间过滤,按 planKeyword 分组聚合。" +
11
+ "\n\n典型用法:" +
12
+ "\n- get_hook_events({}) — 查最近 50 条拦截事件" +
13
+ "\n- get_hook_events({ sinceMinutes: 1440 }) — 过去 24h" +
14
+ "\n- get_hook_events({ planKeyword: \"no-active-plan\" }) — 无 Plan 违规" +
15
+ "\n- get_hook_events({ hook: \"pre-tool-use\" }) — 按 hook 类型过滤",
16
+ inputSchema: z.object({
17
+ planKeyword: z.string().optional().describe("按 Plan 关键词过滤"),
18
+ hook: z.string().optional().describe("按 hook 名称过滤: pre-tool-use / doc-format-guard / prompt-submit"),
19
+ sinceMinutes: z.number().optional().describe("时间窗口起始(分钟前),如 1440=过去24h"),
20
+ untilMinutes: z.number().optional().describe("时间窗口截止(分钟前),默认 0(现在)"),
21
+ limit: z.number().optional().default(50).describe("返回最大条数,默认 50,最大 200"),
22
+ }),
23
+ }, async (args: Record<string, string | number | undefined>, _ctx: unknown) => {
24
+ try {
25
+ const { planKeyword, hook, sinceMinutes, untilMinutes, limit = 50 } = args
26
+
27
+ const where: Record<string, unknown> = { action: "HOOK_INTERCEPT" }
28
+ if (planKeyword) where.planKeyword = planKeyword
29
+ if (hook) where.targetType = hook
30
+
31
+ // 时间范围过滤
32
+ if (sinceMinutes !== undefined || untilMinutes !== undefined) {
33
+ const now = Date.now()
34
+ const since = sinceMinutes !== undefined ? new Date(now - (sinceMinutes as number) * 60 * 1000) : new Date(0)
35
+ const until = untilMinutes !== undefined ? new Date(now - (untilMinutes as number) * 60 * 1000) : new Date()
36
+ where.createdAt = { gte: since, lte: until }
37
+ }
38
+
39
+ const ops = prisma.devOperation as Record<string, (...a: unknown[]) => unknown>
40
+ const logs = await ops.findMany({
41
+ where,
42
+ orderBy: { createdAt: "desc" },
43
+ take: Math.min(limit as number, 200),
44
+ }) as Array<Record<string, unknown>>
45
+
46
+ if (logs.length === 0) {
47
+ const f: string[] = []
48
+ if (planKeyword) f.push(`planKeyword=${planKeyword}`)
49
+ if (hook) f.push(`hook=${hook}`)
50
+ if (sinceMinutes !== undefined) f.push(`过去${sinceMinutes}分钟`)
51
+ return textResponse(
52
+ `=== Hook 拦截事件 ===\n\n未找到匹配的拦截记录(条件: ${f.length > 0 ? f.join(", ") : "无过滤条件"})。`
53
+ )
54
+ }
55
+
56
+ // 按 planKeyword 分组统计
57
+ const groupMap = new Map<string, number>()
58
+ for (const l of logs) {
59
+ const kw = (l.planKeyword as string) || "unknown"
60
+ groupMap.set(kw, (groupMap.get(kw) || 0) + 1)
61
+ }
62
+
63
+ const lines = ["=== Hook 拦截事件 ===", `共 ${logs.length} 条记录`, ""]
64
+ lines.push("## 按 Plan 分组", "")
65
+ for (const [kw, count] of [...groupMap.entries()].sort((a, b) => b[1] - a[1])) {
66
+ lines.push(` ${kw}: ${count} 次`)
67
+ }
68
+
69
+ lines.push("", "## 最近事件", "")
70
+ for (let i = 0; i < Math.min(logs.length, 20); i++) {
71
+ const l = logs[i]
72
+ lines.push(
73
+ ` [${(l.createdAt as Date).toISOString().slice(11, 19)}] ${l.targetType || "?"} → ${l.reason || "(无)"} | plan: ${l.planKeyword || "?"}`
74
+ )
75
+ }
76
+
77
+ // 治理信号:无 Plan 违规告警
78
+ const noPlanCount = groupMap.get("no-active-plan") || 0
79
+ if (noPlanCount >= 10) {
80
+ lines.push("", `⚠️ 阈值告警: 过去 ${sinceMinutes || "?"} 分钟内无 Plan 违规 ${noPlanCount} 次(≥10),建议创建 Plan 或检查 hooks 误报`)
81
+ }
82
+
83
+ lines.push("", "=== 稀疏推理建议 ===", "基于以上 hook 事件,可分析近期治理合规情况。")
84
+ return textResponse(lines.join("\n"))
85
+ } catch (error) {
86
+ return errorResponse(
87
+ `查询 hook 事件失败: ${error instanceof Error ? error.message : String(error)}\n可能原因: 数据库未运行。`
88
+ )
89
+ }
90
+ })
91
+
92
+ }
@@ -0,0 +1,17 @@
1
+ import type { McpServer } from "@modelcontextprotocol/server"
2
+ import { registerContextTools } from "./context.js"
3
+ import { registerAuditTools } from "./audit.js"
4
+ import { registerDocsTools } from "./docs.js"
5
+ import { registerQualityTools } from "./quality.js"
6
+ import { registerGatewayTools } from "./gateway.js"
7
+ import { registerHookEventTools } from "./hook-event-report.js"
8
+
9
+ export function registerAllTools(server: McpServer) {
10
+ registerContextTools(server) // 5 tools
11
+ registerAuditTools(server) // 2 tools
12
+ registerDocsTools(server) // 1 tool
13
+ registerQualityTools(server) // 4 tools
14
+ registerGatewayTools(server) // 5 tools
15
+ registerHookEventTools(server) // 1 tool: get_hook_events
16
+ // Total: 18 tools
17
+ }
@@ -0,0 +1,93 @@
1
+ import * as z from "zod/v4"
2
+ import type { McpServer } from "@modelcontextprotocol/server"
3
+ import { textResponse, errorResponse } from "../shared/response.js"
4
+
5
+ export function registerQualityTools(server: McpServer) {
6
+
7
+ // ===== check_phase_symmetry (L615-679) =====
8
+ server.registerTool("check_phase_symmetry", {
9
+ description: "检查代码中的阶段标记对称性(ADD-2 阶段标记对称)。统计 auditPhaseStart/End 配对情况。",
10
+ inputSchema: z.object({ code: z.string().describe("要检查的 TypeScript 代码文本") }),
11
+ }, (args, _ctx) => {
12
+ try {
13
+ const code = args.code; if(!code) return errorResponse("code 参数不能为空")
14
+ const sr=/auditPhaseStart\(["']([^"']+)["']/g, er=/auditPhaseEnd\(["']([^"']+)["']/g
15
+ const ss:string[]=[], es:string[]=[]; let m
16
+ while((m=sr.exec(code))!==null) ss.push(m[1]); while((m=er.exec(code))!==null) es.push(m[1])
17
+ const sc:Record<string,number>={}, ec:Record<string,number>={}
18
+ for(const s of ss) sc[s]=(sc[s]||0)+1; for(const e of es) ec[e]=(ec[e]||0)+1
19
+ const ap=new Set([...Object.keys(sc),...Object.keys(ec)]); const asym:string[]=[]
20
+ ap.forEach(p=>{ const a=sc[p]||0,b=ec[p]||0; if(a!==b) asym.push(` ⚠️ ${p}: Start=${a}, End=${b} (${a>b?"缺少 End":"缺少 Start"})`) })
21
+ const l=[`=== ADD-2 阶段标记对称性检查 ===`,`Start 总数: ${ss.length}`,`End 总数: ${es.length}`,""]
22
+ if(!asym.length) l.push("✅ 阶段标记完全对称"); else l.push(`❌ 发现 ${asym.length} 个不对称阶段:`,...asym)
23
+ l.push("","=== 所有阶段明细 ==="); ap.forEach(p=>l.push(` ${p}: Start=${sc[p]||0}, End=${ec[p]||0}`))
24
+ return textResponse(l.join("\n"))
25
+ } catch(e) { return errorResponse(`检查阶段对称性失败: ${e instanceof Error?e.message:String(e)}`) }
26
+ })
27
+
28
+ // ===== check_failure_path (L681-757) =====
29
+ server.registerTool("check_failure_path", {
30
+ description: "检查失败路径审计信息密度(ADD-6)。对比 try 块与 catch 块的 extra 字段数。",
31
+ inputSchema: z.object({ code: z.string().describe("要检查的 TypeScript 代码文本") }),
32
+ }, (args, _ctx) => {
33
+ try {
34
+ const code = args.code; if(!code) return errorResponse("code 参数不能为空")
35
+ const sec = args.code.split(/catch\s*\(/); if(sec.length<=1) return textResponse("=== ADD-6 失败路径审计检查 ===\n\n未检测到 try/catch 块。")
36
+ const l=[`=== ADD-6 失败路径审计信息密度检查 ===`,`检测到 ${sec.length-1} 个 catch 块`,""]; let ap=true
37
+ for(let i=1;i<sec.length;i++){ const cb=sec[i],ce=cb.indexOf("{"); if(ce===-1) continue
38
+ const tb=sec[i-1]; const tem=tb.match(/extra[:\s]*\{[^}]*\}/g); const tfc=tem?tem.reduce((s,m)=>s+(m.match(/\w+:/g)?.length||0),0):0
39
+ const ci=cb.indexOf("}"),cbd=cb.slice(ce,ci+1); const cem=cbd.match(/extra[:\s]*\{[^}]*\}/g); const cfc=cem?cem.reduce((s,m)=>s+(m.match(/\w+:/g)?.length||0),0):0
40
+ const ch=cbd.includes("throw"), ca=cbd.includes("audit")||cbd.includes("Audit"); const cid=cfc+(ch?2:0)+(ca?2:0)
41
+ l.push(`--- Catch 块 #${i} ---`); if(cid>=tfc&&ca) l.push(" ✅ 失败路径审计信息密度充足"); else { ap=false; if(!ca) l.push(" ❌ catch 块缺少审计调用"); if(cid<tfc) l.push(` ❌ 信息密度不足: catch=${cfc}, try=${tfc}`) }
42
+ l.push(` try extra 字段数: ${tfc}`,` catch extra 字段数: ${cfc}`,` 有审计调用: ${ca?"是":"否"}`,` 有 throw: ${ch?"是":"否"}`,"")
43
+ }
44
+ l.push(ap?"✅ 所有 catch 块满足 ADD-6":"⚠️ 部分 catch 块需补充审计信息")
45
+ return textResponse(l.join("\n"))
46
+ } catch(e) { return errorResponse(`检查失败路径失败: ${e instanceof Error?e.message:String(e)}`) }
47
+ })
48
+
49
+ // ===== generate_audit_logger (L759-990) =====
50
+ server.registerTool("generate_audit_logger", {
51
+ description: "生成符合三层分离模式的新审计日志器完整代码。遵循 ADD-1~6 原则。",
52
+ inputSchema: z.object({
53
+ domain: z.string().describe("审计日志器域名(小写中划线)"),
54
+ phases: z.string().describe("业务阶段枚举列表(逗号分隔,大写蛇形)"),
55
+ prefix: z.string().describe("审计前缀标识"),
56
+ }),
57
+ }, (args, _ctx) => {
58
+ try {
59
+ const {domain,phases,prefix} = args; if(!domain||!phases||!prefix) return errorResponse("domain, phases, prefix 参数均不能为空")
60
+ const pl=phases.split(",").map(p=>p.trim()).filter(Boolean); if(!pl.length) return errorResponse("phases 必须包含至少一个阶段")
61
+ const fn=domain.split("-").map(s=>s.charAt(0).toUpperCase()+s.slice(1)).join(""), ld=`logs/${domain}/`, lf=`${domain}.log`
62
+ const te=pl.map(p=>` | "${p}"`).join("\n"), fp=fn.charAt(0).toLowerCase()+fn.slice(1)
63
+ const dlg=`=== 三层分离式审计日志器: ${domain} ===\n域名: ${domain}\n前缀: [${prefix}]\n日志目录: ${ld}\n阶段数: ${pl.length}\n阶段列表: ${pl.join(", ")}\n\n=== 文件1: src/lib/${domain}-dev-logger.ts (Layer 1) ===\n--- 模板代码 ---\nimport * as fs from "fs/promises"\nimport * as path from "path"\nconst PREFIX = "[${prefix}]"\nconst LOG_DIR = path.join(process.cwd(), "${ld}")\nconst LOG_FILE = "${lf}"\nconst IS_DEV = process.env.NODE_ENV === "development"\ntype ${fn}AuditPhase =\n${te}\nexport function ${fp}Audit(phase: ${fn}AuditPhase, detail: string, extra?: Record<string, unknown>) { if (!IS_DEV) return; const msg = \`\${PREFIX} [\${new Date().toISOString()}] [\${phase}] \${detail}\${extra ? " | " + JSON.stringify(extra) : ""}\`; console.log(msg) }\nexport function ${fp}AuditPhaseStart(phase: ${fn}AuditPhase, description: string) { if (!IS_DEV) return; console.log(\`\${PREFIX} ═══ [\${phase}] 开始: \${description} ═══\`) }\nexport function ${fp}AuditPhaseEnd(phase: ${fn}AuditPhase, detail: string) { if (!IS_DEV) return; console.log(\`\${PREFIX} ═══ [\${phase}] 结束: \${detail} ═══\`) }\n\n=== 文件2: src/lib/${domain}-audit.ts (Layer 2) ===\nimport { prisma } from "@/lib/prisma"\nconst PREFIX2 = "[${prefix}:RUNTIME]"\ntype ${fn}AuditAction =\n${pl.map(p=>` | "${p}"`).join("\n")}\nexport type ${fn}AuditRecord = { action: ${fn}AuditAction; entityId: string; detail: Record<string, unknown>; traceId?: string }\nexport async function record${fn}Audit(record: ${fn}AuditRecord): Promise<void> { console.log(\`\${PREFIX2} [\${new Date().toISOString()}] [\${record.action}] entity=\${record.entityId}\${record.traceId ? " trace="+record.traceId : ""}\${Object.keys(record.detail).length ? " | "+JSON.stringify(record.detail) : ""}\`); try { await prisma.auditLog.create({ data: { userId: "system", action: record.action, targetType: "${fn}", targetId: record.entityId, traceId: record.traceId||null, afterState: record.detail as Record<string,unknown>, reason: \`\${record.action} on \${record.entityId}\` } }) } catch(e) { console.error(\`\${PREFIX2} Failed to write AuditLog: \${e}\`) } }\n\n=== 使用方式 ===\n开发审计: import { ${fp}AuditPhaseStart, ${fp}AuditPhaseEnd, ${fp}Audit } from "@/lib/${domain}-dev-logger"\n运行时审计: import { record${fn}Audit } from "@/lib/${domain}-audit"`
64
+ return textResponse(dlg)
65
+ } catch(e) { return errorResponse(`生成审计日志器失败: ${e instanceof Error?e.message:String(e)}`) }
66
+ })
67
+
68
+ // ===== check_add_compliance (L1875-2101) =====
69
+ server.registerTool("check_add_compliance", {
70
+ description: "综合 ADD 合规检查(ADD-1~6 一键验证)。检查阶段标记对称性、失败路径审计密度、循环体审计、审计日志器导入完整性。",
71
+ inputSchema: z.object({
72
+ code: z.string().describe("要检查的 TypeScript 代码文本"),
73
+ projectPattern: z.string().optional().default("event-based").describe("项目审计模式: 'event-based' 使用 auditPhaseStart/End, 'mixed' 同时检查 event 和 agentAudit"),
74
+ }),
75
+ }, (args, _ctx) => {
76
+ try {
77
+ const code=args.code; if(!code) return errorResponse("code 参数不能为空")
78
+ const l=["=== ADD 合规检查(ADD-1~6)===",""]
79
+ const sr=/auditPhaseStart\(["']([^"']+)["']/g, er=/auditPhaseEnd\(["']([^"']+)["']/g; const ss:string[]=[], es:string[]=[]; let m; while((m=sr.exec(code))!==null) ss.push(m[1]); while((m=er.exec(code))!==null) es.push(m[1])
80
+ const sc:Record<string,number>={}, ec:Record<string,number>={}; for(const s of ss) sc[s]=(sc[s]||0)+1; for(const e of es) ec[e]=(ec[e]||0)+1
81
+ const ap=new Set([...Object.keys(sc),...Object.keys(ec)]); let symOk=true; ap.forEach(p=>{if((sc[p]||0)!==(ec[p]||0)) symOk=false})
82
+ l.push("[ADD-2] 阶段标记对称性:"," Start: "+ss.length+", End: "+es.length,symOk?" ✅ 对称":" ❌ 不对称: "+[...ap].filter(p=>(sc[p]||0)!==(ec[p]||0)).join(", "),"")
83
+ const hasAgentAudit=code.includes("agentAudit(")
84
+ l.push("[ADD-1] 审计导入: "+ (hasAgentAudit||ss.length>0?"✅ 已导入审计调用":"⚠️ 未检测到审计调用"),"")
85
+ l.push("[ADD-3] 循环体审计: "+ (code.includes("for")||code.includes("while")||code.includes(".forEach")||code.includes(".map")? "⚠️ 循环体需逐项审计": "✅ 未检测到循环"),"")
86
+ const hasConsole=code.includes("console.log"); const hasFile=code.includes("writeToFile")||code.includes("appendFile"); const hasPrisma=code.includes("prisma.")||code.includes("AuditLog")
87
+ l.push("[ADD-4] 三通道输出: ",` console: ${hasConsole?"✅":"⚠️"}`,` file: ${hasFile?"✅":"⚠️"}`,` DB: ${hasPrisma?"✅":"⚠️"}`,"")
88
+ l.push("[ADD-5/6] 失败路径: "+(code.includes("catch")?"⚠️ catch 块需等价审计":"✅ 无异常处理"))
89
+ return textResponse(l.join("\n"))
90
+ } catch(e) { return errorResponse(`合规检查失败: ${e instanceof Error?e.message:String(e)}`) }
91
+ })
92
+
93
+ }
@@ -0,0 +1,7 @@
1
+ import type { McpServer } from "@modelcontextprotocol/server"
2
+
3
+ export type ToolResponse = Array<{ type: "text"; text: string }>
4
+
5
+ export interface GuardResult { ok: boolean; issues: string }
6
+
7
+ export type ToolRegistrar = (server: McpServer) => void