diff --git a/.mcp.json.example b/.mcp.json.example new file mode 100644 index 0000000..670e69b --- /dev/null +++ b/.mcp.json.example @@ -0,0 +1,11 @@ +{ + "mcpServers": { + "maestro": { + "command": "node", + "args": ["/绝对路径/maestro/dist/mcp/index.js"], + "env": { + "MAESTRO_URL": "http://127.0.0.1:4517" + } + } + } +} diff --git a/README.md b/README.md index ce2e37a..0813547 100644 --- a/README.md +++ b/README.md @@ -27,3 +27,22 @@ npm run dev # 启动 daemon(开发) ``` > 依赖版本为初始值;MCP SDK 与 Claude Agent SDK 在 Phase 2 接入时按官方文档核定后再加。 + +## 接入 Claude Code(MCP) + +先 `npm run build`,并保证 maestrod 在跑(`npm run dev`,默认 http://127.0.0.1:4517)。两种接法: + +```bash +# 方式一:claude mcp add(在要接入的项目里执行) +claude mcp add maestro -e MAESTRO_URL=http://127.0.0.1:4517 -- node /绝对路径/maestro/dist/mcp/index.js +``` + +```jsonc +// 方式二:项目根放 .mcp.json(模板见本仓库 .mcp.json.example) +{ "mcpServers": { "maestro": { + "command": "node", + "args": ["/绝对路径/maestro/dist/mcp/index.js"], + "env": { "MAESTRO_URL": "http://127.0.0.1:4517" } } } } +``` + +工具:`list_projects` / `create_project` / `list_tasks` / `get_task` / `create_task` / `decompose_task` / `write_plan` / `write_spec` / `write_operations` / `update_status` / `get_next_executable` / `get_pending_approvals`(只读;accept/reject 在 Web 看板做)。 diff --git a/package-lock.json b/package-lock.json index b7f8575..3ccbc52 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "maestro", "version": "0.1.0", "dependencies": { + "@modelcontextprotocol/sdk": "^1.29.0", "better-sqlite3": "^11.3.0", "fastify": "^4.28.1", "nanoid": "^5.0.7", @@ -504,6 +505,75 @@ "fast-deep-equal": "^3.1.3" } }, + "node_modules/@hono/node-server": { + "version": "1.19.14", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.14.tgz", + "integrity": "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==", + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz", + "integrity": "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.19.9", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.2.1", + "express-rate-limit": "^8.2.1", + "hono": "^4.11.4", + "jose": "^6.1.3", + "json-schema-typed": "^8.0.2", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, "node_modules/@pinojs/redact": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz", @@ -546,6 +616,19 @@ "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==", "license": "MIT" }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/ajv": { "version": "8.20.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", @@ -665,6 +748,30 @@ "readable-stream": "^3.4.0" } }, + "node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/buffer": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", @@ -689,12 +796,72 @@ "ieee754": "^1.1.13" } }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/chownr": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", "license": "ISC" }, + "node_modules/content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/cookie": { "version": "0.7.2", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", @@ -704,6 +871,63 @@ "node": ">= 0.6" } }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, "node_modules/decompress-response": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", @@ -728,6 +952,15 @@ "node": ">=4.0.0" } }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -737,6 +970,35 @@ "node": ">=8" } }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/end-of-stream": { "version": "1.4.5", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", @@ -746,6 +1008,36 @@ "once": "^1.4.0" } }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/esbuild": { "version": "0.28.1", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", @@ -788,6 +1080,42 @@ "@esbuild/win32-x64": "0.28.1" } }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.0.tgz", + "integrity": "sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/expand-template": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", @@ -797,6 +1125,67 @@ "node": ">=6" } }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.2.tgz", + "integrity": "sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==", + "license": "MIT", + "dependencies": { + "ip-address": "^10.2.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, "node_modules/fast-content-type-parse": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-1.1.0.tgz", @@ -911,6 +1300,27 @@ "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", "license": "MIT" }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/find-my-way": { "version": "8.2.2", "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-8.2.2.tgz", @@ -934,6 +1344,15 @@ "node": ">= 0.6" } }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/fs-constants": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", @@ -955,12 +1374,139 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "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.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/github-from-package": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", "license": "MIT" }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hono": { + "version": "4.12.25", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.25.tgz", + "integrity": "sha512-2NFaIyNVgJmBs/ecmtGzlmluTFs5cHEWGTdu0t1HBwYzoGXOL5nUQBRMXsXWla5i4KkG//QMzVP88m1+I3fdAQ==", + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -993,6 +1539,15 @@ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", "license": "ISC" }, + "node_modules/ip-address": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -1002,6 +1557,27 @@ "node": ">= 0.10" } }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jose": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.3.tgz", + "integrity": "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/json-schema-ref-resolver": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-schema-ref-resolver/-/json-schema-ref-resolver-1.0.1.tgz", @@ -1017,6 +1593,12 @@ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "license": "MIT" }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "license": "BSD-2-Clause" + }, "node_modules/light-my-request": { "version": "5.14.0", "resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-5.14.0.tgz", @@ -1028,6 +1610,61 @@ "set-cookie-parser": "^2.4.1" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/mimic-response": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", @@ -1055,6 +1692,12 @@ "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", "license": "MIT" }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, "node_modules/nanoid": { "version": "5.1.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.11.tgz", @@ -1079,6 +1722,15 @@ "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", "license": "MIT" }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/node-abi": { "version": "3.92.0", "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.92.0.tgz", @@ -1091,6 +1743,27 @@ "node": ">=10" } }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/on-exit-leak-free": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", @@ -1100,6 +1773,18 @@ "node": ">=14.0.0" } }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -1109,6 +1794,34 @@ "wrappy": "1" } }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/pino": { "version": "9.14.0", "resolved": "https://registry.npmjs.org/pino/-/pino-9.14.0.tgz", @@ -1162,6 +1875,15 @@ ], "license": "MIT" }, + "node_modules/pkce-challenge": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", + "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, "node_modules/prebuild-install": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", @@ -1218,12 +1940,51 @@ "once": "^1.3.1" } }, + "node_modules/qs": { + "version": "6.15.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", + "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/quick-format-unescaped": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", "license": "MIT" }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/rc": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", @@ -1296,6 +2057,22 @@ "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", "license": "MIT" }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -1334,6 +2111,12 @@ "node": ">=10" } }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, "node_modules/secure-json-parse": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz", @@ -1352,12 +2135,156 @@ "node": ">=10" } }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "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" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/set-cookie-parser": { "version": "2.7.2", "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", "license": "MIT" }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "license": "MIT", + "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" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/simple-concat": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", @@ -1421,6 +2348,15 @@ "node": ">= 10.x" } }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -1485,6 +2421,15 @@ "node": ">=20" } }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, "node_modules/tsx": { "version": "4.22.4", "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.4.tgz", @@ -1516,6 +2461,37 @@ "node": "*" } }, + "node_modules/type-is": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", + "license": "MIT", + "dependencies": { + "content-type": "^2.0.0", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/type-is/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -1537,12 +2513,45 @@ "dev": true, "license": "MIT" }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -1569,6 +2578,24 @@ "optional": true } } + }, + "node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.25.2", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", + "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.25.28 || ^4" + } } } } diff --git a/package.json b/package.json index 3fa74bd..63e618c 100644 --- a/package.json +++ b/package.json @@ -4,8 +4,13 @@ "private": true, "type": "module", "description": "多项目 TODO 管理与 Agent 执行系统(本地优先 daemon)", - "engines": { "node": ">=20" }, - "bin": { "maestro": "dist/cli/index.js" }, + "engines": { + "node": ">=20" + }, + "bin": { + "maestro": "dist/cli/index.js", + "maestro-mcp": "dist/mcp/index.js" + }, "scripts": { "build": "tsc -p tsconfig.json && cp src/store/schema.sql dist/store/schema.sql", "typecheck": "tsc -p tsconfig.json --noEmit", @@ -13,16 +18,17 @@ "test": "tsx --test test/*.test.ts" }, "dependencies": { + "@modelcontextprotocol/sdk": "^1.29.0", "better-sqlite3": "^11.3.0", "fastify": "^4.28.1", - "ws": "^8.18.0", - "nanoid": "^5.0.7" + "nanoid": "^5.0.7", + "ws": "^8.18.0" }, "devDependencies": { - "typescript": "^5.5.4", - "tsx": "^4.16.5", - "@types/node": "^20.14.0", "@types/better-sqlite3": "^7.6.11", - "@types/ws": "^8.5.12" + "@types/node": "^20.14.0", + "@types/ws": "^8.5.12", + "tsx": "^4.16.5", + "typescript": "^5.5.4" } } diff --git a/src/api/static.ts b/src/api/static.ts new file mode 100644 index 0000000..ed14b76 --- /dev/null +++ b/src/api/static.ts @@ -0,0 +1,69 @@ +import { readFile } from 'node:fs/promises'; +import { fileURLToPath } from 'node:url'; +import { dirname, extname, join, resolve, sep } from 'node:path'; +import type { FastifyInstance, FastifyReply } from 'fastify'; + +/** + * Web 看板静态文件服务(零依赖,手写)。 + * web/ 不参与 tsc 构建,直接相对仓库根定位: + * dev 时本文件在 src/api/,build 后在 dist/api/,两者到仓库根都是 ../..(与 store/db.ts 同思路)。 + */ +const HERE = dirname(fileURLToPath(import.meta.url)); +const WEB_ROOT = resolve(HERE, '..', '..', 'web'); + +const MIME: Record = { + '.html': 'text/html; charset=utf-8', + '.js': 'text/javascript; charset=utf-8', + '.mjs': 'text/javascript; charset=utf-8', + '.css': 'text/css; charset=utf-8', + '.json': 'application/json; charset=utf-8', + '.svg': 'image/svg+xml', + '.png': 'image/png', + '.jpg': 'image/jpeg', + '.gif': 'image/gif', + '.ico': 'image/x-icon', + '.webp': 'image/webp', + '.woff': 'font/woff', + '.woff2': 'font/woff2', + '.map': 'application/json', + '.txt': 'text/plain; charset=utf-8', +}; + +async function serveFile(reply: FastifyReply, rawPath: string): Promise { + let rel: string; + try { + rel = decodeURIComponent(rawPath); + } catch { + return reply.code(400).type('text/plain; charset=utf-8').send('Bad Request'); + } + if (rel.includes('\0')) { + return reply.code(400).type('text/plain; charset=utf-8').send('Bad Request'); + } + + // 路径穿越防护:解析后必须仍落在 WEB_ROOT 之内 + const abs = resolve(WEB_ROOT, '.' + sep + rel); + if (abs !== WEB_ROOT && !abs.startsWith(WEB_ROOT + sep)) { + return reply.code(403).type('text/plain; charset=utf-8').send('Forbidden'); + } + + const file = abs === WEB_ROOT ? join(WEB_ROOT, 'index.html') : abs; + try { + const buf = await readFile(file); + const type = MIME[extname(file).toLowerCase()] ?? 'application/octet-stream'; + return reply.type(type).send(buf); + } catch { + return reply.code(404).type('text/plain; charset=utf-8').send('Not Found'); + } +} + +/** + * 在 Fastify 上注册看板静态路由。 + * `/` → web/index.html;`/*` → web/ 下对应文件(/api/**、/ws 等更具体路由优先匹配,不受影响)。 + */ +export function registerStatic(app: FastifyInstance): void { + app.get('/', (_req, reply) => serveFile(reply, 'index.html')); + app.get('/*', (req, reply) => { + const params = req.params as { '*': string }; + return serveFile(reply, params['*'] ?? ''); + }); +} diff --git a/src/cli/index.ts b/src/cli/index.ts new file mode 100644 index 0000000..f2ea30a --- /dev/null +++ b/src/cli/index.ts @@ -0,0 +1,611 @@ +#!/usr/bin/env node +/** + * maestro CLI — 通过 REST API 操作 maestrod(零新依赖,node:util parseArgs)。 + * + * 子命令: + * project add [--name N] [--branch B] [--verify CMD] [--concurrency N] + * project list + * task list + * task add --complexity hard|medium|easy [--parent ID] [--priority N] + * next <projectIdOrName> + * approvals [projectIdOrName] + * import-todo <todo.json 路径> --repo <repoPath> [--name N] + * + * 环境变量:MAESTRO_URL(默认 http://127.0.0.1:4517) + */ +import { parseArgs, type ParseArgsConfig } from 'node:util'; +import { readFileSync } from 'node:fs'; +import { resolve, basename } from 'node:path'; +import type { Project, Task } from '../model/types.js'; +import { STATUS_LABEL, type TaskStatus } from '../model/status.js'; +import { COMPLEXITY_LABEL, TIER_TO_COMPLEXITY, isComplexity, type Complexity } from '../model/complexity.js'; + +const BASE = process.env.MAESTRO_URL ?? 'http://127.0.0.1:4517'; + +const USAGE = `maestro — 多项目 TODO 管理与 Agent 执行系统 CLI + +用法: + maestro project add <repoPath> [--name 名称] [--branch 分支] [--verify 命令] [--concurrency 并发数] + 注册项目(name 缺省取目录名) + maestro project list + 列出所有项目 + maestro task list <项目id或名称> + 树形列出项目任务(项目名支持模糊匹配) + maestro task add <项目id或名称> <标题> --complexity hard|medium|easy [--parent 父任务id] [--priority 优先级] + 新建任务 + maestro next <项目id或名称> + 取下一个可执行任务 + maestro approvals [项目id或名称] + 列出待审批任务(不带参数 = 全部项目) + maestro import-todo <todo.json 路径> --repo <仓库路径> [--name 项目名] + 新建项目并导入旧 todo skill 的数据(tier 1/2/3 → hard/medium/easy) + maestro --help | help + 显示本说明 + +环境变量: + MAESTRO_URL daemon 地址(默认 http://127.0.0.1:4517)`; + +// ---------- 基础工具 ---------- + +class ApiError extends Error {} + +function fail(msg: string): never { + console.error(`错误:${msg}`); + process.exit(1); +} + +async function api<T>(method: 'GET' | 'POST', path: string, body?: unknown): Promise<T> { + let res: Response; + try { + res = await fetch(`${BASE}${path}`, { + method, + headers: body === undefined ? undefined : { 'content-type': 'application/json' }, + body: body === undefined ? undefined : JSON.stringify(body), + }); + } catch { + console.error(`无法连接 maestro daemon(${BASE})。`); + console.error('请先启动 maestrod:在 maestro 仓库目录执行 `npm run dev`;'); + console.error('若 daemon 跑在其他地址/端口,请设置 MAESTRO_URL 环境变量。'); + process.exit(1); + } + const text = await res.text(); + let data: unknown = null; + try { + data = text ? JSON.parse(text) : null; + } catch { + /* 非 JSON 响应 */ + } + if (!res.ok) { + const msg = (data as { error?: string } | null)?.error ?? `HTTP ${res.status}`; + throw new ApiError(msg); + } + return data as T; +} + +/** 中日韩等全角字符按宽度 2 计算,保证表格对齐 */ +function displayWidth(s: string): number { + let w = 0; + for (const ch of s) { + w += /[ᄀ-ᅟ⺀-〾ぁ-㏿㐀-䶿一-鿿ꀀ-꓏가-힣豈-﫿︰-﹏＀-⦆¢-₩]/.test(ch) ? 2 : 1; + } + return w; +} + +function pad(s: string, width: number): string { + const gap = width - displayWidth(s); + return gap > 0 ? s + ' '.repeat(gap) : s; +} + +function printTable(headers: string[], rows: string[][]): void { + const widths = headers.map((h, i) => { + let w = displayWidth(h); + for (const r of rows) w = Math.max(w, displayWidth(r[i] ?? '')); + return w; + }); + const line = (cells: string[]): string => cells.map((c, i) => pad(c, widths[i])).join(' '); + console.log(line(headers)); + console.log(widths.map((w) => '-'.repeat(w)).join(' ')); + for (const r of rows) console.log(line(r)); +} + +function parseCmdArgs( + rest: string[], + options: NonNullable<ParseArgsConfig['options']>, +): { values: Record<string, string | boolean | undefined>; positionals: string[] } { + try { + const { values, positionals } = parseArgs({ args: rest, options, allowPositionals: true }); + return { values: values as Record<string, string | boolean | undefined>, positionals }; + } catch (e) { + fail(`参数不合法:${(e as Error).message}\n\n${USAGE}`); + } +} + +async function resolveProject(idOrName: string): Promise<Project> { + const projects = await api<Project[]>('GET', '/api/projects'); + const byId = projects.find((p) => p.id === idOrName); + if (byId) return byId; + let matched = projects.filter((p) => p.name === idOrName); + if (matched.length === 0) { + const lower = idOrName.toLowerCase(); + matched = projects.filter((p) => p.name.toLowerCase().includes(lower)); + } + if (matched.length === 1) return matched[0]; + if (matched.length > 1) { + fail( + `项目「${idOrName}」匹配到 ${matched.length} 个,请用更精确的名称或直接用 id:\n` + + matched.map((p) => ` - ${p.name} (${p.id})`).join('\n'), + ); + } + fail(`找不到项目「${idOrName}」。可先用 \`maestro project list\` 查看全部项目。`); +} + +// ---------- project ---------- + +async function cmdProjectAdd(rest: string[]): Promise<void> { + const { values, positionals } = parseCmdArgs(rest, { + name: { type: 'string' }, + branch: { type: 'string' }, + verify: { type: 'string' }, + concurrency: { type: 'string' }, + }); + const repoArg = positionals[0]; + if (!repoArg) fail('用法:maestro project add <repoPath> [--name N] [--branch B] [--verify CMD] [--concurrency N]'); + const repoPath = resolve(repoArg); + const name = (values.name as string | undefined) ?? basename(repoPath); + const concurrency = values.concurrency === undefined ? undefined : Number(values.concurrency); + if (concurrency !== undefined && (!Number.isInteger(concurrency) || concurrency < 1)) { + fail('--concurrency 必须是 >=1 的整数'); + } + const p = await api<Project>('POST', '/api/projects', { + name, + repoPath, + defaultBranch: values.branch as string | undefined, + verifyCmd: values.verify as string | undefined, + concurrency, + }); + console.log(`已注册项目「${p.name}」(${p.id})`); + console.log(` repo: ${p.repoPath} · 分支: ${p.defaultBranch} · 并发: ${p.concurrency} · verify: ${p.verifyCmd ?? '无'}`); +} + +async function cmdProjectList(): Promise<void> { + const projects = await api<Project[]>('GET', '/api/projects'); + if (projects.length === 0) { + console.log('暂无项目。用 `maestro project add <repoPath>` 注册一个。'); + return; + } + printTable( + ['id', 'name', 'repoPath', 'status'], + projects.map((p) => [p.id, p.name, p.repoPath, p.status]), + ); +} + +// ---------- task ---------- + +function taskLine(t: Task): string { + const deps = t.deps.length ? ` deps:${t.deps.length}` : ''; + return `[${COMPLEXITY_LABEL[t.complexity]}·${STATUS_LABEL[t.status]}] ${t.title}${deps} (${t.id})`; +} + +function printTaskTree(tasks: Task[]): void { + const byParent = new Map<string | null, Task[]>(); + for (const t of tasks) { + const key = t.parentId; + const list = byParent.get(key) ?? []; + list.push(t); + byParent.set(key, list); + } + const sortFn = (a: Task, b: Task): number => b.priority - a.priority || a.createdAt.localeCompare(b.createdAt); + const walk = (parentId: string | null, indent: string): void => { + const list = (byParent.get(parentId) ?? []).sort(sortFn); + for (const t of list) { + console.log(`${indent}- ${taskLine(t)}`); + walk(t.id, indent + ' '); + } + }; + walk(null, ''); +} + +async function cmdTaskList(rest: string[]): Promise<void> { + const { positionals } = parseCmdArgs(rest, {}); + if (!positionals[0]) fail('用法:maestro task list <项目id或名称>'); + const project = await resolveProject(positionals[0]); + const tasks = await api<Task[]>('GET', `/api/projects/${project.id}/tasks`); + console.log(`项目「${project.name}」(${project.id}) · 共 ${tasks.length} 条任务\n`); + if (tasks.length === 0) { + console.log('(暂无任务)'); + return; + } + printTaskTree(tasks); +} + +async function cmdTaskAdd(rest: string[]): Promise<void> { + const { values, positionals } = parseCmdArgs(rest, { + complexity: { type: 'string' }, + parent: { type: 'string' }, + priority: { type: 'string' }, + }); + const projectArg = positionals[0]; + const title = positionals.slice(1).join(' ').trim(); + if (!projectArg || !title) { + fail('用法:maestro task add <项目id或名称> <标题> --complexity hard|medium|easy [--parent ID] [--priority N]'); + } + if (!isComplexity(values.complexity)) fail('--complexity 必填,且只能是 hard | medium | easy'); + const priority = values.priority === undefined ? undefined : Number(values.priority); + if (priority !== undefined && !Number.isFinite(priority)) fail('--priority 必须是数字'); + const project = await resolveProject(projectArg); + const t = await api<Task>('POST', `/api/projects/${project.id}/tasks`, { + title, + complexity: values.complexity, + parentId: values.parent as string | undefined, + priority, + }); + console.log(`已创建任务 ${taskLine(t)}`); +} + +// ---------- next / approvals ---------- + +async function cmdNext(rest: string[]): Promise<void> { + const { positionals } = parseCmdArgs(rest, {}); + if (!positionals[0]) fail('用法:maestro next <项目id或名称>'); + const project = await resolveProject(positionals[0]); + const r = await api<Task | { next: null }>('GET', `/api/projects/${project.id}/next`); + if (!r || !('id' in r)) { + console.log(`项目「${project.name}」当前没有可执行任务(叶子 + ready + 依赖满足)。`); + return; + } + console.log(`下一个可执行任务:`); + console.log(` ${taskLine(r)}`); +} + +async function cmdApprovals(rest: string[]): Promise<void> { + const { positionals } = parseCmdArgs(rest, {}); + let query = ''; + let scope = '全部项目'; + if (positionals[0]) { + const project = await resolveProject(positionals[0]); + query = `?projectId=${encodeURIComponent(project.id)}`; + scope = `项目「${project.name}」`; + } + const tasks = await api<Task[]>('GET', `/api/approvals${query}`); + if (tasks.length === 0) { + console.log(`${scope}当前没有待审批任务。`); + return; + } + const projects = await api<Project[]>('GET', '/api/projects'); + const nameOf = new Map(projects.map((p) => [p.id, p.name])); + console.log(`${scope}待审批任务 ${tasks.length} 条:\n`); + printTable( + ['闸', '复杂度', '标题', '任务id', '项目'], + tasks.map((t) => [ + STATUS_LABEL[t.status], + COMPLEXITY_LABEL[t.complexity], + t.title, + t.id, + nameOf.get(t.projectId) ?? t.projectId, + ]), + ); +} + +// ---------- import-todo ---------- + +interface OldGate { + kind?: string | null; + note?: string | null; + ref?: string | null; + approval?: string | null; +} +interface OldSub { + sid: string; + title?: string; + tier?: number | null; + deps?: string[]; + status?: string; +} +interface OldItem { + id: number; + title?: string; + desc?: string | null; + level?: string | null; + tier?: number | null; + tags?: string[]; + status?: string; + done?: boolean; + version?: string | null; + subtasks?: OldSub[]; + gate?: OldGate | null; + reject_reason?: string | null; +} +interface OldDb { + meta?: { title?: string }; + items?: OldItem[]; +} + +type OldStatus = 'open' | 'doing' | 'done' | 'accepted'; +const OLD_STATUS_LABEL: Record<OldStatus, string> = { + open: '待开始', + doing: '开发中', + done: '待验收', + accepted: '已验收', +}; + +interface ImportStats { + total: number; + byComplexity: Record<Complexity, number>; + done: number; + skipped: number; + warnings: string[]; +} + +function oldStatusOf(raw: string | undefined, doneFlag: boolean | undefined): OldStatus { + if (raw === 'open' || raw === 'doing' || raw === 'done' || raw === 'accepted') return raw; + return doneFlag ? 'accepted' : 'open'; +} + +function complexityOfTier(tier: number | null | undefined, label: string, stats: ImportStats): Complexity { + const c = tier == null ? undefined : TIER_TO_COMPLEXITY[tier]; + if (c) return c; + stats.warnings.push(`${label} 缺少有效 tier(${tier ?? '无'}),按 medium 导入`); + return 'medium'; +} + +function priorityOfLevel(level: string | null | undefined): number { + if (level === 'high') return 2; + if (level === 'mid') return 1; + return 0; +} + +/** 旧 item 的描述性字段合成产出内容(plan/spec/operations) */ +function buildImportNote(item: OldItem, st: OldStatus, extra: string[] = []): string { + const lines = [`【从旧 todo 导入】原 #${item.id} · 原状态:${st}(${OLD_STATUS_LABEL[st]})`]; + if (st === 'doing') lines.push('注:导入前处于「开发中」,导入后回到初始状态,需按新流程重新推进。'); + for (const l of extra) lines.push(l); + if (item.desc) lines.push('', item.desc); + if (item.tags?.length) lines.push('', `标签:${item.tags.join(' / ')}`); + if (item.level) lines.push(`原重要度:${item.level}`); + if (item.gate && (item.gate.note || item.gate.ref)) { + const ref = item.gate.ref ? `(详见 ${item.gate.ref})` : ''; + lines.push('', `原方案/改动说明(approval=${item.gate.approval ?? '无'}):${item.gate.note ?? ''}${ref}`); + } + if (item.reject_reason) lines.push(`原拒绝原因:${item.reject_reason}`); + if (item.version) lines.push(`原验收版本:${item.version}`); + return lines.join('\n'); +} + +const PRODUCE_PATH: Record<Complexity, 'plan' | 'spec' | 'operations'> = { + hard: 'plan', + medium: 'spec', + easy: 'operations', +}; + +async function setProduce(taskId: string, complexity: Complexity, content: string): Promise<void> { + const field = PRODUCE_PATH[complexity]; + await api('POST', `/api/tasks/${taskId}/${field}`, { [field]: content }); +} + +async function doTransition(taskId: string, to: TaskStatus): Promise<void> { + await api('POST', `/api/tasks/${taskId}/transition`, { to }); +} + +async function doAccept(taskId: string): Promise<void> { + await api('POST', `/api/tasks/${taskId}/decide`, { action: 'accept', actor: 'importer' }); +} + +/** + * 把非 Hard 容器任务沿合法路径推到 done: + * easy: ready → queued → executing → exec_review → accept(done) + * medium: speccing → spec_review → accept(ready) → queued → executing → exec_review → accept(done) + * 推不动时记 warning、保留现状,返回 false。 + */ +async function pushLeafToDone(taskId: string, complexity: Complexity, label: string, stats: ImportStats): Promise<boolean> { + try { + if (complexity === 'medium') { + await doTransition(taskId, 'spec_review'); + await doAccept(taskId); // → ready + } + await doTransition(taskId, 'queued'); + await doTransition(taskId, 'executing'); + await doTransition(taskId, 'exec_review'); + await doAccept(taskId); // → done + stats.done += 1; + return true; + } catch (e) { + stats.warnings.push(`${label} 推进 done 失败:${(e as Error).message}(已保留当前状态)`); + return false; + } +} + +/** + * Hard 容器推进:analyzing → plan_review → accept(decomposed)。 + * 子任务全部 done(或无子任务,已在 plan 注明)时再 decomposed → done。 + */ +async function pushHardContainer( + taskId: string, + label: string, + hasChildren: boolean, + allChildrenDone: boolean, + stats: ImportStats, +): Promise<void> { + try { + await doTransition(taskId, 'plan_review'); + await doAccept(taskId); // → decomposed + if (!hasChildren || allChildrenDone) { + await doTransition(taskId, 'done'); + stats.done += 1; + } else { + stats.warnings.push(`${label} 原状态为已完成,但子任务未能全部推到 done,容器停在 decomposed`); + } + } catch (e) { + stats.warnings.push(`${label} 推进 done 失败:${(e as Error).message}(已保留当前状态)`); + } +} + +async function importItem(projectId: string, item: OldItem, stats: ImportStats): Promise<void> { + const label = `旧 #${item.id}「${item.title ?? '?'}」`; + if (!item.title) { + stats.skipped += 1; + stats.warnings.push(`旧 #${item.id} 缺少 title,已跳过`); + return; + } + const st = oldStatusOf(item.status, item.done); + const complexity = complexityOfTier(item.tier, label, stats); + const subs = item.subtasks ?? []; + const isDone = st === 'done' || st === 'accepted'; + + // 1) 建父任务 + const task = await api<Task>('POST', `/api/projects/${projectId}/tasks`, { + title: item.title, + complexity, + priority: priorityOfLevel(item.level), + }); + stats.total += 1; + stats.byComplexity[complexity] += 1; + + // 2) 写产出字段(hard→plan / medium→spec / easy→operations) + const extra: string[] = []; + if (isDone && complexity === 'hard' && subs.length === 0) { + extra.push('注:原任务已完成且无子任务,导入时容器直接推到 done。'); + } + await setProduce(task.id, complexity, buildImportNote(item, st, extra)); + + // 3) 建子任务(sid 依赖 → 新任务 id) + const sidToId = new Map<string, string>(); + const childPlan: Array<{ id: string; complexity: Complexity; oldStatus: string; label: string }> = []; + for (const sub of subs) { + const subLabel = `旧子任务 ${sub.sid}「${sub.title ?? '?'}」`; + if (!sub.title) { + stats.skipped += 1; + stats.warnings.push(`${subLabel} 缺少 title,已跳过`); + continue; + } + const subComplexity = complexityOfTier(sub.tier, subLabel, stats); + const deps: string[] = []; + for (const dep of sub.deps ?? []) { + const depId = sidToId.get(dep.toUpperCase()); + if (depId) deps.push(depId); + else stats.warnings.push(`${subLabel} 的依赖 ${dep} 未找到对应任务,已忽略该依赖`); + } + const subStatus = sub.status === 'accepted' ? 'done' : (sub.status ?? 'open'); + const child = await api<Task>('POST', `/api/projects/${projectId}/tasks`, { + title: sub.title, + complexity: subComplexity, + parentId: task.id, + deps, + }); + sidToId.set(sub.sid.toUpperCase(), child.id); + stats.total += 1; + stats.byComplexity[subComplexity] += 1; + const subNote = [ + `【从旧 todo 导入】原子任务 ${sub.sid}(父:旧 #${item.id})· 原状态:${subStatus}`, + subStatus === 'doing' ? '注:导入前处于「开发中」,导入后回到初始状态。' : '', + ].filter(Boolean).join('\n'); + await setProduce(child.id, subComplexity, subNote); + childPlan.push({ id: child.id, complexity: subComplexity, oldStatus: subStatus, label: subLabel }); + } + + // 4) 先推子任务,再推父任务 + let allChildrenDone = childPlan.length > 0; + for (const c of childPlan) { + if (c.oldStatus === 'done') { + let ok: boolean; + if (c.complexity === 'hard') { + // 已完成的 hard 子任务(无下级)按容器路径收口 + await pushHardContainer(c.id, c.label, false, true, stats); + const cur = await api<Task>('GET', `/api/tasks/${c.id}`); + ok = cur.status === 'done'; + } else { + ok = await pushLeafToDone(c.id, c.complexity, c.label, stats); + } + if (!ok) allChildrenDone = false; + } else { + allChildrenDone = false; // open/doing:建完即停 + } + } + + if (!isDone) return; // open / doing:建完即停(doing 已在产出里备注) + + if (complexity === 'hard') { + await pushHardContainer(task.id, label, childPlan.length > 0, allChildrenDone, stats); + } else { + await pushLeafToDone(task.id, complexity, label, stats); + } +} + +async function cmdImportTodo(rest: string[]): Promise<void> { + const { values, positionals } = parseCmdArgs(rest, { + repo: { type: 'string' }, + name: { type: 'string' }, + }); + const file = positionals[0]; + if (!file || !values.repo) { + fail('用法:maestro import-todo <todo.json 路径> --repo <仓库路径> [--name 项目名]'); + } + const filePath = resolve(file); + let db: OldDb; + try { + db = JSON.parse(readFileSync(filePath, 'utf8')) as OldDb; + } catch (e) { + fail(`读取 ${filePath} 失败:${(e as Error).message}`); + } + const items = db.items ?? []; + if (!Array.isArray(items)) fail(`${filePath} 不是合法的旧 todo.json(缺少 items 数组)`); + + const repoPath = resolve(values.repo as string); + const name = (values.name as string | undefined) ?? basename(repoPath); + const project = await api<Project>('POST', '/api/projects', { name, repoPath }); + console.log(`已创建项目「${project.name}」(${project.id}),开始导入 ${items.length} 条旧任务…\n`); + + const stats: ImportStats = { + total: 0, + byComplexity: { hard: 0, medium: 0, easy: 0 }, + done: 0, + skipped: 0, + warnings: [], + }; + for (const item of items) { + await importItem(project.id, item, stats); + } + + console.log('导入完成:'); + console.log( + ` 共导入 ${stats.total} 条任务(hard ${stats.byComplexity.hard} / medium ${stats.byComplexity.medium} / easy ${stats.byComplexity.easy})`, + ); + console.log(` 推到 done:${stats.done} 条 · 跳过:${stats.skipped} 条 · 警告:${stats.warnings.length} 条`); + for (const w of stats.warnings) console.log(` ⚠ ${w}`); + console.log(`\n查看结果:maestro task list ${project.name}`); +} + +// ---------- 入口 ---------- + +async function main(): Promise<void> { + const argv = process.argv.slice(2); + const cmd = argv[0]; + + if (!cmd || cmd === 'help' || cmd === '--help' || cmd === '-h') { + console.log(USAGE); + return; + } + + if (cmd === 'project') { + const sub = argv[1]; + if (sub === 'add') return cmdProjectAdd(argv.slice(2)); + if (sub === 'list') return cmdProjectList(); + fail(`未知子命令:project ${sub ?? ''}\n\n${USAGE}`); + } + if (cmd === 'task') { + const sub = argv[1]; + if (sub === 'list') return cmdTaskList(argv.slice(2)); + if (sub === 'add') return cmdTaskAdd(argv.slice(2)); + fail(`未知子命令:task ${sub ?? ''}\n\n${USAGE}`); + } + if (cmd === 'next') return cmdNext(argv.slice(1)); + if (cmd === 'approvals') return cmdApprovals(argv.slice(1)); + if (cmd === 'import-todo') return cmdImportTodo(argv.slice(1)); + + fail(`未知命令:${cmd}\n\n${USAGE}`); +} + +main().catch((err) => { + if (err instanceof ApiError) fail(err.message); + console.error('执行失败:', err); + process.exit(1); +}); diff --git a/src/daemon/index.ts b/src/daemon/index.ts index eeaa780..a0ca6ff 100644 --- a/src/daemon/index.ts +++ b/src/daemon/index.ts @@ -1,5 +1,6 @@ import { Store } from '../store/index.js'; import { buildServer, attachWebSocket } from '../api/server.js'; +import { registerStatic } from '../api/static.js'; import { loadConfig } from './config.js'; /** maestrod:核心 daemon。Phase 1 = Store + REST/WS API(手动驱动;编排器在 Phase 2 接入)。 */ @@ -8,6 +9,7 @@ async function main(): Promise<void> { const store = new Store(cfg.dbFile); const app = buildServer({ store, logger: true }); + registerStatic(app); // Web 看板(web/ 静态文件) attachWebSocket(app, store); await app.listen({ host: cfg.host, port: cfg.port }); diff --git a/src/mcp/index.ts b/src/mcp/index.ts new file mode 100644 index 0000000..d05ab2f --- /dev/null +++ b/src/mcp/index.ts @@ -0,0 +1,286 @@ +#!/usr/bin/env node +/** + * maestro MCP server(stdio transport)。 + * + * 给任意 Claude Code 会话提供任务读写工具:全部经 REST 调本机 maestrod, + * 自身不碰数据库。base URL 取环境变量 MAESTRO_URL(默认 http://127.0.0.1:4517)。 + * + * 注意:审批(accept/reject)是用户在 Web 看板上的动作,本 server 只提供 + * 只读的 get_pending_approvals,不提供 decide 类工具。 + */ +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; +import { z } from 'zod'; +import { STATUS_LABEL, type TaskStatus } from '../model/status.js'; + +const BASE_URL = process.env.MAESTRO_URL ?? 'http://127.0.0.1:4517'; + +// ---------- REST 薄封装 ---------- + +/** 业务错误(daemon 返回 400 {error}),消息原样透传给调用方。 */ +class ApiError extends Error {} + +async function api<T = unknown>(method: 'GET' | 'POST', path: string, body?: unknown): Promise<T> { + let res: Response; + try { + res = await fetch(`${BASE_URL}${path}`, { + method, + headers: body === undefined ? undefined : { 'content-type': 'application/json' }, + body: body === undefined ? undefined : JSON.stringify(body), + }); + } catch { + throw new ApiError( + `无法连接 maestro daemon(${BASE_URL})。请先启动 maestrod:在 maestro 目录执行 \`npm run dev\`(或设置 MAESTRO_URL 指向运行中的 daemon)。`, + ); + } + const text = await res.text(); + let data: unknown = null; + try { data = text ? JSON.parse(text) : null; } catch { data = text; } + if (!res.ok) { + const msg = (data as { error?: string } | null)?.error; + throw new ApiError(msg ?? `daemon 返回 HTTP ${res.status}: ${text}`); + } + return data as T; +} + +// ---------- 工具结果辅助 ---------- + +interface ToolResult { + content: { type: 'text'; text: string }[]; + isError?: boolean; + [key: string]: unknown; +} + +function ok(data: unknown): ToolResult { + return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] }; +} + +function fail(err: unknown): ToolResult { + const msg = err instanceof Error ? err.message : String(err); + return { content: [{ type: 'text', text: msg }], isError: true }; +} + +/** 包一层:把 ApiError / 网络错误转成 isError 的工具结果(消息原样给调用方)。 */ +async function run(fn: () => Promise<unknown>): Promise<ToolResult> { + try { + return ok(await fn()); + } catch (err) { + return fail(err); + } +} + +// ---------- 共享 schema 片段 ---------- + +const STATUSES = Object.keys(STATUS_LABEL) as [TaskStatus, ...TaskStatus[]]; + +const complexitySchema = z + .enum(['hard', 'medium', 'easy']) + .describe('复杂度:hard(须分析拆解)| medium(须写方案过审)| easy(写明操作即可执行)'); + +const subtaskSchema = z.object({ + title: z.string().min(1).describe('子任务标题'), + complexity: complexitySchema, + deps: z.array(z.string()).optional().describe('依赖的任务 id 列表(须全部 done 才可执行)'), + priority: z.number().int().optional().describe('优先级,数字越大越优先(默认 0)'), +}); + +// ---------- MCP server ---------- + +const server = new McpServer({ name: 'maestro', version: '0.1.0' }); + +server.registerTool( + 'list_projects', + { + title: '列出项目', + description: '列出 maestro 中注册的所有项目(id、名称、repo 路径、自治级别等)。', + inputSchema: {}, + }, + async () => run(() => api('GET', '/api/projects')), +); + +server.registerTool( + 'create_project', + { + title: '创建项目', + description: '在 maestro 注册一个本地 git 项目。name 与 repoPath 必填。', + inputSchema: { + name: z.string().min(1).describe('项目名'), + repoPath: z.string().min(1).describe('本地 git 仓库绝对路径'), + defaultBranch: z.string().optional().describe('默认分支(默认 main)'), + verifyCmd: z.string().optional().describe('验证命令(执行后跑,如 npm test)'), + autonomy: z.enum(['manual', 'auto-easy', 'auto-approved']).optional() + .describe('自治级别:manual(全手动)| auto-easy(Easy 自动执行)| auto-approved(过审即自动执行)'), + model: z.string().optional().describe('执行 agent 使用的模型'), + concurrency: z.number().int().min(1).optional().describe('worker 并发数(默认 1)'), + }, + }, + async (args) => run(() => api('POST', '/api/projects', args)), +); + +server.registerTool( + 'list_tasks', + { + title: '列出任务', + description: '列出某项目的全部任务(含层级、复杂度、状态、依赖与审批历史)。', + inputSchema: { projectId: z.string().min(1).describe('项目 id(prj_ 开头)') }, + }, + async ({ projectId }) => run(() => api('GET', `/api/projects/${encodeURIComponent(projectId)}/tasks`)), +); + +server.registerTool( + 'get_task', + { + title: '查看任务详情', + description: '取单个任务的完整详情:字段(plan/spec/operations/result)、审批历史(approvals)以及全部子任务(children)。', + inputSchema: { taskId: z.string().min(1).describe('任务 id(tsk_ 开头)') }, + }, + async ({ taskId }) => + run(async () => { + const tid = encodeURIComponent(taskId); + const task = await api('GET', `/api/tasks/${tid}`); + const children = await api('GET', `/api/tasks/${tid}/children`); + return { task, children }; + }), +); + +server.registerTool( + 'create_task', + { + title: '创建任务', + description: + '在项目下创建任务。初始状态由复杂度决定:hard→analyzing(须先分析拆解)、medium→speccing(须先写方案)、easy→ready(写明 operations 后即可执行)。', + inputSchema: { + projectId: z.string().min(1).describe('项目 id'), + title: z.string().min(1).describe('任务标题'), + complexity: complexitySchema, + parentId: z.string().optional().describe('父任务 id(建子任务时填,最多 3-4 层)'), + deps: z.array(z.string()).optional().describe('依赖的任务 id 列表'), + priority: z.number().int().optional().describe('优先级,数字越大越优先(默认 0)'), + }, + }, + async ({ projectId, ...rest }) => + run(() => api('POST', `/api/projects/${encodeURIComponent(projectId)}/tasks`, rest)), +); + +server.registerTool( + 'decompose_task', + { + title: '拆解任务', + description: + '把一个任务批量拆解为子任务(常用于 Hard 任务 analyzing 阶段的产出)。逐个创建,返回每条的创建结果;某条失败不影响其余(结果里带 error)。拆完通常还需 write_plan 写入分析、update_status 到 plan_review 等用户审批。', + inputSchema: { + taskId: z.string().min(1).describe('要拆解的父任务 id'), + subtasks: z.array(subtaskSchema).min(1).describe('子任务列表,各自带复杂度'), + }, + }, + async ({ taskId, subtasks }) => + run(async () => { + const parent = await api<{ projectId: string }>('GET', `/api/tasks/${encodeURIComponent(taskId)}`); + const results: unknown[] = []; + for (const st of subtasks) { + try { + const created = await api('POST', `/api/projects/${encodeURIComponent(parent.projectId)}/tasks`, { + ...st, + parentId: taskId, + }); + results.push({ ok: true, task: created }); + } catch (err) { + results.push({ ok: false, title: st.title, error: err instanceof Error ? err.message : String(err) }); + } + } + return { parentId: taskId, created: results }; + }), +); + +server.registerTool( + 'write_plan', + { + title: '写入分析拆解(plan)', + description: 'Hard 任务的产出:写入「分析 + 拆解说明」。写完用 update_status 把任务转到 plan_review 等用户审批。', + inputSchema: { + taskId: z.string().min(1).describe('任务 id'), + content: z.string().min(1).describe('plan 内容(Markdown)'), + }, + }, + async ({ taskId, content }) => + run(() => api('POST', `/api/tasks/${encodeURIComponent(taskId)}/plan`, { plan: content })), +); + +server.registerTool( + 'write_spec', + { + title: '写入方案(spec)', + description: 'Medium 任务的产出:写入「具体方案 = 改动内容 + 为什么这么做」。写完用 update_status 转到 spec_review 等用户审批。', + inputSchema: { + taskId: z.string().min(1).describe('任务 id'), + content: z.string().min(1).describe('spec 内容(Markdown)'), + }, + }, + async ({ taskId, content }) => + run(() => api('POST', `/api/tasks/${encodeURIComponent(taskId)}/spec`, { spec: content })), +); + +server.registerTool( + 'write_operations', + { + title: '写入操作记录(operations)', + description: 'Easy 任务的产出:写清「将执行的操作」。Easy 无前置审批闸,写完即可 update_status 到 ready。', + inputSchema: { + taskId: z.string().min(1).describe('任务 id'), + content: z.string().min(1).describe('operations 内容(Markdown)'), + }, + }, + async ({ taskId, content }) => + run(() => api('POST', `/api/tasks/${encodeURIComponent(taskId)}/operations`, { operations: content })), +); + +server.registerTool( + 'update_status', + { + title: '变更任务状态', + description: + `按状态机流转任务状态(合法流转见 daemon 守卫;非法流转会返回中文错误,原样透传)。可选值:${STATUSES.join(' | ')}。注意:plan_review/spec_review/exec_review 的 accept/reject 是用户在看板上的动作,不要用本工具绕过审批闸。`, + inputSchema: { + taskId: z.string().min(1).describe('任务 id'), + to: z.enum(STATUSES).describe('目标状态'), + }, + }, + async ({ taskId, to }) => + run(() => api('POST', `/api/tasks/${encodeURIComponent(taskId)}/transition`, { to })), +); + +server.registerTool( + 'get_next_executable', + { + title: '取下一个可执行任务', + description: '取项目中下一个可执行任务(叶子、ready、依赖全部 done,按优先级排序);没有则返回 {"next": null}。', + inputSchema: { projectId: z.string().min(1).describe('项目 id') }, + }, + async ({ projectId }) => run(() => api('GET', `/api/projects/${encodeURIComponent(projectId)}/next`)), +); + +server.registerTool( + 'get_pending_approvals', + { + title: '查看待审批任务(只读)', + description: + '列出处于审批闸(plan_review / spec_review / exec_review)的任务,可按项目过滤。只读:accept/reject 是用户在 Web 看板上的动作,本 MCP 不提供审批工具——发现待审批项请提醒用户去看板处理。', + inputSchema: { projectId: z.string().optional().describe('项目 id(不填则跨全部项目)') }, + }, + async ({ projectId }) => + run(() => api('GET', projectId ? `/api/approvals?projectId=${encodeURIComponent(projectId)}` : '/api/approvals')), +); + +// ---------- 启动 ---------- + +async function main(): Promise<void> { + const transport = new StdioServerTransport(); + await server.connect(transport); + // stdio 模式下 stdout 是协议通道,日志只能走 stderr + console.error(`maestro-mcp 就绪 · daemon=${BASE_URL}`); +} + +main().catch((err) => { + console.error('maestro-mcp 启动失败:', err); + process.exit(1); +}); diff --git a/web/app.js b/web/app.js new file mode 100644 index 0000000..0158cc5 --- /dev/null +++ b/web/app.js @@ -0,0 +1,615 @@ +/* ════════════════════════════════════════════════════════════ + MAESTRO 调度台 · 前端逻辑(无框架,直连 daemon REST + WS) + ════════════════════════════════════════════════════════════ */ +'use strict'; + +// ── 模型常量(与 src/model/status.ts / complexity.ts 对齐) ── +const STATUS_LABEL = { + init: '新建', analyzing: '分析拆解中', plan_review: '待确认拆解', + decomposed: '已拆解', speccing: '写方案中', spec_review: '待确认方案', + ready: '可执行', blocked: '被依赖阻塞', queued: '排队中', + executing: '执行中', exec_review: '待审/合', failed: '失败', + needs_attention: '需人工', done: '完成', paused: '暂停', cancelled: '取消', +}; +const STATUS_GROUP = { + init: 'idle', analyzing: 'work', speccing: 'work', + plan_review: 'gate', spec_review: 'gate', exec_review: 'gate', + decomposed: 'container', ready: 'go', queued: 'go', executing: 'run', + blocked: 'hold', paused: 'hold', failed: 'bad', needs_attention: 'bad', + done: 'done', cancelled: 'dead', +}; +const GATE_OF = { plan_review: 'plan', spec_review: 'spec', exec_review: 'exec' }; +const GATE_LABEL = { plan: '拆解评审', spec: '方案评审', exec: '结果评审' }; +const CPLX_LABEL = { hard: 'HARD', medium: 'MED', easy: 'EASY' }; +const EVENT_LABEL = { + 'task.created': '任务创建', 'task.updated': '任务更新', 'status.changed': '状态变更', + 'approval.requested': '请求审批', 'approval.granted': '审批通过', 'approval.rejected': '审批驳回', + 'run.started': '运行开始', 'run.finished': '运行结束', +}; +const EVENT_CLASS = { + 'task.created': 'ev-created', 'task.updated': 'ev-updated', 'status.changed': 'ev-status', + 'approval.requested': 'ev-gatewait', 'approval.granted': 'ev-approve', + 'approval.rejected': 'ev-rejected', 'run.started': 'ev-run', 'run.finished': 'ev-run', +}; + +// ── 全局状态 ── +const S = { + projects: [], + currentProjectId: null, + tasks: [], + approvals: [], + events: [], // 新→旧 + collapsed: new Set(), + expanded: new Set(), + rejectOpen: new Set(), +}; + +const $ = (sel) => document.querySelector(sel); + +function esc(s) { + return String(s ?? '').replace(/[&<>"']/g, (c) => + ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[c]); +} +function fmtTime(iso) { + try { return new Date(iso).toLocaleTimeString('zh-CN', { hour12: false }); } + catch { return iso; } +} + +// ── API ── +async function api(path, opts = {}) { + let res; + try { + res = await fetch(path, { + headers: { 'Content-Type': 'application/json' }, + ...opts, + }); + } catch (e) { + throw new Error('无法连接 daemon:' + e.message); + } + let data = null; + try { data = await res.json(); } catch { /* 非 JSON 响应 */ } + if (!res.ok) throw new Error((data && data.error) || `HTTP ${res.status}`); + return data; +} + +// ── Toast ── +function toast(msg, kind = 'err') { + const el = document.createElement('div'); + el.className = `toast ${kind}`; + el.textContent = msg; + $('#toastRoot').appendChild(el); + setTimeout(() => { el.classList.add('out'); setTimeout(() => el.remove(), 350); }, 4200); +} + +// ── 数据加载 ── +async function loadProjects() { + S.projects = await api('/api/projects'); + if (!S.currentProjectId && S.projects.length) S.currentProjectId = S.projects[0].id; + if (S.currentProjectId && !S.projects.some((p) => p.id === S.currentProjectId)) { + S.currentProjectId = S.projects.length ? S.projects[0].id : null; + } +} + +async function loadProjectData() { + const pid = S.currentProjectId; + if (!pid) { S.tasks = []; S.events = []; S.approvals = []; return; } + const [tasks, events, approvals] = await Promise.all([ + api(`/api/projects/${pid}/tasks`), + api(`/api/projects/${pid}/events`), + api(`/api/approvals?projectId=${encodeURIComponent(pid)}`), + ]); + S.tasks = tasks; + S.events = events.slice().reverse(); // 接口升序 → 展示新在前 + S.approvals = approvals; +} + +async function refresh() { + try { + await loadProjectData(); + renderAll(); + } catch (e) { toast(e.message); } +} + +async function fullRefresh() { + try { + await loadProjects(); + await loadProjectData(); + renderAll(); + } catch (e) { toast(e.message); } +} + +// ── 渲染:草稿保护(重绘前保存输入框内容与焦点) ── +function snapshotDrafts() { + const map = new Map(); + document.querySelectorAll('textarea[id], input[id][type=text]').forEach((el) => { + if (el.value) map.set(el.id, el.value); + }); + const focusId = document.activeElement && document.activeElement.id; + return { map, focusId }; +} +function restoreDrafts(snap) { + snap.map.forEach((v, id) => { + const el = document.getElementById(id); + if (el && !el.value) el.value = v; + }); + if (snap.focusId) { + const el = document.getElementById(snap.focusId); + if (el) { el.focus(); try { el.selectionStart = el.value.length; } catch { /* 非文本 */ } } + } +} + +function renderAll() { + const snap = snapshotDrafts(); + renderSidebar(); + renderTopbar(); + renderGates(); + renderTree(); + renderEvents(); + renderParentOptions(); + restoreDrafts(snap); +} + +// ── 渲染:侧栏 ── +function renderSidebar() { + const ul = $('#projectList'); + if (!S.projects.length) { + ul.innerHTML = `<li class="p-empty">暂无项目<br>点「+ 新建」登记第一个仓库</li>`; + return; + } + ul.innerHTML = S.projects.map((p) => ` + <li class="${p.id === S.currentProjectId ? 'active' : ''}" data-action="select-project" data-id="${esc(p.id)}"> + <span class="p-name">${esc(p.name)}</span> + <span class="p-path">${esc(p.repoPath)}</span> + </li>`).join(''); +} + +function renderTopbar() { + const p = S.projects.find((x) => x.id === S.currentProjectId); + $('#projTitle').textContent = p ? p.name : '未选择项目'; + $('#projMeta').textContent = p + ? `${p.repoPath} · 分支 ${p.defaultBranch} · ${p.status === 'active' ? '活跃' : '暂停'} · 任务 ${S.tasks.length}` + : ''; + const gc = $('#gateCount'); + if (S.approvals.length) { + gc.hidden = false; + gc.textContent = `⚠ ${S.approvals.length} 项待审批`; + } else gc.hidden = true; +} + +// ── 渲染:审批闸 ── +function gateDocs(t) { + const gate = GATE_OF[t.status]; + const blocks = []; + const doc = (label, text) => blocks.push( + `<div class="gate-doc-label">${label}</div>` + + (text ? `<pre class="doc">${esc(text)}</pre>` : `<pre class="doc empty">(未填写)</pre>`)); + if (gate === 'plan') doc('PLAN · 分析与拆解', t.plan); + if (gate === 'spec') doc('SPEC · 改动方案', t.spec); + if (gate === 'exec') { + if (t.result) { + const r = t.result; + blocks.push(`<div class="gate-doc-label">RESULT · 执行结果</div> + <dl class="result-kv"> + ${r.branch ? `<dt>分支</dt><dd>${esc(r.branch)}</dd>` : ''} + ${r.worktree ? `<dt>worktree</dt><dd>${esc(r.worktree)}</dd>` : ''} + ${r.prUrl ? `<dt>PR</dt><dd><a href="${esc(r.prUrl)}" target="_blank">${esc(r.prUrl)}</a></dd>` : ''} + </dl>`); + if (r.diffSummary) blocks.push(`<div class="gate-doc-label">DIFF 摘要</div><pre class="doc">${esc(r.diffSummary)}</pre>`); + } else { + blocks.push(`<pre class="doc empty">(无执行结果记录)</pre>`); + } + if (t.operations) doc('OPERATIONS · 执行的操作', t.operations); + else if (t.spec) doc('SPEC · 改动方案', t.spec); + } + return blocks.join(''); +} + +function renderGates() { + const sec = $('#gateSection'); + if (!S.approvals.length) { sec.hidden = true; return; } + sec.hidden = false; + $('#gateList').innerHTML = S.approvals.map((t) => { + const gate = GATE_OF[t.status]; + const rejOpen = S.rejectOpen.has(t.id); + return ` + <div class="gate-card"> + <div class="gate-card-head"> + <span class="gate-kind">${GATE_LABEL[gate]}</span> + <span class="gate-title">${esc(t.title)}</span> + ${cplxBadge(t.complexity)} + ${statusChip(t.status)} + </div> + <div class="gate-body">${gateDocs(t)}</div> + <div class="gate-actions"> + <button class="btn btn-accept" data-action="gate-accept" data-id="${esc(t.id)}">✓ 接受</button> + <button class="btn btn-reject" data-action="gate-reject-toggle" data-id="${esc(t.id)}">✗ 拒绝</button> + ${gate === 'exec' ? `<span class="proj-meta">接受 = 认可改动并标记完成(合并不自动执行)</span>` : ''} + </div> + ${rejOpen ? ` + <div class="reject-form"> + <textarea id="rej-${esc(t.id)}" placeholder="驳回意见(必填)——说明问题与改进方向"></textarea> + <button class="btn btn-reject" data-action="gate-reject-confirm" data-id="${esc(t.id)}">确认驳回</button> + <button class="btn" data-action="gate-reject-toggle" data-id="${esc(t.id)}">收起</button> + </div>` : ''} + </div>`; + }).join(''); +} + +// ── 渲染:任务树 ── +function cplxBadge(c) { + return `<span class="cplx cplx-${esc(c)}">${CPLX_LABEL[c] || esc(c)}</span>`; +} +function statusChip(st) { + return `<span class="chip chip-${STATUS_GROUP[st] || 'idle'}">${STATUS_LABEL[st] || esc(st)}</span>`; +} + +function childrenMap() { + const m = new Map(); + for (const t of S.tasks) { + const key = t.parentId || '__root__'; + if (!m.has(key)) m.set(key, []); + m.get(key).push(t); + } + return m; +} + +function renderTree() { + const root = $('#taskTree'); + if (!S.currentProjectId) { + root.innerHTML = `<div class="tree-empty"><b>NO PROJECT</b>先在左侧新建或选择一个项目</div>`; + return; + } + if (!S.tasks.length) { + root.innerHTML = `<div class="tree-empty"><b>EMPTY</b>还没有任务 —— 点上方「+ 新建任务」开始</div>`; + return; + } + const m = childrenMap(); + const renderNode = (t) => { + const kids = m.get(t.id) || []; + const collapsed = S.collapsed.has(t.id); + const expanded = S.expanded.has(t.id); + const isGate = !!GATE_OF[t.status]; + const caret = kids.length + ? `<span class="caret ${collapsed ? '' : 'open'}" data-action="toggle-collapse" data-id="${esc(t.id)}">▶</span>` + : `<span class="caret leaf">·</span>`; + return ` + <div class="task-node depth-${t.depth}"> + <div class="task-row ${isGate ? 'is-gate' : ''} ${expanded ? 'expanded' : ''}" data-action="toggle-detail" data-id="${esc(t.id)}"> + ${caret} + <span class="t-title">${esc(t.title)}<span class="t-id">${esc(t.id.slice(-6))}</span></span> + <span class="spacer"></span> + <span class="t-prio ${t.priority > 0 ? 'hot' : ''}">P${t.priority}</span> + ${cplxBadge(t.complexity)} + ${statusChip(t.status)} + </div> + ${expanded ? renderDetail(t) : ''} + ${kids.length && !collapsed ? `<div class="task-children">${kids.map(renderNode).join('')}</div>` : ''} + </div>`; + }; + root.innerHTML = (m.get('__root__') || []).map(renderNode).join(''); +} + +// ── 渲染:任务详情 ── +function writableField(t) { + if (t.status === 'analyzing') return { field: 'plan', label: '分析与拆解(plan)', next: 'plan_review' }; + if (t.status === 'speccing') return { field: 'spec', label: '改动方案(spec)', next: 'spec_review' }; + if (t.status === 'ready') return { field: 'operations', label: '将执行的操作(operations)', next: null }; + return null; +} + +function renderDetail(t) { + const parts = []; + + // 已有产出 + const docs = [['plan', 'PLAN · 分析拆解'], ['spec', 'SPEC · 方案'], ['operations', 'OPERATIONS · 操作']]; + for (const [f, label] of docs) { + if (t[f]) parts.push(`<div><div class="detail-label">${label}</div><pre class="doc">${esc(t[f])}</pre></div>`); + } + + // 写产出入口 + const w = writableField(t); + if (w) { + parts.push(` + <div class="write-box"> + <div class="detail-label">填写 ${w.label}</div> + <textarea id="out-${esc(t.id)}" placeholder="多行文本…">${esc(t[w.field] || '')}</textarea> + <div class="form-actions form-row"> + <button class="btn" data-action="save-output" data-id="${esc(t.id)}" data-field="${w.field}">保存${w.field}</button> + ${w.next ? `<button class="btn btn-solid" data-action="submit-review" data-id="${esc(t.id)}" data-to="${w.next}">提交评审 →</button>` : ''} + </div> + </div>`); + } + + // result + if (t.result) { + const r = t.result; + parts.push(` + <div><div class="detail-label">RESULT · 执行结果</div> + <dl class="result-kv"> + <dt>分支</dt><dd>${esc(r.branch || '—')}</dd> + <dt>worktree</dt><dd>${esc(r.worktree || '—')}</dd> + ${r.prUrl ? `<dt>PR</dt><dd>${esc(r.prUrl)}</dd>` : ''} + ${r.commits && r.commits.length ? `<dt>commits</dt><dd>${r.commits.map(esc).join('<br>')}</dd>` : ''} + </dl> + ${r.diffSummary ? `<pre class="doc" style="margin-top:6px">${esc(r.diffSummary)}</pre>` : ''} + </div>`); + } + + // 审批历史 + if (t.approvals && t.approvals.length) { + parts.push(` + <div><div class="detail-label">审批历史</div> + ${t.approvals.map((a) => ` + <div class="approval-item"> + <span class="ap-act-${a.action}">${a.action === 'accept' ? '✓ 通过' : '✗ 驳回'}</span> + <span class="ap-gate">${GATE_LABEL[a.gate] || esc(a.gate)} · ${esc(a.actor)}</span> + <span class="ap-time">${fmtTime(a.at)}</span> + ${a.reason ? `<span class="ap-reason">${esc(a.reason)}</span>` : ''} + </div>`).join('')} + </div>`); + } + + if (!parts.length) parts.push(`<div class="proj-meta">暂无产出与历史 —— 状态:${STATUS_LABEL[t.status]}</div>`); + + parts.push(`<div class="proj-meta">id ${esc(t.id)} · 深度 ${t.depth} · 创建 ${fmtTime(t.createdAt)} · 更新 ${fmtTime(t.updatedAt)}${t.deps && t.deps.length ? ' · 依赖 ' + t.deps.map(esc).join(', ') : ''}</div>`); + + return `<div class="task-detail"><div class="detail-grid">${parts.join('')}</div></div>`; +} + +// ── 渲染:事件流 ── +function eventDetail(e) { + const t = S.tasks.find((x) => x.id === e.taskId); + const name = t ? t.title : (e.taskId ? e.taskId.slice(-6) : ''); + const p = e.payload || {}; + if (e.type === 'status.changed') { + return `${name}:${STATUS_LABEL[p.from] || p.from} → ${STATUS_LABEL[p.to] || p.to}`; + } + if (e.type === 'approval.granted' || e.type === 'approval.rejected') { + const r = p.reason ? `(${p.reason})` : ''; + return `${name} · ${GATE_LABEL[p.gate] || p.gate}${r}`; + } + if (e.type === 'task.created') { + if (p.kind === 'project') return `项目「${p.name || ''}」已登记`; + return `${p.title || name}(${CPLX_LABEL[p.complexity] || ''})`; + } + if (e.type === 'task.updated') return `${name} · 字段 ${p.field || ''}`; + if (e.type === 'run.started' || e.type === 'run.finished') { + return `${name} · ${p.kind || ''} ${p.status || ''}`; + } + return name; +} + +function renderEvents() { + const ul = $('#eventList'); + if (!S.events.length) { + ul.innerHTML = `<li class="ev-empty">暂无事件</li>`; + return; + } + ul.innerHTML = S.events.slice(0, 80).map((e) => ` + <li class="${EVENT_CLASS[e.type] || ''}"> + <div class="ev-head"> + <span class="ev-type">${EVENT_LABEL[e.type] || esc(e.type)}</span> + <span class="ev-time">${fmtTime(e.at)}</span> + </div> + <div class="ev-detail">${eventDetail(e)}</div> + </li>`).join(''); +} + +// ── 渲染:父任务下拉 ── +function renderParentOptions() { + const sel = document.querySelector('#newTaskPanel select[name=parentId]'); + const cur = sel.value; + sel.innerHTML = `<option value="">(顶层)</option>` + + S.tasks.map((t) => `<option value="${esc(t.id)}">${esc('· '.repeat(t.depth - 1) + t.title)}</option>`).join(''); + sel.value = cur; +} + +// ── 动作 ── +async function act(fn, okMsg) { + try { + await fn(); + if (okMsg) toast(okMsg, 'ok'); + await refresh(); + } catch (e) { toast(e.message); } +} + +document.addEventListener('click', (ev) => { + const el = ev.target.closest('[data-action]'); + if (!el) return; + const action = el.dataset.action; + const id = el.dataset.id; + + switch (action) { + case 'select-project': + if (S.currentProjectId !== id) { + S.currentProjectId = id; + S.expanded.clear(); S.collapsed.clear(); S.rejectOpen.clear(); + refresh(); + } + break; + + case 'open-new-project': + $('#modalRoot').hidden = false; + document.querySelector('#newProjectForm input[name=name]').focus(); + break; + case 'close-modal': + $('#modalRoot').hidden = true; + break; + + case 'toggle-new-task': { + const p = $('#newTaskPanel'); + p.hidden = !p.hidden; + if (!p.hidden) p.querySelector('input[name=title]').focus(); + break; + } + + case 'toggle-collapse': + ev.stopPropagation(); + S.collapsed.has(id) ? S.collapsed.delete(id) : S.collapsed.add(id); + renderTree(); + break; + + case 'toggle-detail': { + // 点的是 caret 时由上面分支处理(stopPropagation);输入区内点击不折叠 + if (ev.target.closest('.task-detail')) break; + S.expanded.has(id) ? S.expanded.delete(id) : S.expanded.add(id); + renderTree(); + break; + } + + case 'gate-accept': + act(() => api(`/api/tasks/${id}/decide`, { + method: 'POST', body: JSON.stringify({ action: 'accept' }), + }), '已接受'); + break; + + case 'gate-reject-toggle': + S.rejectOpen.has(id) ? S.rejectOpen.delete(id) : S.rejectOpen.add(id); + renderGates(); + if (S.rejectOpen.has(id)) { + const ta = document.getElementById(`rej-${id}`); + if (ta) ta.focus(); + } + break; + + case 'gate-reject-confirm': { + const ta = document.getElementById(`rej-${id}`); + const reason = ta ? ta.value.trim() : ''; + if (!reason) { toast('驳回意见不能为空'); if (ta) ta.focus(); return; } + S.rejectOpen.delete(id); + act(() => api(`/api/tasks/${id}/decide`, { + method: 'POST', body: JSON.stringify({ action: 'reject', reason }), + }), '已驳回,任务退回返工'); + break; + } + + case 'save-output': { + const field = el.dataset.field; + const ta = document.getElementById(`out-${id}`); + const value = ta ? ta.value.trim() : ''; + if (!value) { toast(`${field} 内容不能为空`); return; } + act(() => api(`/api/tasks/${id}/${field}`, { + method: 'POST', body: JSON.stringify({ [field]: value }), + }), `${field} 已保存`); + break; + } + + case 'submit-review': { + const to = el.dataset.to; + const ta = document.getElementById(`out-${id}`); + const task = S.tasks.find((t) => t.id === id); + const field = task && writableField(task) ? writableField(task).field : null; + const value = ta ? ta.value.trim() : ''; + act(async () => { + // 先保存当前草稿(有内容才保存),再流转进评审闸 + if (field && value) { + await api(`/api/tasks/${id}/${field}`, { + method: 'POST', body: JSON.stringify({ [field]: value }), + }); + } + await api(`/api/tasks/${id}/transition`, { + method: 'POST', body: JSON.stringify({ to }), + }); + }, '已提交评审'); + break; + } + } +}); + +// 阻止详情区域内点击冒泡触发折叠 +document.addEventListener('click', (ev) => { + if (ev.target.closest('.task-detail') && !ev.target.closest('[data-action]')) { + ev.stopPropagation(); + } +}, true); + +// ── 表单提交 ── +$('#newProjectForm').addEventListener('submit', (ev) => { + ev.preventDefault(); + const f = ev.target; + const name = f.name.value.trim(); + const repoPath = f.repoPath.value.trim(); + if (!name || !repoPath) { toast('名称与仓库路径必填'); return; } + const body = { name, repoPath }; + if (f.defaultBranch.value.trim()) body.defaultBranch = f.defaultBranch.value.trim(); + if (f.verifyCmd.value.trim()) body.verifyCmd = f.verifyCmd.value.trim(); + act(async () => { + const p = await api('/api/projects', { method: 'POST', body: JSON.stringify(body) }); + S.currentProjectId = p.id; + f.reset(); + $('#modalRoot').hidden = true; + await loadProjects(); + }, `项目「${name}」已创建`); +}); + +$('#newTaskPanel').addEventListener('submit', (ev) => { + ev.preventDefault(); + const f = ev.target; + if (!S.currentProjectId) { toast('请先选择项目'); return; } + const title = f.title.value.trim(); + if (!title) { toast('标题必填'); return; } + const body = { + title, + complexity: f.complexity.value, + priority: Number(f.priority.value || 0), + }; + if (f.parentId.value) body.parentId = f.parentId.value; + act(async () => { + await api(`/api/projects/${S.currentProjectId}/tasks`, { + method: 'POST', body: JSON.stringify(body), + }); + f.title.value = ''; + }, '任务已创建'); +}); + +// Esc 关模态 +document.addEventListener('keydown', (ev) => { + if (ev.key === 'Escape') $('#modalRoot').hidden = true; +}); + +// ── WebSocket 实时刷新(指数退避重连) ── +let wsAttempt = 0; +let refreshTimer = null; + +function setWsState(on, text) { + $('#wsDot').className = `ws-dot ${on ? 'on' : 'off'}`; + $('#wsText').textContent = text; +} + +function scheduleRefresh() { + if (refreshTimer) return; + refreshTimer = setTimeout(() => { refreshTimer = null; refresh(); }, 200); +} + +function connectWs() { + const proto = location.protocol === 'https:' ? 'wss' : 'ws'; + const ws = new WebSocket(`${proto}://${location.host}/ws`); + + ws.onopen = () => { + wsAttempt = 0; + setWsState(true, '实时连接'); + fullRefresh(); // 重连后补齐错过的状态 + }; + + ws.onmessage = (msg) => { + let evt; + try { evt = JSON.parse(msg.data); } catch { return; } + if (evt.projectId === S.currentProjectId) { + scheduleRefresh(); + } else if (evt.type === 'task.created' && evt.payload && evt.payload.kind === 'project') { + loadProjects().then(renderSidebar).catch(() => {}); + } + }; + + const retry = () => { + wsAttempt += 1; + const delay = Math.min(30000, 1000 * Math.pow(2, wsAttempt - 1)); + setWsState(false, `已断开 · ${Math.round(delay / 1000)}s 后重连`); + setTimeout(connectWs, delay); + }; + ws.onclose = retry; + ws.onerror = () => { try { ws.close(); } catch { /* noop */ } }; +} + +// ── 启动 ── +fullRefresh().then(connectWs); diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..4fc63d7 --- /dev/null +++ b/web/index.html @@ -0,0 +1,122 @@ +<!doctype html> +<html lang="zh-CN"> +<head> +<meta charset="utf-8"> +<meta name="viewport" content="width=device-width, initial-scale=1"> +<title>MAESTRO · 任务调度台 + + + + + + +
+ + + + + +
+
+
+
+
+
+
+ +
+ + + +
+
+ 任务树 + +
+ + + +
+
+
+ + + +
+ + + + +
+ + + diff --git a/web/style.css b/web/style.css new file mode 100644 index 0000000..bdec40d --- /dev/null +++ b/web/style.css @@ -0,0 +1,443 @@ +/* ════════════════════════════════════════════════════════════════ + MAESTRO 调度台 · phosphor console + 碳绿底 + 磷光绿/琥珀/信号红,等宽中文混排,扫描线质感 + ════════════════════════════════════════════════════════════════ */ + +:root { + --bg: #0a0d0b; + --bg-deep: #070908; + --panel: #10140f; + --panel-2: #151b14; + --line: #232d23; + --line-soft: #1a221a; + --ink: #d8e4d4; + --muted: #76876f; + --faint: #4a5747; + + --green: #5fdd7d; + --green-dim: #2e6b3d; + --amber: #f0b429; + --amber-dim: #6b5414; + --red: #ff5d5d; + --red-dim: #6b2424; + --cyan: #59c8d8; + --cyan-dim: #1e4e57; + + --mono: 'IBM Plex Mono', 'Noto Sans SC', ui-monospace, monospace; +} + +* { box-sizing: border-box; margin: 0; padding: 0; } + +html, body { height: 100%; } + +body { + background: var(--bg); + color: var(--ink); + font-family: var(--mono); + font-size: 13px; + line-height: 1.55; + overflow: hidden; +} + +/* 扫描线 + 暗角 氛围层 */ +body::before { + content: ''; + position: fixed; inset: 0; z-index: 999; pointer-events: none; + background: + repeating-linear-gradient(0deg, rgba(0,0,0,.16) 0 1px, transparent 1px 3px), + radial-gradient(ellipse at 50% 40%, transparent 55%, rgba(0,0,0,.5)); + opacity: .5; +} + +::selection { background: var(--green-dim); color: #fff; } + +/* 滚动条 */ +::-webkit-scrollbar { width: 8px; height: 8px; } +::-webkit-scrollbar-thumb { background: var(--line); border-radius: 0; } +::-webkit-scrollbar-thumb:hover { background: var(--green-dim); } + +/* ── 三栏骨架 ─────────────────────────────────────────────── */ +#app { + display: grid; + grid-template-columns: 232px minmax(0,1fr) 320px; + height: 100vh; +} + +#sidebar { + background: var(--bg-deep); + border-right: 1px solid var(--line); + display: flex; flex-direction: column; + overflow-y: auto; +} + +#main { overflow-y: auto; padding: 0 22px 60px; } + +#eventPanel { + background: var(--bg-deep); + border-left: 1px solid var(--line); + overflow-y: auto; + padding-bottom: 30px; +} + +@media (max-width: 1100px) { + body { overflow: auto; } + #app { grid-template-columns: 1fr; height: auto; } + #sidebar, #eventPanel { border: none; border-bottom: 1px solid var(--line); } +} + +/* ── Logo ────────────────────────────────────────────────── */ +.logo { padding: 20px 16px 14px; border-bottom: 1px solid var(--line-soft); } +.logo-word { + font-size: 19px; font-weight: 700; letter-spacing: .28em; + color: var(--green); + text-shadow: 0 0 12px rgba(95,221,125,.45); +} +.cursor { animation: blink 1.1s steps(1) infinite; margin-left: 1px; } +@keyframes blink { 50% { opacity: 0; } } +.logo-sub { margin-top: 3px; font-size: 11px; color: var(--muted); letter-spacing: .35em; } + +/* ── 区块标题 ─────────────────────────────────────────────── */ +.sec-head { + display: flex; align-items: center; gap: 6px; + font-size: 11px; font-weight: 600; letter-spacing: .22em; + color: var(--muted); text-transform: uppercase; + padding: 18px 0 10px; + position: sticky; top: 0; z-index: 5; + background: linear-gradient(var(--bg) 75%, transparent); +} +#sidebar .sec-head, .side-head { + display: flex; align-items: center; gap: 6px; + font-size: 11px; font-weight: 600; letter-spacing: .22em; color: var(--muted); + padding: 16px 16px 8px; background: none; position: static; +} +#eventPanel .sec-head { padding: 18px 16px 10px; background: linear-gradient(var(--bg-deep) 75%, transparent); } +.head-mark { color: var(--green); } +.head-mark.amber { color: var(--amber); } +.side-head .btn, .sec-head .btn { margin-left: auto; letter-spacing: .1em; } + +/* ── 项目列表 ─────────────────────────────────────────────── */ +.project-list { list-style: none; flex: 1; } +.project-list li { + padding: 9px 16px 9px 14px; + border-left: 2px solid transparent; + cursor: pointer; + display: flex; flex-direction: column; gap: 1px; +} +.project-list li:hover { background: var(--panel); } +.project-list li.active { + background: var(--panel-2); + border-left-color: var(--green); +} +.project-list li.active .p-name { color: var(--green); } +.p-name { font-weight: 600; font-size: 13px; } +.p-path { font-size: 10.5px; color: var(--faint); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.p-empty { padding: 14px 16px; color: var(--faint); font-size: 12px; } + +.side-foot { + border-top: 1px solid var(--line-soft); + padding: 10px 16px; font-size: 11px; color: var(--muted); + display: flex; align-items: center; gap: 7px; +} +.ws-dot { width: 7px; height: 7px; border-radius: 50%; display: inline-block; } +.ws-dot.on { background: var(--green); box-shadow: 0 0 8px var(--green); } +.ws-dot.off { background: var(--amber); animation: pulse 1s infinite; } +@keyframes pulse { 50% { opacity: .25; } } + +/* ── 顶栏 ────────────────────────────────────────────────── */ +#topbar { + display: flex; align-items: flex-end; gap: 14px; + padding: 22px 0 14px; + border-bottom: 1px solid var(--line); +} +.proj-title { font-size: 20px; font-weight: 700; letter-spacing: .04em; } +.proj-meta { font-size: 11px; color: var(--muted); margin-top: 2px; } +.spacer { flex: 1; } +.gate-count { + font-size: 11px; letter-spacing: .12em; color: var(--amber); + border: 1px solid var(--amber-dim); padding: 4px 10px; + animation: pulse 2.2s infinite; +} + +/* ── 审批闸 ─────────────────────────────────────────────── */ +.gate-section { margin-top: 18px; } +.gate-stripe { + height: 5px; + background: repeating-linear-gradient(-45deg, + var(--amber) 0 9px, transparent 9px 18px); + opacity: .8; +} +.gate-head { padding-top: 12px; position: static; background: none; color: var(--amber); } + +.gate-card { + background: var(--panel); + border: 1px solid var(--amber-dim); + margin-bottom: 12px; + animation: rise .25s ease both; +} +@keyframes rise { from { opacity: 0; transform: translateY(5px); } } + +.gate-card-head { + display: flex; align-items: center; gap: 10px; flex-wrap: wrap; + padding: 10px 14px; + border-bottom: 1px solid var(--line-soft); +} +.gate-kind { + font-size: 10.5px; font-weight: 700; letter-spacing: .18em; + color: var(--bg); background: var(--amber); + padding: 2px 8px; +} +.gate-title { font-weight: 600; } +.gate-body { padding: 12px 14px; } +.gate-doc-label { font-size: 10.5px; color: var(--muted); letter-spacing: .18em; margin: 8px 0 4px; } +.gate-doc-label:first-child { margin-top: 0; } + +pre.doc { + background: var(--bg-deep); + border: 1px solid var(--line-soft); + border-left: 2px solid var(--green-dim); + padding: 10px 12px; + font-family: var(--mono); font-size: 12.5px; + white-space: pre-wrap; word-break: break-word; + max-height: 280px; overflow-y: auto; + color: var(--ink); +} +pre.doc.empty { color: var(--faint); border-left-color: var(--line); } + +.gate-actions { + display: flex; gap: 10px; align-items: center; + padding: 10px 14px; + border-top: 1px solid var(--line-soft); +} + +.reject-form { padding: 0 14px 12px; display: flex; gap: 10px; align-items: flex-start; } +.reject-form textarea { + flex: 1; min-height: 56px; + border-color: var(--red-dim); +} +.reject-form textarea:focus { border-color: var(--red); } + +/* ── 按钮 ────────────────────────────────────────────────── */ +.btn { + font-family: var(--mono); font-size: 12px; font-weight: 600; + letter-spacing: .08em; + background: transparent; color: var(--ink); + border: 1px solid var(--line); + padding: 6px 14px; cursor: pointer; + transition: all .12s; +} +.btn:hover { border-color: var(--muted); background: var(--panel-2); } +.btn-xs { padding: 2px 8px; font-size: 11px; } +.btn-ghost { border-color: transparent; color: var(--muted); } +.btn-ghost:hover { color: var(--green); border-color: var(--green-dim); background: transparent; } + +.btn-solid { + background: var(--green); color: var(--bg-deep); border-color: var(--green); +} +.btn-solid:hover { background: #79ec94; border-color: #79ec94; box-shadow: 0 0 14px rgba(95,221,125,.35); } + +.btn-accept { border-color: var(--green-dim); color: var(--green); } +.btn-accept:hover { background: var(--green); color: var(--bg-deep); border-color: var(--green); box-shadow: 0 0 14px rgba(95,221,125,.3); } +.btn-reject { border-color: var(--red-dim); color: var(--red); } +.btn-reject:hover { background: var(--red); color: var(--bg-deep); border-color: var(--red); box-shadow: 0 0 14px rgba(255,93,93,.3); } + +.btn:disabled { opacity: .4; cursor: not-allowed; } + +/* ── 表单 ────────────────────────────────────────────────── */ +.panel { background: var(--panel); border: 1px solid var(--line); } +.form-panel { padding: 14px; margin-bottom: 14px; } +.form-row { display: flex; gap: 14px; align-items: flex-end; flex-wrap: wrap; } +.form-row + .form-row { margin-top: 12px; } +.form-actions { justify-content: flex-end; } + +label { display: flex; flex-direction: column; gap: 4px; font-size: 11px; color: var(--muted); letter-spacing: .08em; } +label.grow { flex: 1; } +label.narrow input { width: 76px; } +.req { color: var(--red); } + +input[type=text], input[type=number], select, textarea { + font-family: var(--mono); font-size: 13px; + background: var(--bg-deep); color: var(--ink); + border: 1px solid var(--line); + padding: 7px 10px; + outline: none; + transition: border-color .12s; +} +input:focus, select:focus, textarea:focus { border-color: var(--green-dim); box-shadow: 0 0 0 1px var(--green-dim); } +textarea { resize: vertical; min-height: 72px; width: 100%; } + +/* 复杂度分段选择 */ +.seg { display: inline-flex; border: 1px solid var(--line); } +.seg input { position: absolute; opacity: 0; pointer-events: none; } +.seg label { + padding: 7px 13px; cursor: pointer; + font-size: 11px; font-weight: 700; letter-spacing: .12em; + color: var(--faint); flex-direction: row; +} +.seg label + input + label, .seg input + label { border-left: 0; } +.seg label:not(:first-of-type) { border-left: 1px solid var(--line); } +.seg input:checked + label.seg-h { background: var(--red-dim); color: var(--red); } +.seg input:checked + label.seg-m { background: var(--amber-dim); color: var(--amber); } +.seg input:checked + label.seg-e { background: var(--green-dim); color: var(--green); } + +/* ── 任务树 ─────────────────────────────────────────────── */ +.tree-empty { + padding: 46px 0; text-align: center; color: var(--faint); + border: 1px dashed var(--line); +} +.tree-empty b { display: block; font-size: 15px; color: var(--muted); margin-bottom: 6px; letter-spacing: .2em; } + +.task-node { border-left: 1px solid var(--line-soft); } +.task-node.depth-1 { border-left: none; } + +.task-row { + display: flex; align-items: center; gap: 8px; + padding: 8px 10px 8px 6px; + border-bottom: 1px solid var(--line-soft); + cursor: pointer; + transition: background .1s; +} +.task-row:hover { background: var(--panel); } +.task-row.is-gate { background: rgba(240,180,41,.045); } +.task-row.expanded { background: var(--panel-2); } + +.caret { + width: 16px; flex: none; text-align: center; + color: var(--muted); font-size: 10px; + transition: transform .12s; user-select: none; +} +.caret.open { transform: rotate(90deg); color: var(--green); } +.caret.leaf { color: var(--faint); } + +.t-title { font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.t-title .t-id { color: var(--faint); font-size: 10.5px; margin-left: 6px; letter-spacing: 0; } +.task-row .spacer { flex: 1; min-width: 8px; } + +.t-prio { font-size: 10.5px; color: var(--faint); flex: none; } +.t-prio.hot { color: var(--amber); } + +/* 复杂度徽章 */ +.cplx { + flex: none; font-size: 10px; font-weight: 700; letter-spacing: .14em; + padding: 1px 7px; border: 1px solid; +} +.cplx-hard { color: var(--red); border-color: var(--red-dim); background: rgba(255,93,93,.07); } +.cplx-medium { color: var(--amber); border-color: var(--amber-dim); background: rgba(240,180,41,.07); } +.cplx-easy { color: var(--green); border-color: var(--green-dim); background: rgba(95,221,125,.07); } + +/* 状态 chip */ +.chip { + flex: none; display: inline-flex; align-items: center; gap: 5px; + font-size: 11px; padding: 1px 8px; + border: 1px solid var(--line); color: var(--muted); + white-space: nowrap; +} +.chip::before { content: ''; width: 6px; height: 6px; border-radius: 50%; background: currentColor; } +.chip-idle { color: var(--muted); } +.chip-work { color: var(--cyan); border-color: var(--cyan-dim); } +.chip-gate { color: var(--amber); border-color: var(--amber-dim); } +.chip-gate::before { animation: pulse 1.4s infinite; } +.chip-container { color: #9bb4c8; border-color: #2c3c4a; } +.chip-go { color: var(--green); border-color: var(--green-dim); } +.chip-run { color: var(--cyan); border-color: var(--cyan); box-shadow: 0 0 10px rgba(89,200,216,.25); } +.chip-run::before { animation: pulse .8s infinite; } +.chip-hold { color: var(--faint); } +.chip-bad { color: var(--red); border-color: var(--red-dim); } +.chip-bad::before { animation: pulse 1s infinite; } +.chip-done { color: var(--bg-deep); background: var(--green); border-color: var(--green); font-weight: 700; } +.chip-dead { color: var(--faint); text-decoration: line-through; } +.chip-dead::before { background: var(--faint); } + +/* 子节点缩进 */ +.task-children { margin-left: 22px; } + +/* ── 任务详情 ─────────────────────────────────────────────── */ +.task-detail { + background: var(--panel); + border-bottom: 1px solid var(--line); + border-left: 2px solid var(--green-dim); + padding: 14px 16px; + animation: rise .2s ease both; +} +.detail-grid { display: grid; gap: 14px; } +.detail-label { font-size: 10.5px; color: var(--muted); letter-spacing: .18em; margin-bottom: 4px; } + +.approval-item { + display: flex; gap: 10px; align-items: baseline; flex-wrap: wrap; + font-size: 12px; padding: 5px 0; + border-bottom: 1px dashed var(--line-soft); +} +.approval-item:last-child { border-bottom: none; } +.ap-act-accept { color: var(--green); font-weight: 700; } +.ap-act-reject { color: var(--red); font-weight: 700; } +.ap-gate { color: var(--muted); } +.ap-time { color: var(--faint); font-size: 11px; margin-left: auto; } +.ap-reason { width: 100%; color: var(--amber); font-size: 12px; padding-left: 14px; } +.ap-reason::before { content: '↳ '; color: var(--faint); } + +.result-kv { font-size: 12px; display: grid; grid-template-columns: auto 1fr; gap: 3px 12px; } +.result-kv dt { color: var(--muted); } +.result-kv dd { word-break: break-all; } + +.write-box { margin-top: 4px; } +.write-box .form-actions { margin-top: 8px; } +.detail-actions { display: flex; gap: 10px; } + +/* ── 事件流 ─────────────────────────────────────────────── */ +.event-list { list-style: none; padding: 0 14px; } +.event-list li { + padding: 7px 0 7px 12px; + border-bottom: 1px solid var(--line-soft); + position: relative; + font-size: 11.5px; + animation: rise .25s ease both; +} +.event-list li::before { + content: ''; position: absolute; left: 0; top: 13px; + width: 5px; height: 5px; background: var(--ev, var(--muted)); +} +.ev-head { display: flex; gap: 8px; align-items: baseline; } +.ev-type { font-weight: 600; color: var(--ev, var(--ink)); letter-spacing: .04em; } +.ev-time { color: var(--faint); font-size: 10.5px; margin-left: auto; flex: none; } +.ev-detail { color: var(--muted); margin-top: 1px; word-break: break-word; } +.ev-empty { padding: 30px 0; color: var(--faint); text-align: center; border: none !important; } +.ev-empty::before { display: none; } + +li.ev-created { --ev: var(--green); } +li.ev-status { --ev: var(--cyan); } +li.ev-approve { --ev: var(--green); } +li.ev-rejected { --ev: var(--red); } +li.ev-gatewait { --ev: var(--amber); } +li.ev-run { --ev: var(--cyan); } +li.ev-updated { --ev: var(--muted); } + +/* ── 模态 ────────────────────────────────────────────────── */ +.modal-root { position: fixed; inset: 0; z-index: 1000; display: grid; place-items: center; } +.modal-root[hidden] { display: none; } +.modal-mask { position: absolute; inset: 0; background: rgba(4,6,5,.78); backdrop-filter: blur(2px); } +.modal { + position: relative; width: min(460px, 92vw); + padding: 18px 20px 20px; + display: flex; flex-direction: column; gap: 12px; + box-shadow: 0 0 0 1px var(--green-dim), 0 18px 60px rgba(0,0,0,.6); + animation: rise .18s ease both; +} +.modal .sec-head { padding: 0 0 4px; position: static; background: none; } + +/* ── Toast ───────────────────────────────────────────────── */ +.toast-root { + position: fixed; bottom: 18px; left: 50%; transform: translateX(-50%); + z-index: 1200; display: flex; flex-direction: column; gap: 8px; align-items: center; +} +.toast { + font-family: var(--mono); font-size: 12.5px; + background: var(--bg-deep); + border: 1px solid var(--line); + padding: 9px 16px; + max-width: 70vw; + animation: toastIn .18s ease both; + box-shadow: 0 8px 30px rgba(0,0,0,.55); +} +.toast.err { border-color: var(--red); color: var(--red); } +.toast.ok { border-color: var(--green-dim); color: var(--green); } +.toast.out { opacity: 0; transition: opacity .3s; } +@keyframes toastIn { from { opacity: 0; transform: translateY(8px); } }