From 02c5b359368542ec97d97b63cffcaa0301530fad Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Thu, 11 Jun 2026 18:36:24 -0700 Subject: [PATCH 01/11] Bump openclaw to 2026.6.5. Latest npm stable; Fable 5 support lands in the 2026.6.6 train once its publish is fixed. Co-Authored-By: Claude Fable 5 --- package-lock.json | 1878 +++------------------------------------------ package.json | 2 +- 2 files changed, 123 insertions(+), 1757 deletions(-) diff --git a/package-lock.json b/package-lock.json index ea0addca..8ad7c759 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "dependencies": { "express": "^4.21.0", "http-proxy": "^1.18.1", - "openclaw": "2026.5.28", + "openclaw": "2026.6.5", "ws": "^8.19.0" }, "bin": { @@ -108,16 +108,6 @@ "node": ">=18" } }, - "node_modules/@borewit/text-codec": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/@borewit/text-codec/-/text-codec-0.2.2.tgz", - "integrity": "sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.12", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", @@ -560,42 +550,6 @@ "node": ">=18" } }, - "node_modules/@grammyjs/runner": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@grammyjs/runner/-/runner-2.0.3.tgz", - "integrity": "sha512-nckmTs1dPWfVQteK9cxqxzE+0m1VRvluLWB8UgFzsjg62w3qthPJt0TYtJBEdG7OedvfQq4vnFAyE6iaMkR42A==", - "license": "MIT", - "dependencies": { - "abort-controller": "^3.0.0" - }, - "engines": { - "node": ">=12.20.0 || >=14.13.1" - }, - "peerDependencies": { - "grammy": "^1.13.1" - } - }, - "node_modules/@grammyjs/transformer-throttler": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@grammyjs/transformer-throttler/-/transformer-throttler-1.2.1.tgz", - "integrity": "sha512-CpWB0F3rJdUiKsq7826QhQsxbZi4wqfz1ccKX+fr+AOC+o8K7ZvS+wqX0suSu1QCsyUq2MDpNiKhyL2ZOJUS4w==", - "license": "MIT", - "dependencies": { - "bottleneck": "^2.0.0" - }, - "engines": { - "node": "^12.20.0 || >=14.13.1" - }, - "peerDependencies": { - "grammy": "^1.0.0" - } - }, - "node_modules/@grammyjs/types": { - "version": "3.26.0", - "resolved": "https://registry.npmjs.org/@grammyjs/types/-/types-3.26.0.tgz", - "integrity": "sha512-jlnyfxfev/2o68HlvAGRocAXgdPPX5QabG7jZlbqC2r9DZyWBfzTlg+nu3O3Fy4EhgLWu28hZ/8wr7DsNamP9A==", - "license": "MIT" - }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -642,107 +596,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@lydell/node-pty": { - "version": "1.2.0-beta.12", - "resolved": "https://registry.npmjs.org/@lydell/node-pty/-/node-pty-1.2.0-beta.12.tgz", - "integrity": "sha512-qIK890UwPupoj07osVvgOIa++1mxeHbcGry4PKRHhNVNs81V2SCG34eJr46GybiOmBtc8Sj5PB1/GGM5PL549g==", - "license": "MIT", - "optionalDependencies": { - "@lydell/node-pty-darwin-arm64": "1.2.0-beta.12", - "@lydell/node-pty-darwin-x64": "1.2.0-beta.12", - "@lydell/node-pty-linux-arm64": "1.2.0-beta.12", - "@lydell/node-pty-linux-x64": "1.2.0-beta.12", - "@lydell/node-pty-win32-arm64": "1.2.0-beta.12", - "@lydell/node-pty-win32-x64": "1.2.0-beta.12" - } - }, - "node_modules/@lydell/node-pty-darwin-arm64": { - "version": "1.2.0-beta.12", - "resolved": "https://registry.npmjs.org/@lydell/node-pty-darwin-arm64/-/node-pty-darwin-arm64-1.2.0-beta.12.tgz", - "integrity": "sha512-tqaifcY9Cr41SblO1+FLzh8oxxtkNhuW9Dhl22lKme9BreYvKvxEZcdPIXTuqkJc5tagOEC4QHShKmJjLyLXLQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@lydell/node-pty-darwin-x64": { - "version": "1.2.0-beta.12", - "resolved": "https://registry.npmjs.org/@lydell/node-pty-darwin-x64/-/node-pty-darwin-x64-1.2.0-beta.12.tgz", - "integrity": "sha512-4LrS5pCJwqHKDVf1zS2gyNV0m4hKAXch+XZNhbZ6LY8uwVL8BhchzQBO40Os5anuRxRCWzHpw4Sp64Ie8q7E4Q==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@lydell/node-pty-linux-arm64": { - "version": "1.2.0-beta.12", - "resolved": "https://registry.npmjs.org/@lydell/node-pty-linux-arm64/-/node-pty-linux-arm64-1.2.0-beta.12.tgz", - "integrity": "sha512-Sx+A71x5BDGHt9ansfrtGxwq2VFVDWvJUAdlUL0Hv0qeiJUfts+hgopx+CgT4PSwahKjdEgtu0+FAfY9rICKRw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@lydell/node-pty-linux-x64": { - "version": "1.2.0-beta.12", - "resolved": "https://registry.npmjs.org/@lydell/node-pty-linux-x64/-/node-pty-linux-x64-1.2.0-beta.12.tgz", - "integrity": "sha512-bJzs94njofYhGg/UDqW1nj0dtvvu+2OvxMY+RlLS1T17VgcktKoIR6PuenTwE5HJ/D6StCPADmXcT0nNsCKmIQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@lydell/node-pty-win32-arm64": { - "version": "1.2.0-beta.12", - "resolved": "https://registry.npmjs.org/@lydell/node-pty-win32-arm64/-/node-pty-win32-arm64-1.2.0-beta.12.tgz", - "integrity": "sha512-p7POgjVEiFaBC3/y+AKuV1FzePCsJ6HmZDv2XK+jBZSfwP8+uBAw181ZiKYN1YuRa/XpmBGaWezcI8hZkbW++g==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@lydell/node-pty-win32-x64": { - "version": "1.2.0-beta.12", - "resolved": "https://registry.npmjs.org/@lydell/node-pty-win32-x64/-/node-pty-win32-x64-1.2.0-beta.12.tgz", - "integrity": "sha512-IDFa00g7qUDGUYgByrUBJtC+mOjYVt/8KYyWivCg5JjGOHbBUACUQZLl0jTWmnr+tld/UyTpX90a2PY6oTVtRw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@mozilla/readability": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@mozilla/readability/-/readability-0.6.0.tgz", - "integrity": "sha512-juG5VWh4qAivzTAeMzvY9xs9HY5rAcr2E4I7tiSSCokRFi7XIZCAu92ZkSTsIj1OPceCifL3cpfteP3pDT9/QQ==", - "license": "Apache-2.0", - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/@noble/hashes": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", @@ -1161,52 +1014,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@tokenizer/inflate": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.4.1.tgz", - "integrity": "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.3", - "token-types": "^6.1.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, - "node_modules/@tokenizer/inflate/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/@tokenizer/inflate/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/@tokenizer/token": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", - "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", - "license": "MIT" - }, "node_modules/@types/chai": { "version": "5.2.3", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", @@ -1232,18 +1039,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/node": { - "version": "25.6.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz", - "integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "undici-types": "~7.19.0" - } - }, "node_modules/@vitest/coverage-v8": { "version": "4.0.18", "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.0.18.tgz", @@ -1401,19 +1196,8 @@ "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz", "integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==", "dev": true, - "license": "MIT" - }, - "node_modules/abort-controller": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", - "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", "license": "MIT", - "dependencies": { - "event-target-shim": "^5.0.0" - }, - "engines": { - "node": ">=6.5" - } + "peer": true }, "node_modules/accepts": { "version": "1.3.8", @@ -1428,30 +1212,6 @@ "node": ">= 0.6" } }, - "node_modules/agent-base": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, "node_modules/any-promise": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", @@ -1506,18 +1266,6 @@ "dev": true, "license": "MIT" }, - "node_modules/asn1.js": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", - "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==", - "license": "MIT", - "dependencies": { - "bn.js": "^4.0.0", - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0", - "safer-buffer": "^2.1.0" - } - }, "node_modules/assertion-error": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", @@ -1560,12 +1308,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/bn.js": { - "version": "4.12.3", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz", - "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==", - "license": "MIT" - }, "node_modules/body-parser": { "version": "1.20.4", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", @@ -1590,18 +1332,6 @@ "npm": "1.2.8000 || >= 1.4.16" } }, - "node_modules/boolbase": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", - "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", - "license": "ISC" - }, - "node_modules/bottleneck": { - "version": "2.19.5", - "resolved": "https://registry.npmjs.org/bottleneck/-/bottleneck-2.19.5.tgz", - "integrity": "sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw==", - "license": "MIT" - }, "node_modules/braces": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", @@ -1615,12 +1345,6 @@ "node": ">=8" } }, - "node_modules/buffer-equal-constant-time": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", - "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", - "license": "BSD-3-Clause" - }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -1659,15 +1383,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/camelcase-css": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", @@ -1701,39 +1416,6 @@ "pnpm": ">=8" } }, - "node_modules/chokidar": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", - "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", - "license": "MIT", - "dependencies": { - "readdirp": "^5.0.0" - }, - "engines": { - "node": ">= 20.19.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "license": "MIT" - }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -1800,59 +1482,6 @@ "dev": true, "license": "MIT" }, - "node_modules/core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "license": "MIT" - }, - "node_modules/croner": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/croner/-/croner-10.0.1.tgz", - "integrity": "sha512-ixNtAJndqh173VQ4KodSdJEI6nuioBWI0V1ITNKhZZsO0pEMoDxz539T4FTTbSZ/xIOSuDnzxLVRqBVSvPNE2g==", - "funding": [ - { - "type": "other", - "url": "https://paypal.me/hexagonpp" - }, - { - "type": "github", - "url": "https://github.com/sponsors/hexagon" - } - ], - "license": "MIT", - "engines": { - "node": ">=18.0" - } - }, - "node_modules/css-select": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", - "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", - "license": "BSD-2-Clause", - "dependencies": { - "boolbase": "^1.0.0", - "css-what": "^6.1.0", - "domhandler": "^5.0.2", - "domutils": "^3.0.1", - "nth-check": "^2.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/css-what": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", - "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", - "license": "BSD-2-Clause", - "engines": { - "node": ">= 6" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -1866,12 +1495,6 @@ "node": ">=4" } }, - "node_modules/cssom": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz", - "integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==", - "license": "MIT" - }, "node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -1881,15 +1504,6 @@ "ms": "2.0.0" } }, - "node_modules/decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -1937,12 +1551,6 @@ "dev": true, "license": "Apache-2.0" }, - "node_modules/dijkstrajs": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", - "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==", - "license": "MIT" - }, "node_modules/dlv": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", @@ -1950,73 +1558,6 @@ "dev": true, "license": "MIT" }, - "node_modules/dom-serializer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", - "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", - "license": "MIT", - "dependencies": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.2", - "entities": "^4.2.0" - }, - "funding": { - "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" - } - }, - "node_modules/domelementtype": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", - "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], - "license": "BSD-2-Clause" - }, - "node_modules/domhandler": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", - "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", - "license": "BSD-2-Clause", - "dependencies": { - "domelementtype": "^2.3.0" - }, - "engines": { - "node": ">= 4" - }, - "funding": { - "url": "https://github.com/fb55/domhandler?sponsor=1" - } - }, - "node_modules/domutils": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", - "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", - "license": "BSD-2-Clause", - "dependencies": { - "dom-serializer": "^2.0.0", - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3" - }, - "funding": { - "url": "https://github.com/fb55/domutils?sponsor=1" - } - }, - "node_modules/dotenv": { - "version": "17.4.2", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", - "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -2031,27 +1572,12 @@ "node": ">= 0.4" } }, - "node_modules/ecdsa-sig-formatter": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", - "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", - "license": "Apache-2.0", - "dependencies": { - "safe-buffer": "^5.0.1" - } - }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", "license": "MIT" }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, "node_modules/encodeurl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", @@ -2061,18 +1587,6 @@ "node": ">= 0.8" } }, - "node_modules/entities": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -2168,15 +1682,6 @@ "@esbuild/win32-x64": "0.25.12" } }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -2202,15 +1707,6 @@ "node": ">= 0.6" } }, - "node_modules/event-target-shim": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", - "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/eventemitter3": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", @@ -2338,24 +1834,6 @@ } } }, - "node_modules/file-type": { - "version": "22.0.1", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-22.0.1.tgz", - "integrity": "sha512-ww5Mhre0EE+jmBvOXTmXAbEMuZE7uX4a3+oRCQFNj8w++g3ev913N6tXQz0XTXbueQ5TWQfm6BdaViEHHn8bhA==", - "license": "MIT", - "dependencies": { - "@tokenizer/inflate": "^0.4.1", - "strtok3": "^10.3.5", - "token-types": "^6.1.2", - "uint8array-extras": "^1.5.0" - }, - "engines": { - "node": ">=22" - }, - "funding": { - "url": "https://github.com/sindresorhus/file-type?sponsor=1" - } - }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -2387,19 +1865,6 @@ "node": ">= 0.8" } }, - "node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "license": "MIT", - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/follow-redirects": { "version": "1.16.0", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", @@ -2497,15 +1962,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "license": "ISC", - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -2568,58 +2024,20 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/grammy": { - "version": "1.42.0", - "resolved": "https://registry.npmjs.org/grammy/-/grammy-1.42.0.tgz", - "integrity": "sha512-1AdCge+AkjSdp2FwfICSFnVbl8Mq3KVHJDy+DgTI9+D6keJ0zWALPRKas5jv/8psiCzL4N2cEOcGW7O45Kn39g==", + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, "license": "MIT", - "dependencies": { - "@grammyjs/types": "3.26.0", - "abort-controller": "^3.0.0", - "debug": "^4.4.3", - "node-fetch": "^2.7.0" - }, "engines": { - "node": "^12.20.0 || >=14.13.1" + "node": ">=8" } }, - "node_modules/grammy/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/grammy/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -2670,46 +2088,6 @@ "dev": true, "license": "MIT" }, - "node_modules/htmlparser2": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", - "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==", - "funding": [ - "https://github.com/fb55/htmlparser2?sponsor=1", - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], - "license": "MIT", - "dependencies": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3", - "domutils": "^3.2.2", - "entities": "^7.0.1" - } - }, - "node_modules/htmlparser2/node_modules/entities": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", - "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/http_ece": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/http_ece/-/http_ece-1.2.0.tgz", - "integrity": "sha512-JrF8SSLVmcvc5NducxgyOrKXe3EsyHMgBFgSaIUGmArKe+rwr0uphRkRXvwiom3I+fpIfoItveHrfudL8/rxuA==", - "license": "MIT", - "engines": { - "node": ">=16" - } - }, "node_modules/http-errors": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", @@ -2744,42 +2122,6 @@ "node": ">=8.0.0" } }, - "node_modules/https-proxy-agent": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/https-proxy-agent/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/https-proxy-agent/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -2792,47 +2134,12 @@ "node": ">=0.10.0" } }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "BSD-3-Clause" - }, - "node_modules/immediate": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", - "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", - "license": "MIT" - }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, - "node_modules/ipaddr.js": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.4.0.tgz", - "integrity": "sha512-9VGk3HGanVE6JoZXHiCpnGy5X0jYDnN4EA4lntFPj+1vIWlFhIylq2CrrCOJH9EAhc5CYhq18F2Av2tgoAPsYQ==", - "license": "MIT", - "engines": { - "node": ">= 10" - } - }, "node_modules/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -2872,15 +2179,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -2904,18 +2202,6 @@ "node": ">=0.12.0" } }, - "node_modules/is-promise": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", - "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", - "license": "MIT" - }, - "node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "license": "MIT" - }, "node_modules/istanbul-lib-coverage": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", @@ -2955,15 +2241,6 @@ "node": ">=8" } }, - "node_modules/jiti": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", - "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", - "license": "MIT", - "bin": { - "jiti": "lib/jiti-cli.mjs" - } - }, "node_modules/js-tokens": { "version": "10.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", @@ -2971,90 +2248,6 @@ "dev": true, "license": "MIT" }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/jszip": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", - "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", - "license": "(MIT OR GPL-3.0-or-later)", - "dependencies": { - "lie": "~3.3.0", - "pako": "~1.0.2", - "readable-stream": "~2.3.6", - "setimmediate": "^1.0.5" - } - }, - "node_modules/jszip/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/jszip/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "license": "MIT" - }, - "node_modules/jszip/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/jwa": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", - "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", - "license": "MIT", - "dependencies": { - "buffer-equal-constant-time": "^1.0.1", - "ecdsa-sig-formatter": "1.0.11", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/jws": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", - "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", - "license": "MIT", - "dependencies": { - "jwa": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/lie": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", - "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", - "license": "MIT", - "dependencies": { - "immediate": "~3.0.5" - } - }, "node_modules/lilconfig": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", @@ -3075,48 +2268,6 @@ "dev": true, "license": "MIT" }, - "node_modules/linkedom": { - "version": "0.18.12", - "resolved": "https://registry.npmjs.org/linkedom/-/linkedom-0.18.12.tgz", - "integrity": "sha512-jalJsOwIKuQJSeTvsgzPe9iJzyfVaEJiEXl+25EkKevsULHvMJzpNqwvj1jOESWdmgKDiXObyjOYwlUqG7wo1Q==", - "license": "ISC", - "dependencies": { - "css-select": "^5.1.0", - "cssom": "^0.5.0", - "html-escaper": "^3.0.3", - "htmlparser2": "^10.0.0", - "uhyphen": "^0.2.0" - }, - "engines": { - "node": ">=16" - }, - "peerDependencies": { - "canvas": ">= 2" - }, - "peerDependenciesMeta": { - "canvas": { - "optional": true - } - } - }, - "node_modules/linkedom/node_modules/html-escaper": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz", - "integrity": "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==", - "license": "MIT" - }, - "node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "license": "MIT", - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -3177,15 +2328,6 @@ "node": ">= 0.4" } }, - "node_modules/media-typer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", - "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/merge-descriptors": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", @@ -3267,26 +2409,11 @@ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/minimalistic-assert": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", - "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", - "license": "ISC" - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" } }, "node_modules/mitt": { @@ -3323,122 +2450,6 @@ "node": ">= 0.6" } }, - "node_modules/node-addon-api": { - "version": "8.7.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.7.0.tgz", - "integrity": "sha512-9MdFxmkKaOYVTV+XVRG8ArDwwQ77XIgIPyKASB1k3JPq3M8fGQQQE3YpMOrKm6g//Ktx8ivZr8xo1Qmtqub+GA==", - "license": "MIT", - "engines": { - "node": "^18 || ^20 || >= 21" - } - }, - "node_modules/node-edge-tts": { - "version": "1.2.10", - "resolved": "https://registry.npmjs.org/node-edge-tts/-/node-edge-tts-1.2.10.tgz", - "integrity": "sha512-bV2i4XU54D45+US0Zm1HcJRkifuB3W438dWyuJEHLQdKxnuqlI1kim2MOvR6Q3XUQZvfF9PoDyR1Rt7aeXhPdQ==", - "license": "MIT", - "dependencies": { - "https-proxy-agent": "^7.0.1", - "ws": "^8.13.0", - "yargs": "^17.7.2" - }, - "bin": { - "node-edge-tts": "bin.js" - } - }, - "node_modules/node-edge-tts/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/node-edge-tts/node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/node-edge-tts/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/node-edge-tts/node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "license": "MIT", - "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/node-edge-tts/node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "license": "MIT", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/node-gyp-build": { - "version": "4.8.4", - "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", - "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", - "license": "MIT", - "bin": { - "node-gyp-build": "bin.js", - "node-gyp-build-optional": "optional.js", - "node-gyp-build-test": "build-test.js" - } - }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -3449,18 +2460,6 @@ "node": ">=0.10.0" } }, - "node_modules/nth-check": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", - "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", - "license": "BSD-2-Clause", - "dependencies": { - "boolbase": "^1.0.0" - }, - "funding": { - "url": "https://github.com/fb55/nth-check?sponsor=1" - } - }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -3520,25 +2519,26 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, "license": "ISC", "dependencies": { "wrappy": "1" } }, "node_modules/openclaw": { - "version": "2026.5.28", - "resolved": "https://registry.npmjs.org/openclaw/-/openclaw-2026.5.28.tgz", - "integrity": "sha512-p7jGN9wzCrqEvHNI6Y7+eh6DWoYDzJ1iQGKTm8xqQ2uQ9/2mY1CCf87WoZeb0+m3eHKSGchlI3tN33fE1lMtEA==", + "version": "2026.6.5", + "resolved": "https://registry.npmjs.org/openclaw/-/openclaw-2026.6.5.tgz", + "integrity": "sha512-sRgF0TexfRcJX8Eg0lcL6Jj0YdZbSxUbbp8EbG+qo3v6TtVayE6tKPEs3oCKD7YfYe2C/8Qg26HUxTnycd44ZQ==", "hasInstallScript": true, "hasShrinkwrap": true, "license": "MIT", "dependencies": { "@agentclientprotocol/sdk": "0.22.1", - "@anthropic-ai/sdk": "0.98.0", + "@anthropic-ai/sdk": "0.100.1", "@clack/core": "1.3.1", "@clack/prompts": "1.4.0", - "@earendil-works/pi-tui": "0.76.0", - "@google/genai": "2.6.0", + "@earendil-works/pi-tui": "0.78.0", + "@google/genai": "2.7.0", "@grammyjs/runner": "2.0.3", "@grammyjs/transformer-throttler": "1.2.1", "@homebridge/ciao": "1.3.9", @@ -3548,10 +2548,9 @@ "@mozilla/readability": "0.6.0", "@openclaw/fs-safe": "0.3.0", "@openclaw/proxyline": "0.3.3", - "@silvia-odwyer/photon-node": "0.3.4", "chalk": "5.6.2", "chokidar": "5.0.0", - "clawpdf": "0.2.0", + "clawpdf": "0.3.0", "commander": "14.0.3", "croner": "10.0.1", "cross-spawn": "7.0.6", @@ -3562,28 +2561,26 @@ "glob": "13.0.6", "grammy": "1.43.0", "highlight.js": "11.11.1", - "hosted-git-info": "9.0.3", + "hosted-git-info": "10.1.1", "ignore": "7.0.5", - "ipaddr.js": "2.4.0", "jiti": "2.7.0", "json5": "2.2.3", "jszip": "3.10.1", "kysely": "0.29.2", "linkedom": "0.18.12", - "markdown-it": "14.2.0", "minimatch": "10.2.5", "node-edge-tts": "1.2.10", - "openai": "6.39.0", + "openai": "6.39.1", "partial-json": "0.1.7", "playwright-core": "1.60.0", "proper-lockfile": "4.1.2", "qrcode": "1.5.4", "quickjs-wasi": "3.0.0", - "rastermill": "0.3.0", + "rastermill": "0.3.1", "tar": "7.5.15", "tree-sitter-bash": "0.25.1", "tslog": "4.10.2", - "typebox": "1.1.38", + "typebox": "1.1.39", "typescript": "6.0.3", "undici": "8.3.0", "web-push": "3.6.7", @@ -3612,9 +2609,9 @@ } }, "node_modules/openclaw/node_modules/@anthropic-ai/sdk": { - "version": "0.98.0", - "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.98.0.tgz", - "integrity": "sha512-N7aXtCvC5g6T1Y4V29lJjceu/zTkVkIZF0jdBvagr0TRFHuKeImffalGWEfqZKrvjH+IQbzJWw6TmSmUzrlMgg==", + "version": "0.100.1", + "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.100.1.tgz", + "integrity": "sha512-RANcEe7LpiLczkKGOwoXOTuFdPhuubS0i4xaAKOMpcqc55YO0mukgxppV7eygx3DXNjxWT6RYOLPyOy0aIAmwg==", "license": "MIT", "dependencies": { "json-schema-to-ts": "^3.1.1", @@ -3633,9 +2630,9 @@ } }, "node_modules/openclaw/node_modules/@babel/runtime": { - "version": "7.29.2", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", - "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -3680,9 +2677,9 @@ } }, "node_modules/openclaw/node_modules/@earendil-works/pi-tui": { - "version": "0.76.0", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.76.0.tgz", - "integrity": "sha512-TWQEWqc38gVRYr/VTrlfePQPpJk938gNNLL1xuv0M+9cTkVr880/Q3baZQrxKoLrmN/MmVx7TyR8knYZGxTqqg==", + "version": "0.78.0", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.78.0.tgz", + "integrity": "sha512-3a705FnsVVUhAyceShNB3kS2rpxcxLcx+hqB0u6MMMpHwQGbW+m++MqA6r7eOzq/8FLx5e3vDh38h/SVTk2qzw==", "license": "MIT", "dependencies": { "get-east-asian-width": "1.6.0", @@ -3693,9 +2690,9 @@ } }, "node_modules/openclaw/node_modules/@google/genai": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@google/genai/-/genai-2.6.0.tgz", - "integrity": "sha512-HjoW3mPuEn7pnuKABJl9VbDoWDSF4nbwYKYvYYor7YjPeDxrrBxHzu2d1Prcd+BAuC4w+85UP6y7ZdcrQAoO7g==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@google/genai/-/genai-2.7.0.tgz", + "integrity": "sha512-tv0DRtcndt2oEhBYy+5mA0TaXH98+L1Gt0AP9unBfH7DP20KhB7+O3QqAN1Lz+laMARGTHS7BFQSNpLbl4gm1g==", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { @@ -3899,6 +2896,7 @@ "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz", "integrity": "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==", "license": "MIT", + "peer": true, "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", @@ -4004,9 +3002,9 @@ "license": "MIT" }, "node_modules/openclaw/node_modules/@types/retry": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", - "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.5.tgz", + "integrity": "sha512-3xSjTp3v03X/lSQLkczaN9UIEwJMoMCA1+Nb5HfbJEQWogdeQIyVtTvxPXDQjZ5zws8rFQfVfRdz03ARihPJgw==", "license": "MIT" }, "node_modules/openclaw/node_modules/abort-controller": { @@ -4100,12 +3098,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/openclaw/node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "license": "Python-2.0" - }, "node_modules/openclaw/node_modules/asn1.js": { "version": "5.4.1", "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", @@ -4306,10 +3298,13 @@ } }, "node_modules/openclaw/node_modules/clawpdf": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/clawpdf/-/clawpdf-0.2.0.tgz", - "integrity": "sha512-Za4HD3CMRHNqOXOOyVJiQLnEuezRZR/oXiBzraTwL5XEQZuBwFxnyC1UzN4AjQWV2JrLN3ItbzfPRGE0gGOVwg==", + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/clawpdf/-/clawpdf-0.3.0.tgz", + "integrity": "sha512-41+3AnKk9yek2sm+/9XvUlDTN8Wi+ag7fmxZuqw+ySn4lqaf/fCgLeamqPLiXY4gVbizKEHGoTG/JrIIFNE2rw==", "license": "MIT", + "bin": { + "clawpdf": "dist/cli.js" + }, "engines": { "node": ">=20" } @@ -4686,9 +3681,9 @@ } }, "node_modules/openclaw/node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -4743,9 +3738,9 @@ } }, "node_modules/openclaw/node_modules/eventsource-parser": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.8.tgz", - "integrity": "sha512-70QWGkr4snxr0OXLRWsFLeRBIRPuQOvt4s8QYjmUlmlkyTZkRqS7EDVRZtzU3TiyDbXSzaOeF0XUKy8PchzukQ==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.0.tgz", + "integrity": "sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg==", "license": "MIT", "engines": { "node": ">=18.0.0" @@ -4756,6 +3751,7 @@ "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "license": "MIT", + "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", @@ -5136,6 +4132,7 @@ "resolved": "https://registry.npmjs.org/grammy/-/grammy-1.43.0.tgz", "integrity": "sha512-7dYm06A945mXuIk/5HUlSjeyIYChW8vCEiU2dkOKKqJJzwAWxTkCc91Eqbz7TgODh2rtFFKWI/fekowWHOkmjQ==", "license": "MIT", + "peer": true, "dependencies": { "@grammyjs/types": "3.27.3", "abort-controller": "^3.0.0", @@ -5179,9 +4176,9 @@ } }, "node_modules/openclaw/node_modules/hasown": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", - "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -5204,20 +4201,21 @@ "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.18.tgz", "integrity": "sha512-RWzP96k/yv0PQfyXnWjs6zot20TqfpfsNXhOnev8d1InAxubW93L11/oNUc3tQqn2G0bSdAOBpX+2uDFHV7kdQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=16.9.0" } }, "node_modules/openclaw/node_modules/hosted-git-info": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-9.0.3.tgz", - "integrity": "sha512-Hc+ghLoSt6QaYZUv0WBiIvmMDZuZZ7oaDvdH8MbfOO4lOsxdXLEvuC6ePoGs9H1X9oCLyq6+NVN0MKqD+ydxyg==", + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-10.1.1.tgz", + "integrity": "sha512-DeOnSPAvOndYKfw075gt8yZzQ7S2hNztw34zBTfhIzLhmBTswIBg5/y+pqu/VD5cYWm5goAFTusDmUEmKZ0PEQ==", "license": "ISC", "dependencies": { "lru-cache": "^11.1.0" }, "engines": { - "node": "^20.17.0 || >=22.9.0" + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" } }, "node_modules/openclaw/node_modules/html-escaper": { @@ -5366,12 +4364,12 @@ } }, "node_modules/openclaw/node_modules/ipaddr.js": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.4.0.tgz", - "integrity": "sha512-9VGk3HGanVE6JoZXHiCpnGy5X0jYDnN4EA4lntFPj+1vIWlFhIylq2CrrCOJH9EAhc5CYhq18F2Av2tgoAPsYQ==", + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", "license": "MIT", "engines": { - "node": ">= 10" + "node": ">= 0.10" } }, "node_modules/openclaw/node_modules/is-fullwidth-code-point": { @@ -5540,25 +4538,6 @@ } } }, - "node_modules/openclaw/node_modules/linkify-it": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.1.tgz", - "integrity": "sha512-wVoTjP4Q6R0NW5hiZkVJaFZPWgtXfoGF+6LucL3/FtiNjmcHhYjEr5f1Kqjirc1nBW07J/ZuRFumqr2oqccEWg==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/puzrin" - }, - { - "type": "github", - "url": "https://github.com/sponsors/markdown-it" - } - ], - "license": "MIT", - "dependencies": { - "uc.micro": "^2.0.0" - } - }, "node_modules/openclaw/node_modules/locate-path": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", @@ -5578,41 +4557,14 @@ "license": "Apache-2.0" }, "node_modules/openclaw/node_modules/lru-cache": { - "version": "11.5.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.0.tgz", - "integrity": "sha512-5YgH9UJd7wVb9hIouI2adWpgqrrICkt070Dnj8EUY1+B4B2P9eRLPAkAAo6NICA7CEhOIeBHl46u9zSNpNu7zA==", + "version": "11.5.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", + "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", "license": "BlueOak-1.0.0", "engines": { "node": "20 || >=22" } }, - "node_modules/openclaw/node_modules/markdown-it": { - "version": "14.2.0", - "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.2.0.tgz", - "integrity": "sha512-1TGiQiJVRQ3NPmZH6sx5Cfnmg6GQm9jvC1ch4TK511NjSJvjzKLzn5pPfZRNZkRPZP0HqCioSndqH8v2nRaWVQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/puzrin" - }, - { - "type": "github", - "url": "https://github.com/sponsors/markdown-it" - } - ], - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1", - "entities": "^4.4.0", - "linkify-it": "^5.0.1", - "mdurl": "^2.0.0", - "punycode.js": "^2.3.1", - "uc.micro": "^2.1.0" - }, - "bin": { - "markdown-it": "bin/markdown-it.mjs" - } - }, "node_modules/openclaw/node_modules/marked": { "version": "15.0.12", "resolved": "https://registry.npmjs.org/marked/-/marked-15.0.12.tgz", @@ -5634,12 +4586,6 @@ "node": ">= 0.4" } }, - "node_modules/openclaw/node_modules/mdurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", - "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", - "license": "MIT" - }, "node_modules/openclaw/node_modules/media-typer": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", @@ -5753,9 +4699,9 @@ } }, "node_modules/openclaw/node_modules/node-addon-api": { - "version": "8.7.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.7.0.tgz", - "integrity": "sha512-9MdFxmkKaOYVTV+XVRG8ArDwwQ77XIgIPyKASB1k3JPq3M8fGQQQE3YpMOrKm6g//Ktx8ivZr8xo1Qmtqub+GA==", + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.8.0.tgz", + "integrity": "sha512-c5Ko1fZJIJmzhFIkhRN76WTq+fC6tWnGy9CXA0fA+XygsWZmEwG8vmbkNqxMyoaa0Tin4djul49NzdVcJJcjeA==", "license": "MIT", "engines": { "node": "^18 || ^20 || >= 21" @@ -5869,9 +4815,9 @@ } }, "node_modules/openclaw/node_modules/openai": { - "version": "6.39.0", - "resolved": "https://registry.npmjs.org/openai/-/openai-6.39.0.tgz", - "integrity": "sha512-O61LIsimY3acVabwvomwFhwrnN36yvHY2quIfy9keEcFytGgWeV35yLHQ6NVMLSBxRpHmcg2yuhCnlu2HT4pLQ==", + "version": "6.39.1", + "resolved": "https://registry.npmjs.org/openai/-/openai-6.39.1.tgz", + "integrity": "sha512-z3dO9fEWOXBzlXynVb/xZ/tujzUjFWQWn3C0n0mw6Vo0zJTbEkaN4b2cLWjhJ6haJQx8LlREoafHRl+Gu/Hl+A==", "license": "Apache-2.0", "bin": { "openai": "bin/cli" @@ -6085,24 +5031,6 @@ "node": ">= 0.10" } }, - "node_modules/openclaw/node_modules/proxy-addr/node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/openclaw/node_modules/punycode.js": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", - "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/openclaw/node_modules/qrcode": { "version": "1.5.4", "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz", @@ -6217,15 +5145,15 @@ } }, "node_modules/openclaw/node_modules/rastermill": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/rastermill/-/rastermill-0.3.0.tgz", - "integrity": "sha512-4g2i0I7M5sba//lFBh19Wi0hDGw8o+isnt/BtEyqQXIZaYclhcNBwL/Fw/6gDCp7aaLwQHADuUvyHCB0Oat5Vw==", + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/rastermill/-/rastermill-0.3.1.tgz", + "integrity": "sha512-CX4nij6+ZLHYIaojJNfLTr7W+AiH/IPJi6E9Aw1br2///1KZL2KBOHd68rkcLedc47MPvb4hhH+fzYeGFa4A/Q==", "license": "MIT", "dependencies": { "@silvia-odwyer/photon-node": "0.3.4" }, "engines": { - "node": ">=20" + "node": ">=22" } }, "node_modules/openclaw/node_modules/raw-body": { @@ -6818,9 +5746,9 @@ } }, "node_modules/openclaw/node_modules/typebox": { - "version": "1.1.38", - "resolved": "https://registry.npmjs.org/typebox/-/typebox-1.1.38.tgz", - "integrity": "sha512-pZ0aQPmMmXoUvSbeuWf/Hzsc+avNw/Zd6VeE8CFgkVGWyuHPJvqeJJDeJqLve+K70LvjYIoleGcoJHPT17cWoA==", + "version": "1.1.39", + "resolved": "https://registry.npmjs.org/typebox/-/typebox-1.1.39.tgz", + "integrity": "sha512-vj0afVtOfLQvv0GR0VxVagYxsXN64btL7Z9XoaG0ZggH3mruMMkOO6hXdgMsjCY3shZgEvooAWVeznQVs5c43w==", "license": "MIT" }, "node_modules/openclaw/node_modules/typescript": { @@ -6836,12 +5764,6 @@ "node": ">=14.17" } }, - "node_modules/openclaw/node_modules/uc.micro": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", - "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", - "license": "MIT" - }, "node_modules/openclaw/node_modules/uhyphen": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/uhyphen/-/uhyphen-0.2.0.tgz", @@ -6865,6 +5787,7 @@ "resolved": "https://registry.npmjs.org/undici/-/undici-8.3.0.tgz", "integrity": "sha512-TkUDgb6tl7KOGZ+7e8E3d2FYgUQgF6z5YypqjWmixVQSQERFcVrVg0ySADm2LVLRh5ljAaHTCR5Fmz3Q34rB7Q==", "license": "MIT", + "peer": true, "engines": { "node": ">=22.19.0" } @@ -6992,6 +5915,7 @@ "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", "license": "MIT", + "peer": true, "engines": { "node": ">=10.0.0" }, @@ -7073,6 +5997,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } @@ -7086,48 +6011,6 @@ "zod": "^3.25.28 || ^4" } }, - "node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "license": "MIT", - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "license": "MIT", - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/pako": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", - "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", - "license": "(MIT AND Zlib)" - }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -7137,15 +6020,6 @@ "node": ">= 0.8" } }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", @@ -7179,6 +6053,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -7203,16 +6078,7 @@ "dev": true, "license": "MIT", "engines": { - "node": ">= 6" - } - }, - "node_modules/pngjs": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", - "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==", - "license": "MIT", - "engines": { - "node": ">=10.13.0" + "node": ">= 6" } }, "node_modules/postcss": { @@ -7235,6 +6101,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -7403,17 +6270,12 @@ "integrity": "sha512-wSAGyk2bYR1c7t3SZ3jHcM6xy0lcBcDel6lODcs9ME6Th++Dx2KU+6D3HD8wMMKGA8Wpw7OMd3/4RGzYRpzwRg==", "dev": true, "license": "MIT", + "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/preact" } }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "license": "MIT" - }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -7436,110 +6298,6 @@ "node": ">= 0.10" } }, - "node_modules/qrcode": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz", - "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==", - "license": "MIT", - "dependencies": { - "dijkstrajs": "^1.0.1", - "pngjs": "^5.0.0", - "yargs": "^15.3.1" - }, - "bin": { - "qrcode": "bin/qrcode" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/qrcode/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/qrcode/node_modules/cliui": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", - "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^6.2.0" - } - }, - "node_modules/qrcode/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/qrcode/node_modules/wrap-ansi": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", - "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/qrcode/node_modules/y18n": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", - "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", - "license": "ISC" - }, - "node_modules/qrcode/node_modules/yargs": { - "version": "15.4.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", - "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", - "license": "MIT", - "dependencies": { - "cliui": "^6.0.0", - "decamelize": "^1.2.0", - "find-up": "^4.1.0", - "get-caller-file": "^2.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^4.2.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^18.1.2" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/qrcode/node_modules/yargs-parser": { - "version": "18.1.3", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", - "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", - "license": "ISC", - "dependencies": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/qs": { "version": "6.14.2", "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", @@ -7610,19 +6368,6 @@ "pify": "^2.3.0" } }, - "node_modules/readdirp": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", - "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", - "license": "MIT", - "engines": { - "node": ">= 20.19.0" - }, - "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - }, "node_modules/regexparam": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/regexparam/-/regexparam-3.0.0.tgz", @@ -7633,21 +6378,6 @@ "node": ">=8" } }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/require-main-filename": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", - "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", - "license": "ISC" - }, "node_modules/requires-port": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", @@ -7731,55 +6461,6 @@ "fsevents": "~2.3.2" } }, - "node_modules/router": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", - "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "depd": "^2.0.0", - "is-promise": "^4.0.0", - "parseurl": "^1.3.3", - "path-to-regexp": "^8.0.0" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/router/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/router/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/router/node_modules/path-to-regexp": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", - "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -7888,18 +6569,6 @@ "node": ">= 0.8.0" } }, - "node_modules/set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", - "license": "ISC" - }, - "node_modules/setimmediate": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", - "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", - "license": "MIT" - }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -7969,110 +6638,31 @@ "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", - "side-channel-map": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/siginfo": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", - "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", - "dev": true, - "license": "ISC" - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/sqlite-vec": { - "version": "0.1.9", - "resolved": "https://registry.npmjs.org/sqlite-vec/-/sqlite-vec-0.1.9.tgz", - "integrity": "sha512-L7XJWRIBNvR9O5+vh1FQ+IGkh/3D2AzVksW5gdtk28m78Hy8skFD0pqReKH1Yp0/BUKRGcffgKvyO/EON5JXpA==", - "license": "MIT OR Apache", - "optional": true, - "optionalDependencies": { - "sqlite-vec-darwin-arm64": "0.1.9", - "sqlite-vec-darwin-x64": "0.1.9", - "sqlite-vec-linux-arm64": "0.1.9", - "sqlite-vec-linux-x64": "0.1.9", - "sqlite-vec-windows-x64": "0.1.9" - } - }, - "node_modules/sqlite-vec-darwin-arm64": { - "version": "0.1.9", - "resolved": "https://registry.npmjs.org/sqlite-vec-darwin-arm64/-/sqlite-vec-darwin-arm64-0.1.9.tgz", - "integrity": "sha512-jSsZpE42OfBkGL/ItyJTVCUwl6o6Ka3U5rc4j+UBDIQzC1ulSSKMEhQLthsOnF/MdAf1MuAkYhkdKmmcjaIZQg==", - "cpu": [ - "arm64" - ], - "license": "MIT OR Apache", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/sqlite-vec-darwin-x64": { - "version": "0.1.9", - "resolved": "https://registry.npmjs.org/sqlite-vec-darwin-x64/-/sqlite-vec-darwin-x64-0.1.9.tgz", - "integrity": "sha512-KDlVyqQT7pnOhU1ymB9gs7dMbSoVmKHitT+k1/xkjarcX8bBqPxWrGlK/R+C5WmWkfvWwyq5FfXfiBYCBs6PlA==", - "cpu": [ - "x64" - ], - "license": "MIT OR Apache", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/sqlite-vec-linux-arm64": { - "version": "0.1.9", - "resolved": "https://registry.npmjs.org/sqlite-vec-linux-arm64/-/sqlite-vec-linux-arm64-0.1.9.tgz", - "integrity": "sha512-5wXVJ9c9kR4CHm/wVqXb/R+XUHTdpZ4nWbPHlS+gc9qQFVHs92Km4bPnCKX4rtcPMzvNis+SIzMJR1SCEwpuUw==", - "cpu": [ - "arm64" - ], - "license": "MIT OR Apache", - "optional": true, - "os": [ - "linux" - ] + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "node_modules/sqlite-vec-linux-x64": { - "version": "0.1.9", - "resolved": "https://registry.npmjs.org/sqlite-vec-linux-x64/-/sqlite-vec-linux-x64-0.1.9.tgz", - "integrity": "sha512-w3tCH8xK2finW8fQJ/m8uqKodXUZ9KAuAar2UIhz4BHILfpE0WM/MTGCRfa7RjYbrYim5Luk3guvMOGI7T7JQA==", - "cpu": [ - "x64" - ], - "license": "MIT OR Apache", - "optional": true, - "os": [ - "linux" - ] + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" }, - "node_modules/sqlite-vec-windows-x64": { - "version": "0.1.9", - "resolved": "https://registry.npmjs.org/sqlite-vec-windows-x64/-/sqlite-vec-windows-x64-0.1.9.tgz", - "integrity": "sha512-y3gEIyy/17bq2QFPQOWLE68TYWcRZkBQVA2XLrTPHNTOp55xJi/BBBmOm40tVMDMjtP+Elpk6UBUXdaq+46b0Q==", - "cpu": [ - "x64" - ], - "license": "MIT OR Apache", - "optional": true, - "os": [ - "win32" - ] + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } }, "node_modules/stackback": { "version": "0.0.2", @@ -8097,57 +6687,6 @@ "dev": true, "license": "MIT" }, - "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strtok3": { - "version": "10.3.5", - "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.3.5.tgz", - "integrity": "sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA==", - "license": "MIT", - "dependencies": { - "@tokenizer/token": "^0.3.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, "node_modules/sucrase": { "version": "3.35.1", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", @@ -8492,49 +7031,6 @@ "node": ">=0.6" } }, - "node_modules/token-types": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.1.2.tgz", - "integrity": "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==", - "license": "MIT", - "dependencies": { - "@borewit/text-codec": "^0.2.1", - "@tokenizer/token": "^0.3.0", - "ieee754": "^1.2.1" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, - "node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "license": "MIT" - }, - "node_modules/tree-sitter-bash": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/tree-sitter-bash/-/tree-sitter-bash-0.25.1.tgz", - "integrity": "sha512-7hMytuYIMoXOq24yRulgIxthE9YmggZIOHCyPTTuJcu6EU54tYD+4G39cUb28kxC6jMf/AbPfWGLQtgPTdh3xw==", - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "node-addon-api": "^8.2.1", - "node-gyp-build": "^4.8.2" - }, - "peerDependencies": { - "tree-sitter": "^0.25.0" - }, - "peerDependenciesMeta": { - "tree-sitter": { - "optional": true - } - } - }, "node_modules/ts-interface-checker": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", @@ -8542,18 +7038,6 @@ "dev": true, "license": "Apache-2.0" }, - "node_modules/tslog": { - "version": "4.10.2", - "resolved": "https://registry.npmjs.org/tslog/-/tslog-4.10.2.tgz", - "integrity": "sha512-XuELoRpMR+sq8fuWwX7P0bcj+PRNiicOKDEb3fGNURhxWVyykCi9BNq7c4uVz7h7P0sj8qgBsr5SWS6yBClq3g==", - "license": "MIT", - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/fullstack-build/tslog?sponsor=1" - } - }, "node_modules/type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", @@ -8576,32 +7060,13 @@ "node": ">= 0.6" } }, - "node_modules/uhyphen": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/uhyphen/-/uhyphen-0.2.0.tgz", - "integrity": "sha512-qz3o9CHXmJJPGBdqzab7qAYuW8kQGKNEuoHFYrBwV6hWIMcpAmxDLXojcHfFr9US1Pe6zUswEIJIbLI610fuqA==", - "license": "ISC" - }, - "node_modules/uint8array-extras": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.5.0.tgz", - "integrity": "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/undici-types": { "version": "7.19.2", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz", "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==", "dev": true, "license": "MIT", - "optional": true, - "peer": true + "optional": true }, "node_modules/unpipe": { "version": "1.0.0", @@ -8616,6 +7081,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, "license": "MIT" }, "node_modules/utils-merge": { @@ -8642,6 +7108,7 @@ "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -9201,6 +7668,7 @@ "integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@vitest/expect": "4.0.18", "@vitest/mocker": "4.0.18", @@ -9273,47 +7741,6 @@ } } }, - "node_modules/web-push": { - "version": "3.6.7", - "resolved": "https://registry.npmjs.org/web-push/-/web-push-3.6.7.tgz", - "integrity": "sha512-OpiIUe8cuGjrj3mMBFWY+e4MMIkW3SVT+7vEIjvD9kejGUypv8GPDf84JdPWskK8zMRIJ6xYGm+Kxr8YkPyA0A==", - "license": "MPL-2.0", - "dependencies": { - "asn1.js": "^5.3.0", - "http_ece": "1.2.0", - "https-proxy-agent": "^7.0.0", - "jws": "^4.0.0", - "minimist": "^1.2.5" - }, - "bin": { - "web-push": "src/cli.js" - }, - "engines": { - "node": ">= 16" - } - }, - "node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "license": "BSD-2-Clause" - }, - "node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "license": "MIT", - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, - "node_modules/which-module": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", - "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", - "license": "ISC" - }, "node_modules/why-is-node-running": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", @@ -9345,48 +7772,11 @@ "preact": "^10.0.0" } }, - "node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, "license": "ISC" }, "node_modules/ws": { @@ -9409,30 +7799,6 @@ "optional": true } } - }, - "node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/yaml": { - "version": "2.8.4", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.4.tgz", - "integrity": "sha512-ml/JPOj9fOQK8RNnWojA67GbZ0ApXAUlN2UQclwv2eVgTgn7O9gg9o7paZWKMp4g0H3nTLtS9LVzhkpOFIKzog==", - "license": "ISC", - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14.6" - }, - "funding": { - "url": "https://github.com/sponsors/eemeli" - } } } } diff --git a/package.json b/package.json index a2115de9..d8be7eaf 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "dependencies": { "express": "^4.21.0", "http-proxy": "^1.18.1", - "openclaw": "2026.5.28", + "openclaw": "2026.6.5", "ws": "^8.19.0" }, "devDependencies": { From 92f25f8b5bfde8cbccfb4f848d4b0ca61b3d6e0f Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Thu, 11 Jun 2026 19:13:32 -0700 Subject: [PATCH 02/11] Add prepare script so git-dependency installs build the UI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit npm only runs prepack for registry publishes, not when installing from a git URL — so consumers pinning github:garrytan/alphaclaw got no lib/public/dist/ or generated Tailwind CSS (both gitignored), leaving the setup UI blank. prepare runs for git deps and rebuilds the same artifacts. Co-Authored-By: Claude Fable 5 --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index d8be7eaf..f60d443e 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,8 @@ "test:watch": "vitest", "test:watchdog": "vitest run tests/server/watchdog.test.js tests/server/watchdog-db.test.js tests/server/routes-watchdog.test.js", "test:coverage": "vitest run --coverage", - "prepack": "npm run build:ui" + "prepack": "npm run build:ui", + "prepare": "npm run build:ui" }, "dependencies": { "express": "^4.21.0", From 20f818ca6709a3c4be7c1168166ddf6d9c871760 Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Thu, 11 Jun 2026 19:51:12 -0700 Subject: [PATCH 03/11] Prune stale usage-tracker plugin paths on boot MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ensureUsageTrackerPluginEntry only ever *added* the canonical __dirname-resolved usage-tracker path; it never removed paths left by a previous install at a different location. After a deployment switches from the @chrysb/alphaclaw npm package (installed at /app/node_modules/@chrysb/alphaclaw/...) to a git dependency (installed at /app/node_modules/alphaclaw/...), the old path lingers in openclaw.json, no longer exists on disk, and OpenClaw rejects the whole config as invalid — breaking the gateway and every openclaw subcommand (status/health/logs/doctor). Prune any `.../plugin/usage-tracker` load path that isn't the canonical one in the boot reconcile (ensureUsageTrackerPluginConfig), so existing configs migrate on the next boot. Kept out of the shared reconcile path so config sanitization semantics are unchanged. Co-Authored-By: Claude Fable 5 --- lib/server/usage-tracker-config.js | 28 +++++++++++- tests/server/usage-tracker-config.test.js | 55 +++++++++++++++++++++++ 2 files changed, 82 insertions(+), 1 deletion(-) diff --git a/lib/server/usage-tracker-config.js b/lib/server/usage-tracker-config.js index 05428b19..1b92cf40 100644 --- a/lib/server/usage-tracker-config.js +++ b/lib/server/usage-tracker-config.js @@ -7,6 +7,10 @@ const kUsageTrackerPluginPath = path.resolve( "plugin", "usage-tracker", ); +// Matches any `.../lib/plugin/usage-tracker` load path regardless of where the +// AlphaClaw package was installed (npm scope dir, git alias dir, version bumps). +const kUsageTrackerPluginPathPattern = /[\\/]plugin[\\/]usage-tracker[\\/]?$/; + const kConversationAccessHookPolicyKey = "allowConversationAccess"; const kChannelPluginIds = ["telegram", "discord", "slack", "whatsapp"]; const kDefaultDiscordGroupPolicy = "disabled"; @@ -72,6 +76,24 @@ const ensureUsageTrackerPluginEntry = (cfg = {}) => { return JSON.stringify(cfg) !== before; }; +// Remove usage-tracker plugin paths left behind by a previous install location +// (e.g. a prior `@chrysb/alphaclaw` npm install at `/app/node_modules/@chrysb/alphaclaw/...` +// after switching to a git dependency installed at `/app/node_modules/alphaclaw/...`). +// Only the current `__dirname`-resolved path is valid; any other entry pointing at a +// `.../plugin/usage-tracker` dir is dead and makes OpenClaw reject the whole config. +const pruneStaleUsageTrackerPaths = (cfg = {}) => { + ensurePluginsShell(cfg); + const paths = cfg.plugins.load.paths; + const filtered = paths.filter( + (entry) => + entry === kUsageTrackerPluginPath || + !kUsageTrackerPluginPathPattern.test(String(entry || "")), + ); + if (filtered.length === paths.length) return false; + cfg.plugins.load.paths = filtered; + return true; +}; + const hasDiscordGuildAllowlist = (discordConfig = {}) => { const guilds = discordConfig.guilds; return !!guilds && typeof guilds === "object" && Object.keys(guilds).length > 0; @@ -123,7 +145,10 @@ const ensureUsageTrackerPluginConfig = ({ fsModule, openclawDir }) => { openclawDir, fallback: {}, }); - const changed = reconcileManagedPluginConfig(cfg); + // Migrate configs written by a previous install location before reconciling, + // so the canonical path is the only usage-tracker entry that remains. + const prunedStale = pruneStaleUsageTrackerPaths(cfg); + const changed = reconcileManagedPluginConfig(cfg) || prunedStale; if (!changed) return false; writeOpenclawConfig({ fsModule, @@ -139,6 +164,7 @@ module.exports = { kDefaultDiscordGroupPolicy, ensurePluginsShell, ensurePluginAllowed, + pruneStaleUsageTrackerPaths, ensureUsageTrackerPluginEntry, reconcileDiscordGroupPolicy, reconcileEnabledChannelPlugins, diff --git a/tests/server/usage-tracker-config.test.js b/tests/server/usage-tracker-config.test.js index 7aebfc63..fe29e275 100644 --- a/tests/server/usage-tracker-config.test.js +++ b/tests/server/usage-tracker-config.test.js @@ -5,6 +5,7 @@ const path = require("path"); const { ensureUsageTrackerPluginConfig, ensureUsageTrackerPluginEntry, + pruneStaleUsageTrackerPaths, kUsageTrackerPluginPath, } = require("../../lib/server/usage-tracker-config"); @@ -97,4 +98,58 @@ describe("server/usage-tracker-config", () => { allowConversationAccess: true, }); }); + + it("prunes a usage-tracker path left by a previous install location", () => { + const cfg = { + plugins: { + allow: [], + load: { + paths: [ + "/app/node_modules/@chrysb/alphaclaw/lib/plugin/usage-tracker", + kUsageTrackerPluginPath, + "/app/node_modules/some-other-plugin", + ], + }, + entries: {}, + }, + }; + + const changed = pruneStaleUsageTrackerPaths(cfg); + + expect(changed).toBe(true); + expect(cfg.plugins.load.paths).toEqual([ + kUsageTrackerPluginPath, + "/app/node_modules/some-other-plugin", + ]); + }); + + it("migrates a stale @chrysb usage-tracker path to the canonical path on boot", () => { + const openclawDir = createTempOpenclawDir(); + const configPath = path.join(openclawDir, "openclaw.json"); + fs.writeFileSync( + configPath, + JSON.stringify( + { + plugins: { + allow: ["usage-tracker"], + load: { + paths: [ + "/app/node_modules/@chrysb/alphaclaw/lib/plugin/usage-tracker", + ], + }, + entries: { "usage-tracker": { enabled: true } }, + }, + }, + null, + 2, + ), + "utf8", + ); + + const changed = ensureUsageTrackerPluginConfig({ fsModule: fs, openclawDir }); + + expect(changed).toBe(true); + const next = JSON.parse(fs.readFileSync(configPath, "utf8")); + expect(next.plugins.load.paths).toEqual([kUsageTrackerPluginPath]); + }); }); From 096a13db888ec4ddb3dc0b23893c1dce8f97c5df Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Thu, 11 Jun 2026 19:51:19 -0700 Subject: [PATCH 04/11] Support git-based deployment of AlphaClaw AlphaClaw assumed it was always the published @chrysb/alphaclaw npm package: it resolved its own install dir and version by that exact dependency key, and its in-place "Update" ran npm install @chrysb/alphaclaw@latest from the registry. None of that holds when a consumer pins it as a git dependency (e.g. "alphaclaw": "git+https://github.com//alphaclaw.git#main"). - Add lib/server/self-dependency.js: resolveSelfDependency() finds the consumer app root and how AlphaClaw is pinned, matching either the `alphaclaw` alias (git installs) or the `@chrysb/alphaclaw` npm scope, and classifies the spec as git vs npm (looksLikeGitDependency). - Rename the package to `alphaclaw`. - Route findInstallDir (openclaw-version + alphaclaw-version) and the template/version lookups through the shared resolver (dual-key). - Self-update is now spec-aware: npm-pinned installs keep the in-place npm update (using the resolved package name, no hardcoded scope); git-pinned installs return redeploy/reinstall instructions instead and skip the npm registry version check entirely. The pending-update path in bin/alphaclaw.js does the same. - Point the Render template + GitHub releases URLs at the garrytan fork. Co-Authored-By: Claude Fable 5 --- bin/alphaclaw.js | 48 ++++++++------ lib/server/alphaclaw-version.js | 92 +++++++++++++++----------- lib/server/constants.js | 2 +- lib/server/openclaw-version.js | 34 ++-------- lib/server/self-dependency.js | 75 +++++++++++++++++++++ package.json | 2 +- tests/server/alphaclaw-version.test.js | 52 +++++++++++++++ tests/server/self-dependency.test.js | 88 ++++++++++++++++++++++++ 8 files changed, 301 insertions(+), 92 deletions(-) create mode 100644 lib/server/self-dependency.js create mode 100644 tests/server/self-dependency.test.js diff --git a/bin/alphaclaw.js b/bin/alphaclaw.js index 45356b4b..fea98077 100755 --- a/bin/alphaclaw.js +++ b/bin/alphaclaw.js @@ -20,6 +20,7 @@ const { restoreMissingOpenclawConfigFromRemote, } = require("../lib/cli/openclaw-config-restore"); const { buildSecretReplacements } = require("../lib/server/helpers"); +const { resolveSelfDependency } = require("../lib/server/self-dependency"); const { migrateManagedInternalFiles, } = require("../lib/server/internal-files-migration"); @@ -170,29 +171,34 @@ console.log(`[alphaclaw] Root directory: ${rootDir}`); // from the fresh container using the persistent volume marker. const pendingUpdateMarker = path.join(rootDir, ".alphaclaw-update-pending"); if (fs.existsSync(pendingUpdateMarker)) { - console.log( - "[alphaclaw] Pending update detected, installing @chrysb/alphaclaw@latest...", - ); - const alphaPkgRoot = path.resolve(__dirname, ".."); - const nmIndex = alphaPkgRoot.lastIndexOf( - `${path.sep}node_modules${path.sep}`, - ); - const installDir = - nmIndex >= 0 ? alphaPkgRoot.slice(0, nmIndex) : alphaPkgRoot; - try { - execSync( - "npm install @chrysb/alphaclaw@latest --omit=dev --prefer-online", - { - cwd: installDir, - stdio: "inherit", - timeout: 180000, - }, + const selfDep = resolveSelfDependency({ fsImpl: fs }); + if (selfDep.isGit) { + // Git-based installs update by redeploying (which reinstalls from the pinned + // ref), not by `npm install @latest`. Clear the marker and move on. + console.log( + "[alphaclaw] Pending update marker found, but this install is git-based; updates apply on redeploy. Skipping npm install.", ); fs.unlinkSync(pendingUpdateMarker); - console.log("[alphaclaw] Update applied successfully"); - } catch (e) { - console.log(`[alphaclaw] Update install failed: ${e.message}`); - fs.unlinkSync(pendingUpdateMarker); + } else { + const selfUpdatePackageName = selfDep.key || "alphaclaw"; + console.log( + `[alphaclaw] Pending update detected, installing ${selfUpdatePackageName}@latest...`, + ); + try { + execSync( + `npm install ${selfUpdatePackageName}@latest --omit=dev --prefer-online`, + { + cwd: selfDep.installDir, + stdio: "inherit", + timeout: 180000, + }, + ); + fs.unlinkSync(pendingUpdateMarker); + console.log("[alphaclaw] Update applied successfully"); + } catch (e) { + console.log(`[alphaclaw] Update install failed: ${e.message}`); + fs.unlinkSync(pendingUpdateMarker); + } } } diff --git a/lib/server/alphaclaw-version.js b/lib/server/alphaclaw-version.js index f3337785..0c52ae5a 100644 --- a/lib/server/alphaclaw-version.js +++ b/lib/server/alphaclaw-version.js @@ -14,6 +14,10 @@ const { normalizeOpenclawVersion, resolveGithubRepoUrl, } = require("./helpers"); +const { + resolveSelfDependency, + looksLikeGitDependency, +} = require("./self-dependency"); const kGithubApiBaseUrl = "https://api.github.com/repos"; const kGithubRawBaseUrl = "https://raw.githubusercontent.com"; @@ -21,7 +25,7 @@ const kDefaultTemplateBranch = "main"; const kRailwayTemplateRepoUrl = "https://github.com/chrysb/openclaw-railway-template.git"; const kRenderTemplateRepoUrl = - "https://github.com/chrysb/openclaw-render-template.git"; + "https://github.com/garrytan/openclaw-render-template.git"; const kApexTemplateRepoUrl = "https://github.com/chrysb/openclaw-apex-template.git"; @@ -91,10 +95,18 @@ const buildGithubHeaders = ({ env = process.env, accept = "application/json" } = return headers; }; -const extractTemplateVersions = (pkg) => ({ - latestVersion: normalizeVersion(pkg?.dependencies?.["@chrysb/alphaclaw"]), - latestOpenclawVersion: normalizeOpenclawVersion(pkg?.dependencies?.openclaw), -}); +const extractTemplateVersions = (pkg) => { + const alphaclawSpec = + pkg?.dependencies?.["alphaclaw"] || pkg?.dependencies?.["@chrysb/alphaclaw"]; + return { + // A git-pinned template has no semver to compare against — report null so we + // never surface a bogus "update available" from a git URL string. + latestVersion: looksLikeGitDependency(alphaclawSpec) + ? null + : normalizeVersion(alphaclawSpec), + latestOpenclawVersion: normalizeOpenclawVersion(pkg?.dependencies?.openclaw), + }; +}; const fetchLatestVersionFromRegistry = async ({ fetchImpl, version = null }) => { if (typeof fetchImpl !== "function") { @@ -343,12 +355,33 @@ const detectUpdateStrategy = ({ }); } + // Git-based installs (e.g. `"alphaclaw": "git+https://github.com//alphaclaw.git#main"`) + // can't be updated by `npm install @latest` from the registry — updates come + // from pulling the source and reinstalling/redeploying. Keep the in-place npm + // self-update only when AlphaClaw is pinned to an npm version. + const selfDep = resolveSelfDependency({ fsImpl }); + if (selfDep.isGit) { + return createUpdateStrategy({ + action: "instructions", + provider: "git", + label: "Git source", + description: + "This AlphaClaw is installed from a git repository. Update by pulling the latest commit of your source repo and reinstalling (or redeploying), then restart AlphaClaw.", + steps: [ + "Pull the latest commit of your AlphaClaw source repository", + "Reinstall dependencies so the new version is built (npm install)", + "Restart AlphaClaw to load the update", + ], + primaryActionLabel: "Done", + }); + } + + const selfUpdatePackageName = selfDep.key || "alphaclaw"; return createUpdateStrategy({ action: "self-update", provider: "self-hosted", label: "This install", - description: - "This will install the latest @chrysb/alphaclaw package in place and restart AlphaClaw.", + description: `This will install the latest ${selfUpdatePackageName} package in place and restart AlphaClaw.`, steps: [ "AlphaClaw will install the latest published package in place", "The process will restart after the new files are copied into node_modules", @@ -451,7 +484,9 @@ const createAlphaclawVersionService = ({ const installLatestAlphaclaw = () => new Promise((resolve, reject) => { - const installDir = findInstallDir(fsImpl); + const selfDep = resolveSelfDependency({ fsImpl }); + const installDir = selfDep.installDir; + const selfUpdatePackageName = selfDep.key || "alphaclaw"; const tmpDir = fsImpl.mkdtempSync(path.join(os.tmpdir(), "alphaclaw-update-")); const cleanup = () => { @@ -464,7 +499,7 @@ const createAlphaclawVersionService = ({ path.join(tmpDir, "package.json"), JSON.stringify({ private: true, - dependencies: { "@chrysb/alphaclaw": "latest" }, + dependencies: { [selfUpdatePackageName]: "latest" }, }), ); @@ -476,7 +511,7 @@ const createAlphaclawVersionService = ({ }; console.log( - `[alphaclaw] Running: npm install @chrysb/alphaclaw@latest in temp dir (target: ${installDir})`, + `[alphaclaw] Running: npm install ${selfUpdatePackageName}@latest in temp dir (target: ${installDir})`, ); childProcess.exec( "npm install --omit=dev --prefer-online --package-lock=false", @@ -494,7 +529,7 @@ const createAlphaclawVersionService = ({ cleanup(); return reject( new Error( - message || "Failed to install @chrysb/alphaclaw@latest", + message || `Failed to install ${selfUpdatePackageName}@latest`, ), ); } @@ -616,6 +651,12 @@ const createAlphaclawVersionService = ({ const getVersionStatus = async (refresh) => { const strategy = detectUpdateStrategy({ env, fsImpl }); try { + if (strategy.provider === "git") { + // Git-sourced installs have no npm registry or template version to compare + // against — updates come from redeploying the pinned ref. Report current + // state without a remote version check. + return buildVersionStatus({ strategy }); + } if (strategy.templateRepoUrl) { const status = await readTemplateStatus({ repoUrl: strategy.templateRepoUrl, @@ -719,35 +760,6 @@ const createAlphaclawVersionService = ({ }; }; -const findInstallDir = (fsImpl) => { - let dir = kNpmPackageRoot; - while (dir !== path.dirname(dir)) { - const parent = path.dirname(dir); - if ( - path.basename(parent) === "node_modules" || - parent.includes(`${path.sep}node_modules${path.sep}`) - ) { - dir = parent; - continue; - } - const pkgPath = path.join(parent, "package.json"); - if (fsImpl.existsSync(pkgPath)) { - try { - const pkg = JSON.parse(fsImpl.readFileSync(pkgPath, "utf8")); - if ( - pkg.dependencies?.["@chrysb/alphaclaw"] || - pkg.devDependencies?.["@chrysb/alphaclaw"] || - pkg.optionalDependencies?.["@chrysb/alphaclaw"] - ) { - return parent; - } - } catch {} - } - dir = parent; - } - return kNpmPackageRoot; -}; - module.exports = { createAlphaclawVersionService, detectUpdateStrategy, diff --git a/lib/server/constants.js b/lib/server/constants.js index 49cfaeba..21511c85 100644 --- a/lib/server/constants.js +++ b/lib/server/constants.js @@ -211,7 +211,7 @@ const kOpenclawUpdateCopyTimeoutMs = 5 * 60 * 1000; const kOpenclawRegistryUrl = "https://registry.npmjs.org/openclaw"; const kAlphaclawRegistryUrl = "https://registry.npmjs.org/@chrysb%2falphaclaw"; const kAlphaclawGithubReleasesBaseUrl = - "https://api.github.com/repos/chrysb/alphaclaw/releases"; + "https://api.github.com/repos/garrytan/alphaclaw/releases"; const kAppDir = kNpmPackageRoot; const kMaxPayloadBytes = parsePositiveInt(process.env.WEBHOOK_LOG_MAX_BYTES, 50 * 1024); const kWebhookPruneDays = parsePositiveInt(process.env.WEBHOOK_LOG_RETENTION_DAYS, 30); diff --git a/lib/server/openclaw-version.js b/lib/server/openclaw-version.js index 9e580c22..0e6ffc01 100644 --- a/lib/server/openclaw-version.js +++ b/lib/server/openclaw-version.js @@ -10,6 +10,7 @@ const { } = require("./constants"); const { normalizeOpenclawVersion } = require("./helpers"); const { parseJsonObjectFromNoisyOutput } = require("./utils/json"); +const { resolveSelfDependency } = require("./self-dependency"); const createOpenclawVersionService = ({ gatewayEnv, @@ -88,35 +89,10 @@ const createOpenclawVersionService = ({ } }; - const findInstallDir = () => { - // Resolve the consumer app root (for example /app in Docker), not this package directory. - let dir = kNpmPackageRoot; - while (dir !== path.dirname(dir)) { - const parent = path.dirname(dir); - if ( - path.basename(parent) === "node_modules" || - parent.includes(`${path.sep}node_modules${path.sep}`) - ) { - dir = parent; - continue; - } - const pkgPath = path.join(parent, "package.json"); - if (fs.existsSync(pkgPath)) { - try { - const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8")); - if ( - pkg.dependencies?.["@chrysb/alphaclaw"] || - pkg.devDependencies?.["@chrysb/alphaclaw"] || - pkg.optionalDependencies?.["@chrysb/alphaclaw"] - ) { - return parent; - } - } catch {} - } - dir = parent; - } - return kNpmPackageRoot; - }; + // Resolve the consumer app root (for example /app in Docker), not this package + // directory. Matches AlphaClaw under either the `alphaclaw` alias (git installs) + // or the `@chrysb/alphaclaw` npm scope. + const findInstallDir = () => resolveSelfDependency({ fsImpl: fs }).installDir; // Install to a temp directory, then copy into the real node_modules. // Running `npm install` directly in the app dir causes EBUSY on Docker diff --git a/lib/server/self-dependency.js b/lib/server/self-dependency.js new file mode 100644 index 00000000..83e32b22 --- /dev/null +++ b/lib/server/self-dependency.js @@ -0,0 +1,75 @@ +const fs = require("fs"); +const path = require("path"); +const { kNpmPackageRoot } = require("./constants"); + +// AlphaClaw may be consumed under different dependency keys: the published npm +// scope (`@chrysb/alphaclaw`) or a plain alias used by git-based deployments +// (`alphaclaw`, e.g. `"alphaclaw": "git+https://github.com//alphaclaw.git#main"`). +// Check the alias first so git deployments resolve correctly. +const kSelfDependencyKeys = ["alphaclaw", "@chrysb/alphaclaw"]; + +// A dependency spec points at a git source (rather than an npm version/range) +// when it uses a git protocol/shorthand or otherwise references a git host. +const looksLikeGitDependency = (spec) => { + const value = String(spec || "").trim(); + if (!value) return false; + return ( + /^(git\+|github:|gitlab:|bitbucket:|gist:|git:|git@|ssh:)/i.test(value) || + /\.git($|#)/i.test(value) || + /github\.com/i.test(value) + ); +}; + +const readDependencySpec = (pkg, key) => + pkg?.dependencies?.[key] || + pkg?.devDependencies?.[key] || + pkg?.optionalDependencies?.[key] || + null; + +// Find the consumer app root (e.g. /app in Docker) — the nearest ancestor +// package.json that declares AlphaClaw as a dependency — and report how it is +// pinned. Falls back to the AlphaClaw package root with no dependency info. +const resolveSelfDependency = ({ fsImpl = fs, startDir = kNpmPackageRoot } = {}) => { + let dir = startDir; + while (dir !== path.dirname(dir)) { + const parent = path.dirname(dir); + if ( + path.basename(parent) === "node_modules" || + parent.includes(`${path.sep}node_modules${path.sep}`) + ) { + dir = parent; + continue; + } + const pkgPath = path.join(parent, "package.json"); + if (fsImpl.existsSync(pkgPath)) { + try { + const pkg = JSON.parse(fsImpl.readFileSync(pkgPath, "utf8")); + for (const key of kSelfDependencyKeys) { + const spec = readDependencySpec(pkg, key); + if (spec) { + return { + installDir: parent, + key, + spec, + isGit: looksLikeGitDependency(spec), + }; + } + } + } catch {} + } + dir = parent; + } + return { installDir: kNpmPackageRoot, key: null, spec: null, isGit: false }; +}; + +// Resolve just the install dir (consumer app root) — preserves the prior +// findInstallDir() contract used by the OpenClaw/AlphaClaw self-updaters. +const findInstallDir = (fsImpl = fs) => + resolveSelfDependency({ fsImpl }).installDir; + +module.exports = { + kSelfDependencyKeys, + looksLikeGitDependency, + resolveSelfDependency, + findInstallDir, +}; diff --git a/package.json b/package.json index f60d443e..10d70fe7 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "@chrysb/alphaclaw", + "name": "alphaclaw", "version": "0.9.18", "publishConfig": { "access": "public" diff --git a/tests/server/alphaclaw-version.test.js b/tests/server/alphaclaw-version.test.js index e5cc32be..fea87d89 100644 --- a/tests/server/alphaclaw-version.test.js +++ b/tests/server/alphaclaw-version.test.js @@ -97,6 +97,58 @@ describe("server/alphaclaw-version", () => { ); }); + it("returns git-source instructions and skips the npm registry for git installs", async () => { + const fetchMock = vi.fn(); + const gitPkg = JSON.stringify({ + dependencies: { + alphaclaw: "git+https://github.com/garrytan/alphaclaw.git#main", + }, + }); + const { service } = createService({ + env: {}, + fetchMock, + fsImpl: { + ...fs, + // /.dockerenv absent (so we reach the self-hosted fallback) but the + // consumer package.json is found by resolveSelfDependency. + existsSync: vi.fn((p) => String(p).endsWith("package.json")), + readFileSync: vi.fn(() => gitPkg), + }, + }); + + const status = await service.getVersionStatus(false); + + expect(fetchMock).not.toHaveBeenCalled(); + expect(status.hasUpdate).toBe(false); + expect(status.updateStrategy).toEqual( + expect.objectContaining({ action: "instructions", provider: "git" }), + ); + }); + + it("refuses an in-place update for git installs", async () => { + const gitPkg = JSON.stringify({ + dependencies: { + alphaclaw: "git+https://github.com/garrytan/alphaclaw.git#main", + }, + }); + const execMock = vi.fn(); + const { service } = createService({ + env: {}, + execMock, + fsImpl: { + ...fs, + existsSync: vi.fn((p) => String(p).endsWith("package.json")), + readFileSync: vi.fn(() => gitPkg), + }, + }); + + const result = await service.updateAlphaclaw(); + + expect(result.status).toBe(409); + expect(result.body.ok).toBe(false); + expect(execMock).not.toHaveBeenCalled(); + }); + it("returns template-managed status for railway deployments", async () => { const fetchMock = vi.fn(async (url) => { expect(url).toContain( diff --git a/tests/server/self-dependency.test.js b/tests/server/self-dependency.test.js new file mode 100644 index 00000000..04e67121 --- /dev/null +++ b/tests/server/self-dependency.test.js @@ -0,0 +1,88 @@ +const fs = require("fs"); + +const { + looksLikeGitDependency, + resolveSelfDependency, +} = require("../../lib/server/self-dependency"); + +describe("server/self-dependency", () => { + describe("looksLikeGitDependency", () => { + it("flags git URL specs", () => { + for (const spec of [ + "git+https://github.com/garrytan/alphaclaw.git#main", + "github:garrytan/alphaclaw#main", + "git+ssh://git@github.com/garrytan/alphaclaw.git", + "https://github.com/garrytan/alphaclaw", + "git://example.com/alphaclaw.git", + ]) { + expect(looksLikeGitDependency(spec)).toBe(true); + } + }); + + it("does not flag npm version specs", () => { + for (const spec of ["0.9.18", "^0.9.0", "latest", "~1.2.3", ""]) { + expect(looksLikeGitDependency(spec)).toBe(false); + } + }); + }); + + describe("resolveSelfDependency", () => { + const makeFs = (pkgByPath) => ({ + ...fs, + existsSync: (p) => Object.prototype.hasOwnProperty.call(pkgByPath, p), + readFileSync: (p) => { + if (!pkgByPath[p]) throw new Error(`no such file: ${p}`); + return JSON.stringify(pkgByPath[p]); + }, + }); + + it("resolves a git-pinned consumer dependency under the alphaclaw alias", () => { + const fsImpl = makeFs({ + "/app/package.json": { + dependencies: { + alphaclaw: "git+https://github.com/garrytan/alphaclaw.git#main", + }, + }, + }); + const result = resolveSelfDependency({ + fsImpl, + startDir: "/app/node_modules/alphaclaw/lib/server", + }); + expect(result).toEqual({ + installDir: "/app", + key: "alphaclaw", + spec: "git+https://github.com/garrytan/alphaclaw.git#main", + isGit: true, + }); + }); + + it("resolves a legacy npm-pinned @chrysb/alphaclaw dependency", () => { + const fsImpl = makeFs({ + "/app/package.json": { + dependencies: { "@chrysb/alphaclaw": "0.9.18" }, + }, + }); + const result = resolveSelfDependency({ + fsImpl, + startDir: "/app/node_modules/@chrysb/alphaclaw/lib/server", + }); + expect(result.key).toBe("@chrysb/alphaclaw"); + expect(result.spec).toBe("0.9.18"); + expect(result.isGit).toBe(false); + expect(result.installDir).toBe("/app"); + }); + + it("returns no dependency when none is declared", () => { + const fsImpl = makeFs({ + "/app/package.json": { dependencies: { express: "^4.0.0" } }, + }); + const result = resolveSelfDependency({ + fsImpl, + startDir: "/app/node_modules/alphaclaw/lib/server", + }); + expect(result.key).toBeNull(); + expect(result.spec).toBeNull(); + expect(result.isGit).toBe(false); + }); + }); +}); From dbe63d70574dc41ef82bd47543d116b99eb0dfd5 Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Thu, 11 Jun 2026 20:01:40 -0700 Subject: [PATCH 05/11] Prune stale usage-tracker plugin paths on every boot, not just onboarded MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The boot-time prune added in the previous change lived in ensureUsageTrackerPluginConfig, which only runs via runOnboardedBootSequence — i.e. only when isOnboarded() is true. A deployment that carries a stale @chrysb/alphaclaw usage-tracker path but is not (yet) onboarded never got the path pruned, so OpenClaw kept rejecting the config and every openclaw command failed. bin/alphaclaw.js already has an unconditional "Config exists, reconciling channels" block that runs on every boot whenever openclaw.json exists, and it was *adding* the canonical usage-tracker path without removing stale ones (leaving both). Prune stale `.../plugin/usage-tracker` entries there too, so the migration is load-bearing regardless of onboarding state. The onboarded reconcile prune remains as a backstop. Co-Authored-By: Claude Fable 5 --- bin/alphaclaw.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/bin/alphaclaw.js b/bin/alphaclaw.js index fea98077..73b0781c 100755 --- a/bin/alphaclaw.js +++ b/bin/alphaclaw.js @@ -771,6 +771,22 @@ if (fs.existsSync(configPath)) { console.log("[alphaclaw] Discord added"); changed = true; } + // Drop usage-tracker plugin paths left by a previous install location (e.g. a + // prior @chrysb/alphaclaw npm install at /app/node_modules/@chrysb/alphaclaw/... + // after switching to a git dependency at /app/node_modules/alphaclaw/...). The + // dead path makes OpenClaw reject the whole config. This block runs on every + // boot whenever a config exists — onboarded or not — so it is the migration's + // load-bearing prune; the onboarded reconcile prune is a backstop. + const usageTrackerPathPattern = /[\\/]plugin[\\/]usage-tracker[\\/]?$/; + const prunedPaths = cfg.plugins.load.paths.filter( + (entry) => + entry === kUsageTrackerPluginPath || + !usageTrackerPathPattern.test(String(entry || "")), + ); + if (prunedPaths.length !== cfg.plugins.load.paths.length) { + cfg.plugins.load.paths = prunedPaths; + changed = true; + } if (!cfg.plugins.load.paths.includes(kUsageTrackerPluginPath)) { cfg.plugins.load.paths.push(kUsageTrackerPluginPath); changed = true; From 3fc02c13c3561bca77981f3780f8e5f8aeb291a1 Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Wed, 17 Jun 2026 09:06:18 -0700 Subject: [PATCH 06/11] Bump openclaw to 2026.6.8. Latest npm stable. Brings Claude Fable 5 (registry-verified) and Haiku 4.5 model support, plus the 6.6 security-hardening batch (fail-closed exec approvals, sandbox bind-path validation, MCP stdio/SSE hardening) and 6.8 admin-gating on HTTP model-override/session-kill endpoints. Gateway is spawned with fixed args ("gateway run"), so the 6.8 CLI usage-error exit-code change (0 -> non-zero) does not affect the watchdog's exit-code handling. Co-Authored-By: Claude Opus 4.8 (1M context) --- package-lock.json | 45 ++++++++++++++++++--------------------------- package.json | 2 +- 2 files changed, 19 insertions(+), 28 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8ad7c759..ccb79753 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,17 +1,17 @@ { - "name": "@chrysb/alphaclaw", + "name": "alphaclaw", "version": "0.9.18", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "@chrysb/alphaclaw", + "name": "alphaclaw", "version": "0.9.18", "license": "MIT", "dependencies": { "express": "^4.21.0", "http-proxy": "^1.18.1", - "openclaw": "2026.6.5", + "openclaw": "2026.6.8", "ws": "^8.19.0" }, "bin": { @@ -2526,9 +2526,9 @@ } }, "node_modules/openclaw": { - "version": "2026.6.5", - "resolved": "https://registry.npmjs.org/openclaw/-/openclaw-2026.6.5.tgz", - "integrity": "sha512-sRgF0TexfRcJX8Eg0lcL6Jj0YdZbSxUbbp8EbG+qo3v6TtVayE6tKPEs3oCKD7YfYe2C/8Qg26HUxTnycd44ZQ==", + "version": "2026.6.8", + "resolved": "https://registry.npmjs.org/openclaw/-/openclaw-2026.6.8.tgz", + "integrity": "sha512-iziR8fi69+ojrtX7FYYvTpkGcVnmyLpIhvchgt5LFkkdHVWw973XAAekKVZ3/xQJ5FG4NwgHkXL0LLTrgsNOSQ==", "hasInstallScript": true, "hasShrinkwrap": true, "license": "MIT", @@ -3981,9 +3981,9 @@ } }, "node_modules/openclaw/node_modules/gaxios": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.4.tgz", - "integrity": "sha512-bTIgTsM2bWn3XklZISBTQX7ZSddGW+IO3bMdGaemHZ3tbqExMENHLx6kKZ/KlejgrMtj8q7wBItt51yegqalrA==", + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.5.tgz", + "integrity": "sha512-5FZy72Rh8LhtjmvDrKkI+lVhrsQrVKVsItxMoDm5mNQE+xR0WVIIs+jzPSJgBvKVsLi24fZhXJIsNI0bihDzFg==", "license": "Apache-2.0", "dependencies": { "extend": "^3.0.2", @@ -4084,9 +4084,9 @@ } }, "node_modules/openclaw/node_modules/google-auth-library": { - "version": "10.6.2", - "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.6.2.tgz", - "integrity": "sha512-e27Z6EThmVNNvtYASwQxose/G57rkRuaRbQyxM2bvYLLX/GqWZ5chWq2EBoUchJbCc57eC9ArzO5wMsEmWftCw==", + "version": "10.7.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.7.0.tgz", + "integrity": "sha512-QpTAbNJ36TliZLx3TTtahR8HG0hN9RllL1e3FymOvQSIKK8JmgV58H924ub2wa2DsS3ANjjP1Aw1N+Ramc8hqQ==", "license": "Apache-2.0", "dependencies": { "base64-js": "^1.3.0", @@ -4197,9 +4197,9 @@ } }, "node_modules/openclaw/node_modules/hono": { - "version": "4.12.18", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.18.tgz", - "integrity": "sha512-RWzP96k/yv0PQfyXnWjs6zot20TqfpfsNXhOnev8d1InAxubW93L11/oNUc3tQqn2G0bSdAOBpX+2uDFHV7kdQ==", + "version": "4.12.25", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.25.tgz", + "integrity": "sha512-2NFaIyNVgJmBs/ecmtGzlmluTFs5cHEWGTdu0t1HBwYzoGXOL5nUQBRMXsXWla5i4KkG//QMzVP88m1+I3fdAQ==", "license": "MIT", "peer": true, "engines": { @@ -5006,9 +5006,9 @@ } }, "node_modules/openclaw/node_modules/protobufjs": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-8.4.0.tgz", - "integrity": "sha512-iriNhQ57SYA5Jbdi+41AyPdx6jPPkFO7DODzkOBmqFhgYn/JzX2HxgxYPY18eQAs3CP/AWqtPvkWn8rclRAxdQ==", + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-8.4.1.tgz", + "integrity": "sha512-oXf2UgIty8jnwfN4yvL1x79VLhL5uiKjZJbSGXGCIUmHmItTP4eS/UIlWDCeNx3seg+ujfn9vDlPMSrsh7wO+Q==", "hasInstallScript": true, "license": "BSD-3-Clause", "dependencies": { @@ -5915,7 +5915,6 @@ "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", "license": "MIT", - "peer": true, "engines": { "node": ">=10.0.0" }, @@ -7060,14 +7059,6 @@ "node": ">= 0.6" } }, - "node_modules/undici-types": { - "version": "7.19.2", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz", - "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==", - "dev": true, - "license": "MIT", - "optional": true - }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", diff --git a/package.json b/package.json index 10d70fe7..659c30b4 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "dependencies": { "express": "^4.21.0", "http-proxy": "^1.18.1", - "openclaw": "2026.6.5", + "openclaw": "2026.6.8", "ws": "^8.19.0" }, "devDependencies": { From 5f1eb984fdbd078ca95ead3e57acb97380f30558 Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Sat, 11 Jul 2026 06:42:07 -0700 Subject: [PATCH 07/11] Bump openclaw to 2026.6.11. Latest npm stable. Co-Authored-By: Claude Opus 4.8 (1M context) --- package-lock.json | 177 +++++++++++++++++++++++++++++++++++++++++----- package.json | 2 +- 2 files changed, 162 insertions(+), 17 deletions(-) diff --git a/package-lock.json b/package-lock.json index ccb79753..f7081a37 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "dependencies": { "express": "^4.21.0", "http-proxy": "^1.18.1", - "openclaw": "2026.6.8", + "openclaw": "2026.6.11", "ws": "^8.19.0" }, "bin": { @@ -550,6 +550,19 @@ "node": ">=18" } }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "license": "ISC", + "optional": true, + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -1416,6 +1429,16 @@ "pnpm": ">=8" } }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "license": "BlueOak-1.0.0", + "optional": true, + "engines": { + "node": ">=18" + } + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -2416,6 +2439,29 @@ "node": ">= 0.6" } }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", + "optional": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "license": "MIT", + "optional": true, + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/mitt": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", @@ -2526,9 +2572,9 @@ } }, "node_modules/openclaw": { - "version": "2026.6.8", - "resolved": "https://registry.npmjs.org/openclaw/-/openclaw-2026.6.8.tgz", - "integrity": "sha512-iziR8fi69+ojrtX7FYYvTpkGcVnmyLpIhvchgt5LFkkdHVWw973XAAekKVZ3/xQJ5FG4NwgHkXL0LLTrgsNOSQ==", + "version": "2026.6.11", + "resolved": "https://registry.npmjs.org/openclaw/-/openclaw-2026.6.11.tgz", + "integrity": "sha512-T+P/g19IheeT1ckXMoPN61dYuE8vBF4MderI+kWkvpuFYxPkJxn8AXLpu9IXCnN9g36Acpm9+mMD/V+lsvOkyA==", "hasInstallScript": true, "hasShrinkwrap": true, "license": "MIT", @@ -2553,7 +2599,6 @@ "clawpdf": "0.3.0", "commander": "14.0.3", "croner": "10.0.1", - "cross-spawn": "7.0.6", "diff": "9.0.0", "dotenv": "17.4.2", "express": "5.2.1", @@ -2577,12 +2622,12 @@ "qrcode": "1.5.4", "quickjs-wasi": "3.0.0", "rastermill": "0.3.1", - "tar": "7.5.15", + "tar": "7.5.16", "tree-sitter-bash": "0.25.1", "tslog": "4.10.2", "typebox": "1.1.39", "typescript": "6.0.3", - "undici": "8.3.0", + "undici": "8.5.0", "web-push": "3.6.7", "web-tree-sitter": "0.26.9", "ws": "8.21.0", @@ -2966,6 +3011,69 @@ "undici": ">=8.3.0 <9" } }, + "node_modules/openclaw/node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/openclaw/node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/openclaw/node_modules/@protobufjs/codegen": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz", + "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==", + "license": "BSD-3-Clause" + }, + "node_modules/openclaw/node_modules/@protobufjs/eventemitter": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz", + "integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==", + "license": "BSD-3-Clause" + }, + "node_modules/openclaw/node_modules/@protobufjs/fetch": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz", + "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1" + } + }, + "node_modules/openclaw/node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/openclaw/node_modules/@protobufjs/inquire": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.2.tgz", + "integrity": "sha512-pa0vFRuws4wkvaXKK1uXZMAwAX4/t8ANaJo45iw/oQHNQ9q5xUzwgFmVJGXiga2BeN+zpX7Vf9vmsiIa2J+MUw==", + "license": "BSD-3-Clause" + }, + "node_modules/openclaw/node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/openclaw/node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/openclaw/node_modules/@protobufjs/utf8": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.1.tgz", + "integrity": "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==", + "license": "BSD-3-Clause" + }, "node_modules/openclaw/node_modules/@silvia-odwyer/photon-node": { "version": "0.3.4", "resolved": "https://registry.npmjs.org/@silvia-odwyer/photon-node/-/photon-node-0.3.4.tgz", @@ -3001,6 +3109,15 @@ "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", "license": "MIT" }, + "node_modules/openclaw/node_modules/@types/node": { + "version": "25.9.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.1.tgz", + "integrity": "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==", + "license": "MIT", + "dependencies": { + "undici-types": ">=7.24.0 <7.24.7" + } + }, "node_modules/openclaw/node_modules/@types/retry": { "version": "0.12.5", "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.5.tgz", @@ -5006,12 +5123,23 @@ } }, "node_modules/openclaw/node_modules/protobufjs": { - "version": "8.4.1", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-8.4.1.tgz", - "integrity": "sha512-oXf2UgIty8jnwfN4yvL1x79VLhL5uiKjZJbSGXGCIUmHmItTP4eS/UIlWDCeNx3seg+ujfn9vDlPMSrsh7wO+Q==", + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.3.tgz", + "integrity": "sha512-+k0vdJKNdW+Vu+dYe8tZA/VvQb6XKNWexC6URwBFXxNnjLJz9nQJCemGyNgRAWD+B7+nGNc9qMPGwcD7s4nzUw==", "hasInstallScript": true, "license": "BSD-3-Clause", "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.5", + "@protobufjs/eventemitter": "^1.1.1", + "@protobufjs/fetch": "^1.1.1", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.2", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.1", + "@types/node": ">=13.7.0", "long": "^5.3.2" }, "engines": { @@ -5623,9 +5751,9 @@ } }, "node_modules/openclaw/node_modules/tar": { - "version": "7.5.15", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.15.tgz", - "integrity": "sha512-dzGK0boVlC4W5QFuQN1EFSl3bIDYsk7Tj40U6eIBnK2k/8ml7TZ5agbI5j5+qnoVcAA+rNtBml8SEiLxZpNqRQ==", + "version": "7.5.16", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.16.tgz", + "integrity": "sha512-56adEpPMouktRlBLXiaYFFzZ/3+JXa8P9n7WbR+ibIjtviN55mEaOkiysCnPnWm+7kkui1Dn8J9l+g6zV8731w==", "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/fs-minipass": "^4.0.0", @@ -5783,15 +5911,21 @@ } }, "node_modules/openclaw/node_modules/undici": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-8.3.0.tgz", - "integrity": "sha512-TkUDgb6tl7KOGZ+7e8E3d2FYgUQgF6z5YypqjWmixVQSQERFcVrVg0ySADm2LVLRh5ljAaHTCR5Fmz3Q34rB7Q==", + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-8.5.0.tgz", + "integrity": "sha512-xamtWoB1EshgjpmlXd7GGm2VfdDtw1+rD8uhry8pSNW3If6S8E0m2T2+orSKeZXEn/aPJMviCpDBA65WJt8zhg==", "license": "MIT", "peer": true, "engines": { "node": ">=22.19.0" } }, + "node_modules/openclaw/node_modules/undici-types": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", + "license": "MIT" + }, "node_modules/openclaw/node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -5915,6 +6049,7 @@ "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", "license": "MIT", + "peer": true, "engines": { "node": ">=10.0.0" }, @@ -7790,6 +7925,16 @@ "optional": true } } + }, + "node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "license": "BlueOak-1.0.0", + "optional": true, + "engines": { + "node": ">=18" + } } } } diff --git a/package.json b/package.json index 659c30b4..cd3fb72b 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "dependencies": { "express": "^4.21.0", "http-proxy": "^1.18.1", - "openclaw": "2026.6.8", + "openclaw": "2026.6.11", "ws": "^8.19.0" }, "devDependencies": { From 32849d26612d5e771da8e571b6eaf5435618c355 Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Sun, 19 Jul 2026 08:00:52 -0700 Subject: [PATCH 08/11] Bump openclaw to 2026.7.1-2. Current npm latest: the 2026.7.1 stabilization train (gateway crash-loop recovery, fail-closed container upgrades, Sonnet 5 / Mythos 5 catalog) plus the out-of-band 2026.7.1-2 hotfix for npm plugin updates (#108336). Tarball diff against 2026.7.1 verified clean (same deps, no new install scripts) since the hotfix skipped the upstream release pipeline. OpenClaw 2026.7.1 moved normalizeThinkLevel into a thinking.shared-*.js chunk that the sentinel scan never covered; add a fallback scan so the thinking API resolves on both old and new dist layouts. Co-Authored-By: Claude Fable 5 --- lib/server/openclaw-thinking.js | 35 ++- package-lock.json | 367 ++++++++++++++++---------------- package.json | 2 +- 3 files changed, 218 insertions(+), 186 deletions(-) diff --git a/lib/server/openclaw-thinking.js b/lib/server/openclaw-thinking.js index a4d01588..905d3950 100644 --- a/lib/server/openclaw-thinking.js +++ b/lib/server/openclaw-thinking.js @@ -40,16 +40,41 @@ const isNormalizeThinkLevel = (candidate) => { } }; -const resolveNormalizeThinkLevel = (mod) => { +const findNormalizeThinkLevel = (mod) => { const candidates = [ mod.normalizeThinkLevel, mod.f, mod.p, ...Object.values(mod), ]; - const match = candidates.find(isNormalizeThinkLevel); - if (!match) throw new Error("OpenClaw normalizeThinkLevel export not found"); - return match; + return candidates.find(isNormalizeThinkLevel) || null; +}; + +let normalizeThinkLevelPromise = null; + +// OpenClaw 2026.7.1+ moved normalizeThinkLevel out of the sentinel chunk +// into a thinking.shared-*.js chunk, which the sentinel scan does not cover. +const resolveNormalizeThinkLevel = async (mod) => { + const direct = findNormalizeThinkLevel(mod); + if (direct) return direct; + if (!normalizeThinkLevelPromise) { + normalizeThinkLevelPromise = (async () => { + const distDir = resolveOpenclawDistDir(); + for (const name of fs.readdirSync(distDir)) { + if (!/^thinking\.shared-.*\.js$/.test(name)) continue; + const sharedMod = await import( + pathToFileURL(path.join(distDir, name)).href + ); + const match = findNormalizeThinkLevel(sharedMod); + if (match) return match; + } + throw new Error("OpenClaw normalizeThinkLevel export not found"); + })(); + normalizeThinkLevelPromise.catch(() => { + normalizeThinkLevelPromise = null; + }); + } + return normalizeThinkLevelPromise; }; const splitModelKey = (modelKey = "") => { @@ -80,7 +105,7 @@ const resolveThinkingApi = async () => { return { listThinkingLevelOptions: mod.listThinkingLevelOptions || mod.i, resolveThinkingDefaultForModel: mod.resolveThinkingDefaultForModel || mod.s, - normalizeThinkLevel: resolveNormalizeThinkLevel(mod), + normalizeThinkLevel: await resolveNormalizeThinkLevel(mod), }; }; diff --git a/package-lock.json b/package-lock.json index f7081a37..fc47c979 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "dependencies": { "express": "^4.21.0", "http-proxy": "^1.18.1", - "openclaw": "2026.6.11", + "openclaw": "2026.7.1-2", "ws": "^8.19.0" }, "bin": { @@ -550,19 +550,6 @@ "node": ">=18" } }, - "node_modules/@isaacs/fs-minipass": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", - "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", - "license": "ISC", - "optional": true, - "dependencies": { - "minipass": "^7.0.4" - }, - "engines": { - "node": ">=18.0.0" - } - }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -1429,16 +1416,6 @@ "pnpm": ">=8" } }, - "node_modules/chownr": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", - "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", - "license": "BlueOak-1.0.0", - "optional": true, - "engines": { - "node": ">=18" - } - }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -2439,29 +2416,6 @@ "node": ">= 0.6" } }, - "node_modules/minipass": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", - "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", - "license": "BlueOak-1.0.0", - "optional": true, - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/minizlib": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", - "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", - "license": "MIT", - "optional": true, - "dependencies": { - "minipass": "^7.1.2" - }, - "engines": { - "node": ">= 18" - } - }, "node_modules/mitt": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", @@ -2572,39 +2526,41 @@ } }, "node_modules/openclaw": { - "version": "2026.6.11", - "resolved": "https://registry.npmjs.org/openclaw/-/openclaw-2026.6.11.tgz", - "integrity": "sha512-T+P/g19IheeT1ckXMoPN61dYuE8vBF4MderI+kWkvpuFYxPkJxn8AXLpu9IXCnN9g36Acpm9+mMD/V+lsvOkyA==", + "version": "2026.7.1-2", + "resolved": "https://registry.npmjs.org/openclaw/-/openclaw-2026.7.1-2.tgz", + "integrity": "sha512-ycF3yPcbjN6bUPeaUx6Mh6vze1hQWoD3CT/wWcmD7a8xaHHHRUaAlaq+lFxMHf1ssEgODVAwjlzYqp2twkYZ7g==", "hasInstallScript": true, "hasShrinkwrap": true, "license": "MIT", "dependencies": { - "@agentclientprotocol/sdk": "0.22.1", - "@anthropic-ai/sdk": "0.100.1", - "@clack/core": "1.3.1", - "@clack/prompts": "1.4.0", - "@earendil-works/pi-tui": "0.78.0", - "@google/genai": "2.7.0", + "@agentclientprotocol/sdk": "1.1.0", + "@anthropic-ai/sdk": "0.109.1", + "@clack/core": "1.4.2", + "@clack/prompts": "1.6.0", + "@earendil-works/pi-tui": "0.80.3", + "@google/genai": "2.10.0", "@grammyjs/runner": "2.0.3", "@grammyjs/transformer-throttler": "1.2.1", "@homebridge/ciao": "1.3.9", "@lydell/node-pty": "1.2.0-beta.12", - "@mistralai/mistralai": "2.2.5", + "@mistralai/mistralai": "2.4.0", "@modelcontextprotocol/sdk": "1.29.0", "@mozilla/readability": "0.6.0", - "@openclaw/fs-safe": "0.3.0", + "@openclaw/ai": "2026.7.1-2", + "@openclaw/fs-safe": "0.4.1", "@openclaw/proxyline": "0.3.3", + "@silvia-odwyer/photon-node": "0.3.4", "chalk": "5.6.2", "chokidar": "5.0.0", "clawpdf": "0.3.0", - "commander": "14.0.3", + "commander": "15.0.0", "croner": "10.0.1", "diff": "9.0.0", "dotenv": "17.4.2", "express": "5.2.1", "file-type": "22.0.1", "glob": "13.0.6", - "grammy": "1.43.0", + "grammy": "1.44.0", "highlight.js": "11.11.1", "hosted-git-info": "10.1.1", "ignore": "7.0.5", @@ -2615,21 +2571,21 @@ "linkedom": "0.18.12", "minimatch": "10.2.5", "node-edge-tts": "1.2.10", - "openai": "6.39.1", + "openai": "6.45.0", "partial-json": "0.1.7", - "playwright-core": "1.60.0", + "playwright-core": "1.61.1", "proper-lockfile": "4.1.2", "qrcode": "1.5.4", - "quickjs-wasi": "3.0.0", + "quickjs-wasi": "3.0.2", "rastermill": "0.3.1", - "tar": "7.5.16", + "tar": "7.5.19", "tree-sitter-bash": "0.25.1", "tslog": "4.10.2", - "typebox": "1.1.39", + "typebox": "1.3.3", "typescript": "6.0.3", "undici": "8.5.0", "web-push": "3.6.7", - "web-tree-sitter": "0.26.9", + "web-tree-sitter": "0.26.10", "ws": "8.21.0", "yaml": "2.9.0", "zod": "4.4.3" @@ -2638,25 +2594,25 @@ "openclaw": "openclaw.mjs" }, "engines": { - "node": ">=22.19.0" + "node": ">=22.22.3 <23 || >=24.15.0 <25 || >=25.9.0" }, "optionalDependencies": { "sqlite-vec": "0.1.9" } }, "node_modules/openclaw/node_modules/@agentclientprotocol/sdk": { - "version": "0.22.1", - "resolved": "https://registry.npmjs.org/@agentclientprotocol/sdk/-/sdk-0.22.1.tgz", - "integrity": "sha512-DfqXtl/8gO9NImq094MTaCXEU2vkhh6v7q/kT+9UjZxUqj8hYaya2OjLVIqn16MzNHcXEpShTR2RIauLSYeDQQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@agentclientprotocol/sdk/-/sdk-1.1.0.tgz", + "integrity": "sha512-NT2KqphUJ3w6EksUL51ZhJgIYgq/ZLGcBPkyMKgRSO5PMVwe9DnKKX+Htnvk6KHh6dUuh34UHK4gKp+4te1Mdg==", "license": "Apache-2.0", "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "node_modules/openclaw/node_modules/@anthropic-ai/sdk": { - "version": "0.100.1", - "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.100.1.tgz", - "integrity": "sha512-RANcEe7LpiLczkKGOwoXOTuFdPhuubS0i4xaAKOMpcqc55YO0mukgxppV7eygx3DXNjxWT6RYOLPyOy0aIAmwg==", + "version": "0.109.1", + "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.109.1.tgz", + "integrity": "sha512-q9OnEKLr5H9nxSuXdgDgJhxfYMiE+AaUEBze2Gk91UcaaLnsN+Lx5fbCYywiqurU/APLdwv23x03Wm6WN3EBsg==", "license": "MIT", "dependencies": { "json-schema-to-ts": "^3.1.1", @@ -2694,9 +2650,9 @@ } }, "node_modules/openclaw/node_modules/@clack/core": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@clack/core/-/core-1.3.1.tgz", - "integrity": "sha512-fT1qHVGAag4IEkrupZ6lRRbNCs1vS9P01KB/sG8zKgvUztbYtFBtQpjSITNwooDZ83tpsPzP0mRNs1/KVszCRA==", + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@clack/core/-/core-1.4.2.tgz", + "integrity": "sha512-0Ty/1Gfm+Kb07sXcuESjyKfwEhSy4Ns1AgeEisHb/bDY5fWme0tTeTkU14T1Gmcs17YIjB/teiDe4uaCghbYqQ==", "license": "MIT", "dependencies": { "fast-wrap-ansi": "^0.2.0", @@ -2707,12 +2663,12 @@ } }, "node_modules/openclaw/node_modules/@clack/prompts": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-1.4.0.tgz", - "integrity": "sha512-S0My7XPGIgpRWMDG8uRqalbgT+a6FmCUdOW+HaIOVVpUPHOb7RrpvjTjiODadKp06fsrVDJZlIzc6yCTp4AnxA==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-1.6.0.tgz", + "integrity": "sha512-EYlRokl8szrP9Z25qT5aepMdBjzBvHF9ZEhzIiUBc9guz/T31EqRgvD0QSgZcpE93xiwrr+OkB4nz0BZyF6fSA==", "license": "MIT", "dependencies": { - "@clack/core": "1.3.1", + "@clack/core": "1.4.2", "fast-string-width": "^3.0.2", "fast-wrap-ansi": "^0.2.0", "sisteransi": "^1.0.5" @@ -2722,22 +2678,22 @@ } }, "node_modules/openclaw/node_modules/@earendil-works/pi-tui": { - "version": "0.78.0", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.78.0.tgz", - "integrity": "sha512-3a705FnsVVUhAyceShNB3kS2rpxcxLcx+hqB0u6MMMpHwQGbW+m++MqA6r7eOzq/8FLx5e3vDh38h/SVTk2qzw==", + "version": "0.80.3", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.80.3.tgz", + "integrity": "sha512-2BJI6qwRQfnM0Q7seL1+SbacU/jRRjBnN7Hu3n9BjAn7/s5FaBNnvdD1qBQYRsFTHfjqMaDsjYqanPyqwXj99w==", "license": "MIT", "dependencies": { "get-east-asian-width": "1.6.0", - "marked": "15.0.12" + "marked": "18.0.5" }, "engines": { "node": ">=22.19.0" } }, "node_modules/openclaw/node_modules/@google/genai": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/@google/genai/-/genai-2.7.0.tgz", - "integrity": "sha512-tv0DRtcndt2oEhBYy+5mA0TaXH98+L1Gt0AP9unBfH7DP20KhB7+O3QqAN1Lz+laMARGTHS7BFQSNpLbl4gm1g==", + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@google/genai/-/genai-2.10.0.tgz", + "integrity": "sha512-e4cFxj3tiuMtsgOT4G9c1hXyGJhg7/Buj7VVeBacRY3fRtkRZZ59Q3nuVp2xbq8BGQXLXCDB253qMhklMOeUDg==", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { @@ -2789,9 +2745,9 @@ } }, "node_modules/openclaw/node_modules/@grammyjs/types": { - "version": "3.27.3", - "resolved": "https://registry.npmjs.org/@grammyjs/types/-/types-3.27.3.tgz", - "integrity": "sha512-yUKMLliGsGbnxu96YUJ7km7B0zy4PzeH/Jvti5705R/LeKDMqkDV4DckMSt+OrliWQpTwQljHE0QLol5zgxBkg==", + "version": "3.28.0", + "resolved": "https://registry.npmjs.org/@grammyjs/types/-/types-3.28.0.tgz", + "integrity": "sha512-4JvXCdxRZHCje0M4gHzLwtB4bLno3WD28xd8CNfk4POWIu73BFnSvGeW6OQ5gPem4eYTEwkD9yDaXssixl6tMQ==", "license": "MIT" }, "node_modules/openclaw/node_modules/@homebridge/ciao": { @@ -2926,14 +2882,23 @@ ] }, "node_modules/openclaw/node_modules/@mistralai/mistralai": { - "version": "2.2.5", - "resolved": "https://registry.npmjs.org/@mistralai/mistralai/-/mistralai-2.2.5.tgz", - "integrity": "sha512-ATbWzKkNzNAZ+gtw9MI/c/ULTMG80tKUiRNIbQFfg4OP0uEZZpTfXZeBCNfs5Dq0uqMQ/tQWc4o6RRJQtMrpDA==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@mistralai/mistralai/-/mistralai-2.4.0.tgz", + "integrity": "sha512-t6hCx242MTGolB76CI+17jDtPIe/bzLsMdUTMMoMn9Qo1h02N2G5jQYHmKDGU3X//OgR2wvngTD7tO6tPp5poQ==", "license": "Apache-2.0", "dependencies": { + "@opentelemetry/semantic-conventions": "^1.40.0", "ws": "^8.18.0", "zod": "^3.25.0 || ^4.0.0", "zod-to-json-schema": "^3.25.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.9.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + } } }, "node_modules/openclaw/node_modules/@modelcontextprotocol/sdk": { @@ -2986,17 +2951,34 @@ "node": ">=14.0.0" } }, + "node_modules/openclaw/node_modules/@openclaw/ai": { + "version": "2026.7.1-2", + "resolved": "https://registry.npmjs.org/@openclaw/ai/-/ai-2026.7.1-2.tgz", + "integrity": "sha512-st+NH0cxlQqdbEur//yYqM7WlYBjeEBnop3cztJTSCONKjv6LNoGguI9cH65asZG94FdM/39z857isRGPnZvEw==", + "license": "MIT", + "dependencies": { + "@anthropic-ai/sdk": "0.109.1", + "@google/genai": "2.10.0", + "@mistralai/mistralai": "2.4.0", + "openai": "6.45.0", + "partial-json": "0.1.7", + "typebox": "1.3.3" + }, + "engines": { + "node": ">=22.19.0" + } + }, "node_modules/openclaw/node_modules/@openclaw/fs-safe": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@openclaw/fs-safe/-/fs-safe-0.3.0.tgz", - "integrity": "sha512-uIBE441CIt1kIURoP9qRGKZ8LkGyfD9ZzeESjwAd29ZPWtghws/5GR3Pjb67jKdcJHP1I6roNXcvnhzAU7lHlA==", + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@openclaw/fs-safe/-/fs-safe-0.4.1.tgz", + "integrity": "sha512-hQi+BxO10KdRFlYUot1syC+hTaUnGeQNdqX5kwkKJig8CFq1tKsYJLPm+zkiiGsSKOprPAquQl/txejEhpKPgg==", "license": "MIT", "engines": { - "node": ">=20.11" + "node": ">=22" }, "optionalDependencies": { "jszip": "^3.10.1", - "tar": "7.5.13" + "tar": "7.5.19" } }, "node_modules/openclaw/node_modules/@openclaw/proxyline": { @@ -3011,6 +2993,15 @@ "undici": ">=8.3.0 <9" } }, + "node_modules/openclaw/node_modules/@opentelemetry/semantic-conventions": { + "version": "1.41.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.41.1.tgz", + "integrity": "sha512-/UhIkaZgPutTFmQ7RnIJGgDXZmtEJ7Dvi86xNTFWcnRxVRNk/aotsqDJYeEvDP+FSMB2SdW+pQzNMcWP0rwuNA==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, "node_modules/openclaw/node_modules/@protobufjs/aspromise": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", @@ -3110,12 +3101,12 @@ "license": "MIT" }, "node_modules/openclaw/node_modules/@types/node": { - "version": "25.9.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.1.tgz", - "integrity": "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==", + "version": "26.1.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.0.tgz", + "integrity": "sha512-O0A1G3xPGy4w7AgQdAQYUlQ+BKk2Oovw8eRpofyp5KdBZULnbe+WqaOVNrm705SHphCiG4XHsACrSmPu1f+Kgw==", "license": "MIT", "dependencies": { - "undici-types": ">=7.24.0 <7.24.7" + "undici-types": "~8.3.0" } }, "node_modules/openclaw/node_modules/@types/retry": { @@ -3266,26 +3257,26 @@ } }, "node_modules/openclaw/node_modules/bn.js": { - "version": "4.12.3", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz", - "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==", + "version": "4.12.4", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.4.tgz", + "integrity": "sha512-njR1b+ixG2ufvL9Zn9JGneW+b5GV6jqpYyPPpg4QVt723b5kJPGUczkUyWEH9BwEA74UakJZ43I4FDLBF7ci0g==", "license": "MIT" }, "node_modules/openclaw/node_modules/body-parser": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", - "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", + "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", "license": "MIT", "dependencies": { "bytes": "^3.1.2", - "content-type": "^1.0.5", + "content-type": "^2.0.0", "debug": "^4.4.3", - "http-errors": "^2.0.0", - "iconv-lite": "^0.7.0", + "http-errors": "^2.0.1", + "iconv-lite": "^0.7.2", "on-finished": "^2.4.1", - "qs": "^6.14.1", - "raw-body": "^3.0.1", - "type-is": "^2.0.1" + "qs": "^6.15.2", + "raw-body": "^3.0.2", + "type-is": "^2.1.0" }, "engines": { "node": ">=18" @@ -3295,6 +3286,19 @@ "url": "https://opencollective.com/express" } }, + "node_modules/openclaw/node_modules/body-parser/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/openclaw/node_modules/boolbase": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", @@ -3308,9 +3312,9 @@ "license": "MIT" }, "node_modules/openclaw/node_modules/brace-expansion": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", - "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", + "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" @@ -3459,12 +3463,12 @@ "license": "MIT" }, "node_modules/openclaw/node_modules/commander": { - "version": "14.0.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", - "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-15.0.0.tgz", + "integrity": "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg==", "license": "MIT", "engines": { - "node": ">=20" + "node": ">=22.12.0" } }, "node_modules/openclaw/node_modules/content-disposition": { @@ -4201,9 +4205,9 @@ } }, "node_modules/openclaw/node_modules/google-auth-library": { - "version": "10.7.0", - "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.7.0.tgz", - "integrity": "sha512-QpTAbNJ36TliZLx3TTtahR8HG0hN9RllL1e3FymOvQSIKK8JmgV58H924ub2wa2DsS3ANjjP1Aw1N+Ramc8hqQ==", + "version": "10.9.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.9.0.tgz", + "integrity": "sha512-xtvUqvINPhTaBm7nXqlYPcrMHJPm1lCNdSovxnKKhTm+4JsvQ+KGVYJViLoH9Yxu8w+T0Qv5HubzYT9BLrppJg==", "license": "Apache-2.0", "dependencies": { "base64-js": "^1.3.0", @@ -4245,13 +4249,13 @@ "license": "ISC" }, "node_modules/openclaw/node_modules/grammy": { - "version": "1.43.0", - "resolved": "https://registry.npmjs.org/grammy/-/grammy-1.43.0.tgz", - "integrity": "sha512-7dYm06A945mXuIk/5HUlSjeyIYChW8vCEiU2dkOKKqJJzwAWxTkCc91Eqbz7TgODh2rtFFKWI/fekowWHOkmjQ==", + "version": "1.44.0", + "resolved": "https://registry.npmjs.org/grammy/-/grammy-1.44.0.tgz", + "integrity": "sha512-gGVykS5+c5f1tPV97LuU6IDRMawE2NzpwM9pNz58HQ35IZXDnYL3VOLvNzYognPSeBIOSzQXRu5w96V0aY8y8A==", "license": "MIT", "peer": true, "dependencies": { - "@grammyjs/types": "3.27.3", + "@grammyjs/types": "3.28.0", "abort-controller": "^3.0.0", "debug": "^4.4.3", "node-fetch": "^2.7.0" @@ -4683,15 +4687,15 @@ } }, "node_modules/openclaw/node_modules/marked": { - "version": "15.0.12", - "resolved": "https://registry.npmjs.org/marked/-/marked-15.0.12.tgz", - "integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==", + "version": "18.0.5", + "resolved": "https://registry.npmjs.org/marked/-/marked-18.0.5.tgz", + "integrity": "sha512-S6GcvALHg6K4ohtu4E7x0a1AqhAjp6cV8KhLSyN9qVapnzJkusVBxZRcIU9AeYsbe6P1hKDusSbEOzGyyuce6w==", "license": "MIT", "bin": { "marked": "bin/marked.js" }, "engines": { - "node": ">= 18" + "node": ">= 20" } }, "node_modules/openclaw/node_modules/math-intrinsics": { @@ -4816,9 +4820,9 @@ } }, "node_modules/openclaw/node_modules/node-addon-api": { - "version": "8.8.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.8.0.tgz", - "integrity": "sha512-c5Ko1fZJIJmzhFIkhRN76WTq+fC6tWnGy9CXA0fA+XygsWZmEwG8vmbkNqxMyoaa0Tin4djul49NzdVcJJcjeA==", + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.9.0.tgz", + "integrity": "sha512-ekZMeaaIzSQTSpr7X2X3iJM7lTzgnx8ahAG9pJfT/7+14mlEM8ZYQ9cgCDvSSRbReFK0oHli3WrZdCiRsgAT9Q==", "license": "MIT", "engines": { "node": "^18 || ^20 || >= 21" @@ -4932,18 +4936,27 @@ } }, "node_modules/openclaw/node_modules/openai": { - "version": "6.39.1", - "resolved": "https://registry.npmjs.org/openai/-/openai-6.39.1.tgz", - "integrity": "sha512-z3dO9fEWOXBzlXynVb/xZ/tujzUjFWQWn3C0n0mw6Vo0zJTbEkaN4b2cLWjhJ6haJQx8LlREoafHRl+Gu/Hl+A==", + "version": "6.45.0", + "resolved": "https://registry.npmjs.org/openai/-/openai-6.45.0.tgz", + "integrity": "sha512-5DQVNErssk0afNpTTHUm/qZPU4iKR9OYdNid8Ib4puq4gHNNvGWZht2zY4h9a8JMF949Ik6m8gQutllVPbjdnw==", "license": "Apache-2.0", - "bin": { - "openai": "bin/cli" - }, "peerDependencies": { + "@aws-sdk/credential-provider-node": ">=3.972.0 <4", + "@smithy/hash-node": ">=4.3.0 <5", + "@smithy/signature-v4": ">=5.4.0 <6", "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "peerDependenciesMeta": { + "@aws-sdk/credential-provider-node": { + "optional": true + }, + "@smithy/hash-node": { + "optional": true + }, + "@smithy/signature-v4": { + "optional": true + }, "ws": { "optional": true }, @@ -5076,9 +5089,9 @@ } }, "node_modules/openclaw/node_modules/playwright-core": { - "version": "1.60.0", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.60.0.tgz", - "integrity": "sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==", + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.1.tgz", + "integrity": "sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==", "license": "Apache-2.0", "bin": { "playwright-core": "cli.js" @@ -5258,18 +5271,22 @@ } }, "node_modules/openclaw/node_modules/quickjs-wasi": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/quickjs-wasi/-/quickjs-wasi-3.0.0.tgz", - "integrity": "sha512-X7ouKC4ZVf9bXQ8rsE7+L6TeBbesejAJH61x16xRaGAQGfBHHRcniWgzJZZVtHc8rS9yVsY+Tvk8/usAosg4bg==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/quickjs-wasi/-/quickjs-wasi-3.0.2.tgz", + "integrity": "sha512-SyfPzlrfz67/kv0SogmQgW4c2I1klkLcbvj9Y2gc1h7+VylmvuGevFljLXibGKajKJKiJV29d4S6FQLA6Sc80A==", "license": "MIT" }, "node_modules/openclaw/node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz", + "integrity": "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==", "license": "MIT", "engines": { "node": ">= 0.6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/openclaw/node_modules/rastermill": { @@ -5493,14 +5510,14 @@ } }, "node_modules/openclaw/node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" }, @@ -5751,9 +5768,9 @@ } }, "node_modules/openclaw/node_modules/tar": { - "version": "7.5.16", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.16.tgz", - "integrity": "sha512-56adEpPMouktRlBLXiaYFFzZ/3+JXa8P9n7WbR+ibIjtviN55mEaOkiysCnPnWm+7kkui1Dn8J9l+g6zV8731w==", + "version": "7.5.19", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.19.tgz", + "integrity": "sha512-4LeEWl96twnS2Q7Bz4MGqgazLqO+hJN63GZxXoIqh1T3VweYD997gbU1ItNsQafqqXTXd5WFyFdReLtwvRBNiw==", "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/fs-minipass": "^4.0.0", @@ -5874,9 +5891,9 @@ } }, "node_modules/openclaw/node_modules/typebox": { - "version": "1.1.39", - "resolved": "https://registry.npmjs.org/typebox/-/typebox-1.1.39.tgz", - "integrity": "sha512-vj0afVtOfLQvv0GR0VxVagYxsXN64btL7Z9XoaG0ZggH3mruMMkOO6hXdgMsjCY3shZgEvooAWVeznQVs5c43w==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/typebox/-/typebox-1.3.3.tgz", + "integrity": "sha512-URXGUE31PJDQC+PtRMJeLdF4kmmOdFoVPikPCtV2oOIhUpNpppEdIz7W8bH8cFYPYHdDpaRvqwdegMTmHliudg==", "license": "MIT" }, "node_modules/openclaw/node_modules/typescript": { @@ -5921,9 +5938,9 @@ } }, "node_modules/openclaw/node_modules/undici-types": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", - "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", + "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", "license": "MIT" }, "node_modules/openclaw/node_modules/unpipe": { @@ -5979,9 +5996,9 @@ } }, "node_modules/openclaw/node_modules/web-tree-sitter": { - "version": "0.26.9", - "resolved": "https://registry.npmjs.org/web-tree-sitter/-/web-tree-sitter-0.26.9.tgz", - "integrity": "sha512-YJwSHANl6XFgeEjB8nitgj0qZYt5gkIesJ4w2srS2wcLB4GUa4xcOkM0YaMsU6WNR53YVIkDSY7Ej4pf3IXtCA==", + "version": "0.26.10", + "resolved": "https://registry.npmjs.org/web-tree-sitter/-/web-tree-sitter-0.26.10.tgz", + "integrity": "sha512-vengBGYS7FpAerkR3o04oBL4L8MkVmjawK50AFBu7v0HZBkmF9ZavPGKoXLSSmRhp7T/YgsJ7joAS3yAxHPEqQ==", "license": "MIT" }, "node_modules/openclaw/node_modules/webidl-conversions": { @@ -6100,9 +6117,9 @@ } }, "node_modules/openclaw/node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", "license": "MIT", "dependencies": { "cliui": "^8.0.1", @@ -7925,16 +7942,6 @@ "optional": true } } - }, - "node_modules/yallist": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", - "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", - "license": "BlueOak-1.0.0", - "optional": true, - "engines": { - "node": ">=18" - } } } } diff --git a/package.json b/package.json index cd3fb72b..644c6f8f 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "dependencies": { "express": "^4.21.0", "http-proxy": "^1.18.1", - "openclaw": "2026.6.11", + "openclaw": "2026.7.1-2", "ws": "^8.19.0" }, "devDependencies": { From 6aef4851b3230b5ef9ac79629b8f7d5e71a46607 Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Sun, 19 Jul 2026 13:44:02 -0700 Subject: [PATCH 09/11] Harden watchdog for OpenClaw 2026.7.1 gateway lifecycle contract. OpenClaw 2026.7.1 introduced two lifecycle behaviors the watchdog was blind to: 1. Fatal config errors now exit with EX_CONFIG (78), meaning "do not restart until the config is fixed". The watchdog previously treated 78 as a generic crash and restart-looped against that contract. It now enters a config_error lifecycle, attempts at most one auto-repair per incident (doctor --fix can genuinely fix config), never counts 78 toward the crash-loop window, and pauses with a notification otherwise. Failing health probes no longer demote the config_error state. 2. The gateway's own crash-loop breaker can boot into control-plane-safe mode: /health stays green while channel autostart is suppressed, which previously read as fully healthy. The watchdog now probes /readyz after each healthy check, surfaces safeMode/suppressedChannels in status, notifies once per suppression set, and exposes POST /api/watchdog/resume-channels which issues the gateway's channels.start override per suppressed channel. Covered by unit tests and an e2e suite driving the real watchdog through the real routes against a stateful fake gateway. Co-Authored-By: Claude Fable 5 --- lib/server.js | 1 + lib/server/routes/watchdog.js | 13 + lib/server/watchdog.js | 210 ++++++++++++++ tests/server/routes-watchdog.test.js | 37 +++ .../watchdog-gateway-hardening.e2e.test.js | 225 ++++++++++++++ tests/server/watchdog.test.js | 274 ++++++++++++++++++ 6 files changed, 760 insertions(+) create mode 100644 tests/server/watchdog-gateway-hardening.e2e.test.js diff --git a/lib/server.js b/lib/server.js index 4832f44e..eac7a748 100644 --- a/lib/server.js +++ b/lib/server.js @@ -289,6 +289,7 @@ const watchdog = createWatchdog({ reloadEnv, resolveSetupUrl, resolveGatewayHealthUrl: () => `${getGatewayUrl()}/health`, + resolveGatewayReadyzUrl: () => `${getGatewayUrl()}/readyz`, }); const watchdogTerminal = createWatchdogTerminalService({ cwd: constants.OPENCLAW_DIR, diff --git a/lib/server/routes/watchdog.js b/lib/server/routes/watchdog.js index 45b1f078..23ddfef1 100644 --- a/lib/server/routes/watchdog.js +++ b/lib/server/routes/watchdog.js @@ -51,6 +51,19 @@ const registerWatchdogRoutes = ({ } }); + app.post("/api/watchdog/resume-channels", requireAuth, async (req, res) => { + try { + const result = await watchdog.resumeChannels(); + if (result?.skipped) { + res.status(409).json({ ok: false, error: result.reason, result }); + return; + } + res.json({ ok: !!result?.ok, result }); + } catch (err) { + res.status(500).json({ ok: false, error: err.message }); + } + }); + app.get("/api/watchdog/settings", requireAuth, (req, res) => { try { res.json({ ok: true, settings: watchdog.getSettings() }); diff --git a/lib/server/watchdog.js b/lib/server/watchdog.js index 7855726f..2d2b4c9a 100644 --- a/lib/server/watchdog.js +++ b/lib/server/watchdog.js @@ -11,6 +11,16 @@ const kHealthStartupGraceMs = 30 * 1000; const kBootstrapHealthCheckMs = 5 * 1000; const kExpectedRestartWindowMs = 15 * 1000; const kGatewayHealthTimeoutMs = 5 * 1000; +// OpenClaw 2026.7.1+ exits with EX_CONFIG (78, sysexits.h) on fatal +// configuration errors. The contract is "do not restart until the config is +// fixed" — restarting blindly recreates the restart storm the gateway is +// trying to prevent. +const kOpenclawConfigErrorExitCode = 78; + +const shellEscapeArg = (value) => { + const safeValue = String(value || ""); + return `'${safeValue.replace(/'/g, `'\\''`)}'`; +}; const isTruthy = (value) => ["1", "true", "yes", "on"].includes( @@ -42,6 +52,7 @@ const createWatchdog = ({ reloadEnv, resolveSetupUrl, resolveGatewayHealthUrl = () => "", + resolveGatewayReadyzUrl = () => "", }) => { const state = { lifecycle: "stopped", @@ -63,6 +74,10 @@ const createWatchdog = ({ pendingRecoveryNoticeSource: "", awaitingAutoRepairRecovery: false, startupConsecutiveHealthFailures: 0, + configErrorRepairAttempted: false, + safeMode: false, + suppressedChannels: [], + safeModeNotifiedKey: "", }; let healthTimer = null; let bootstrapHealthTimer = null; @@ -336,6 +351,138 @@ const createWatchdog = ({ } }; + // OpenClaw 2026.7.1+ can boot into control-plane-safe mode after its own + // crash-loop breaker trips: /health stays green while channel autostart is + // suppressed. /readyz reports the suppressed channels. + const probeGatewayReadiness = async () => { + const readyzUrl = String(resolveGatewayReadyzUrl() || "").trim(); + if (!readyzUrl) { + return { ok: false, reason: "gateway readyz URL unavailable" }; + } + const controller = new AbortController(); + const timeoutId = setTimeout( + () => controller.abort(), + kGatewayHealthTimeoutMs, + ); + try { + const response = await fetch(readyzUrl, { + method: "GET", + headers: { Accept: "application/json" }, + signal: controller.signal, + }); + const rawBody = await response.text(); + let parsedBody = null; + try { + parsedBody = rawBody ? JSON.parse(rawBody) : null; + } catch {} + if (!response.ok || !parsedBody || typeof parsedBody !== "object") { + return { + ok: false, + reason: `gateway readyz returned HTTP ${response.status}`, + }; + } + return { + ok: true, + ready: parsedBody.ready !== false, + failing: Array.isArray(parsedBody.failing) ? parsedBody.failing : [], + suppressed: Array.isArray(parsedBody.suppressed) + ? parsedBody.suppressed.map((entry) => String(entry || "")).filter(Boolean) + : [], + }; + } catch (error) { + const message = + error?.name === "AbortError" + ? `gateway readyz timed out after ${kGatewayHealthTimeoutMs}ms` + : error?.message || "gateway readyz request failed"; + return { ok: false, reason: message }; + } finally { + clearTimeout(timeoutId); + } + }; + + const clearSafeModeState = () => { + state.safeMode = false; + state.suppressedChannels = []; + state.safeModeNotifiedKey = ""; + }; + + const evaluateChannelSuppression = async (source, correlationId) => { + const readiness = await probeGatewayReadiness(); + if (!readiness.ok) return; + const suppressed = readiness.suppressed; + if (suppressed.length > 0) { + const notifiedKey = suppressed.slice().sort().join(","); + const changed = state.safeModeNotifiedKey !== notifiedKey; + state.safeMode = true; + state.suppressedChannels = suppressed; + if (!changed) return; + state.safeModeNotifiedKey = notifiedKey; + logEvent( + "safe_mode", + source, + "failed", + { suppressed, failing: readiness.failing }, + correlationId, + ); + await notify( + [ + "🐺 *AlphaClaw Watchdog*", + withViewLogsSuffix( + "🟡 Gateway is in safe mode — channel autostart suppressed by its crash-loop breaker", + ), + `Suppressed channels: ${suppressed.join(", ")}`, + "The gateway is up but these channels are not delivering messages. Resume them from the Watchdog tab once the crash cause is fixed.", + ].join("\n"), + correlationId, + "crash", + ); + return; + } + if (state.safeMode) { + clearSafeModeState(); + logEvent("safe_mode", source, "ok", { recovered: true }, correlationId); + await notify( + [ + "🐺 *AlphaClaw Watchdog*", + withViewLogsSuffix("🟢 Gateway safe mode cleared — channels resumed"), + ].join("\n"), + correlationId, + "recovery", + ); + } + }; + + const resumeChannels = async () => { + const correlationId = createCorrelationId(); + const channels = [...state.suppressedChannels]; + if (channels.length === 0) { + return { ok: false, skipped: true, reason: "no_suppressed_channels" }; + } + const results = []; + for (const channel of channels) { + const params = JSON.stringify({ channel }); + const result = await clawCmd( + `gateway call channels.start --params ${shellEscapeArg(params)}`, + { quiet: true }, + ); + const ok = !!result?.ok; + results.push({ channel, ok, stderr: ok ? undefined : result?.stderr }); + logEvent( + "safe_mode_resume", + "manual", + ok ? "ok" : "failed", + { channel, stderr: result?.stderr || null }, + correlationId, + ); + } + await runHealthCheck({ + source: "resume_channels", + allowAutoRepair: false, + allowDuringOperation: true, + }); + return { ok: results.every((entry) => entry.ok), results }; + }; + const updateSettings = ({ autoRepair, notificationsEnabled } = {}) => { const hasAutoRepair = typeof autoRepair === "boolean"; const hasNotificationsEnabled = typeof notificationsEnabled === "boolean"; @@ -523,6 +670,7 @@ const createWatchdog = ({ state.repairAttempts = 0; state.crashRecoveryActive = false; state.awaitingAutoRepairRecovery = false; + state.configErrorRepairAttempted = false; if (shouldNotifyRecovery) { logEvent( "recovery", @@ -558,6 +706,7 @@ const createWatchdog = ({ parsed.details || { ok: true }, correlationId, ); + await evaluateChannelSuppression(source, correlationId); return true; } if (restartWindowActive) { @@ -579,6 +728,20 @@ const createWatchdog = ({ return false; } + if (state.lifecycle === "config_error") { + // The gateway is intentionally stopped on a fatal config error; a + // failing probe adds no signal and must not demote the state or + // trigger repairs beyond the one-shot config-error attempt. + logEvent( + "health_check", + source, + "ok", + { reason: parsed.reason, skipped: true, configErrorActive: true }, + correlationId, + ); + return false; + } + const withinStartupGrace = !!state.gatewayStartedAt && Date.now() - state.gatewayStartedAt < kHealthStartupGraceMs && @@ -687,6 +850,7 @@ const createWatchdog = ({ } = {}) => { const correlationId = createCorrelationId(); clearDegradedHealthCheckTimer(); + clearSafeModeState(); if (expectedExit && (code == null || code === 0)) { state.lifecycle = "restarting"; state.health = "unknown"; @@ -727,6 +891,46 @@ const createWatchdog = ({ return; } + if (code === kOpenclawConfigErrorExitCode) { + // EX_CONFIG: the gateway refuses to run until the config is fixed. + // Restarting in a loop would fight that contract, so attempt at most + // one auto-repair per incident and otherwise stay stopped. + const willAttemptRepair = + state.autoRepair && !state.configErrorRepairAttempted; + state.lifecycle = "config_error"; + state.health = "unhealthy"; + state.uptimeStartedAt = null; + state.crashRecoveryActive = false; + openIncident("gateway_config_error"); + logEvent( + "config_error", + "exit_event", + "failed", + { code, signal: signal ?? null, stderrTail }, + correlationId, + ); + void notifyOncePerIncident( + willAttemptRepair ? "config_error_detected" : "config_error_paused", + [ + "🐺 *AlphaClaw Watchdog*", + withViewLogsSuffix( + "🔴 Gateway stopped: fatal configuration error (exit 78)", + ), + "OpenClaw refuses to start until its configuration is fixed.", + willAttemptRepair + ? "Attempting one-time auto-repair (doctor --fix)..." + : "Auto-restart paused; manual action required.", + ].join("\n"), + correlationId, + "crash", + ); + if (willAttemptRepair) { + state.configErrorRepairAttempted = true; + void runRepair({ source: "config_error", correlationId }); + } + return; + } + state.lifecycle = "crashed"; state.health = "unhealthy"; state.uptimeStartedAt = null; @@ -800,6 +1004,7 @@ const createWatchdog = ({ const onExpectedRestart = () => { clearDegradedHealthCheckTimer(); + clearSafeModeState(); state.lifecycle = "restarting"; state.health = "unknown"; state.uptimeStartedAt = null; @@ -811,6 +1016,7 @@ const createWatchdog = ({ const triggerRepair = async () => { const correlationId = createCorrelationId(); + state.configErrorRepairAttempted = false; return runRepair({ source: "manual", correlationId, @@ -843,6 +1049,7 @@ const createWatchdog = ({ state.startupConsecutiveHealthFailures = 0; state.awaitingAutoRepairRecovery = false; state.pendingRecoveryNoticeSource = ""; + clearSafeModeState(); closeIncident(); }; @@ -863,6 +1070,8 @@ const createWatchdog = ({ crashLoopWindowMs: kWatchdogCrashLoopWindowMs, operationInProgress: state.operationInProgress, gatewayPid: state.gatewayPid, + safeMode: state.safeMode, + suppressedChannels: [...state.suppressedChannels], }; }; @@ -871,6 +1080,7 @@ const createWatchdog = ({ getSettings, updateSettings, triggerRepair, + resumeChannels, onExpectedRestart, onGatewayExit, onGatewayLaunch, diff --git a/tests/server/routes-watchdog.test.js b/tests/server/routes-watchdog.test.js index b718089b..29461224 100644 --- a/tests/server/routes-watchdog.test.js +++ b/tests/server/routes-watchdog.test.js @@ -8,6 +8,10 @@ const createDeps = () => { const watchdog = { getStatus: vi.fn(() => ({ lifecycle: "running", health: "healthy" })), triggerRepair: vi.fn(async () => ({ ok: true })), + resumeChannels: vi.fn(async () => ({ + ok: true, + results: [{ channel: "telegram", ok: true }], + })), getSettings: vi.fn(() => ({ autoRepair: true, notificationsEnabled: true })), updateSettings: vi.fn(({ autoRepair }) => ({ autoRepair, notificationsEnabled: true })), }; @@ -98,6 +102,39 @@ describe("server/routes/watchdog", () => { }); }); + it("resumes suppressed channels on POST /api/watchdog/resume-channels", async () => { + const deps = createDeps(); + const app = createApp(deps); + + const res = await request(app).post("/api/watchdog/resume-channels"); + + expect(res.status).toBe(200); + expect(deps.watchdog.resumeChannels).toHaveBeenCalledTimes(1); + expect(res.body).toEqual({ + ok: true, + result: { + ok: true, + results: [{ channel: "telegram", ok: true }], + }, + }); + }); + + it("returns 409 when resume-channels has nothing to resume", async () => { + const deps = createDeps(); + deps.watchdog.resumeChannels.mockResolvedValue({ + ok: false, + skipped: true, + reason: "no_suppressed_channels", + }); + const app = createApp(deps); + + const res = await request(app).post("/api/watchdog/resume-channels"); + + expect(res.status).toBe(409); + expect(res.body.ok).toBe(false); + expect(res.body.error).toBe("no_suppressed_channels"); + }); + it("returns 400 when updateSettings throws", async () => { const deps = createDeps(); deps.watchdog.updateSettings.mockImplementation(() => { diff --git a/tests/server/watchdog-gateway-hardening.e2e.test.js b/tests/server/watchdog-gateway-hardening.e2e.test.js new file mode 100644 index 00000000..a26664f8 --- /dev/null +++ b/tests/server/watchdog-gateway-hardening.e2e.test.js @@ -0,0 +1,225 @@ +const express = require("express"); +const request = require("supertest"); + +const { createWatchdog } = require("../../lib/server/watchdog"); +const { registerWatchdogRoutes } = require("../../lib/server/routes/watchdog"); + +// End-to-end coverage for the OpenClaw 2026.7.1+ gateway-lifecycle contract: +// exit code 78 (EX_CONFIG) fatal config errors, and control-plane-safe mode +// where /health stays green while /readyz reports suppressed channels. Uses +// the real watchdog wired into the real routes against a stateful fake +// gateway. + +const flushMicrotasks = async () => + new Promise((resolve) => { + setImmediate(resolve); + }); + +const kOriginalAutoRepair = process.env.WATCHDOG_AUTO_REPAIR; +const kOriginalNotificationsDisabled = + process.env.WATCHDOG_NOTIFICATIONS_DISABLED; +const kOriginalFetch = global.fetch; + +const createFakeGateway = () => ({ + healthy: true, + suppressed: [], +}); + +const createStack = ({ autoRepair = false, fakeGateway } = {}) => { + process.env.WATCHDOG_AUTO_REPAIR = autoRepair ? "true" : "false"; + process.env.WATCHDOG_NOTIFICATIONS_DISABLED = "false"; + + const gateway = fakeGateway || createFakeGateway(); + + global.fetch = vi.fn(async (url) => { + if (!gateway.healthy) throw new Error("gateway unavailable"); + if (String(url).includes("/readyz")) { + return { + ok: true, + status: 200, + text: async () => + JSON.stringify({ + ready: true, + failing: [], + ...(gateway.suppressed.length > 0 + ? { suppressed: gateway.suppressed } + : {}), + }), + }; + } + return { + ok: true, + status: 200, + text: async () => JSON.stringify({ ok: true, status: "live" }), + }; + }); + + const clawCmd = vi.fn(async (command) => { + if (command.startsWith("gateway call channels.start")) { + const paramsJson = command.replace( + /^gateway call channels\.start --params '(.*)'$/, + "$1", + ); + const params = JSON.parse(paramsJson); + gateway.suppressed = gateway.suppressed.filter( + (channel) => channel !== params.channel, + ); + return { ok: true, stdout: "{}" }; + } + if (command === "doctor --fix --yes") { + gateway.healthy = true; + return { ok: true, stdout: "fixed" }; + } + return { ok: true, stdout: "" }; + }); + + const launchGatewayProcess = vi.fn(() => ({ pid: 4242 })); + const insertWatchdogEvent = vi.fn(); + const notifier = { notify: vi.fn(async () => ({ ok: true })) }; + + const watchdog = createWatchdog({ + clawCmd, + launchGatewayProcess, + insertWatchdogEvent, + notifier, + readEnvFile: vi.fn(() => []), + writeEnvFile: vi.fn(), + reloadEnv: vi.fn(), + resolveSetupUrl: () => "https://setup.example.com", + resolveGatewayHealthUrl: () => "http://127.0.0.1:18789/health", + resolveGatewayReadyzUrl: () => "http://127.0.0.1:18789/readyz", + }); + + const app = express(); + app.use(express.json()); + registerWatchdogRoutes({ + app, + requireAuth: (req, res, next) => next(), + watchdog, + watchdogNotifier: notifier, + getRecentEvents: vi.fn(() => []), + readLogTail: vi.fn(() => ""), + watchdogTerminal: {}, + }); + + return { + app, + watchdog, + gateway, + clawCmd, + launchGatewayProcess, + insertWatchdogEvent, + notifier, + }; +}; + +describe("server/watchdog gateway hardening (e2e)", () => { + afterEach(() => { + if (kOriginalAutoRepair == null) { + delete process.env.WATCHDOG_AUTO_REPAIR; + } else { + process.env.WATCHDOG_AUTO_REPAIR = kOriginalAutoRepair; + } + if (kOriginalNotificationsDisabled == null) { + delete process.env.WATCHDOG_NOTIFICATIONS_DISABLED; + } else { + process.env.WATCHDOG_NOTIFICATIONS_DISABLED = + kOriginalNotificationsDisabled; + } + if (kOriginalFetch == null) { + delete global.fetch; + } else { + global.fetch = kOriginalFetch; + } + vi.useRealTimers(); + vi.restoreAllMocks(); + }); + + it("surfaces safe mode in the status API and recovers via resume-channels", async () => { + const { app, watchdog, gateway, clawCmd } = createStack(); + gateway.suppressed = ["telegram", "discord"]; + + watchdog.start(); + await flushMicrotasks(); + await flushMicrotasks(); + + const statusRes = await request(app).get("/api/watchdog/status"); + expect(statusRes.status).toBe(200); + expect(statusRes.body.status).toEqual( + expect.objectContaining({ + lifecycle: "running", + health: "healthy", + safeMode: true, + suppressedChannels: ["telegram", "discord"], + }), + ); + + const resumeRes = await request(app).post("/api/watchdog/resume-channels"); + expect(resumeRes.status).toBe(200); + expect(resumeRes.body.ok).toBe(true); + expect(resumeRes.body.result.results).toEqual([ + { channel: "telegram", ok: true }, + { channel: "discord", ok: true }, + ]); + expect(clawCmd).toHaveBeenCalledWith( + `gateway call channels.start --params '{"channel":"telegram"}'`, + { quiet: true }, + ); + expect(clawCmd).toHaveBeenCalledWith( + `gateway call channels.start --params '{"channel":"discord"}'`, + { quiet: true }, + ); + expect(gateway.suppressed).toEqual([]); + + const clearedRes = await request(app).get("/api/watchdog/status"); + expect(clearedRes.body.status).toEqual( + expect.objectContaining({ safeMode: false, suppressedChannels: [] }), + ); + + const idempotentRes = await request(app).post( + "/api/watchdog/resume-channels", + ); + expect(idempotentRes.status).toBe(409); + expect(idempotentRes.body.error).toBe("no_suppressed_channels"); + watchdog.stop(); + }); + + it("reports config_error on exit 78 and recovers through manual repair", async () => { + const { app, watchdog, gateway, clawCmd, launchGatewayProcess } = + createStack({ autoRepair: false }); + gateway.healthy = false; + + watchdog.onGatewayLaunch({ startedAt: Date.now(), pid: 1234 }); + watchdog.onGatewayExit({ + code: 78, + expectedExit: false, + stderrTail: ["fatal configuration error"], + }); + await flushMicrotasks(); + + const statusRes = await request(app).get("/api/watchdog/status"); + expect(statusRes.body.status).toEqual( + expect.objectContaining({ + lifecycle: "config_error", + health: "unhealthy", + crashCountInWindow: 0, + }), + ); + // The EX_CONFIG contract: no automatic relaunch without repair. + expect(launchGatewayProcess).not.toHaveBeenCalled(); + + const repairRes = await request(app).post("/api/watchdog/repair"); + expect(repairRes.status).toBe(200); + expect(repairRes.body.ok).toBe(true); + expect(clawCmd).toHaveBeenCalledWith("doctor --fix --yes", { + quiet: true, + }); + expect(launchGatewayProcess).toHaveBeenCalledTimes(1); + + const recoveredRes = await request(app).get("/api/watchdog/status"); + expect(recoveredRes.body.status).toEqual( + expect.objectContaining({ lifecycle: "running", health: "healthy" }), + ); + watchdog.stop(); + }); +}); diff --git a/tests/server/watchdog.test.js b/tests/server/watchdog.test.js index e6a8018b..8b7b90e9 100644 --- a/tests/server/watchdog.test.js +++ b/tests/server/watchdog.test.js @@ -15,6 +15,7 @@ const createHarness = ({ clawCmdImpl, resolveSetupUrl = () => "https://setup.example.com", resolveGatewayHealthUrl = () => "http://127.0.0.1:18789/health", + resolveGatewayReadyzUrl = () => "", fetchImpl = async () => ({ ok: true, status: 200, @@ -49,6 +50,7 @@ const createHarness = ({ reloadEnv, resolveSetupUrl, resolveGatewayHealthUrl, + resolveGatewayReadyzUrl, }); return { @@ -646,4 +648,276 @@ describe("server/watchdog", () => { notificationsEnabled: false, }); }); + + it("treats exit code 78 as a fatal config error without crash-loop restarts", async () => { + const { watchdog, insertWatchdogEvent, notifier, launchGatewayProcess, clawCmd } = + createHarness({ + autoRepair: false, + fetchImpl: async () => { + throw new Error("gateway unavailable"); + }, + }); + + watchdog.onGatewayLaunch({ startedAt: Date.now(), pid: 1234 }); + watchdog.onGatewayExit({ code: 78, expectedExit: false, stderrTail: ["invalid config"] }); + watchdog.onGatewayExit({ code: 78, expectedExit: false }); + watchdog.onGatewayExit({ code: 78, expectedExit: false }); + await flushMicrotasks(); + + expect(watchdog.getStatus()).toEqual( + expect.objectContaining({ + lifecycle: "config_error", + health: "unhealthy", + crashCountInWindow: 0, + }), + ); + expect(launchGatewayProcess).not.toHaveBeenCalled(); + expect(clawCmd).not.toHaveBeenCalledWith("doctor --fix --yes", { quiet: true }); + expect(insertWatchdogEvent).toHaveBeenCalledWith( + expect.objectContaining({ + eventType: "config_error", + source: "exit_event", + status: "failed", + details: expect.objectContaining({ code: 78 }), + }), + ); + expect( + notifier.notify.mock.calls.some( + (call) => + String(call?.[0] || "").includes("fatal configuration error") && + String(call?.[0] || "").includes("Auto-restart paused"), + ), + ).toBe(true); + }); + + it("attempts exactly one auto-repair per config-error incident", async () => { + const doctorCalls = []; + const { watchdog, notifier, launchGatewayProcess } = createHarness({ + autoRepair: true, + clawCmdImpl: async (command) => { + if (command === "doctor --fix --yes") { + doctorCalls.push(command); + return { ok: true, stdout: "fixed" }; + } + return { ok: true, stdout: "" }; + }, + fetchImpl: async () => { + throw new Error("still unhealthy"); + }, + }); + + watchdog.onGatewayExit({ code: 78, expectedExit: false }); + await flushMicrotasks(); + await flushMicrotasks(); + + expect(doctorCalls).toHaveLength(1); + expect(launchGatewayProcess).toHaveBeenCalledTimes(1); + expect( + notifier.notify.mock.calls.some((call) => + String(call?.[0] || "").includes("Attempting one-time auto-repair"), + ), + ).toBe(true); + + watchdog.onGatewayExit({ code: 78, expectedExit: false }); + await flushMicrotasks(); + await flushMicrotasks(); + + expect(doctorCalls).toHaveLength(1); + expect(launchGatewayProcess).toHaveBeenCalledTimes(1); + expect(watchdog.getStatus().lifecycle).toBe("config_error"); + }); + + it("re-arms config-error auto-repair after a verified healthy recovery", async () => { + const doctorCalls = []; + const { watchdog } = createHarness({ + autoRepair: true, + clawCmdImpl: async (command) => { + if (command === "doctor --fix --yes") { + doctorCalls.push(command); + return { ok: true, stdout: "fixed" }; + } + return { ok: true, stdout: "" }; + }, + }); + + watchdog.onGatewayExit({ code: 78, expectedExit: false }); + await flushMicrotasks(); + await flushMicrotasks(); + expect(doctorCalls).toHaveLength(1); + + // Repair verification saw a healthy gateway, so a later config error is a + // new incident and earns a fresh repair attempt. + watchdog.onGatewayExit({ code: 78, expectedExit: false }); + await flushMicrotasks(); + await flushMicrotasks(); + expect(doctorCalls).toHaveLength(2); + }); + + const buildSafeModeFetch = (gatewayState) => async (url) => { + if (String(url).includes("/readyz")) { + return { + ok: true, + status: 200, + text: async () => + JSON.stringify({ + ready: true, + failing: [], + ...(gatewayState.suppressed.length > 0 + ? { suppressed: gatewayState.suppressed } + : {}), + }), + }; + } + return { + ok: true, + status: 200, + text: async () => JSON.stringify({ ok: true, status: "live" }), + }; + }; + + it("detects gateway safe mode from readyz and notifies once", async () => { + vi.useFakeTimers(); + const gatewayState = { suppressed: ["telegram", "discord"] }; + const { watchdog, insertWatchdogEvent, notifier } = createHarness({ + autoRepair: false, + resolveGatewayReadyzUrl: () => "http://127.0.0.1:18789/readyz", + fetchImpl: buildSafeModeFetch(gatewayState), + }); + + watchdog.start(); + await vi.advanceTimersByTimeAsync(10); + + expect(watchdog.getStatus()).toEqual( + expect.objectContaining({ + lifecycle: "running", + health: "healthy", + safeMode: true, + suppressedChannels: ["telegram", "discord"], + }), + ); + expect(insertWatchdogEvent).toHaveBeenCalledWith( + expect.objectContaining({ + eventType: "safe_mode", + status: "failed", + details: expect.objectContaining({ + suppressed: ["telegram", "discord"], + }), + }), + ); + const safeModeNotices = () => + notifier.notify.mock.calls.filter((call) => + String(call?.[0] || "").includes("safe mode"), + ); + expect(safeModeNotices()).toHaveLength(1); + + // Subsequent checks with unchanged suppression must not re-notify. + await vi.advanceTimersByTimeAsync(120_000); + expect(safeModeNotices()).toHaveLength(1); + watchdog.stop(); + }); + + it("clears safe mode and notifies recovery when suppression ends", async () => { + vi.useFakeTimers(); + const gatewayState = { suppressed: ["telegram"] }; + const { watchdog, insertWatchdogEvent, notifier } = createHarness({ + autoRepair: false, + resolveGatewayReadyzUrl: () => "http://127.0.0.1:18789/readyz", + fetchImpl: buildSafeModeFetch(gatewayState), + }); + + watchdog.start(); + await vi.advanceTimersByTimeAsync(10); + expect(watchdog.getStatus().safeMode).toBe(true); + + gatewayState.suppressed = []; + await vi.advanceTimersByTimeAsync(120_000); + + expect(watchdog.getStatus()).toEqual( + expect.objectContaining({ safeMode: false, suppressedChannels: [] }), + ); + expect(insertWatchdogEvent).toHaveBeenCalledWith( + expect.objectContaining({ + eventType: "safe_mode", + status: "ok", + details: expect.objectContaining({ recovered: true }), + }), + ); + expect( + notifier.notify.mock.calls.some((call) => + String(call?.[0] || "").includes("safe mode cleared"), + ), + ).toBe(true); + watchdog.stop(); + }); + + it("resumeChannels issues channels.start for each suppressed channel", async () => { + vi.useFakeTimers(); + const gatewayState = { suppressed: ["telegram", "discord"] }; + const startCalls = []; + const { watchdog } = createHarness({ + autoRepair: false, + resolveGatewayReadyzUrl: () => "http://127.0.0.1:18789/readyz", + fetchImpl: buildSafeModeFetch(gatewayState), + clawCmdImpl: async (command) => { + if (command.startsWith("gateway call channels.start")) { + startCalls.push(command); + return { ok: true, stdout: "{}" }; + } + return { ok: true, stdout: "" }; + }, + }); + + watchdog.start(); + await vi.advanceTimersByTimeAsync(10); + expect(watchdog.getStatus().safeMode).toBe(true); + + gatewayState.suppressed = []; + const resultPromise = watchdog.resumeChannels(); + await vi.advanceTimersByTimeAsync(10); + const result = await resultPromise; + + expect(result.ok).toBe(true); + expect(startCalls).toEqual([ + `gateway call channels.start --params '{"channel":"telegram"}'`, + `gateway call channels.start --params '{"channel":"discord"}'`, + ]); + expect(watchdog.getStatus()).toEqual( + expect.objectContaining({ safeMode: false, suppressedChannels: [] }), + ); + watchdog.stop(); + }); + + it("resumeChannels skips when no channels are suppressed", async () => { + const { watchdog, clawCmd } = createHarness({ autoRepair: false }); + + const result = await watchdog.resumeChannels(); + + expect(result).toEqual({ + ok: false, + skipped: true, + reason: "no_suppressed_channels", + }); + expect(clawCmd).not.toHaveBeenCalled(); + }); + + it("clears safe-mode status when the gateway exits", async () => { + vi.useFakeTimers(); + const gatewayState = { suppressed: ["telegram"] }; + const { watchdog } = createHarness({ + autoRepair: false, + resolveGatewayReadyzUrl: () => "http://127.0.0.1:18789/readyz", + fetchImpl: buildSafeModeFetch(gatewayState), + }); + + watchdog.start(); + await vi.advanceTimersByTimeAsync(10); + expect(watchdog.getStatus().safeMode).toBe(true); + + watchdog.onGatewayExit({ code: 1, expectedExit: false }); + + expect(watchdog.getStatus()).toEqual( + expect.objectContaining({ safeMode: false, suppressedChannels: [] }), + ); + watchdog.stop(); + }); }); From accfe42257882d971c1d0017264601dfb37a106b Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Sun, 19 Jul 2026 15:38:56 -0700 Subject: [PATCH 10/11] Raise test coverage from 71% to 99.63% lines (2,077 tests). Systematic coverage push across the whole codebase, test files only (no source changes): server core (chat-ws WebSocket bridge via a real ws fake-gateway, gateway process supervision), all route surfaces (telegram, nodes, webhooks, codex OAuth, browse with real temp git repos, models, agents, doctor, system, onboarding), db layers (usage/webhooks/auth/doctor/ watchdog sqlite), gmail/google integration, cron service, cost/version services, CLI helpers, and frontend lib/components (including preact component logic tested via a hooks harness without a DOM). Remaining uncovered lines (53 of 14,370) are documented dead code or v8-to-istanbul instrumentation artifacts, verified per file by adversarial inspection. Two small CLI scripts (lib/cli/git-sync.js, lib/scripts/migrate-openclaw-codex.js) are not loaded by any test and sit outside the measured set. Also set vitest testTimeout to 30s: openclaw's plugin-sdk pays a >5s first-import cost per worker, which flaked timing-sensitive files under parallel load. Full suite verified green four consecutive runs on Node 24.18.0. Co-Authored-By: Claude Fable 5 --- tests/bin/openclaw-config-restore.test.js | 142 ++ tests/frontend/api-cache.test.js | 128 ++ tests/frontend/api.test.js | 677 +++++++++ .../channel-create-operation-more.test.js | 134 ++ .../cron-calendar-helpers-more.test.js | 180 +++ tests/frontend/cron-helpers.test.js | 357 +++++ tests/frontend/doctor-helpers-more.test.js | 165 +++ tests/frontend/file-viewer-utils-more.test.js | 21 + tests/frontend/format.test.js | 148 ++ .../models-tab-model-picker-component.test.js | 286 ++++ .../models-tab-model-picker-helpers.test.js | 104 ++ tests/frontend/session-keys-more.test.js | 136 ++ tests/frontend/sse.test.js | 148 ++ .../syntax-highlighters-markdown.test.js | 77 + tests/frontend/syntax-highlighters.test.js | 279 ++++ tests/frontend/watchdog-helpers-more.test.js | 171 +++ ...watchdog-helpers-xterm-missing-fit.test.js | 19 + ...dog-helpers-xterm-missing-terminal.test.js | 16 + tests/frontend/welcome-config-more.test.js | 135 ++ tests/server/agents-service-coverage.test.js | 1198 ++++++++++++++++ tests/server/alphaclaw-version.test.js | 377 +++++ tests/server/auth-db.test.js | 41 + tests/server/auth-profiles.test.js | 230 +++ tests/server/chat-ws-bridge.test.js | 1051 ++++++++++++++ tests/server/chat-ws-missing-ws.test.js | 41 + tests/server/chat-ws-stale-socket.test.js | 177 +++ tests/server/commands.test.js | 90 ++ tests/server/cost-utils.test.js | 189 +++ tests/server/cron-service.test.js | 851 +++++++++++ tests/server/doctor-bootstrap-context.test.js | 90 ++ tests/server/doctor-normalize.test.js | 126 ++ tests/server/doctor-service.test.js | 321 +++++ tests/server/env.test.js | 90 ++ .../device-bootstrap-callable-loader.mjs | 12 + .../device-bootstrap-callable-stub.mjs | 8 + .../server/fixtures/device-bootstrap-stub.mjs | 4 + .../device-bootstrap-unavailable-loader.mjs | 14 + tests/server/gateway.test.js | 606 ++++++++ tests/server/git-runtime.test.js | 41 + tests/server/gmail-push.test.js | 318 +++++ tests/server/gmail-serve.test.js | 474 +++++++ tests/server/gmail-watch-service.test.js | 1075 ++++++++++++++ tests/server/gog-skill.test.js | 131 +- tests/server/google-state.test.js | 541 +++++++ tests/server/helpers.test.js | 182 ++- tests/server/import-applier.test.js | 512 +++++++ tests/server/import-scanner.test.js | 37 + tests/server/internal-files-migration.test.js | 79 ++ tests/server/login-throttle.test.js | 79 ++ .../model-catalog-cache-coverage.test.js | 198 +++ tests/server/onboarding-github.test.js | 511 +++++++ tests/server/onboarding-openclaw.test.js | 253 ++++ tests/server/onboarding-validation.test.js | 93 ++ tests/server/onboarding-workspace.test.js | 289 ++++ .../server/openclaw-thinking-coverage.test.js | 111 ++ tests/server/openclaw-version.test.js | 103 ++ tests/server/operation-events.test.js | 99 ++ tests/server/reconcile-codex-plugin.test.js | 67 + tests/server/routes-agents-coverage.test.js | 410 ++++++ tests/server/routes-auth.test.js | 69 +- tests/server/routes-browse-git-mocked.test.js | 189 +++ .../routes-browse-sqlite-unavailable.test.js | 44 + tests/server/routes-browse.test.js | 1239 +++++++++++++++++ tests/server/routes-codex-coverage.test.js | 394 ++++++ tests/server/routes-cron.test.js | 146 ++ tests/server/routes-doctor.test.js | 144 ++ tests/server/routes-models-coverage.test.js | 511 +++++++ tests/server/routes-nodes-coverage.test.js | 810 +++++++++++ .../routes-onboarding-scan-fallback.test.js | 120 ++ tests/server/routes-onboarding.test.js | 646 +++++++++ .../routes-pairings-device-approve.test.js | 82 ++ .../routes-pairings-device-bootstrap.test.js | 43 + tests/server/routes-pairings.test.js | 831 ++++++++++- tests/server/routes-proxy-coverage.test.js | 393 ++++++ tests/server/routes-system.test.js | 891 +++++++++++- tests/server/routes-telegram.test.js | 1025 +++++++++++++- tests/server/routes-usage.test.js | 123 ++ tests/server/routes-watchdog.test.js | 349 +++++ tests/server/routes-webhooks-coverage.test.js | 657 +++++++++ tests/server/secret-detector.test.js | 32 + tests/server/slack-api.test.js | 186 +++ tests/server/startup.test.js | 63 + tests/server/system-resources.test.js | 275 ++++ tests/server/telegram-api.test.js | 161 +++ tests/server/telegram-workspace.test.js | 161 +++ tests/server/topic-registry.test.js | 222 +++ tests/server/usage-db-coverage.test.js | 516 +++++++ tests/server/usage-tracker-config.test.js | 129 ++ tests/server/watchdog-notify.test.js | 262 ++++ tests/server/watchdog.test.js | 646 +++++++++ .../webhook-middleware-coverage.test.js | 555 ++++++++ tests/server/webhooks-coverage.test.js | 450 ++++++ tests/server/webhooks-db.test.js | 169 ++- tests/server/workspace-fingerprint.test.js | 96 ++ vitest.config.js | 3 + 95 files changed, 27048 insertions(+), 26 deletions(-) create mode 100644 tests/frontend/api-cache.test.js create mode 100644 tests/frontend/channel-create-operation-more.test.js create mode 100644 tests/frontend/cron-calendar-helpers-more.test.js create mode 100644 tests/frontend/doctor-helpers-more.test.js create mode 100644 tests/frontend/file-viewer-utils-more.test.js create mode 100644 tests/frontend/format.test.js create mode 100644 tests/frontend/models-tab-model-picker-component.test.js create mode 100644 tests/frontend/models-tab-model-picker-helpers.test.js create mode 100644 tests/frontend/session-keys-more.test.js create mode 100644 tests/frontend/sse.test.js create mode 100644 tests/frontend/syntax-highlighters-markdown.test.js create mode 100644 tests/frontend/watchdog-helpers-more.test.js create mode 100644 tests/frontend/watchdog-helpers-xterm-missing-fit.test.js create mode 100644 tests/frontend/watchdog-helpers-xterm-missing-terminal.test.js create mode 100644 tests/frontend/welcome-config-more.test.js create mode 100644 tests/server/agents-service-coverage.test.js create mode 100644 tests/server/chat-ws-bridge.test.js create mode 100644 tests/server/chat-ws-missing-ws.test.js create mode 100644 tests/server/chat-ws-stale-socket.test.js create mode 100644 tests/server/doctor-bootstrap-context.test.js create mode 100644 tests/server/fixtures/device-bootstrap-callable-loader.mjs create mode 100644 tests/server/fixtures/device-bootstrap-callable-stub.mjs create mode 100644 tests/server/fixtures/device-bootstrap-stub.mjs create mode 100644 tests/server/fixtures/device-bootstrap-unavailable-loader.mjs create mode 100644 tests/server/gmail-serve.test.js create mode 100644 tests/server/gmail-watch-service.test.js create mode 100644 tests/server/google-state.test.js create mode 100644 tests/server/model-catalog-cache-coverage.test.js create mode 100644 tests/server/openclaw-thinking-coverage.test.js create mode 100644 tests/server/routes-agents-coverage.test.js create mode 100644 tests/server/routes-browse-git-mocked.test.js create mode 100644 tests/server/routes-browse-sqlite-unavailable.test.js create mode 100644 tests/server/routes-codex-coverage.test.js create mode 100644 tests/server/routes-models-coverage.test.js create mode 100644 tests/server/routes-nodes-coverage.test.js create mode 100644 tests/server/routes-onboarding-scan-fallback.test.js create mode 100644 tests/server/routes-pairings-device-approve.test.js create mode 100644 tests/server/routes-pairings-device-bootstrap.test.js create mode 100644 tests/server/routes-proxy-coverage.test.js create mode 100644 tests/server/routes-webhooks-coverage.test.js create mode 100644 tests/server/system-resources.test.js create mode 100644 tests/server/telegram-api.test.js create mode 100644 tests/server/usage-db-coverage.test.js create mode 100644 tests/server/webhook-middleware-coverage.test.js create mode 100644 tests/server/webhooks-coverage.test.js create mode 100644 tests/server/workspace-fingerprint.test.js diff --git a/tests/bin/openclaw-config-restore.test.js b/tests/bin/openclaw-config-restore.test.js index 70e332db..951184ec 100644 --- a/tests/bin/openclaw-config-restore.test.js +++ b/tests/bin/openclaw-config-restore.test.js @@ -3,6 +3,7 @@ const os = require("os"); const path = require("path"); const { execSync } = require("child_process"); const { + ensureMainUpstream, restoreMissingOpenclawConfigFromRemote, } = require("../../lib/cli/openclaw-config-restore"); @@ -128,3 +129,144 @@ describe("restoreMissingOpenclawConfigFromRemote", () => { ); }); }); + +describe("restoreMissingOpenclawConfigFromRemote with injected modules", () => { + const makeFsModule = (overrides = {}) => ({ + existsSync: vi.fn(() => false), + writeFileSync: vi.fn(), + rmSync: vi.fn(), + ...overrides, + }); + const osModule = { tmpdir: () => "/fake-tmp" }; + + it("requires an openclawDir", () => { + expect(() => restoreMissingOpenclawConfigFromRemote({})).toThrow( + "openclawDir is required", + ); + }); + + it("writes a GIT_ASKPASS helper when a GitHub token exists and skips empty remote configs", () => { + const fsModule = makeFsModule(); + const logs = []; + const commands = []; + const execSyncImpl = vi.fn((command, options = {}) => { + commands.push({ command, options }); + if (command.includes("symbolic-ref")) return "release\n"; + if (command.startsWith("git show ")) return " \n"; + return ""; + }); + + const result = restoreMissingOpenclawConfigFromRemote({ + fsModule, + osModule, + execSyncImpl, + env: { GITHUB_TOKEN: "gh-token", PATH: "/usr/bin" }, + logger: { log: (message) => logs.push(message) }, + processId: 4242, + openclawDir: "/data/.openclaw", + }); + + expect(result).toEqual({ + restored: false, + skipped: true, + reason: "empty_remote", + branch: "release", + }); + const askPassPath = "/fake-tmp/alphaclaw-boot-git-askpass-4242.sh"; + expect(fsModule.writeFileSync).toHaveBeenCalledTimes(1); + expect(fsModule.writeFileSync).toHaveBeenCalledWith( + askPassPath, + expect.stringContaining("x-access-token"), + { mode: 0o700 }, + ); + const lsRemote = commands.find((entry) => entry.command.includes("ls-remote")); + expect(lsRemote.command).toContain("'release'"); + expect(lsRemote.options.env).toEqual( + expect.objectContaining({ + GITHUB_TOKEN: "gh-token", + GIT_TERMINAL_PROMPT: "0", + GIT_ASKPASS: askPassPath, + }), + ); + expect(fsModule.rmSync).toHaveBeenCalledWith(askPassPath, { force: true }); + expect(logs).toContain( + "[alphaclaw] Remote config restore skipped: remote config empty", + ); + }); + + it("falls back to the main branch and reports errors even when askpass cleanup fails", () => { + const fsModule = makeFsModule({ + rmSync: vi.fn(() => { + throw new Error("rm failed"); + }), + }); + const execSyncImpl = vi.fn(() => { + throw new Error("network down"); + }); + const logs = []; + + const result = restoreMissingOpenclawConfigFromRemote({ + fsModule, + osModule, + execSyncImpl, + env: { GITHUB_TOKEN: "gh-token" }, + logger: { log: (message) => logs.push(message) }, + processId: 7, + openclawDir: "/data/.openclaw", + }); + + expect(result).toMatchObject({ + restored: false, + skipped: true, + reason: "error", + branch: "main", + }); + expect(result.error).toBeInstanceOf(Error); + expect(fsModule.rmSync).toHaveBeenCalled(); + expect(logs.some((message) => message.includes("network down"))).toBe(true); + }); +}); + +describe("ensureMainUpstream", () => { + it("returns false when main already has an upstream", () => { + const execSyncImpl = vi.fn(() => ""); + + expect(ensureMainUpstream({ execSyncImpl, openclawDir: "/d" })).toBe(false); + expect(execSyncImpl).toHaveBeenCalledTimes(2); + }); + + it("sets origin/main as the upstream when missing", () => { + const gitEnv = { GIT_ASKPASS: "/tmp/askpass.sh" }; + const execSyncImpl = vi.fn((command) => { + if (command.includes("rev-parse")) throw new Error("no upstream"); + return ""; + }); + + expect(ensureMainUpstream({ execSyncImpl, openclawDir: "/d", gitEnv })).toBe( + true, + ); + expect(execSyncImpl).toHaveBeenCalledWith( + "git branch --set-upstream-to=origin/main main", + expect.objectContaining({ cwd: "/d", env: gitEnv }), + ); + }); + + it("returns false when the main branch does not exist", () => { + const execSyncImpl = vi.fn((command) => { + if (command.includes("show-ref")) throw new Error("no main branch"); + return ""; + }); + + expect(ensureMainUpstream({ execSyncImpl, openclawDir: "/d" })).toBe(false); + expect(execSyncImpl).toHaveBeenCalledTimes(1); + }); + + it("returns false when setting the upstream fails", () => { + const execSyncImpl = vi.fn((command) => { + if (command.includes("show-ref")) return ""; + throw new Error("cannot set upstream"); + }); + + expect(ensureMainUpstream({ execSyncImpl, openclawDir: "/d" })).toBe(false); + }); +}); diff --git a/tests/frontend/api-cache.test.js b/tests/frontend/api-cache.test.js new file mode 100644 index 00000000..2be0c4dd --- /dev/null +++ b/tests/frontend/api-cache.test.js @@ -0,0 +1,128 @@ +import { describe, expect, it, vi } from "vitest"; +import { + cachedFetch, + getCached, + invalidateCache, + setCached, +} from "../../lib/public/js/lib/api-cache.js"; + +// The cache is module-level state, so every test uses its own key. + +describe("frontend/api-cache", () => { + it("gets, sets, and invalidates cache entries", () => { + expect(getCached("")).toBe(null); + expect(getCached("never-set")).toBe(null); + + expect(setCached("basic-key", { a: 1 })).toEqual({ a: 1 }); + expect(getCached("basic-key")).toEqual({ a: 1 }); + + // Empty keys are no-ops that pass data through. + expect(setCached("", "ignored")).toBe("ignored"); + expect(getCached("")).toBe(null); + + invalidateCache("basic-key"); + expect(getCached("basic-key")).toBe(null); + expect(invalidateCache("")).toBeUndefined(); + }); + + it("bypasses caching for empty keys or non-function fetchers", async () => { + const fetcher = vi.fn(async () => "direct"); + await expect(cachedFetch("", fetcher)).resolves.toBe("direct"); + expect(fetcher).toHaveBeenCalledTimes(1); + + await expect(cachedFetch("bad-fetcher-key", null)).rejects.toThrow(); + }); + + it("returns fresh entries without refetching", async () => { + setCached("fresh-key", "cached-value"); + const fetcher = vi.fn(async () => "new-value"); + await expect( + cachedFetch("fresh-key", fetcher, { maxAgeMs: 60000 }), + ).resolves.toBe("cached-value"); + expect(fetcher).not.toHaveBeenCalled(); + }); + + it("serves stale data while revalidating in the background once", async () => { + setCached("swr-key", "stale-value"); + let resolveFetch; + const fetcher = vi.fn( + () => + new Promise((resolve) => { + resolveFetch = resolve; + }), + ); + const onRevalidate = vi.fn(); + + await expect( + cachedFetch("swr-key", fetcher, { maxAgeMs: 0, onRevalidate }), + ).resolves.toBe("stale-value"); + + // A second stale read while the revalidation is in flight does not + // schedule another fetch. + await expect( + cachedFetch("swr-key", fetcher, { maxAgeMs: 0 }), + ).resolves.toBe("stale-value"); + await vi.waitFor(() => expect(fetcher).toHaveBeenCalledTimes(1)); + + resolveFetch("fresh-value"); + await vi.waitFor(() => + expect(onRevalidate).toHaveBeenCalledWith("fresh-value"), + ); + expect(getCached("swr-key")).toBe("fresh-value"); + }); + + it("revalidates stale data without an onRevalidate callback", async () => { + setCached("swr-silent-key", "old"); + const fetcher = vi.fn(async () => "new"); + + await expect( + cachedFetch("swr-silent-key", fetcher, { maxAgeMs: 0 }), + ).resolves.toBe("old"); + await vi.waitFor(() => expect(getCached("swr-silent-key")).toBe("new")); + }); + + it("refetches stale entries when staleWhileRevalidate is off", async () => { + setCached("no-swr-key", "old"); + const fetcher = vi.fn(async () => "new"); + + await expect( + cachedFetch("no-swr-key", fetcher, { + maxAgeMs: 0, + staleWhileRevalidate: false, + }), + ).resolves.toBe("new"); + expect(getCached("no-swr-key")).toBe("new"); + }); + + it("forces a refetch past a fresh cache entry", async () => { + setCached("force-key", "old"); + const fetcher = vi.fn(async () => "forced"); + + await expect( + cachedFetch("force-key", fetcher, { maxAgeMs: 60000, force: true }), + ).resolves.toBe("forced"); + expect(fetcher).toHaveBeenCalledTimes(1); + }); + + it("deduplicates concurrent uncached fetches", async () => { + let resolveFetch; + const fetcher = vi.fn( + () => + new Promise((resolve) => { + resolveFetch = resolve; + }), + ); + const otherFetcher = vi.fn(async () => "should-not-run"); + + const firstPromise = cachedFetch("inflight-key", fetcher); + const secondPromise = cachedFetch("inflight-key", otherFetcher); + await vi.waitFor(() => expect(fetcher).toHaveBeenCalledTimes(1)); + resolveFetch("shared-result"); + + await expect(firstPromise).resolves.toBe("shared-result"); + await expect(secondPromise).resolves.toBe("shared-result"); + expect(fetcher).toHaveBeenCalledTimes(1); + expect(otherFetcher).not.toHaveBeenCalled(); + expect(getCached("inflight-key")).toBe("shared-result"); + }); +}); diff --git a/tests/frontend/api.test.js b/tests/frontend/api.test.js index 5f982b06..b5c0853b 100644 --- a/tests/frontend/api.test.js +++ b/tests/frontend/api.test.js @@ -656,3 +656,680 @@ describe("frontend/api", () => { expect(result).toEqual({ ok: true }); }); }); + +const mockTextResponse = (status, text) => ({ + status, + ok: status >= 200 && status < 300, + text: async () => text, +}); + +class FakeEventSource { + static instances = []; + + constructor(url, options) { + this.url = url; + this.options = options; + this.listeners = new Map(); + this.closed = false; + this.onopen = undefined; + this.onerror = undefined; + FakeEventSource.instances.push(this); + } + + addEventListener(type, handler) { + const handlers = this.listeners.get(type) || []; + handlers.push(handler); + this.listeners.set(type, handlers); + } + + removeEventListener(type, handler) { + const handlers = (this.listeners.get(type) || []).filter( + (entry) => entry !== handler, + ); + this.listeners.set(type, handlers); + } + + close() { + this.closed = true; + } + + emit(type, event = {}) { + for (const handler of this.listeners.get(type) || []) handler(event); + } +} + +describe("frontend/api endpoint wrapper coverage", () => { + beforeEach(() => { + global.fetch = vi.fn().mockResolvedValue(mockJsonResponse(200, { ok: true })); + global.window = { location: { href: "http://localhost/" } }; + }); + + afterEach(() => { + delete global.fetch; + delete global.window; + }); + + const kWrapperCases = [ + ["fetchPairings", [], "/api/pairings", undefined], + ["approvePairing", ["p1", "telegram", "acct"], "/api/pairings/p1/approve", "POST"], + ["rejectPairing", ["p1", "telegram"], "/api/pairings/p1/reject", "POST"], + ["fetchGoogleAccounts", [], "/api/google/accounts", undefined], + ["fetchGoogleStatus", [], "/api/google/status", undefined], + ["fetchGoogleStatus", ["acct-1"], "/api/google/status?accountId=acct-1", undefined], + ["fetchGoogleCredentials", [], "/api/google/credentials", undefined], + [ + "fetchGoogleCredentials", + [{ accountId: "a", client: "gmail" }], + "/api/google/credentials?accountId=a&client=gmail", + undefined, + ], + ["checkGoogleApis", ["a"], "/api/google/check?accountId=a", undefined], + [ + "saveGoogleCredentials", + [{ clientId: "id", clientSecret: "sec", email: "e@x.com" }], + "/api/google/credentials", + "POST", + ], + ["saveGoogleAccount", [{ email: "e@x.com" }], "/api/google/accounts", "POST"], + ["disconnectGoogle", ["a"], "/api/google/disconnect", "POST"], + ["fetchGmailConfig", [], "/api/gmail/config", undefined], + ["saveGmailConfig", [], "/api/gmail/config", "POST"], + ["startGmailWatch", ["acct"], "/api/gmail/watch/start", "POST"], + ["stopGmailWatch", ["acct"], "/api/gmail/watch/stop", "POST"], + ["renewGmailWatch", [], "/api/gmail/watch/renew", "POST"], + ["fetchAgentSessions", [], "/api/agent/sessions", undefined], + ["fetchDoctorRuns", [5], "/api/doctor/runs?limit=5", undefined], + ["fetchDoctorCards", [{ runId: "" }], "/api/doctor/cards", undefined], + ["fetchDoctorRun", ["r1"], "/api/doctor/runs/r1", undefined], + ["fetchDoctorRunCards", ["r1"], "/api/doctor/runs/r1/cards", undefined], + [ + "updateDoctorCardStatus", + [{ cardId: "c1", status: "resolved" }], + "/api/doctor/cards/c1/status", + "POST", + ], + ["sendAgentMessage", [{ message: "hi", sessionKey: "k" }], "/api/agent/message", "POST"], + ["sendAgentMessage", [], "/api/agent/message", "POST"], + ["sendDoctorCardFix", [], "/api/doctor/findings//fix", "POST"], + ["restartGateway", [], "/api/gateway/restart", "POST"], + ["fetchRestartStatus", [], "/api/restart-status", undefined], + ["dismissRestartStatus", [], "/api/restart-status/dismiss", "POST"], + ["fetchWatchdogStatus", [], "/api/watchdog/status", undefined], + ["fetchUsageSummary", [], "/api/usage/summary?days=30", undefined], + ["fetchUsageSessions", [], "/api/usage/sessions?limit=50", undefined], + [ + "fetchUsageSessionTimeSeries", + ["s1", 50], + "/api/usage/sessions/s1/timeseries?maxPoints=50", + undefined, + ], + ["fetchWatchdogEvents", [], "/api/watchdog/events?limit=20", undefined], + ["createWatchdogTerminalSession", [], "/api/watchdog/terminal/session", "POST"], + [ + "fetchWatchdogTerminalOutput", + ["s1", 5], + "/api/watchdog/terminal/output?sessionId=s1&cursor=5", + undefined, + ], + [ + "fetchWatchdogTerminalOutput", + ["s1"], + "/api/watchdog/terminal/output?sessionId=s1&cursor=0", + undefined, + ], + ["sendWatchdogTerminalInput", ["s1", "ls"], "/api/watchdog/terminal/input", "POST"], + ["closeWatchdogTerminalSession", ["s1"], "/api/watchdog/terminal/close", "POST"], + ["triggerWatchdogRepair", [], "/api/watchdog/repair", "POST"], + ["fetchWatchdogResources", [], "/api/watchdog/resources", undefined], + ["fetchWatchdogSettings", [], "/api/watchdog/settings", undefined], + ["updateWatchdogSettings", [{ enabled: true }], "/api/watchdog/settings", "PUT"], + ["updateWatchdogSettings", [null], "/api/watchdog/settings", "PUT"], + ["fetchDashboardUrl", [], "/api/gateway/dashboard", undefined], + ["fetchAlphaclawVersion", [], "/api/alphaclaw/version", undefined], + ["fetchAlphaclawVersion", [true], "/api/alphaclaw/version?refresh=1", undefined], + ["updateAlphaclaw", [], "/api/alphaclaw/update", "POST"], + ["fetchSyncCron", [], "/api/sync-cron", undefined], + ["updateSyncCron", [{ schedule: "0 0 * * *" }], "/api/sync-cron", "PUT"], + [ + "updateOpenAiCompatApiFeature", + [true], + "/api/alphaclaw/config/features/openai-compat-api", + "PUT", + ], + ["fetchCronJobs", [], "/api/cron/jobs?sortBy=nextRunAtMs&sortDir=asc", undefined], + ["fetchCronJobs", [{ sortBy: "", sortDir: "" }], "/api/cron/jobs", undefined], + ["fetchCronStatus", [], "/api/cron/status", undefined], + [ + "fetchCronJobRuns", + ["j1"], + "/api/cron/jobs/j1/runs?limit=20&offset=0&status=all&deliveryStatus=all&sortDir=desc", + undefined, + ], + [ + "fetchCronJobRuns", + ["j1", { query: " find me " }], + "/api/cron/jobs/j1/runs?limit=20&offset=0&status=all&deliveryStatus=all&sortDir=desc&query=find+me", + undefined, + ], + ["fetchCronJobUsage", ["j1"], "/api/cron/jobs/j1/usage?days=30", undefined], + ["fetchCronJobTrends", ["j1"], "/api/cron/jobs/j1/trends?range=7d", undefined], + ["fetchCronBulkUsage", [], "/api/cron/usage/bulk?days=30", undefined], + [ + "fetchCronBulkRuns", + [], + "/api/cron/runs/bulk?sinceMs=0&limitPerJob=20&status=all&deliveryStatus=all&sortDir=desc", + undefined, + ], + ["triggerCronJobRun", ["j1"], "/api/cron/jobs/j1/run", "POST"], + ["setCronJobEnabled", ["j1", true], "/api/cron/jobs/j1/enable", "POST"], + ["setCronJobEnabled", ["j1", false], "/api/cron/jobs/j1/disable", "POST"], + ["updateCronJobPrompt", ["j1", "new prompt"], "/api/cron/jobs/j1/prompt", "PUT"], + ["updateCronJobRouting", ["j1"], "/api/cron/jobs/j1/routing", "PUT"], + ["fetchDevicePairings", [], "/api/devices", undefined], + ["rejectDevice", ["d1"], "/api/devices/d1/reject", "POST"], + ["fetchNodesStatus", [], "/api/nodes", undefined], + ["approveNode", ["n1"], "/api/nodes/n1/approve", "POST"], + ["removeNode", ["n1"], "/api/nodes/n1", "DELETE"], + ["routeExecToNode", ["n1"], "/api/nodes/n1/route", "POST"], + ["fetchNodeConnectInfo", [], "/api/nodes/connect-info", undefined], + [ + "fetchNodeBrowserStatusForNode", + ["n1"], + "/api/nodes/n1/browser-status?profile=user", + undefined, + ], + ["fetchNodeExecConfig", [], "/api/nodes/exec-config", undefined], + ["saveNodeExecConfig", [{ security: "allowlist" }], "/api/nodes/exec-config", "POST"], + ["fetchNodeExecApprovals", [], "/api/nodes/exec-approvals", undefined], + [ + "addNodeExecAllowlistPattern", + ["npm run *"], + "/api/nodes/exec-approvals/allowlist", + "POST", + ], + [ + "removeNodeExecAllowlistPattern", + ["e1"], + "/api/nodes/exec-approvals/allowlist/e1", + "DELETE", + ], + ["fetchAuthStatus", [], "/api/auth/status", undefined], + ["logout", [], "/api/auth/logout", "POST"], + ["fetchOnboardStatus", [], "/api/onboard/status", undefined], + ["fetchModels", [], "/api/models", undefined], + ["fetchModelStatus", [], "/api/models/status", undefined], + [ + "fetchThinkingOptions", + ["anthropic/claude"], + "/api/models/thinking-options?modelKey=anthropic%2Fclaude", + undefined, + ], + ["setPrimaryModel", ["k1"], "/api/models/set", "POST"], + ["fetchModelsConfig", [], "/api/models/config", undefined], + ["fetchModelsConfig", [{ agentId: "a1" }], "/api/models/config?agentId=a1", undefined], + ["saveModelsConfig", [], "/api/models/config", "PUT"], + [ + "saveModelsConfig", + [{ agentId: "a1", primary: "k" }], + "/api/models/config?agentId=a1", + "PUT", + ], + ["fetchAuthProfiles", [], "/api/models/auth", undefined], + ["upsertAuthProfile", ["p1", { apiKey: "sk" }], "/api/models/auth/p1", "PUT"], + ["deleteAuthProfile", ["p1"], "/api/models/auth/p1", "DELETE"], + ["fetchAgents", [], "/api/agents", undefined], + ["fetchChannelAccounts", [], "/api/channels/accounts", undefined], + [ + "fetchChannelAccountToken", + [{ provider: "telegram" }], + "/api/channels/accounts/token?provider=telegram&accountId=default", + undefined, + ], + [ + "fetchChannelAccountToken", + [], + "/api/channels/accounts/token?provider=&accountId=default", + undefined, + ], + ["createChannelAccountJob", [{ provider: "telegram" }], "/api/channels/accounts/jobs", "POST"], + ["runChannelAccountLogin", [{ provider: "whatsapp" }], "/api/channels/accounts/login", "POST"], + [ + "fetchChannelAccountLoginStatus", + [{ provider: "whatsapp" }], + "/api/channels/accounts/login-status?provider=whatsapp&accountId=default", + undefined, + ], + [ + "fetchChannelAccountLoginStatus", + [], + "/api/channels/accounts/login-status?provider=&accountId=default", + undefined, + ], + ["fetchAgent", ["a1"], "/api/agents/a1", undefined], + ["fetchAgentWorkspaceSize", ["a1"], "/api/agents/a1/workspace-size", undefined], + ["fetchAgentBindings", ["a1"], "/api/agents/a1/bindings", undefined], + ["createAgent", [{ name: "Ops" }], "/api/agents", "POST"], + ["updateAgent", ["a1", { name: "Ops" }], "/api/agents/a1", "PUT"], + ["addAgentBinding", ["a1", { channel: "telegram" }], "/api/agents/a1/bindings", "POST"], + ["removeAgentBinding", ["a1", { channel: "telegram" }], "/api/agents/a1/bindings", "DELETE"], + ["deleteAgent", ["a1"], "/api/agents/a1?keepWorkspace=true", "DELETE"], + [ + "deleteAgent", + ["a1", { keepWorkspace: false }], + "/api/agents/a1?keepWorkspace=false", + "DELETE", + ], + ["setDefaultAgent", ["a1"], "/api/agents/a1/default", "POST"], + ["fetchCodexStatus", [], "/api/codex/status", undefined], + ["disconnectCodex", [], "/api/codex/disconnect", "POST"], + ["exchangeCodexOAuth", ["code-1"], "/api/codex/exchange", "POST"], + ["fetchEnvVars", [], "/api/env", undefined], + ["saveEnvVars", [[{ key: "A", value: "1" }]], "/api/env", "PUT"], + ["fetchWebhooks", [], "/api/webhooks", undefined], + ["fetchWebhookDetail", ["hook"], "/api/webhooks/hook", undefined], + ["createWebhook", ["hook"], "/api/webhooks", "POST"], + ["deleteWebhook", ["hook"], "/api/webhooks/hook", "DELETE"], + ["updateWebhookDestination", ["hook"], "/api/webhooks/hook/destination", "PUT"], + ["createWebhookOauthCallback", ["hook"], "/api/webhooks/hook/oauth-callback", "POST"], + ["rotateWebhookOauthCallback", ["hook"], "/api/webhooks/hook/oauth-callback/rotate", "POST"], + ["deleteWebhookOauthCallback", ["hook"], "/api/webhooks/hook/oauth-callback", "DELETE"], + [ + "fetchWebhookRequests", + ["hook"], + "/api/webhooks/hook/requests?limit=50&offset=0&status=all", + undefined, + ], + ["fetchWebhookRequest", ["hook", 3], "/api/webhooks/hook/requests/3", undefined], + ["fetchFileContent", ["notes/a.txt"], "/api/browse/read?path=notes%2Fa.txt", undefined], + ["saveFileContent", ["notes/a.txt", "hello"], "/api/browse/write", "PUT"], + ["saveFileContent", ["notes/a.txt", null], "/api/browse/write", "PUT"], + ["createBrowseFile", ["notes/new.txt"], "/api/browse/create-file", "POST"], + ["createBrowseFolder", ["notes/dir"], "/api/browse/create-folder", "POST"], + ["moveBrowsePath", ["a.txt", "b.txt"], "/api/browse/move", "POST"], + ["deleteBrowseFile", ["a.txt"], "/api/browse/delete", "DELETE"], + ["restoreBrowseFile", ["a.txt"], "/api/browse/restore", "POST"], + ["fetchBrowseGitSummary", [], "/api/browse/git-summary", undefined], + [ + "fetchBrowseSqliteTable", + [{ filePath: "db.sqlite", table: "runs" }], + "/api/browse/sqlite-table?path=db.sqlite&table=runs&limit=50&offset=0", + undefined, + ], + ]; + + it.each(kWrapperCases)( + "%s requests %s", + async (name, args, expectedUrl, method) => { + const api = await loadApiModule(); + + const result = await api[name](...args); + + const [calledUrl, options = {}] = global.fetch.mock.calls[0]; + expect(calledUrl).toBe(expectedUrl); + expect(options.method).toBe(method); + expect(options.headers).toBeInstanceOf(Headers); + expect(result).toEqual({ ok: true }); + }, + ); +}); + +describe("frontend/api behaviors", () => { + const kRealIntl = global.Intl; + + beforeEach(() => { + global.fetch = vi.fn().mockResolvedValue(mockJsonResponse(200, { ok: true })); + global.window = { location: { href: "http://localhost/" } }; + FakeEventSource.instances = []; + }); + + afterEach(() => { + global.Intl = kRealIntl; + delete global.fetch; + delete global.window; + delete global.document; + }); + + it("authFetch attaches the browser timezone header", async () => { + const api = await loadApiModule(); + + const res = await api.authFetch("/api/ping"); + + expect(res.status).toBe(200); + const headers = global.fetch.mock.calls[0][1].headers; + expect(headers.get("x-client-timezone")).toBe( + new Intl.DateTimeFormat().resolvedOptions().timeZone, + ); + }); + + it("authFetch keeps a caller-provided timezone header", async () => { + const api = await loadApiModule(); + + await api.authFetch("/api/ping", { + headers: { "x-client-timezone": "UTC" }, + }); + + const headers = global.fetch.mock.calls[0][1].headers; + expect(headers.get("x-client-timezone")).toBe("UTC"); + }); + + it("authFetch omits the timezone header when Intl lookup throws", async () => { + global.Intl = { + DateTimeFormat: () => { + throw new Error("boom"); + }, + }; + const api = await loadApiModule(); + + await api.authFetch("/api/ping"); + + const headers = global.fetch.mock.calls[0][1].headers; + expect(headers.get("x-client-timezone")).toBe(null); + }); + + it("authFetch omits the timezone header when timezone is empty", async () => { + global.Intl = { + DateTimeFormat: () => ({ resolvedOptions: () => ({ timeZone: "" }) }), + }; + const api = await loadApiModule(); + + await api.authFetch("/api/ping"); + + const headers = global.fetch.mock.calls[0][1].headers; + expect(headers.get("x-client-timezone")).toBe(null); + }); + + it("still redirects on 401 when localStorage.clear throws", async () => { + global.window.localStorage = { + clear: () => { + throw new Error("denied"); + }, + }; + global.fetch.mockResolvedValue(mockJsonResponse(401, {})); + const api = await loadApiModule(); + + await expect(api.fetchStatus()).rejects.toThrow("Unauthorized"); + expect(window.location.href).toBe("/setup"); + }); + + it("subscribeStatusEvents throws when EventSource is unavailable", async () => { + const api = await loadApiModule(); + + expect(() => api.subscribeStatusEvents()).toThrow( + "Server events are not supported in this browser", + ); + }); + + it("subscribeStatusEvents wires status events and unsubscribes", async () => { + global.window.EventSource = FakeEventSource; + const api = await loadApiModule(); + const events = []; + const onOpen = vi.fn(); + const onError = vi.fn(); + + const unsubscribe = api.subscribeStatusEvents({ + onMessage: (payload) => events.push(payload), + onOpen, + onError, + }); + + const source = FakeEventSource.instances[0]; + expect(source.url).toBe("/api/events/status"); + expect(source.options).toEqual({ withCredentials: true }); + + source.emit("status", { data: JSON.stringify({ gateway: "running" }) }); + source.emit("status", { data: "not json" }); + source.emit("status", { data: "null" }); + source.emit("status", {}); + source.onopen(); + source.onerror("err"); + + expect(events).toEqual([{ gateway: "running" }, {}, {}, {}]); + expect(onOpen).toHaveBeenCalledTimes(1); + expect(onError).toHaveBeenCalledWith("err"); + + unsubscribe(); + expect(source.closed).toBe(true); + expect(source.onopen).toBe(null); + expect(source.onerror).toBe(null); + source.emit("status", { data: "{}" }); + expect(events).toHaveLength(4); + }); + + it("subscribeStatusEvents defaults its callbacks to no-ops", async () => { + global.window.EventSource = FakeEventSource; + const api = await loadApiModule(); + + const unsubscribe = api.subscribeStatusEvents({}); + + const source = FakeEventSource.instances[0]; + expect(() => { + source.emit("status", { data: "{}" }); + source.onopen(); + source.onerror("err"); + }).not.toThrow(); + unsubscribe(); + }); + + it("subscribeOperationEvents subscribes to the operation SSE stream", async () => { + global.window.EventSource = FakeEventSource; + const api = await loadApiModule(); + const messages = []; + + const unsubscribe = api.subscribeOperationEvents({ + operationId: "op 1", + onMessage: (message) => messages.push(message), + }); + + const source = FakeEventSource.instances[0]; + expect(source.url).toBe("/api/operations/op%201/events"); + source.emit("phase", { data: JSON.stringify({ phase: "start" }) }); + expect(messages).toEqual([{ event: "phase", data: { phase: "start" } }]); + unsubscribe(); + expect(source.closed).toBe(true); + }); + + it("parseJsonOrThrow rejects when the payload marks ok false", async () => { + global.fetch.mockResolvedValue(mockJsonResponse(200, { ok: false, error: "nope" })); + const api = await loadApiModule(); + + await expect(api.rejectPairing("p1", "telegram")).rejects.toThrow("nope"); + }); + + it("parseJsonOrThrow resolves an empty body to an empty object", async () => { + global.fetch.mockResolvedValue(mockTextResponse(200, "")); + const api = await loadApiModule(); + + await expect(api.rejectPairing("p1", "telegram")).resolves.toEqual({}); + }); + + it("parseJsonOrThrow rejects with raw text for invalid JSON", async () => { + global.fetch.mockResolvedValue(mockTextResponse(200, "garbage")); + const api = await loadApiModule(); + + await expect(api.rejectPairing("p1", "telegram")).rejects.toThrow("garbage"); + }); + + it("parseJsonOrThrow falls back to HTTP status errors", async () => { + global.fetch.mockResolvedValue(mockTextResponse(500, "")); + const api = await loadApiModule(); + + await expect(api.rejectPairing("p1", "telegram")).rejects.toThrow("HTTP 500"); + }); + + it("fetchWatchdogLogs returns raw text", async () => { + global.fetch.mockResolvedValue(mockTextResponse(200, "log text")); + const api = await loadApiModule(); + + await expect(api.fetchWatchdogLogs(1024)).resolves.toBe("log text"); + expect(global.fetch.mock.calls[0][0]).toBe("/api/watchdog/logs?tail=1024"); + }); + + it("fetchWatchdogLogs throws on non-OK responses", async () => { + global.fetch.mockResolvedValue(mockTextResponse(500, "boom")); + const api = await loadApiModule(); + + await expect(api.fetchWatchdogLogs()).rejects.toThrow( + "Could not load watchdog logs", + ); + }); + + it("routeExecToNode maps AbortError to a timeout message", async () => { + global.fetch.mockRejectedValue( + Object.assign(new Error("aborted"), { name: "AbortError" }), + ); + const api = await loadApiModule(); + + await expect(api.routeExecToNode("n1")).rejects.toThrow( + "Routing timed out. Gateway may be restarting or unavailable.", + ); + }); + + it("routeExecToNode rethrows other errors", async () => { + global.fetch.mockRejectedValue(new Error("network down")); + const api = await loadApiModule(); + + await expect(api.routeExecToNode("n1")).rejects.toThrow("network down"); + }); + + it("downloadBrowseFile throws with server error text", async () => { + global.fetch.mockResolvedValue(mockTextResponse(404, "missing file")); + const api = await loadApiModule(); + + await expect(api.downloadBrowseFile("a.txt")).rejects.toThrow("missing file"); + }); + + it("downloadBrowseFile throws when object URLs are unsupported", async () => { + global.window.URL = { createObjectURL: null }; + global.fetch.mockResolvedValue({ + status: 200, + ok: true, + blob: async () => new Blob(["x"]), + text: async () => "", + }); + const api = await loadApiModule(); + + await expect(api.downloadBrowseFile("a.txt")).rejects.toThrow( + "Download is not supported in this browser", + ); + }); + + it("fetchAlphaclawReleaseNotes returns server release notes with a tag query", async () => { + global.fetch.mockResolvedValue(mockJsonResponse(200, { ok: true, tag: "v1" })); + const api = await loadApiModule(); + + const result = await api.fetchAlphaclawReleaseNotes("v1"); + + expect(global.fetch.mock.calls[0][0]).toBe("/api/alphaclaw/release-notes?tag=v1"); + expect(result).toEqual({ ok: true, tag: "v1" }); + }); + + it("fetchAlphaclawReleaseNotes falls back to the GitHub tag endpoint", async () => { + global.fetch + .mockResolvedValueOnce(mockTextResponse(500, JSON.stringify({ error: "nope" }))) + .mockResolvedValueOnce( + mockTextResponse( + 200, + JSON.stringify({ + tag_name: "v2", + name: "Release 2", + body: "Notes", + html_url: "https://example.com/v2", + published_at: "2026-01-01T00:00:00Z", + }), + ), + ); + const api = await loadApiModule(); + + const result = await api.fetchAlphaclawReleaseNotes("v2"); + + expect(global.fetch.mock.calls[1][0]).toBe( + "https://api.github.com/repos/chrysb/alphaclaw/releases/tags/v2", + ); + expect(result).toEqual({ + ok: true, + tag: "v2", + name: "Release 2", + body: "Notes", + htmlUrl: "https://example.com/v2", + publishedAt: "2026-01-01T00:00:00Z", + }); + }); + + it("fetchAlphaclawReleaseNotes falls back to the latest release endpoint", async () => { + global.fetch + .mockResolvedValueOnce(mockTextResponse(500, "boom")) + .mockResolvedValueOnce(mockTextResponse(200, "")); + const api = await loadApiModule(); + + const result = await api.fetchAlphaclawReleaseNotes(); + + expect(global.fetch.mock.calls[1][0]).toBe( + "https://api.github.com/repos/chrysb/alphaclaw/releases/latest", + ); + expect(result).toEqual({ + ok: true, + tag: "", + name: "", + body: "", + htmlUrl: "", + publishedAt: "", + }); + }); + + it("fetchAlphaclawReleaseNotes surfaces raw fallback text errors", async () => { + global.fetch + .mockResolvedValueOnce(mockTextResponse(500, "boom")) + .mockResolvedValueOnce(mockTextResponse(500, "oops")); + const api = await loadApiModule(); + + await expect(api.fetchAlphaclawReleaseNotes()).rejects.toThrow("oops"); + }); + + it("fetchAlphaclawReleaseNotes surfaces GitHub error messages", async () => { + global.fetch + .mockResolvedValueOnce(mockTextResponse(500, "boom")) + .mockResolvedValueOnce( + mockTextResponse(403, JSON.stringify({ message: "rate limited" })), + ); + const api = await loadApiModule(); + + await expect(api.fetchAlphaclawReleaseNotes()).rejects.toThrow("rate limited"); + }); + + it("fetchSyncCron throws on invalid JSON and API errors", async () => { + const api = await loadApiModule(); + + global.fetch.mockResolvedValue(mockTextResponse(200, "garbage")); + await expect(api.fetchSyncCron()).rejects.toThrow("garbage"); + + global.fetch.mockResolvedValue(mockTextResponse(400, JSON.stringify({ error: "bad" }))); + await expect(api.fetchSyncCron()).rejects.toThrow("bad"); + }); + + it("updateSyncCron throws on invalid JSON and API errors", async () => { + const api = await loadApiModule(); + + global.fetch.mockResolvedValue(mockTextResponse(200, "garbage")); + await expect(api.updateSyncCron({})).rejects.toThrow("garbage"); + + global.fetch.mockResolvedValue(mockTextResponse(400, JSON.stringify({ error: "bad" }))); + await expect(api.updateSyncCron({})).rejects.toThrow("bad"); + }); + + it("updateOpenAiCompatApiFeature throws on invalid JSON and API errors", async () => { + const api = await loadApiModule(); + + global.fetch.mockResolvedValue(mockTextResponse(200, "garbage")); + await expect(api.updateOpenAiCompatApiFeature(true)).rejects.toThrow("garbage"); + + global.fetch.mockResolvedValue(mockTextResponse(400, JSON.stringify({ error: "bad" }))); + await expect(api.updateOpenAiCompatApiFeature(false)).rejects.toThrow("bad"); + }); + + it("saveEnvVars throws raw text for invalid JSON responses", async () => { + global.fetch.mockResolvedValue(mockTextResponse(200, "garbage")); + const api = await loadApiModule(); + + await expect(api.saveEnvVars([])).rejects.toThrow("garbage"); + }); +}); diff --git a/tests/frontend/channel-create-operation-more.test.js b/tests/frontend/channel-create-operation-more.test.js new file mode 100644 index 00000000..2916c70b --- /dev/null +++ b/tests/frontend/channel-create-operation-more.test.js @@ -0,0 +1,134 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +vi.mock("../../lib/public/js/lib/api.js", () => ({ + createChannelAccount: vi.fn(), + createChannelAccountJob: vi.fn(), + subscribeOperationEvents: vi.fn(), +})); + +import { createChannelAccountWithProgress } from "../../lib/public/js/lib/channel-create-operation.js"; +import { + createChannelAccountJob, + subscribeOperationEvents, +} from "../../lib/public/js/lib/api.js"; + +describe("frontend/channel-create-operation (extended)", () => { + let handlers = null; + let close = null; + + beforeEach(() => { + global.window = { EventSource: function EventSource() {} }; + handlers = null; + close = vi.fn(); + subscribeOperationEvents.mockImplementation((nextHandlers) => { + handlers = nextHandlers; + return close; + }); + createChannelAccountJob.mockResolvedValue({ operationId: "op-x" }); + }); + + afterEach(() => { + vi.useRealTimers(); + delete global.window; + }); + + it("throws when the job start returns no operation id", async () => { + createChannelAccountJob.mockResolvedValue({}); + await expect( + createChannelAccountWithProgress({ payload: {}, onPhase: vi.fn() }), + ).rejects.toThrow("Could not start channel creation operation"); + }); + + it("ignores phases without labels and replaces deferred phases", async () => { + vi.useFakeTimers(); + const onPhase = vi.fn(); + const operationPromise = createChannelAccountWithProgress({ + payload: {}, + onPhase, + }); + await Promise.resolve(); + expect(handlers).toBeTruthy(); + + // Unknown events and label-less phases are ignored. + handlers.onMessage({ event: "unknown-event", data: {} }); + handlers.onMessage({ event: "phase", data: { phase: "x", label: " " } }); + expect(onPhase.mock.calls.map((call) => call[0])).toEqual(["Loading..."]); + + handlers.onMessage({ + event: "phase", + data: { phase: "restarting", label: "Restarting gateway..." }, + }); + // Two updates inside the restart minimum-visibility window: the second + // deferral must clear the first deferred timer. + handlers.onMessage({ + event: "phase", + data: { phase: "step-1", label: "Step 1" }, + }); + handlers.onMessage({ + event: "phase", + data: { phase: "step-2", label: "Step 2" }, + }); + vi.advanceTimersByTime(1200); + expect(onPhase.mock.calls.map((call) => call[0])).toEqual([ + "Loading...", + "Restarting gateway...", + "Step 2", + ]); + + handlers.onMessage({ event: "done", data: { ok: true } }); + await expect(operationPromise).resolves.toEqual({ ok: true }); + expect(close).toHaveBeenCalledTimes(1); + }); + + it("ignores error events after the operation already resolved", async () => { + const operationPromise = createChannelAccountWithProgress({ + payload: {}, + onPhase: vi.fn(), + }); + await Promise.resolve(); + + handlers.onMessage({ event: "done", data: { created: true } }); + handlers.onMessage({ event: "error", data: { error: "too late" } }); + + await expect(operationPromise).resolves.toEqual({ created: true }); + expect(close).toHaveBeenCalledTimes(1); + }); + + it("rejects with a fallback message when the error payload is empty", async () => { + const operationPromise = createChannelAccountWithProgress({ + payload: {}, + onPhase: vi.fn(), + }); + await Promise.resolve(); + + handlers.onMessage({ event: "error", data: {} }); + await expect(operationPromise).rejects.toThrow( + "Could not create channel", + ); + }); + + it("rejects when the stream disconnects before settling", async () => { + const operationPromise = createChannelAccountWithProgress({ + payload: {}, + onPhase: vi.fn(), + }); + await Promise.resolve(); + + handlers.onError(); + await expect(operationPromise).rejects.toThrow( + "Channel operation stream disconnected", + ); + expect(close).toHaveBeenCalledTimes(1); + }); + + it("resolves done events with a default payload object", async () => { + const operationPromise = createChannelAccountWithProgress({ + payload: {}, + onPhase: vi.fn(), + }); + await Promise.resolve(); + + handlers.onMessage({ event: "done" }); + await expect(operationPromise).resolves.toEqual({}); + }); +}); diff --git a/tests/frontend/cron-calendar-helpers-more.test.js b/tests/frontend/cron-calendar-helpers-more.test.js new file mode 100644 index 00000000..36a748d4 --- /dev/null +++ b/tests/frontend/cron-calendar-helpers-more.test.js @@ -0,0 +1,180 @@ +import { describe, expect, it } from "vitest"; +import { + buildSlotKey, + buildTokenTierByJobId, + classifyRepeatingJobs, + expandJobsToRollingSlots, + getRollingRange, + mapRunStatusesToSlots, +} from "../../lib/public/js/components/cron-tab/cron-calendar-helpers.js"; + +const kMinuteMs = 60 * 1000; +const kHourMs = 60 * kMinuteMs; +const kDayMs = 24 * kHourMs; +// Local-time noon so day boundaries are stable regardless of timezone. +const kNowMs = new Date(2026, 5, 15, 12, 0, 0, 0).getTime(); + +describe("frontend/cron-calendar-helpers (extended)", () => { + it("classifies schedule kinds and malformed cron expressions", () => { + const jobs = [ + { id: "every", schedule: { kind: "every", everyMs: kMinuteMs } }, + { id: "at", schedule: { kind: "at", at: kNowMs } }, + { id: "short-cron", schedule: { kind: "cron", expr: "* *" } }, + { id: "step", schedule: { kind: "cron", expr: "*/15 * * * *" } }, + { id: "zero-step", schedule: { kind: "cron", expr: "*/0 * * * *" } }, + { + id: "dense-list", + schedule: { kind: "cron", expr: "0,15,30 6-13 * * 1-5" }, + }, + { id: "narrow-step", schedule: { kind: "cron", expr: "*/15 8 * * *" } }, + { id: "daily", schedule: { kind: "cron", expr: "0 9 * * *" } }, + ]; + + const { repeatingJobs, scheduledJobs } = classifyRepeatingJobs(jobs); + expect(repeatingJobs.map((job) => job.id)).toEqual([ + "every", + "step", + "dense-list", + ]); + expect(scheduledJobs.map((job) => job.id)).toEqual([ + "at", + "short-cron", + "zero-step", + "narrow-step", + "daily", + ]); + }); + + it("discards out-of-range, inverted, overflowing, and junk cron tokens", () => { + const hugeDigits = "9".repeat(400); + const jobs = [ + // minute 99 out of range, hour range inverted after clamping. + { id: "bad-values", schedule: { kind: "cron", expr: "99 30-10 * * *" } }, + // parseInt overflows to Infinity for absurdly long digit runs. + { + id: "huge-range", + schedule: { kind: "cron", expr: `${hugeDigits}-${hugeDigits} * * * *` }, + }, + { id: "junk-token", schedule: { kind: "cron", expr: "abc 1,2 * * *" } }, + ]; + // Empty minute/hour sets fall back to "match everything", so none of + // these are dense enough to be repeating. + const { repeatingJobs, scheduledJobs } = classifyRepeatingJobs(jobs); + expect(repeatingJobs).toEqual([]); + expect(scheduledJobs).toHaveLength(3); + }); + + it("expands at and cron jobs into rolling slots", () => { + const atInsideMs = kNowMs + kHourMs; + const result = expandJobsToRollingSlots({ + jobs: [ + { id: "every", schedule: { kind: "every", everyMs: kMinuteMs } }, + { id: "at-in", name: "At In", schedule: { kind: "at", at: atInsideMs } }, + { id: "at-out", schedule: { kind: "at", at: kNowMs + 40 * kDayMs } }, + { id: "bad-cron", schedule: { kind: "cron", expr: "* *" } }, + { id: "daily", schedule: { kind: "cron", expr: "30 9 * * *" } }, + ], + nowMs: kNowMs, + }); + + expect(result.range.dayCount).toBe(7); + expect(result.days).toHaveLength(7); + expect(result.days[0].dayKey).toBe("2026-06-12"); + + const atSlots = result.slots.filter((slot) => slot.jobId === "at-in"); + expect(atSlots).toHaveLength(1); + expect(atSlots[0]).toMatchObject({ + key: buildSlotKey({ jobId: "at-in", scheduledAtMs: atInsideMs }), + jobName: "At In", + scheduledAtMs: atInsideMs, + dayKey: "2026-06-15", + hourOfDay: 13, + }); + + const dailySlots = result.slots.filter((slot) => slot.jobId === "daily"); + expect(dailySlots).toHaveLength(7); + expect(new Date(dailySlots[0].scheduledAtMs).getHours()).toBe(9); + expect(new Date(dailySlots[0].scheduledAtMs).getMinutes()).toBe(30); + + expect( + result.slots.some((slot) => + ["every", "at-out", "bad-cron"].includes(slot.jobId), + ), + ).toBe(false); + + // Slots are sorted by time. + const times = result.slots.map((slot) => slot.scheduledAtMs); + expect([...times].sort((left, right) => left - right)).toEqual(times); + }); + + it("expands day-of-week cron fields including the Sunday alias", () => { + // June 14 2026 is a Sunday; day-of-week 7 must match it too. + const result = expandJobsToRollingSlots({ + jobs: [{ id: "sunday", schedule: { kind: "cron", expr: "0 6 * * 7" } }], + nowMs: kNowMs, + }); + expect(result.slots).toHaveLength(1); + expect(new Date(result.slots[0].scheduledAtMs).getDay()).toBe(0); + }); + + it("maps run statuses onto slots with tolerance and consumption", () => { + const baseMs = kNowMs - 6 * kHourMs; + const slots = [ + { key: "job:1", jobId: "job", scheduledAtMs: baseMs }, + { key: "job:2", jobId: "job", scheduledAtMs: baseMs + kMinuteMs }, + { key: "job:3", jobId: "job", scheduledAtMs: baseMs + 2 * kMinuteMs }, + { key: "far:1", jobId: "far", scheduledAtMs: baseMs }, + { key: "none:1", jobId: "none", scheduledAtMs: baseMs }, + { key: "future:1", jobId: "job", scheduledAtMs: kNowMs + kHourMs }, + ]; + const statusBySlotKey = mapRunStatusesToSlots({ + slots, + bulkRunsByJobId: { + job: { + entries: [ + { ts: baseMs + 10 * kMinuteMs, status: "ERROR" }, + { ts: baseMs, status: "ok" }, + { ts: 0, status: "ok" }, + { ts: baseMs + 20 * kMinuteMs, status: "bogus-status" }, + ], + }, + far: { + entries: [{ ts: baseMs + 3 * kHourMs, status: "skipped" }], + }, + empty: { entries: "not-an-array" }, + }, + nowMs: kNowMs, + }); + + expect(statusBySlotKey).toEqual({ + "job:1": "ok", + "job:2": "error", + }); + // job:3 finds every entry consumed, far:1 only has an out-of-tolerance + // run, none:1 has no runs, and future:1 is skipped entirely. + expect(statusBySlotKey["job:3"]).toBeUndefined(); + expect(statusBySlotKey["far:1"]).toBeUndefined(); + expect(statusBySlotKey["none:1"]).toBeUndefined(); + expect(statusBySlotKey["future:1"]).toBeUndefined(); + }); + + it("marks every job unknown or disabled when no usage exists", () => { + expect( + buildTokenTierByJobId({ + jobs: [{ id: "a" }, { id: "b", enabled: false }], + usageByJobId: {}, + }), + ).toEqual({ a: "unknown", b: "disabled" }); + expect(buildTokenTierByJobId()).toEqual({}); + }); + + it("falls back to defaults for junk rolling range inputs", () => { + const range = getRollingRange({ + nowMs: "not-a-number", + pastDays: "junk", + futureDays: "junk", + }); + expect(range.dayCount).toBe(7); + expect(range.rangeEndMs).toBeGreaterThan(range.rangeStartMs); + }); +}); diff --git a/tests/frontend/cron-helpers.test.js b/tests/frontend/cron-helpers.test.js index 83790d49..9542028c 100644 --- a/tests/frontend/cron-helpers.test.js +++ b/tests/frontend/cron-helpers.test.js @@ -171,5 +171,362 @@ describe("frontend/cron-helpers", () => { expect(formatRelativeCompact(nowMs - 10 * 60 * 60 * 1000, nowMs)).toBe("10h"); expect(formatRelativeCompact(nowMs - 10 * 24 * 60 * 60 * 1000, nowMs)).toBe("10d"); expect(formatRelativeCompact(nowMs - 30 * 24 * 60 * 60 * 1000, nowMs)).toBe("1mo"); + expect(formatRelativeCompact(0, nowMs)).toBe("—"); + expect(formatRelativeCompact("junk", nowMs)).toBe("—"); + expect(formatRelativeCompact(nowMs + 90 * 1000, nowMs)).toBe("2m"); + }); + + it("formats relative timestamps in both directions", async () => { + const { formatRelativeMs } = await loadCronHelpers(); + const nowMs = Date.now(); + expect(formatRelativeMs(0, nowMs)).toBe("—"); + expect(formatRelativeMs(Number.NaN, nowMs)).toBe("—"); + expect(formatRelativeMs(nowMs + 10 * 1000, nowMs)).toBe("in <1m"); + expect(formatRelativeMs(nowMs - 10 * 1000, nowMs)).toBe("just now"); + expect(formatRelativeMs(nowMs + 5 * 60 * 1000, nowMs)).toBe("in 5m"); + expect(formatRelativeMs(nowMs - 5 * 60 * 1000, nowMs)).toBe("5m ago"); + expect(formatRelativeMs(nowMs + 3 * 60 * 60 * 1000, nowMs)).toBe("in 3h"); + expect(formatRelativeMs(nowMs - 3 * 60 * 60 * 1000, nowMs)).toBe("3h ago"); + expect(formatRelativeMs(nowMs + 2 * 24 * 60 * 60 * 1000, nowMs)).toBe("in 2d"); + expect(formatRelativeMs(nowMs - 2 * 24 * 60 * 60 * 1000, nowMs)).toBe("2d ago"); + }); + + it("formats overdue next runs at hour and day granularity", async () => { + const { formatNextRunRelativeMs } = await loadCronHelpers(); + const nowMs = Date.now(); + expect(formatNextRunRelativeMs(0, nowMs)).toBe("—"); + expect(formatNextRunRelativeMs(-5, nowMs)).toBe("—"); + expect(formatNextRunRelativeMs(nowMs - 3 * 60 * 60 * 1000, nowMs)).toBe("overdue by 3h"); + expect(formatNextRunRelativeMs(nowMs - 4 * 24 * 60 * 60 * 1000, nowMs)).toBe( + "overdue by 4d", + ); + }); + + it("humanizes interval, daily, and edge-case cron expressions", async () => { + const { formatCronScheduleLabel } = await loadCronHelpers(); + expect(formatCronScheduleLabel({ kind: "cron", expr: "*/5 * * * *" })).toBe("Every 5m"); + expect(formatCronScheduleLabel({ kind: "cron", expr: "0 */2 * * *" })).toBe("Every 2h"); + expect(formatCronScheduleLabel({ kind: "cron", expr: "30 18 * * *" })).toBe( + "Daily at 6:30pm", + ); + expect(formatCronScheduleLabel({ kind: "cron", expr: "0 0 * * *" })).toBe( + "Daily at 12:00am", + ); + expect(formatCronScheduleLabel({ kind: "cron", expr: "15 9 * * 0,6" })).toBe( + "Every Sun, Sat at 9:15am", + ); + // Non-humanizable expressions fall back to the raw expression. + expect(formatCronScheduleLabel({ kind: "cron", expr: "0 8 15 6 *" })).toBe("0 8 15 6 *"); + expect(formatCronScheduleLabel({ kind: "cron", expr: "a b * * *" })).toBe("a b * * *"); + expect(formatCronScheduleLabel({ kind: "cron", expr: "0 8" })).toBe("0 8"); + // Day-of-month out of the 1-31 range is not humanized. + expect(formatCronScheduleLabel({ kind: "cron", expr: "0 4 0 * *" })).toBe("0 4 0 * *"); + // Minute-step with a weekday hour range but broken minute field. + expect(formatCronScheduleLabel({ kind: "cron", expr: "*/x 6-13 * * 1-5" })).toBe( + "*/x 6-13 * * 1-5", + ); + }); + + it("covers remaining schedule label branches", async () => { + const { formatCronScheduleLabel } = await loadCronHelpers(); + expect(formatCronScheduleLabel({ kind: "every" })).toBe("Every interval"); + expect(formatCronScheduleLabel({ kind: "cron" })).toBe("Cron"); + expect(formatCronScheduleLabel({ kind: "cron", expr: " " })).toBe("Cron"); + expect(formatCronScheduleLabel({})).toBe("Unknown schedule"); + expect(formatCronScheduleLabel()).toBe("Unknown schedule"); + + // includeTimeZone always appends the schedule tz. + expect( + formatCronScheduleLabel( + { kind: "cron", expr: "0 9 * * *", tz: "UTC" }, + { includeTimeZone: true }, + ), + ).toBe("Daily at 9:00am (UTC)"); + // Non-humanizable expr with tz appended. + expect( + formatCronScheduleLabel( + { kind: "cron", expr: "0 8 15 6 *", tz: "UTC" }, + { includeTimeZone: true }, + ), + ).toBe("0 8 15 6 * (UTC)"); + // No schedule tz -> nothing to append even when requested. + expect( + formatCronScheduleLabel( + { kind: "cron", expr: "0 9 * * *" }, + { includeTimeZone: true, includeTimeZoneWhenDifferent: true }, + ), + ).toBe("Daily at 9:00am"); + + // Fallback path (no kind) via cronExpr/timezone aliases. + expect( + formatCronScheduleLabel( + { cronExpr: "0 9 * * *", timezone: "UTC" }, + { includeTimeZone: true }, + ), + ).toBe("Daily at 9:00am (UTC)"); + expect( + formatCronScheduleLabel( + { expr: "0 8 15 6 *", timezone: "UTC" }, + { includeTimeZone: true }, + ), + ).toBe("0 8 15 6 * (UTC)"); + expect(formatCronScheduleLabel({ cron: "0 8 15 6 *" })).toBe("0 8 15 6 *"); + }); + + it("resolves the client time zone from Intl when not provided", async () => { + const { formatCronScheduleLabel } = await loadCronHelpers(); + const schedule = { kind: "cron", expr: "0 9 * * *", tz: "Etc/GMT+8" }; + const options = { includeTimeZoneWhenDifferent: true }; + try { + // Client tz differs from schedule tz -> append. + vi.stubGlobal("Intl", { + DateTimeFormat: () => ({ + resolvedOptions: () => ({ timeZone: "America/New_York" }), + }), + }); + expect(formatCronScheduleLabel(schedule, options)).toBe( + "Daily at 9:00am (Etc/GMT+8)", + ); + + // Client tz matches schedule tz -> no suffix. + vi.stubGlobal("Intl", { + DateTimeFormat: () => ({ + resolvedOptions: () => ({ timeZone: "etc/gmt+8" }), + }), + }); + expect(formatCronScheduleLabel(schedule, options)).toBe("Daily at 9:00am"); + + // Intl blows up -> unknown client tz -> append defensively. + vi.stubGlobal("Intl", { + DateTimeFormat: () => { + throw new Error("no intl"); + }, + }); + expect(formatCronScheduleLabel(schedule, options)).toBe( + "Daily at 9:00am (Etc/GMT+8)", + ); + + // Intl reports no time zone -> append defensively. + vi.stubGlobal("Intl", { + DateTimeFormat: () => ({ resolvedOptions: () => ({}) }), + }); + expect(formatCronScheduleLabel(schedule, options)).toBe( + "Daily at 9:00am (Etc/GMT+8)", + ); + } finally { + vi.unstubAllGlobals(); + } + }); + + it("derives job health and health class names", async () => { + const { getCronJobHealth, getCronJobHealthClassName } = await loadCronHelpers(); + expect(getCronJobHealth({ enabled: false })).toBe("disabled"); + expect(getCronJobHealth({ state: { runningAtMs: 123 } })).toBe("running"); + expect(getCronJobHealth({ state: { lastStatus: "ERROR" } })).toBe("error"); + expect(getCronJobHealth({ state: { lastRunStatus: "ok" } })).toBe("ok"); + expect(getCronJobHealth({ state: {} })).toBe("unknown"); + expect(getCronJobHealth()).toBe("unknown"); + + expect(getCronJobHealthClassName("ok")).toBe("bg-green-500"); + expect(getCronJobHealthClassName("error")).toBe("bg-red-500"); + expect(getCronJobHealthClassName("running")).toBe("bg-yellow-400"); + expect(getCronJobHealthClassName("disabled")).toBe("bg-gray-500"); + expect(getCronJobHealthClassName()).toBe("bg-gray-500"); + }); + + it("formats token counts and costs", async () => { + const { formatTokenCount, formatCost } = await loadCronHelpers(); + expect(formatTokenCount(1234)).toBe("1,234"); + expect(formatTokenCount()).toBe("0"); + expect(formatCost(1.5)).toContain("1.5"); + expect(formatCost()).toBeTruthy(); + }); + + it("computes run token totals from components and fallbacks", async () => { + const { getCronRunTotalTokens } = await loadCronHelpers(); + expect( + getCronRunTotalTokens({ + usage: { + input_tokens: 10, + outputTokens: 20, + cache_read_tokens: 30, + cacheWriteTokens: 40, + inputTokens: -1, + output_tokens: "junk", + }, + }), + ).toBe(100); + expect(getCronRunTotalTokens({ usage: { total_tokens: 500 } })).toBe(500); + expect(getCronRunTotalTokens({ usage: { totalTokens: 400 } })).toBe(400); + expect(getCronRunTotalTokens({ total_tokens: 300 })).toBe(300); + expect(getCronRunTotalTokens({ totalTokens: 200 })).toBe(200); + expect(getCronRunTotalTokens({ usage: { total_tokens: -5 } })).toBe(0); + expect(getCronRunTotalTokens({})).toBe(0); + expect(getCronRunTotalTokens()).toBe(0); + }); + + it("reads estimated run costs from candidate fields", async () => { + const { getCronRunEstimatedCost } = await loadCronHelpers(); + expect(getCronRunEstimatedCost({ estimatedCost: 0.25 })).toBe(0.25); + expect(getCronRunEstimatedCost({ estimated_cost: 0.5 })).toBe(0.5); + expect(getCronRunEstimatedCost({ usage: { estimatedCost: 0.75 } })).toBe(0.75); + expect(getCronRunEstimatedCost({ usage: { totalCost: 1.25 } })).toBe(1.25); + expect(getCronRunEstimatedCost({ usage: { cost: 0.05 } })).toBe(0.05); + expect(getCronRunEstimatedCost({ estimatedCost: "junk", usage: { cost: -1 } })).toBeNull(); + expect(getCronRunEstimatedCost({})).toBeNull(); + expect(getCronRunEstimatedCost()).toBeNull(); + }); + + it("covers heartbeat suppression and warning edge cases", async () => { + const { buildCronOptimizationWarnings } = await loadCronHelpers(); + + // Latest bulk run is picked by ts; HEARTBEAT_OK in nested summaries suppresses. + const suppressedByLatest = buildCronOptimizationWarnings( + [ + { + id: "job-nested", + name: "Nested Heartbeat", + delivery: { mode: "announce" }, + payload: { kind: "agentTurn", message: "noop" }, + state: { lastDelivered: false, lastDeliveryStatus: "not-delivered" }, + }, + ], + { + "job-nested": { + entries: [ + { ts: 100, summary: "older failure" }, + { ts: 300, result: { summary: "HEARTBEAT_OK nested" } }, + { ts: 200, summary: "middle" }, + ], + }, + }, + ); + expect(suppressedByLatest).toHaveLength(0); + + // payload.summary candidate also suppresses. + const suppressedByPayload = buildCronOptimizationWarnings( + [ + { + id: "job-payload", + name: "Payload Heartbeat", + delivery: { mode: "announce" }, + payload: { kind: "agentTurn", message: "noop" }, + state: { lastDelivered: false, lastDeliveryStatus: "not-delivered" }, + }, + ], + { + "job-payload": { + entries: [{ ts: 100, payload: { summary: "heartbeat_ok lower case" } }], + }, + }, + ); + expect(suppressedByPayload).toHaveLength(0); + + // Latest run status "ok" from bulk entries suppresses the warning too. + const suppressedByStatus = buildCronOptimizationWarnings( + [ + { + id: "job-ok", + name: "Latest Ok", + delivery: { mode: "announce" }, + payload: { kind: "agentTurn", message: "noop" }, + state: { lastDelivered: false, lastDeliveryStatus: "not-delivered" }, + }, + ], + { "job-ok": { entries: [{ ts: 100, status: "OK" }] } }, + ); + expect(suppressedByStatus).toHaveLength(0); + + // Circular job state makes JSON.stringify throw; warning still fires. + const circularState = { + lastDelivered: false, + lastDeliveryStatus: "not-delivered", + }; + circularState.self = circularState; + const circularWarnings = buildCronOptimizationWarnings( + [ + { + id: "job-circular", + delivery: { mode: "announce" }, + payload: { kind: "agentTurn", message: "noop" }, + state: circularState, + }, + ], + {}, + ); + expect(circularWarnings).toHaveLength(1); + expect(circularWarnings[0].title).toContain("job-circular"); + + // agentTurn prompt mentioning the message tool with delivery mode none warns. + const mismatch = buildCronOptimizationWarnings( + [ + { + id: "job-mismatch", + name: "Mismatch", + delivery: { mode: "none" }, + payload: { kind: "agentTurn", message: "Please use the MESSAGE TOOL here" }, + state: {}, + }, + ], + {}, + ); + expect(mismatch).toHaveLength(1); + expect(mismatch[0].tone).toBe("warning"); + + // A job without an id still evaluates safely. + const anonymous = buildCronOptimizationWarnings( + [ + { + name: "Anonymous", + delivery: { mode: "announce" }, + payload: {}, + state: { + lastDelivered: false, + lastDeliveryStatus: "not-delivered", + consecutiveErrors: 2, + }, + }, + ], + {}, + ); + expect(anonymous).toHaveLength(2); + + // Warnings are capped at eight. + const manyJobs = Array.from({ length: 12 }, (_, index) => ({ + id: `job-${index}`, + name: `Job ${index}`, + delivery: { mode: "announce" }, + payload: { kind: "agentTurn", message: "noop" }, + state: { consecutiveErrors: 5 }, + })); + expect(buildCronOptimizationWarnings(manyJobs, {})).toHaveLength(8); + expect(buildCronOptimizationWarnings()).toEqual([]); + }); + + it("finds the next scheduled run across enabled jobs", async () => { + const { getNextScheduledRunAcrossJobs, kAllCronJobsRouteKey } = await loadCronHelpers(); + expect(kAllCronJobsRouteKey).toBe("__all__"); + expect( + getNextScheduledRunAcrossJobs([ + { enabled: true, state: { nextRunAtMs: 2000 } }, + { enabled: false, state: { nextRunAtMs: 100 } }, + { enabled: true, state: { nextRunAtMs: 1000 } }, + { enabled: true, state: {} }, + { enabled: true, state: { nextRunAtMs: "junk" } }, + ]), + ).toBe(1000); + expect(getNextScheduledRunAcrossJobs([])).toBeNull(); + expect(getNextScheduledRunAcrossJobs()).toBeNull(); + expect( + getNextScheduledRunAcrossJobs([{ enabled: false, state: { nextRunAtMs: 5 } }]), + ).toBeNull(); + }); + + it("handles non-object payloads when reading prompts", async () => { + const { readCronJobPrompt } = await loadCronHelpers(); + expect(readCronJobPrompt({ payload: null })).toBe(""); + expect(readCronJobPrompt()).toBe(""); + expect(readCronJobPrompt({ payload: { kind: "systemEvent", text: 42 } })).toBe(""); + expect(readCronJobPrompt({ payload: { kind: "agentTurn", message: 42 } })).toBe(""); }); }); diff --git a/tests/frontend/doctor-helpers-more.test.js b/tests/frontend/doctor-helpers-more.test.js new file mode 100644 index 00000000..959e7c2c --- /dev/null +++ b/tests/frontend/doctor-helpers-more.test.js @@ -0,0 +1,165 @@ +import { describe, expect, it } from "vitest"; +import { + buildDoctorRunMarkers, + buildDoctorStatusFilterOptions, + formatDoctorCategory, + formatDoctorCharCount, + getDoctorBootstrapWarningTitle, + getDoctorCategoryTone, + getDoctorChangeLabel, + getDoctorPriorityTone, + getDoctorRunPillDetail, + getDoctorStatusTone, + getDoctorWarningMessage, + shouldShowDoctorWarning, +} from "../../lib/public/js/components/doctor/helpers.js"; + +const bootstrapStatus = ({ truncated = [], nearLimit = [] } = {}) => ({ + bootstrapContext: { + activeTruncatedFiles: truncated, + activeNearLimitFiles: nearLimit, + }, +}); + +describe("frontend/doctor helpers (extended)", () => { + it("maps priorities to tones", () => { + expect(getDoctorPriorityTone("P0")).toBe("danger"); + expect(getDoctorPriorityTone(" p1 ")).toBe("warning"); + expect(getDoctorPriorityTone("P2")).toBe("neutral"); + expect(getDoctorPriorityTone()).toBe("neutral"); + }); + + it("maps statuses to tones", () => { + expect(getDoctorStatusTone("Fixed")).toBe("success"); + expect(getDoctorStatusTone("working")).toBe("info"); + expect(getDoctorStatusTone("dismissed")).toBe("neutral"); + expect(getDoctorStatusTone("open")).toBe("warning"); + expect(getDoctorStatusTone()).toBe("warning"); + }); + + it("maps categories to tones with a default", () => { + expect(getDoctorCategoryTone("mixed-concerns")).toBe("cyan"); + expect(getDoctorCategoryTone("something else")).toBe("info"); + expect(getDoctorCategoryTone()).toBe("info"); + }); + + it("formats empty categories as Workspace", () => { + expect(formatDoctorCategory("")).toBe("Workspace"); + expect(formatDoctorCategory(null)).toBe("Workspace"); + }); + + it("suppresses the warning for missing or in-progress statuses", () => { + expect(shouldShowDoctorWarning(null)).toBe(false); + expect( + shouldShowDoctorWarning({ + runInProgress: true, + needsInitialRun: false, + stale: true, + changeSummary: { hasMeaningfulChanges: true }, + }), + ).toBe(false); + }); + + it("builds warning messages for all change states", () => { + expect(getDoctorWarningMessage(null)).toBe(""); + expect( + getDoctorWarningMessage({ changeSummary: { changedFilesCount: 0 } }), + ).toBe("Doctor has not been run in the last week."); + expect(getDoctorWarningMessage({})).toBe( + "Doctor has not been run in the last week.", + ); + expect( + getDoctorWarningMessage({ changeSummary: { changedFilesCount: 1 } }), + ).toBe( + "Drift Doctor has not been run in the last week and 1 file changed since the last review.", + ); + }); + + it("formats char counts", () => { + expect(formatDoctorCharCount(1234)).toBe("1,234 chars"); + expect(formatDoctorCharCount()).toBe("0 chars"); + }); + + it("builds bootstrap warning titles for each combination", () => { + expect(getDoctorBootstrapWarningTitle(null)).toBe(""); + expect(getDoctorBootstrapWarningTitle(bootstrapStatus())).toBe(""); + + const truncatedFile = { path: "notes.md", rawChars: 10, injectedChars: 4 }; + const nearLimitFile = { path: "todo.md", rawChars: 9 }; + const managedFile = { path: "hooks/bootstrap/managed.md", rawChars: 9 }; + + expect( + getDoctorBootstrapWarningTitle( + bootstrapStatus({ + truncated: [truncatedFile, managedFile], + nearLimit: [nearLimitFile, managedFile], + }), + ), + ).toBe("Some of your main files are being truncated or nearing the limit:"); + expect( + getDoctorBootstrapWarningTitle( + bootstrapStatus({ nearLimit: [nearLimitFile] }), + ), + ).toBe("One of your main files is nearing the limit:"); + expect( + getDoctorBootstrapWarningTitle( + bootstrapStatus({ nearLimit: [nearLimitFile, { path: "b.md" }] }), + ), + ).toBe("Some of your main files are nearing the limit:"); + expect( + getDoctorBootstrapWarningTitle( + bootstrapStatus({ truncated: [truncatedFile] }), + ), + ).toBe("One of your main files is being truncated:"); + expect( + getDoctorBootstrapWarningTitle( + bootstrapStatus({ truncated: [truncatedFile, { path: "b.md" }] }), + ), + ).toBe("Some of your main files are being truncated:"); + }); + + it("labels change summaries with correct pluralization", () => { + expect(getDoctorChangeLabel({ changedFilesCount: 1 })).toBe( + "1 change since last run", + ); + expect(getDoctorChangeLabel({ changedFilesCount: 2 })).toBe( + "2 changes since last run", + ); + expect(getDoctorChangeLabel(null)).toBe("No changes since last run"); + }); + + it("describes run pills for every run shape", () => { + expect(getDoctorRunPillDetail(null)).toBe(""); + expect(getDoctorRunPillDetail("not-an-object")).toBe(""); + expect(getDoctorRunPillDetail({ status: "running" })).toBe("Running"); + expect(getDoctorRunPillDetail({ status: "completed", cardCount: 1 })).toBe( + "1 finding", + ); + expect(getDoctorRunPillDetail({ status: "completed", cardCount: 3 })).toBe( + "3 findings", + ); + }); + + it("builds run markers for failures and P2-only runs", () => { + expect(buildDoctorRunMarkers(null)).toEqual([]); + expect(buildDoctorRunMarkers({ status: "failed" })).toEqual([ + { tone: "neutral", count: 0, label: "Failed" }, + ]); + expect( + buildDoctorRunMarkers({ + status: "completed", + cardCount: 2, + priorityCounts: { P0: 0, P1: 0, P2: 2 }, + }), + ).toEqual([{ tone: "neutral", count: 0, label: "P2" }]); + }); + + it("lists the status filter options", () => { + expect(buildDoctorStatusFilterOptions().map((option) => option.value)).toEqual([ + "open", + "working", + "dismissed", + "fixed", + ]); + }); +}); diff --git a/tests/frontend/file-viewer-utils-more.test.js b/tests/frontend/file-viewer-utils-more.test.js new file mode 100644 index 00000000..0fa543f9 --- /dev/null +++ b/tests/frontend/file-viewer-utils-more.test.js @@ -0,0 +1,21 @@ +import { describe, expect, it } from "vitest"; +import { + clampSelectionIndex, + parsePathSegments, +} from "../../lib/public/js/components/file-viewer/utils.js"; + +describe("frontend/file-viewer-utils (extended)", () => { + it("parses path segments, trimming and dropping empties", () => { + expect(parsePathSegments("a//b/ c /")).toEqual(["a", "b", "c"]); + expect(parsePathSegments("")).toEqual([]); + expect(parsePathSegments(null)).toEqual([]); + }); + + it("clamps selection indexes to the valid range", () => { + expect(clampSelectionIndex("3", 5)).toBe(3); + expect(clampSelectionIndex(9, 5)).toBe(5); + expect(clampSelectionIndex(-2, 5)).toBe(0); + expect(clampSelectionIndex("not-a-number", 5)).toBe(0); + expect(clampSelectionIndex(undefined, 5)).toBe(0); + }); +}); diff --git a/tests/frontend/format.test.js b/tests/frontend/format.test.js new file mode 100644 index 00000000..968b2492 --- /dev/null +++ b/tests/frontend/format.test.js @@ -0,0 +1,148 @@ +const loadFormatModule = async () => import("../../lib/public/js/lib/format.js"); + +class ThrowingDate extends Date { + getTime() { + throw new Error("boom"); + } +} + +describe("frontend/format", () => { + it("formatInteger formats with grouping and defaults to zero", async () => { + const { formatInteger } = await loadFormatModule(); + + expect(formatInteger(1234567)).toBe("1,234,567"); + expect(formatInteger(undefined)).toBe("0"); + }); + + it("formatCompactNumber handles small, large, and non-finite values", async () => { + const { formatCompactNumber } = await loadFormatModule(); + + expect(formatCompactNumber(999)).toBe("999"); + expect(formatCompactNumber(-999)).toBe("-999"); + expect(formatCompactNumber(1500)).toBe("1.5K"); + expect(formatCompactNumber(-2500000)).toBe("-2.5M"); + expect(formatCompactNumber(Infinity)).toBe("0"); + expect(formatCompactNumber(undefined)).toBe("0"); + }); + + it("formatBytes scales through units with adaptive precision", async () => { + const { formatBytes } = await loadFormatModule(); + + expect(formatBytes(0)).toBe("0 B"); + expect(formatBytes(-5)).toBe("0 B"); + expect(formatBytes(Infinity)).toBe("0 B"); + expect(formatBytes(512)).toBe("512 B"); + expect(formatBytes(2048)).toBe("2.00 KB"); + expect(formatBytes(15 * 1024)).toBe("15.0 KB"); + expect(formatBytes(200 * 1024)).toBe("200 KB"); + expect(formatBytes(5 * 1024 ** 3)).toBe("5.00 GB"); + expect(formatBytes(2000 * 1024 ** 4)).toBe("2000 TB"); + }); + + it("formatUsd formats currency values", async () => { + const { formatUsd } = await loadFormatModule(); + + expect(formatUsd(1234.5)).toBe("$1,234.50"); + expect(formatUsd(0.005)).toBe("$0.005"); + expect(formatUsd(undefined)).toBe("$0.00"); + }); + + it("formatLocaleDateTime converts supported value shapes", async () => { + const { formatLocaleDateTime } = await loadFormatModule(); + const date = new Date(2026, 0, 2, 3, 4, 5); + + expect(formatLocaleDateTime(date)).toBe(date.toLocaleString()); + expect(formatLocaleDateTime(1700000000, { valueIsUnixSeconds: true })).toBe( + new Date(1700000000 * 1000).toLocaleString(), + ); + expect(formatLocaleDateTime(date.getTime(), { valueIsEpochMs: true })).toBe( + date.toLocaleString(), + ); + expect(formatLocaleDateTime("2026-01-02T03:04:05")).toBe( + new Date("2026-01-02T03:04:05").toLocaleString(), + ); + }); + + it("formatLocaleDateTime falls back for empty, invalid, and throwing values", async () => { + const { formatLocaleDateTime } = await loadFormatModule(); + + expect(formatLocaleDateTime(null)).toBe("—"); + expect(formatLocaleDateTime("")).toBe("—"); + expect(formatLocaleDateTime("not-a-date")).toBe("—"); + expect(formatLocaleDateTime("not-a-date", { fallback: "n/a" })).toBe("n/a"); + expect(formatLocaleDateTime(new ThrowingDate())).toBe("—"); + }); + + it("formatLocaleDateTimeWithTodayTime prints time only for today", async () => { + const { formatLocaleDateTimeWithTodayTime } = await loadFormatModule(); + const now = new Date(); + const twoDaysAgo = new Date(Date.now() - 2 * 86400000); + + expect(formatLocaleDateTimeWithTodayTime(now)).toBe(now.toLocaleTimeString()); + expect(formatLocaleDateTimeWithTodayTime(twoDaysAgo)).toBe( + twoDaysAgo.toLocaleString(), + ); + expect(formatLocaleDateTimeWithTodayTime(null)).toBe("—"); + expect(formatLocaleDateTimeWithTodayTime("nope", { fallback: "x" })).toBe("x"); + expect(formatLocaleDateTimeWithTodayTime(new ThrowingDate())).toBe("—"); + expect( + formatLocaleDateTimeWithTodayTime(1700000000, { valueIsUnixSeconds: true }), + ).toBe(new Date(1700000000 * 1000).toLocaleString()); + }); + + it("formatDurationCompactMs formats durations", async () => { + const { formatDurationCompactMs } = await loadFormatModule(); + + expect(formatDurationCompactMs(0)).toBe("0s"); + expect(formatDurationCompactMs(-10)).toBe("0s"); + expect(formatDurationCompactMs(Infinity)).toBe("0s"); + expect(formatDurationCompactMs(500)).toBe("500ms"); + expect(formatDurationCompactMs(5000)).toBe("5s"); + expect(formatDurationCompactMs(59_400)).toBe("59s"); + expect(formatDurationCompactMs(60_000)).toBe("1m 0s"); + expect(formatDurationCompactMs(125_000)).toBe("2m 5s"); + }); + + it("formatChartBucketLabel formats day keys per range", async () => { + const { formatChartBucketLabel } = await loadFormatModule(); + const date = new Date(2026, 6, 1); + + expect( + formatChartBucketLabel("2026-07-01", { range: "7d", valueType: "day-key" }), + ).toBe( + date.toLocaleDateString([], { + weekday: "short", + month: "numeric", + day: "numeric", + }), + ); + expect( + formatChartBucketLabel("2026-07-01", { range: "30d", valueType: "day-key" }), + ).toBe(date.toLocaleDateString([], { month: "numeric", day: "numeric" })); + expect(formatChartBucketLabel("not-a-day", { valueType: "day-key" })).toBe( + "not-a-day", + ); + expect(formatChartBucketLabel(null, { valueType: "day-key" })).toBe(""); + }); + + it("formatChartBucketLabel formats epoch and date values", async () => { + const { formatChartBucketLabel } = await loadFormatModule(); + const date = new Date(2026, 6, 1, 13, 30); + + expect(formatChartBucketLabel(date.getTime(), { range: "24h" })).toBe( + date.toLocaleTimeString([], { hour: "numeric" }), + ); + expect(formatChartBucketLabel(date.getTime())).toBe( + date.toLocaleDateString([], { + weekday: "short", + month: "numeric", + day: "numeric", + }), + ); + expect(formatChartBucketLabel("abc", {})).toBe("abc"); + expect(formatChartBucketLabel(date, { range: "30d", valueType: "date" })).toBe( + date.toLocaleDateString([], { month: "numeric", day: "numeric" }), + ); + expect(formatChartBucketLabel("nope", { valueType: "date" })).toBe("nope"); + }); +}); diff --git a/tests/frontend/models-tab-model-picker-component.test.js b/tests/frontend/models-tab-model-picker-component.test.js new file mode 100644 index 00000000..d169f901 --- /dev/null +++ b/tests/frontend/models-tab-model-picker-component.test.js @@ -0,0 +1,286 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +// A minimal hook harness so the SearchableModelPicker component function can +// be invoked directly (the repo has no DOM/preact render harness). State is +// stored per hook-call index and persists between simulated renders. +vi.mock("preact/hooks", () => { + const harness = { + slots: [], + cursor: 0, + effects: [], + }; + harness.beginRender = () => { + harness.cursor = 0; + harness.effects = []; + }; + harness.reset = () => { + harness.slots = []; + harness.cursor = 0; + harness.effects = []; + }; + const useState = (initialValue) => { + const index = harness.cursor++; + if (!(index in harness.slots)) { + harness.slots[index] = + typeof initialValue === "function" ? initialValue() : initialValue; + } + const setState = (next) => { + harness.slots[index] = + typeof next === "function" ? next(harness.slots[index]) : next; + }; + return [harness.slots[index], setState]; + }; + const useRef = (initialValue = null) => { + const index = harness.cursor++; + if (!(index in harness.slots)) { + harness.slots[index] = { current: initialValue }; + } + return harness.slots[index]; + }; + const useMemo = (factory) => factory(); + const useEffect = (effect) => { + harness.effects.push(effect); + }; + return { useState, useRef, useMemo, useEffect, __harness: harness }; +}); + +import { SearchableModelPicker } from "../../lib/public/js/components/models-tab/model-picker.js"; +import * as preactHooks from "preact/hooks"; + +const harness = preactHooks.__harness; +// Hook call order in the component: useState(query)=0, useState(open)=1, useRef=2. +const kQuerySlot = 0; +const kOpenSlot = 1; +const kRootRefSlot = 2; + +const renderPicker = (props = {}) => { + harness.beginRender(); + return SearchableModelPicker(props); +}; + +const collectVnodes = (node, out = []) => { + if (node == null || typeof node !== "object") return out; + if (Array.isArray(node)) { + for (const child of node) collectVnodes(child, out); + return out; + } + out.push(node); + if (node.props) collectVnodes(node.props.children, out); + return out; +}; + +const findAllByType = (tree, type) => + collectVnodes(tree).filter((vnode) => vnode.type === type); + +const collectText = (node, out = []) => { + if (typeof node === "string" || typeof node === "number") { + out.push(String(node)); + return out; + } + if (Array.isArray(node)) { + for (const child of node) collectText(child, out); + return out; + } + if (node && typeof node === "object" && node.props) { + collectText(node.props.children, out); + } + return out; +}; + +const treeText = (tree) => collectText(tree).join(" "); + +const kOptions = [ + { key: "anthropic/claude-opus-4-6", label: "Opus 4.6" }, + { key: "anthropic/claude-sonnet-4-6", label: "Sonnet 4.6" }, + { key: "openai/gpt-5.5", label: "gpt-5.5" }, +]; +const kPopular = [{ key: "openai/gpt-5.5" }, { key: "missing/model" }]; + +describe("frontend/models-tab SearchableModelPicker", () => { + beforeEach(() => { + harness.reset(); + }); + + afterEach(() => { + delete global.document; + }); + + it("registers and cleans up the outside-pointer listener", () => { + const listeners = {}; + global.document = { + addEventListener: vi.fn((name, handler) => { + listeners[name] = handler; + }), + removeEventListener: vi.fn(), + }; + renderPicker({}); + expect(harness.effects).toHaveLength(1); + const cleanup = harness.effects[0](); + const handler = listeners.mousedown; + expect(typeof handler).toBe("function"); + + // rootRef.current is null -> pointer counts as outside -> closes. + harness.slots[kOpenSlot] = true; + handler({ target: {} }); + expect(harness.slots[kOpenSlot]).toBe(false); + + // Pointer inside the root keeps the dropdown open. + harness.slots[kRootRefSlot].current = { contains: () => true }; + harness.slots[kOpenSlot] = true; + handler({ target: {} }); + expect(harness.slots[kOpenSlot]).toBe(true); + + cleanup(); + expect(global.document.removeEventListener).toHaveBeenCalledWith( + "mousedown", + handler, + ); + }); + + it("opens on focus, tracks input, and ignores Enter with no options", () => { + const tree = renderPicker({}); + const input = findAllByType(tree, "input")[0]; + expect(input).toBeTruthy(); + + input.props.onFocus(); + expect(harness.slots[kOpenSlot]).toBe(true); + + input.props.onInput({ target: { value: "sonnet" } }); + expect(harness.slots[kQuerySlot]).toBe("sonnet"); + expect(harness.slots[kOpenSlot]).toBe(true); + + // No visible options: Enter is a no-op, other keys fall through. + harness.slots[kQuerySlot] = ""; + input.props.onKeyDown({ key: "Enter" }); + input.props.onKeyDown({ key: "ArrowDown" }); + expect(harness.slots[kOpenSlot]).toBe(true); + }); + + it("groups open options with a popular section and selects on click", () => { + const onSelect = vi.fn(); + harness.slots[kOpenSlot] = true; + const tree = renderPicker({ + options: kOptions, + popularModels: kPopular, + onSelect, + }); + + const text = treeText(tree); + expect(text).toContain("POPULAR"); + expect(text).toContain("ANTHROPIC"); + expect(text).toContain("OPENAI"); + + const buttons = findAllByType(tree, "button"); + // 1 visible popular model (missing/model filtered out) + 3 options. + expect(buttons).toHaveLength(4); + + const preventDefault = vi.fn(); + buttons[0].props.onMouseDown({ preventDefault }); + expect(preventDefault).toHaveBeenCalledTimes(1); + + buttons[0].props.onClick(); + expect(onSelect).toHaveBeenCalledWith("openai/gpt-5.5"); + expect(harness.slots[kQuerySlot]).toBe(""); + expect(harness.slots[kOpenSlot]).toBe(false); + }); + + it("selects the first visible option on Enter and closes on Escape", () => { + const onSelect = vi.fn(); + harness.slots[kOpenSlot] = true; + const tree = renderPicker({ options: kOptions, onSelect }); + const input = findAllByType(tree, "input")[0]; + + const preventDefault = vi.fn(); + input.props.onKeyDown({ key: "Enter", preventDefault }); + expect(preventDefault).toHaveBeenCalledTimes(1); + expect(onSelect).toHaveBeenCalledWith("anthropic/claude-opus-4-6"); + expect(harness.slots[kOpenSlot]).toBe(false); + + harness.slots[kOpenSlot] = true; + input.props.onKeyDown({ key: "Escape" }); + expect(harness.slots[kOpenSlot]).toBe(false); + }); + + it("filters options by query and hides the popular section", () => { + harness.slots[kQuerySlot] = "sonnet"; + harness.slots[kOpenSlot] = true; + const tree = renderPicker({ + options: kOptions, + popularModels: kPopular, + }); + + const text = treeText(tree); + expect(text).not.toContain("POPULAR"); + expect(text).toContain("ANTHROPIC"); + + const buttons = findAllByType(tree, "button"); + expect(buttons).toHaveLength(1); + // Exercises the default onSelect noop. + buttons[0].props.onClick(); + expect(harness.slots[kOpenSlot]).toBe(false); + }); + + it("explains when the only match is already configured", () => { + harness.slots[kQuerySlot] = "gpt"; + harness.slots[kOpenSlot] = true; + const tree = renderPicker({ + options: [kOptions[0], kOptions[1]], + configuredOptions: [{ key: "openai/gpt-5.5", label: "gpt-5.5" }], + }); + + const text = treeText(tree); + expect(text).toContain("Already added above:"); + expect(text).toContain("GPT-5.5"); + expect(findAllByType(tree, "button")).toHaveLength(0); + }); + + it("summarizes multiple already-configured matches", () => { + harness.slots[kQuerySlot] = "gpt"; + harness.slots[kOpenSlot] = true; + const tree = renderPicker({ + options: [], + configuredOptions: [ + { key: "openai/gpt-5.5", label: "gpt-5.5" }, + { key: "openai/gpt-5.6-sol", label: "gpt-5.6-sol" }, + ], + }); + + expect(treeText(tree)).toContain( + "2 matching models are already added above.", + ); + }); + + it("shows the empty state when nothing matches", () => { + harness.slots[kQuerySlot] = "zzz-no-match"; + harness.slots[kOpenSlot] = true; + const tree = renderPicker({ + options: kOptions, + configuredOptions: [{ key: "openai/gpt-5.5", label: "gpt-5.5" }], + }); + + expect(treeText(tree)).toContain("No models match that search."); + }); + + it("stays closed and inert while disabled", () => { + const onSelect = vi.fn(); + harness.slots[kOpenSlot] = true; + const tree = renderPicker({ + options: kOptions, + onSelect, + disabled: true, + }); + + // Dropdown suppressed even though open state is true. + expect(findAllByType(tree, "button")).toHaveLength(0); + + const input = findAllByType(tree, "input")[0]; + harness.slots[kOpenSlot] = false; + input.props.onFocus(); + expect(harness.slots[kOpenSlot]).toBe(false); + + const preventDefault = vi.fn(); + input.props.onKeyDown({ key: "Enter", preventDefault }); + expect(preventDefault).toHaveBeenCalledTimes(1); + expect(onSelect).not.toHaveBeenCalled(); + }); +}); diff --git a/tests/frontend/models-tab-model-picker-helpers.test.js b/tests/frontend/models-tab-model-picker-helpers.test.js new file mode 100644 index 00000000..3ac499ab --- /dev/null +++ b/tests/frontend/models-tab-model-picker-helpers.test.js @@ -0,0 +1,104 @@ +import { describe, expect, it } from "vitest"; +import { + buildProviderHasAuth, + buildSyntheticModelEntry, + getModelCatalogProvider, + getModelDisplayLabel, + getModelsTabAuthProvider, + getModelsTabRequiredAuthProviders, + getProviderAuthDisplayOrder, + getProviderSortIndex, +} from "../../lib/public/js/components/models-tab/model-picker.js"; + +describe("frontend/models-tab/model-picker helpers", () => { + it("maps non-OpenAI model providers to their auth provider", () => { + expect(getModelsTabAuthProvider("anthropic/claude-opus-4-6")).toBe( + "anthropic", + ); + expect(getModelsTabAuthProvider("google/gemini-3.1-pro-preview")).toBe( + "google", + ); + }); + + it("returns the single auth provider for non-OpenAI models", () => { + expect( + getModelsTabRequiredAuthProviders("anthropic/claude-opus-4-6"), + ).toEqual(["anthropic"]); + expect(getModelsTabRequiredAuthProviders("")).toEqual([]); + }); + + it("appends unknown required providers after the known display order", () => { + expect( + getProviderAuthDisplayOrder(["anthropic", "custom-lab"]), + ).toEqual(["anthropic", "openai-codex", "custom-lab"]); + }); + + it("resolves the catalog provider from explicit field or key", () => { + expect(getModelCatalogProvider({ provider: " openai " })).toBe("openai"); + expect(getModelCatalogProvider({ key: "google/gemini-3.1-pro" })).toBe( + "google", + ); + expect(getModelCatalogProvider(null)).toBe(""); + }); + + it("sorts providers by display order with unknowns last", () => { + expect(getProviderSortIndex("anthropic")).toBe(0); + expect(getProviderSortIndex("openai-codex")).toBe(2); + expect(getProviderSortIndex("not-a-provider")).toBe( + Number.MAX_SAFE_INTEGER, + ); + }); + + it("builds friendly Anthropic labels for synthetic entries", () => { + expect(buildSyntheticModelEntry("anthropic/claude-opus-4-6")).toEqual({ + key: "anthropic/claude-opus-4-6", + provider: "anthropic", + label: "Claude Opus 4.6", + }); + expect(buildSyntheticModelEntry("anthropic/claude-sonnet-4.6")).toEqual({ + key: "anthropic/claude-sonnet-4.6", + provider: "anthropic", + label: "Claude Sonnet 4.6", + }); + }); + + it("falls back to the raw key when no friendly label matches", () => { + expect(buildSyntheticModelEntry("anthropic/claude-custom")).toEqual({ + key: "anthropic/claude-custom", + provider: "anthropic", + label: "anthropic/claude-custom", + }); + expect(buildSyntheticModelEntry("plainmodel")).toEqual({ + key: "plainmodel", + provider: "plainmodel", + label: "plainmodel", + }); + expect(buildSyntheticModelEntry("")).toEqual({ + key: "", + provider: "", + label: "", + }); + }); + + it("prefers featured labels for display", () => { + expect(getModelDisplayLabel({ featuredLabel: "Featured" })).toBe( + "Featured", + ); + expect(getModelDisplayLabel({ key: "zai/glm-5" })).toBe("zai/glm-5"); + expect(getModelDisplayLabel(null)).toBe(""); + }); + + it("marks providers authenticated for key, token, or access profiles", () => { + expect( + buildProviderHasAuth({ + authProfiles: [ + { provider: "anthropic", token: "tok" }, + { provider: "google", access: "acc" }, + { provider: "zai" }, + ], + }), + ).toEqual({ anthropic: true, google: true }); + expect(buildProviderHasAuth()).toEqual({}); + expect(buildProviderHasAuth({})).toEqual({}); + }); +}); diff --git a/tests/frontend/session-keys-more.test.js b/tests/frontend/session-keys-more.test.js new file mode 100644 index 00000000..56ee05b8 --- /dev/null +++ b/tests/frontend/session-keys-more.test.js @@ -0,0 +1,136 @@ +import { describe, expect, it } from "vitest"; +import { + getAgentIdFromSessionKey, + getDestinationFromSession, + getSessionChannelForIcon, + getSessionDisplayLabel, + getSessionKind, + getSessionPriority, + isDestinationSessionKey, + kDestinationSessionFilter, + parseChannelFromSessionKey, + sortSessionsByPriority, +} from "../../lib/public/js/lib/session-keys.js"; + +describe("session-keys destination and sorting helpers", () => { + it("extracts the agent id from a session key", () => { + expect(getAgentIdFromSessionKey("agent:bob:telegram:direct:1")).toBe("bob"); + expect(getAgentIdFromSessionKey("main")).toBe(""); + expect(getAgentIdFromSessionKey("")).toBe(""); + }); + + it("detects destination session keys", () => { + expect(isDestinationSessionKey("agent:a:telegram:DIRECT:123")).toBe(true); + expect(isDestinationSessionKey("agent:a:telegram:group:9")).toBe(true); + expect(isDestinationSessionKey("agent:a:main")).toBe(false); + }); + + it("filters destination sessions by reply metadata or key shape", () => { + expect( + kDestinationSessionFilter({ + key: "agent:a:main", + replyChannel: "telegram", + replyTo: "42", + }), + ).toBe(true); + expect( + kDestinationSessionFilter({ key: "agent:a:telegram:direct:42" }), + ).toBe(true); + expect(kDestinationSessionFilter({ key: "agent:a:main" })).toBe(false); + expect(kDestinationSessionFilter(null)).toBe(false); + }); + + it("prioritizes destination sessions ahead of others", () => { + expect(getSessionPriority({ key: "agent:a:telegram:direct:1" })).toBe(0); + expect(getSessionPriority({ key: "agent:a:main" })).toBe(1); + expect(getSessionPriority(null)).toBe(1); + }); + + it("sorts by priority, recency, then key", () => { + const sessions = [ + { key: "agent:a:main", updatedAt: 500 }, + { key: "agent:b:telegram:direct:2", updatedAt: 100 }, + { key: "agent:a:telegram:direct:1", updatedAt: 100 }, + { key: "agent:c:telegram:direct:3", updatedAt: 900 }, + ]; + expect(sortSessionsByPriority(sessions).map((row) => row.key)).toEqual([ + "agent:c:telegram:direct:3", + "agent:a:telegram:direct:1", + "agent:b:telegram:direct:2", + "agent:a:main", + ]); + expect(sortSessionsByPriority()).toEqual([]); + expect(sortSessionsByPriority("nope")).toEqual([]); + }); + + it("builds destinations from reply metadata", () => { + expect(getDestinationFromSession({ replyChannel: "telegram" })).toBe(null); + expect(getDestinationFromSession(null)).toBe(null); + expect( + getDestinationFromSession({ + key: "agent:bob:telegram:direct:7", + replyChannel: "telegram", + replyTo: "7", + }), + ).toEqual({ channel: "telegram", to: "7", agentId: "bob" }); + expect( + getDestinationFromSession({ + key: "standalone", + replyChannel: "slack", + replyTo: "C123", + }), + ).toEqual({ channel: "slack", to: "C123" }); + }); + + it("parses channels from session keys", () => { + expect(parseChannelFromSessionKey("agent:a:telegram:direct:1")).toBe( + "telegram", + ); + expect(parseChannelFromSessionKey("agent:a:discord:direct:1")).toBe( + "discord", + ); + expect(parseChannelFromSessionKey("agent:a:slack:direct:C1")).toBe("slack"); + expect(parseChannelFromSessionKey("agent:a:main")).toBe(""); + expect(parseChannelFromSessionKey()).toBe(""); + }); + + it("classifies session kinds", () => { + expect(getSessionKind("")).toBe("other"); + expect(getSessionKind("main")).toBe("main"); + expect(getSessionKind("agent:a:telegram:group:9:topic:4")).toBe("topic"); + expect(getSessionKind("agent:a:slash:cmd")).toBe("slash"); + expect(getSessionKind("agent:a:subagent:x")).toBe("subagent"); + expect(getSessionKind("agent:a:discord:direct:1")).toBe("direct"); + expect(getSessionKind("agent:a:something")).toBe("other"); + }); + + it("labels doctor and fallback sessions", () => { + expect(getSessionDisplayLabel({ key: "agent:a:doctor:12" })).toBe( + "Doctor Run #12", + ); + expect(getSessionDisplayLabel({ key: "agent:a:doctor" })).toBe( + "Doctor Run", + ); + expect(getSessionDisplayLabel({ key: "agent:a:custom-session" })).toBe( + "agent:a:custom-session", + ); + expect(getSessionDisplayLabel(null)).toBe("Session"); + expect(getSessionDisplayLabel({ key: "agent:a:discord:direct:99" })).toBe( + "Direct 99", + ); + expect(getSessionDisplayLabel({ key: "agent:a:telegram:direct:99" })).toBe( + "Direct message", + ); + }); + + it("resolves the channel icon source in preference order", () => { + expect( + getSessionChannelForIcon({ channel: "discord", replyChannel: "slack" }), + ).toBe("discord"); + expect(getSessionChannelForIcon({ replyChannel: "slack" })).toBe("slack"); + expect( + getSessionChannelForIcon({ key: "agent:a:telegram:direct:1" }), + ).toBe("telegram"); + expect(getSessionChannelForIcon(null)).toBe(""); + }); +}); diff --git a/tests/frontend/sse.test.js b/tests/frontend/sse.test.js new file mode 100644 index 00000000..d9af3531 --- /dev/null +++ b/tests/frontend/sse.test.js @@ -0,0 +1,148 @@ +const loadSseModule = async () => import("../../lib/public/js/lib/sse.js"); + +class FakeEventSource { + static instances = []; + + constructor(url, options) { + this.url = url; + this.options = options; + this.listeners = new Map(); + this.closed = false; + this.onerror = undefined; + FakeEventSource.instances.push(this); + } + + addEventListener(type, handler) { + const handlers = this.listeners.get(type) || []; + handlers.push(handler); + this.listeners.set(type, handlers); + } + + removeEventListener(type, handler) { + const handlers = (this.listeners.get(type) || []).filter( + (entry) => entry !== handler, + ); + this.listeners.set(type, handlers); + } + + close() { + this.closed = true; + } + + emit(type, event = {}) { + for (const handler of this.listeners.get(type) || []) handler(event); + } +} + +describe("frontend/sse", () => { + beforeEach(() => { + FakeEventSource.instances = []; + global.window = { EventSource: FakeEventSource }; + }); + + afterEach(() => { + delete global.window; + }); + + it("throws when EventSource is not available", async () => { + global.window = {}; + const { subscribeToSse } = await loadSseModule(); + + expect(() => subscribeToSse({ url: "/api/x" })).toThrow( + "Server events are not supported in this browser", + ); + }); + + it("opens a credentialed stream and forwards named events", async () => { + const { subscribeToSse } = await loadSseModule(); + const messages = []; + const onError = vi.fn(); + + subscribeToSse({ + url: "/api/operations/op-1/events", + onMessage: (message) => messages.push(message), + onError, + }); + + const source = FakeEventSource.instances[0]; + expect(source.url).toBe("/api/operations/op-1/events"); + expect(source.options).toEqual({ withCredentials: true }); + + source.emit("phase", { data: JSON.stringify({ phase: "cloning" }) }); + source.emit("done", { data: JSON.stringify({ ok: true }) }); + source.emit("error", { data: JSON.stringify({ error: "failed" }) }); + + expect(messages).toEqual([ + { event: "phase", data: { phase: "cloning" } }, + { event: "done", data: { ok: true } }, + { event: "error", data: { error: "failed" } }, + ]); + expect(onError).not.toHaveBeenCalled(); + }); + + it("normalizes missing, blank, non-string, and invalid payloads to empty objects", async () => { + const { subscribeToSse } = await loadSseModule(); + const messages = []; + + subscribeToSse({ + url: "/api/x", + onMessage: (message) => messages.push(message), + }); + + const source = FakeEventSource.instances[0]; + source.emit("phase", {}); + source.emit("phase", { data: " " }); + source.emit("phase", { data: 42 }); + source.emit("done", { data: "not json" }); + source.emit("error"); + + expect(messages).toEqual([ + { event: "phase", data: {} }, + { event: "phase", data: {} }, + { event: "phase", data: {} }, + { event: "done", data: {} }, + { event: "error", data: {} }, + ]); + }); + + it("invokes onError for transport errors and defaults callbacks", async () => { + const { subscribeToSse } = await loadSseModule(); + const onError = vi.fn(); + + subscribeToSse({ url: "/api/x", onError }); + const source = FakeEventSource.instances[0]; + const errorEvent = { type: "error" }; + source.onerror(errorEvent); + expect(onError).toHaveBeenCalledWith(errorEvent); + + // Defaults: no url, no callbacks — nothing should throw. + const unsubscribe = subscribeToSse({}); + const defaultSource = FakeEventSource.instances[1]; + expect(defaultSource.url).toBe(""); + expect(() => { + defaultSource.emit("phase", { data: "{}" }); + defaultSource.onerror({}); + }).not.toThrow(); + unsubscribe(); + }); + + it("unsubscribes listeners and closes the stream", async () => { + const { subscribeToSse } = await loadSseModule(); + const messages = []; + + const unsubscribe = subscribeToSse({ + url: "/api/x", + onMessage: (message) => messages.push(message), + }); + const source = FakeEventSource.instances[0]; + + unsubscribe(); + + expect(source.closed).toBe(true); + expect(source.onerror).toBe(null); + source.emit("phase", { data: "{}" }); + source.emit("done", { data: "{}" }); + source.emit("error", { data: "{}" }); + expect(messages).toEqual([]); + }); +}); diff --git a/tests/frontend/syntax-highlighters-markdown.test.js b/tests/frontend/syntax-highlighters-markdown.test.js new file mode 100644 index 00000000..26205909 --- /dev/null +++ b/tests/frontend/syntax-highlighters-markdown.test.js @@ -0,0 +1,77 @@ +import { describe, expect, it } from "vitest"; +import { highlightMarkdownContent } from "../../lib/public/js/lib/syntax-highlighters/markdown.js"; +import { + getFileSyntaxKind, + highlightEditorLines, +} from "../../lib/public/js/lib/syntax-highlighters/index.js"; + +const htmlForLine = (content) => highlightMarkdownContent(content)[0].html; + +describe("frontend/syntax-highlighters markdown", () => { + it("highlights headings, quotes, fences, and table separators", () => { + expect(htmlForLine("# Title")).toBe( + '# Title', + ); + expect(htmlForLine("### Deep ")).toBe( + '### Deep <b>', + ); + expect(htmlForLine("> quoted text")).toBe( + '> quoted text', + ); + expect(htmlForLine("```js")).toBe('```js'); + expect(htmlForLine("|---|---|")).toBe( + '|---|---|', + ); + }); + + it("highlights bullets with inline markdown", () => { + expect(htmlForLine("- item")).toBe('- item'); + expect(htmlForLine(" * starred")).toBe( + ' * starred', + ); + expect(htmlForLine("- has `code`")).toContain( + '`code`', + ); + }); + + it("highlights inline code, bold, and links on plain lines", () => { + const html = htmlForLine("say `hi` to **you** via [site](https://x.dev)"); + expect(html).toContain('`hi`'); + expect(html).toContain('**you**'); + expect(html).toContain( + '[site](https://x.dev)', + ); + }); + + it("escapes plain lines and keeps line numbering", () => { + const lines = highlightMarkdownContent("plain \nsecond & line"); + expect(lines).toHaveLength(2); + expect(lines[0]).toEqual({ lineNumber: 1, html: "plain <tag>" }); + expect(lines[1]).toEqual({ lineNumber: 2, html: "second & line" }); + }); +}); + +describe("frontend/syntax-highlighters editor line dispatch", () => { + it("falls back to plain for unknown extensions", () => { + expect(getFileSyntaxKind("notes/file.txt")).toBe("plain"); + expect(getFileSyntaxKind("")).toBe("plain"); + }); + + it("routes each syntax kind to its highlighter", () => { + expect(highlightEditorLines("# hi", "markdown")[0].html).toBe( + '# hi', + ); + expect( + highlightEditorLines("const x = 1;", "javascript")[0].html, + ).toContain("hl-"); + expect(highlightEditorLines("a { color: red; }", "css")[0].html).toContain( + "hl-", + ); + expect(highlightEditorLines("x < y", "plain")).toEqual([ + { lineNumber: 1, html: "x < y" }, + ]); + expect(highlightEditorLines("x < y", "unknown-kind")).toEqual([ + { lineNumber: 1, html: "x < y" }, + ]); + }); +}); diff --git a/tests/frontend/syntax-highlighters.test.js b/tests/frontend/syntax-highlighters.test.js index 37c4d01f..074dff72 100644 --- a/tests/frontend/syntax-highlighters.test.js +++ b/tests/frontend/syntax-highlighters.test.js @@ -39,3 +39,282 @@ describe("frontend/syntax-highlighters", () => { expect(lines[1].html).toContain('const'); }); }); + +const loadJavaScriptHighlighter = async () => + import("../../lib/public/js/lib/syntax-highlighters/javascript.js"); +const loadCssHighlighter = async () => + import("../../lib/public/js/lib/syntax-highlighters/css.js"); +const loadHtmlHighlighter = async () => + import("../../lib/public/js/lib/syntax-highlighters/html.js"); +const loadFrontmatter = async () => + import("../../lib/public/js/lib/syntax-highlighters/frontmatter.js"); + +describe("frontend/syntax-highlighters/javascript", () => { + it("highlights keywords, literals, numbers, strings, and comments", async () => { + const { highlightJavaScriptContent } = await loadJavaScriptHighlighter(); + const lines = highlightJavaScriptContent( + [ + "const x = 42; // trailing comment", + "let flag = true; const nothing = null; let missing = undefined; let off = false;", + "const hex = 0xFF; const exp = 1.5e10; const neg = -7;", + 'const dq = "double \\" quote";', + "const sq = 'single';", + "const tpl = `template`;", + "before /* inline */ typeof after", + ].join("\n"), + ); + + expect(lines).toHaveLength(7); + expect(lines[0].html).toContain('const'); + expect(lines[0].html).toContain('42'); + expect(lines[0].html).toContain('// trailing comment'); + expect(lines[1].html).toContain('true'); + expect(lines[1].html).toContain('false'); + expect(lines[1].html).toContain('null'); + expect(lines[1].html).toContain('undefined'); + expect(lines[2].html).toContain('0xFF'); + expect(lines[2].html).toContain('1.5e10'); + expect(lines[2].html).toContain('-7'); + expect(lines[3].html).toContain('"double \\" quote"'); + expect(lines[4].html).toContain("'single'"); + expect(lines[5].html).toContain('`template`'); + expect(lines[6].html).toContain('/* inline */'); + expect(lines[6].html).toContain('typeof'); + }); + + it("tracks block comments across lines and unterminated tokens", async () => { + const { highlightJavaScriptContent } = await loadJavaScriptHighlighter(); + const lines = highlightJavaScriptContent( + [ + "start /* spans", + "middle of comment", + "end */ return this;", + "plain text line", + 'const open = "unterminated', + "/* never closed", + ].join("\n"), + ); + + expect(lines[0].html).toContain('/* spans'); + expect(lines[1].html).toBe('middle of comment'); + expect(lines[2].html).toContain('end */'); + expect(lines[2].html).toContain('return'); + expect(lines[2].html).toContain('this'); + expect(lines[3].html).toContain("plain text line"); + expect(lines[4].html).toContain('"unterminated'); + expect(lines[5].html).toBe('/* never closed'); + }); + + it("handles escaped closing quotes at line end and default state", async () => { + const { highlightJavaScriptLine, highlightJavaScriptContent } = + await loadJavaScriptHighlighter(); + + const rendered = highlightJavaScriptLine("const a = 1"); + expect(rendered.html).toContain('const'); + expect(rendered.state).toEqual({ inBlockComment: false }); + + const trailingEscape = highlightJavaScriptLine('const s = "abc\\'); + expect(trailingEscape.html).toContain('class="hl-string"'); + + expect(highlightJavaScriptContent("")).toEqual([{ lineNumber: 1, html: "" }]); + expect(highlightJavaScriptContent(null)).toEqual([{ lineNumber: 1, html: "" }]); + }); +}); + +describe("frontend/syntax-highlighters/css", () => { + it("highlights at-rules, colors, numbers, units, and properties", async () => { + const { highlightCssContent } = await loadCssHighlighter(); + const lines = highlightCssContent( + [ + "@media (min-width: 600px) {", + " .box { color: #ff0000; margin: 10px 1.5em; width: 100%; }", + "}", + ].join("\n"), + ); + + expect(lines[0].html).toContain('@media'); + expect(lines[0].html).toContain('600px'); + expect(lines[1].html).toContain('color'); + expect(lines[1].html).toContain('margin'); + expect(lines[1].html).toContain('#ff0000'); + expect(lines[1].html).toContain('10px'); + expect(lines[1].html).toContain('1.5em'); + expect(lines[1].html).toContain('100%'); + }); + + it("tracks comments and strings across lines", async () => { + const { highlightCssContent } = await loadCssHighlighter(); + const lines = highlightCssContent( + [ + "before /* inline */ after", + "start /* spans", + "still comment", + 'end */ body { background: url("img.png"); }', + "content: 'quoted \\' esc';", + 'broken: "unterminated', + "/* never closed", + ].join("\n"), + ); + + expect(lines[0].html).toContain('/* inline */'); + expect(lines[1].html).toContain('/* spans'); + expect(lines[2].html).toBe('still comment'); + expect(lines[3].html).toContain('end */'); + expect(lines[3].html).toContain('"img.png"'); + expect(lines[4].html).toContain("hl-string"); + expect(lines[5].html).toContain('"unterminated'); + expect(lines[6].html).toBe('/* never closed'); + }); + + it("uses a default state and tolerates empty content", async () => { + const { highlightCssLine, highlightCssContent } = await loadCssHighlighter(); + + const rendered = highlightCssLine("a { b: 1; }"); + expect(rendered.html).toContain('b'); + expect(rendered.state).toEqual({ inBlockComment: false }); + + expect(highlightCssContent("")).toEqual([{ lineNumber: 1, html: "" }]); + }); +}); + +describe("frontend/syntax-highlighters/html", () => { + it("highlights doctype, comments, tags, attributes, and entities", async () => { + const { highlightHtmlContent } = await loadHtmlHighlighter(); + const lines = highlightHtmlContent( + [ + "", + "", + "", + "
", + "text bold tail", + '
', + ].join("\n"), + ); + + expect(lines[0].html).toBe('<!DOCTYPE html>'); + expect(lines[1].html).toBe('<!-- a comment -->'); + expect(lines[2].html).toContain('div'); + expect(lines[2].html).toContain('class'); + expect(lines[2].html).toContain('"box"'); + expect(lines[2].html).toContain("'main'"); + expect(lines[2].html).toContain('5'); + expect(lines[2].html).toContain('hidden'); + expect(lines[2].html).toContain('&amp;'); + expect(lines[3].html).toContain('/>'); + expect(lines[4].html).toContain("text "); + expect(lines[4].html).toContain(" tail"); + expect(lines[4].html).toContain('</'); + expect(lines[5].html).toContain('a'); + expect(lines[5].html).toContain('='); + expect(lines[5].html).toContain('"v"'); + expect(lines[5].html).toContain("~"); + }); + + it("switches into script and style modes across lines", async () => { + const { highlightHtmlContent } = await loadHtmlHighlighter(); + const lines = highlightHtmlContent( + [ + "", + "", + ].join("\n"), + ); + + expect(lines[0].html).toContain('style'); + expect(lines[1].html).toContain('color'); + expect(lines[2].html).toContain(''); + expect(lines[3].html).toContain('style'); + expect(lines[4].html).toContain('script'); + expect(lines[5].html).toContain('const'); + expect(lines[5].html).toContain('// js'); + expect(lines[6].html).toContain('class="hl-string"'); + expect(lines[7].html).toContain('script'); + }); + + it("handles inline script/style blocks that open and close on one line", async () => { + const { highlightHtmlContent } = await loadHtmlHighlighter(); + const lines = highlightHtmlContent( + [ + "after", + "", + "", + "