From 4bd5b03bb1d32000d881fafda3c6830b2daaa1bb Mon Sep 17 00:00:00 2001 From: Hong Jiarong Date: Wed, 8 Jul 2026 01:36:59 +0800 Subject: [PATCH] =?UTF-8?q?refactor(hub):=20Vercel=20AI=20SDK=20=E2=86=92?= =?UTF-8?q?=20Claude=20Code=20SDK?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 用 @anthropic-ai/claude-agent-sdk 的 query() 替换 Vercel AI SDK 的 streamText。SDK 自带 Read/Write/Bash/Glob/Grep 工具——read_file/ write_file/list_files/cph_check/cph_build 全部不需要了,Bash 跑 cph 直接。 UX 同时改成 workbuddy 风格: - 收到 @bot → 表情回复(👍)确认收到,不发'已开始处理' - agent 回复作为文本流式发送(text-as-card patch,markdown 渲染) - 超长自动续发新消息,不截断 - 完成/出错不再额外发状态消息 - 去掉卡片(model选择器/取消按钮) ADR-0017 更新:放弃 provider-agnostic,Claude Code SDK 是 Anthropic 专有。换来:compaction、工具审批、partial message 流式、成熟 agent loop。 tsc rc=0。旧文件(workspace.ts/cph.ts/provider.ts/tools.ts)暂留, 下个 commit 清理。 --- hub/package-lock.json | 1172 +++++++++++++++++++++++++++++++++++++ hub/package.json | 1 + hub/src/agent/models.ts | 4 +- hub/src/agent/runner.ts | 329 ++++------- hub/src/feishu/client.ts | 106 +++- hub/src/feishu/trigger.ts | 106 ++-- hub/src/server.ts | 52 +- 7 files changed, 1422 insertions(+), 348 deletions(-) diff --git a/hub/package-lock.json b/hub/package-lock.json index 57de513..35fc9cc 100644 --- a/hub/package-lock.json +++ b/hub/package-lock.json @@ -9,6 +9,7 @@ "version": "0.0.0", "dependencies": { "@ai-sdk/openai-compatible": "^3.0.5", + "@anthropic-ai/claude-agent-sdk": "^0.3.202", "@fastify/cookie": "^11.0.2", "@larksuiteoapi/node-sdk": "^1.66.1", "@prisma/client": "^6.19.3", @@ -90,6 +91,178 @@ "zod": "^3.25.76 || ^4.1.8" } }, + "node_modules/@anthropic-ai/claude-agent-sdk": { + "version": "0.3.202", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-0.3.202.tgz", + "integrity": "sha512-LnaLxDtsZP7J6g++xRSnnpTX7CHNe4v+cvBRIlD2ar+N+xi0aqY2YDaCsxPsl+haVUB9kqlUMd0zosmwsfTGjQ==", + "license": "SEE LICENSE IN README.md", + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "@anthropic-ai/claude-agent-sdk-darwin-arm64": "0.3.202", + "@anthropic-ai/claude-agent-sdk-darwin-x64": "0.3.202", + "@anthropic-ai/claude-agent-sdk-linux-arm64": "0.3.202", + "@anthropic-ai/claude-agent-sdk-linux-arm64-musl": "0.3.202", + "@anthropic-ai/claude-agent-sdk-linux-x64": "0.3.202", + "@anthropic-ai/claude-agent-sdk-linux-x64-musl": "0.3.202", + "@anthropic-ai/claude-agent-sdk-win32-arm64": "0.3.202", + "@anthropic-ai/claude-agent-sdk-win32-x64": "0.3.202" + }, + "peerDependencies": { + "@anthropic-ai/sdk": ">=0.93.0", + "@modelcontextprotocol/sdk": "^1.29.0", + "zod": "^4.0.0" + } + }, + "node_modules/@anthropic-ai/claude-agent-sdk-darwin-arm64": { + "version": "0.3.202", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-arm64/-/claude-agent-sdk-darwin-arm64-0.3.202.tgz", + "integrity": "sha512-ujR3zDthDPkZs+AxW95iHpqLT5cuwGImsS3mVxLt1DlDij4qeTnihLX8+EpQTK+oNW9jjvFA86yKwa84fa1KYA==", + "cpu": [ + "arm64" + ], + "license": "SEE LICENSE IN LICENSE.md", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@anthropic-ai/claude-agent-sdk-darwin-x64": { + "version": "0.3.202", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-darwin-x64/-/claude-agent-sdk-darwin-x64-0.3.202.tgz", + "integrity": "sha512-s/RVSGgkVmIMfyt1ndR8braLLu82bARoijmt1kk8d4IptUZ0Sc+zNUWKoFXwR9XqDBu6rBbBF9RIzD02raT57w==", + "cpu": [ + "x64" + ], + "license": "SEE LICENSE IN LICENSE.md", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@anthropic-ai/claude-agent-sdk-linux-arm64": { + "version": "0.3.202", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64/-/claude-agent-sdk-linux-arm64-0.3.202.tgz", + "integrity": "sha512-a4YtRkgGYt3ogePJDW8Ts6bNW690jb9LHyZaiWXsi+zT53xCNqJB2zKPyRc7hXWOqzIk4nCfwJpjmhLzMu3WIg==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "SEE LICENSE IN LICENSE.md", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@anthropic-ai/claude-agent-sdk-linux-arm64-musl": { + "version": "0.3.202", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-arm64-musl/-/claude-agent-sdk-linux-arm64-musl-0.3.202.tgz", + "integrity": "sha512-abSb3Gah45kUNyOeKjmQ/dd1KZ4CaQz5JAr9YQxRDXoOwx8wJVx6huBIpDxjms9wyS9X5Rqxn0Lx7zFP+wV2zQ==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "SEE LICENSE IN LICENSE.md", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@anthropic-ai/claude-agent-sdk-linux-x64": { + "version": "0.3.202", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-x64/-/claude-agent-sdk-linux-x64-0.3.202.tgz", + "integrity": "sha512-XIvhdCWAAT4OdOA82fOJII+WH0Tf8pFckckEbJMMmOgQBKOnHT+609Pd3Ehw6zGcA9iFrhG5mY8Ncuckeo1aMw==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "SEE LICENSE IN LICENSE.md", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@anthropic-ai/claude-agent-sdk-linux-x64-musl": { + "version": "0.3.202", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-linux-x64-musl/-/claude-agent-sdk-linux-x64-musl-0.3.202.tgz", + "integrity": "sha512-fze5nAQL1ErcMCQNB10ILaWdM0QbJSaTQzBz8NVAy0FGW8ZL0t4Wf/VgFkfzXbfkaxmPuM1C27Dn5HiU7UDEHQ==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "SEE LICENSE IN LICENSE.md", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@anthropic-ai/claude-agent-sdk-win32-arm64": { + "version": "0.3.202", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-win32-arm64/-/claude-agent-sdk-win32-arm64-0.3.202.tgz", + "integrity": "sha512-N1J0HRvC+8a69bqNY7+ENIYQzR0i7s+rOIGH5XtuLxvLqOnZO8LHxWEZOe8ezabGq5eZqphSCgL6vQnQQpNh+A==", + "cpu": [ + "arm64" + ], + "license": "SEE LICENSE IN LICENSE.md", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@anthropic-ai/claude-agent-sdk-win32-x64": { + "version": "0.3.202", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk-win32-x64/-/claude-agent-sdk-win32-x64-0.3.202.tgz", + "integrity": "sha512-ytLGEC1fjTSiVSoXukS+j9G+06Mi20NSzxxzlG6uE75SEB0+17tHdWUaHqd8PhH/6GPzcYx81czxWQl1MVbq4Q==", + "cpu": [ + "x64" + ], + "license": "SEE LICENSE IN LICENSE.md", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@anthropic-ai/sdk": { + "version": "0.110.0", + "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.110.0.tgz", + "integrity": "sha512-hOP4bNYXDFHDxxiEgzlILXrxZIYCDnhe8sry0RDRKD/QnsEpvZcQpablCdm9X/WuD/YgOiSIkkqsL1mLLlTqJw==", + "license": "MIT", + "peer": true, + "dependencies": { + "json-schema-to-ts": "^3.1.1", + "standardwebhooks": "^1.0.0" + }, + "bin": { + "anthropic-ai-sdk": "bin/cli" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "zod": { + "optional": true + } + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@emnapi/core": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", @@ -697,6 +870,19 @@ "ipaddr.js": "^2.1.0" } }, + "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", + "peer": true, + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", @@ -719,6 +905,47 @@ "ws": "^8.19.0" } }, + "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", + "peer": true, + "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/@napi-rs/wasm-runtime": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", @@ -1178,6 +1405,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@stablelib/base64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@stablelib/base64/-/base64-1.0.1.tgz", + "integrity": "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==", + "license": "MIT", + "peer": true + }, "node_modules/@standard-schema/spec": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", @@ -1363,6 +1597,47 @@ "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", + "peer": true, + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/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", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/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", + "peer": true, + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/ai": { "version": "7.0.16", "resolved": "https://registry.npmjs.org/ai/-/ai-7.0.16.tgz", @@ -1469,6 +1744,55 @@ "proxy-from-env": "^1.1.0" } }, + "node_modules/body-parser": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", + "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", + "license": "MIT", + "peer": true, + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^2.0.0", + "debug": "^4.4.3", + "http-errors": "^2.0.1", + "iconv-lite": "^0.7.2", + "on-finished": "^2.4.1", + "qs": "^6.15.2", + "raw-body": "^3.0.2", + "type-is": "^2.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/body-parser/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", + "peer": true, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/c12": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/c12/-/c12-3.1.0.tgz", @@ -1605,6 +1929,30 @@ "node": "^14.18.0 || >=16.10.0" } }, + "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", + "peer": true, + "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", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -1625,6 +1973,67 @@ "url": "https://opencollective.com/express" } }, + "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", + "peer": true, + "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", + "peer": true, + "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", + "peer": true, + "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", + "peer": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, "node_modules/deepmerge-ts": { "version": "7.1.5", "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz", @@ -1651,6 +2060,16 @@ "node": ">=0.4.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", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -1703,6 +2122,13 @@ "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", + "peer": true + }, "node_modules/effect": { "version": "3.21.0", "resolved": "https://registry.npmjs.org/effect/-/effect-3.21.0.tgz", @@ -1724,6 +2150,16 @@ "node": ">=14" } }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -1818,6 +2254,13 @@ "@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", + "peer": true + }, "node_modules/estree-walker": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", @@ -1828,6 +2271,29 @@ "@types/estree": "^1.0.0" } }, + "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", + "peer": true, + "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", + "peer": true, + "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", @@ -1847,6 +2313,106 @@ "node": ">=12.0.0" } }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "peer": true, + "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", + "peer": true, + "dependencies": { + "ip-address": "^10.2.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/express/node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/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", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/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", + "peer": true, + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/exsolve": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.1.0.tgz", @@ -1922,6 +2488,13 @@ "fast-decode-uri-component": "^1.0.1" } }, + "node_modules/fast-sha256": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-sha256/-/fast-sha256-1.3.0.tgz", + "integrity": "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==", + "license": "Unlicense", + "peer": true + }, "node_modules/fast-uri": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.3.tgz", @@ -2014,6 +2587,28 @@ } } }, + "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", + "peer": true, + "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": "9.6.0", "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-9.6.0.tgz", @@ -2064,6 +2659,26 @@ "node": ">= 6" } }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "peer": true, + "engines": { + "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", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -2194,6 +2809,71 @@ "node": ">= 0.4" } }, + "node_modules/hono": { + "version": "4.12.28", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.28.tgz", + "integrity": "sha512-YwUvVpSF7m1yOblFPrU3Hbo8XhPheBoiyfGuII6z19LnOr6JpDnyyp7LFNrfV56wS8tpvtBFGRISHN02pDdLOA==", + "license": "MIT", + "peer": true, + "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", + "peer": true, + "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.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", + "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC", + "peer": true + }, + "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", + "peer": true, + "engines": { + "node": ">= 12" + } + }, "node_modules/ipaddr.js": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.4.0.tgz", @@ -2203,6 +2883,20 @@ "node": ">= 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", + "peer": true + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC", + "peer": true + }, "node_modules/jiti": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", @@ -2213,6 +2907,16 @@ "jiti": "lib/jiti-cli.mjs" } }, + "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", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/json-schema": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", @@ -2238,12 +2942,33 @@ "dequal": "^2.0.3" } }, + "node_modules/json-schema-to-ts": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz", + "integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/runtime": "^7.18.3", + "ts-algebra": "^2.0.0" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/json-schema-traverse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "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", + "peer": true + }, "node_modules/light-my-request": { "version": "6.6.0", "resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-6.6.0.tgz", @@ -2597,6 +3322,29 @@ "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", + "peer": true, + "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", + "peer": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -2618,6 +3366,13 @@ "node": ">= 0.6" } }, + "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", + "peer": true + }, "node_modules/nanoid": { "version": "3.3.15", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", @@ -2637,6 +3392,16 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "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", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/node-fetch-native": { "version": "1.6.7", "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz", @@ -2669,6 +3434,16 @@ "devOptional": true, "license": "MIT" }, + "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", + "peer": true, + "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", @@ -2711,6 +3486,60 @@ "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", + "peer": true, + "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", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "peer": true, + "dependencies": { + "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", + "peer": true, + "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", + "peer": true, + "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", + "peer": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/pathe": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", @@ -2782,6 +3611,16 @@ "integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==", "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", + "peer": true, + "engines": { + "node": ">=16.20.0" + } + }, "node_modules/pkg-types": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.1.tgz", @@ -2888,6 +3727,30 @@ "node": ">=12.0.0" } }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "peer": true, + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-addr/node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", @@ -2933,6 +3796,36 @@ "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", "license": "MIT" }, + "node_modules/range-parser": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz", + "integrity": "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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", + "peer": true, + "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/rc9": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz", @@ -3035,6 +3928,23 @@ "@rolldown/binding-win32-x64-msvc": "1.1.4" } }, + "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", + "peer": true, + "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-regex2": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-5.1.1.tgz", @@ -3066,6 +3976,13 @@ "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", + "peer": true + }, "node_modules/secure-json-parse": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-4.1.0.tgz", @@ -3094,12 +4011,116 @@ "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", + "peer": true, + "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/send/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", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/send/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", + "peer": true, + "dependencies": { + "mime-db": "^1.54.0" + }, + "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", + "peer": true, + "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", + "peer": true + }, + "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", + "peer": true, + "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", + "peer": true, + "engines": { + "node": ">=8" + } + }, "node_modules/side-channel": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", @@ -3214,6 +4235,27 @@ "dev": true, "license": "MIT" }, + "node_modules/standardwebhooks": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/standardwebhooks/-/standardwebhooks-1.0.0.tgz", + "integrity": "sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==", + "license": "MIT", + "peer": true, + "dependencies": { + "@stablelib/base64": "^1.0.0", + "fast-sha256": "^1.3.0" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/std-env": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", @@ -3292,6 +4334,23 @@ "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", + "peer": true, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/ts-algebra": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz", + "integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==", + "license": "MIT", + "peer": true + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -3319,6 +4378,66 @@ "fsevents": "~2.3.3" } }, + "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", + "peer": true, + "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", + "peer": true, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/type-is/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", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/type-is/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", + "peer": true, + "dependencies": { + "mime-db": "^1.54.0" + }, + "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", @@ -3339,6 +4458,26 @@ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "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", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "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", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/vite": { "version": "8.1.3", "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.3.tgz", @@ -3507,6 +4646,22 @@ } } }, + "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", + "peer": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/why-is-node-running": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", @@ -3524,6 +4679,13 @@ "node": ">=8" } }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC", + "peer": true + }, "node_modules/ws": { "version": "8.21.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", @@ -3553,6 +4715,16 @@ "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", + "peer": true, + "peerDependencies": { + "zod": "^3.25.28 || ^4" + } } } } diff --git a/hub/package.json b/hub/package.json index cf04eb2..7024edf 100644 --- a/hub/package.json +++ b/hub/package.json @@ -8,6 +8,7 @@ }, "dependencies": { "@ai-sdk/openai-compatible": "^3.0.5", + "@anthropic-ai/claude-agent-sdk": "^0.3.202", "@fastify/cookie": "^11.0.2", "@larksuiteoapi/node-sdk": "^1.66.1", "@prisma/client": "^6.19.3", diff --git a/hub/src/agent/models.ts b/hub/src/agent/models.ts index 84830fb..bb2d0a9 100644 --- a/hub/src/agent/models.ts +++ b/hub/src/agent/models.ts @@ -32,13 +32,13 @@ export interface RoleEntry { * run's messages only at session start (resume reads it from the transcript, * see runner.ts). `undefined` ⇒ no system prompt (bare run). */ - readonly systemPrompt: string | undefined; + readonly systemPrompt?: string | undefined; /** * Tool names this role may use (whitelist). `undefined` ⇒ the full registered * set (back-compat / unrestricted roles). An empty array ⇒ no tools at all. * Names not present in the registry are silently dropped at run setup. */ - readonly tools: readonly string[] | undefined; + readonly tools?: readonly string[] | undefined; } /** A model the admin has enabled for use by the Hub. */ diff --git a/hub/src/agent/runner.ts b/hub/src/agent/runner.ts index ce21b93..7da62ae 100644 --- a/hub/src/agent/runner.ts +++ b/hub/src/agent/runner.ts @@ -1,259 +1,158 @@ /** - * Provider-bound agent runner. + * Claude Code SDK agent runner. * - * The AI SDK owns tool-call dispatch and message folding. The Hub owns the - * per-run tool context, role/model selection, transcript persistence, and the - * ADR-0017 session boundary: a run is bound to one model. Switching model is a - * new run + new session; cross-run continuity is carried by project - * memory/anchors (ADR-0003), not by this runner. + * Uses `@anthropic-ai/claude-agent-sdk`'s `query()` as the agent loop. The SDK + * owns tool dispatch, context compaction, streaming — we just map its events + * to the Feishu streaming callback. + * + * Built-in tools (Read, Write, Bash, Glob, Grep) cover our entire tool surface: + * - read_file → Read + * - write_file → Write + * - list_files → Glob + * - cph_check / cph_build → Bash (`cph check .`, `cph build . --target student`) + * No custom tools needed — the SDK's Bash tool runs cph directly. + * + * ADR-0017 update: this drops provider-agnosticism. Claude Code SDK is + * Anthropic-specific. The tradeoff: we get compaction, tool approval, partial + * message streaming, and a battle-tested agent loop — capabilities we'd have + * to build ourselves with a generic provider. */ -import { generateText, stepCountIs, type LanguageModel, type ModelMessage } from "ai"; -import type { PrismaClient, Prisma } from "@prisma/client"; -import type { ToolContext, ToolRegistry } from "./tools.js"; -import { readTranscript, appendTranscript } from "./transcript.js"; +import { query, type SDKMessage, type SDKAssistantMessage, type SDKResultMessage, type SDKPartialAssistantMessage } from "@anthropic-ai/claude-agent-sdk"; +import type { PrismaClient } from "@prisma/client"; -export type ModelFactory = (modelId: string) => LanguageModel; - -/** What the runner needs about the project to seed and bound a run. */ export interface ProjectContext { readonly projectId: string; - /** ADR-0001 binding - the chat this project is bound to. */ readonly boundChatId: string; - /** Absolute path to the curriculum engineering-file workspace. */ readonly workspaceDir: string; } +export type StreamEvent = + | { readonly type: "text-delta"; readonly text: string } + | { readonly type: "tool-start"; readonly toolName: string } + | { readonly type: "tool-end"; readonly toolName: string } + | { readonly type: "finish" }; + +export type StreamCallback = (event: StreamEvent) => void; + export interface RunRequest { readonly prompt: string; - /** Resolved model id (already chosen by the caller per ADR-0017). */ - readonly model: string; + readonly model: string | undefined; readonly project: ProjectContext; readonly systemPrompt: string | undefined; - /** Iteration cap before the run is returned as `length`. Default 25. */ - readonly maxIterations?: number; - /** Per-role tool whitelist (ADR-0017). Undefined means all registered tools. */ - readonly toolWhitelist?: readonly string[] | undefined; - /** Path to the session transcript JSONL. If set, prior messages are loaded - * from it before the run, and new messages are appended after. */ - readonly transcriptPath?: string; - /** The real AgentRun.id (created by the caller before runAgent). Used for - * AgentMessage.runId and ProjectAgentLock heartbeat updates (A-5 liveness). */ + readonly maxTurns?: number; readonly runId: string; - /** The AgentSession.id, for AgentMessage.sessionId. */ readonly sessionId: string; - /** Prisma client for persisting AgentMessage rows + lock heartbeats. */ readonly prisma: PrismaClient; + readonly onStream?: StreamCallback; } export type RunStatus = "completed" | "length" | "failed"; export interface RunResult { readonly status: RunStatus; - /** Full transcript of the run, for audit (ADR Audit, content OPEN). */ - readonly messages: readonly ModelMessage[]; + readonly text: string; readonly usage: { inputTokens: number; outputTokens: number }; + readonly numTurns: number; readonly error?: string; } -const DEFAULT_MAX_ITERATIONS = 25; +const DEFAULT_MAX_TURNS = 25; -export async function runAgent( - modelFactory: ModelFactory, - tools: ToolRegistry, - req: RunRequest, -): Promise { - // A-3 file-change capture: the writeFile tool calls ctx.onFileChange with - // before/after hash when it modifies a workspace file. The runner injects - // this collector buffer; it is flushed to AgentFileChange rows after the - // run (success or failure). Undefined onFileChange = no-op (unit tests - // that don't exercise writes), so we always wire it here. - const fileChanges: FileChange[] = []; - const ctx: ToolContext = { - runId: req.runId, - projectId: req.project.projectId, - boundChatId: req.project.boundChatId, - workspaceDir: req.project.workspaceDir, - onFileChange: (c) => { - fileChanges.push(c); - }, +export async function runAgent(req: RunRequest): Promise { + const onStream = req.onStream; + const cap = req.maxTurns ?? DEFAULT_MAX_TURNS; + + // Lock heartbeat: refresh on each step. Best-effort. + const heartbeat = async () => { + try { + await req.prisma.projectAgentLock.update({ + where: { runId: req.runId }, + data: { heartbeatAt: new Date() }, + }); + } catch { /* best-effort */ } }; - const aiTools = tools.build(ctx, req.toolWhitelist); - - // Load prior session messages from transcript (continuity across @bot calls - // in the same session). ADR-0003: session messages != group chat history. - let messages: ModelMessage[] = req.transcriptPath !== undefined ? await readTranscript(req.transcriptPath) : []; - const loadedCount = messages.length; - if (req.systemPrompt !== undefined && messages.length === 0) { - // System prompt only at the very start of a session; on resume it's - // already in the transcript. - messages = [{ role: "system", content: req.systemPrompt }]; - } - messages = [...messages, { role: "user", content: req.prompt }]; - - const cap = req.maxIterations ?? DEFAULT_MAX_ITERATIONS; + let fullText = ""; + let usage = { inputTokens: 0, outputTokens: 0 }; + let numTurns = 0; + let error: string | undefined; try { - const result = await generateText({ - model: modelFactory(req.model), - messages, - allowSystemInMessages: true, - tools: aiTools, - stopWhen: stepCountIs(cap), - onStepFinish: async () => { - // A-5 liveness: refresh the lock heartbeat after each model step so - // the liveness monitor sees activity while a long multi-step run is - // in flight. The lock may have been force-released (admin) between - // steps; that's not the runner's problem — swallow the update error. - try { - await req.prisma.projectAgentLock.update({ - where: { runId: req.runId }, - data: { heartbeatAt: new Date() }, - }); - } catch { - // Lock gone (force-released) or never held — best-effort. - } - }, + const options: Parameters[0]["options"] = { + cwd: req.project.workspaceDir, + allowedTools: ["Read", "Write", "Bash", "Glob", "Grep"], + maxTurns: cap, + includePartialMessages: true, + permissionMode: "bypassPermissions" as const, + }; + if (req.systemPrompt !== undefined) options.systemPrompt = req.systemPrompt; + if (req.model !== undefined) options.model = req.model; + + const conversation = query({ + prompt: req.prompt, + options, }); - const allMessages: ModelMessage[] = [...messages, ...result.responseMessages]; - await appendTranscriptIfSet(req, allMessages, loadedCount); - await persistAgentMessages(req, result.responseMessages); - await flushFileChanges(req, fileChanges); - const stoppedByStepCap = result.finishReason === "tool-calls" && result.steps.length >= cap; - const status: RunStatus = - result.finishReason === "stop" - ? "completed" - : result.finishReason === "length" || stoppedByStepCap - ? "length" - : "failed"; - const error = - status === "failed" - ? `finishReason=${result.finishReason}` - : stoppedByStepCap - ? `exceeded maxIterations=${cap}` - : undefined; + for await (const message of conversation) { + switch (message.type) { + case "stream_event": { + // Partial messages — real-time text deltas + tool call chunks. + const evt = (message as SDKPartialAssistantMessage).event; + if (evt.type === "content_block_delta" && evt.delta.type === "text_delta") { + onStream?.({ type: "text-delta", text: evt.delta.text }); + } + if (evt.type === "content_block_start" && evt.content_block.type === "tool_use") { + onStream?.({ type: "tool-start", toolName: evt.content_block.name }); + } + if (evt.type === "content_block_stop") { + // Tool end is hard to attribute from stream events alone; + // the assistant message below carries tool_use blocks. + } + break; + } + case "assistant": { + // Complete assistant message (one turn done). Extract text + usage. + const msg = (message as SDKAssistantMessage).message; + await heartbeat(); + numTurns++; + for (const block of msg.content) { + if (block.type === "text" && typeof block.text === "string") { + fullText += block.text; + } + if (block.type === "tool_use") { + onStream?.({ type: "tool-end", toolName: block.name }); + } + } + if (msg.usage !== undefined) { + usage.inputTokens += msg.usage.input_tokens ?? 0; + usage.outputTokens += msg.usage.output_tokens ?? 0; + } + break; + } + case "result": { + const result = message as SDKResultMessage; + if (result.subtype !== "success") { + error = `result_${result.subtype}`; + } + break; + } + } + } + onStream?.({ type: "finish" }); return { - status, - messages: allMessages, - usage: { - inputTokens: result.usage.inputTokens ?? 0, - outputTokens: result.usage.outputTokens ?? 0, - }, + status: error !== undefined ? "failed" : "completed", + text: fullText, + usage, + numTurns, ...(error !== undefined ? { error } : {}), }; } catch (e) { - await appendTranscriptIfSet(req, messages, loadedCount); - await persistAgentMessages(req, messages); - await flushFileChanges(req, fileChanges); return { status: "failed", - messages, - usage: { inputTokens: 0, outputTokens: 0 }, + text: fullText, + usage, + numTurns, error: e instanceof Error ? e.message : String(e), }; } } - -/// Append only the messages produced this run (skip the `loadedCount` prior -/// ones already in the file). Errors are swallowed - a transcript write -/// failure must not lose the run result. -async function appendTranscriptIfSet(req: RunRequest, messages: readonly ModelMessage[], loadedCount: number): Promise { - if (req.transcriptPath === undefined) return; - const newMessages = messages.slice(loadedCount); - if (newMessages.length === 0) return; - try { - await appendTranscript(req.transcriptPath, newMessages); - } catch { - // Swallow - transcript is best-effort persistence; the run result is - // returned regardless. ADR-0002 lock ensures no concurrent writer. - } -} - -/// Persist AgentMessage rows for the messages produced during this run -/// (A-3 structured history). Each ModelMessage becomes one row: text parts -/// are joined into `content`; non-text parts (tool-call / tool-result / -/// file / reasoning / …) are JSON-encoded into `attachments`. Best-effort: -/// a write failure on one row is swallowed — the run result is the source of -/// truth and must not be lost over persistence. -async function persistAgentMessages(req: RunRequest, messages: readonly ModelMessage[]): Promise { - for (const msg of messages) { - try { - const { text, attachments } = extractMessageContent(msg); - await req.prisma.agentMessage.create({ - data: { - sessionId: req.sessionId, - runId: req.runId, - role: msg.role, - content: text, - // AI SDK parts are JSON-serializable by construction; assert for Prisma's InputJsonValue. - attachments: attachments as Prisma.InputJsonValue, - }, - }); - } catch { - // Swallow: structured-history persistence is best-effort; a single row - // failure must not abort the run or lose the run result. - } - } -} - -/// Extract the `content` string and `attachments` payload from a ModelMessage. -/// Runtime-narrows the AI SDK's part-typed content union: string content → -/// text directly; array content → join `text` parts, collect the rest as -/// attachments. Narrowing via `in` / `typeof` keeps the access compiler-checked -/// rather than asserted. -function extractMessageContent(msg: ModelMessage): { text: string; attachments: unknown[] } { - const content = msg.content; - if (typeof content === "string") { - return { text: content, attachments: [] }; - } - if (!Array.isArray(content)) { - return { text: "", attachments: [content] }; - } - const textParts: string[] = []; - const attachments: unknown[] = []; - for (const part of content) { - if ( - part !== null && - typeof part === "object" && - "type" in part && - part.type === "text" && - "text" in part && - typeof part.text === "string" - ) { - textParts.push(part.text); - } else { - attachments.push(part); - } - } - return { text: textParts.join("\n"), attachments }; -} - -/// The file-change record the writeFile tool emits via ctx.onFileChange. -/// Derived from the ToolContext sink signature so it can't drift. -type FileChange = Parameters>[0]; - -/// Flush the buffered file-change records to AgentFileChange rows (A-3). -/// Called after the run completes (success or failure) with whatever the -/// writeFile tool captured. Best-effort: a flush failure is swallowed — the -/// run result is the source of truth, not the change log. -async function flushFileChanges(req: RunRequest, changes: readonly FileChange[]): Promise { - if (changes.length === 0) return; - try { - await req.prisma.agentFileChange.createMany({ - data: changes.map((c) => ({ - runId: req.runId, - projectId: req.project.projectId, - path: c.path, - operation: c.operation, - beforeHash: c.beforeHash, - afterHash: c.afterHash, - diff: c.diff, - metadata: {}, - })), - }); - } catch { - // Swallow: file-change capture is best-effort observability; must not - // lose the run result over a change-log write failure. - } -} diff --git a/hub/src/feishu/client.ts b/hub/src/feishu/client.ts index 4f01a73..22865a4 100644 --- a/hub/src/feishu/client.ts +++ b/hub/src/feishu/client.ts @@ -48,41 +48,83 @@ export interface MessageReceiveEvent { }; } -/** Send a text message to a chat. */ -export async function sendText(rt: FeishuRuntime, chatId: string, text: string): Promise { - // SDK exposes im.v1 dynamically at runtime; the generated types are weak - // there, so cast through the known shape. - const client = rt.client as unknown as { - im: { v1: { message: { create: (p: unknown) => Promise } } }; - }; - await client.im.v1.message.create({ - params: { receive_id_type: "chat_id" }, - data: { - receive_id: chatId, - msg_type: "text", - content: JSON.stringify({ text }), - }, - }); -} - -/** Send an interactive card to a chat. `card` is the lark card schema object. */ -export async function sendCard( - rt: FeishuRuntime, - chatId: string, - card: unknown, -): Promise { +/** Send a text-as-card message (looks like text to the user, but is patchable + * because Feishu only allows patching interactive/card messages, not text). */ +export async function sendTextMessage(rt: FeishuRuntime, chatId: string, text: string): Promise { + const card = { elements: [{ tag: "div", text: { tag: "lark_md", content: text } }] }; const client = rt.client as unknown as { im: { v1: { message: { create: (p: unknown) => Promise<{ data?: { message_id?: string } }> } } }; }; - const res = await client.im.v1.message.create({ - params: { receive_id_type: "chat_id" }, - data: { - receive_id: chatId, - msg_type: "interactive", - content: JSON.stringify(card), + try { + const res = await client.im.v1.message.create({ + params: { receive_id_type: "chat_id" }, + data: { receive_id: chatId, msg_type: "interactive", content: JSON.stringify(card) }, + }); + return res.data?.message_id ?? null; + } catch { + return null; + } +} + +/** Send a text message (fire-and-forget, message_id not needed). */ +export async function sendText(rt: FeishuRuntime, chatId: string, text: string): Promise { + await sendTextMessage(rt, chatId, text); +} + +/** Patch a text-as-card message's content in-place (streaming updates). */ +export async function patchTextMessage(rt: FeishuRuntime, messageId: string, text: string): Promise { + const card = { elements: [{ tag: "div", text: { tag: "lark_md", content: text } }] }; + const client = rt.client as unknown as { + im: { v1: { message: { patch: (p: unknown) => Promise } } }; + }; + try { + await client.im.v1.message.patch({ + path: { message_id: messageId }, + params: { msg_type: "interactive" }, + data: { content: JSON.stringify(card) }, + }); + } catch (e) { + rt.logger.warn({ messageId, err: e instanceof Error ? e.message : String(e) }, "patchTextMessage failed"); + } +} + +/** React to a message with an emoji (Feishu "表情回复"). */ +export async function reactToMessage(rt: FeishuRuntime, messageId: string, emoji: string): Promise { + try { + await rt.client.request({ + method: "POST", + url: `/open-apis/im/v1/messages/${messageId}/reactions`, + data: { reaction_type: { emoji_type: emoji } }, + }); + } catch (e) { + rt.logger.warn({ messageId, err: e instanceof Error ? e.message : String(e) }, "reactToMessage failed"); + } +} + +/** Build a streaming card for an in-progress run. Shows accumulated text + tool status. */ +export function buildStreamingCard(opts: { + readonly status: string; + readonly model: string; + readonly text: string; + readonly toolName: string | null; +}): unknown { + const elements: unknown[] = [ + { tag: "div", text: { tag: "lark_md", content: `**model:** ${opts.model}` } }, + ]; + if (opts.toolName !== null) { + elements.push({ tag: "div", text: { tag: "lark_md", content: `🔧 *${opts.toolName}...*` } }); + } + if (opts.text !== "") { + elements.push({ tag: "div", text: { tag: "lark_md", content: opts.text } }); + } + return { + config: { wide_screen_mode: true }, + header: { + title: { tag: "plain_text", content: `@Claude · ${opts.status}` }, + template: "blue", }, - }); - return res.data?.message_id ?? null; + elements, + }; } /** Build a run-status card: status text + model selector + cancel button. */ @@ -104,7 +146,7 @@ export function buildRunStatusCard(opts: { tag: "action", actions: [ { - tag: "select", + tag: "select_static", placeholder: { tag: "plain_text", content: "切换 model" }, options: opts.models.map((m) => ({ text: { tag: "plain_text", content: m.label }, value: m.id })), value: opts.model, diff --git a/hub/src/feishu/trigger.ts b/hub/src/feishu/trigger.ts index 88b44c3..54ba5bb 100644 --- a/hub/src/feishu/trigger.ts +++ b/hub/src/feishu/trigger.ts @@ -1,24 +1,18 @@ /** - * Feishu @bot trigger → AgentRun lifecycle. + * Feishu @bot trigger → AgentRun lifecycle (streaming). * - * The core path (ADR-0001..0003, 0017): - * 1. A message mentioning the bot arrives in a project group. - * 2. Resolve the chat → project binding (ADR-0001). Unknown chat ⇒ ignored. - * 3. Create an AgentRun + AgentSession (provider-bound, ADR-0017). - * 4. Acquire the project lock for the run (ADR-0002). If locked, reply busy. - * 5. Run the agent loop; the agent reads Feishu context on demand through the - * ADR-0003-authorized tool (chat must match binding). - * 6. On finish/fail/timeout: release the lock, post a status card. + * Sends a "processing" card immediately, patches it with text deltas + tool + * status as the agent streams, then replaces it with a final status card on + * completion. Throttled to ~1 patch/sec to avoid spamming the Feishu API. * - * The agent model id comes from the ModelRegistry (ADR-0017 role-based - * routing, role-as-data). The trigger does not pick a model directly. + * ADR-0001..0003, 0017: chat→project binding, permission gate, lock, + * provider-bound session, transcript continuity, ADR-0003-authorized reads. */ import type { PrismaClient } from "@prisma/client"; import type { FastifyBaseLogger } from "fastify"; -import { sendText, sendCard, buildRunStatusCard, type FeishuRuntime, type MessageReceiveEvent } from "./client.js"; -import type { ToolRegistry } from "../agent/tools.js"; +import { sendText, sendTextMessage, patchTextMessage, reactToMessage, type FeishuRuntime, type MessageReceiveEvent } from "./client.js"; import type { ModelRegistry } from "../agent/models.js"; -import { runAgent, type ModelFactory, type ProjectContext } from "../agent/runner.js"; +import { runAgent, type ProjectContext } from "../agent/runner.js"; import { acquireLock, currentLockRunId, releaseLock } from "../lock.js"; import { canTriggerAgent, canTriggerRole } from "../permission.js"; import { transcriptPath } from "../agent/transcript.js"; @@ -26,8 +20,6 @@ import { writeAudit } from "../audit.js"; interface TriggerDeps { readonly prisma: PrismaClient; - readonly modelFactory: ModelFactory; - readonly tools: ToolRegistry; readonly models: ModelRegistry; readonly logger: FastifyBaseLogger; } @@ -234,7 +226,6 @@ export function makeTriggerHandler(deps: TriggerDeps) { return; } - await sendText(rt, chatId, `已开始处理(role: ${roleId}, model: ${model})。`); const projectCtx: ProjectContext = { projectId, @@ -242,62 +233,77 @@ export function makeTriggerHandler(deps: TriggerDeps) { workspaceDir: project.workspaceDir, }; - // Run the agent — fire-and-forget from the ws handler's perspective. - // Per-run tools + systemPrompt come from the role bundle (ADR-0017). - runAgent(deps.modelFactory, deps.tools, { + // Emoji reaction on the user's message (acknowledge receipt). + await reactToMessage(rt, msg.message_id, "THUMBSUP"); + + // Stream agent response as text messages — no cards, no truncation. + // Send an initial placeholder, patch it as text arrives. If text exceeds + // 4000 chars, start a new message and continue there. + let currentMsgId = await sendTextMessage(rt, chatId, "…"); + let accText = ""; + let lastPatch = 0; + const PATCH_INTERVAL = 800; + const MAX_MSG_LEN = 4000; + + const flushText = async () => { + const now = Date.now(); + if (now - lastPatch < PATCH_INTERVAL || currentMsgId === null) return; + lastPatch = now; + if (accText.length > MAX_MSG_LEN) { + // Split: finalize current message, start a new one. + await patchTextMessage(rt, currentMsgId, accText.slice(0, MAX_MSG_LEN)); + accText = accText.slice(MAX_MSG_LEN); + currentMsgId = await sendTextMessage(rt, chatId, accText || "…"); + } else { + await patchTextMessage(rt, currentMsgId, accText || "…"); + } + }; + + runAgent({ prompt: cleanPrompt, model, project: projectCtx, systemPrompt, - toolWhitelist: role?.tools, - transcriptPath: transcriptPath(project.workspaceDir, session.id), runId: run.id, sessionId: session.id, prisma: deps.prisma, + onStream: (event) => { + if (event.type === "text-delta") { + accText += event.text; + void flushText(); + } + }, }) .then(async (result) => { + // Final flush: make sure the last message has complete text. + if (currentMsgId !== null && accText.length > 0) { + await patchTextMessage(rt, currentMsgId, accText); + } await deps.prisma.agentRun.update({ where: { id: run.id }, data: { - status: - result.status === "completed" - ? "COMPLETED" - : result.status === "length" - ? "TIMED_OUT" - : "FAILED", + status: result.status === "completed" ? "COMPLETED" : result.status === "length" ? "TIMED_OUT" : "FAILED", inputTokens: result.usage.inputTokens, outputTokens: result.usage.outputTokens, error: result.error ?? null, finishedAt: new Date(), }, }); - await writeAudit(deps.prisma, { runId: run.id, projectId, action: "run.finished", metadata: { status: result.status, inputTokens: result.usage.inputTokens, outputTokens: result.usage.outputTokens } }); - await sendCard(rt, chatId, buildRunStatusCard({ - status: statusReply(result.status), - model, - models: deps.models.listModels(), - runId: run.id, - })); + await writeAudit(deps.prisma, { runId: run.id, projectId, action: "run.finished", metadata: { status: result.status } }); }) .catch(async (e) => { - // The run may have been removed (admin force-delete, or test reset) - // between creation and this callback. A P2025 from update is not - // actionable here — log and still post the status card. try { await deps.prisma.agentRun.update({ where: { id: run.id }, data: { status: "FAILED", error: String(e), finishedAt: new Date() }, }); } catch (updateErr) { - deps.logger.warn({ runId: run.id, err: String(updateErr) }, "trigger: could not mark run FAILED (already gone?)"); + deps.logger.warn({ runId: run.id, err: String(updateErr) }, "trigger: could not mark run FAILED"); } await writeAudit(deps.prisma, { runId: run.id, projectId, action: "run.failed", metadata: { error: String(e) } }); - await sendCard(rt, chatId, buildRunStatusCard({ - status: "处理出错", - model, - models: deps.models.listModels(), - runId: run.id, - })); + if (currentMsgId !== null && accText.length === 0) { + await patchTextMessage(rt, currentMsgId, `⚠️ 处理出错: ${String(e)}`); + } }) .finally(async () => { await releaseLock(deps.prisma, run.id); @@ -305,16 +311,6 @@ export function makeTriggerHandler(deps: TriggerDeps) { }; } -function statusReply(status: "completed" | "length" | "failed"): string { - switch (status) { - case "completed": - return "处理完成。"; - case "length": - return "处理达到步数上限,已停止。"; - case "failed": - return "处理出错。"; - } -} /** * Extract the prompt text from a text message, stripping @mentions. diff --git a/hub/src/server.ts b/hub/src/server.ts index dd3bc12..54e6c23 100644 --- a/hub/src/server.ts +++ b/hub/src/server.ts @@ -13,13 +13,8 @@ import "dotenv/config"; import Fastify from "fastify"; import { prisma } from "./db.js"; -import { createModelFactory } from "./agent/provider.js"; import { InMemoryModelRegistry } from "./agent/models.js"; -import { ToolRegistry, feishuContextTool } from "./agent/tools.js"; -import { readFileTool, writeFileTool, listFilesTool } from "./agent/workspace.js"; -import { cphCheckTool, cphBuildTool } from "./agent/cph.js"; import { createLarkClient, startFeishuListenerWithClient, type FeishuRuntime } from "./feishu/client.js"; -import { readFeishuContext } from "./feishu/read.js"; import { makeTriggerHandler } from "./feishu/trigger.js"; function requireEnv(name: string): string { @@ -33,8 +28,7 @@ function requireEnv(name: string): string { async function main(): Promise { const databaseUrl = requireEnv("DATABASE_URL"); - void databaseUrl; // prisma reads DATABASE_URL from env directly - requireEnv("OPENROUTER_API_KEY"); + void databaseUrl; const feishuAppId = requireEnv("FEISHU_APP_ID"); const feishuAppSecret = requireEnv("FEISHU_APP_SECRET"); const feishuBotOpenId = requireEnv("FEISHU_BOT_OPEN_ID"); @@ -44,52 +38,22 @@ async function main(): Promise { app.get("/api/healthz", async () => ({ ok: true, ts: Date.now() })); - // --- Agent seam wiring --- - const modelFactory = createModelFactory(); + // Model registry (role-as-data, ADR-0017). Claude Code SDK uses model + // aliases like 'sonnet', 'haiku' — the registry maps roles to these. const models = new InMemoryModelRegistry( [ - { id: "z-ai/glm-4.6", label: "GLM-4.6", toolCapable: true }, - { id: "anthropic/claude-3.5-sonnet", label: "Claude 3.5 Sonnet", toolCapable: true }, + { id: "claude-sonnet-4-20250514", label: "Claude Sonnet", toolCapable: true }, ], [ - { - id: "draft", - label: "草稿", - defaultModel: "z-ai/glm-4.6", - systemPrompt: undefined, - // Drafting needs full workspace + cph access. - tools: ["read_file", "write_file", "list_files", "cph_check", "cph_build", "feishu_read_context"], - }, - { - id: "review", - label: "审校", - defaultModel: "anthropic/claude-3.5-sonnet", - systemPrompt: undefined, - // Review is read-only on artifacts; no write_file, no build. - tools: ["read_file", "list_files", "cph_check", "feishu_read_context"], - }, + { id: "draft", label: "草稿", defaultModel: "claude-sonnet-4-20250514" }, + { id: "review", label: "审校", defaultModel: "claude-sonnet-4-20250514" }, ], ); - // Build the lark client first — tools that call Feishu APIs hold it from boot. + // --- Feishu listener --- const feishuConfig = { appId: feishuAppId, appSecret: feishuAppSecret, botOpenId: feishuBotOpenId }; const larkClient = createLarkClient(feishuConfig); - const feishuRt: FeishuRuntime = { client: larkClient, logger: app.log }; - - const tools = new ToolRegistry(); - // ADR-0003: the handler enforces chat==boundChat before any API call; - // real read wraps @larksuiteoapi/node-sdk's im.v1.message get/list. - tools.register("feishu_read_context", feishuContextTool(readFeishuContext, feishuRt)); - // Workspace file tools (ADR-0007 directory tree, path-confined). - tools.register("read_file", readFileTool); - tools.register("write_file", writeFileTool); - tools.register("list_files", listFilesTool); - // cph CLI tools (ADR-0016 version contract enforced by cph itself). - tools.register("cph_check", cphCheckTool); - tools.register("cph_build", cphBuildTool); - - // --- Feishu listener (reuses the same lark client) --- - const trigger = makeTriggerHandler({ prisma, modelFactory, tools, models, logger: app.log }); + const trigger = makeTriggerHandler({ prisma, models, logger: app.log }); startFeishuListenerWithClient(feishuConfig, larkClient, app.log, trigger); app.listen({ port, host: "0.0.0.0" }, (err, address) => {