Compare commits
17 Commits
b3c5a4b41b
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
84672590d7 | ||
|
|
41b3b4e0e2 | ||
|
|
2259471b66 | ||
|
|
5376df5389 | ||
|
|
007e95fe40 | ||
|
|
bc9ba1c2d2 | ||
|
|
984922884c | ||
|
|
f5d1ec302d | ||
|
|
03e6f8bcc6 | ||
|
|
6fad6dcfab | ||
|
|
712664ca00 | ||
|
|
80df025672 | ||
|
|
89d68927c6 | ||
|
|
7b27167af6 | ||
|
|
e81357e1df | ||
|
|
08c99675c6 | ||
|
|
c016b9c2fa |
22
.gitignore
vendored
22
.gitignore
vendored
@@ -16,6 +16,10 @@ data/
|
|||||||
data-*/
|
data-*/
|
||||||
logs/
|
logs/
|
||||||
.ejclaw-reviewer-runtime/
|
.ejclaw-reviewer-runtime/
|
||||||
|
# Scratch dirs created in CWD by outbound-attachments tests; leak into the repo
|
||||||
|
# root only when a test run is interrupted mid-flight.
|
||||||
|
.ejclaw-attachment-*/
|
||||||
|
.ejclaw-*images-*/
|
||||||
|
|
||||||
# Groups - only track base structure and specific CLAUDE.md files
|
# Groups - only track base structure and specific CLAUDE.md files
|
||||||
groups-*/
|
groups-*/
|
||||||
@@ -54,3 +58,21 @@ runners/codex-runner-backups/
|
|||||||
.vscode/
|
.vscode/
|
||||||
|
|
||||||
agents-sdk-docs
|
agents-sdk-docs
|
||||||
|
|
||||||
|
# --- merged from upstream 048a89d (python/node/build/secret noise) ---
|
||||||
|
build/
|
||||||
|
.bun-cache/
|
||||||
|
.DS_Store
|
||||||
|
*.egg-info/
|
||||||
|
.env
|
||||||
|
!.env.example
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
.pytest_cache/
|
||||||
|
*.sqlite
|
||||||
|
*.sqlite-journal
|
||||||
|
*.sqlite-shm
|
||||||
|
*.sqlite-wal
|
||||||
|
*.swp
|
||||||
|
*.tsbuildinfo
|
||||||
|
.venv/
|
||||||
|
|||||||
278
bun.lock
278
bun.lock
@@ -6,8 +6,8 @@
|
|||||||
"name": "ejclaw",
|
"name": "ejclaw",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@modelcontextprotocol/sdk": "^1.27.1",
|
"@modelcontextprotocol/sdk": "^1.27.1",
|
||||||
"cron-parser": "^5.5.0",
|
"cron-parser": "^5.6.1",
|
||||||
"discord.js": "^14.18.0",
|
"discord.js": "^14.26.4",
|
||||||
"ejclaw-runners-shared": "workspace:*",
|
"ejclaw-runners-shared": "workspace:*",
|
||||||
"lucide-react": "^1.11.0",
|
"lucide-react": "^1.11.0",
|
||||||
"pino": "^9.6.0",
|
"pino": "^9.6.0",
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
"react-dom": "^19.2.5",
|
"react-dom": "^19.2.5",
|
||||||
"react-markdown": "^10.1.0",
|
"react-markdown": "^10.1.0",
|
||||||
"remark-gfm": "^4.0.1",
|
"remark-gfm": "^4.0.1",
|
||||||
"yaml": "^2.8.2",
|
"yaml": "^2.9.0",
|
||||||
"zod": "^4.3.6",
|
"zod": "^4.3.6",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
"@types/react": "^19.2.14",
|
"@types/react": "^19.2.14",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
"@vitejs/plugin-react": "^6.0.1",
|
"@vitejs/plugin-react": "^6.0.1",
|
||||||
"@vitest/coverage-v8": "^4.0.18",
|
"@vitest/coverage-v8": "^4.1.9",
|
||||||
"better-sqlite3": "^12.8.0",
|
"better-sqlite3": "^12.8.0",
|
||||||
"eslint": "^10.2.1",
|
"eslint": "^10.2.1",
|
||||||
"globals": "^17.5.0",
|
"globals": "^17.5.0",
|
||||||
@@ -37,8 +37,8 @@
|
|||||||
"prettier": "^3.8.1",
|
"prettier": "^3.8.1",
|
||||||
"typescript": "^5.7.0",
|
"typescript": "^5.7.0",
|
||||||
"typescript-eslint": "^8.59.0",
|
"typescript-eslint": "^8.59.0",
|
||||||
"vite": "^8.0.10",
|
"vite": "^8.1.3",
|
||||||
"vitest": "^4.0.18",
|
"vitest": "^4.1.9",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"runners/agent-runner": {
|
"runners/agent-runner": {
|
||||||
@@ -113,76 +113,18 @@
|
|||||||
|
|
||||||
"@bcoe/v8-coverage": ["@bcoe/v8-coverage@1.0.2", "", {}, "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA=="],
|
"@bcoe/v8-coverage": ["@bcoe/v8-coverage@1.0.2", "", {}, "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA=="],
|
||||||
|
|
||||||
"@discordjs/builders": ["@discordjs/builders@1.13.1", "", { "dependencies": { "@discordjs/formatters": "^0.6.2", "@discordjs/util": "^1.2.0", "@sapphire/shapeshift": "^4.0.0", "discord-api-types": "^0.38.33", "fast-deep-equal": "^3.1.3", "ts-mixer": "^6.0.4", "tslib": "^2.6.3" } }, "sha512-cOU0UDHc3lp/5nKByDxkmRiNZBpdp0kx55aarbiAfakfKJHlxv/yFW1zmIqCAmwH5CRlrH9iMFKJMpvW4DPB+w=="],
|
"@discordjs/builders": ["@discordjs/builders@1.14.1", "", { "dependencies": { "@discordjs/formatters": "^0.6.2", "@discordjs/util": "^1.2.0", "@sapphire/shapeshift": "^4.0.0", "discord-api-types": "^0.38.40", "fast-deep-equal": "^3.1.3", "ts-mixer": "^6.0.4", "tslib": "^2.6.3" } }, "sha512-gSKkhXLqs96TCzk66VZuHHl8z2bQMJFGwrXC0f33ngK+FLNau4hU1PYny3DNJfNdSH+gVMzE85/d5FQ2BpcNwQ=="],
|
||||||
|
|
||||||
"@discordjs/collection": ["@discordjs/collection@1.5.3", "", {}, "sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ=="],
|
"@discordjs/collection": ["@discordjs/collection@1.5.3", "", {}, "sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ=="],
|
||||||
|
|
||||||
"@discordjs/formatters": ["@discordjs/formatters@0.6.2", "", { "dependencies": { "discord-api-types": "^0.38.33" } }, "sha512-y4UPwWhH6vChKRkGdMB4odasUbHOUwy7KL+OVwF86PvT6QVOwElx+TiI1/6kcmcEe+g5YRXJFiXSXUdabqZOvQ=="],
|
"@discordjs/formatters": ["@discordjs/formatters@0.6.2", "", { "dependencies": { "discord-api-types": "^0.38.33" } }, "sha512-y4UPwWhH6vChKRkGdMB4odasUbHOUwy7KL+OVwF86PvT6QVOwElx+TiI1/6kcmcEe+g5YRXJFiXSXUdabqZOvQ=="],
|
||||||
|
|
||||||
"@discordjs/rest": ["@discordjs/rest@2.6.0", "", { "dependencies": { "@discordjs/collection": "^2.1.1", "@discordjs/util": "^1.1.1", "@sapphire/async-queue": "^1.5.3", "@sapphire/snowflake": "^3.5.3", "@vladfrangu/async_event_emitter": "^2.4.6", "discord-api-types": "^0.38.16", "magic-bytes.js": "^1.10.0", "tslib": "^2.6.3", "undici": "6.21.3" } }, "sha512-RDYrhmpB7mTvmCKcpj+pc5k7POKszS4E2O9TYc+U+Y4iaCP+r910QdO43qmpOja8LRr1RJ0b3U+CqVsnPqzf4w=="],
|
"@discordjs/rest": ["@discordjs/rest@2.6.3", "", { "dependencies": { "@discordjs/collection": "^2.1.1", "@discordjs/util": "^1.2.0", "@sapphire/async-queue": "^1.5.3", "@sapphire/snowflake": "^3.5.5", "@vladfrangu/async_event_emitter": "^2.4.6", "discord-api-types": "^0.38.50", "magic-bytes.js": "^1.13.0", "tslib": "^2.6.3", "undici": "^6.27.0" } }, "sha512-wvOylxNYJkwKjctS/Mn5GP1w9r3/rzyH+ThD1JlAca6zEdlHs8QWBBUQJpU5Q+W6DoIj/Ljh1IPlZs7hTU+UAg=="],
|
||||||
|
|
||||||
"@discordjs/util": ["@discordjs/util@1.2.0", "", { "dependencies": { "discord-api-types": "^0.38.33" } }, "sha512-3LKP7F2+atl9vJFhaBjn4nOaSWahZ/yWjOvA4e5pnXkt2qyXRCHLxoBQy81GFtLGCq7K9lPm9R517M1U+/90Qg=="],
|
"@discordjs/util": ["@discordjs/util@1.2.0", "", { "dependencies": { "discord-api-types": "^0.38.33" } }, "sha512-3LKP7F2+atl9vJFhaBjn4nOaSWahZ/yWjOvA4e5pnXkt2qyXRCHLxoBQy81GFtLGCq7K9lPm9R517M1U+/90Qg=="],
|
||||||
|
|
||||||
"@discordjs/ws": ["@discordjs/ws@1.2.3", "", { "dependencies": { "@discordjs/collection": "^2.1.0", "@discordjs/rest": "^2.5.1", "@discordjs/util": "^1.1.0", "@sapphire/async-queue": "^1.5.2", "@types/ws": "^8.5.10", "@vladfrangu/async_event_emitter": "^2.2.4", "discord-api-types": "^0.38.1", "tslib": "^2.6.2", "ws": "^8.17.0" } }, "sha512-wPlQDxEmlDg5IxhJPuxXr3Vy9AjYq5xCvFWGJyD7w7Np8ZGu+Mc+97LCoEc/+AYCo2IDpKioiH0/c/mj5ZR9Uw=="],
|
"@discordjs/ws": ["@discordjs/ws@1.2.3", "", { "dependencies": { "@discordjs/collection": "^2.1.0", "@discordjs/rest": "^2.5.1", "@discordjs/util": "^1.1.0", "@sapphire/async-queue": "^1.5.2", "@types/ws": "^8.5.10", "@vladfrangu/async_event_emitter": "^2.2.4", "discord-api-types": "^0.38.1", "tslib": "^2.6.2", "ws": "^8.17.0" } }, "sha512-wPlQDxEmlDg5IxhJPuxXr3Vy9AjYq5xCvFWGJyD7w7Np8ZGu+Mc+97LCoEc/+AYCo2IDpKioiH0/c/mj5ZR9Uw=="],
|
||||||
|
|
||||||
"@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="],
|
|
||||||
|
|
||||||
"@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="],
|
|
||||||
|
|
||||||
"@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="],
|
|
||||||
|
|
||||||
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.3", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg=="],
|
|
||||||
|
|
||||||
"@esbuild/android-arm": ["@esbuild/android-arm@0.27.3", "", { "os": "android", "cpu": "arm" }, "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA=="],
|
|
||||||
|
|
||||||
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.3", "", { "os": "android", "cpu": "arm64" }, "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg=="],
|
|
||||||
|
|
||||||
"@esbuild/android-x64": ["@esbuild/android-x64@0.27.3", "", { "os": "android", "cpu": "x64" }, "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ=="],
|
|
||||||
|
|
||||||
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg=="],
|
|
||||||
|
|
||||||
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg=="],
|
|
||||||
|
|
||||||
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.3", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w=="],
|
|
||||||
|
|
||||||
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA=="],
|
|
||||||
|
|
||||||
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.3", "", { "os": "linux", "cpu": "arm" }, "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw=="],
|
|
||||||
|
|
||||||
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg=="],
|
|
||||||
|
|
||||||
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.3", "", { "os": "linux", "cpu": "ia32" }, "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg=="],
|
|
||||||
|
|
||||||
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA=="],
|
|
||||||
|
|
||||||
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw=="],
|
|
||||||
|
|
||||||
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA=="],
|
|
||||||
|
|
||||||
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ=="],
|
|
||||||
|
|
||||||
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw=="],
|
|
||||||
|
|
||||||
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.3", "", { "os": "linux", "cpu": "x64" }, "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA=="],
|
|
||||||
|
|
||||||
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA=="],
|
|
||||||
|
|
||||||
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.3", "", { "os": "none", "cpu": "x64" }, "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA=="],
|
|
||||||
|
|
||||||
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.3", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw=="],
|
|
||||||
|
|
||||||
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.3", "", { "os": "openbsd", "cpu": "x64" }, "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ=="],
|
|
||||||
|
|
||||||
"@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g=="],
|
|
||||||
|
|
||||||
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.3", "", { "os": "sunos", "cpu": "x64" }, "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA=="],
|
|
||||||
|
|
||||||
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA=="],
|
|
||||||
|
|
||||||
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q=="],
|
|
||||||
|
|
||||||
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.3", "", { "os": "win32", "cpu": "x64" }, "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA=="],
|
|
||||||
|
|
||||||
"@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="],
|
"@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="],
|
||||||
|
|
||||||
"@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="],
|
"@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="],
|
||||||
@@ -219,8 +161,6 @@
|
|||||||
|
|
||||||
"@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.27.1", "", { "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" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-sr6GbP+4edBwFndLbM60gf07z0FQ79gaExpnsjMGePXqFcSSb7t6iscpjk9DhFhwd+mTEQrzNafGP8/iGGFYaA=="],
|
"@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.27.1", "", { "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" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-sr6GbP+4edBwFndLbM60gf07z0FQ79gaExpnsjMGePXqFcSSb7t6iscpjk9DhFhwd+mTEQrzNafGP8/iGGFYaA=="],
|
||||||
|
|
||||||
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.4", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow=="],
|
|
||||||
|
|
||||||
"@openai/codex": ["@openai/codex@0.133.0", "", { "optionalDependencies": { "@openai/codex-darwin-arm64": "npm:@openai/codex@0.133.0-darwin-arm64", "@openai/codex-darwin-x64": "npm:@openai/codex@0.133.0-darwin-x64", "@openai/codex-linux-arm64": "npm:@openai/codex@0.133.0-linux-arm64", "@openai/codex-linux-x64": "npm:@openai/codex@0.133.0-linux-x64", "@openai/codex-win32-arm64": "npm:@openai/codex@0.133.0-win32-arm64", "@openai/codex-win32-x64": "npm:@openai/codex@0.133.0-win32-x64" }, "bin": { "codex": "bin/codex.js" } }, "sha512-Gh42kLLBo/6gpnHmDzUWDVvyS57ekCB1+1Dz0RG2oIl3Lhk1uwrjSj/PwaJWWh4Rw/rUp1RqkwrMugFfFEOlqQ=="],
|
"@openai/codex": ["@openai/codex@0.133.0", "", { "optionalDependencies": { "@openai/codex-darwin-arm64": "npm:@openai/codex@0.133.0-darwin-arm64", "@openai/codex-darwin-x64": "npm:@openai/codex@0.133.0-darwin-x64", "@openai/codex-linux-arm64": "npm:@openai/codex@0.133.0-linux-arm64", "@openai/codex-linux-x64": "npm:@openai/codex@0.133.0-linux-x64", "@openai/codex-win32-arm64": "npm:@openai/codex@0.133.0-win32-arm64", "@openai/codex-win32-x64": "npm:@openai/codex@0.133.0-win32-x64" }, "bin": { "codex": "bin/codex.js" } }, "sha512-Gh42kLLBo/6gpnHmDzUWDVvyS57ekCB1+1Dz0RG2oIl3Lhk1uwrjSj/PwaJWWh4Rw/rUp1RqkwrMugFfFEOlqQ=="],
|
||||||
|
|
||||||
"@openai/codex-darwin-arm64": ["@openai/codex@0.133.0-darwin-arm64", "", { "os": "darwin", "cpu": "arm64" }, "sha512-W7f8+DckLujnqGlptKCzgJU+ooeHKMuk6KYgMFP6A9asn7YUsGUgJqjiBaX8oNcXO6w/pTbKGRARx1kCNS8lIg=="],
|
"@openai/codex-darwin-arm64": ["@openai/codex@0.133.0-darwin-arm64", "", { "os": "darwin", "cpu": "arm64" }, "sha512-W7f8+DckLujnqGlptKCzgJU+ooeHKMuk6KYgMFP6A9asn7YUsGUgJqjiBaX8oNcXO6w/pTbKGRARx1kCNS8lIg=="],
|
||||||
@@ -235,104 +175,52 @@
|
|||||||
|
|
||||||
"@openai/codex-win32-x64": ["@openai/codex@0.133.0-win32-x64", "", { "os": "win32", "cpu": "x64" }, "sha512-u3ji78DIPZCGJeELuovsAnaZH+vK9gsA4F6M1y+Uy2s80Sz7/i1S0KL81qGReYji3urSjgBpkQuNP47GXOqxrQ=="],
|
"@openai/codex-win32-x64": ["@openai/codex@0.133.0-win32-x64", "", { "os": "win32", "cpu": "x64" }, "sha512-u3ji78DIPZCGJeELuovsAnaZH+vK9gsA4F6M1y+Uy2s80Sz7/i1S0KL81qGReYji3urSjgBpkQuNP47GXOqxrQ=="],
|
||||||
|
|
||||||
"@oxc-project/types": ["@oxc-project/types@0.127.0", "", {}, "sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ=="],
|
"@oxc-project/types": ["@oxc-project/types@0.146.0", "", {}, "sha512-XC0QsnnhVe7sLIWmYmdPw7x5P0h4W8vUU3Nv1ySgWXtvCz8NizoAEpGXA0sOYoJQV2Rl13LgURAHQ5cI5ILCSA=="],
|
||||||
|
|
||||||
"@pinojs/redact": ["@pinojs/redact@0.4.0", "", {}, "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg=="],
|
"@pinojs/redact": ["@pinojs/redact@0.4.0", "", {}, "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg=="],
|
||||||
|
|
||||||
"@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-rc.17", "", { "os": "android", "cpu": "arm64" }, "sha512-s70pVGhw4zqGeFnXWvAzJDlvxhlRollagdCCKRgOsgUOH3N1l0LIxf83AtGzmb5SiVM4Hjl5HyarMRfdfj3DaQ=="],
|
"@rolldown/binding-android-arm-eabi": ["@rolldown/binding-android-arm-eabi@1.2.5", "", { "os": "android", "cpu": "arm" }, "sha512-DLe/i+l8ynIBY7XEQ191TeZvCoowIGa18R+dIV30GW7DiOtp74i/xX8hs8GUjW5ARV7VZuie3d6AumSmCwbeRA=="],
|
||||||
|
|
||||||
"@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0-rc.17", "", { "os": "darwin", "cpu": "arm64" }, "sha512-4ksWc9n0mhlZpZ9PMZgTGjeOPRu8MB1Z3Tz0Mo02eWfWCHMW1zN82Qz/pL/rC+yQa+8ZnutMF0JjJe7PjwasYw=="],
|
"@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.2.5", "", { "os": "android", "cpu": "arm64" }, "sha512-zXcwKlQApYAOELHd8PwKDFkagYF9Wy4e0RJ+0qnzl9Pjnpj75TEG8ufv40p2J7kCEfwZAsNiuzRIyNNMWT38ig=="],
|
||||||
|
|
||||||
"@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.0-rc.17", "", { "os": "darwin", "cpu": "x64" }, "sha512-SUSDOI6WwUVNcWxd02QEBjLdY1VPHvlEkw6T/8nYG322iYWCTxRb1vzk4E+mWWYehTp7ERibq54LSJGjmouOsw=="],
|
"@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.2.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-dK4QakI42nzWgJT5sm4y4y/O//D4OxM75/cH28RLV+nzIN9AY+YsbuUVrUTjlLjXR6vpyxFbSsbmNuJ6BP9sww=="],
|
||||||
|
|
||||||
"@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.0-rc.17", "", { "os": "freebsd", "cpu": "x64" }, "sha512-hwnz3nw9dbJ05EDO/PvcjaaewqqDy7Y1rn1UO81l8iIK1GjenME75dl16ajbvSSMfv66WXSRCYKIqfgq2KCfxw=="],
|
"@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.2.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-fqSALaUu1Wjd1nK2uW2kJDWdLCc8lx1IcY+MTY26Aurfdx19anlzhqXOgCFbBFQnlFDTn4TC1/7Nz4Bl2mLP3A=="],
|
||||||
|
|
||||||
"@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.17", "", { "os": "linux", "cpu": "arm" }, "sha512-IS+W7epTcwANmFSQFrS1SivEXHtl1JtuQA9wlxrZTcNi6mx+FDOYrakGevvvTwgj2JvWiK8B29/qD9BELZPyXQ=="],
|
"@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.2.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-/vCnNxlkxs9tKxNDcyWUePpJ/PgTzxIaVhoM5SmG8UV+GR/IcPam4VYxi7GIMo7PSDuNqlJqvprqii9NqqVCMw=="],
|
||||||
|
|
||||||
"@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.0-rc.17", "", { "os": "linux", "cpu": "arm64" }, "sha512-e6usGaHKW5BMNZOymS1UcEYGowQMWcgZ71Z17Sl/h2+ZziNJ1a9n3Zvcz6LdRyIW5572wBCTH/Z+bKuZouGk9Q=="],
|
"@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.2.5", "", { "os": "linux", "cpu": "arm" }, "sha512-abk0NLA519LxRCszmbE0jYKuQ9YPocOXTiOXOo6Yr+YAT95VH+PtqYAjOJvGKt3viEd/x4qzabAlwd5bHOOARg=="],
|
||||||
|
|
||||||
"@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.0-rc.17", "", { "os": "linux", "cpu": "arm64" }, "sha512-b/CgbwAJpmrRLp02RPfhbudf5tZnN9nsPWK82znefso832etkem8H7FSZwxrOI9djcdTP7U6YfNhbRnh7djErg=="],
|
"@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.2.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-Y7eALiJ8lr0M2HH103Js+g7V34wf6snlpZLAsHI90uLhr3PVlNsbFVAXJC9d/V6BnPyKtpSwI+NcB/RLxsQxuA=="],
|
||||||
|
|
||||||
"@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.17", "", { "os": "linux", "cpu": "ppc64" }, "sha512-4EII1iNGRUN5WwGbF/kOh/EIkoDN9HsupgLQoXfY+D1oyJm7/F4t5PYU5n8SWZgG0FEwakyM8pGgwcBYruGTlA=="],
|
"@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.2.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-xMvZgnbZg4YVnR/AX2b3oOPDTFYJvUVaJg5FedA/LuvexAtXibZQej4cnTkw3rjsJ/ggUROB64TdtETiim+FYA=="],
|
||||||
|
|
||||||
"@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.0.0-rc.17", "", { "os": "linux", "cpu": "s390x" }, "sha512-AH8oq3XqQo4IibpVXvPeLDI5pzkpYn0WiZAfT05kFzoJ6tQNzwRdDYQ45M8I/gslbodRZwW8uxLhbSBbkv96rA=="],
|
"@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.2.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-GRjeqTUDHTo5GwntsLaAMcBahG3nlpjftXWZLN73HiYQlhwEowvarFgQnRnQZtIp4keXX7quXFbG38uPZBa2EA=="],
|
||||||
|
|
||||||
"@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.0-rc.17", "", { "os": "linux", "cpu": "x64" }, "sha512-cLnjV3xfo7KslbU41Z7z8BH/E1y5mzUYzAqih1d1MDaIGZRCMqTijqLv76/P7fyHuvUcfGsIpqCdddbxLLK9rA=="],
|
"@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.2.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-vLNTR45F2Uwc8AufkNXPmB4VliaXs+FvcheEogIzOXzO4l+LzieXF5A/TWxLy5HtqpsRCHUfd0lPVrrdgXdLHQ=="],
|
||||||
|
|
||||||
"@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.0-rc.17", "", { "os": "linux", "cpu": "x64" }, "sha512-0phclDw1spsL7dUB37sIARuis2tAgomCJXAHZlpt8PXZ4Ba0dRP1e+66lsRqrfhISeN9bEGNjQs+T/Fbd7oYGw=="],
|
"@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.2.5", "", { "os": "linux", "cpu": "x64" }, "sha512-Mgj59/HTuYeK9Gz2MA+mBWKnHsAgkBSec15ZMb1st3oIfFbX7gCjOae7GydHhzcyQi9Z/7M1QuN9bR3oFqF0jQ=="],
|
||||||
|
|
||||||
"@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.0-rc.17", "", { "os": "none", "cpu": "arm64" }, "sha512-0ag/hEgXOwgw4t8QyQvUCxvEg+V0KBcA6YuOx9g0r02MprutRF5dyljgm3EmR02O292UX7UeS6HzWHAl6KgyhA=="],
|
"@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.2.5", "", { "os": "linux", "cpu": "x64" }, "sha512-mY8AP0/ichsbhAxGnLa3d3+MwV0EfgrPND2bplI3Ym8T6R2pJ0N87bvrKVwNXmdy3jnr6eQBecdqx/HMknBmpA=="],
|
||||||
|
|
||||||
"@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.0-rc.17", "", { "dependencies": { "@emnapi/core": "1.10.0", "@emnapi/runtime": "1.10.0", "@napi-rs/wasm-runtime": "^1.1.4" }, "cpu": "none" }, "sha512-LEXei6vo0E5wTGwpkJ4KoT3OZJRnglwldt5ziLzOlc6qqb55z4tWNq2A+PFqCJuvWWdP53CVhG1Z9NtToDPJrA=="],
|
"@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.2.5", "", { "os": "none", "cpu": "arm64" }, "sha512-8SLssA2oweAxyRgDp789ACfRb/3P+zNRJpzZxSizxF9m8NUDQ4+3xjo8ttjhVGGw6Qxb70oZiEtIjaKikCO7Yw=="],
|
||||||
|
|
||||||
"@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.0-rc.17", "", { "os": "win32", "cpu": "arm64" }, "sha512-gUmyzBl3SPMa6hrqFUth9sVfcLBlYsbMzBx5PlexMroZStgzGqlZ26pYG89rBb45Mnia+oil6YAIFeEWGWhoZA=="],
|
"@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.2.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-vGbruD5zquhoc8D9SViXgN2FBJtNdTyQ4DtG+SWiEGlJiAzoKcZ2xp+xuXCffhubVdt0NJlTZqkeRuERy7g8Cw=="],
|
||||||
|
|
||||||
"@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.0-rc.17", "", { "os": "win32", "cpu": "x64" }, "sha512-3hkiolcUAvPB9FLb3UZdfjVVNWherN1f/skkGWJP/fgSQhYUZpSIRr0/I8ZK9TkF3F7kxvJAk0+IcKvPHk9qQg=="],
|
"@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.2.5", "", { "os": "win32", "cpu": "x64" }, "sha512-e/SXpgISz+IoqVcSSI0rx/d/he8zqLex+/rCWpnHpmVfmPIUjag9H6P7zotf0gJHwPUhQxZ/mF8tr6acebT9yw=="],
|
||||||
|
|
||||||
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.7", "", {}, "sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA=="],
|
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.7", "", {}, "sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA=="],
|
||||||
|
|
||||||
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.57.1", "", { "os": "android", "cpu": "arm" }, "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg=="],
|
|
||||||
|
|
||||||
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.57.1", "", { "os": "android", "cpu": "arm64" }, "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w=="],
|
|
||||||
|
|
||||||
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.57.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg=="],
|
|
||||||
|
|
||||||
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.57.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w=="],
|
|
||||||
|
|
||||||
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.57.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug=="],
|
|
||||||
|
|
||||||
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.57.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q=="],
|
|
||||||
|
|
||||||
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.57.1", "", { "os": "linux", "cpu": "arm" }, "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw=="],
|
|
||||||
|
|
||||||
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.57.1", "", { "os": "linux", "cpu": "arm" }, "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw=="],
|
|
||||||
|
|
||||||
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.57.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g=="],
|
|
||||||
|
|
||||||
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.57.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q=="],
|
|
||||||
|
|
||||||
"@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.57.1", "", { "os": "linux", "cpu": "none" }, "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA=="],
|
|
||||||
|
|
||||||
"@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.57.1", "", { "os": "linux", "cpu": "none" }, "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw=="],
|
|
||||||
|
|
||||||
"@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.57.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w=="],
|
|
||||||
|
|
||||||
"@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.57.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw=="],
|
|
||||||
|
|
||||||
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.57.1", "", { "os": "linux", "cpu": "none" }, "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A=="],
|
|
||||||
|
|
||||||
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.57.1", "", { "os": "linux", "cpu": "none" }, "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw=="],
|
|
||||||
|
|
||||||
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.57.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg=="],
|
|
||||||
|
|
||||||
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.57.1", "", { "os": "linux", "cpu": "x64" }, "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg=="],
|
|
||||||
|
|
||||||
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.57.1", "", { "os": "linux", "cpu": "x64" }, "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw=="],
|
|
||||||
|
|
||||||
"@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.57.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw=="],
|
|
||||||
|
|
||||||
"@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.57.1", "", { "os": "none", "cpu": "arm64" }, "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ=="],
|
|
||||||
|
|
||||||
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.57.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ=="],
|
|
||||||
|
|
||||||
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.57.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew=="],
|
|
||||||
|
|
||||||
"@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.57.1", "", { "os": "win32", "cpu": "x64" }, "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ=="],
|
|
||||||
|
|
||||||
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.57.1", "", { "os": "win32", "cpu": "x64" }, "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA=="],
|
|
||||||
|
|
||||||
"@sapphire/async-queue": ["@sapphire/async-queue@1.5.5", "", {}, "sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg=="],
|
"@sapphire/async-queue": ["@sapphire/async-queue@1.5.5", "", {}, "sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg=="],
|
||||||
|
|
||||||
"@sapphire/shapeshift": ["@sapphire/shapeshift@4.0.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "lodash": "^4.17.21" } }, "sha512-d9dUmWVA7MMiKobL3VpLF8P2aeanRTu6ypG2OIaEv/ZHH/SUQ2iHOVyi5wAPjQ+HmnMuL0whK9ez8I/raWbtIg=="],
|
"@sapphire/shapeshift": ["@sapphire/shapeshift@4.0.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "lodash": "^4.17.21" } }, "sha512-d9dUmWVA7MMiKobL3VpLF8P2aeanRTu6ypG2OIaEv/ZHH/SUQ2iHOVyi5wAPjQ+HmnMuL0whK9ez8I/raWbtIg=="],
|
||||||
|
|
||||||
"@sapphire/snowflake": ["@sapphire/snowflake@3.5.3", "", {}, "sha512-jjmJywLAFoWeBi1W7994zZyiNWPIiqRRNAmSERxyg93xRGzNYvGjlZ0gR6x0F4gPRi2+0O6S71kOZYyr3cxaIQ=="],
|
"@sapphire/snowflake": ["@sapphire/snowflake@3.5.5", "", {}, "sha512-xzvBr1Q1c4lCe7i6sRnrofxeO1QTP/LKQ6A6qy0iB4x5yfiSfARMEQEghojzTNALDTcv8En04qYNIco9/K9eZQ=="],
|
||||||
|
|
||||||
"@stablelib/base64": ["@stablelib/base64@1.0.1", "", {}, "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ=="],
|
"@stablelib/base64": ["@stablelib/base64@1.0.1", "", {}, "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ=="],
|
||||||
|
|
||||||
"@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
"@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||||
|
|
||||||
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
|
|
||||||
|
|
||||||
"@types/better-sqlite3": ["@types/better-sqlite3@7.6.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA=="],
|
"@types/better-sqlite3": ["@types/better-sqlite3@7.6.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA=="],
|
||||||
|
|
||||||
"@types/bun": ["@types/bun@1.3.11", "", { "dependencies": { "bun-types": "1.3.11" } }, "sha512-5vPne5QvtpjGpsGYXiFyycfpDF2ECyPcTSsFBMa0fraoxiQyMJ3SmuQIGhzPg2WJuWxVBoxWJ2kClYTcw/4fAg=="],
|
"@types/bun": ["@types/bun@1.3.11", "", { "dependencies": { "bun-types": "1.3.11" } }, "sha512-5vPne5QvtpjGpsGYXiFyycfpDF2ECyPcTSsFBMa0fraoxiQyMJ3SmuQIGhzPg2WJuWxVBoxWJ2kClYTcw/4fAg=="],
|
||||||
@@ -391,21 +279,21 @@
|
|||||||
|
|
||||||
"@vitejs/plugin-react": ["@vitejs/plugin-react@6.0.1", "", { "dependencies": { "@rolldown/pluginutils": "1.0.0-rc.7" }, "peerDependencies": { "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", "babel-plugin-react-compiler": "^1.0.0", "vite": "^8.0.0" }, "optionalPeers": ["@rolldown/plugin-babel", "babel-plugin-react-compiler"] }, "sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ=="],
|
"@vitejs/plugin-react": ["@vitejs/plugin-react@6.0.1", "", { "dependencies": { "@rolldown/pluginutils": "1.0.0-rc.7" }, "peerDependencies": { "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", "babel-plugin-react-compiler": "^1.0.0", "vite": "^8.0.0" }, "optionalPeers": ["@rolldown/plugin-babel", "babel-plugin-react-compiler"] }, "sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ=="],
|
||||||
|
|
||||||
"@vitest/coverage-v8": ["@vitest/coverage-v8@4.0.18", "", { "dependencies": { "@bcoe/v8-coverage": "^1.0.2", "@vitest/utils": "4.0.18", "ast-v8-to-istanbul": "^0.3.10", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", "istanbul-reports": "^3.2.0", "magicast": "^0.5.1", "obug": "^2.1.1", "std-env": "^3.10.0", "tinyrainbow": "^3.0.3" }, "peerDependencies": { "@vitest/browser": "4.0.18", "vitest": "4.0.18" }, "optionalPeers": ["@vitest/browser"] }, "sha512-7i+N2i0+ME+2JFZhfuz7Tg/FqKtilHjGyGvoHYQ6iLV0zahbsJ9sljC9OcFcPDbhYKCet+sG8SsVqlyGvPflZg=="],
|
"@vitest/coverage-v8": ["@vitest/coverage-v8@4.1.11", "", { "dependencies": { "@bcoe/v8-coverage": "^1.0.2", "@vitest/utils": "4.1.11", "ast-v8-to-istanbul": "^1.0.0", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", "istanbul-reports": "^3.2.0", "magicast": "^0.5.2", "obug": "^2.1.1", "std-env": "^4.0.0-rc.1", "tinyrainbow": "^3.1.0" }, "peerDependencies": { "@vitest/browser": "4.1.11", "vitest": "4.1.11" }, "optionalPeers": ["@vitest/browser"] }, "sha512-8MVGEFnJIcdGjcbfKmeq8z0pZHH0JlVtoVZH9Q/qwUp6wyFnEJUBMrw9DCaj+ra3vShGmhavjalMIhPNxZAUcw=="],
|
||||||
|
|
||||||
"@vitest/expect": ["@vitest/expect@4.0.18", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.0.18", "@vitest/utils": "4.0.18", "chai": "^6.2.1", "tinyrainbow": "^3.0.3" } }, "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ=="],
|
"@vitest/expect": ["@vitest/expect@4.1.11", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.11", "@vitest/utils": "4.1.11", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" } }, "sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw=="],
|
||||||
|
|
||||||
"@vitest/mocker": ["@vitest/mocker@4.0.18", "", { "dependencies": { "@vitest/spy": "4.0.18", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0-0" }, "optionalPeers": ["msw"] }, "sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ=="],
|
"@vitest/mocker": ["@vitest/mocker@4.1.11", "", { "dependencies": { "@vitest/spy": "4.1.11", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ=="],
|
||||||
|
|
||||||
"@vitest/pretty-format": ["@vitest/pretty-format@4.0.18", "", { "dependencies": { "tinyrainbow": "^3.0.3" } }, "sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw=="],
|
"@vitest/pretty-format": ["@vitest/pretty-format@4.1.11", "", { "dependencies": { "tinyrainbow": "^3.1.0" } }, "sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw=="],
|
||||||
|
|
||||||
"@vitest/runner": ["@vitest/runner@4.0.18", "", { "dependencies": { "@vitest/utils": "4.0.18", "pathe": "^2.0.3" } }, "sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw=="],
|
"@vitest/runner": ["@vitest/runner@4.1.11", "", { "dependencies": { "@vitest/utils": "4.1.11", "pathe": "^2.0.3" } }, "sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw=="],
|
||||||
|
|
||||||
"@vitest/snapshot": ["@vitest/snapshot@4.0.18", "", { "dependencies": { "@vitest/pretty-format": "4.0.18", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA=="],
|
"@vitest/snapshot": ["@vitest/snapshot@4.1.11", "", { "dependencies": { "@vitest/pretty-format": "4.1.11", "@vitest/utils": "4.1.11", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog=="],
|
||||||
|
|
||||||
"@vitest/spy": ["@vitest/spy@4.0.18", "", {}, "sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw=="],
|
"@vitest/spy": ["@vitest/spy@4.1.11", "", {}, "sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA=="],
|
||||||
|
|
||||||
"@vitest/utils": ["@vitest/utils@4.0.18", "", { "dependencies": { "@vitest/pretty-format": "4.0.18", "tinyrainbow": "^3.0.3" } }, "sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA=="],
|
"@vitest/utils": ["@vitest/utils@4.1.11", "", { "dependencies": { "@vitest/pretty-format": "4.1.11", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ=="],
|
||||||
|
|
||||||
"@vladfrangu/async_event_emitter": ["@vladfrangu/async_event_emitter@2.4.7", "", {}, "sha512-Xfe6rpCTxSxfbswi/W/Pz7zp1WWSNn4A0eW4mLkQUewCrXXtMj31lCg+iQyTkh/CkusZSq9eDflu7tjEDXUY6g=="],
|
"@vladfrangu/async_event_emitter": ["@vladfrangu/async_event_emitter@2.4.7", "", {}, "sha512-Xfe6rpCTxSxfbswi/W/Pz7zp1WWSNn4A0eW4mLkQUewCrXXtMj31lCg+iQyTkh/CkusZSq9eDflu7tjEDXUY6g=="],
|
||||||
|
|
||||||
@@ -421,7 +309,7 @@
|
|||||||
|
|
||||||
"assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="],
|
"assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="],
|
||||||
|
|
||||||
"ast-v8-to-istanbul": ["ast-v8-to-istanbul@0.3.11", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.31", "estree-walker": "^3.0.3", "js-tokens": "^10.0.0" } }, "sha512-Qya9fkoofMjCBNVdWINMjB5KZvkYfaO9/anwkWnjxibpWUxo5iHl2sOdP7/uAqaRuUYuoo8rDwnbaaKVFxoUvw=="],
|
"ast-v8-to-istanbul": ["ast-v8-to-istanbul@1.0.5", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.31", "estree-walker": "^3.0.3", "js-tokens": "^10.0.0" } }, "sha512-UPAgKJFSEGMWSDr3LX4tqnAb4f7KGT8O40Tyx8wbYmmZ/yn58lNCm8h3svs3eXgiGd5AXxz8NDOvXWvicq+rJA=="],
|
||||||
|
|
||||||
"atomic-sleep": ["atomic-sleep@1.0.0", "", {}, "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ=="],
|
"atomic-sleep": ["atomic-sleep@1.0.0", "", {}, "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ=="],
|
||||||
|
|
||||||
@@ -475,13 +363,15 @@
|
|||||||
|
|
||||||
"content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="],
|
"content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="],
|
||||||
|
|
||||||
|
"convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
|
||||||
|
|
||||||
"cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
|
"cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
|
||||||
|
|
||||||
"cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="],
|
"cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="],
|
||||||
|
|
||||||
"cors": ["cors@2.8.6", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw=="],
|
"cors": ["cors@2.8.6", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw=="],
|
||||||
|
|
||||||
"cron-parser": ["cron-parser@5.5.0", "", { "dependencies": { "luxon": "^3.7.1" } }, "sha512-oML4lKUXxizYswqmxuOCpgFS8BNUJpIu6k/2HVHyaL8Ynnf3wdf9tkns0yRdJLSIjkJ+b0DXHMZEHGpMwjnPww=="],
|
"cron-parser": ["cron-parser@5.10.0", "", { "dependencies": { "luxon": "^3.7.2" } }, "sha512-izNAxJyRWUP8ljBoDSub5WyrVOUlT4SLGShswE7eoRBpp6QUsSycYxLBMJlbshgPBMcPT/nrfgjNY2918ayv2A=="],
|
||||||
|
|
||||||
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
|
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
|
||||||
|
|
||||||
@@ -507,9 +397,9 @@
|
|||||||
|
|
||||||
"devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="],
|
"devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="],
|
||||||
|
|
||||||
"discord-api-types": ["discord-api-types@0.38.41", "", {}, "sha512-yMECyR8j9c2fVTvCQ+Qc24pweYFIZk/XoxDOmt1UvPeSw5tK6gXBd/2hhP+FEAe9Y6ny8pRMaf618XDK4U53OQ=="],
|
"discord-api-types": ["discord-api-types@0.38.53", "", {}, "sha512-HL1zz/UuZ+bbJjA/X8Kbxx9gk8v9rJAbTeWRNYKmIdjwJ7EovjlHgoJTxcLpATfNJ+AonOtMdy3Y5MVIJAAd/A=="],
|
||||||
|
|
||||||
"discord.js": ["discord.js@14.25.1", "", { "dependencies": { "@discordjs/builders": "^1.13.0", "@discordjs/collection": "1.5.3", "@discordjs/formatters": "^0.6.2", "@discordjs/rest": "^2.6.0", "@discordjs/util": "^1.2.0", "@discordjs/ws": "^1.2.3", "@sapphire/snowflake": "3.5.3", "discord-api-types": "^0.38.33", "fast-deep-equal": "3.1.3", "lodash.snakecase": "4.1.1", "magic-bytes.js": "^1.10.0", "tslib": "^2.6.3", "undici": "6.21.3" } }, "sha512-2l0gsPOLPs5t6GFZfQZKnL1OJNYFcuC/ETWsW4VtKVD/tg4ICa9x+jb9bkPffkMdRpRpuUaO/fKkHCBeiCKh8g=="],
|
"discord.js": ["discord.js@14.27.0", "", { "dependencies": { "@discordjs/builders": "^1.14.1", "@discordjs/collection": "1.5.3", "@discordjs/formatters": "^0.6.2", "@discordjs/rest": "^2.6.2", "@discordjs/util": "^1.2.0", "@discordjs/ws": "^1.2.3", "@sapphire/snowflake": "3.5.5", "discord-api-types": "^0.38.49", "fast-deep-equal": "3.1.3", "lodash.snakecase": "4.1.1", "magic-bytes.js": "^1.13.0", "tslib": "^2.6.3", "undici": "^6.27.0" } }, "sha512-qHbFlFG2N7y3LjPySYsL6A1+BnX6bkTVgo842EX0CqVPk/KTMwZkojPHEXKsQUpWZNyz5BISNHK1cPpQw0+m4A=="],
|
||||||
|
|
||||||
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
|
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
|
||||||
|
|
||||||
@@ -529,12 +419,10 @@
|
|||||||
|
|
||||||
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
|
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
|
||||||
|
|
||||||
"es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="],
|
"es-module-lexer": ["es-module-lexer@2.3.2", "", {}, "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw=="],
|
||||||
|
|
||||||
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
|
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
|
||||||
|
|
||||||
"esbuild": ["esbuild@0.27.3", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.3", "@esbuild/android-arm": "0.27.3", "@esbuild/android-arm64": "0.27.3", "@esbuild/android-x64": "0.27.3", "@esbuild/darwin-arm64": "0.27.3", "@esbuild/darwin-x64": "0.27.3", "@esbuild/freebsd-arm64": "0.27.3", "@esbuild/freebsd-x64": "0.27.3", "@esbuild/linux-arm": "0.27.3", "@esbuild/linux-arm64": "0.27.3", "@esbuild/linux-ia32": "0.27.3", "@esbuild/linux-loong64": "0.27.3", "@esbuild/linux-mips64el": "0.27.3", "@esbuild/linux-ppc64": "0.27.3", "@esbuild/linux-riscv64": "0.27.3", "@esbuild/linux-s390x": "0.27.3", "@esbuild/linux-x64": "0.27.3", "@esbuild/netbsd-arm64": "0.27.3", "@esbuild/netbsd-x64": "0.27.3", "@esbuild/openbsd-arm64": "0.27.3", "@esbuild/openbsd-x64": "0.27.3", "@esbuild/openharmony-arm64": "0.27.3", "@esbuild/sunos-x64": "0.27.3", "@esbuild/win32-arm64": "0.27.3", "@esbuild/win32-ia32": "0.27.3", "@esbuild/win32-x64": "0.27.3" }, "bin": "bin/esbuild" }, "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg=="],
|
|
||||||
|
|
||||||
"escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="],
|
"escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="],
|
||||||
|
|
||||||
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
|
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
|
||||||
@@ -617,8 +505,6 @@
|
|||||||
|
|
||||||
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
|
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
|
||||||
|
|
||||||
"get-tsconfig": ["get-tsconfig@4.13.6", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw=="],
|
|
||||||
|
|
||||||
"github-from-package": ["github-from-package@0.0.0", "", {}, "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw=="],
|
"github-from-package": ["github-from-package@0.0.0", "", {}, "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw=="],
|
||||||
|
|
||||||
"glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
|
"glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
|
||||||
@@ -711,29 +597,29 @@
|
|||||||
|
|
||||||
"levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
|
"levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
|
||||||
|
|
||||||
"lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="],
|
"lightningcss": ["lightningcss@1.33.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.33.0", "lightningcss-darwin-arm64": "1.33.0", "lightningcss-darwin-x64": "1.33.0", "lightningcss-freebsd-x64": "1.33.0", "lightningcss-linux-arm-gnueabihf": "1.33.0", "lightningcss-linux-arm64-gnu": "1.33.0", "lightningcss-linux-arm64-musl": "1.33.0", "lightningcss-linux-x64-gnu": "1.33.0", "lightningcss-linux-x64-musl": "1.33.0", "lightningcss-win32-arm64-msvc": "1.33.0", "lightningcss-win32-x64-msvc": "1.33.0" } }, "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA=="],
|
||||||
|
|
||||||
"lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="],
|
"lightningcss-android-arm64": ["lightningcss-android-arm64@1.33.0", "", { "os": "android", "cpu": "arm64" }, "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg=="],
|
||||||
|
|
||||||
"lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="],
|
"lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.33.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg=="],
|
||||||
|
|
||||||
"lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="],
|
"lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.33.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ=="],
|
||||||
|
|
||||||
"lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="],
|
"lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.33.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg=="],
|
||||||
|
|
||||||
"lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="],
|
"lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.33.0", "", { "os": "linux", "cpu": "arm" }, "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ=="],
|
||||||
|
|
||||||
"lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="],
|
"lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.33.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg=="],
|
||||||
|
|
||||||
"lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="],
|
"lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.33.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ=="],
|
||||||
|
|
||||||
"lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="],
|
"lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.33.0", "", { "os": "linux", "cpu": "x64" }, "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg=="],
|
||||||
|
|
||||||
"lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="],
|
"lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.33.0", "", { "os": "linux", "cpu": "x64" }, "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw=="],
|
||||||
|
|
||||||
"lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="],
|
"lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.33.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA=="],
|
||||||
|
|
||||||
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="],
|
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.33.0", "", { "os": "win32", "cpu": "x64" }, "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA=="],
|
||||||
|
|
||||||
"locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],
|
"locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],
|
||||||
|
|
||||||
@@ -863,7 +749,7 @@
|
|||||||
|
|
||||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||||
|
|
||||||
"nanoid": ["nanoid@3.3.11", "", { "bin": "bin/nanoid.cjs" }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
"nanoid": ["nanoid@3.3.18", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w=="],
|
||||||
|
|
||||||
"napi-build-utils": ["napi-build-utils@2.0.0", "", {}, "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA=="],
|
"napi-build-utils": ["napi-build-utils@2.0.0", "", {}, "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA=="],
|
||||||
|
|
||||||
@@ -905,7 +791,7 @@
|
|||||||
|
|
||||||
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||||
|
|
||||||
"picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="],
|
"picomatch": ["picomatch@4.0.5", "", {}, "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A=="],
|
||||||
|
|
||||||
"pino": ["pino@9.14.0", "", { "dependencies": { "@pinojs/redact": "^0.4.0", "atomic-sleep": "^1.0.0", "on-exit-leak-free": "^2.1.0", "pino-abstract-transport": "^2.0.0", "pino-std-serializers": "^7.0.0", "process-warning": "^5.0.0", "quick-format-unescaped": "^4.0.3", "real-require": "^0.2.0", "safe-stable-stringify": "^2.3.1", "sonic-boom": "^4.0.1", "thread-stream": "^3.0.0" }, "bin": "bin.js" }, "sha512-8OEwKp5juEvb/MjpIc4hjqfgCNysrS94RIOMXYvpYCdm/jglrKEiAYmiumbmGhCvs+IcInsphYDFwqrjr7398w=="],
|
"pino": ["pino@9.14.0", "", { "dependencies": { "@pinojs/redact": "^0.4.0", "atomic-sleep": "^1.0.0", "on-exit-leak-free": "^2.1.0", "pino-abstract-transport": "^2.0.0", "pino-std-serializers": "^7.0.0", "process-warning": "^5.0.0", "quick-format-unescaped": "^4.0.3", "real-require": "^0.2.0", "safe-stable-stringify": "^2.3.1", "sonic-boom": "^4.0.1", "thread-stream": "^3.0.0" }, "bin": "bin.js" }, "sha512-8OEwKp5juEvb/MjpIc4hjqfgCNysrS94RIOMXYvpYCdm/jglrKEiAYmiumbmGhCvs+IcInsphYDFwqrjr7398w=="],
|
||||||
|
|
||||||
@@ -921,7 +807,7 @@
|
|||||||
|
|
||||||
"playwright-core": ["playwright-core@1.59.1", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg=="],
|
"playwright-core": ["playwright-core@1.59.1", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg=="],
|
||||||
|
|
||||||
"postcss": ["postcss@8.5.10", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ=="],
|
"postcss": ["postcss@8.5.26", "", { "dependencies": { "nanoid": "^3.3.17", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ=="],
|
||||||
|
|
||||||
"prebuild-install": ["prebuild-install@7.1.3", "", { "dependencies": { "detect-libc": "^2.0.0", "expand-template": "^2.0.3", "github-from-package": "0.0.0", "minimist": "^1.2.3", "mkdirp-classic": "^0.5.3", "napi-build-utils": "^2.0.0", "node-abi": "^3.3.0", "pump": "^3.0.0", "rc": "^1.2.7", "simple-get": "^4.0.0", "tar-fs": "^2.0.0", "tunnel-agent": "^0.6.0" }, "bin": { "prebuild-install": "bin.js" } }, "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug=="],
|
"prebuild-install": ["prebuild-install@7.1.3", "", { "dependencies": { "detect-libc": "^2.0.0", "expand-template": "^2.0.3", "github-from-package": "0.0.0", "minimist": "^1.2.3", "mkdirp-classic": "^0.5.3", "napi-build-utils": "^2.0.0", "node-abi": "^3.3.0", "pump": "^3.0.0", "rc": "^1.2.7", "simple-get": "^4.0.0", "tar-fs": "^2.0.0", "tunnel-agent": "^0.6.0" }, "bin": { "prebuild-install": "bin.js" } }, "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug=="],
|
||||||
|
|
||||||
@@ -969,11 +855,7 @@
|
|||||||
|
|
||||||
"require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
|
"require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
|
||||||
|
|
||||||
"resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="],
|
"rolldown": ["rolldown@1.2.5", "", { "dependencies": { "@oxc-project/types": "=0.146.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm-eabi": "1.2.5", "@rolldown/binding-android-arm64": "1.2.5", "@rolldown/binding-darwin-arm64": "1.2.5", "@rolldown/binding-darwin-x64": "1.2.5", "@rolldown/binding-freebsd-x64": "1.2.5", "@rolldown/binding-linux-arm-gnueabihf": "1.2.5", "@rolldown/binding-linux-arm64-gnu": "1.2.5", "@rolldown/binding-linux-arm64-musl": "1.2.5", "@rolldown/binding-linux-ppc64-gnu": "1.2.5", "@rolldown/binding-linux-s390x-gnu": "1.2.5", "@rolldown/binding-linux-x64-gnu": "1.2.5", "@rolldown/binding-linux-x64-musl": "1.2.5", "@rolldown/binding-openharmony-arm64": "1.2.5", "@rolldown/binding-win32-arm64-msvc": "1.2.5", "@rolldown/binding-win32-x64-msvc": "1.2.5" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-VD2IE5PUG4Oj8zz2VGykiYd5wbnjdIiSsNQb8Qu5B+noEp+A78mu2iVvpp27g8es14Tk9rofNs5Tku9iQCS4fA=="],
|
||||||
|
|
||||||
"rolldown": ["rolldown@1.0.0-rc.17", "", { "dependencies": { "@oxc-project/types": "=0.127.0", "@rolldown/pluginutils": "1.0.0-rc.17" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-rc.17", "@rolldown/binding-darwin-arm64": "1.0.0-rc.17", "@rolldown/binding-darwin-x64": "1.0.0-rc.17", "@rolldown/binding-freebsd-x64": "1.0.0-rc.17", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.17", "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.17", "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.17", "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.17", "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.17", "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.17", "@rolldown/binding-linux-x64-musl": "1.0.0-rc.17", "@rolldown/binding-openharmony-arm64": "1.0.0-rc.17", "@rolldown/binding-wasm32-wasi": "1.0.0-rc.17", "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.17", "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.17" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-ZrT53oAKrtA4+YtBWPQbtPOxIbVDbxT0orcYERKd63VJTF13zPcgXTvD4843L8pcsI7M6MErt8QtON6lrB9tyA=="],
|
|
||||||
|
|
||||||
"rollup": ["rollup@4.57.1", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.57.1", "@rollup/rollup-android-arm64": "4.57.1", "@rollup/rollup-darwin-arm64": "4.57.1", "@rollup/rollup-darwin-x64": "4.57.1", "@rollup/rollup-freebsd-arm64": "4.57.1", "@rollup/rollup-freebsd-x64": "4.57.1", "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", "@rollup/rollup-linux-arm-musleabihf": "4.57.1", "@rollup/rollup-linux-arm64-gnu": "4.57.1", "@rollup/rollup-linux-arm64-musl": "4.57.1", "@rollup/rollup-linux-loong64-gnu": "4.57.1", "@rollup/rollup-linux-loong64-musl": "4.57.1", "@rollup/rollup-linux-ppc64-gnu": "4.57.1", "@rollup/rollup-linux-ppc64-musl": "4.57.1", "@rollup/rollup-linux-riscv64-gnu": "4.57.1", "@rollup/rollup-linux-riscv64-musl": "4.57.1", "@rollup/rollup-linux-s390x-gnu": "4.57.1", "@rollup/rollup-linux-x64-gnu": "4.57.1", "@rollup/rollup-linux-x64-musl": "4.57.1", "@rollup/rollup-openbsd-x64": "4.57.1", "@rollup/rollup-openharmony-arm64": "4.57.1", "@rollup/rollup-win32-arm64-msvc": "4.57.1", "@rollup/rollup-win32-ia32-msvc": "4.57.1", "@rollup/rollup-win32-x64-gnu": "4.57.1", "@rollup/rollup-win32-x64-msvc": "4.57.1", "fsevents": "~2.3.2" }, "bin": "dist/bin/rollup" }, "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A=="],
|
|
||||||
|
|
||||||
"router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="],
|
"router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="],
|
||||||
|
|
||||||
@@ -1027,7 +909,7 @@
|
|||||||
|
|
||||||
"statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
|
"statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
|
||||||
|
|
||||||
"std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="],
|
"std-env": ["std-env@4.2.0", "", {}, "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw=="],
|
||||||
|
|
||||||
"string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="],
|
"string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="],
|
||||||
|
|
||||||
@@ -1051,9 +933,9 @@
|
|||||||
|
|
||||||
"tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="],
|
"tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="],
|
||||||
|
|
||||||
"tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="],
|
"tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="],
|
||||||
|
|
||||||
"tinyrainbow": ["tinyrainbow@3.0.3", "", {}, "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q=="],
|
"tinyrainbow": ["tinyrainbow@3.1.1", "", {}, "sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw=="],
|
||||||
|
|
||||||
"toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
|
"toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
|
||||||
|
|
||||||
@@ -1069,8 +951,6 @@
|
|||||||
|
|
||||||
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||||
|
|
||||||
"tsx": ["tsx@4.21.0", "", { "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": "dist/cli.mjs" }, "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw=="],
|
|
||||||
|
|
||||||
"tunnel-agent": ["tunnel-agent@0.6.0", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w=="],
|
"tunnel-agent": ["tunnel-agent@0.6.0", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w=="],
|
||||||
|
|
||||||
"type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
|
"type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
|
||||||
@@ -1081,7 +961,7 @@
|
|||||||
|
|
||||||
"typescript-eslint": ["typescript-eslint@8.59.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.59.0", "@typescript-eslint/parser": "8.59.0", "@typescript-eslint/typescript-estree": "8.59.0", "@typescript-eslint/utils": "8.59.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-BU3ONW9X+v90EcCH9ZS6LMackcVtxRLlI3XrYyqZIwVSHIk7Qf7bFw1z0M9Q0IUxhTMZCf8piY9hTYaNEIASrw=="],
|
"typescript-eslint": ["typescript-eslint@8.59.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.59.0", "@typescript-eslint/parser": "8.59.0", "@typescript-eslint/typescript-estree": "8.59.0", "@typescript-eslint/utils": "8.59.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-BU3ONW9X+v90EcCH9ZS6LMackcVtxRLlI3XrYyqZIwVSHIk7Qf7bFw1z0M9Q0IUxhTMZCf8piY9hTYaNEIASrw=="],
|
||||||
|
|
||||||
"undici": ["undici@6.21.3", "", {}, "sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw=="],
|
"undici": ["undici@6.28.0", "", {}, "sha512-LIY910g9TI13YS95lrMFrs8Rm/u/irgHeTWoKCoteeJ04CUJ92eEfj0rVn+7VKMPBpUPiUoBKfhNyLI23EE/KA=="],
|
||||||
|
|
||||||
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
||||||
|
|
||||||
@@ -1109,9 +989,9 @@
|
|||||||
|
|
||||||
"vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="],
|
"vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="],
|
||||||
|
|
||||||
"vite": ["vite@8.0.10", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.10", "rolldown": "1.0.0-rc.17", "tinyglobby": "^0.2.16" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-rZuUu9j6J5uotLDs+cAA4O5H4K1SfPliUlQwqa6YEwSrWDZzP4rhm00oJR5snMewjxF5V/K3D4kctsUTsIU9Mw=="],
|
"vite": ["vite@8.2.2", "", { "dependencies": { "lightningcss": "^1.33.0", "picomatch": "^4.0.5", "postcss": "^8.5.26", "rolldown": "~1.2.4", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.4.0 || ^0.5.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-cFKLV/PRgAUlIRm5WjMjJ86jrftzpqcgH+Us+DS8mI3CDNiH30Whrz8uHL3+MOLPAgqbMBAqWdAHAphOAM+z/Q=="],
|
||||||
|
|
||||||
"vitest": ["vitest@4.0.18", "", { "dependencies": { "@vitest/expect": "4.0.18", "@vitest/mocker": "4.0.18", "@vitest/pretty-format": "4.0.18", "@vitest/runner": "4.0.18", "@vitest/snapshot": "4.0.18", "@vitest/spy": "4.0.18", "@vitest/utils": "4.0.18", "es-module-lexer": "^1.7.0", "expect-type": "^1.2.2", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^3.10.0", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.0.3", "vite": "^6.0.0 || ^7.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.0.18", "@vitest/browser-preview": "4.0.18", "@vitest/browser-webdriverio": "4.0.18", "@vitest/ui": "4.0.18", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/ui", "happy-dom", "jsdom"], "bin": "vitest.mjs" }, "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ=="],
|
"vitest": ["vitest@4.1.11", "", { "dependencies": { "@vitest/expect": "4.1.11", "@vitest/mocker": "4.1.11", "@vitest/pretty-format": "4.1.11", "@vitest/runner": "4.1.11", "@vitest/snapshot": "4.1.11", "@vitest/spy": "4.1.11", "@vitest/utils": "4.1.11", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.1.0", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.1.11", "@vitest/browser-preview": "4.1.11", "@vitest/browser-webdriverio": "4.1.11", "@vitest/coverage-istanbul": "4.1.11", "@vitest/coverage-v8": "4.1.11", "@vitest/ui": "4.1.11", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/coverage-istanbul", "@vitest/coverage-v8", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "./vitest.mjs" } }, "sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw=="],
|
||||||
|
|
||||||
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
||||||
|
|
||||||
@@ -1123,7 +1003,7 @@
|
|||||||
|
|
||||||
"ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="],
|
"ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="],
|
||||||
|
|
||||||
"yaml": ["yaml@2.8.2", "", { "bin": "bin.mjs" }, "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A=="],
|
"yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="],
|
||||||
|
|
||||||
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
|
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
|
||||||
|
|
||||||
@@ -1133,20 +1013,32 @@
|
|||||||
|
|
||||||
"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
|
"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
|
||||||
|
|
||||||
|
"@discordjs/formatters/discord-api-types": ["discord-api-types@0.38.41", "", {}, "sha512-yMECyR8j9c2fVTvCQ+Qc24pweYFIZk/XoxDOmt1UvPeSw5tK6gXBd/2hhP+FEAe9Y6ny8pRMaf618XDK4U53OQ=="],
|
||||||
|
|
||||||
"@discordjs/rest/@discordjs/collection": ["@discordjs/collection@2.1.1", "", {}, "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg=="],
|
"@discordjs/rest/@discordjs/collection": ["@discordjs/collection@2.1.1", "", {}, "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg=="],
|
||||||
|
|
||||||
|
"@discordjs/util/discord-api-types": ["discord-api-types@0.38.41", "", {}, "sha512-yMECyR8j9c2fVTvCQ+Qc24pweYFIZk/XoxDOmt1UvPeSw5tK6gXBd/2hhP+FEAe9Y6ny8pRMaf618XDK4U53OQ=="],
|
||||||
|
|
||||||
"@discordjs/ws/@discordjs/collection": ["@discordjs/collection@2.1.1", "", {}, "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg=="],
|
"@discordjs/ws/@discordjs/collection": ["@discordjs/collection@2.1.1", "", {}, "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg=="],
|
||||||
|
|
||||||
|
"@discordjs/ws/@discordjs/rest": ["@discordjs/rest@2.6.0", "", { "dependencies": { "@discordjs/collection": "^2.1.1", "@discordjs/util": "^1.1.1", "@sapphire/async-queue": "^1.5.3", "@sapphire/snowflake": "^3.5.3", "@vladfrangu/async_event_emitter": "^2.4.6", "discord-api-types": "^0.38.16", "magic-bytes.js": "^1.10.0", "tslib": "^2.6.3", "undici": "6.21.3" } }, "sha512-RDYrhmpB7mTvmCKcpj+pc5k7POKszS4E2O9TYc+U+Y4iaCP+r910QdO43qmpOja8LRr1RJ0b3U+CqVsnPqzf4w=="],
|
||||||
|
|
||||||
|
"@discordjs/ws/discord-api-types": ["discord-api-types@0.38.41", "", {}, "sha512-yMECyR8j9c2fVTvCQ+Qc24pweYFIZk/XoxDOmt1UvPeSw5tK6gXBd/2hhP+FEAe9Y6ny8pRMaf618XDK4U53OQ=="],
|
||||||
|
|
||||||
"@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
|
"@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
|
||||||
|
|
||||||
"@modelcontextprotocol/sdk/ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="],
|
"@modelcontextprotocol/sdk/ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="],
|
||||||
|
|
||||||
"@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
|
"@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
|
||||||
|
|
||||||
|
"@typescript-eslint/typescript-estree/tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="],
|
||||||
|
|
||||||
"ajv-formats/ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="],
|
"ajv-formats/ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="],
|
||||||
|
|
||||||
"ejclaw-agent-runner/@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.29.0", "", { "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" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ=="],
|
"ejclaw-agent-runner/@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.29.0", "", { "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" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ=="],
|
||||||
|
|
||||||
|
"ejclaw-agent-runner/cron-parser": ["cron-parser@5.5.0", "", { "dependencies": { "luxon": "^3.7.1" } }, "sha512-oML4lKUXxizYswqmxuOCpgFS8BNUJpIu6k/2HVHyaL8Ynnf3wdf9tkns0yRdJLSIjkJ+b0DXHMZEHGpMwjnPww=="],
|
||||||
|
|
||||||
"mdast-util-find-and-replace/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="],
|
"mdast-util-find-and-replace/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="],
|
||||||
|
|
||||||
"parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="],
|
"parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="],
|
||||||
@@ -1155,30 +1047,26 @@
|
|||||||
|
|
||||||
"rc/strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="],
|
"rc/strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="],
|
||||||
|
|
||||||
"rolldown/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.17", "", {}, "sha512-n8iosDOt6Ig1UhJ2AYqoIhHWh/isz0xpicHTzpKBeotdVsTEcxsSA/i3EVM7gQAj0rU27OLAxCjzlj15IWY7bg=="],
|
"rolldown/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.1", "", {}, "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw=="],
|
||||||
|
|
||||||
"rollup/fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
|
||||||
|
|
||||||
"tsx/fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
|
||||||
|
|
||||||
"vite/fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
"vite/fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
||||||
|
|
||||||
"vitest/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
|
"vitest/picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="],
|
||||||
|
|
||||||
"vitest/tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
|
"vitest/tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="],
|
||||||
|
|
||||||
"vitest/vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser"], "bin": "bin/vite.js" }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="],
|
"@discordjs/ws/@discordjs/rest/@sapphire/snowflake": ["@sapphire/snowflake@3.5.3", "", {}, "sha512-jjmJywLAFoWeBi1W7994zZyiNWPIiqRRNAmSERxyg93xRGzNYvGjlZ0gR6x0F4gPRi2+0O6S71kOZYyr3cxaIQ=="],
|
||||||
|
|
||||||
|
"@discordjs/ws/@discordjs/rest/undici": ["undici@6.21.3", "", {}, "sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw=="],
|
||||||
|
|
||||||
"@modelcontextprotocol/sdk/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
|
"@modelcontextprotocol/sdk/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
|
||||||
|
|
||||||
|
"@typescript-eslint/typescript-estree/tinyglobby/picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="],
|
||||||
|
|
||||||
"ajv-formats/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
|
"ajv-formats/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
|
||||||
|
|
||||||
"ejclaw-agent-runner/@modelcontextprotocol/sdk/ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="],
|
"ejclaw-agent-runner/@modelcontextprotocol/sdk/ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="],
|
||||||
|
|
||||||
"vitest/vite/fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
|
||||||
|
|
||||||
"vitest/vite/postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
|
|
||||||
|
|
||||||
"ejclaw-agent-runner/@modelcontextprotocol/sdk/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
|
"ejclaw-agent-runner/@modelcontextprotocol/sdk/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
105
claude_relogin.sh
Executable file
105
claude_relogin.sh
Executable file
@@ -0,0 +1,105 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# claude_relogin.sh — 이 폴더의 Claude 계정을 OAuth로 재로그인한다.
|
||||||
|
#
|
||||||
|
# 사용법:
|
||||||
|
# ./claude_relogin.sh
|
||||||
|
#
|
||||||
|
# 동작:
|
||||||
|
# 1) 이 스크립트가 있는 폴더의 .env 에서 CLAUDE_CREDENTIALS_PATH 를 읽어온다.
|
||||||
|
# 2) 인증(authorize) URL 을 출력한다 → 브라우저로 열어 로그인한다.
|
||||||
|
# 3) 로그인 완료 후 화면에 나오는 코드를 붙여넣는다.
|
||||||
|
# 4) 코드를 토큰으로 교환해 credentials 파일에 저장한다.
|
||||||
|
# 5) 해당 systemd(--user) 서비스를 재시작한다.
|
||||||
|
#
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# --- 이 스크립트가 있는 폴더로 이동 ---------------------------------------
|
||||||
|
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
cd "$DIR"
|
||||||
|
|
||||||
|
# --- bun 실행 파일 찾기 ----------------------------------------------------
|
||||||
|
BUN="$(command -v bun 2>/dev/null || true)"
|
||||||
|
[ -z "$BUN" ] && [ -x "$HOME/.bun/bin/bun" ] && BUN="$HOME/.bun/bin/bun"
|
||||||
|
if [ -z "$BUN" ]; then
|
||||||
|
echo "❌ bun 을 찾을 수 없습니다. PATH 를 확인하세요." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- .env 에서 CLAUDE_CREDENTIALS_PATH 읽기 --------------------------------
|
||||||
|
if [ ! -f .env ]; then
|
||||||
|
echo "❌ $DIR/.env 가 없습니다." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
CRED_PATH="$(grep -E '^CLAUDE_CREDENTIALS_PATH=' .env | tail -n1 | cut -d= -f2- | tr -d '"'"'"'' )"
|
||||||
|
if [ -z "${CRED_PATH:-}" ]; then
|
||||||
|
echo "❌ .env 에 CLAUDE_CREDENTIALS_PATH 가 설정되어 있지 않습니다." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
export CLAUDE_CREDENTIALS_PATH="$CRED_PATH"
|
||||||
|
|
||||||
|
# --- 서비스명 자동 감지 (폴더명 소문자 + .service) -------------------------
|
||||||
|
SERVICE="$(basename "$DIR" | tr '[:upper:]' '[:lower:]').service"
|
||||||
|
|
||||||
|
echo "======================================================================"
|
||||||
|
echo " Claude 재로그인"
|
||||||
|
echo " 폴더 : $DIR"
|
||||||
|
echo " credentials : $CLAUDE_CREDENTIALS_PATH"
|
||||||
|
echo " 서비스 : $SERVICE"
|
||||||
|
echo "======================================================================"
|
||||||
|
echo
|
||||||
|
|
||||||
|
# --- 1단계: authorize URL 발급 --------------------------------------------
|
||||||
|
echo "▶ 1단계: 로그인 URL 생성 중..."
|
||||||
|
PHASE1_OUT="$("$BUN" setup/index.ts --step login 2>/dev/null)"
|
||||||
|
URL="$(printf '%s\n' "$PHASE1_OUT" | grep -E '^AUTHORIZE_URL=' | head -n1 | sed 's/^AUTHORIZE_URL=//')"
|
||||||
|
if [ -z "$URL" ]; then
|
||||||
|
echo "❌ 인증 URL 생성 실패. 출력:" >&2
|
||||||
|
printf '%s\n' "$PHASE1_OUT" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "▼ 아래 URL 을 브라우저에서 열어 로그인하세요:"
|
||||||
|
echo "----------------------------------------------------------------------"
|
||||||
|
echo "$URL"
|
||||||
|
echo "----------------------------------------------------------------------"
|
||||||
|
echo
|
||||||
|
echo "로그인 완료 후 화면에 표시되는 코드를 복사하세요."
|
||||||
|
echo "(코드는 'xxxx' 또는 'xxxx#yyyy' 형태 모두 가능합니다.)"
|
||||||
|
echo
|
||||||
|
|
||||||
|
# --- 코드 입력 받기 --------------------------------------------------------
|
||||||
|
printf "붙여넣을 코드: "
|
||||||
|
read -r CODE
|
||||||
|
CODE="$(printf '%s' "$CODE" | tr -d '[:space:]')"
|
||||||
|
if [ -z "$CODE" ]; then
|
||||||
|
echo "❌ 코드가 비어 있습니다. 중단합니다." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- 2단계: 코드 → 토큰 교환 ----------------------------------------------
|
||||||
|
echo
|
||||||
|
echo "▶ 2단계: 코드 교환 및 credentials 저장 중..."
|
||||||
|
if "$BUN" setup/index.ts --step login --code "$CODE"; then
|
||||||
|
echo "✅ 로그인 성공 — credentials 저장 완료: $CLAUDE_CREDENTIALS_PATH"
|
||||||
|
else
|
||||||
|
echo "❌ 코드 교환 실패. 코드가 만료되었거나 잘못되었을 수 있습니다." >&2
|
||||||
|
echo " 스크립트를 다시 실행해 새 URL 로 시도하세요." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- 3단계: 서비스 재시작 --------------------------------------------------
|
||||||
|
echo
|
||||||
|
echo "▶ 3단계: $SERVICE 재시작..."
|
||||||
|
if systemctl --user restart "$SERVICE" 2>/dev/null; then
|
||||||
|
sleep 1
|
||||||
|
STATE="$(systemctl --user is-active "$SERVICE" 2>/dev/null || true)"
|
||||||
|
echo "✅ $SERVICE 상태: $STATE"
|
||||||
|
else
|
||||||
|
echo "⚠ $SERVICE 재시작을 건너뜀(서비스가 없거나 권한 없음). 수동으로 재시작하세요:" >&2
|
||||||
|
echo " systemctl --user restart $SERVICE" >&2
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "완료. 🎉"
|
||||||
95
codex_relogin.sh
Executable file
95
codex_relogin.sh
Executable file
@@ -0,0 +1,95 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# codex_relogin.sh — Codex(ChatGPT) 계정을 device-code OAuth 로 재로그인한다.
|
||||||
|
#
|
||||||
|
# 사용법:
|
||||||
|
# ./codex_relogin.sh # 기본 슬롯(0) 재로그인
|
||||||
|
# ./codex_relogin.sh 1 # ~/.codex-accounts/1 슬롯 재로그인
|
||||||
|
#
|
||||||
|
# 참고:
|
||||||
|
# * Codex 로그인은 이 호스트에서 두 봇(EJClaw / KAClaw)이 "공유"한다.
|
||||||
|
# 계정 저장 위치가 폴더별이 아니라 ~/.codex-accounts/{n} 이기 때문.
|
||||||
|
# → 한 번만 재로그인하면 두 봇 모두에 반영된다.
|
||||||
|
# * codex CLI 가 URL + 일회용 코드를 출력한다(15분 유효). 브라우저에서
|
||||||
|
# 로그인/코드입력을 마치면 codex 가 auth.json 을 자동 저장한다.
|
||||||
|
#
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
cd "$DIR"
|
||||||
|
|
||||||
|
# --- codex 실행 파일 찾기 --------------------------------------------------
|
||||||
|
CODEX="$(command -v codex 2>/dev/null || true)"
|
||||||
|
if [ -z "$CODEX" ]; then
|
||||||
|
# fnm 로 설치된 node 전역 bin 에서 탐색
|
||||||
|
for c in "$HOME"/.local/share/fnm/node-versions/*/installation/bin/codex; do
|
||||||
|
[ -x "$c" ] && CODEX="$c" && break
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
if [ -z "$CODEX" ]; then
|
||||||
|
echo "❌ codex CLI 를 찾을 수 없습니다. 'command -v codex' 로 확인하세요." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- 대상 슬롯 결정 --------------------------------------------------------
|
||||||
|
# 앱은 ~/.codex-accounts/{n}/auth.json 를 읽는다. 없으면 ~/.codex 로 폴백.
|
||||||
|
SLOT="${1:-0}"
|
||||||
|
ACCOUNTS_DIR="$HOME/.codex-accounts"
|
||||||
|
if [ -d "$ACCOUNTS_DIR/$SLOT" ]; then
|
||||||
|
CODEX_TARGET="$ACCOUNTS_DIR/$SLOT"
|
||||||
|
elif [ -d "$ACCOUNTS_DIR" ] && ls -d "$ACCOUNTS_DIR"/[0-9]* >/dev/null 2>&1; then
|
||||||
|
echo "❌ 슬롯 '$SLOT' 이(가) 없습니다. 사용 가능한 슬롯:" >&2
|
||||||
|
ls -d "$ACCOUNTS_DIR"/[0-9]* 2>/dev/null | sed 's#.*/# - #' >&2
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
# accounts 디렉터리가 비어있으면 기본 codex home 사용
|
||||||
|
CODEX_TARGET="$HOME/.codex"
|
||||||
|
fi
|
||||||
|
export CODEX_HOME="$CODEX_TARGET"
|
||||||
|
|
||||||
|
# --- 서비스명 자동 감지 (폴더명 소문자 + .service) -------------------------
|
||||||
|
SERVICE="$(basename "$DIR" | tr '[:upper:]' '[:lower:]').service"
|
||||||
|
|
||||||
|
echo "======================================================================"
|
||||||
|
echo " Codex 재로그인 (device-code OAuth)"
|
||||||
|
echo " codex : $CODEX"
|
||||||
|
echo " CODEX_HOME : $CODEX_HOME (auth.json 저장 위치)"
|
||||||
|
echo " 서비스 : $SERVICE"
|
||||||
|
echo " ※ Codex 로그인은 EJClaw/KAClaw 두 봇이 공유합니다."
|
||||||
|
echo "======================================================================"
|
||||||
|
echo
|
||||||
|
echo "▶ 아래 안내대로 브라우저에서 URL 을 열고 표시되는 코드를 입력하세요."
|
||||||
|
echo " (로그인이 끝나면 자동으로 저장되고 이 창이 넘어갑니다.)"
|
||||||
|
echo "----------------------------------------------------------------------"
|
||||||
|
|
||||||
|
# --- 로그인 실행 (codex 가 URL/코드 출력 + 폴링까지 담당) ------------------
|
||||||
|
if "$CODEX" login --device-auth; then
|
||||||
|
echo "----------------------------------------------------------------------"
|
||||||
|
echo "✅ Codex 로그인 성공 — auth.json 저장: $CODEX_HOME/auth.json"
|
||||||
|
else
|
||||||
|
echo "----------------------------------------------------------------------"
|
||||||
|
echo "❌ Codex 로그인 실패 또는 취소됨. 다시 실행해 시도하세요." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- 상태 확인 -------------------------------------------------------------
|
||||||
|
echo
|
||||||
|
echo "▶ 로그인 상태 확인:"
|
||||||
|
"$CODEX" login status 2>&1 | sed 's/^/ /' || true
|
||||||
|
|
||||||
|
# --- 서비스 재시작 ---------------------------------------------------------
|
||||||
|
echo
|
||||||
|
echo "▶ $SERVICE 재시작..."
|
||||||
|
if systemctl --user restart "$SERVICE" 2>/dev/null; then
|
||||||
|
sleep 1
|
||||||
|
STATE="$(systemctl --user is-active "$SERVICE" 2>/dev/null || true)"
|
||||||
|
echo "✅ $SERVICE 상태: $STATE"
|
||||||
|
echo " (공유 계정이므로 다른 봇도 새 로그인을 사용합니다. 필요하면"
|
||||||
|
echo " 상대 봇도 재시작하세요: systemctl --user restart <다른서비스>)"
|
||||||
|
else
|
||||||
|
echo "⚠ $SERVICE 재시작 건너뜀(서비스 없음/권한 없음). 수동 재시작:" >&2
|
||||||
|
echo " systemctl --user restart $SERVICE" >&2
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "완료. 🎉"
|
||||||
12
package.json
12
package.json
@@ -41,8 +41,8 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@modelcontextprotocol/sdk": "^1.27.1",
|
"@modelcontextprotocol/sdk": "^1.27.1",
|
||||||
"cron-parser": "^5.5.0",
|
"cron-parser": "^5.6.1",
|
||||||
"discord.js": "^14.18.0",
|
"discord.js": "^14.26.4",
|
||||||
"ejclaw-runners-shared": "workspace:*",
|
"ejclaw-runners-shared": "workspace:*",
|
||||||
"lucide-react": "^1.11.0",
|
"lucide-react": "^1.11.0",
|
||||||
"pino": "^9.6.0",
|
"pino": "^9.6.0",
|
||||||
@@ -51,7 +51,7 @@
|
|||||||
"react-dom": "^19.2.5",
|
"react-dom": "^19.2.5",
|
||||||
"react-markdown": "^10.1.0",
|
"react-markdown": "^10.1.0",
|
||||||
"remark-gfm": "^4.0.1",
|
"remark-gfm": "^4.0.1",
|
||||||
"yaml": "^2.8.2",
|
"yaml": "^2.9.0",
|
||||||
"zod": "^4.3.6"
|
"zod": "^4.3.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -63,7 +63,7 @@
|
|||||||
"@types/react": "^19.2.14",
|
"@types/react": "^19.2.14",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
"@vitejs/plugin-react": "^6.0.1",
|
"@vitejs/plugin-react": "^6.0.1",
|
||||||
"@vitest/coverage-v8": "^4.0.18",
|
"@vitest/coverage-v8": "^4.1.9",
|
||||||
"better-sqlite3": "^12.8.0",
|
"better-sqlite3": "^12.8.0",
|
||||||
"eslint": "^10.2.1",
|
"eslint": "^10.2.1",
|
||||||
"globals": "^17.5.0",
|
"globals": "^17.5.0",
|
||||||
@@ -72,8 +72,8 @@
|
|||||||
"prettier": "^3.8.1",
|
"prettier": "^3.8.1",
|
||||||
"typescript": "^5.7.0",
|
"typescript": "^5.7.0",
|
||||||
"typescript-eslint": "^8.59.0",
|
"typescript-eslint": "^8.59.0",
|
||||||
"vite": "^8.0.10",
|
"vite": "^8.1.3",
|
||||||
"vitest": "^4.0.18"
|
"vitest": "^4.1.9"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20"
|
"node": ">=20"
|
||||||
|
|||||||
@@ -22,6 +22,11 @@ You have been summoned because the owner and reviewer reached a deadlock after m
|
|||||||
- The situation cannot be resolved without user input, regardless of technical agreement
|
- The situation cannot be resolved without user input, regardless of technical agreement
|
||||||
- The same NEEDS_CONTEXT or BLOCKED is repeated after a prior PROCEED — this means your PROCEED did not resolve the issue
|
- The same NEEDS_CONTEXT or BLOCKED is repeated after a prior PROCEED — this means your PROCEED did not resolve the issue
|
||||||
|
|
||||||
|
When you ESCALATE, your message is the ONLY thing the user reads to make their choice — they will not scroll back through the owner↔reviewer conversation. So make it fully self-contained:
|
||||||
|
- Restate the actual decision the user must make, in one plain sentence.
|
||||||
|
- List the concrete options as a short numbered list (1., 2., …) with a one-line consequence for each, so the user can reply with just a number or a short phrase.
|
||||||
|
- Include only the minimum context needed to decide — do not assume the user remembers the prior discussion.
|
||||||
|
|
||||||
## MoA (Mixture of Agents) Reference Opinions
|
## MoA (Mixture of Agents) Reference Opinions
|
||||||
|
|
||||||
You may receive reference opinions from external models appended to your prompt. When present:
|
You may receive reference opinions from external models appended to your prompt. When present:
|
||||||
@@ -43,3 +48,8 @@ You may receive reference opinions from external models appended to your prompt.
|
|||||||
- If both sides are saying the same thing but not acting on it, call it out and direct the owner to act
|
- If both sides are saying the same thing but not acting on it, call it out and direct the owner to act
|
||||||
- If the conversation shows the owner asking the user a question (not the reviewer), always ESCALATE — the arbiter cannot answer on behalf of the user
|
- If the conversation shows the owner asking the user a question (not the reviewer), always ESCALATE — the arbiter cannot answer on behalf of the user
|
||||||
- If you see a prior arbiter verdict of PROCEED in the history but the same issue persists, do NOT repeat PROCEED — use ESCALATE instead
|
- If you see a prior arbiter verdict of PROCEED in the history but the same issue persists, do NOT repeat PROCEED — use ESCALATE instead
|
||||||
|
|
||||||
|
## Language
|
||||||
|
|
||||||
|
- Write your verdict in the user's language (Korean for this deployment) unless the user wrote in another language. The user must be able to read your verdict.
|
||||||
|
- Keep ONLY the leading verdict keyword in English — `PROCEED` / `REVISE` / `RESET` / `ESCALATE` — exactly as specified above, because the system parses that first token. Write everything after it (reasoning, evidence, the required action for the owner) in Korean.
|
||||||
|
|||||||
@@ -5,6 +5,18 @@ Use it to acknowledge a request before starting longer work.
|
|||||||
|
|
||||||
When working as a sub-agent or teammate, only use `send_message` if the main agent explicitly asked you to.
|
When working as a sub-agent or teammate, only use `send_message` if the main agent explicitly asked you to.
|
||||||
|
|
||||||
|
## Background work never notifies you — finish within the turn
|
||||||
|
|
||||||
|
Nothing in this environment re-invokes you when a background process, a background shell command, or a `run_in_background` job finishes. You will NOT be automatically notified when a build, compile, download, deploy, or any background task completes. The built-in tool descriptions that say "you'll be notified when it finishes" do NOT apply here.
|
||||||
|
|
||||||
|
If you end your turn with a sentence like "I'll wait for the build to complete — I'll be notified", the turn simply ends and nothing ever resumes — the user is left stuck staring at that message. This is a real, repeated failure. Do not do it.
|
||||||
|
|
||||||
|
Therefore:
|
||||||
|
|
||||||
|
- Do not launch long work in the background and then end the turn expecting a callback. Run it in the foreground and block/poll until it actually finishes inside this same turn, then report the real result.
|
||||||
|
- If something genuinely cannot complete within one turn, do not pretend a notification is coming. Finish with a concrete status and explicitly ask the user to send a follow-up message to continue. For recurring polling of CI/scheduled work, use the `schedule_task` / `watch_ci` tools, which DO re-invoke you.
|
||||||
|
- Never leave an intermediate "I'll wait … I'll be notified" planning sentence as your final answer.
|
||||||
|
|
||||||
## Media attachments
|
## Media attachments
|
||||||
|
|
||||||
When a locally generated image, screenshot, video, audio, or document should appear in Discord, include a `MEDIA:` directive on its own line with an absolute local path:
|
When a locally generated image, screenshot, video, audio, or document should appear in Discord, include a `MEDIA:` directive on its own line with an absolute local path:
|
||||||
|
|||||||
@@ -5,13 +5,13 @@
|
|||||||
* builds (captured from the official CLI). This is the manual / paste-code
|
* builds (captured from the official CLI). This is the manual / paste-code
|
||||||
* variant: the user visits the authorize URL, completes login in their
|
* variant: the user visits the authorize URL, completes login in their
|
||||||
* browser, and pastes the code back. We exchange it for tokens and write
|
* browser, and pastes the code back. We exchange it for tokens and write
|
||||||
* `~/.claude/.credentials.json`.
|
* `CLAUDE_CREDENTIALS_PATH` or `~/.claude/.credentials.json`.
|
||||||
*
|
*
|
||||||
* Usage:
|
* Usage:
|
||||||
* bun setup/index.ts --step login # phase 1: print authorize URL
|
* bun setup/index.ts --step login # phase 1: print authorize URL
|
||||||
* bun setup/index.ts --step login --code <c> # phase 2: exchange the code
|
* bun setup/index.ts --step login --code <c> # phase 2: exchange the code
|
||||||
*
|
*
|
||||||
* Phase 1 stashes the PKCE verifier + state in /tmp/ejclaw-claude-login.json
|
* Phase 1 stashes the PKCE verifier + state in a /tmp login state file
|
||||||
* (mode 0600). Phase 2 reads it back, exchanges the code, writes credentials.
|
* (mode 0600). Phase 2 reads it back, exchanges the code, writes credentials.
|
||||||
*
|
*
|
||||||
* This step is run by hand for re-auth. Once `.credentials.json` exists, the
|
* This step is run by hand for re-auth. Once `.credentials.json` exists, the
|
||||||
@@ -39,8 +39,21 @@ const SCOPES = [
|
|||||||
'user:file_upload',
|
'user:file_upload',
|
||||||
];
|
];
|
||||||
|
|
||||||
const STATE_FILE = path.join(os.tmpdir(), 'ejclaw-claude-login.json');
|
function credentialsPath(): string {
|
||||||
const CREDS_PATH = path.join(os.homedir(), '.claude', '.credentials.json');
|
const configured = process.env.CLAUDE_CREDENTIALS_PATH?.trim();
|
||||||
|
return configured
|
||||||
|
? path.resolve(configured)
|
||||||
|
: path.join(os.homedir(), '.claude', '.credentials.json');
|
||||||
|
}
|
||||||
|
|
||||||
|
function stateFilePath(): string {
|
||||||
|
const hash = crypto
|
||||||
|
.createHash('sha256')
|
||||||
|
.update(credentialsPath())
|
||||||
|
.digest('hex')
|
||||||
|
.slice(0, 12);
|
||||||
|
return path.join(os.tmpdir(), `ejclaw-claude-login-${hash}.json`);
|
||||||
|
}
|
||||||
|
|
||||||
interface PendingState {
|
interface PendingState {
|
||||||
verifier: string;
|
verifier: string;
|
||||||
@@ -90,13 +103,14 @@ function buildAuthorizeUrl(state: string, challenge: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function writePendingState(p: PendingState): void {
|
function writePendingState(p: PendingState): void {
|
||||||
fs.writeFileSync(STATE_FILE, JSON.stringify(p), { mode: 0o600 });
|
fs.writeFileSync(stateFilePath(), JSON.stringify(p), { mode: 0o600 });
|
||||||
}
|
}
|
||||||
|
|
||||||
function readPendingState(): PendingState | null {
|
function readPendingState(): PendingState | null {
|
||||||
if (!fs.existsSync(STATE_FILE)) return null;
|
const stateFile = stateFilePath();
|
||||||
|
if (!fs.existsSync(stateFile)) return null;
|
||||||
try {
|
try {
|
||||||
return JSON.parse(fs.readFileSync(STATE_FILE, 'utf-8')) as PendingState;
|
return JSON.parse(fs.readFileSync(stateFile, 'utf-8')) as PendingState;
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -136,6 +150,7 @@ async function exchangeCode(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function writeCredentials(resp: ExchangeResponse): void {
|
function writeCredentials(resp: ExchangeResponse): void {
|
||||||
|
const credsPath = credentialsPath();
|
||||||
const expiresAt = Date.now() + resp.expires_in * 1000;
|
const expiresAt = Date.now() + resp.expires_in * 1000;
|
||||||
const creds = {
|
const creds = {
|
||||||
claudeAiOauth: {
|
claudeAiOauth: {
|
||||||
@@ -150,11 +165,11 @@ function writeCredentials(resp: ExchangeResponse): void {
|
|||||||
: '',
|
: '',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const dir = path.dirname(CREDS_PATH);
|
const dir = path.dirname(credsPath);
|
||||||
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
||||||
const tmp = `${CREDS_PATH}.tmp`;
|
const tmp = `${credsPath}.tmp`;
|
||||||
fs.writeFileSync(tmp, JSON.stringify(creds, null, 2), { mode: 0o600 });
|
fs.writeFileSync(tmp, JSON.stringify(creds, null, 2), { mode: 0o600 });
|
||||||
fs.renameSync(tmp, CREDS_PATH);
|
fs.renameSync(tmp, credsPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Args {
|
interface Args {
|
||||||
@@ -208,15 +223,15 @@ export async function run(args: string[]): Promise<void> {
|
|||||||
try {
|
try {
|
||||||
const resp = await exchangeCode(code, pending.verifier, pending.state);
|
const resp = await exchangeCode(code, pending.verifier, pending.state);
|
||||||
writeCredentials(resp);
|
writeCredentials(resp);
|
||||||
fs.unlinkSync(STATE_FILE);
|
fs.unlinkSync(stateFilePath());
|
||||||
const newScopes = (resp.scope || SCOPES.join(' ')).split(' ').sort();
|
const newScopes = (resp.scope || SCOPES.join(' ')).split(' ').sort();
|
||||||
logger.info(
|
logger.info(
|
||||||
{ scopes: newScopes, expiresInMin: Math.round(resp.expires_in / 60) },
|
{ scopes: newScopes, expiresInMin: Math.round(resp.expires_in / 60) },
|
||||||
'Wrote ~/.claude/.credentials.json',
|
'Wrote Claude credentials',
|
||||||
);
|
);
|
||||||
emitStatus('LOGIN', {
|
emitStatus('LOGIN', {
|
||||||
STATUS: 'success',
|
STATUS: 'success',
|
||||||
CREDENTIALS_PATH: CREDS_PATH,
|
CREDENTIALS_PATH: credentialsPath(),
|
||||||
SCOPES: newScopes.join(','),
|
SCOPES: newScopes.join(','),
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ describe('migrate room registrations step', () => {
|
|||||||
chat_jid: 'dc:legacy-room',
|
chat_jid: 'dc:legacy-room',
|
||||||
room_mode: 'tribunal',
|
room_mode: 'tribunal',
|
||||||
mode_source: 'inferred',
|
mode_source: 'inferred',
|
||||||
owner_agent_type: 'codex',
|
owner_agent_type: 'claude-code',
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
expect(
|
expect(
|
||||||
@@ -141,12 +141,12 @@ describe('migrate room registrations step', () => {
|
|||||||
{
|
{
|
||||||
chat_jid: 'dc:legacy-room',
|
chat_jid: 'dc:legacy-room',
|
||||||
role: 'owner',
|
role: 'owner',
|
||||||
agent_type: 'codex',
|
agent_type: 'claude-code',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
chat_jid: 'dc:legacy-room',
|
chat_jid: 'dc:legacy-room',
|
||||||
role: 'reviewer',
|
role: 'reviewer',
|
||||||
agent_type: 'claude-code',
|
agent_type: 'codex',
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
expect(
|
expect(
|
||||||
|
|||||||
@@ -28,6 +28,19 @@ vi.mock('../src/group-folder.js', () => ({
|
|||||||
isValidGroupFolder: isValidGroupFolderMock,
|
isValidGroupFolder: isValidGroupFolderMock,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// Registration now provisions the paired workspace (which shells out to git)
|
||||||
|
// and hot-reloads the running service. Stub both so the unit test stays hermetic
|
||||||
|
// and only verifies the registration delegation, not workspace/git side effects.
|
||||||
|
vi.mock('../src/paired-workspace-manager.js', () => ({
|
||||||
|
ensurePairedWorkspaceProvisioned: vi.fn(
|
||||||
|
() => '/tmp/ejclaw-groups/test-room/owner',
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../src/runtime-reload-signal.js', () => ({
|
||||||
|
signalEjclawReload: vi.fn(() => false),
|
||||||
|
}));
|
||||||
|
|
||||||
vi.mock('../src/logger.js', () => ({
|
vi.mock('../src/logger.js', () => ({
|
||||||
logger: {
|
logger: {
|
||||||
info: loggerInfoMock,
|
info: loggerInfoMock,
|
||||||
@@ -84,6 +97,7 @@ describe('register step', () => {
|
|||||||
FOLDER: 'test-room',
|
FOLDER: 'test-room',
|
||||||
CHANNEL: 'discord',
|
CHANNEL: 'discord',
|
||||||
STATUS: 'success',
|
STATUS: 'success',
|
||||||
|
RELOADED: 'no',
|
||||||
LOG: 'logs/setup.log',
|
LOG: 'logs/setup.log',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
} from './codex-token-rotation.js';
|
} from './codex-token-rotation.js';
|
||||||
import { readCodexFeatureFromFile } from './codex-config-features.js';
|
import { readCodexFeatureFromFile } from './codex-config-features.js';
|
||||||
import { ensureClaudeSessionSettings } from './claude-session-settings.js';
|
import { ensureClaudeSessionSettings } from './claude-session-settings.js';
|
||||||
|
import { getClaudeCredentialsPath } from './claude-credentials-path.js';
|
||||||
import {
|
import {
|
||||||
getConfiguredClaudeTokens,
|
getConfiguredClaudeTokens,
|
||||||
getCurrentToken,
|
getCurrentToken,
|
||||||
@@ -53,16 +54,35 @@ function syncDirectoryEntries(sources: string[], destination: string): void {
|
|||||||
for (const entry of fs.readdirSync(source)) {
|
for (const entry of fs.readdirSync(source)) {
|
||||||
const srcPath = path.join(source, entry);
|
const srcPath = path.join(source, entry);
|
||||||
const dstPath = path.join(destination, entry);
|
const dstPath = path.join(destination, entry);
|
||||||
|
fs.mkdirSync(destination, { recursive: true });
|
||||||
if (fs.statSync(srcPath).isDirectory()) {
|
if (fs.statSync(srcPath).isDirectory()) {
|
||||||
fs.cpSync(srcPath, dstPath, { recursive: true });
|
fs.cpSync(srcPath, dstPath, { recursive: true });
|
||||||
} else {
|
} else {
|
||||||
fs.mkdirSync(destination, { recursive: true });
|
|
||||||
fs.copyFileSync(srcPath, dstPath);
|
fs.copyFileSync(srcPath, dstPath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function syncClaudeCredentialsToSessionDir(sessionClaudeDir: string): void {
|
||||||
|
// The agent spawns with CLAUDE_CONFIG_DIR=<sessionClaudeDir>. Without this
|
||||||
|
// pre-sync, a freshly created session dir has no credentials.json and the
|
||||||
|
// child Claude process fails to authenticate on its first run. token-
|
||||||
|
// refresh.ts's syncToSessionDirs only kicks in on the next refresh cycle.
|
||||||
|
const srcPath = getClaudeCredentialsPath(0, { allowHomeFallback: true });
|
||||||
|
if (!srcPath || !fs.existsSync(srcPath)) return;
|
||||||
|
const dest = path.join(sessionClaudeDir, '.credentials.json');
|
||||||
|
try {
|
||||||
|
fs.copyFileSync(srcPath, dest);
|
||||||
|
fs.chmodSync(dest, 0o600);
|
||||||
|
} catch (err) {
|
||||||
|
logger.warn(
|
||||||
|
{ err, srcPath, dest },
|
||||||
|
'Failed to pre-sync Claude credentials to session dir',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type SkillSyncScope = 'codex-user' | 'claude-user' | 'runner' | 'workdir';
|
type SkillSyncScope = 'codex-user' | 'claude-user' | 'runner' | 'workdir';
|
||||||
|
|
||||||
interface SkillSyncSource {
|
interface SkillSyncSource {
|
||||||
@@ -595,6 +615,7 @@ export function prepareGroupEnvironment(
|
|||||||
const groupSessionsDir = path.join(sessionRootDir, '.claude');
|
const groupSessionsDir = path.join(sessionRootDir, '.claude');
|
||||||
fs.mkdirSync(groupSessionsDir, { recursive: true });
|
fs.mkdirSync(groupSessionsDir, { recursive: true });
|
||||||
ensureClaudeSessionSettings(groupSessionsDir);
|
ensureClaudeSessionSettings(groupSessionsDir);
|
||||||
|
syncClaudeCredentialsToSessionDir(groupSessionsDir);
|
||||||
|
|
||||||
const workDirClaude = group.workDir
|
const workDirClaude = group.workDir
|
||||||
? path.join(group.workDir, '.claude')
|
? path.join(group.workDir, '.claude')
|
||||||
|
|||||||
@@ -1050,6 +1050,57 @@ describe('sendMessage', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// --- editMessage ---
|
||||||
|
|
||||||
|
describe('editMessage', () => {
|
||||||
|
it('edits in place when text is within the 2000 char limit', async () => {
|
||||||
|
const opts = createTestOpts();
|
||||||
|
const channel = new DiscordChannel('test-token', opts);
|
||||||
|
await channel.connect();
|
||||||
|
|
||||||
|
const mockMsg = { edit: vi.fn().mockResolvedValue(undefined) };
|
||||||
|
const mockChannel = {
|
||||||
|
messages: { fetch: vi.fn().mockResolvedValue(mockMsg) },
|
||||||
|
send: vi.fn().mockResolvedValue(undefined),
|
||||||
|
};
|
||||||
|
currentClient().channels.fetch.mockResolvedValue(mockChannel);
|
||||||
|
|
||||||
|
await channel.editMessage('dc:1234567890123456', 'msg-1', 'short final');
|
||||||
|
|
||||||
|
expect(mockMsg.edit).toHaveBeenCalledWith('short final');
|
||||||
|
expect(mockChannel.send).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('edits the first chunk and sends the remainder when text exceeds 2000 chars', async () => {
|
||||||
|
const opts = createTestOpts();
|
||||||
|
const channel = new DiscordChannel('test-token', opts);
|
||||||
|
await channel.connect();
|
||||||
|
|
||||||
|
const mockMsg = { edit: vi.fn().mockResolvedValue(undefined) };
|
||||||
|
const mockChannel = {
|
||||||
|
messages: { fetch: vi.fn().mockResolvedValue(mockMsg) },
|
||||||
|
send: vi.fn().mockResolvedValue(undefined),
|
||||||
|
};
|
||||||
|
currentClient().channels.fetch.mockResolvedValue(mockChannel);
|
||||||
|
|
||||||
|
const longText = 'y'.repeat(4500);
|
||||||
|
await channel.editMessage('dc:1234567890123456', 'msg-1', longText);
|
||||||
|
|
||||||
|
// First 2000 chars replace the tracked message, remainder as follow-ups.
|
||||||
|
expect(mockMsg.edit).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mockMsg.edit).toHaveBeenCalledWith('y'.repeat(2000));
|
||||||
|
expect(mockChannel.send).toHaveBeenCalledTimes(2);
|
||||||
|
expect(mockChannel.send).toHaveBeenNthCalledWith(1, {
|
||||||
|
content: 'y'.repeat(2000),
|
||||||
|
flags: 1 << 2,
|
||||||
|
});
|
||||||
|
expect(mockChannel.send).toHaveBeenNthCalledWith(2, {
|
||||||
|
content: 'y'.repeat(500),
|
||||||
|
flags: 1 << 2,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// --- ownsJid ---
|
// --- ownsJid ---
|
||||||
|
|
||||||
describe('ownsJid', () => {
|
describe('ownsJid', () => {
|
||||||
@@ -1159,4 +1210,50 @@ describe('channel properties', () => {
|
|||||||
const channel = new DiscordChannel('test-token', createTestOpts());
|
const channel = new DiscordChannel('test-token', createTestOpts());
|
||||||
expect(channel.name).toBe('discord');
|
expect(channel.name).toBe('discord');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('fetches missing channel metadata individually after a guild batch', async () => {
|
||||||
|
const channel = new DiscordChannel('test-token', createTestOpts());
|
||||||
|
await channel.connect();
|
||||||
|
|
||||||
|
const firstGuild = {
|
||||||
|
channels: {
|
||||||
|
fetch: vi.fn().mockResolvedValue(
|
||||||
|
new Map([
|
||||||
|
[
|
||||||
|
'111',
|
||||||
|
{
|
||||||
|
name: 'first-chat',
|
||||||
|
position: 1,
|
||||||
|
parent: { name: 'First Guild', position: 1 },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
currentClient().channels.fetch.mockImplementation(async (id: string) => {
|
||||||
|
if (id === '111') {
|
||||||
|
return {
|
||||||
|
name: 'first-chat',
|
||||||
|
position: 1,
|
||||||
|
parent: { name: 'First Guild', position: 1 },
|
||||||
|
guild: firstGuild,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (id === '222') {
|
||||||
|
return {
|
||||||
|
name: 'second-chat',
|
||||||
|
position: 2,
|
||||||
|
parent: { name: 'Second Guild', position: 2 },
|
||||||
|
guild: { channels: { fetch: vi.fn() } },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
const metadata = await channel.getChannelMeta(['dc:111', 'dc:222']);
|
||||||
|
|
||||||
|
expect(metadata.get('dc:111')?.name).toBe('first-chat');
|
||||||
|
expect(metadata.get('dc:222')?.name).toBe('second-chat');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -675,6 +675,21 @@ export class DiscordChannel implements Channel {
|
|||||||
categoryPosition: channel.parent?.position ?? 999,
|
categoryPosition: channel.parent?.position ?? 999,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const jid of dcJids) {
|
||||||
|
if (result.has(jid)) continue;
|
||||||
|
const channelId = jid.replace(/^dc:/, '');
|
||||||
|
const channel = await this.client.channels.fetch(channelId);
|
||||||
|
if (channel && 'position' in channel) {
|
||||||
|
const tc = channel as TextChannel;
|
||||||
|
result.set(jid, {
|
||||||
|
name: tc.name,
|
||||||
|
position: tc.position,
|
||||||
|
category: tc.parent?.name || '',
|
||||||
|
categoryPosition: tc.parent?.position ?? 999,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// Fallback: individual fetches
|
// Fallback: individual fetches
|
||||||
for (const jid of dcJids) {
|
for (const jid of dcJids) {
|
||||||
@@ -769,7 +784,23 @@ export class DiscordChannel implements Channel {
|
|||||||
throw new Error(`Discord channel not found or not editable: ${jid}`);
|
throw new Error(`Discord channel not found or not editable: ${jid}`);
|
||||||
}
|
}
|
||||||
const msg = await (channel as TextChannel).messages.fetch(messageId);
|
const msg = await (channel as TextChannel).messages.fetch(messageId);
|
||||||
await msg.edit(text);
|
// Discord caps a single message (edits included) at 2000 chars. A long
|
||||||
|
// final answer edited straight in is rejected with
|
||||||
|
// content[BASE_TYPE_MAX_LENGTH]; edit the tracked message with the first
|
||||||
|
// chunk and send the remainder as follow-up messages so nothing is
|
||||||
|
// dropped and no failed-edit fallback is needed.
|
||||||
|
const MAX_LENGTH = 2000;
|
||||||
|
if (text.length <= MAX_LENGTH) {
|
||||||
|
await msg.edit(text);
|
||||||
|
} else {
|
||||||
|
await msg.edit(text.slice(0, MAX_LENGTH));
|
||||||
|
for (let i = MAX_LENGTH; i < text.length; i += MAX_LENGTH) {
|
||||||
|
await (channel as TextChannel).send({
|
||||||
|
content: text.slice(i, i + MAX_LENGTH),
|
||||||
|
flags: MessageFlags.SuppressEmbeds,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
logger.info(
|
logger.info(
|
||||||
{
|
{
|
||||||
jid,
|
jid,
|
||||||
|
|||||||
40
src/claude-credentials-path.ts
Normal file
40
src/claude-credentials-path.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import os from 'os';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
function readPathEnv(key: string): string | null {
|
||||||
|
const value = process.env[key]?.trim();
|
||||||
|
return value ? path.resolve(value) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getClaudeCredentialsPath(
|
||||||
|
accountIndex: number,
|
||||||
|
options: { allowHomeFallback?: boolean } = {},
|
||||||
|
): string | null {
|
||||||
|
const explicitPath = readPathEnv('CLAUDE_CREDENTIALS_PATH');
|
||||||
|
if (accountIndex === 0 && explicitPath) return explicitPath;
|
||||||
|
|
||||||
|
const explicitAccountsDir = readPathEnv('CLAUDE_ACCOUNTS_DIR');
|
||||||
|
if (accountIndex > 0 && explicitAccountsDir) {
|
||||||
|
return path.join(
|
||||||
|
explicitAccountsDir,
|
||||||
|
String(accountIndex),
|
||||||
|
'.credentials.json',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.allowHomeFallback === false) return null;
|
||||||
|
|
||||||
|
if (accountIndex === 0) {
|
||||||
|
return path.join(os.homedir(), '.claude', '.credentials.json');
|
||||||
|
}
|
||||||
|
return path.join(
|
||||||
|
os.homedir(),
|
||||||
|
'.claude-accounts',
|
||||||
|
String(accountIndex),
|
||||||
|
'.credentials.json',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hasExplicitClaudeCredentialsPath(): boolean {
|
||||||
|
return Boolean(readPathEnv('CLAUDE_CREDENTIALS_PATH'));
|
||||||
|
}
|
||||||
@@ -102,6 +102,32 @@ describe('Claude usage 429 Retry-After backoff', () => {
|
|||||||
expect(second[0].usageRateLimited).toBe(true);
|
expect(second[0].usageRateLimited).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('falls back to the default cooldown when Retry-After is 0 (does not disable backoff)', async () => {
|
||||||
|
// Production regression: the usage endpoint was returning 429 with
|
||||||
|
// `retry-after: 0`, which made `retryAfterMs ?? DEFAULT` evaluate to 0 — a
|
||||||
|
// ~5s cooldown. The poller then re-hit the endpoint every 60s and re-tripped
|
||||||
|
// the 429 indefinitely. A non-positive Retry-After must use the default.
|
||||||
|
vi.useFakeTimers();
|
||||||
|
vi.setSystemTime(new Date('2026-06-20T00:00:00Z'));
|
||||||
|
|
||||||
|
const fetchMock = vi.fn(async () =>
|
||||||
|
makeResponse(429, { 'retry-after': '0' }),
|
||||||
|
);
|
||||||
|
vi.stubGlobal('fetch', fetchMock);
|
||||||
|
|
||||||
|
const { fetchAllClaudeUsage } = await import('./claude-usage.js');
|
||||||
|
|
||||||
|
await fetchAllClaudeUsage();
|
||||||
|
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
// +70s: past the 60s throttle. With the bug (0 cooldown) this would refetch;
|
||||||
|
// with the default 5-min cooldown it must still be held.
|
||||||
|
vi.setSystemTime(Date.now() + 70_000);
|
||||||
|
const held = await fetchAllClaudeUsage();
|
||||||
|
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(held[0].usageRateLimited).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
it('keeps backing off after the 60s throttle elapses but before the cooldown closes', async () => {
|
it('keeps backing off after the 60s throttle elapses but before the cooldown closes', async () => {
|
||||||
// This is the core regression: a 93s Retry-After must outlast the 60s
|
// This is the core regression: a 93s Retry-After must outlast the 60s
|
||||||
// MIN_FETCH_INTERVAL throttle. Without the cooldown the poller would refetch
|
// MIN_FETCH_INTERVAL throttle. Without the cooldown the poller would refetch
|
||||||
|
|||||||
@@ -6,9 +6,12 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import os from 'os';
|
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
|
||||||
|
import {
|
||||||
|
getClaudeCredentialsPath,
|
||||||
|
hasExplicitClaudeCredentialsPath,
|
||||||
|
} from './claude-credentials-path.js';
|
||||||
import { DATA_DIR } from './config.js';
|
import { DATA_DIR } from './config.js';
|
||||||
import { logger } from './logger.js';
|
import { logger } from './logger.js';
|
||||||
import {
|
import {
|
||||||
@@ -32,6 +35,14 @@ export interface ClaudeUsageData {
|
|||||||
const USAGE_ENDPOINT = 'https://api.anthropic.com/api/oauth/usage';
|
const USAGE_ENDPOINT = 'https://api.anthropic.com/api/oauth/usage';
|
||||||
const FETCH_TIMEOUT_MS = 10_000;
|
const FETCH_TIMEOUT_MS = 10_000;
|
||||||
|
|
||||||
|
// Master gate for credentials-backed usage queries. When false, the module
|
||||||
|
// uses only env-supplied tokens and skips all credentials-file reads, per-
|
||||||
|
// account cache keys, and on-401 refresh attempts. Enabled when either an
|
||||||
|
// explicit CLAUDE_CREDENTIALS_PATH is set or the home fallback is allowed.
|
||||||
|
const USE_CLAUDE_CREDENTIALS_FOR_USAGE =
|
||||||
|
hasExplicitClaudeCredentialsPath() ||
|
||||||
|
process.env.CLAUDE_USAGE_USE_HOME_CREDENTIALS !== 'false';
|
||||||
|
|
||||||
interface UsageApiResponse {
|
interface UsageApiResponse {
|
||||||
five_hour?: { utilization: number; resets_at?: string };
|
five_hour?: { utilization: number; resets_at?: string };
|
||||||
seven_day?: { utilization: number; resets_at?: string };
|
seven_day?: { utilization: number; resets_at?: string };
|
||||||
@@ -112,7 +123,7 @@ export function getUsageCacheWriteKey(
|
|||||||
token: string,
|
token: string,
|
||||||
accountIndex?: number,
|
accountIndex?: number,
|
||||||
): string {
|
): string {
|
||||||
return accountIndex != null
|
return USE_CLAUDE_CREDENTIALS_FOR_USAGE && accountIndex != null
|
||||||
? accountCacheKey(accountIndex)
|
? accountCacheKey(accountIndex)
|
||||||
: legacyTokenCacheKey(token);
|
: legacyTokenCacheKey(token);
|
||||||
}
|
}
|
||||||
@@ -123,9 +134,11 @@ export function getUsageCacheReadKeys(
|
|||||||
credentialsAccessToken?: string | null,
|
credentialsAccessToken?: string | null,
|
||||||
): string[] {
|
): string[] {
|
||||||
const keys: string[] = [];
|
const keys: string[] = [];
|
||||||
if (accountIndex != null) keys.push(accountCacheKey(accountIndex));
|
if (USE_CLAUDE_CREDENTIALS_FOR_USAGE && accountIndex != null) {
|
||||||
|
keys.push(accountCacheKey(accountIndex));
|
||||||
|
}
|
||||||
|
|
||||||
if (credentialsAccessToken) {
|
if (USE_CLAUDE_CREDENTIALS_FOR_USAGE && credentialsAccessToken) {
|
||||||
const credsKey = legacyTokenCacheKey(credentialsAccessToken);
|
const credsKey = legacyTokenCacheKey(credentialsAccessToken);
|
||||||
if (!keys.includes(credsKey)) keys.push(credsKey);
|
if (!keys.includes(credsKey)) keys.push(credsKey);
|
||||||
}
|
}
|
||||||
@@ -265,7 +278,11 @@ async function fetchUsageForToken(
|
|||||||
// 401 = token expired; 403 = token lacks `user:profile` scope.
|
// 401 = token expired; 403 = token lacks `user:profile` scope.
|
||||||
// Both are recoverable by exchanging refresh_token for a fresh access
|
// Both are recoverable by exchanging refresh_token for a fresh access
|
||||||
// token (mint includes current default scope set). Retry once.
|
// token (mint includes current default scope set). Retry once.
|
||||||
if (accountIndex != null && !refreshAttempted) {
|
if (
|
||||||
|
USE_CLAUDE_CREDENTIALS_FOR_USAGE &&
|
||||||
|
accountIndex != null &&
|
||||||
|
!refreshAttempted
|
||||||
|
) {
|
||||||
try {
|
try {
|
||||||
const { forceRefreshToken } = await import('./token-refresh.js');
|
const { forceRefreshToken } = await import('./token-refresh.js');
|
||||||
const newToken = await forceRefreshToken(accountIndex);
|
const newToken = await forceRefreshToken(accountIndex);
|
||||||
@@ -303,7 +320,14 @@ async function fetchUsageForToken(
|
|||||||
if (res.status === 429) {
|
if (res.status === 429) {
|
||||||
const staleMs = cached ? Date.now() - cached.fetchedAt : 0;
|
const staleMs = cached ? Date.now() - cached.fetchedAt : 0;
|
||||||
const retryAfterMs = parseRetryAfterMs(res.headers.get('retry-after'));
|
const retryAfterMs = parseRetryAfterMs(res.headers.get('retry-after'));
|
||||||
const cooldownMs = retryAfterMs ?? DEFAULT_RATE_LIMIT_COOLDOWN_MS;
|
// A `Retry-After` of 0 (or a past HTTP-date that parses to 0) must NOT
|
||||||
|
// disable the backoff — honoring it literally makes the poller retry on
|
||||||
|
// the next 60s tick and re-trip the 429 indefinitely. Treat any
|
||||||
|
// non-positive value as "no usable hint" and fall back to the default.
|
||||||
|
const cooldownMs =
|
||||||
|
retryAfterMs != null && retryAfterMs > 0
|
||||||
|
? retryAfterMs
|
||||||
|
: DEFAULT_RATE_LIMIT_COOLDOWN_MS;
|
||||||
const cooldownUntil = Date.now() + cooldownMs + RATE_LIMIT_MARGIN_MS;
|
const cooldownUntil = Date.now() + cooldownMs + RATE_LIMIT_MARGIN_MS;
|
||||||
logger.warn(
|
logger.warn(
|
||||||
{
|
{
|
||||||
@@ -403,12 +427,11 @@ async function fetchUsageForToken(
|
|||||||
* Uses the current active token from rotation.
|
* Uses the current active token from rotation.
|
||||||
*/
|
*/
|
||||||
export async function fetchClaudeUsage(): Promise<ClaudeUsageData | null> {
|
export async function fetchClaudeUsage(): Promise<ClaudeUsageData | null> {
|
||||||
// Prefer the access token in ~/.claude/.credentials.json when present.
|
// Prefer the access token in credentials.json when present. The static .env
|
||||||
// The static .env token (CLAUDE_CODE_OAUTH_TOKEN) was issued before the
|
// token (CLAUDE_CODE_OAUTH_TOKEN) was issued before the `user:profile` scope
|
||||||
// `user:profile` scope was required for /api/oauth/usage and so always 403s.
|
// was required for /api/oauth/usage and so always 403s. The credentials file
|
||||||
// The credentials file is the canonical source written by `claude auth
|
// is the canonical source written by `claude auth login` and kept fresh by
|
||||||
// login` and kept fresh by token-refresh.ts — its accessToken carries the
|
// token-refresh.ts — its accessToken carries the full scope set.
|
||||||
// full scope set including user:profile.
|
|
||||||
const credsToken = readCredentialsAccessToken(0);
|
const credsToken = readCredentialsAccessToken(0);
|
||||||
const token =
|
const token =
|
||||||
credsToken || getCurrentToken() || getConfiguredClaudeTokens()[0];
|
credsToken || getCurrentToken() || getConfiguredClaudeTokens()[0];
|
||||||
@@ -416,7 +439,8 @@ export async function fetchClaudeUsage(): Promise<ClaudeUsageData | null> {
|
|||||||
logger.debug('No Claude OAuth token available for usage check');
|
logger.debug('No Claude OAuth token available for usage check');
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return (await fetchUsageForToken(token, undefined)).usage;
|
// Pass accountIndex=0 so 401/403 → forceRefreshToken retry path engages.
|
||||||
|
return (await fetchUsageForToken(token, 0)).usage;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ClaudeAccountProfile {
|
export interface ClaudeAccountProfile {
|
||||||
@@ -428,21 +452,18 @@ const profileCache = new Map<number, ClaudeAccountProfile>();
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Read planType from credentials file as fallback when profile API fails.
|
* Read planType from credentials file as fallback when profile API fails.
|
||||||
* Account 0: ~/.claude/.credentials.json
|
* Path is resolved by getClaudeCredentialsPath (honours CLAUDE_CREDENTIALS_PATH
|
||||||
* Account 1+: ~/.claude-accounts/{index}/.credentials.json
|
* / CLAUDE_ACCOUNTS_DIR / home fallback). Returns null when the credentials
|
||||||
|
* gate is off or no file is found.
|
||||||
*/
|
*/
|
||||||
function readCredentialsPlanType(accountIndex: number): string | null {
|
function readCredentialsPlanType(accountIndex: number): string | null {
|
||||||
|
if (!USE_CLAUDE_CREDENTIALS_FOR_USAGE) return null;
|
||||||
try {
|
try {
|
||||||
const credsPath =
|
const credsPath = getClaudeCredentialsPath(accountIndex, {
|
||||||
accountIndex === 0
|
allowHomeFallback:
|
||||||
? path.join(os.homedir(), '.claude', '.credentials.json')
|
process.env.CLAUDE_USAGE_USE_HOME_CREDENTIALS !== 'false',
|
||||||
: path.join(
|
});
|
||||||
os.homedir(),
|
if (!credsPath || !fs.existsSync(credsPath)) return null;
|
||||||
'.claude-accounts',
|
|
||||||
String(accountIndex),
|
|
||||||
'.credentials.json',
|
|
||||||
);
|
|
||||||
if (!fs.existsSync(credsPath)) return null;
|
|
||||||
const data = readJsonFile<{
|
const data = readJsonFile<{
|
||||||
claudeAiOauth?: { subscriptionType?: string };
|
claudeAiOauth?: { subscriptionType?: string };
|
||||||
}>(credsPath);
|
}>(credsPath);
|
||||||
@@ -453,17 +474,13 @@ function readCredentialsPlanType(accountIndex: number): string | null {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function readCredentialsAccessToken(accountIndex: number): string | null {
|
function readCredentialsAccessToken(accountIndex: number): string | null {
|
||||||
|
if (!USE_CLAUDE_CREDENTIALS_FOR_USAGE) return null;
|
||||||
try {
|
try {
|
||||||
const credsPath =
|
const credsPath = getClaudeCredentialsPath(accountIndex, {
|
||||||
accountIndex === 0
|
allowHomeFallback:
|
||||||
? path.join(os.homedir(), '.claude', '.credentials.json')
|
process.env.CLAUDE_USAGE_USE_HOME_CREDENTIALS !== 'false',
|
||||||
: path.join(
|
});
|
||||||
os.homedir(),
|
if (!credsPath || !fs.existsSync(credsPath)) return null;
|
||||||
'.claude-accounts',
|
|
||||||
String(accountIndex),
|
|
||||||
'.credentials.json',
|
|
||||||
);
|
|
||||||
if (!fs.existsSync(credsPath)) return null;
|
|
||||||
const data = readJsonFile<{
|
const data = readJsonFile<{
|
||||||
claudeAiOauth?: { accessToken?: string };
|
claudeAiOauth?: { accessToken?: string };
|
||||||
}>(credsPath);
|
}>(credsPath);
|
||||||
@@ -519,7 +536,10 @@ async function fetchProfileForToken(
|
|||||||
export async function fetchAllClaudeProfiles(): Promise<void> {
|
export async function fetchAllClaudeProfiles(): Promise<void> {
|
||||||
const allTokens = getAllTokens();
|
const allTokens = getAllTokens();
|
||||||
for (const t of allTokens) {
|
for (const t of allTokens) {
|
||||||
let profile = await fetchProfileForToken(t.token);
|
// Prefer the per-account credentials access token; the static env token
|
||||||
|
// may lack the `user:profile` scope.
|
||||||
|
const credsToken = readCredentialsAccessToken(t.index);
|
||||||
|
let profile = await fetchProfileForToken(credsToken || t.token);
|
||||||
|
|
||||||
// Fallback: if profile API failed or returned unknown plan, use credentials file
|
// Fallback: if profile API failed or returned unknown plan, use credentials file
|
||||||
if (!profile || profile.planType === '?') {
|
if (!profile || profile.planType === '?') {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
import type { DashboardOptions } from './dashboard-status-content.js';
|
import type { DashboardOptions } from './dashboard-status-content.js';
|
||||||
|
import { formatRoomName } from './unified-dashboard.js';
|
||||||
|
|
||||||
function makeOptions(sessionId?: string): DashboardOptions {
|
function makeOptions(sessionId?: string): DashboardOptions {
|
||||||
const sessions: Record<string, string> = sessionId
|
const sessions: Record<string, string> = sessionId
|
||||||
@@ -85,3 +86,32 @@ describe('buildStatusContent', () => {
|
|||||||
expect(buildStatusContent(makeOptions())).not.toContain('**clone-test**');
|
expect(buildStatusContent(makeOptions())).not.toContain('**clone-test**');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('formatRoomName', () => {
|
||||||
|
it('uses the stored chat name without adding an extra Discord hash', () => {
|
||||||
|
expect(
|
||||||
|
formatRoomName(
|
||||||
|
'dc:123',
|
||||||
|
undefined,
|
||||||
|
'registered-room-name',
|
||||||
|
'My Server #bot-chat',
|
||||||
|
),
|
||||||
|
).toBe('My Server #bot-chat');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('adds a Discord hash for raw channel metadata names', () => {
|
||||||
|
expect(
|
||||||
|
formatRoomName(
|
||||||
|
'dc:123',
|
||||||
|
{
|
||||||
|
name: 'bot-chat',
|
||||||
|
position: 1,
|
||||||
|
category: 'Bots',
|
||||||
|
categoryPosition: 1,
|
||||||
|
},
|
||||||
|
'registered-room-name',
|
||||||
|
'My Server #bot-chat',
|
||||||
|
),
|
||||||
|
).toBe('#bot-chat');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -318,4 +318,14 @@ describe('formatOutbound with tool-call leaks', () => {
|
|||||||
'Key: sk-ant-XXXXXXXXXXXXXXXXXXXXXXXX\nto=functions.exec_command code {"cmd":"ls"}';
|
'Key: sk-ant-XXXXXXXXXXXXXXXXXXXXXXXX\nto=functions.exec_command code {"cmd":"ls"}';
|
||||||
expect(formatOutbound(input)).toBe('Key: [REDACTED]');
|
expect(formatOutbound(input)).toBe('Key: [REDACTED]');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('redacts a leaked Discord bot token', () => {
|
||||||
|
// Built from parts so the literal never appears in source (push protection).
|
||||||
|
const fakeToken = [
|
||||||
|
'MTA5ODc2NTQzMjEwOTg3NjU0',
|
||||||
|
'GaBcDe',
|
||||||
|
'abcdefghijklmnopqrstuvwxyz0123',
|
||||||
|
].join('.');
|
||||||
|
expect(formatOutbound(`token=${fakeToken}`)).toBe('token=[REDACTED]');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -36,6 +36,10 @@ import {
|
|||||||
hasHumanMessageAfterWorkItem,
|
hasHumanMessageAfterWorkItem,
|
||||||
} from './message-runtime-preflight-messages.js';
|
} from './message-runtime-preflight-messages.js';
|
||||||
import { deliverCanonicalOutboundMessage } from './ipc-outbound-delivery.js';
|
import { deliverCanonicalOutboundMessage } from './ipc-outbound-delivery.js';
|
||||||
|
import {
|
||||||
|
isReloginRequired,
|
||||||
|
RELOGIN_REQUIRED_MESSAGE,
|
||||||
|
} from './token-refresh.js';
|
||||||
import { findChannel, formatMessages } from './router.js';
|
import { findChannel, formatMessages } from './router.js';
|
||||||
import { createScopedLogger, logger } from './logger.js';
|
import { createScopedLogger, logger } from './logger.js';
|
||||||
import type { AgentOutput } from './agent-runner.js';
|
import type { AgentOutput } from './agent-runner.js';
|
||||||
@@ -308,6 +312,15 @@ async function processMissedMessages(
|
|||||||
return gateOutcome;
|
return gateOutcome;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const reloginOutcome = await runReloginRequiredGate(
|
||||||
|
args,
|
||||||
|
runtime,
|
||||||
|
missedMessages,
|
||||||
|
);
|
||||||
|
if (reloginOutcome !== null) {
|
||||||
|
return reloginOutcome;
|
||||||
|
}
|
||||||
|
|
||||||
return runQueuedGroupTurn({
|
return runQueuedGroupTurn({
|
||||||
chatJid: runtime.chatJid,
|
chatJid: runtime.chatJid,
|
||||||
group: runtime.group,
|
group: runtime.group,
|
||||||
@@ -415,6 +428,48 @@ function advancePastBotOnlyCollaboration(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When Claude OAuth auto-refresh has permanently given up (login lost and the
|
||||||
|
* refresh token is dead — see token-refresh.ts), don't spawn a doomed agent.
|
||||||
|
* Instead reply once, in this chat, asking the user to re-login, then advance
|
||||||
|
* the cursor so a still-logged-out bot doesn't re-notify on every poll. The
|
||||||
|
* next fresh incoming request re-triggers the notice. Recovers automatically
|
||||||
|
* once a manual re-login writes a valid token (isReloginRequired() clears).
|
||||||
|
*
|
||||||
|
* Returns true when the notice was sent (turn handled), null to fall through.
|
||||||
|
*/
|
||||||
|
async function runReloginRequiredGate(
|
||||||
|
args: ProcessGroupMessagesDeps,
|
||||||
|
runtime: RuntimeContext,
|
||||||
|
missedMessages: NewMessage[],
|
||||||
|
): Promise<boolean | null> {
|
||||||
|
// Only claude-code turns depend on the Claude OAuth token. Codex-backed
|
||||||
|
// groups authenticate separately and must not be blocked here.
|
||||||
|
const agentType = runtime.group.agentType ?? 'claude-code';
|
||||||
|
if (agentType !== 'claude-code') return null;
|
||||||
|
if (!isReloginRequired()) return null;
|
||||||
|
|
||||||
|
await deliverSessionCommandMessage(
|
||||||
|
args,
|
||||||
|
runtime.chatJid,
|
||||||
|
RELOGIN_REQUIRED_MESSAGE,
|
||||||
|
);
|
||||||
|
|
||||||
|
const lastMessage = missedMessages[missedMessages.length - 1];
|
||||||
|
if (lastMessage?.seq != null) {
|
||||||
|
advanceLastAgentCursor(
|
||||||
|
args.getLastAgentTimestamps(),
|
||||||
|
args.saveState,
|
||||||
|
runtime.chatJid,
|
||||||
|
lastMessage.seq,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
runtime.log.warn(
|
||||||
|
'Claude OAuth login lost (gave up refreshing) — asked user to re-login instead of running the agent',
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
async function runQueuedRunGates(
|
async function runQueuedRunGates(
|
||||||
args: ProcessGroupMessagesDeps,
|
args: ProcessGroupMessagesDeps,
|
||||||
runtime: RuntimeContext,
|
runtime: RuntimeContext,
|
||||||
|
|||||||
@@ -96,6 +96,28 @@ describe('message-runtime-prompts carry-forward guidance', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('prepends the carry-forward warning for a carried arbiter verdict too', () => {
|
||||||
|
const prompt = buildPairedTurnPrompt({
|
||||||
|
taskId: 'task-1',
|
||||||
|
chatJid: 'group@test',
|
||||||
|
timezone: 'UTC',
|
||||||
|
missedMessages: [makeHumanMessage('1번 선택할게')],
|
||||||
|
labeledFallbackMessages: [makeHumanMessage('1번 선택할게')],
|
||||||
|
turnOutputs: [
|
||||||
|
makeTurnOutput(
|
||||||
|
'[Carried forward context from the previous task: latest arbiter verdict]\nESCALATE\n1) A 2) B',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(
|
||||||
|
prompt.startsWith('System note:\nIf you see a message beginning with'),
|
||||||
|
).toBe(true);
|
||||||
|
expect(prompt).toContain(
|
||||||
|
'Respond only to the latest human request and the current task.',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('prepends a carry-forward warning to reviewer pending prompts', () => {
|
it('prepends a carry-forward warning to reviewer pending prompts', () => {
|
||||||
const prompt = buildReviewerPendingPrompt({
|
const prompt = buildReviewerPendingPrompt({
|
||||||
chatJid: 'group@test',
|
chatJid: 'group@test',
|
||||||
|
|||||||
@@ -10,11 +10,15 @@ import type {
|
|||||||
PairedTurnOutput,
|
PairedTurnOutput,
|
||||||
} from './types.js';
|
} from './types.js';
|
||||||
|
|
||||||
const CARRIED_FORWARD_OWNER_FINAL_MARKER =
|
// Common prefix for every carried-forward context block. Covers both the owner
|
||||||
'[Carried forward context from the previous task: latest owner final]';
|
// final ("… latest owner final]") and the arbiter verdict ("… latest arbiter
|
||||||
|
// verdict]"), so the guidance triggers whichever the previous task ended on —
|
||||||
|
// e.g. an arbiter ESCALATE the user is now replying to.
|
||||||
|
const CARRIED_FORWARD_MARKER_PREFIX =
|
||||||
|
'[Carried forward context from the previous task:';
|
||||||
|
|
||||||
const CARRIED_FORWARD_OWNER_FINAL_GUIDANCE = `System note:
|
const CARRIED_FORWARD_OWNER_FINAL_GUIDANCE = `System note:
|
||||||
If you see a message beginning with "${CARRIED_FORWARD_OWNER_FINAL_MARKER}", treat it as background only. Do not repeat, continue, or answer that carried-forward final directly. Respond only to the latest human request and the current task.`;
|
If you see a message beginning with "${CARRIED_FORWARD_MARKER_PREFIX}", treat it as background only. Do not repeat, continue, or answer that carried-forward final directly. Respond only to the latest human request and the current task.`;
|
||||||
|
|
||||||
const ARBITER_TURN_OUTPUT_CONTEXT_LIMIT = 6;
|
const ARBITER_TURN_OUTPUT_CONTEXT_LIMIT = 6;
|
||||||
|
|
||||||
@@ -159,9 +163,9 @@ function currentTaskHumanMessages(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasCarriedForwardOwnerFinal(outputs: PairedTurnOutput[]): boolean {
|
function hasCarriedForwardFinal(outputs: PairedTurnOutput[]): boolean {
|
||||||
return outputs.some((output) =>
|
return outputs.some((output) =>
|
||||||
output.output_text.startsWith(CARRIED_FORWARD_OWNER_FINAL_MARKER),
|
output.output_text.startsWith(CARRIED_FORWARD_MARKER_PREFIX),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,7 +173,7 @@ function prependCarriedForwardGuidance(
|
|||||||
prompt: string,
|
prompt: string,
|
||||||
turnOutputs: PairedTurnOutput[],
|
turnOutputs: PairedTurnOutput[],
|
||||||
): string {
|
): string {
|
||||||
if (!hasCarriedForwardOwnerFinal(turnOutputs)) {
|
if (!hasCarriedForwardFinal(turnOutputs)) {
|
||||||
return prompt;
|
return prompt;
|
||||||
}
|
}
|
||||||
return `${CARRIED_FORWARD_OWNER_FINAL_GUIDANCE}\n\n${prompt}`;
|
return `${CARRIED_FORWARD_OWNER_FINAL_GUIDANCE}\n\n${prompt}`;
|
||||||
|
|||||||
@@ -11,10 +11,9 @@ vi.mock('./config.js', async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
vi.mock('./service-routing.js', async () => {
|
vi.mock('./service-routing.js', async () => {
|
||||||
const actual =
|
const actual = await vi.importActual<typeof import('./service-routing.js')>(
|
||||||
await vi.importActual<typeof import('./service-routing.js')>(
|
'./service-routing.js',
|
||||||
'./service-routing.js',
|
);
|
||||||
);
|
|
||||||
return { ...actual, hasReviewerLease: vi.fn(() => true) };
|
return { ...actual, hasReviewerLease: vi.fn(() => true) };
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
import { type AgentOutput } from './agent-runner.js';
|
import { type AgentOutput } from './agent-runner.js';
|
||||||
import { getLastBotFinalMessage } from './db.js';
|
import {
|
||||||
|
getLastBotFinalMessage,
|
||||||
|
hasActiveCiWatcherForChat,
|
||||||
|
storeMessage,
|
||||||
|
} from './db.js';
|
||||||
|
import {
|
||||||
|
resolvePhantomFinalOutcome,
|
||||||
|
scheduleAutoContinueForPhantomFinal,
|
||||||
|
} from './phantom-notification-guard.js';
|
||||||
import { runAgentForGroup } from './message-agent-executor.js';
|
import { runAgentForGroup } from './message-agent-executor.js';
|
||||||
import { MessageTurnController } from './message-turn-controller.js';
|
import { MessageTurnController } from './message-turn-controller.js';
|
||||||
import {
|
import {
|
||||||
@@ -207,9 +215,25 @@ export function createExecuteTurn(deps: CreateExecuteTurnDeps): ExecuteTurnFn {
|
|||||||
getCloseReason: () =>
|
getCloseReason: () =>
|
||||||
deps.queue.getCloseReasonForRun?.(chatJid, runId) ?? null,
|
deps.queue.getCloseReasonForRun?.(chatJid, runId) ?? null,
|
||||||
deliverFinalText: async (text, options) => {
|
deliverFinalText: async (text, options) => {
|
||||||
|
// Guard against phantom "I'll wait … I'll be notified" dead-end finals
|
||||||
|
// with no registered watcher. Paired flows already continue on their own
|
||||||
|
// (owner → reviewer → …), so this only applies to non-paired turns:
|
||||||
|
// re-invoke the agent once to actually finish (capped, no loop), and if
|
||||||
|
// it dead-ends again, fall back to an explicit follow-up notice so the
|
||||||
|
// user is never left staring at silence.
|
||||||
|
const outcome =
|
||||||
|
resolvedDeliveryRole == null
|
||||||
|
? resolvePhantomFinalOutcome({
|
||||||
|
chatJid,
|
||||||
|
text,
|
||||||
|
hasActiveWatcher: hasActiveCiWatcherForChat(chatJid),
|
||||||
|
canAutoContinue: true,
|
||||||
|
})
|
||||||
|
: { kind: 'clean' as const, text };
|
||||||
|
let delivered = false;
|
||||||
try {
|
try {
|
||||||
return await deps.deliverFinalText({
|
delivered = await deps.deliverFinalText({
|
||||||
text,
|
text: outcome.text,
|
||||||
...(options?.attachments?.length
|
...(options?.attachments?.length
|
||||||
? { attachments: options.attachments }
|
? { attachments: options.attachments }
|
||||||
: {}),
|
: {}),
|
||||||
@@ -231,6 +255,30 @@ export function createExecuteTurn(deps: CreateExecuteTurnDeps): ExecuteTurnFn {
|
|||||||
);
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
|
const scheduled = scheduleAutoContinueForPhantomFinal({
|
||||||
|
outcomeKind: outcome.kind,
|
||||||
|
delivered,
|
||||||
|
chatJid,
|
||||||
|
groupFolder: group.folder,
|
||||||
|
runId,
|
||||||
|
storeMessage,
|
||||||
|
enqueueMessageCheck: (jid, folder) =>
|
||||||
|
deps.queue.enqueueMessageCheck(jid, folder),
|
||||||
|
});
|
||||||
|
if (scheduled) {
|
||||||
|
logger.info(
|
||||||
|
{ group: group.name, chatJid, runId },
|
||||||
|
'Auto-continuing after phantom-notification dead-end final (no watcher registered)',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
logger.warn(
|
||||||
|
{ group: group.name, chatJid, runId, err },
|
||||||
|
'Failed to schedule phantom-notification auto-continue',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return delivered;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -4172,7 +4172,7 @@ describe('createMessageRuntime', () => {
|
|||||||
// First progress flushed when the second progress arrives
|
// First progress flushed when the second progress arrives
|
||||||
expect(channel.sendAndTrack).toHaveBeenCalledWith(
|
expect(channel.sendAndTrack).toHaveBeenCalledWith(
|
||||||
chatJid,
|
chatJid,
|
||||||
P('CI 상태 확인 중입니다.\n\n0초'),
|
P('CI 상태 확인 중입니다.'),
|
||||||
);
|
);
|
||||||
// Timer tick edits the tracked progress with updated elapsed time
|
// Timer tick edits the tracked progress with updated elapsed time
|
||||||
expect(channel.editMessage).toHaveBeenCalledWith(
|
expect(channel.editMessage).toHaveBeenCalledWith(
|
||||||
@@ -4272,11 +4272,11 @@ describe('createMessageRuntime', () => {
|
|||||||
// First progress flushed when second arrives — sendAndTrack throws, falls back to sendMessage
|
// First progress flushed when second arrives — sendAndTrack throws, falls back to sendMessage
|
||||||
expect(channel.sendAndTrack).toHaveBeenCalledWith(
|
expect(channel.sendAndTrack).toHaveBeenCalledWith(
|
||||||
chatJid,
|
chatJid,
|
||||||
P('진행 중입니다.\n\n0초'),
|
P('진행 중입니다.'),
|
||||||
);
|
);
|
||||||
expect(channel.sendMessage).toHaveBeenCalledWith(
|
expect(channel.sendMessage).toHaveBeenCalledWith(
|
||||||
chatJid,
|
chatJid,
|
||||||
P('진행 중입니다.\n\n0초'),
|
P('진행 중입니다.'),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -4353,11 +4353,11 @@ describe('createMessageRuntime', () => {
|
|||||||
// First progress flushed when second arrives — sendAndTrack returns null, falls back to sendMessage
|
// First progress flushed when second arrives — sendAndTrack returns null, falls back to sendMessage
|
||||||
expect(channel.sendAndTrack).toHaveBeenCalledWith(
|
expect(channel.sendAndTrack).toHaveBeenCalledWith(
|
||||||
chatJid,
|
chatJid,
|
||||||
P('진행 중입니다.\n\n0초'),
|
P('진행 중입니다.'),
|
||||||
);
|
);
|
||||||
expect(channel.sendMessage).toHaveBeenCalledWith(
|
expect(channel.sendMessage).toHaveBeenCalledWith(
|
||||||
chatJid,
|
chatJid,
|
||||||
P('진행 중입니다.\n\n0초'),
|
P('진행 중입니다.'),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -4461,7 +4461,7 @@ describe('createMessageRuntime', () => {
|
|||||||
expect(channel.sendAndTrack).toHaveBeenCalledTimes(1);
|
expect(channel.sendAndTrack).toHaveBeenCalledTimes(1);
|
||||||
expect(channel.sendAndTrack).toHaveBeenCalledWith(
|
expect(channel.sendAndTrack).toHaveBeenCalledWith(
|
||||||
chatJid,
|
chatJid,
|
||||||
P('첫 번째 진행상황입니다.\n\n0초'),
|
P('첫 번째 진행상황입니다.'),
|
||||||
);
|
);
|
||||||
// Timer tick updates tracked progress via edit
|
// Timer tick updates tracked progress via edit
|
||||||
expect(channel.editMessage).toHaveBeenCalledWith(
|
expect(channel.editMessage).toHaveBeenCalledWith(
|
||||||
@@ -4573,7 +4573,7 @@ describe('createMessageRuntime', () => {
|
|||||||
// First progress flushed when second arrives
|
// First progress flushed when second arrives
|
||||||
expect(channel.sendAndTrack).toHaveBeenCalledWith(
|
expect(channel.sendAndTrack).toHaveBeenCalledWith(
|
||||||
chatJid,
|
chatJid,
|
||||||
P('오래 걸리는 작업입니다.\n\n0초'),
|
P('오래 걸리는 작업입니다.'),
|
||||||
);
|
);
|
||||||
// Timer ticks update the tracked progress with longer durations
|
// Timer ticks update the tracked progress with longer durations
|
||||||
expect(channel.editMessage).toHaveBeenCalledWith(
|
expect(channel.editMessage).toHaveBeenCalledWith(
|
||||||
@@ -4679,7 +4679,7 @@ describe('createMessageRuntime', () => {
|
|||||||
// First progress flushed when second arrives
|
// First progress flushed when second arrives
|
||||||
expect(channel.sendAndTrack).toHaveBeenCalledWith(
|
expect(channel.sendAndTrack).toHaveBeenCalledWith(
|
||||||
chatJid,
|
chatJid,
|
||||||
P('테스트를 돌리는 중입니다.\n\n0초'),
|
P('테스트를 돌리는 중입니다.'),
|
||||||
);
|
);
|
||||||
// Timer tick updates tracked progress
|
// Timer tick updates tracked progress
|
||||||
expect(channel.editMessage).toHaveBeenCalledWith(
|
expect(channel.editMessage).toHaveBeenCalledWith(
|
||||||
@@ -5088,13 +5088,13 @@ describe('createMessageRuntime', () => {
|
|||||||
expect(channel.sendAndTrack).toHaveBeenNthCalledWith(
|
expect(channel.sendAndTrack).toHaveBeenNthCalledWith(
|
||||||
1,
|
1,
|
||||||
chatJid,
|
chatJid,
|
||||||
P('첫 번째 진행상황입니다.\n\n0초'),
|
P('첫 번째 진행상황입니다.'),
|
||||||
);
|
);
|
||||||
// After empty final resets state, third progress flushed when fourth arrives (new message)
|
// After empty final resets state, third progress flushed when fourth arrives (new message)
|
||||||
expect(channel.sendAndTrack).toHaveBeenNthCalledWith(
|
expect(channel.sendAndTrack).toHaveBeenNthCalledWith(
|
||||||
2,
|
2,
|
||||||
chatJid,
|
chatJid,
|
||||||
P('두 번째 진행상황입니다.\n\n0초'),
|
P('두 번째 진행상황입니다.'),
|
||||||
);
|
);
|
||||||
// Timer tick edits the first tracked progress
|
// Timer tick edits the first tracked progress
|
||||||
expect(channel.editMessage).toHaveBeenCalledWith(
|
expect(channel.editMessage).toHaveBeenCalledWith(
|
||||||
@@ -5213,7 +5213,7 @@ describe('createMessageRuntime', () => {
|
|||||||
// First progress flushed when second arrives
|
// First progress flushed when second arrives
|
||||||
expect(channel.sendAndTrack).toHaveBeenCalledWith(
|
expect(channel.sendAndTrack).toHaveBeenCalledWith(
|
||||||
chatJid,
|
chatJid,
|
||||||
P('검증 중입니다.\n\n0초'),
|
P('검증 중입니다.'),
|
||||||
);
|
);
|
||||||
// Once the tracked progress exists, later progress updates replace the visible heading directly.
|
// Once the tracked progress exists, later progress updates replace the visible heading directly.
|
||||||
expect(channel.editMessage).toHaveBeenCalledWith(
|
expect(channel.editMessage).toHaveBeenCalledWith(
|
||||||
@@ -5329,7 +5329,7 @@ describe('createMessageRuntime', () => {
|
|||||||
// The first flushed progress is still tracked
|
// The first flushed progress is still tracked
|
||||||
expect(channel.sendAndTrack).toHaveBeenCalledWith(
|
expect(channel.sendAndTrack).toHaveBeenCalledWith(
|
||||||
chatJid,
|
chatJid,
|
||||||
P('진행 중입니다.\n\n0초'),
|
P('진행 중입니다.'),
|
||||||
);
|
);
|
||||||
// Edit is attempted on the tracked message (first fails, subsequent succeed)
|
// Edit is attempted on the tracked message (first fails, subsequent succeed)
|
||||||
expect(channel.editMessage).toHaveBeenCalledWith(
|
expect(channel.editMessage).toHaveBeenCalledWith(
|
||||||
@@ -5568,7 +5568,7 @@ describe('createMessageRuntime', () => {
|
|||||||
// First progress flushed when second arrives
|
// First progress flushed when second arrives
|
||||||
expect(channel.sendAndTrack).toHaveBeenCalledWith(
|
expect(channel.sendAndTrack).toHaveBeenCalledWith(
|
||||||
chatJid,
|
chatJid,
|
||||||
P('중간 진행상황입니다.\n\n0초'),
|
P('중간 진행상황입니다.'),
|
||||||
);
|
);
|
||||||
// Error replaces the tracked progress message with the failure final
|
// Error replaces the tracked progress message with the failure final
|
||||||
expect(channel.sendMessage).not.toHaveBeenCalled();
|
expect(channel.sendMessage).not.toHaveBeenCalled();
|
||||||
|
|||||||
@@ -496,7 +496,10 @@ export class MessageTurnController {
|
|||||||
? 0
|
? 0
|
||||||
: Math.floor((Date.now() - this.progressStartedAt) / 5_000) * 5000;
|
: Math.floor((Date.now() - this.progressStartedAt) / 5_000) * 5000;
|
||||||
|
|
||||||
const suffix = `\n\n${formatElapsedKorean(elapsedMs)}`;
|
// Skip the elapsed-time label until at least one 5s bucket has elapsed, so
|
||||||
|
// a freshly-created progress message doesn't show a meaningless "0초".
|
||||||
|
const suffix =
|
||||||
|
elapsedMs >= 5000 ? `\n\n${formatElapsedKorean(elapsedMs)}` : '';
|
||||||
const body = this.composeProgressBody(text);
|
const body = this.composeProgressBody(text);
|
||||||
|
|
||||||
this.persistProgressBody(body);
|
this.persistProgressBody(body);
|
||||||
|
|||||||
48
src/moa.test.ts
Normal file
48
src/moa.test.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
import { assertSafeMoaBaseUrl } from './moa.js';
|
||||||
|
|
||||||
|
describe('assertSafeMoaBaseUrl', () => {
|
||||||
|
it('accepts public https endpoints', () => {
|
||||||
|
expect(() =>
|
||||||
|
assertSafeMoaBaseUrl('https://api.moonshot.cn/v1'),
|
||||||
|
).not.toThrow();
|
||||||
|
expect(() =>
|
||||||
|
assertSafeMoaBaseUrl('https://open.bigmodel.cn/api/paas/v4'),
|
||||||
|
).not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects non-http(s) schemes', () => {
|
||||||
|
expect(() => assertSafeMoaBaseUrl('file:///etc/passwd')).toThrow(
|
||||||
|
/http\(s\)/,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects loopback and localhost', () => {
|
||||||
|
expect(() => assertSafeMoaBaseUrl('http://127.0.0.1:8080')).toThrow(
|
||||||
|
/not allowed/,
|
||||||
|
);
|
||||||
|
expect(() => assertSafeMoaBaseUrl('http://localhost/v1')).toThrow(
|
||||||
|
/not allowed/,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects cloud metadata and private ranges', () => {
|
||||||
|
expect(() => assertSafeMoaBaseUrl('http://169.254.169.254/latest')).toThrow(
|
||||||
|
/not allowed/,
|
||||||
|
);
|
||||||
|
expect(() => assertSafeMoaBaseUrl('http://10.0.0.5/v1')).toThrow(
|
||||||
|
/not allowed/,
|
||||||
|
);
|
||||||
|
expect(() => assertSafeMoaBaseUrl('http://192.168.1.1/v1')).toThrow(
|
||||||
|
/not allowed/,
|
||||||
|
);
|
||||||
|
expect(() => assertSafeMoaBaseUrl('http://100.101.210.95/v1')).toThrow(
|
||||||
|
/not allowed/,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects malformed URLs', () => {
|
||||||
|
expect(() => assertSafeMoaBaseUrl('not-a-url')).toThrow(/Invalid/);
|
||||||
|
});
|
||||||
|
});
|
||||||
58
src/moa.ts
58
src/moa.ts
@@ -44,6 +44,63 @@ function normalizeError(err: unknown): string {
|
|||||||
return err instanceof Error ? err.message : String(err);
|
return err instanceof Error ? err.message : String(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reject MoA base URLs that point at loopback / private / link-local hosts.
|
||||||
|
*
|
||||||
|
* MoA reference models are external SaaS endpoints (Kimi, GLM, ...). Allowing
|
||||||
|
* an internal host lets a compromised or misconfigured settings write turn the
|
||||||
|
* server-side MoA fetch into an SSRF probe (e.g. cloud metadata at
|
||||||
|
* 169.254.169.254). Only http(s) to a non-private host is permitted.
|
||||||
|
*/
|
||||||
|
export function assertSafeMoaBaseUrl(baseUrl: string): URL {
|
||||||
|
let parsed: URL;
|
||||||
|
try {
|
||||||
|
parsed = new URL(baseUrl);
|
||||||
|
} catch {
|
||||||
|
throw new Error(`Invalid MoA base URL: ${baseUrl}`);
|
||||||
|
}
|
||||||
|
if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') {
|
||||||
|
throw new Error(`MoA base URL must use http(s): ${baseUrl}`);
|
||||||
|
}
|
||||||
|
if (isPrivateOrLocalHost(parsed.hostname)) {
|
||||||
|
throw new Error(
|
||||||
|
`MoA base URL host is not allowed (loopback/private/link-local): ${parsed.hostname}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPrivateOrLocalHost(hostname: string): boolean {
|
||||||
|
const host = hostname.replace(/^\[|\]$/g, '').toLowerCase();
|
||||||
|
if (
|
||||||
|
host === 'localhost' ||
|
||||||
|
host.endsWith('.localhost') ||
|
||||||
|
host.endsWith('.internal') ||
|
||||||
|
host.endsWith('.local')
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// IPv6 loopback / link-local / unique-local
|
||||||
|
if (host === '::1' || host === '::') return true;
|
||||||
|
if (
|
||||||
|
host.startsWith('fe80:') ||
|
||||||
|
host.startsWith('fc') ||
|
||||||
|
host.startsWith('fd')
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const ipv4 = host.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
|
||||||
|
if (ipv4) {
|
||||||
|
const [a, b] = [Number(ipv4[1]), Number(ipv4[2])];
|
||||||
|
if (a === 127 || a === 0 || a === 10) return true; // loopback / this-host / private
|
||||||
|
if (a === 169 && b === 254) return true; // link-local incl. cloud metadata
|
||||||
|
if (a === 172 && b >= 16 && b <= 31) return true; // private
|
||||||
|
if (a === 192 && b === 168) return true; // private
|
||||||
|
if (a === 100 && b >= 64 && b <= 127) return true; // CGNAT / tailnet
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
function recordReferenceStatus(
|
function recordReferenceStatus(
|
||||||
status: Omit<MoaReferenceStatus, 'checkedAt'>,
|
status: Omit<MoaReferenceStatus, 'checkedAt'>,
|
||||||
): MoaReferenceStatus {
|
): MoaReferenceStatus {
|
||||||
@@ -69,6 +126,7 @@ async function queryModel(
|
|||||||
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
assertSafeMoaBaseUrl(model.baseUrl);
|
||||||
const base = model.baseUrl.replace(/\/+$/, '');
|
const base = model.baseUrl.replace(/\/+$/, '');
|
||||||
const isAnthropic = model.apiFormat === 'anthropic';
|
const isAnthropic = model.apiFormat === 'anthropic';
|
||||||
|
|
||||||
|
|||||||
@@ -161,4 +161,151 @@ describe('paired execution carry-forward attachments', () => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('carries context forward on a cold start after the previous task already closed', () => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
const previousTask = buildTask({
|
||||||
|
id: 'task-previous',
|
||||||
|
status: 'completed',
|
||||||
|
completion_reason: 'done',
|
||||||
|
});
|
||||||
|
// No open task exists -> cold start path.
|
||||||
|
vi.mocked(db.getLatestOpenPairedTaskForChat).mockReturnValue(undefined);
|
||||||
|
vi.mocked(db.getLatestPairedTaskForChat).mockReturnValue(previousTask);
|
||||||
|
vi.mocked(db.getPairedTurnOutputs).mockReturnValue([
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
task_id: previousTask.id,
|
||||||
|
turn_number: 1,
|
||||||
|
role: 'owner',
|
||||||
|
output_text:
|
||||||
|
'TASK_DONE\n사용자 액션 아이템: 1. 재배포 2. 서버 업데이트',
|
||||||
|
created_at: '2026-03-28T00:01:00.000Z',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const resolved = resolveOwnerTaskForHumanMessage({
|
||||||
|
group,
|
||||||
|
chatJid: 'dc:test',
|
||||||
|
roomRoleContext: ownerContext,
|
||||||
|
// no existingTask -> resolves via getLatestOpenPairedTaskForChat (none)
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(resolved.supersededTask).toBeNull();
|
||||||
|
expect(db.insertPairedTurnOutput).toHaveBeenCalledWith(
|
||||||
|
expect.any(String),
|
||||||
|
0,
|
||||||
|
'owner',
|
||||||
|
expect.stringContaining(
|
||||||
|
'TASK_DONE\n사용자 액션 아이템: 1. 재배포 2. 서버 업데이트',
|
||||||
|
),
|
||||||
|
expect.objectContaining({ createdAt: '2026-03-28T00:01:00.000Z' }),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('carries the last final owner output, skipping an intermediate STEP_DONE', () => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
const previousTask = buildTask({
|
||||||
|
id: 'task-arbiter',
|
||||||
|
status: 'completed',
|
||||||
|
completion_reason: 'arbiter_escalated',
|
||||||
|
});
|
||||||
|
vi.mocked(db.getLatestOpenPairedTaskForChat).mockReturnValue(undefined);
|
||||||
|
vi.mocked(db.getLatestPairedTaskForChat).mockReturnValue(previousTask);
|
||||||
|
vi.mocked(db.getPairedTurnOutputs).mockReturnValue([
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
task_id: previousTask.id,
|
||||||
|
turn_number: 1,
|
||||||
|
role: 'owner',
|
||||||
|
output_text:
|
||||||
|
'TASK_DONE\n사용자 액션 아이템: 1. 재배포 2. 서버 업데이트',
|
||||||
|
created_at: '2026-03-28T00:01:00.000Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
task_id: previousTask.id,
|
||||||
|
turn_number: 3,
|
||||||
|
role: 'owner',
|
||||||
|
output_text: 'STEP_DONE\nexception 경로 버그를 고쳤습니다.',
|
||||||
|
created_at: '2026-03-28T00:03:00.000Z',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
resolveOwnerTaskForHumanMessage({
|
||||||
|
group,
|
||||||
|
chatJid: 'dc:test',
|
||||||
|
roomRoleContext: ownerContext,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(db.insertPairedTurnOutput).toHaveBeenCalledWith(
|
||||||
|
expect.any(String),
|
||||||
|
0,
|
||||||
|
'owner',
|
||||||
|
expect.stringContaining('사용자 액션 아이템'),
|
||||||
|
expect.objectContaining({ createdAt: '2026-03-28T00:01:00.000Z' }),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('carries the arbiter verdict forward when the previous task ended on an arbiter escalation', () => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
const previousTask = buildTask({
|
||||||
|
id: 'task-escalated',
|
||||||
|
status: 'completed',
|
||||||
|
completion_reason: 'arbiter_escalated',
|
||||||
|
});
|
||||||
|
vi.mocked(db.getLatestOpenPairedTaskForChat).mockReturnValue(undefined);
|
||||||
|
vi.mocked(db.getLatestPairedTaskForChat).mockReturnValue(previousTask);
|
||||||
|
// The arbiter's ESCALATE is the last thing the user saw — they are now
|
||||||
|
// replying to it, so it (not the earlier owner turn) must carry forward.
|
||||||
|
vi.mocked(db.getPairedTurnOutputs).mockReturnValue([
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
task_id: previousTask.id,
|
||||||
|
turn_number: 1,
|
||||||
|
role: 'owner',
|
||||||
|
output_text: 'STEP_DONE\n방향 A로 진행했습니다.',
|
||||||
|
created_at: '2026-03-28T00:01:00.000Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
task_id: previousTask.id,
|
||||||
|
turn_number: 2,
|
||||||
|
role: 'reviewer',
|
||||||
|
output_text: 'REVISE\n방향 B가 맞습니다.',
|
||||||
|
created_at: '2026-03-28T00:02:00.000Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
task_id: previousTask.id,
|
||||||
|
turn_number: 3,
|
||||||
|
role: 'arbiter',
|
||||||
|
output_text:
|
||||||
|
'ESCALATE\n두 방향(A/B) 중 사용자가 선택해야 합니다. 1) A 유지 2) B 전환',
|
||||||
|
created_at: '2026-03-28T00:03:00.000Z',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
resolveOwnerTaskForHumanMessage({
|
||||||
|
group,
|
||||||
|
chatJid: 'dc:test',
|
||||||
|
roomRoleContext: ownerContext,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(db.insertPairedTurnOutput).toHaveBeenCalledWith(
|
||||||
|
expect.any(String),
|
||||||
|
0,
|
||||||
|
'owner',
|
||||||
|
expect.stringContaining('latest arbiter verdict'),
|
||||||
|
expect.objectContaining({ createdAt: '2026-03-28T00:03:00.000Z' }),
|
||||||
|
);
|
||||||
|
// And it must carry the arbiter's escalation text, not the earlier owner turn.
|
||||||
|
expect(db.insertPairedTurnOutput).toHaveBeenCalledWith(
|
||||||
|
expect.any(String),
|
||||||
|
0,
|
||||||
|
'owner',
|
||||||
|
expect.stringContaining('두 방향(A/B) 중 사용자가 선택'),
|
||||||
|
expect.anything(),
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ vi.mock('./db.js', () => {
|
|||||||
return {
|
return {
|
||||||
cancelPairedTurn: vi.fn(),
|
cancelPairedTurn: vi.fn(),
|
||||||
createPairedTask: vi.fn(),
|
createPairedTask: vi.fn(),
|
||||||
|
getEffectiveRuntimeRoomMode: vi.fn(() => 'tribunal'),
|
||||||
getLatestPairedTaskForChat: vi.fn(),
|
getLatestPairedTaskForChat: vi.fn(),
|
||||||
getLatestOpenPairedTaskForChat: vi.fn(),
|
getLatestOpenPairedTaskForChat: vi.fn(),
|
||||||
getPairedTaskById: vi.fn(),
|
getPairedTaskById: vi.fn(),
|
||||||
@@ -33,6 +34,7 @@ vi.mock('./db.js', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
vi.mock('./paired-workspace-manager.js', () => ({
|
vi.mock('./paired-workspace-manager.js', () => ({
|
||||||
|
ensurePairedWorkspaceProvisioned: vi.fn(() => '/repo/self-healed-canonical'),
|
||||||
isOwnerWorkspaceRepairNeededError: vi.fn(() => false),
|
isOwnerWorkspaceRepairNeededError: vi.fn(() => false),
|
||||||
markPairedTaskReviewReady: vi.fn(),
|
markPairedTaskReviewReady: vi.fn(),
|
||||||
prepareReviewerWorkspaceForExecution: vi.fn(),
|
prepareReviewerWorkspaceForExecution: vi.fn(),
|
||||||
@@ -48,6 +50,19 @@ vi.mock('./logger.js', () => ({
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// This suite verifies the flag-OFF ("by default") carry-forward behavior. Pin
|
||||||
|
// the switch to false so the result is deterministic regardless of the
|
||||||
|
// deployment's .env override (the flag-ON path is covered separately in
|
||||||
|
// paired-execution-context-carry-forward.test.ts).
|
||||||
|
vi.mock('./config.js', async () => {
|
||||||
|
const actual =
|
||||||
|
await vi.importActual<typeof import('./config.js')>('./config.js');
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
PAIRED_CARRY_FORWARD_LATEST_OWNER_FINAL: false,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
import * as db from './db.js';
|
import * as db from './db.js';
|
||||||
import * as config from './config.js';
|
import * as config from './config.js';
|
||||||
import {
|
import {
|
||||||
@@ -299,6 +314,54 @@ describe('paired execution context', () => {
|
|||||||
expect(db.insertPairedTurnOutput).not.toHaveBeenCalled();
|
expect(db.insertPairedTurnOutput).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('self-heals a tribunal room with no work_dir by provisioning the canonical workspace', () => {
|
||||||
|
// Reproduces the silent-room incident: a tribunal room whose work_dir was
|
||||||
|
// never provisioned would return a null task and never run the owner.
|
||||||
|
const noWorkDirGroup: RegisteredGroup = { ...group, workDir: undefined };
|
||||||
|
vi.mocked(db.getEffectiveRuntimeRoomMode).mockReturnValue('tribunal');
|
||||||
|
vi.mocked(
|
||||||
|
pairedWorkspaceManager.ensurePairedWorkspaceProvisioned,
|
||||||
|
).mockReturnValue('/repo/self-healed-canonical');
|
||||||
|
vi.mocked(db.getLatestOpenPairedTaskForChat).mockReturnValue(undefined);
|
||||||
|
vi.mocked(db.getLatestPairedTaskForChat).mockReturnValue(undefined);
|
||||||
|
|
||||||
|
const result = resolveOwnerTaskForHumanMessage({
|
||||||
|
group: noWorkDirGroup,
|
||||||
|
chatJid: 'dc:test',
|
||||||
|
roomRoleContext: ownerContext,
|
||||||
|
existingTask: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(
|
||||||
|
pairedWorkspaceManager.ensurePairedWorkspaceProvisioned,
|
||||||
|
).toHaveBeenCalledWith({
|
||||||
|
chatJid: 'dc:test',
|
||||||
|
groupFolder: noWorkDirGroup.folder,
|
||||||
|
});
|
||||||
|
expect(result.task).not.toBeNull();
|
||||||
|
expect(db.createPairedTask).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not provision a workspace for a single-mode room with no work_dir', () => {
|
||||||
|
const noWorkDirGroup: RegisteredGroup = { ...group, workDir: undefined };
|
||||||
|
vi.mocked(db.getEffectiveRuntimeRoomMode).mockReturnValue('single');
|
||||||
|
vi.mocked(db.getLatestOpenPairedTaskForChat).mockReturnValue(undefined);
|
||||||
|
vi.mocked(db.getLatestPairedTaskForChat).mockReturnValue(undefined);
|
||||||
|
|
||||||
|
const result = resolveOwnerTaskForHumanMessage({
|
||||||
|
group: noWorkDirGroup,
|
||||||
|
chatJid: 'dc:test',
|
||||||
|
roomRoleContext: ownerContext,
|
||||||
|
existingTask: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(
|
||||||
|
pairedWorkspaceManager.ensurePairedWorkspaceProvisioned,
|
||||||
|
).not.toHaveBeenCalled();
|
||||||
|
expect(result.task).toBeNull();
|
||||||
|
expect(db.createPairedTask).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
it('records a quick reopen when a new owner task starts shortly after TASK_DONE completion', () => {
|
it('records a quick reopen when a new owner task starts shortly after TASK_DONE completion', () => {
|
||||||
const previousTask = buildPairedTask({
|
const previousTask = buildPairedTask({
|
||||||
id: 'task-completed',
|
id: 'task-completed',
|
||||||
@@ -677,7 +740,9 @@ describe('paired execution context', () => {
|
|||||||
group,
|
group,
|
||||||
chatJid: 'dc:test',
|
chatJid: 'dc:test',
|
||||||
runId: 'run-host-reviewer',
|
runId: 'run-host-reviewer',
|
||||||
roomRoleContext: reviewerContext,
|
// Force a claude-code reviewer so this test exercises the Claude
|
||||||
|
// read-only branch regardless of the deployment's default reviewer type.
|
||||||
|
roomRoleContext: { ...reviewerContext, reviewerAgentType: 'claude-code' },
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result?.envOverrides).toMatchObject({
|
expect(result?.envOverrides).toMatchObject({
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
cancelPairedTurn,
|
cancelPairedTurn,
|
||||||
createPairedTask,
|
createPairedTask,
|
||||||
|
getEffectiveRuntimeRoomMode,
|
||||||
getLatestPairedTaskForChat,
|
getLatestPairedTaskForChat,
|
||||||
getLatestOpenPairedTaskForChat,
|
getLatestOpenPairedTaskForChat,
|
||||||
getPairedTaskById,
|
getPairedTaskById,
|
||||||
@@ -48,6 +49,7 @@ import {
|
|||||||
} from './paired-task-status.js';
|
} from './paired-task-status.js';
|
||||||
import { resolveCanonicalSourceRef } from './paired-source-ref.js';
|
import { resolveCanonicalSourceRef } from './paired-source-ref.js';
|
||||||
import {
|
import {
|
||||||
|
ensurePairedWorkspaceProvisioned,
|
||||||
isOwnerWorkspaceRepairNeededError,
|
isOwnerWorkspaceRepairNeededError,
|
||||||
prepareReviewerWorkspaceForExecution,
|
prepareReviewerWorkspaceForExecution,
|
||||||
provisionOwnerWorkspaceForPairedTask,
|
provisionOwnerWorkspaceForPairedTask,
|
||||||
@@ -74,7 +76,21 @@ function ensurePairedProject(
|
|||||||
chatJid: string,
|
chatJid: string,
|
||||||
): string | null {
|
): string | null {
|
||||||
if (!group.workDir) {
|
if (!group.workDir) {
|
||||||
return null;
|
// Self-heal: a tribunal room whose work_dir was never provisioned (null)
|
||||||
|
// makes resolveOwnerTaskForHumanMessage return a null task, so the owner
|
||||||
|
// never runs and the room goes completely silent — the user's messages get
|
||||||
|
// no reply at all. Provision the canonical workspace on demand so the owner
|
||||||
|
// can always start. Guarded to tribunal rooms so a single-mode room never
|
||||||
|
// gets a spurious paired workspace. Idempotent: ensurePairedWorkspace-
|
||||||
|
// Provisioned skips git init/commit when they already exist and upserts the
|
||||||
|
// paired project itself.
|
||||||
|
if (getEffectiveRuntimeRoomMode(chatJid) !== 'tribunal') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return ensurePairedWorkspaceProvisioned({
|
||||||
|
chatJid,
|
||||||
|
groupFolder: group.folder,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const now = new Date().toISOString();
|
const now = new Date().toISOString();
|
||||||
@@ -229,18 +245,32 @@ function cancelOutstandingFinalizeOwnerTurn(task: PairedTask): void {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getLatestTurnOutputByRole(
|
function isIntermediateStepOutput(outputText: string): boolean {
|
||||||
|
// STEP_DONE marks an intermediate step that keeps the task active; it is not
|
||||||
|
// the user-facing finalize summary, so it is a poor carry-forward anchor.
|
||||||
|
return /^\s*STEP_DONE\b/.test(outputText);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLatestUserFacingFinalOutput(
|
||||||
taskId: string,
|
taskId: string,
|
||||||
role: PairedRoomRole,
|
|
||||||
): PairedTurnOutput | null {
|
): PairedTurnOutput | null {
|
||||||
|
// The user only ever sees owner finals and arbiter verdicts (reviewer turns
|
||||||
|
// are internal). Whichever of those the previous task ended on is what the
|
||||||
|
// user is replying to next — e.g. an arbiter ESCALATE asking them to choose.
|
||||||
|
// So carry the chronologically-latest owner/arbiter final forward.
|
||||||
|
const userFacingOutputs = [...getPairedTurnOutputs(taskId)]
|
||||||
|
.reverse()
|
||||||
|
.filter((output) => output.role === 'owner' || output.role === 'arbiter');
|
||||||
return (
|
return (
|
||||||
[...getPairedTurnOutputs(taskId)]
|
userFacingOutputs.find(
|
||||||
.reverse()
|
(output) => !isIntermediateStepOutput(output.output_text),
|
||||||
.find((output) => output.role === role) ?? null
|
) ??
|
||||||
|
userFacingOutputs[0] ??
|
||||||
|
null
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function carryForwardLatestOwnerFinal(args: {
|
function carryForwardLatestFinal(args: {
|
||||||
sourceTask: PairedTask;
|
sourceTask: PairedTask;
|
||||||
targetTask: PairedTask;
|
targetTask: PairedTask;
|
||||||
}): void {
|
}): void {
|
||||||
@@ -248,32 +278,34 @@ function carryForwardLatestOwnerFinal(args: {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const latestOwnerFinal = getLatestTurnOutputByRole(
|
const latestFinal = getLatestUserFacingFinalOutput(args.sourceTask.id);
|
||||||
args.sourceTask.id,
|
if (!latestFinal) {
|
||||||
'owner',
|
|
||||||
);
|
|
||||||
if (!latestOwnerFinal) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const label =
|
||||||
|
latestFinal.role === 'arbiter'
|
||||||
|
? 'latest arbiter verdict'
|
||||||
|
: 'latest owner final';
|
||||||
insertPairedTurnOutput(
|
insertPairedTurnOutput(
|
||||||
args.targetTask.id,
|
args.targetTask.id,
|
||||||
0,
|
0,
|
||||||
'owner',
|
'owner',
|
||||||
`[Carried forward context from the previous task: latest owner final]\n${latestOwnerFinal.output_text}`,
|
`[Carried forward context from the previous task: ${label}]\n${latestFinal.output_text}`,
|
||||||
{
|
{
|
||||||
createdAt: latestOwnerFinal.created_at,
|
createdAt: latestFinal.created_at,
|
||||||
attachments: latestOwnerFinal.attachments,
|
attachments: latestFinal.attachments,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
logger.info(
|
logger.info(
|
||||||
{
|
{
|
||||||
sourceTaskId: args.sourceTask.id,
|
sourceTaskId: args.sourceTask.id,
|
||||||
targetTaskId: args.targetTask.id,
|
targetTaskId: args.targetTask.id,
|
||||||
carriedChars: latestOwnerFinal.output_text.length,
|
carriedRole: latestFinal.role,
|
||||||
attachmentCount: latestOwnerFinal.attachments?.length ?? 0,
|
carriedChars: latestFinal.output_text.length,
|
||||||
|
attachmentCount: latestFinal.attachments?.length ?? 0,
|
||||||
},
|
},
|
||||||
'Carried forward latest owner final into superseding paired task',
|
'Carried forward latest user-facing final into superseding paired task',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -293,16 +325,27 @@ export function resolveOwnerTaskForHumanMessage(args: {
|
|||||||
args.existingTask ?? getLatestOpenPairedTaskForChat(args.chatJid) ?? null;
|
args.existingTask ?? getLatestOpenPairedTaskForChat(args.chatJid) ?? null;
|
||||||
|
|
||||||
if (!existing) {
|
if (!existing) {
|
||||||
maybeRecordTaskDoneReopen(getLatestPairedTaskForChat(args.chatJid) ?? null);
|
const previousTask = getLatestPairedTaskForChat(args.chatJid) ?? null;
|
||||||
|
maybeRecordTaskDoneReopen(previousTask);
|
||||||
|
const newTask = canonicalWorkDir
|
||||||
|
? createActiveTaskForRoom({
|
||||||
|
group: args.group,
|
||||||
|
chatJid: args.chatJid,
|
||||||
|
canonicalWorkDir,
|
||||||
|
roomRoleContext: args.roomRoleContext,
|
||||||
|
})
|
||||||
|
: null;
|
||||||
|
// Cold start after the previous task already closed (completed): seed the
|
||||||
|
// fresh task with the previous task's latest owner final so the owner keeps
|
||||||
|
// continuity across sessions instead of answering with "no context".
|
||||||
|
if (newTask && previousTask) {
|
||||||
|
carryForwardLatestFinal({
|
||||||
|
sourceTask: previousTask,
|
||||||
|
targetTask: newTask,
|
||||||
|
});
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
task: canonicalWorkDir
|
task: newTask,
|
||||||
? createActiveTaskForRoom({
|
|
||||||
group: args.group,
|
|
||||||
chatJid: args.chatJid,
|
|
||||||
canonicalWorkDir,
|
|
||||||
roomRoleContext: args.roomRoleContext,
|
|
||||||
})
|
|
||||||
: null,
|
|
||||||
supersededTask: null,
|
supersededTask: null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -341,7 +384,7 @@ export function resolveOwnerTaskForHumanMessage(args: {
|
|||||||
canonicalWorkDir,
|
canonicalWorkDir,
|
||||||
roomRoleContext: args.roomRoleContext,
|
roomRoleContext: args.roomRoleContext,
|
||||||
});
|
});
|
||||||
carryForwardLatestOwnerFinal({
|
carryForwardLatestFinal({
|
||||||
sourceTask: existing,
|
sourceTask: existing,
|
||||||
targetTask: newTask,
|
targetTask: newTask,
|
||||||
});
|
});
|
||||||
|
|||||||
233
src/phantom-notification-guard.test.ts
Normal file
233
src/phantom-notification-guard.test.ts
Normal file
@@ -0,0 +1,233 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
|
import {
|
||||||
|
applyPhantomNotificationGuard,
|
||||||
|
isPhantomNotificationDeadEnd,
|
||||||
|
PHANTOM_CONTINUE_NUDGE,
|
||||||
|
PHANTOM_NOTIFICATION_NOTICE,
|
||||||
|
resolvePhantomFinalOutcome,
|
||||||
|
scheduleAutoContinueForPhantomFinal,
|
||||||
|
_resetPhantomAutoContinueForTests,
|
||||||
|
} from './phantom-notification-guard.js';
|
||||||
|
|
||||||
|
describe('isPhantomNotificationDeadEnd', () => {
|
||||||
|
it('detects the reported "I\'ll wait for the build … I\'ll be notified" final', () => {
|
||||||
|
expect(
|
||||||
|
isPhantomNotificationDeadEnd(
|
||||||
|
"I'll wait for the build to complete — I'll be notified.",
|
||||||
|
),
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('detects close variants of the notification promise', () => {
|
||||||
|
expect(
|
||||||
|
isPhantomNotificationDeadEnd('I will be notified when it finishes.'),
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
isPhantomNotificationDeadEnd('Waiting for the deploy to finish now.'),
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
isPhantomNotificationDeadEnd("I'll be notified once CI is green."),
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not flag ordinary status finals', () => {
|
||||||
|
expect(
|
||||||
|
isPhantomNotificationDeadEnd(
|
||||||
|
'TASK_DONE\n빌드를 끝냈고 테스트 1597개가 모두 통과했습니다.',
|
||||||
|
),
|
||||||
|
).toBe(false);
|
||||||
|
expect(
|
||||||
|
isPhantomNotificationDeadEnd('The build completed and I pushed the fix.'),
|
||||||
|
).toBe(false);
|
||||||
|
expect(isPhantomNotificationDeadEnd('')).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('applyPhantomNotificationGuard', () => {
|
||||||
|
const deadEnd = "I'll wait for the build to complete — I'll be notified.";
|
||||||
|
|
||||||
|
it('appends the follow-up notice for a dead-end final with no active watcher', () => {
|
||||||
|
const result = applyPhantomNotificationGuard(deadEnd, {
|
||||||
|
hasActiveWatcher: false,
|
||||||
|
});
|
||||||
|
expect(result.startsWith(deadEnd)).toBe(true);
|
||||||
|
expect(result).toContain(PHANTOM_NOTIFICATION_NOTICE.trim());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('leaves the final untouched when a watcher is actually active', () => {
|
||||||
|
expect(
|
||||||
|
applyPhantomNotificationGuard(deadEnd, { hasActiveWatcher: true }),
|
||||||
|
).toBe(deadEnd);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('leaves ordinary finals untouched', () => {
|
||||||
|
const ok = 'TASK_DONE\n작업을 마쳤습니다.';
|
||||||
|
expect(applyPhantomNotificationGuard(ok, { hasActiveWatcher: false })).toBe(
|
||||||
|
ok,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('is idempotent — never appends the notice twice', () => {
|
||||||
|
const once = applyPhantomNotificationGuard(deadEnd, {
|
||||||
|
hasActiveWatcher: false,
|
||||||
|
});
|
||||||
|
const twice = applyPhantomNotificationGuard(once, {
|
||||||
|
hasActiveWatcher: false,
|
||||||
|
});
|
||||||
|
expect(twice).toBe(once);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('resolvePhantomFinalOutcome', () => {
|
||||||
|
const deadEnd = "I'll wait for the build to complete — I'll be notified.";
|
||||||
|
const chatJid = 'dc:test-room';
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
_resetPhantomAutoContinueForTests();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('auto-continues once, then falls back to the notice on a repeated dead-end', () => {
|
||||||
|
const first = resolvePhantomFinalOutcome({
|
||||||
|
chatJid,
|
||||||
|
text: deadEnd,
|
||||||
|
hasActiveWatcher: false,
|
||||||
|
canAutoContinue: true,
|
||||||
|
});
|
||||||
|
expect(first.kind).toBe('auto-continue');
|
||||||
|
|
||||||
|
// Second consecutive dead-end for the same chat hits the cap → notice, no
|
||||||
|
// further auto-continue (this is the loop guard).
|
||||||
|
const second = resolvePhantomFinalOutcome({
|
||||||
|
chatJid,
|
||||||
|
text: deadEnd,
|
||||||
|
hasActiveWatcher: false,
|
||||||
|
canAutoContinue: true,
|
||||||
|
});
|
||||||
|
expect(second.kind).toBe('notice');
|
||||||
|
expect(second.text).toContain(PHANTOM_NOTIFICATION_NOTICE.trim());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('resets the streak after a clean final so a later dead-end can auto-continue again', () => {
|
||||||
|
expect(
|
||||||
|
resolvePhantomFinalOutcome({
|
||||||
|
chatJid,
|
||||||
|
text: deadEnd,
|
||||||
|
hasActiveWatcher: false,
|
||||||
|
canAutoContinue: true,
|
||||||
|
}).kind,
|
||||||
|
).toBe('auto-continue');
|
||||||
|
|
||||||
|
// A clean final resets the streak.
|
||||||
|
expect(
|
||||||
|
resolvePhantomFinalOutcome({
|
||||||
|
chatJid,
|
||||||
|
text: 'TASK_DONE\n작업을 마쳤습니다.',
|
||||||
|
hasActiveWatcher: false,
|
||||||
|
canAutoContinue: true,
|
||||||
|
}).kind,
|
||||||
|
).toBe('clean');
|
||||||
|
|
||||||
|
// Streak reset → next dead-end auto-continues again.
|
||||||
|
expect(
|
||||||
|
resolvePhantomFinalOutcome({
|
||||||
|
chatJid,
|
||||||
|
text: deadEnd,
|
||||||
|
hasActiveWatcher: false,
|
||||||
|
canAutoContinue: true,
|
||||||
|
}).kind,
|
||||||
|
).toBe('auto-continue');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('never auto-continues when a watcher is active (legitimate wait)', () => {
|
||||||
|
expect(
|
||||||
|
resolvePhantomFinalOutcome({
|
||||||
|
chatJid,
|
||||||
|
text: deadEnd,
|
||||||
|
hasActiveWatcher: true,
|
||||||
|
canAutoContinue: true,
|
||||||
|
}).kind,
|
||||||
|
).toBe('clean');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('emits the notice (never auto-continue) when auto-continue is disabled', () => {
|
||||||
|
const outcome = resolvePhantomFinalOutcome({
|
||||||
|
chatJid,
|
||||||
|
text: deadEnd,
|
||||||
|
hasActiveWatcher: false,
|
||||||
|
canAutoContinue: false,
|
||||||
|
});
|
||||||
|
expect(outcome.kind).toBe('notice');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('scheduleAutoContinueForPhantomFinal', () => {
|
||||||
|
it('injects the corrective nudge and re-enqueues when the dead-end final delivered', () => {
|
||||||
|
const storeMessage = vi.fn();
|
||||||
|
const enqueueMessageCheck = vi.fn();
|
||||||
|
|
||||||
|
const scheduled = scheduleAutoContinueForPhantomFinal({
|
||||||
|
outcomeKind: 'auto-continue',
|
||||||
|
delivered: true,
|
||||||
|
chatJid: 'dc:test-room',
|
||||||
|
groupFolder: 'test-room',
|
||||||
|
runId: 'run-xyz',
|
||||||
|
storeMessage,
|
||||||
|
enqueueMessageCheck,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(scheduled).toBe(true);
|
||||||
|
expect(storeMessage).toHaveBeenCalledTimes(1);
|
||||||
|
expect(storeMessage).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
chat_jid: 'dc:test-room',
|
||||||
|
content: PHANTOM_CONTINUE_NUDGE,
|
||||||
|
is_from_me: false,
|
||||||
|
is_bot_message: false,
|
||||||
|
message_source_kind: 'ipc_injected_human',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
expect(enqueueMessageCheck).toHaveBeenCalledWith(
|
||||||
|
'dc:test-room',
|
||||||
|
'test-room',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does nothing when the outcome is not auto-continue', () => {
|
||||||
|
const storeMessage = vi.fn();
|
||||||
|
const enqueueMessageCheck = vi.fn();
|
||||||
|
|
||||||
|
const scheduled = scheduleAutoContinueForPhantomFinal({
|
||||||
|
outcomeKind: 'notice',
|
||||||
|
delivered: true,
|
||||||
|
chatJid: 'dc:test-room',
|
||||||
|
groupFolder: 'test-room',
|
||||||
|
runId: 'run-xyz',
|
||||||
|
storeMessage,
|
||||||
|
enqueueMessageCheck,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(scheduled).toBe(false);
|
||||||
|
expect(storeMessage).not.toHaveBeenCalled();
|
||||||
|
expect(enqueueMessageCheck).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does nothing when the final was not delivered', () => {
|
||||||
|
const storeMessage = vi.fn();
|
||||||
|
const enqueueMessageCheck = vi.fn();
|
||||||
|
|
||||||
|
const scheduled = scheduleAutoContinueForPhantomFinal({
|
||||||
|
outcomeKind: 'auto-continue',
|
||||||
|
delivered: false,
|
||||||
|
chatJid: 'dc:test-room',
|
||||||
|
groupFolder: 'test-room',
|
||||||
|
runId: 'run-xyz',
|
||||||
|
storeMessage,
|
||||||
|
enqueueMessageCheck,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(scheduled).toBe(false);
|
||||||
|
expect(storeMessage).not.toHaveBeenCalled();
|
||||||
|
expect(enqueueMessageCheck).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
149
src/phantom-notification-guard.ts
Normal file
149
src/phantom-notification-guard.ts
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
import type { NewMessage } from './types.js';
|
||||||
|
|
||||||
|
// Guard against "phantom notification" dead-end finals.
|
||||||
|
//
|
||||||
|
// Agents sometimes end a turn with a sentence like "I'll wait for the build to
|
||||||
|
// complete — I'll be notified", expecting a background-completion callback that
|
||||||
|
// this environment never delivers. The turn then simply ends and the user is
|
||||||
|
// left staring at that message forever (no watcher was registered, so nothing
|
||||||
|
// ever resumes). The claude-platform prompt already forbids this, but a prompt
|
||||||
|
// rule cannot reliably stop the model, so we also enforce it at delivery time:
|
||||||
|
// when a final looks like such a dead-end and no watcher is actually active, we
|
||||||
|
// append an explicit notice telling the user no notification is coming and to
|
||||||
|
// send a follow-up — turning a silent hang into an actionable prompt.
|
||||||
|
|
||||||
|
const PHANTOM_NOTIFICATION_PATTERNS: readonly RegExp[] = [
|
||||||
|
// First-person promise of a callback that never arrives.
|
||||||
|
/\bi['’]?\s*ll\s+be\s+notified\b/i,
|
||||||
|
/\bi\s+will\s+be\s+notified\b/i,
|
||||||
|
/\bi['’]?\s*m\s+(?:going to|gonna)\s+(?:wait|be notified)\b/i,
|
||||||
|
// "wait for the build/compile/deploy to complete/finish"
|
||||||
|
/\bwait(?:ing)?\s+for\s+the\s+[\w\s-]{0,40}?\bto\s+(?:complete|finish|be\s+done)\b/i,
|
||||||
|
// "notified when it finishes/completes/is done"
|
||||||
|
/\bnotified\s+when\s+[\w\s-]{0,40}?(?:finishes|completes|is\s+done)\b/i,
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True when `text` reads like a background-completion "I'll be notified"
|
||||||
|
* dead-end. Conservative: matches the distinctive first-person phrasing rather
|
||||||
|
* than any mention of waiting, to avoid annotating legitimate status reports.
|
||||||
|
*/
|
||||||
|
export function isPhantomNotificationDeadEnd(text: string): boolean {
|
||||||
|
if (!text) return false;
|
||||||
|
return PHANTOM_NOTIFICATION_PATTERNS.some((pattern) => pattern.test(text));
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PHANTOM_NOTIFICATION_NOTICE =
|
||||||
|
'\n\n[자동 안내] 이 환경에는 백그라운드 완료 알림이 없어서, 위처럼 "기다렸다가 알림을 받겠다"고 해도 실제로는 아무것도 다시 시작되지 않습니다. 감시 작업(watch_ci 등)도 등록되지 않았습니다. 계속 진행하려면 이 방에 메시지를 한 번 더 보내 주세요.';
|
||||||
|
|
||||||
|
const NOTICE_MARKER = '[자동 안내]';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns `text` unchanged unless it is a phantom-notification dead-end AND no
|
||||||
|
* watcher is active for the chat, in which case an explicit follow-up notice is
|
||||||
|
* appended. Idempotent: never appends the notice twice. When a watcher IS
|
||||||
|
* active the "I'll be notified" claim is legitimate, so the text is untouched.
|
||||||
|
*/
|
||||||
|
export function applyPhantomNotificationGuard(
|
||||||
|
text: string,
|
||||||
|
opts: { hasActiveWatcher: boolean },
|
||||||
|
): string {
|
||||||
|
if (opts.hasActiveWatcher) return text;
|
||||||
|
if (!isPhantomNotificationDeadEnd(text)) return text;
|
||||||
|
if (text.includes(NOTICE_MARKER)) return text;
|
||||||
|
return `${text}${PHANTOM_NOTIFICATION_NOTICE}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Auto-continue after a phantom dead-end ────────────────────────────────
|
||||||
|
//
|
||||||
|
// When a non-paired turn dead-ends on a phantom "I'll be notified" final with no
|
||||||
|
// watcher, instead of just leaving a notice we re-invoke the agent once so it
|
||||||
|
// actually finishes the work in the foreground. A hard per-chat cap makes this
|
||||||
|
// safe: at most MAX_PHANTOM_AUTO_CONTINUES re-runs per phantom streak, so a model
|
||||||
|
// that keeps producing the dead-end can never spin the bot in a loop. The streak
|
||||||
|
// resets as soon as any clean (non-phantom) final is produced.
|
||||||
|
|
||||||
|
const MAX_PHANTOM_AUTO_CONTINUES = 1;
|
||||||
|
const phantomAutoContinueStreak = new Map<string, number>();
|
||||||
|
|
||||||
|
/** Corrective nudge injected (as an inbound instruction) before the re-run. */
|
||||||
|
export const PHANTOM_CONTINUE_NUDGE =
|
||||||
|
'[시스템 자동 안내] 방금 실제 감시 작업(watch_ci 등)을 등록하지 않은 채 "기다렸다가 알림을 받겠다"는 식으로 응답을 끝냈습니다. 이 환경에는 백그라운드 완료 알림이 없어, 그대로 두면 아무것도 다시 시작되지 않습니다. 지금 바로 포그라운드에서 작업을 끝까지 진행해 실제 결과를 보고하세요. 정말 오래 걸리는 작업이면 watch_ci/schedule_task로 감시를 등록한 뒤 그 사실을 알려주세요. 다시는 "알림을 받겠다"며 끝내지 마세요.';
|
||||||
|
|
||||||
|
export type PhantomFinalOutcome =
|
||||||
|
| { kind: 'clean'; text: string }
|
||||||
|
| { kind: 'auto-continue'; text: string }
|
||||||
|
| { kind: 'notice'; text: string };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decide what to do with a final that may be a phantom dead-end. Pure except for
|
||||||
|
* the per-chat streak counter, which is the loop guard.
|
||||||
|
*
|
||||||
|
* - `clean`: not a dead-end (or a watcher is active) — deliver as-is; resets streak.
|
||||||
|
* - `auto-continue`: dead-end, under the cap — deliver as-is, and the caller must
|
||||||
|
* re-invoke the agent once with PHANTOM_CONTINUE_NUDGE.
|
||||||
|
* - `notice`: dead-end but the cap is exhausted (or auto-continue disabled) —
|
||||||
|
* deliver with the follow-up notice appended so the user isn't left hanging.
|
||||||
|
*/
|
||||||
|
export function resolvePhantomFinalOutcome(args: {
|
||||||
|
chatJid: string;
|
||||||
|
text: string;
|
||||||
|
hasActiveWatcher: boolean;
|
||||||
|
canAutoContinue: boolean;
|
||||||
|
}): PhantomFinalOutcome {
|
||||||
|
const { chatJid, text, hasActiveWatcher, canAutoContinue } = args;
|
||||||
|
if (hasActiveWatcher || !isPhantomNotificationDeadEnd(text)) {
|
||||||
|
phantomAutoContinueStreak.delete(chatJid);
|
||||||
|
return { kind: 'clean', text };
|
||||||
|
}
|
||||||
|
const prior = phantomAutoContinueStreak.get(chatJid) ?? 0;
|
||||||
|
if (canAutoContinue && prior < MAX_PHANTOM_AUTO_CONTINUES) {
|
||||||
|
phantomAutoContinueStreak.set(chatJid, prior + 1);
|
||||||
|
return { kind: 'auto-continue', text };
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
kind: 'notice',
|
||||||
|
text: applyPhantomNotificationGuard(text, { hasActiveWatcher: false }),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Test-only: clear the per-chat auto-continue streak counters. */
|
||||||
|
export function _resetPhantomAutoContinueForTests(): void {
|
||||||
|
phantomAutoContinueStreak.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform the auto-continue side effect after a final was delivered: inject the
|
||||||
|
* corrective nudge as an inbound instruction and re-enqueue a turn so the agent
|
||||||
|
* runs again and finishes the work. No-op unless the outcome was `auto-continue`
|
||||||
|
* AND the final actually delivered. Returns true when it scheduled a re-run.
|
||||||
|
*
|
||||||
|
* Extracted from the delivery callback so the storeMessage → enqueueMessageCheck
|
||||||
|
* path is unit-testable without standing up a full turn.
|
||||||
|
*/
|
||||||
|
export function scheduleAutoContinueForPhantomFinal(args: {
|
||||||
|
outcomeKind: PhantomFinalOutcome['kind'];
|
||||||
|
delivered: boolean;
|
||||||
|
chatJid: string;
|
||||||
|
groupFolder: string;
|
||||||
|
runId: string;
|
||||||
|
storeMessage: (message: NewMessage) => void;
|
||||||
|
enqueueMessageCheck: (chatJid: string, groupFolder: string) => void;
|
||||||
|
}): boolean {
|
||||||
|
if (args.outcomeKind !== 'auto-continue' || !args.delivered) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
args.storeMessage({
|
||||||
|
id: `phantom-continue-${args.runId}-${Date.now().toString(36)}`,
|
||||||
|
chat_jid: args.chatJid,
|
||||||
|
sender: 'ejclaw-system',
|
||||||
|
sender_name: 'EJClaw',
|
||||||
|
content: PHANTOM_CONTINUE_NUDGE,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
is_from_me: false,
|
||||||
|
is_bot_message: false,
|
||||||
|
message_source_kind: 'ipc_injected_human',
|
||||||
|
});
|
||||||
|
args.enqueueMessageCheck(args.chatJid, args.groupFolder);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
@@ -42,6 +42,8 @@ const SECRET_PATTERNS: RegExp[] = [
|
|||||||
/glpat-[A-Za-z0-9_-]{20,}/g, // GitLab PAT
|
/glpat-[A-Za-z0-9_-]{20,}/g, // GitLab PAT
|
||||||
/AKIA[A-Z0-9]{16}/g, // AWS Access Key
|
/AKIA[A-Z0-9]{16}/g, // AWS Access Key
|
||||||
/Bearer\s+eyJ[A-Za-z0-9_-]{40,}/g, // Bearer JWT
|
/Bearer\s+eyJ[A-Za-z0-9_-]{40,}/g, // Bearer JWT
|
||||||
|
// Discord bot token: base64 id "." 6-char ts "." 27+ char hmac
|
||||||
|
/\b[MN][A-Za-z0-9_-]{23,}\.[A-Za-z0-9_-]{6}\.[A-Za-z0-9_-]{27,}\b/g,
|
||||||
];
|
];
|
||||||
|
|
||||||
function redactSecrets(text: string): string {
|
function redactSecrets(text: string): string {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import fs from 'node:fs';
|
|||||||
import os from 'node:os';
|
import os from 'node:os';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
|
|
||||||
|
import { getClaudeCredentialsPath } from './claude-credentials-path.js';
|
||||||
import {
|
import {
|
||||||
CURRENT_RUNTIME_AGENT_TYPE,
|
CURRENT_RUNTIME_AGENT_TYPE,
|
||||||
DATA_DIR,
|
DATA_DIR,
|
||||||
@@ -236,7 +237,8 @@ export function getRuntimeInventory(
|
|||||||
pathSnapshot('Claude settings.json', claudeSettingsPath),
|
pathSnapshot('Claude settings.json', claudeSettingsPath),
|
||||||
pathSnapshot(
|
pathSnapshot(
|
||||||
'Claude credentials',
|
'Claude credentials',
|
||||||
path.join(homeDir, '.claude', '.credentials.json'),
|
getClaudeCredentialsPath(0, { allowHomeFallback: true }) ??
|
||||||
|
path.join(homeDir, '.claude', '.credentials.json'),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
skillDirs: [
|
skillDirs: [
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ describe('service-routing global failover', () => {
|
|||||||
expect(getEffectiveChannelLease('dc:paired')).toMatchObject({
|
expect(getEffectiveChannelLease('dc:paired')).toMatchObject({
|
||||||
chat_jid: 'dc:paired',
|
chat_jid: 'dc:paired',
|
||||||
owner_service_id: 'codex-review',
|
owner_service_id: 'codex-review',
|
||||||
reviewer_service_id: 'claude',
|
reviewer_service_id: 'codex-review',
|
||||||
owner_failover_active: true,
|
owner_failover_active: true,
|
||||||
reason: 'claude-429',
|
reason: 'claude-429',
|
||||||
explicit: true,
|
explicit: true,
|
||||||
@@ -115,8 +115,8 @@ describe('service-routing global failover', () => {
|
|||||||
expect(getGlobalFailoverInfo().active).toBe(false);
|
expect(getGlobalFailoverInfo().active).toBe(false);
|
||||||
expect(getEffectiveChannelLease('dc:paired')).toMatchObject({
|
expect(getEffectiveChannelLease('dc:paired')).toMatchObject({
|
||||||
chat_jid: 'dc:paired',
|
chat_jid: 'dc:paired',
|
||||||
owner_service_id: 'codex-main',
|
owner_service_id: 'claude',
|
||||||
reviewer_service_id: 'claude',
|
reviewer_service_id: 'codex-review',
|
||||||
owner_failover_active: false,
|
owner_failover_active: false,
|
||||||
explicit: false,
|
explicit: false,
|
||||||
});
|
});
|
||||||
@@ -141,7 +141,7 @@ describe('service-routing global failover', () => {
|
|||||||
|
|
||||||
expect(getEffectiveChannelLease('dc:explicit-single')).toMatchObject({
|
expect(getEffectiveChannelLease('dc:explicit-single')).toMatchObject({
|
||||||
chat_jid: 'dc:explicit-single',
|
chat_jid: 'dc:explicit-single',
|
||||||
owner_service_id: 'codex-main',
|
owner_service_id: 'claude',
|
||||||
reviewer_service_id: null,
|
reviewer_service_id: null,
|
||||||
owner_failover_active: false,
|
owner_failover_active: false,
|
||||||
explicit: false,
|
explicit: false,
|
||||||
@@ -210,7 +210,7 @@ describe('service-routing global failover', () => {
|
|||||||
expect(getEffectiveChannelLease('dc:explicit-tribunal')).toMatchObject({
|
expect(getEffectiveChannelLease('dc:explicit-tribunal')).toMatchObject({
|
||||||
chat_jid: 'dc:explicit-tribunal',
|
chat_jid: 'dc:explicit-tribunal',
|
||||||
owner_service_id: 'claude',
|
owner_service_id: 'claude',
|
||||||
reviewer_service_id: 'claude',
|
reviewer_service_id: 'codex-review',
|
||||||
owner_failover_active: false,
|
owner_failover_active: false,
|
||||||
explicit: false,
|
explicit: false,
|
||||||
});
|
});
|
||||||
@@ -231,7 +231,7 @@ describe('service-routing global failover', () => {
|
|||||||
).toMatchObject({
|
).toMatchObject({
|
||||||
chat_jid: 'dc:explicit-tribunal-codex',
|
chat_jid: 'dc:explicit-tribunal-codex',
|
||||||
owner_service_id: 'codex-main',
|
owner_service_id: 'codex-main',
|
||||||
reviewer_service_id: 'claude',
|
reviewer_service_id: 'codex-review',
|
||||||
owner_failover_active: false,
|
owner_failover_active: false,
|
||||||
explicit: false,
|
explicit: false,
|
||||||
});
|
});
|
||||||
@@ -263,7 +263,7 @@ describe('service-routing global failover', () => {
|
|||||||
it('defaults to the configured owner service for chats without canonical room settings', () => {
|
it('defaults to the configured owner service for chats without canonical room settings', () => {
|
||||||
expect(getEffectiveChannelLease('dc:unregistered')).toMatchObject({
|
expect(getEffectiveChannelLease('dc:unregistered')).toMatchObject({
|
||||||
chat_jid: 'dc:unregistered',
|
chat_jid: 'dc:unregistered',
|
||||||
owner_service_id: 'codex-main',
|
owner_service_id: 'claude',
|
||||||
reviewer_service_id: null,
|
reviewer_service_id: null,
|
||||||
owner_failover_active: false,
|
owner_failover_active: false,
|
||||||
explicit: false,
|
explicit: false,
|
||||||
@@ -289,7 +289,7 @@ describe('service-routing global failover', () => {
|
|||||||
|
|
||||||
expect(getEffectiveChannelLease('dc:legacy-only')).toMatchObject({
|
expect(getEffectiveChannelLease('dc:legacy-only')).toMatchObject({
|
||||||
chat_jid: 'dc:legacy-only',
|
chat_jid: 'dc:legacy-only',
|
||||||
owner_service_id: 'codex-main',
|
owner_service_id: 'claude',
|
||||||
reviewer_service_id: null,
|
reviewer_service_id: null,
|
||||||
owner_failover_active: false,
|
owner_failover_active: false,
|
||||||
explicit: false,
|
explicit: false,
|
||||||
@@ -344,6 +344,8 @@ describe('stored lease ids as SSOT', () => {
|
|||||||
activated_at: '2026-04-09T00:00:00.000Z',
|
activated_at: '2026-04-09T00:00:00.000Z',
|
||||||
reason: 'ssot-test',
|
reason: 'ssot-test',
|
||||||
});
|
});
|
||||||
|
// A stored reviewer lease only applies while the room is paired (tribunal).
|
||||||
|
setExplicitRoomMode('dc:stored-reviewer-ssot', 'tribunal');
|
||||||
refreshChannelOwnerCache(true);
|
refreshChannelOwnerCache(true);
|
||||||
|
|
||||||
const lease = getEffectiveChannelLease('dc:stored-reviewer-ssot');
|
const lease = getEffectiveChannelLease('dc:stored-reviewer-ssot');
|
||||||
@@ -359,3 +361,53 @@ describe('stored lease ids as SSOT', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('single-mode rooms never carry a reviewer lease', () => {
|
||||||
|
it('suppresses a stale stored reviewer lease when the room is single, keeping the owner', () => {
|
||||||
|
// Reproduces the incident: a room was tribunal, got a stored reviewer
|
||||||
|
// lease, then was switched to single. The stale reviewer must not route
|
||||||
|
// single-mode messages into the paired path.
|
||||||
|
setChannelOwnerLease({
|
||||||
|
chat_jid: 'dc:stale-single',
|
||||||
|
owner_service_id: 'codex-main',
|
||||||
|
reviewer_service_id: 'codex-review',
|
||||||
|
owner_agent_type: 'codex',
|
||||||
|
reviewer_agent_type: 'codex',
|
||||||
|
activated_at: '2026-05-27T00:00:00.000Z',
|
||||||
|
reason: 'was-tribunal',
|
||||||
|
});
|
||||||
|
setExplicitRoomMode('dc:stale-single', 'single');
|
||||||
|
refreshChannelOwnerCache(true);
|
||||||
|
|
||||||
|
expect(getEffectiveChannelLease('dc:stale-single')).toMatchObject({
|
||||||
|
chat_jid: 'dc:stale-single',
|
||||||
|
owner_service_id: 'codex-main',
|
||||||
|
reviewer_service_id: null,
|
||||||
|
arbiter_service_id: null,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('restores the stored reviewer lease when the room is switched back to tribunal', () => {
|
||||||
|
setChannelOwnerLease({
|
||||||
|
chat_jid: 'dc:toggle-mode',
|
||||||
|
owner_service_id: 'codex-main',
|
||||||
|
reviewer_service_id: 'codex-review',
|
||||||
|
owner_agent_type: 'codex',
|
||||||
|
reviewer_agent_type: 'codex',
|
||||||
|
activated_at: '2026-05-27T00:00:00.000Z',
|
||||||
|
reason: 'toggle',
|
||||||
|
});
|
||||||
|
setExplicitRoomMode('dc:toggle-mode', 'single');
|
||||||
|
refreshChannelOwnerCache(true);
|
||||||
|
expect(
|
||||||
|
getEffectiveChannelLease('dc:toggle-mode').reviewer_service_id,
|
||||||
|
).toBeNull();
|
||||||
|
|
||||||
|
setExplicitRoomMode('dc:toggle-mode', 'tribunal');
|
||||||
|
refreshChannelOwnerCache(true);
|
||||||
|
expect(getEffectiveChannelLease('dc:toggle-mode')).toMatchObject({
|
||||||
|
owner_service_id: 'codex-main',
|
||||||
|
reviewer_service_id: 'codex-review',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -146,13 +146,36 @@ function getDefaultLease(chatJid: string): EffectiveChannelLease {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function enforceRoomModeOnLease(
|
||||||
|
chatJid: string,
|
||||||
|
lease: EffectiveChannelLease,
|
||||||
|
): EffectiveChannelLease {
|
||||||
|
// Invariant: a single-mode room never runs a reviewer/arbiter, even if a
|
||||||
|
// stored lease still carries them from when the room was tribunal. Without
|
||||||
|
// this, a stale reviewer lease keeps routing single-mode messages into the
|
||||||
|
// paired path, where they stall forever on a stuck/mismatched execution
|
||||||
|
// lease (the "task revision was already claimed elsewhere" hang). The stored
|
||||||
|
// row is left untouched, so switching the room back to tribunal restores it.
|
||||||
|
if (
|
||||||
|
(lease.reviewer_service_id == null && lease.arbiter_service_id == null) ||
|
||||||
|
getEffectiveRuntimeRoomMode(chatJid) !== 'single'
|
||||||
|
) {
|
||||||
|
return lease;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...lease,
|
||||||
|
reviewer_agent_type: null,
|
||||||
|
arbiter_agent_type: null,
|
||||||
|
reviewer_service_id: null,
|
||||||
|
arbiter_service_id: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function getStoredOrDefaultLease(chatJid: string): EffectiveChannelLease {
|
function getStoredOrDefaultLease(chatJid: string): EffectiveChannelLease {
|
||||||
refreshChannelOwnerCache();
|
refreshChannelOwnerCache();
|
||||||
const row = leaseCache.get(chatJid);
|
const row = leaseCache.get(chatJid);
|
||||||
if (row) {
|
const lease = row ? normalizeLeaseRow(row, true) : getDefaultLease(chatJid);
|
||||||
return normalizeLeaseRow(row, true);
|
return enforceRoomModeOnLease(chatJid, lease);
|
||||||
}
|
|
||||||
return getDefaultLease(chatJid);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function refreshChannelOwnerCache(force = false): void {
|
export function refreshChannelOwnerCache(force = false): void {
|
||||||
|
|||||||
@@ -60,6 +60,26 @@ describe('shouldResetSessionOnAgentFailure', () => {
|
|||||||
}),
|
}),
|
||||||
).toBe(true);
|
).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('resets the session on context-window overflow (prompt too long)', () => {
|
||||||
|
expect(
|
||||||
|
shouldResetSessionOnAgentFailure({
|
||||||
|
result: null,
|
||||||
|
error:
|
||||||
|
'API Error: 400 {"type":"error","error":{"type":"invalid_request_error","message":"prompt is too long: 215000 tokens > 200000 maximum"}}',
|
||||||
|
}),
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('resets the session when max_tokens exceeds the context limit', () => {
|
||||||
|
expect(
|
||||||
|
shouldResetSessionOnAgentFailure({
|
||||||
|
result:
|
||||||
|
'input length and `max_tokens` exceed context limit: 190000 + 20000 > 200000',
|
||||||
|
error: undefined,
|
||||||
|
}),
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('shouldRetryFreshSessionOnAgentFailure', () => {
|
describe('shouldRetryFreshSessionOnAgentFailure', () => {
|
||||||
|
|||||||
@@ -9,6 +9,12 @@ const SESSION_RESET_PATTERNS = [
|
|||||||
/400[^\n]*thinking/i,
|
/400[^\n]*thinking/i,
|
||||||
/thinking[^\n]*block[^\n]*(?:invalid|error|signature)/i,
|
/thinking[^\n]*block[^\n]*(?:invalid|error|signature)/i,
|
||||||
/invalid[^\n]*signature[^\n]*thinking/i,
|
/invalid[^\n]*signature[^\n]*thinking/i,
|
||||||
|
// Context-window overflow: the resumed session has grown past the model's
|
||||||
|
// limit, so even auto-compaction fails with "Prompt is too long". Without
|
||||||
|
// this, the same oversized session is reused every turn and the channel
|
||||||
|
// loops forever on the same error. Drop it so the next turn starts fresh.
|
||||||
|
/prompt is too long/i,
|
||||||
|
/input length and .*exceed context limit/i,
|
||||||
];
|
];
|
||||||
|
|
||||||
const SESSION_RETRY_PATTERNS = [
|
const SESSION_RETRY_PATTERNS = [
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import type { AgentType } from './types.js';
|
|||||||
export interface StatusSnapshotEntry {
|
export interface StatusSnapshotEntry {
|
||||||
jid: string;
|
jid: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
chatName?: string;
|
||||||
folder: string;
|
folder: string;
|
||||||
agentType: AgentType;
|
agentType: AgentType;
|
||||||
status: GroupStatus['status'];
|
status: GroupStatus['status'];
|
||||||
|
|||||||
@@ -8,11 +8,38 @@ import {
|
|||||||
formatStatusHeader,
|
formatStatusHeader,
|
||||||
msUntilNextMinuteBoundary,
|
msUntilNextMinuteBoundary,
|
||||||
getDashboardDuplicateCleanupIntervalMs,
|
getDashboardDuplicateCleanupIntervalMs,
|
||||||
|
readCpuUtilizationPct,
|
||||||
renderUsageTable,
|
renderUsageTable,
|
||||||
|
resetCpuUtilizationSample,
|
||||||
shouldPurgeDashboardChannelOnStart,
|
shouldPurgeDashboardChannelOnStart,
|
||||||
summarizeWatcherTasks,
|
summarizeWatcherTasks,
|
||||||
} from './unified-dashboard.js';
|
} from './unified-dashboard.js';
|
||||||
|
|
||||||
|
describe('readCpuUtilizationPct', () => {
|
||||||
|
it('computes busy percentage from consecutive /proc/stat samples', () => {
|
||||||
|
resetCpuUtilizationSample();
|
||||||
|
expect(
|
||||||
|
readCpuUtilizationPct(() => 'cpu 100 0 100 800 0 0 0 0 0 0\n'),
|
||||||
|
).toBeNull();
|
||||||
|
// +100 total ticks: +30 busy, +70 idle => 30%.
|
||||||
|
expect(
|
||||||
|
readCpuUtilizationPct(() => 'cpu 115 0 115 870 0 0 0 0 0 0\n'),
|
||||||
|
).toBe(30);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('counts iowait as idle and rejects malformed samples', () => {
|
||||||
|
resetCpuUtilizationSample();
|
||||||
|
expect(readCpuUtilizationPct(() => 'not-a-cpu-line\n')).toBeNull();
|
||||||
|
expect(
|
||||||
|
readCpuUtilizationPct(() => 'cpu 100 0 100 700 100 0 0 0 0 0\n'),
|
||||||
|
).toBeNull();
|
||||||
|
// +100 total, all +100 is iowait: CPU busy must be 0%, not 100%.
|
||||||
|
expect(
|
||||||
|
readCpuUtilizationPct(() => 'cpu 100 0 100 700 200 0 0 0 0 0\n'),
|
||||||
|
).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('summarizeWatcherTasks', () => {
|
describe('summarizeWatcherTasks', () => {
|
||||||
it('counts active and paused watcher tasks only', () => {
|
it('counts active and paused watcher tasks only', () => {
|
||||||
const summary = summarizeWatcherTasks([
|
const summary = summarizeWatcherTasks([
|
||||||
|
|||||||
@@ -488,6 +488,47 @@ function buildStatusContent(): string {
|
|||||||
return `${header}\n\n${sections}`;
|
return `${header}\n\n${sections}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Previous /proc/stat sample for CPU utilization deltas. */
|
||||||
|
let lastCpuSample: { idle: number; total: number } | null = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Real CPU utilization percent from /proc/stat deltas between calls.
|
||||||
|
* First call (no previous sample) and non-Linux hosts return null.
|
||||||
|
* Exported for testing alongside resetCpuUtilizationSample.
|
||||||
|
*/
|
||||||
|
export function readCpuUtilizationPct(
|
||||||
|
readStat: () => string = () => fs.readFileSync('/proc/stat', 'utf-8'),
|
||||||
|
): number | null {
|
||||||
|
try {
|
||||||
|
const cpuLine = readStat()
|
||||||
|
.split('\n')
|
||||||
|
.find((line) => line.startsWith('cpu '));
|
||||||
|
if (!cpuLine) return null;
|
||||||
|
const fields = cpuLine.trim().split(/\s+/).slice(1).map(Number);
|
||||||
|
if (fields.length < 5 || fields.some((n) => !Number.isFinite(n))) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// user nice system idle iowait irq softirq steal ...
|
||||||
|
const idle = fields[3] + (fields[4] ?? 0); // idle + iowait
|
||||||
|
const total = fields.reduce((a, b) => a + b, 0);
|
||||||
|
const prev = lastCpuSample;
|
||||||
|
lastCpuSample = { idle, total };
|
||||||
|
if (!prev || total <= prev.total) return null;
|
||||||
|
const totalDelta = total - prev.total;
|
||||||
|
const idleDelta = idle - prev.idle;
|
||||||
|
return Math.round(
|
||||||
|
Math.min(100, Math.max(0, ((totalDelta - idleDelta) / totalDelta) * 100)),
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Test-only: reset the CPU sample so the next read starts fresh. */
|
||||||
|
export function resetCpuUtilizationSample(): void {
|
||||||
|
lastCpuSample = null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Render usage table lines from two row groups (Claude and Codex).
|
* Render usage table lines from two row groups (Claude and Codex).
|
||||||
* Returns rendered lines including code block markers.
|
* Returns rendered lines including code block markers.
|
||||||
@@ -517,6 +558,10 @@ export function renderUsageTable(
|
|||||||
|
|
||||||
const lines: string[] = [];
|
const lines: string[] = [];
|
||||||
|
|
||||||
|
// Missing-window placeholder. Must mirror a real cell's char composition
|
||||||
|
// (5 ambiguous-width bar chars + 4 ASCII): mobile fonts render block/box
|
||||||
|
// chars double-width, so a plain '—' cell breaks column alignment.
|
||||||
|
const emptyCell = '─'.repeat(5) + ' ';
|
||||||
const renderRows = (rows: UsageRow[]) => {
|
const renderRows = (rows: UsageRow[]) => {
|
||||||
for (const row of rows) {
|
for (const row of rows) {
|
||||||
if (row.error) {
|
if (row.error) {
|
||||||
@@ -529,11 +574,11 @@ export function renderUsageTable(
|
|||||||
const h5 =
|
const h5 =
|
||||||
row.h5pct >= 0
|
row.h5pct >= 0
|
||||||
? `${bar(row.h5pct)}${String(row.h5pct).padStart(3)}%`
|
? `${bar(row.h5pct)}${String(row.h5pct).padStart(3)}%`
|
||||||
: ' — ';
|
: emptyCell;
|
||||||
const d7 =
|
const d7 =
|
||||||
row.d7pct >= 0
|
row.d7pct >= 0
|
||||||
? `${bar(row.d7pct)}${String(row.d7pct).padStart(3)}%`
|
? `${bar(row.d7pct)}${String(row.d7pct).padStart(3)}%`
|
||||||
: ' — ';
|
: emptyCell;
|
||||||
lines.push(`${padName(row.name)}${h5} ${d7}`);
|
lines.push(`${padName(row.name)}${h5} ${d7}`);
|
||||||
const r5 = compactReset(row.h5reset);
|
const r5 = compactReset(row.h5reset);
|
||||||
const r7 = compactReset(row.d7reset);
|
const r7 = compactReset(row.d7reset);
|
||||||
@@ -626,7 +671,11 @@ async function buildUsageContent(): Promise<string> {
|
|||||||
|
|
||||||
const loadAvg = os.loadavg();
|
const loadAvg = os.loadavg();
|
||||||
const cpuCount = os.cpus().length;
|
const cpuCount = os.cpus().length;
|
||||||
const cpuPct = Math.round((loadAvg[1] / cpuCount) * 100);
|
// Real CPU utilization from /proc/stat deltas between renders.
|
||||||
|
// Load-average-based percent counts D-state (I/O-wait) processes, so a
|
||||||
|
// writeback storm once rendered as "CPU 3104%" despite idle CPUs.
|
||||||
|
const cpuPct = readCpuUtilizationPct() ?? 0;
|
||||||
|
const loadPerCore = loadAvg[0] / cpuCount;
|
||||||
const totalMem = os.totalmem();
|
const totalMem = os.totalmem();
|
||||||
// os.freemem() includes buffers/cache as "used" — misleading.
|
// os.freemem() includes buffers/cache as "used" — misleading.
|
||||||
// Read MemAvailable from /proc/meminfo for actual available memory.
|
// Read MemAvailable from /proc/meminfo for actual available memory.
|
||||||
@@ -696,6 +745,12 @@ async function buildUsageContent(): Promise<string> {
|
|||||||
|
|
||||||
lines.push('```');
|
lines.push('```');
|
||||||
lines.push(`${'CPU'.padEnd(8)}${bar(cpuPct)} ${String(cpuPct).padStart(3)}%`);
|
lines.push(`${'CPU'.padEnd(8)}${bar(cpuPct)} ${String(cpuPct).padStart(3)}%`);
|
||||||
|
// Raw load average with core count: I/O storms (D-state pileups) show up
|
||||||
|
// here without masquerading as CPU usage. Flag when load exceeds cores.
|
||||||
|
const loadFlag = loadPerCore > 1 ? ' ▲' : '';
|
||||||
|
lines.push(
|
||||||
|
`${'Load'.padEnd(8)}${loadAvg[0] >= 100 ? loadAvg[0].toFixed(0) : loadAvg[0].toFixed(1)}/${cpuCount}cpu${loadFlag}`,
|
||||||
|
);
|
||||||
lines.push(
|
lines.push(
|
||||||
`${'Memory'.padEnd(8)}${bar(memPct)} ${String(memPct).padStart(3)}% ${memUsedGB}/${memTotalGB}GB`,
|
`${'Memory'.padEnd(8)}${bar(memPct)} ${String(memPct).padStart(3)}% ${memUsedGB}/${memTotalGB}GB`,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -151,10 +151,14 @@ function readPackageScripts(repoDir: string): Record<string, string> {
|
|||||||
function detectDirectRuntimeVersion(
|
function detectDirectRuntimeVersion(
|
||||||
command: Pick<VerificationCommandSpec, 'file'>,
|
command: Pick<VerificationCommandSpec, 'file'>,
|
||||||
): string {
|
): string {
|
||||||
|
// The command file may be a bare name ("bun") or a resolved absolute path
|
||||||
|
// ("/home/claude/.bun/bin/bun"); match on the basename so version detection
|
||||||
|
// still fires instead of degrading to "host:<path>".
|
||||||
|
const runtimeName = path.basename(command.file);
|
||||||
try {
|
try {
|
||||||
switch (command.file) {
|
switch (runtimeName) {
|
||||||
case 'bun': {
|
case 'bun': {
|
||||||
const bunVersion = execFileSync('bun', ['--version'], {
|
const bunVersion = execFileSync(command.file, ['--version'], {
|
||||||
encoding: 'utf-8',
|
encoding: 'utf-8',
|
||||||
stdio: ['ignore', 'pipe', 'pipe'],
|
stdio: ['ignore', 'pipe', 'pipe'],
|
||||||
timeout: 5000,
|
timeout: 5000,
|
||||||
|
|||||||
@@ -5,7 +5,10 @@ import path from 'path';
|
|||||||
import { afterEach, describe, expect, it } from 'vitest';
|
import { afterEach, describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
import type { NewMessage, PairedTask, RegisteredGroup } from './types.js';
|
import type { NewMessage, PairedTask, RegisteredGroup } from './types.js';
|
||||||
import { createWebDashboardHandler } from './web-dashboard-server.js';
|
import {
|
||||||
|
assertDashboardAuthPosture,
|
||||||
|
createWebDashboardHandler,
|
||||||
|
} from './web-dashboard-server.js';
|
||||||
|
|
||||||
const tempDirs: string[] = [];
|
const tempDirs: string[] = [];
|
||||||
|
|
||||||
@@ -45,6 +48,36 @@ afterEach(() => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('assertDashboardAuthPosture', () => {
|
||||||
|
it('allows loopback binding without a token', () => {
|
||||||
|
expect(() => assertDashboardAuthPosture('127.0.0.1', '')).not.toThrow();
|
||||||
|
expect(() => assertDashboardAuthPosture('localhost', '')).not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('allows any host when a token is set', () => {
|
||||||
|
expect(() => assertDashboardAuthPosture('0.0.0.0', 'secret')).not.toThrow();
|
||||||
|
expect(() =>
|
||||||
|
assertDashboardAuthPosture('203.0.113.5', 'secret'),
|
||||||
|
).not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('warns but starts on a private / tailnet host without a token', () => {
|
||||||
|
expect(() =>
|
||||||
|
assertDashboardAuthPosture('100.101.210.95', ''),
|
||||||
|
).not.toThrow();
|
||||||
|
expect(() => assertDashboardAuthPosture('192.168.1.10', '')).not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('refuses a public / all-interface bind without a token', () => {
|
||||||
|
expect(() => assertDashboardAuthPosture('0.0.0.0', '')).toThrow(
|
||||||
|
/WEB_DASHBOARD_TOKEN/,
|
||||||
|
);
|
||||||
|
expect(() => assertDashboardAuthPosture('203.0.113.5', '')).toThrow(
|
||||||
|
/WEB_DASHBOARD_TOKEN/,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('web dashboard server handler', () => {
|
describe('web dashboard server handler', () => {
|
||||||
it('serves health and overview JSON without requiring Discord', async () => {
|
it('serves health and overview JSON without requiring Discord', async () => {
|
||||||
const handler = createWebDashboardHandler({
|
const handler = createWebDashboardHandler({
|
||||||
|
|||||||
@@ -504,6 +504,54 @@ export function createWebDashboardHandler(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isLoopbackHost(host: string): boolean {
|
||||||
|
const h = host.replace(/^\[|\]$/g, '').toLowerCase();
|
||||||
|
return h === 'localhost' || h === '::1' || /^127\./.test(h);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPrivateBindHost(host: string): boolean {
|
||||||
|
const h = host.replace(/^\[|\]$/g, '').toLowerCase();
|
||||||
|
if (h.startsWith('fe80:') || h.startsWith('fc') || h.startsWith('fd')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const ipv4 = h.match(/^(\d{1,3})\.(\d{1,3})\./);
|
||||||
|
if (!ipv4) return false;
|
||||||
|
const [a, b] = [Number(ipv4[1]), Number(ipv4[2])];
|
||||||
|
if (a === 10) return true;
|
||||||
|
if (a === 169 && b === 254) return true;
|
||||||
|
if (a === 172 && b >= 16 && b <= 31) return true;
|
||||||
|
if (a === 192 && b === 168) return true;
|
||||||
|
if (a === 100 && b >= 64 && b <= 127) return true; // CGNAT / tailnet
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refuse to expose an unauthenticated dashboard on a public interface.
|
||||||
|
*
|
||||||
|
* With no token the API auth gate passes for everyone (see
|
||||||
|
* isDashboardApiAuthorized). Binding that to a public interface / all
|
||||||
|
* interfaces (0.0.0.0) would let any reachable client drive agents and restart
|
||||||
|
* the service, so we fail fast. Loopback is fine; private / tailnet binds warn
|
||||||
|
* loudly but still start so existing deployments keep running.
|
||||||
|
*/
|
||||||
|
export function assertDashboardAuthPosture(host: string, token: string): void {
|
||||||
|
if (token) return;
|
||||||
|
if (isLoopbackHost(host)) return;
|
||||||
|
if (isPrivateBindHost(host)) {
|
||||||
|
logger.warn(
|
||||||
|
{ host },
|
||||||
|
'Web dashboard is bound to a non-loopback host without WEB_DASHBOARD_TOKEN; ' +
|
||||||
|
'anyone who can reach this host can drive agents and restart the service. ' +
|
||||||
|
'Set WEB_DASHBOARD_TOKEN to require authentication.',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw new Error(
|
||||||
|
`Refusing to start web dashboard on public host "${host}" without WEB_DASHBOARD_TOKEN. ` +
|
||||||
|
'Set WEB_DASHBOARD_TOKEN or bind to 127.0.0.1.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function startWebDashboardServer(
|
export function startWebDashboardServer(
|
||||||
opts: {
|
opts: {
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
@@ -521,6 +569,7 @@ export function startWebDashboardServer(
|
|||||||
const host = opts.host ?? WEB_DASHBOARD.host;
|
const host = opts.host ?? WEB_DASHBOARD.host;
|
||||||
const port = opts.port ?? WEB_DASHBOARD.port;
|
const port = opts.port ?? WEB_DASHBOARD.port;
|
||||||
const staticDir = opts.staticDir ?? WEB_DASHBOARD.staticDir;
|
const staticDir = opts.staticDir ?? WEB_DASHBOARD.staticDir;
|
||||||
|
assertDashboardAuthPosture(host, WEB_DASHBOARD.token);
|
||||||
const server = Bun.serve({
|
const server = Bun.serve({
|
||||||
hostname: host,
|
hostname: host,
|
||||||
port,
|
port,
|
||||||
|
|||||||
Reference in New Issue
Block a user