From 4b42cb2eb7b3d8d70d6edd0197e5d95fedf5fb82 Mon Sep 17 00:00:00 2001 From: sunlei Date: Wed, 27 May 2026 18:04:36 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=88=9D=E5=A7=8B=E5=8C=96KT=E5=B7=A5?= =?UTF-8?q?=E4=BD=9C=E6=B5=81MCP?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 4 + README.md | 87 +++ config/codex.example.json | 11 + package.json | 23 + pnpm-lock.yaml | 783 ++++++++++++++++++++++++++ src/server.mjs | 1105 +++++++++++++++++++++++++++++++++++++ 6 files changed, 2013 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 config/codex.example.json create mode 100644 package.json create mode 100644 pnpm-lock.yaml create mode 100644 src/server.mjs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..66b2bbd --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +dist/ +.turbo/ +*.log diff --git a/README.md b/README.md new file mode 100644 index 0000000..bf7491d --- /dev/null +++ b/README.md @@ -0,0 +1,87 @@ +# KT Workflow MCP + +`ktWorkflow` 是 KT 工作区的可复用 MCP 能力包,用来把根目录 `AGENTS.md`、`SKILLS.md`、`TASKS.md` 中的协作流程变成可调用工具。 + +## 能力边界 + +- 读取 KT 工作区上下文:项目清单、硬性规则、协作技能、最近任务记录。 +- 检查子项目环境:Git/SVN、包管理器、Node 版本、env 文件、Git 状态。 +- 生成防偏差工作包:把开工前检查、禁止项、风险扫描、验证计划汇总成一份可执行清单。 +- 生成验证建议:按后端、前端、样式、页面、部署、MCP 等变更类型给出轻量验证命令。 +- 生成页面测试用例:内置“先写用例、可视化证据、事不过三”的测试闭环。 +- 生成接口测试计划:接口改动后输出真实调用命令和统一返回结构断言。 +- 生成验证进程清理计划:按项目路径和端口给出 PowerShell 检查命令,不直接杀进程。 +- 清理历史产物:按目录最近修改时间只保留最近 3 轮测试/验证产物,模板目录永久保留。 +- 检查 env 策略和变更风险:提醒真实配置、锁文件、核心表格组件、部署链路等高风险改动。 +- 生成或写入 `TASKS.md` 改动记录:默认 `dryRun=true`,确认后再落盘。 +- 生成提交前检查清单:校验 KT commit message 约定。 + +## 安装 + +```bash +cd D:/MyFiles/KT/mcp/ktWorkflow +pnpm install +pnpm run self-test +pnpm run cleanup-history -- --dry-run +``` + +## MCP 客户端配置 + +把下面配置加入支持 stdio MCP 的客户端配置里: + +```json +{ + "mcpServers": { + "ktWorkflow": { + "command": "node", + "args": ["D:/MyFiles/KT/mcp/ktWorkflow/src/server.mjs"], + "env": { + "KT_WORKSPACE_ROOT": "D:/MyFiles/KT" + } + } + } +} +``` + +## 工具列表 + +| 工具 | 用途 | +| --- | --- | +| `kt_read_context` | 读取 KT 根目录上下文和最近任务记录 | +| `kt_inspect_project` | 检查子项目仓库、包管理器、Node、env 和 Git 状态 | +| `kt_inspect_all_projects` | 一次性检查所有 KT 项目,适合多仓库联动任务 | +| `kt_guardrails` | 按任务类型生成开工、改动、禁止项和验证约束 | +| `kt_prepare_task` | 生成完整 work packet,降低开工偏差 | +| `kt_suggest_verification` | 生成轻量验证命令和注意事项 | +| `kt_create_page_test_case` | 生成页面级可视化测试用例 | +| `kt_api_test_plan` | 生成接口真实调用测试计划 | +| `kt_cleanup_history` | 清理历史测试/验证产物,默认预览,执行时只保留最近 3 轮 | +| `kt_cleanup_process_plan` | 生成验证进程清理检查命令 | +| `kt_env_policy` | 检查 env 文件现状和提交策略 | +| `kt_risk_scan` | 扫描当前或传入变更文件的偏差风险 | +| `kt_append_task_record` | 预览或写入 `TASKS.md` 改动记录 | +| `kt_commit_checklist` | 生成提交前检查清单并校验 commit message | + +## 项目别名 + +| 别名 | 路径 | +| --- | --- | +| `root` | `D:/MyFiles/KT` | +| `mcp` | `mcp/ktWorkflow` | +| `api` | `Node/kt-template-online-api` | +| `admin` | `Vue/kt-template-admin` | +| `web` | `Vue/kt-template-online-web` | +| `playground` | `Vue/kt-template-online-playground` | + +## 使用建议 + +- 写代码前先调用 `kt_prepare_task`;只需要单项信息时再调用 `kt_read_context` 或 `kt_inspect_project`。 +- 多项目联动时先调用 `kt_inspect_all_projects`。 +- 不确定任务边界时调用 `kt_guardrails`,先拿到“能做什么、不能做什么、怎么验证”。 +- 验证前调用 `kt_suggest_verification`,避免盲跑全量构建。 +- 页面测试前调用 `kt_create_page_test_case`,再执行 Playwright/浏览器测试。 +- 接口改动后调用 `kt_api_test_plan`,并真实请求一次接口。 +- 验证启动过本地服务后调用 `kt_cleanup_process_plan`,清掉本次进程。 +- 每轮测试/验证结束后调用 `kt_cleanup_history`,或运行 `pnpm run cleanup-history -- --keep=3`,让历史产物只保留最近 3 轮。 +- 改完文件后用 `kt_append_task_record` 先 `dryRun` 预览记录,再决定是否写入。 +- 提交前调用 `kt_commit_checklist`,确认文件范围和提交信息。 diff --git a/config/codex.example.json b/config/codex.example.json new file mode 100644 index 0000000..24373b2 --- /dev/null +++ b/config/codex.example.json @@ -0,0 +1,11 @@ +{ + "mcpServers": { + "ktWorkflow": { + "command": "node", + "args": ["D:/MyFiles/KT/mcp/ktWorkflow/src/server.mjs"], + "env": { + "KT_WORKSPACE_ROOT": "D:/MyFiles/KT" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..d127c78 --- /dev/null +++ b/package.json @@ -0,0 +1,23 @@ +{ + "name": "@kt/mcp-kt-workflow", + "version": "0.2.0", + "private": true, + "type": "module", + "description": "Reusable MCP workflow tools for the KT workspace.", + "bin": { + "kt-workflow-mcp": "src/server.mjs" + }, + "scripts": { + "cleanup-history": "node src/server.mjs --cleanup-history", + "self-test": "node src/server.mjs --self-test", + "start": "node src/server.mjs" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^1.20.0", + "zod": "^3.25.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "packageManager": "pnpm@10.28.2" +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..d8b992b --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,783 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@modelcontextprotocol/sdk': + specifier: ^1.20.0 + version: 1.29.0(zod@3.25.76) + zod: + specifier: ^3.25.0 + version: 3.25.76 + +packages: + + '@hono/node-server@1.19.14': + resolution: {integrity: sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==} + engines: {node: '>=18.14.1'} + peerDependencies: + hono: ^4 + + '@modelcontextprotocol/sdk@1.29.0': + resolution: {integrity: sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==} + engines: {node: '>=18'} + peerDependencies: + '@cfworker/json-schema': ^4.1.1 + zod: ^3.25 || ^4.0 + peerDependenciesMeta: + '@cfworker/json-schema': + optional: true + + accepts@2.0.0: + resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} + engines: {node: '>= 0.6'} + + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + + ajv@8.20.0: + resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} + + body-parser@2.2.2: + resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} + engines: {node: '>=18'} + + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + content-disposition@1.1.0: + resolution: {integrity: sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==} + engines: {node: '>=18'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + + content-type@2.0.0: + resolution: {integrity: sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==} + engines: {node: '>=18'} + + cookie-signature@1.2.2: + resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} + engines: {node: '>=6.6.0'} + + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + + cors@2.8.6: + resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} + engines: {node: '>= 0.10'} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + + eventsource-parser@3.0.8: + resolution: {integrity: sha512-70QWGkr4snxr0OXLRWsFLeRBIRPuQOvt4s8QYjmUlmlkyTZkRqS7EDVRZtzU3TiyDbXSzaOeF0XUKy8PchzukQ==} + engines: {node: '>=18.0.0'} + + eventsource@3.0.7: + resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==} + engines: {node: '>=18.0.0'} + + express-rate-limit@8.5.2: + resolution: {integrity: sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==} + engines: {node: '>= 16'} + peerDependencies: + express: '>= 4.11' + + express@5.2.1: + resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} + engines: {node: '>= 18'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-uri@3.1.2: + resolution: {integrity: sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==} + + finalhandler@2.1.1: + resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} + engines: {node: '>= 18.0.0'} + + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + + fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + hasown@2.0.3: + resolution: {integrity: sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==} + engines: {node: '>= 0.4'} + + hono@4.12.21: + resolution: {integrity: sha512-uV63apnb0kyPtAUwoWgaGh9HyIFcv8lgmzPZSiTBQAFOFGIzka5EZ1dZocmGnn0XdX0+XTqJ6Tqv7selMuGLRQ==} + engines: {node: '>=16.9.0'} + + http-errors@2.0.1: + resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} + engines: {node: '>= 0.8'} + + iconv-lite@0.7.2: + resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} + engines: {node: '>=0.10.0'} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ip-address@10.2.0: + resolution: {integrity: sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==} + engines: {node: '>= 12'} + + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + + is-promise@4.0.0: + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + jose@6.2.3: + resolution: {integrity: sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==} + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json-schema-typed@8.0.2: + resolution: {integrity: sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + media-typer@1.1.0: + resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} + engines: {node: '>= 0.8'} + + merge-descriptors@2.0.0: + resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} + engines: {node: '>=18'} + + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + + mime-types@3.0.2: + resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} + engines: {node: '>=18'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + negotiator@1.0.0: + resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} + engines: {node: '>= 0.6'} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-to-regexp@8.4.2: + resolution: {integrity: sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==} + + pkce-challenge@5.0.1: + resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==} + engines: {node: '>=16.20.0'} + + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + + qs@6.15.2: + resolution: {integrity: sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==} + engines: {node: '>=0.6'} + + range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + + raw-body@3.0.2: + resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} + engines: {node: '>= 0.10'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + router@2.2.0: + resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} + engines: {node: '>= 18'} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + send@1.2.1: + resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} + engines: {node: '>= 18'} + + serve-static@2.2.1: + resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} + engines: {node: '>= 18'} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + side-channel-list@1.0.1: + resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + + type-is@2.1.0: + resolution: {integrity: sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==} + engines: {node: '>= 18'} + + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + zod-to-json-schema@3.25.2: + resolution: {integrity: sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==} + peerDependencies: + zod: ^3.25.28 || ^4 + + zod@3.25.76: + resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + +snapshots: + + '@hono/node-server@1.19.14(hono@4.12.21)': + dependencies: + hono: 4.12.21 + + '@modelcontextprotocol/sdk@1.29.0(zod@3.25.76)': + dependencies: + '@hono/node-server': 1.19.14(hono@4.12.21) + ajv: 8.20.0 + ajv-formats: 3.0.1(ajv@8.20.0) + content-type: 1.0.5 + cors: 2.8.6 + cross-spawn: 7.0.6 + eventsource: 3.0.7 + eventsource-parser: 3.0.8 + express: 5.2.1 + express-rate-limit: 8.5.2(express@5.2.1) + hono: 4.12.21 + jose: 6.2.3 + json-schema-typed: 8.0.2 + pkce-challenge: 5.0.1 + raw-body: 3.0.2 + zod: 3.25.76 + zod-to-json-schema: 3.25.2(zod@3.25.76) + transitivePeerDependencies: + - supports-color + + accepts@2.0.0: + dependencies: + mime-types: 3.0.2 + negotiator: 1.0.0 + + ajv-formats@3.0.1(ajv@8.20.0): + optionalDependencies: + ajv: 8.20.0 + + ajv@8.20.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.2 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + body-parser@2.2.2: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 4.4.3 + http-errors: 2.0.1 + iconv-lite: 0.7.2 + on-finished: 2.4.1 + qs: 6.15.2 + raw-body: 3.0.2 + type-is: 2.1.0 + transitivePeerDependencies: + - supports-color + + bytes@3.1.2: {} + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + content-disposition@1.1.0: {} + + content-type@1.0.5: {} + + content-type@2.0.0: {} + + cookie-signature@1.2.2: {} + + cookie@0.7.2: {} + + cors@2.8.6: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + depd@2.0.0: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + ee-first@1.1.1: {} + + encodeurl@2.0.0: {} + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + escape-html@1.0.3: {} + + etag@1.8.1: {} + + eventsource-parser@3.0.8: {} + + eventsource@3.0.7: + dependencies: + eventsource-parser: 3.0.8 + + express-rate-limit@8.5.2(express@5.2.1): + dependencies: + express: 5.2.1 + ip-address: 10.2.0 + + express@5.2.1: + dependencies: + accepts: 2.0.0 + body-parser: 2.2.2 + content-disposition: 1.1.0 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.2.2 + debug: 4.4.3 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 2.1.1 + fresh: 2.0.0 + http-errors: 2.0.1 + merge-descriptors: 2.0.0 + mime-types: 3.0.2 + on-finished: 2.4.1 + once: 1.4.0 + parseurl: 1.3.3 + proxy-addr: 2.0.7 + qs: 6.15.2 + range-parser: 1.2.1 + router: 2.2.0 + send: 1.2.1 + serve-static: 2.2.1 + statuses: 2.0.2 + type-is: 2.1.0 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + fast-deep-equal@3.1.3: {} + + fast-uri@3.1.2: {} + + finalhandler@2.1.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + forwarded@0.2.0: {} + + fresh@2.0.0: {} + + function-bind@1.1.2: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.3 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + gopd@1.2.0: {} + + has-symbols@1.1.0: {} + + hasown@2.0.3: + dependencies: + function-bind: 1.1.2 + + hono@4.12.21: {} + + http-errors@2.0.1: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.2 + toidentifier: 1.0.1 + + iconv-lite@0.7.2: + dependencies: + safer-buffer: 2.1.2 + + inherits@2.0.4: {} + + ip-address@10.2.0: {} + + ipaddr.js@1.9.1: {} + + is-promise@4.0.0: {} + + isexe@2.0.0: {} + + jose@6.2.3: {} + + json-schema-traverse@1.0.0: {} + + json-schema-typed@8.0.2: {} + + math-intrinsics@1.1.0: {} + + media-typer@1.1.0: {} + + merge-descriptors@2.0.0: {} + + mime-db@1.54.0: {} + + mime-types@3.0.2: + dependencies: + mime-db: 1.54.0 + + ms@2.1.3: {} + + negotiator@1.0.0: {} + + object-assign@4.1.1: {} + + object-inspect@1.13.4: {} + + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + parseurl@1.3.3: {} + + path-key@3.1.1: {} + + path-to-regexp@8.4.2: {} + + pkce-challenge@5.0.1: {} + + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + + qs@6.15.2: + dependencies: + side-channel: 1.1.0 + + range-parser@1.2.1: {} + + raw-body@3.0.2: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.1 + iconv-lite: 0.7.2 + unpipe: 1.0.0 + + require-from-string@2.0.2: {} + + router@2.2.0: + dependencies: + debug: 4.4.3 + depd: 2.0.0 + is-promise: 4.0.0 + parseurl: 1.3.3 + path-to-regexp: 8.4.2 + transitivePeerDependencies: + - supports-color + + safer-buffer@2.1.2: {} + + send@1.2.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 2.0.0 + http-errors: 2.0.1 + mime-types: 3.0.2 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + serve-static@2.2.1: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 1.2.1 + transitivePeerDependencies: + - supports-color + + setprototypeof@1.2.0: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + side-channel-list@1.0.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.1 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + statuses@2.0.2: {} + + toidentifier@1.0.1: {} + + type-is@2.1.0: + dependencies: + content-type: 2.0.0 + media-typer: 1.1.0 + mime-types: 3.0.2 + + unpipe@1.0.0: {} + + vary@1.1.2: {} + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + wrappy@1.0.2: {} + + zod-to-json-schema@3.25.2(zod@3.25.76): + dependencies: + zod: 3.25.76 + + zod@3.25.76: {} diff --git a/src/server.mjs b/src/server.mjs new file mode 100644 index 0000000..8a9172d --- /dev/null +++ b/src/server.mjs @@ -0,0 +1,1105 @@ +#!/usr/bin/env node +import { execFile } from 'node:child_process'; +import { + existsSync, + mkdirSync, + readFileSync, + readdirSync, + rmSync, + statSync, + writeFileSync, +} from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { promisify } from 'node:util'; + +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; +import { z } from 'zod'; + +const execFileAsync = promisify(execFile); +const dirname = path.dirname(fileURLToPath(import.meta.url)); +const workspaceRoot = path.resolve( + process.env.KT_WORKSPACE_ROOT || path.join(dirname, '../../..'), +); + +const projectAliases = { + admin: 'Vue/kt-template-admin', + api: 'Node/kt-template-online-api', + mcp: 'mcp/ktWorkflow', + playground: 'Vue/kt-template-online-playground', + root: '.', + web: 'Vue/kt-template-online-web', +}; + +const projectLabels = { + admin: 'Vue Admin 后台', + api: 'NestJS API 后端', + mcp: 'KT Workflow MCP', + playground: 'Playground 编辑器', + root: 'KT 协作根目录', + web: 'Web 前台展示', +}; + +const taskTypeValues = [ + 'api', + 'backend', + 'commit', + 'database', + 'deploy', + 'docs', + 'frontend', + 'general', + 'mcp', + 'page', + 'style', +]; + +const registeredToolNames = [ + 'kt_read_context', + 'kt_inspect_project', + 'kt_inspect_all_projects', + 'kt_guardrails', + 'kt_prepare_task', + 'kt_suggest_verification', + 'kt_create_page_test_case', + 'kt_api_test_plan', + 'kt_cleanup_history', + 'kt_cleanup_process_plan', + 'kt_env_policy', + 'kt_risk_scan', + 'kt_append_task_record', + 'kt_commit_checklist', +]; + +const defaultHistoryRoots = [ + 'test-artifacts', + '.codex-test-logs', + 'codex-test-logs', + '.codex-verify', + '.playwright-mcp', + '.codex-db-sync', +]; + +function toPosix(value) { + return value.replaceAll(path.sep, '/'); +} + +function resolveInsideRoot(target = '.') { + const absoluteTarget = path.isAbsolute(target) + ? path.resolve(target) + : path.resolve(workspaceRoot, target); + const relative = path.relative(workspaceRoot, absoluteTarget); + + if (relative.startsWith('..') || path.isAbsolute(relative)) { + throw new Error(`Path is outside KT workspace: ${target}`); + } + + return absoluteTarget; +} + +function resolveProject(project = 'root') { + const normalized = projectAliases[project] || project; + const absolutePath = resolveInsideRoot(normalized); + const alias = Object.entries(projectAliases).find( + ([, relativePath]) => resolveInsideRoot(relativePath) === absolutePath, + )?.[0]; + + return { + alias: alias || null, + label: alias ? projectLabels[alias] : '自定义路径', + path: absolutePath, + relativePath: toPosix(path.relative(workspaceRoot, absolutePath)) || '.', + }; +} + +function readText(relativePath, fallback = '') { + const filePath = resolveInsideRoot(relativePath); + if (!existsSync(filePath)) return fallback; + return readFileSync(filePath, 'utf8'); +} + +function readTextIfExists(filePath, fallback = '') { + if (!existsSync(filePath)) return fallback; + return readFileSync(filePath, 'utf8'); +} + +function readJson(filePath) { + if (!existsSync(filePath)) return null; + + try { + return JSON.parse(readFileSync(filePath, 'utf8')); + } catch { + return null; + } +} + +function listImmediateChildren(targetPath) { + if (!existsSync(targetPath)) return []; + + return readdirSync(targetPath, { withFileTypes: true }).map((item) => ({ + name: item.name, + type: item.isDirectory() ? 'directory' : 'file', + })); +} + +function getRecentTaskRecords(count = 5) { + const tasks = readText('TASKS.md'); + const records = tasks + .split(/\n(?=### \d{4}-\d{2}-\d{2}:)/) + .filter((section) => section.startsWith('### ')) + .slice(0, count); + + return records.join('\n\n').trim(); +} + +async function tryExecFile(command, args, cwd) { + try { + const result = await execFileAsync(command, args, { + cwd, + maxBuffer: 1024 * 1024 * 4, + timeout: 20_000, + windowsHide: true, + }); + return { + ok: true, + stderr: result.stderr.trim(), + stdout: result.stdout.trim(), + }; + } catch (error) { + return { + ok: false, + stderr: `${error.stderr || error.message || ''}`.trim(), + stdout: `${error.stdout || ''}`.trim(), + }; + } +} + +function detectRepoType(projectPath) { + if (existsSync(path.join(projectPath, '.git'))) return 'git'; + if (existsSync(path.join(projectPath, '.svn'))) return 'svn'; + return 'none'; +} + +function detectPackageManager(projectPath) { + const packageJsonPath = path.join(projectPath, 'package.json'); + const packageJson = readJson(packageJsonPath); + const lockfiles = { + npm: existsSync(path.join(projectPath, 'package-lock.json')), + pnpm: existsSync(path.join(projectPath, 'pnpm-lock.yaml')), + yarn: existsSync(path.join(projectPath, 'yarn.lock')), + }; + const packageManagerSpec = packageJson?.packageManager || null; + const packageManager = + packageManagerSpec?.split('@')[0] || + (lockfiles.pnpm && 'pnpm') || + (lockfiles.yarn && 'yarn') || + (lockfiles.npm && 'npm') || + null; + + return { + engines: packageJson?.engines || {}, + lockfiles, + packageManager, + packageManagerSpec, + scripts: packageJson?.scripts || {}, + }; +} + +async function inspectProject(input) { + const project = resolveProject(input.project); + const repoType = detectRepoType(project.path); + const packageInfo = detectPackageManager(project.path); + const nodeVersionFile = ['.node-version', '.nvmrc'].find((file) => + existsSync(path.join(project.path, file)), + ); + const nodeVersion = nodeVersionFile + ? readFileSync(path.join(project.path, nodeVersionFile), 'utf8').trim() + : null; + const envFiles = ['.env.development', '.env.production', '.env.example'].map( + (file) => ({ + exists: existsSync(path.join(project.path, file)), + file, + }), + ); + const childSummary = listImmediateChildren(project.path).slice(0, 60); + const git = + repoType === 'git' && input.includeGit !== false + ? { + branch: await tryExecFile('git', ['branch', '--show-current'], project.path), + remote: await tryExecFile('git', ['remote', '-v'], project.path), + status: await tryExecFile('git', ['status', '--short'], project.path), + } + : null; + + return { + childSummary, + envFiles, + git, + nodeVersion, + nodeVersionFile: nodeVersionFile || null, + packageInfo, + project, + repoType, + }; +} + +function getTypecheckCommand(project, packageManager, scripts) { + if (project.alias === 'admin') { + return 'pnpm -F @vben/web-antdv-next run typecheck'; + } + if (project.alias === 'web') { + return 'pnpm exec vue-tsc --noEmit'; + } + if (scripts.typecheck) return `${packageManager} run typecheck`; + if (scripts['check:type']) return `${packageManager} run check:type`; + if (scripts.check) return `${packageManager} run check`; + return null; +} + +function buildVerificationPlan(input) { + const project = resolveProject(input.project); + const repoType = detectRepoType(project.path); + const packageInfo = detectPackageManager(project.path); + const packageManager = packageInfo.packageManager || 'pnpm'; + const scripts = packageInfo.scripts; + const changeType = input.changeType || 'general'; + const commands = repoType === 'git' ? ['git status --short'] : []; + const notes = [ + '先确认仓库类型,再确认包管理器和 Node 版本。', + '只跑当前变更所需的最轻验证,不默认全量 build。', + ]; + + const typecheck = getTypecheckCommand(project, packageManager, scripts); + + if (changeType === 'style' && project.alias === 'admin') { + commands.push( + 'pnpm exec stylelint "apps/web-antdv-next/src/components/**/*.{scss,css,vue}"', + ); + } + + if (repoType === 'none') { + notes.push('当前目录不是独立 Git/SVN 仓库,提交和 diff 检查需要切到对应子仓库执行。'); + } + + if ( + typecheck && + ['api', 'frontend', 'general', 'page', 'style'].includes(changeType) + ) { + commands.push(typecheck); + } + + if (project.alias === 'api') { + if (scripts.lint) commands.push(`${packageManager} run lint`); + if (scripts.test && ['api', 'backend', 'general'].includes(changeType)) { + commands.push(`${packageManager} test -- --passWithNoTests`); + } + if (['api', 'backend'].includes(changeType)) { + notes.push('接口改动需要本地启动或复用本地服务,真实调用对应接口一次。'); + } + } + + if (project.alias === 'playground' && scripts['build-preview'] && changeType === 'deploy') { + commands.push(`${packageManager} run build-preview`); + } + + if (project.alias === 'web' && scripts.build && changeType === 'deploy') { + commands.push(`${packageManager} run build`); + } + + if (project.alias === 'mcp') { + if (scripts['self-test']) commands.push(`${packageManager} run self-test`); + notes.push('MCP 变更需要用真实 SDK Client 做 listTools/callTool smoke test。'); + } + + if (input.includePageTest || changeType === 'page') { + notes.push( + '页面级测试必须先写测试用例,使用可见浏览器或截图记录关键步骤,同一用例最多三轮闭环。', + ); + } + + if (repoType === 'git') commands.push('git diff --check'); + + return { + changeType, + commands: Array.from(new Set(commands)), + notes, + project, + repoType, + }; +} + +function createPageTestCase(input) { + const project = resolveProject(input.project); + const title = input.title || `${project.label} 页面级测试`; + const steps = + input.steps?.length > 0 + ? input.steps + : ['打开入口 URL', '完成登录或注入测试登录态', '执行用户关键路径操作', '保存关键步骤截图']; + const assertions = + input.assertions?.length > 0 + ? input.assertions + : ['页面无明显渲染错误', '控制台无 error', '关键接口返回成功', '核心 DOM 或业务状态符合预期']; + + return { + account: input.account || '按当前环境使用数据库管理员账号或测试专用账号', + assertions, + cleanup: [ + '关闭浏览器实例', + '清理本次启动的 Node/Vite 进程', + '保存截图和 result.json', + '调用 kt_cleanup_history,仅保留最近 3 轮历史产物', + ], + entryUrl: input.entryUrl || '待填写', + maxRounds: 3, + project, + protocol: [ + '测试前先写用例。', + '失败后先记录复现证据,再排查和修复。', + '按同一用例复测。', + '第三轮仍失败时停止并等待用户建议。', + ], + steps, + title, + }; +} + +async function inspectAllProjects(input = {}) { + const aliases = input.includeRoot === false + ? Object.keys(projectAliases).filter((key) => key !== 'root') + : Object.keys(projectAliases); + const inspections = await Promise.all( + aliases.map((alias) => + inspectProject({ + includeGit: input.includeGit !== false, + project: alias, + }), + ), + ); + + return { + inspections, + workspaceRoot, + }; +} + +function buildGuardrails(input) { + const project = resolveProject(input.project); + const taskType = input.taskType || input.changeType || 'general'; + const paths = input.paths || []; + const guardrails = { + beforeEdit: [ + '先读最近的 AGENTS.md,再读 TASKS.md 和 SKILLS.md。', + '先检查目标仓库状态,识别用户已有改动,不覆盖、不回滚无关文件。', + '先快速扫现有代码风格和目录结构,再决定改动方式。', + '搜索优先用 rg,路径和接口名尽量保持可 grep。', + ], + duringEdit: [ + '改动范围收敛到本次需求相关文件。', + '复杂度高的地方加简短注释,避免空泛解释。', + '真实环境配置不写入提交内容,优先只维护 .env.example。', + '前端实现遵循现有 antdv-next/Vben 约定,不引入无关抽象。', + ], + doNot: [ + '不要执行 git reset --hard、git checkout -- 等破坏性回滚。', + '不要在用户未要求时自动 push。', + '不要把真实密钥、数据库密码、生产 env 写进可提交文件。', + '不要为了验证长期保留 Node/Vite/浏览器进程。', + ], + verification: buildVerificationPlan({ + changeType: taskType, + includePageTest: input.includePageTest || taskType === 'page', + project: input.project, + }), + }; + + if (['api', 'backend'].includes(taskType)) { + guardrails.beforeEdit.push('接口改动先确认 DTO、Controller、Service、返回结构和鉴权链路。'); + guardrails.duringEdit.push('接口成功返回统一保持 code=200/msg/data,错误返回 err 字段。'); + guardrails.verification.notes.push('接口改完必须本地真实调用一次对应接口。'); + } + + if (taskType === 'page' || input.includePageTest) { + guardrails.beforeEdit.push('页面级测试先写用例,再打开可视化浏览器执行。'); + guardrails.verification.pageTestCase = createPageTestCase({ + project: input.project, + title: input.userRequest || `${project.label} 页面级测试`, + }); + } + + if (taskType === 'database') { + guardrails.beforeEdit.push('数据库同步或修复前先确认源库、目标库、备份库和回滚点。'); + guardrails.doNot.push('不要在未确认目标库前执行覆盖、drop、truncate。'); + } + + if (taskType === 'deploy') { + guardrails.duringEdit.push('部署配置保留真正能提升运行稳定性的内容,删掉噪音配置。'); + guardrails.doNot.push('不要在未要求时触发真实部署或改动远程服务。'); + } + + if (taskType === 'mcp') { + guardrails.duringEdit.push('MCP 工具描述要明确边界,默认输出建议和检查清单,不做高风险副作用。'); + guardrails.verification.notes.push('MCP 变更至少跑 self-test 和真实 SDK Client smoke test。'); + } + + return { + guardrails, + paths, + project, + taskType, + userRequest: input.userRequest || '', + }; +} + +function createApiTestPlan(input) { + const project = resolveProject(input.project || 'api'); + const method = (input.method || 'GET').toUpperCase(); + const endpoint = input.endpoint || '/api/待填写'; + const baseUrl = input.baseUrl || 'http://localhost:3000'; + const headers = []; + + if (input.auth === 'bearer') { + headers.push('-H "Authorization: Bearer "'); + } + + if (input.contentType !== false && ['POST', 'PUT', 'PATCH'].includes(method)) { + headers.push('-H "Content-Type: application/json"'); + } + + const body = + input.body && ['POST', 'PUT', 'PATCH', 'DELETE'].includes(method) + ? ` -d '${JSON.stringify(input.body)}'` + : ''; + const curl = `curl -i -X ${method} ${headers.join(' ')} "${baseUrl}${endpoint}"${body}`; + + return { + assertions: [ + 'HTTP 状态码符合预期。', + '成功响应结构为 { code: 200, msg: string, data: any }。', + '成功响应不返回 err 字段。', + ...(input.expectedFields || []).map((field) => `data 中包含 ${field}`), + ], + curl, + endpoint, + method, + notes: [ + '接口改动后不要只跑类型检查,必须真实调用对应接口一次。', + '如依赖登录态,先调用 login 获取 token,再带 Authorization 复测。', + ], + project, + }; +} + +function createCleanupProcessPlan(input) { + const project = resolveProject(input.project); + const escapedProjectPath = project.path.replaceAll('\\', '\\\\'); + const portFilters = (input.ports || []).map((port) => ({ + command: `Get-NetTCPConnection -LocalPort ${port} -ErrorAction SilentlyContinue | Select-Object -ExpandProperty OwningProcess -Unique`, + port, + })); + + return { + commands: [ + `Get-CimInstance Win32_Process | Where-Object { $_.CommandLine -like '*${escapedProjectPath}*' -and $_.Name -match 'node|pnpm|npm|yarn' } | Select-Object ProcessId,Name,CommandLine`, + '确认命中进程只属于本次验证后,再用 Stop-Process -Id 结束。', + ...portFilters.map((item) => item.command), + ], + notes: [ + '先按项目路径筛选,再按端口交叉确认。', + '不要批量结束与本次验证无关的 Node 进程。', + '页面测试结束后清理浏览器和 Vite/Nest 进程,避免内存累积。', + ], + project, + }; +} + +function listHistoryEntries(rootPath, preservedNames) { + if (!existsSync(rootPath)) return []; + + return readdirSync(rootPath, { withFileTypes: true }) + .filter((item) => !preservedNames.has(item.name)) + .map((item) => { + const entryPath = path.join(rootPath, item.name); + const stats = statSync(entryPath); + + return { + lastWriteTime: stats.mtime.toISOString(), + mtimeMs: stats.mtimeMs, + name: item.name, + path: entryPath, + relativePath: toPosix(path.relative(workspaceRoot, entryPath)), + type: item.isDirectory() ? 'directory' : 'file', + }; + }) + .sort((a, b) => b.mtimeMs - a.mtimeMs); +} + +function cleanupHistoryArtifacts(input = {}) { + const keep = Math.max(0, Math.min(input.keep ?? 3, 50)); + const dryRun = input.dryRun ?? true; + const roots = input.roots?.length > 0 ? input.roots : defaultHistoryRoots; + const result = { + deleted: [], + dryRun, + keep, + preserved: [], + roots: [], + skipped: [], + }; + + for (const root of roots) { + const rootPath = resolveInsideRoot(root); + const rootRelativePath = toPosix(path.relative(workspaceRoot, rootPath)) || '.'; + + if (!existsSync(rootPath)) { + result.skipped.push({ + reason: 'not_exists', + root: rootRelativePath, + }); + continue; + } + + const preservedNames = new Set(rootRelativePath === 'test-artifacts' ? ['_templates'] : []); + const entries = listHistoryEntries(rootPath, preservedNames); + const preserved = entries.slice(0, keep); + const deleting = entries.slice(keep); + + result.roots.push({ + candidates: entries.length, + deleteCount: deleting.length, + keepCount: preserved.length, + preservedNames: Array.from(preservedNames), + root: rootRelativePath, + }); + result.preserved.push(...preserved.map(({ mtimeMs, path: _path, ...entry }) => entry)); + + for (const entry of deleting) { + if (!dryRun) { + rmSync(entry.path, { + force: true, + recursive: true, + }); + } + const { mtimeMs, path: _path, ...publicEntry } = entry; + result.deleted.push(publicEntry); + } + } + + return result; +} + +function parseCliCleanupArgs(args) { + const keepArg = args.find((arg) => arg.startsWith('--keep=')); + const rootsArg = args.find((arg) => arg.startsWith('--roots=')); + + return { + dryRun: args.includes('--dry-run'), + keep: keepArg ? Number.parseInt(keepArg.split('=')[1], 10) : 3, + roots: rootsArg ? rootsArg.split('=')[1].split(',').filter(Boolean) : defaultHistoryRoots, + }; +} + +function buildEnvPolicy(input) { + const project = resolveProject(input.project); + const envFiles = ['.env.development', '.env.production', '.env.example'].map((file) => { + const absolutePath = path.join(project.path, file); + return { + exists: existsSync(absolutePath), + file, + relativePath: toPosix(path.relative(workspaceRoot, absolutePath)), + }; + }); + const gitignore = readTextIfExists(path.join(project.path, '.gitignore')); + const ignoredRules = gitignore + .split(/\r?\n/) + .map((line) => line.trim()) + .filter(Boolean); + + return { + envFiles, + ignoredRules: ignoredRules.filter((line) => line.includes('.env')), + policy: [ + '真实开发和生产配置只保留在本地,不提交。', + '.env.example 用于维护变量名和示例值。', + '接口、数据库、WordPress、MinIO 等真实账号密码不得写入文档或提交记录。', + ], + project, + warnings: envFiles + .filter((file) => file.file !== '.env.example' && file.exists) + .map((file) => `${file.file} 存在,提交前确认已被 .gitignore 忽略。`), + }; +} + +async function scanTaskRisk(input) { + const project = resolveProject(input.project); + let changedFiles = input.changedFiles || []; + + if (changedFiles.length === 0 && detectRepoType(project.path) === 'git') { + const status = await tryExecFile('git', ['status', '--short'], project.path); + if (status.ok) { + changedFiles = status.stdout + .split(/\r?\n/) + .map((line) => line.slice(3).trim()) + .filter(Boolean); + } + } + + const risks = []; + const addRisk = (level, file, reason, suggestion) => { + risks.push({ file, level, reason, suggestion }); + }; + + for (const file of changedFiles) { + const normalized = file.replaceAll('\\', '/'); + + if (/\.env\.(development|production)$/.test(normalized)) { + addRisk('high', file, '真实环境配置文件有改动。', '确认不会提交真实密钥和密码。'); + } + + if (/package\.json$|pnpm-lock\.yaml$|package-lock\.json$|yarn\.lock$/.test(normalized)) { + addRisk('medium', file, '依赖或脚本发生变化。', '确认锁文件与 package.json 同步,并运行对应轻量验证。'); + } + + if (normalized.includes('/components/ktTable/')) { + addRisk( + 'medium', + file, + 'KtTable 是高频核心组件,布局和滚动性能容易回归。', + '保持原生 antdv-next 表格能力,避免监听 scroll/resize 做高频状态更新。', + ); + } + + if (/src\/admin\/|src\/.*controller|src\/.*service|src\/.*dto/.test(normalized)) { + addRisk('medium', file, '后端接口或业务逻辑改动。', '本地启动或复用服务后真实调用对应接口。'); + } + + if (/Jenkinsfile|deploy\/|k8s\/|nginx.*\.conf/.test(normalized)) { + addRisk('medium', file, '部署链路配置改动。', '只验证配置语法或生成产物,不主动触发远程部署。'); + } + } + + return { + changedFiles, + project, + risks, + safeDefaults: [ + '不覆盖用户已有改动。', + '只运行必要验证。', + '提交前重新检查 status 和 diff。', + ], + }; +} + +async function prepareTask(input) { + const project = resolveProject(input.project); + const taskType = input.taskType || 'general'; + const inspection = await inspectProject({ + includeGit: input.includeGit !== false, + project: input.project, + }); + const guardrails = buildGuardrails(input); + const riskScan = await scanTaskRisk({ + changedFiles: input.changedFiles || [], + project: input.project, + }); + const pageTestCase = + input.includePageTest || taskType === 'page' + ? createPageTestCase({ + entryUrl: input.entryUrl, + project: input.project, + title: input.userRequest || `${project.label} 页面级测试`, + }) + : null; + + return { + guardrails, + inspection, + nextSteps: [ + '阅读 workPacket.readFirst 中的上下文文件。', + '按 guardrails.beforeEdit 做开工前检查。', + '按最小范围修改。', + '按 verification.commands 和 notes 做轻量验证。', + '有文件改动时更新 TASKS.md。', + ], + pageTestCase, + project, + readFirst: [ + 'AGENTS.md', + 'SKILLS.md', + 'TASKS.md', + `${project.relativePath}/AGENTS.md`, + `${project.relativePath}/package.json`, + ], + riskScan, + taskRecordDraft: buildTaskRecord({ + content: input.userRequest || '待补充', + scope: [project.relativePath], + testCase: pageTestCase ? pageTestCase.title : '本次无页面测试或待补充。', + title: input.recordTitle || input.userRequest || `${project.label} 任务记录`, + verification: guardrails.guardrails.verification.commands.join(';') || '待补充。', + }), + taskType, + userRequest: input.userRequest || '', + }; +} + +function formatDateInShanghai(date = new Date()) { + const parts = new Intl.DateTimeFormat('en-CA', { + day: '2-digit', + month: '2-digit', + timeZone: 'Asia/Shanghai', + year: 'numeric', + }).formatToParts(date); + const map = Object.fromEntries(parts.map((part) => [part.type, part.value])); + return `${map.year}-${map.month}-${map.day}`; +} + +function buildTaskRecord(input) { + const date = input.date || formatDateInShanghai(); + const scope = input.scope?.length > 0 ? input.scope.join('、') : '待补充'; + const content = input.content || '待补充'; + const testCase = input.testCase || '本次无页面测试或待补充。'; + const verification = input.verification || '待补充。'; + + return `### ${date}:${input.title}\n\n- 范围:${scope}\n- 内容:${content}\n- 页面测试用例:${testCase}\n- 验证:${verification}\n`; +} + +function appendTaskRecord(input) { + const record = buildTaskRecord(input); + const tasksPath = resolveInsideRoot('TASKS.md'); + const current = readFileSync(tasksPath, 'utf8'); + const marker = '## 改动记录'; + const markerIndex = current.indexOf(marker); + + if (markerIndex === -1) { + throw new Error('TASKS.md does not contain ## 改动记录'); + } + + const insertAt = current.indexOf('\n', markerIndex); + const next = `${current.slice(0, insertAt + 1)}\n${record}\n${current.slice(insertAt + 1)}`; + + if (!input.dryRun) { + writeFileSync(tasksPath, next, 'utf8'); + } + + return { + dryRun: input.dryRun, + record, + target: tasksPath, + }; +} + +async function createCommitChecklist(input) { + const inspection = await inspectProject({ + includeGit: true, + project: input.project, + }); + const message = input.message || ''; + const messageOk = + !message || + /^(feat|fix|docs|style|refactor|test|chore|build|ci|perf|revert)(\([^)]+\))?: .*[一-龥]/u.test( + message, + ); + + return { + checks: [ + '确认只包含本次任务相关文件。', + '先运行对应轻量验证命令。', + '每个独立 Git 仓库单独 commit。', + '没有用户明确要求时不 push。', + ], + inspection, + message, + messageOk, + messageRule: '英文 type 前缀 + 中文描述,例如 feat: 功能性提交', + }; +} + +function readWorkflowContext(input) { + const projects = Object.keys(projectAliases).map((key) => { + const project = resolveProject(key); + return { + ...project, + exists: existsSync(project.path), + repoType: existsSync(project.path) ? detectRepoType(project.path) : 'missing', + }; + }); + + return { + docs: { + agents: readText('AGENTS.md'), + recentTasks: getRecentTaskRecords(input.taskRecordCount), + skills: readText('SKILLS.md'), + }, + projects, + workspaceRoot, + }; +} + +function response(data) { + return { + content: [ + { + text: typeof data === 'string' ? data : JSON.stringify(data, null, 2), + type: 'text', + }, + ], + }; +} + +async function runSelfTest() { + const data = { + context: readWorkflowContext({ taskRecordCount: 2 }), + guardrails: buildGuardrails({ + project: 'mcp', + taskType: 'mcp', + userRequest: '扩展 KT 工作区 MCP 可复用能力', + }), + historyCleanup: cleanupHistoryArtifacts({ + dryRun: true, + keep: 3, + }), + inspectAdmin: await inspectProject({ includeGit: false, project: 'admin' }), + prepareTask: await prepareTask({ + includeGit: false, + project: 'mcp', + taskType: 'mcp', + userRequest: '扩展 KT 工作区 MCP 可复用能力', + }), + verification: buildVerificationPlan({ + changeType: 'mcp', + project: 'mcp', + }), + }; + const outputDir = resolveInsideRoot('.codex-verify/ktWorkflow'); + mkdirSync(outputDir, { recursive: true }); + writeFileSync( + path.join(outputDir, 'self-test.json'), + JSON.stringify(data, null, 2), + 'utf8', + ); + console.log(JSON.stringify({ ok: true, outputDir, tools: registeredToolNames.length }, null, 2)); +} + +function registerTools(server) { + server.registerTool( + 'kt_read_context', + { + description: '读取 KT 根目录 AGENTS/SKILLS/TASKS,并返回项目清单和最近任务记录。', + inputSchema: { + taskRecordCount: z.number().int().min(0).max(20).default(5), + }, + title: 'Read KT Workflow Context', + }, + async (input) => response(readWorkflowContext(input)), + ); + + server.registerTool( + 'kt_inspect_project', + { + description: '检查 KT 子项目的仓库类型、包管理器、Node 版本、env 文件和 Git 状态。', + inputSchema: { + includeGit: z.boolean().default(true), + project: z.string().default('root'), + }, + title: 'Inspect KT Project', + }, + async (input) => response(await inspectProject(input)), + ); + + server.registerTool( + 'kt_inspect_all_projects', + { + description: '一次性检查 KT 工作区所有已知项目,适合多仓库联动任务开工前使用。', + inputSchema: { + includeGit: z.boolean().default(true), + includeRoot: z.boolean().default(true), + }, + title: 'Inspect All KT Projects', + }, + async (input) => response(await inspectAllProjects(input)), + ); + + server.registerTool( + 'kt_guardrails', + { + description: '按任务类型生成 KT 防偏差清单,包含开工、改动、禁止项和验证约束。', + inputSchema: { + changeType: z.enum(taskTypeValues).optional(), + includePageTest: z.boolean().default(false), + paths: z.array(z.string()).default([]), + project: z.string().default('root'), + taskType: z.enum(taskTypeValues).default('general'), + userRequest: z.string().optional(), + }, + title: 'KT Guardrails', + }, + async (input) => response(buildGuardrails(input)), + ); + + server.registerTool( + 'kt_prepare_task', + { + description: '生成一份 KT 任务 work packet:上下文、项目检查、防偏差清单、风险扫描和验证计划。', + inputSchema: { + changedFiles: z.array(z.string()).default([]), + entryUrl: z.string().optional(), + includeGit: z.boolean().default(true), + includePageTest: z.boolean().default(false), + paths: z.array(z.string()).default([]), + project: z.string().default('root'), + recordTitle: z.string().optional(), + taskType: z.enum(taskTypeValues).default('general'), + userRequest: z.string().optional(), + }, + title: 'Prepare KT Task', + }, + async (input) => response(await prepareTask(input)), + ); + + server.registerTool( + 'kt_suggest_verification', + { + description: '按 KT 规则为指定项目和变更类型生成轻量验证命令与注意事项。', + inputSchema: { + changeType: z.enum(taskTypeValues).default('general'), + includePageTest: z.boolean().default(false), + project: z.string().default('root'), + }, + title: 'Suggest KT Verification', + }, + async (input) => response(buildVerificationPlan(input)), + ); + + server.registerTool( + 'kt_create_page_test_case', + { + description: '生成 KT 页面级可视化测试用例骨架,包含事不过三闭环规则。', + inputSchema: { + account: z.string().optional(), + assertions: z.array(z.string()).default([]), + entryUrl: z.string().optional(), + project: z.string().default('admin'), + steps: z.array(z.string()).default([]), + title: z.string().optional(), + }, + title: 'Create KT Page Test Case', + }, + async (input) => response(createPageTestCase(input)), + ); + + server.registerTool( + 'kt_api_test_plan', + { + description: '为后端接口改动生成真实调用测试计划和响应结构断言。', + inputSchema: { + auth: z.enum(['none', 'bearer']).default('bearer'), + baseUrl: z.string().default('http://localhost:3000'), + body: z.record(z.any()).optional(), + contentType: z.boolean().default(true), + endpoint: z.string().default('/api/待填写'), + expectedFields: z.array(z.string()).default([]), + method: z.enum(['DELETE', 'GET', 'PATCH', 'POST', 'PUT']).default('GET'), + project: z.string().default('api'), + }, + title: 'KT API Test Plan', + }, + async (input) => response(createApiTestPlan(input)), + ); + + server.registerTool( + 'kt_cleanup_history', + { + description: '清理 KT 根目录历史产物。默认 dryRun=true,只预览;执行时按 LastWriteTime 每个历史目录仅保留最近 3 轮,test-artifacts/_templates 永久保留。', + inputSchema: { + dryRun: z.boolean().default(true), + keep: z.number().int().min(0).max(50).default(3), + roots: z.array(z.string()).default(defaultHistoryRoots), + }, + title: 'KT Cleanup History Artifacts', + }, + async (input) => response(cleanupHistoryArtifacts(input)), + ); + + server.registerTool( + 'kt_cleanup_process_plan', + { + description: '生成按项目路径和端口清理验证进程的 PowerShell 检查命令,不直接杀进程。', + inputSchema: { + ports: z.array(z.number().int().min(1).max(65_535)).default([]), + project: z.string().default('root'), + }, + title: 'KT Cleanup Process Plan', + }, + async (input) => response(createCleanupProcessPlan(input)), + ); + + server.registerTool( + 'kt_env_policy', + { + description: '检查指定项目 env 文件现状,并输出 KT 环境配置提交策略。', + inputSchema: { + project: z.string().default('root'), + }, + title: 'KT Env Policy', + }, + async (input) => response(buildEnvPolicy(input)), + ); + + server.registerTool( + 'kt_risk_scan', + { + description: '按当前变更文件或传入文件列表扫描常见偏差风险和验证提醒。', + inputSchema: { + changedFiles: z.array(z.string()).default([]), + project: z.string().default('root'), + }, + title: 'KT Risk Scan', + }, + async (input) => response(await scanTaskRisk(input)), + ); + + server.registerTool( + 'kt_append_task_record', + { + description: '生成或写入 TASKS.md 改动记录。默认 dryRun=true,只预览不落盘。', + inputSchema: { + content: z.string().optional(), + date: z.string().optional(), + dryRun: z.boolean().default(true), + scope: z.array(z.string()).default([]), + testCase: z.string().optional(), + title: z.string(), + verification: z.string().optional(), + }, + title: 'Append KT Task Record', + }, + async (input) => response(appendTaskRecord(input)), + ); + + server.registerTool( + 'kt_commit_checklist', + { + description: '生成 KT 子仓库提交前检查清单,并校验 commit message 是否符合约定。', + inputSchema: { + message: z.string().optional(), + project: z.string().default('root'), + }, + title: 'KT Commit Checklist', + }, + async (input) => response(await createCommitChecklist(input)), + ); +} + +if (process.argv.includes('--cleanup-history')) { + console.log(JSON.stringify(cleanupHistoryArtifacts(parseCliCleanupArgs(process.argv)), null, 2)); +} else if (process.argv.includes('--self-test')) { + await runSelfTest(); +} else { + const server = new McpServer({ + name: 'kt-workflow', + version: '0.2.0', + }); + registerTools(server); + await server.connect(new StdioServerTransport()); +}