From 877f64e51766cbd3a1be950dfc923154b4253947 Mon Sep 17 00:00:00 2001 From: Jonathan Segev Date: Fri, 12 Jun 2026 10:46:26 -0400 Subject: [PATCH 01/29] chore(strands-ts): scaffold typescript project --- strands-command/scripts/typescript/.gitignore | 5 + .../scripts/typescript/package-lock.json | 3337 +++++++++++++++++ .../scripts/typescript/package.json | 22 + .../scripts/typescript/tsconfig.json | 16 + .../scripts/typescript/vitest.config.ts | 11 + 5 files changed, 3391 insertions(+) create mode 100644 strands-command/scripts/typescript/.gitignore create mode 100644 strands-command/scripts/typescript/package-lock.json create mode 100644 strands-command/scripts/typescript/package.json create mode 100644 strands-command/scripts/typescript/tsconfig.json create mode 100644 strands-command/scripts/typescript/vitest.config.ts diff --git a/strands-command/scripts/typescript/.gitignore b/strands-command/scripts/typescript/.gitignore new file mode 100644 index 0000000..13a8191 --- /dev/null +++ b/strands-command/scripts/typescript/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +dist/ +.artifact/ +# Root .gitignore ignores all package-lock.json; track this one for CI npm ci. +!package-lock.json diff --git a/strands-command/scripts/typescript/package-lock.json b/strands-command/scripts/typescript/package-lock.json new file mode 100644 index 0000000..d4c3393 --- /dev/null +++ b/strands-command/scripts/typescript/package-lock.json @@ -0,0 +1,3337 @@ +{ + "name": "strands-command-ts", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "strands-command-ts", + "dependencies": { + "@strands-agents/sdk": "^1.4.0", + "zod": "^4.1.12" + }, + "devDependencies": { + "@types/node": "^20.0.0", + "typescript": "^5.5.0", + "vitest": "^4.1.6" + } + }, + "node_modules/@aws-crypto/crc32": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-5.2.0.tgz", + "integrity": "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", + "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-js": "^5.2.0", + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha256-js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", + "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-crypto/supports-web-crypto": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", + "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", + "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.222.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/client-bedrock-runtime": { + "version": "3.1067.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-bedrock-runtime/-/client-bedrock-runtime-3.1067.0.tgz", + "integrity": "sha512-TC5/QFkPQU+PkEWb6oc/6AtwBR8tT7bAg/iecN40CjS/f6mzV7OwIyt0iw0K0hs8lq56TeGVIRIZcM4Hvb5V6g==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.974.20", + "@aws-sdk/credential-provider-node": "^3.972.55", + "@aws-sdk/eventstream-handler-node": "^3.972.21", + "@aws-sdk/middleware-eventstream": "^3.972.17", + "@aws-sdk/middleware-websocket": "^3.972.28", + "@aws-sdk/token-providers": "3.1067.0", + "@aws-sdk/types": "^3.973.12", + "@smithy/core": "^3.24.6", + "@smithy/fetch-http-handler": "^5.4.6", + "@smithy/node-http-handler": "^4.7.6", + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/core": { + "version": "3.974.20", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.974.20.tgz", + "integrity": "sha512-7sDi2B2N3mc3nf1nz6FyEx/FCrJ1N1QnBmraHHQNabFaeAh2IaOOLml48/rHOD1bICHgTRkbBgNTvUzEr5Z35g==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.12", + "@aws-sdk/xml-builder": "^3.972.29", + "@aws/lambda-invoke-store": "^0.2.2", + "@smithy/core": "^3.24.6", + "@smithy/signature-v4": "^5.4.6", + "@smithy/types": "^4.14.3", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-env": { + "version": "3.972.46", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.46.tgz", + "integrity": "sha512-+GPXVS2srMOlH74S+SmC1gVuP2TvUZ0siuC0onKO93q+udP+M72dmY8wJfVQ5CX9z/9X5A1HHwz5yRIGBtskvQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.20", + "@aws-sdk/types": "^3.973.12", + "@smithy/core": "^3.24.6", + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-http": { + "version": "3.972.48", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.48.tgz", + "integrity": "sha512-fA5loSdlocacRxyUXtpoHSMuk5rsIKRDzQYVMnMxjcmFeZshaJlJ8lymy/hYKji6sne/UmNGj5pxuEs6kq/Qcg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.20", + "@aws-sdk/types": "^3.973.12", + "@smithy/core": "^3.24.6", + "@smithy/fetch-http-handler": "^5.4.6", + "@smithy/node-http-handler": "^4.7.6", + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.972.53", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.53.tgz", + "integrity": "sha512-ZfdhIOR41q8TcWEnUac+gCOb+O2LBWdHLmjedXpXz4IEFW2ppNuFcm6p0sMTavpM+zD5TYfpH5Gp7guRyqSgsQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.20", + "@aws-sdk/credential-provider-env": "^3.972.46", + "@aws-sdk/credential-provider-http": "^3.972.48", + "@aws-sdk/credential-provider-login": "^3.972.52", + "@aws-sdk/credential-provider-process": "^3.972.46", + "@aws-sdk/credential-provider-sso": "^3.972.52", + "@aws-sdk/credential-provider-web-identity": "^3.972.52", + "@aws-sdk/nested-clients": "^3.997.20", + "@aws-sdk/types": "^3.973.12", + "@smithy/core": "^3.24.6", + "@smithy/credential-provider-imds": "^4.3.7", + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-login": { + "version": "3.972.52", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.52.tgz", + "integrity": "sha512-9hu2oR0qH7Fst5Tzdx+UWxm+w5zCXtErTLtOOW5hwwQc170CLwOeniRxyFY6s9mHfGEfC5zFukNBdKBwJR8mhQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.20", + "@aws-sdk/nested-clients": "^3.997.20", + "@aws-sdk/types": "^3.973.12", + "@smithy/core": "^3.24.6", + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-node": { + "version": "3.972.55", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.55.tgz", + "integrity": "sha512-zMGLa/dhESVqmCD7mmIFFKSwSFrJGScvCXcjvBZEVOOMauFS5JRQvLTMukFpMEFWiV6dTAlsen2ATDBulLPtbg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "^3.972.46", + "@aws-sdk/credential-provider-http": "^3.972.48", + "@aws-sdk/credential-provider-ini": "^3.972.53", + "@aws-sdk/credential-provider-process": "^3.972.46", + "@aws-sdk/credential-provider-sso": "^3.972.52", + "@aws-sdk/credential-provider-web-identity": "^3.972.52", + "@aws-sdk/types": "^3.973.12", + "@smithy/core": "^3.24.6", + "@smithy/credential-provider-imds": "^4.3.7", + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-process": { + "version": "3.972.46", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.46.tgz", + "integrity": "sha512-VUoNFBIjWrUN8NbFiQiuxQEgFjvziAlBRPK+ddh27aj65gk0BYu6bLZnrdrNZwpW6vAihtSUtEMQ1PUJ32QRPA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.20", + "@aws-sdk/types": "^3.973.12", + "@smithy/core": "^3.24.6", + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.972.52", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.52.tgz", + "integrity": "sha512-nb2/n4o/HQf+FVpVbZe9vCTFngmuDoIsltMgLAtjixaKzvzhB4J8WSDFyWgnErgLHk55ctWH+I4PU+LIHhyffg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.20", + "@aws-sdk/nested-clients": "^3.997.20", + "@aws-sdk/token-providers": "3.1066.0", + "@aws-sdk/types": "^3.973.12", + "@smithy/core": "^3.24.6", + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers": { + "version": "3.1066.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1066.0.tgz", + "integrity": "sha512-UqEUJq7dqa44hneLDUcX7UJy95cg8YqEWyakRpvIPnrNS3Mq+UlQHgCDGu5pvwAPtlIW4qcYbvW6reG6++FyvA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.20", + "@aws-sdk/nested-clients": "^3.997.20", + "@aws-sdk/types": "^3.973.12", + "@smithy/core": "^3.24.6", + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.972.52", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.52.tgz", + "integrity": "sha512-lKj6aRSGbqLmpYmM24bY7a1Xmfcq2vkE3hv8CSPYfc1yCu0BPu/XEJ1L4Fm61MsU6ULLNSG8UGsffNoFUBjESA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.20", + "@aws-sdk/nested-clients": "^3.997.20", + "@aws-sdk/types": "^3.973.12", + "@smithy/core": "^3.24.6", + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/eventstream-handler-node": { + "version": "3.972.21", + "resolved": "https://registry.npmjs.org/@aws-sdk/eventstream-handler-node/-/eventstream-handler-node-3.972.21.tgz", + "integrity": "sha512-mVC0hOmwGJmNFezZ+wM8Sqfap/LjsMavEf2Evl0YWrLAcrdZOEdjnY8nRvgakVViWJSGm2eJxLuPVHGdeV06kA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.12", + "@smithy/core": "^3.24.6", + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-eventstream": { + "version": "3.972.17", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-eventstream/-/middleware-eventstream-3.972.17.tgz", + "integrity": "sha512-tdbnXbw73ww62ABWP0G0Z/euvFowEEvAoi/zG4NaZo7HJFpfGho/Z65HyVzkJLT1cMsUregr4pTyxljlarT0wA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.12", + "@smithy/core": "^3.24.6", + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-websocket": { + "version": "3.972.28", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-websocket/-/middleware-websocket-3.972.28.tgz", + "integrity": "sha512-SCW06Zjugn86pq7+dxGnFcyWJuEWHT753HTU/Vj/OzVxP+NoShwdAr4ynxAcvWL883OgRVbSqW3ohnjIxwXjjw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.20", + "@aws-sdk/types": "^3.973.12", + "@smithy/core": "^3.24.6", + "@smithy/fetch-http-handler": "^5.4.6", + "@smithy/signature-v4": "^5.4.6", + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@aws-sdk/nested-clients": { + "version": "3.997.20", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.20.tgz", + "integrity": "sha512-IYJuLpXp2DEILVQpQOy0PMpkftv0AHEOCn52o0atyOaumA0CdWQ3klPyXdViGYLbNpESsVFMVybvHUeZAuiGxA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.974.20", + "@aws-sdk/signature-v4-multi-region": "^3.996.34", + "@aws-sdk/types": "^3.973.12", + "@smithy/core": "^3.24.6", + "@smithy/fetch-http-handler": "^5.4.6", + "@smithy/node-http-handler": "^4.7.6", + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/signature-v4-multi-region": { + "version": "3.996.34", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.34.tgz", + "integrity": "sha512-mx1L5qlumSOt/nKM3BFaHE2HVkWwz0i4Bw0pyYO42FfX/FeLlo8YI6csC0gSPprEk6fTIqI+CZN9RwUwKd5krQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.12", + "@smithy/signature-v4": "^5.4.6", + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/token-providers": { + "version": "3.1067.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1067.0.tgz", + "integrity": "sha512-LDQ+3bleiu0bq2YwUoBZsWH7ixIkurF1M8SUtLlQ/4n9AUJtY5W8lV7a2bTc78COyV9OGninUdmnQ+AjQHvs/A==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.20", + "@aws-sdk/nested-clients": "^3.997.20", + "@aws-sdk/types": "^3.973.12", + "@smithy/core": "^3.24.6", + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/types": { + "version": "3.973.12", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.12.tgz", + "integrity": "sha512-43ajd1NF0RMgX5k0hxCNUyEdrtFUsb2aHT2QvpktSC/2Eyb2Jr/JPVqdp0XIoaHWikZJq5tNWSLO6kB5q2eMCA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/util-locate-window": { + "version": "3.965.7", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.965.7.tgz", + "integrity": "sha512-M0D6oIpohdNHjc7udzTHEQyot0+0iuA36jc2I9Hps+f/GtKi2HO/pyijQnCnNcwZqLB5+rtn81z3eZK/GyjAmA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/xml-builder": { + "version": "3.972.29", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.29.tgz", + "integrity": "sha512-fk0niuGFxfi8yIJuMVM4mhwObkiQSuwZFj3tAPrLVx64Pk3BkrEIpqjzHKY4hKoEBUD6Jg/S74Zj9jy+5F3DnQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.3", + "fast-xml-parser": "5.7.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws/lambda-invoke-store": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.4.tgz", + "integrity": "sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@hono/node-server": { + "version": "1.19.14", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.14.tgz", + "integrity": "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz", + "integrity": "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@hono/node-server": "^1.19.9", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.2.1", + "express-rate-limit": "^8.2.1", + "hono": "^4.11.4", + "jose": "^6.1.3", + "json-schema-typed": "^8.0.2", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz", + "integrity": "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@nodable/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-9uGyhaQavEUMC8AIddIjau4NsnsXhou+j5sBAGojCM1oxmQpVKTWR/9JxABD6UAv12vpIms55fPZKFQEhG6uBg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/nodable" + } + ], + "license": "MIT" + }, + "node_modules/@opentelemetry/api": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.1.tgz", + "integrity": "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==", + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.133.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz", + "integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz", + "integrity": "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz", + "integrity": "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz", + "integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz", + "integrity": "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz", + "integrity": "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz", + "integrity": "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz", + "integrity": "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz", + "integrity": "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz", + "integrity": "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz", + "integrity": "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz", + "integrity": "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz", + "integrity": "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz", + "integrity": "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz", + "integrity": "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz", + "integrity": "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@smithy/core": { + "version": "3.24.6", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.24.6.tgz", + "integrity": "sha512-wBXDRup6UU97VKyaiRo8AssnfStPtG0oAAfpq/bC0a1YYau8pM86YB4kM6ccoVi1mS8l/UHbn9oDM+7uozr/ug==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/crc32": "5.2.0", + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/credential-provider-imds": { + "version": "4.3.8", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.3.8.tgz", + "integrity": "sha512-5cAM+KZC02sTqDt6NaLXyu50M/GNMd1eTzDVR8Lb0BBsVtu7RWHo47VPPEEv1vt3Yub6uzr+M5FHC+GtoT0USg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.24.6", + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/fetch-http-handler": { + "version": "5.4.6", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.4.6.tgz", + "integrity": "sha512-FEwEYJ1jlBKdhe9TPzfghEi1bP55ZeEImlDkEa62bBBYzUcnB6RUCyuiS2mqKt6ZVjUbBgcNhzfIctH+Hevx9g==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.24.6", + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/node-http-handler": { + "version": "4.7.7", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.7.7.tgz", + "integrity": "sha512-ZAFvHXrEk6K180EVhmZVg8GU5pUH5BSFqRs27JW3j1qEFx9YyYwWFx17x/MHcjALYimGAji7qEOlF1++be+G5A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.24.6", + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/signature-v4": { + "version": "5.4.6", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.4.6.tgz", + "integrity": "sha512-Ojg4B6oIDlIr1R86xCDJt1zJWnYa0VINmqdjfe9qxWjdRivHalZ3iSlQgVqYbW0MdpFOC5XfHEWsnbmdnpIILQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.24.6", + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/types": { + "version": "4.14.3", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.14.3.tgz", + "integrity": "sha512-YupL0ZWmFtJexUN2cHzkvvF/b9pKrtAIfT1o7/oY/Ppu8IYeZ+lDPM5vZdQJaSeA132dJCqojjGC9NhXeF71VQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@strands-agents/sdk": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@strands-agents/sdk/-/sdk-1.4.0.tgz", + "integrity": "sha512-sgNtwJNECDSX1VL3JLhGrFiLYs8qFHAoBhwe7cpEpuY0SWK+c6e8sPkAeAeBcoe2OEKzBwTxjefeGR+yV9TjSg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/client-bedrock-runtime": "^3.1037.0", + "@types/json-schema": "^7.0.15", + "uuid": "^14.0.0", + "yaml": "^2.8.3" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@a2a-js/sdk": "^0.3.10", + "@ai-sdk/provider": "^3.0.0", + "@anthropic-ai/sdk": "^0.92.0", + "@aws-sdk/client-s3": "^3.943.0", + "@aws/bedrock-token-generator": "^1.1.0", + "@google/genai": "^1.40.0", + "@modelcontextprotocol/sdk": "^1.25.2", + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/exporter-metrics-otlp-http": "^0.214.0", + "@opentelemetry/exporter-trace-otlp-http": "^0.214.0", + "@opentelemetry/resources": "^2.6.1", + "@opentelemetry/sdk-metrics": "^2.6.1", + "@opentelemetry/sdk-trace-base": "^2.6.1", + "@opentelemetry/sdk-trace-node": "^2.6.1", + "@smithy/types": "^4.0.0", + "express": "^5.1.0", + "openai": "^6.7.0", + "zod": "^4.1.12" + }, + "peerDependenciesMeta": { + "@a2a-js/sdk": { + "optional": true + }, + "@ai-sdk/provider": { + "optional": true + }, + "@anthropic-ai/sdk": { + "optional": true + }, + "@aws-sdk/client-s3": { + "optional": true + }, + "@aws/bedrock-token-generator": { + "optional": true + }, + "@google/genai": { + "optional": true + }, + "@opentelemetry/exporter-metrics-otlp-http": { + "optional": true + }, + "@opentelemetry/exporter-trace-otlp-http": { + "optional": true + }, + "@opentelemetry/resources": { + "optional": true + }, + "@opentelemetry/sdk-metrics": { + "optional": true + }, + "@opentelemetry/sdk-trace-base": { + "optional": true + }, + "@opentelemetry/sdk-trace-node": { + "optional": true + }, + "@smithy/types": { + "optional": true + }, + "express": { + "optional": true + }, + "openai": { + "optional": true + } + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", + "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.43", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz", + "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@vitest/expect": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.8.tgz", + "integrity": "sha512-h3nDO677RDLEGlBxyQ5CW8RlMThSKSRLUePLOx09gNIWRL40edgA1GCZSZgf1W55MFAG6/Sw14KeaAnqv0NKdQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.8", + "@vitest/utils": "4.1.8", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.8.tgz", + "integrity": "sha512-LEiN/xe4OSIbKe9HQIp5OC24agGD9J5CnmMgsLohVVoOPWL9a2sBoR6VBx43jQZb7Kr1l4RCuyCJzcAa0+dojw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.8", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.8.tgz", + "integrity": "sha512-9GasEBxpZ1VYIpqHf/0+YGg121uSNwCKOJqIrTwWP/TB7DmFCiaBpNl3aPZzoLWfWkuqhbH8vJIVobZkvdo2cA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.8.tgz", + "integrity": "sha512-EmVxeBAfMJvycdjd6Hm+RbFBbA9fKvo0Kx37hNpBYoYeavH3RNsBXWDooR1mgD52dCrxIIuP7UotpfiwOikvcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.8", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.8.tgz", + "integrity": "sha512-acfZboRmAIf05DEKcBQy33VXojFJjtUdLyo7oOmV9kebb2xdU01UknNiPuPZoJZQyO7DF0gZdTGTpeAzET9QPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.8", + "@vitest/utils": "4.1.8", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.8.tgz", + "integrity": "sha512-6EevtBp6OZOPF7bmz36HrGMeP3txgVSrgebWxHOafDXGkhIzfXK14f8KF6MuFfgXXUeHxmpD3BQxkV00/3s5mA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.8.tgz", + "integrity": "sha512-uOJamYALNhfJ6iolExyQM40yIQwDqYnkKtQ5VCiSe17E33H0aQ/u+1GlRuz4LZBk6Mm3sg90G9hEbmEt37C1Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.8", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "peer": true, + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/anynum": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/anynum/-/anynum-1.0.0.tgz", + "integrity": "sha512-xjR9/zBVnUOP6ztMIIgShjsxui80nQUQH+5xJnvrYLs+90bF25/KJqaAi8mk+B4RDtX1Nspi6fmp4YTEts8SfA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "license": "MIT", + "peer": true, + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/bowser": { + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz", + "integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==", + "license": "MIT" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "peer": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "peer": true, + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "peer": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "peer": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "peer": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT", + "peer": true + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", + "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "peer": true, + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT", + "peer": true + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "license": "MIT", + "peer": true, + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.0.tgz", + "integrity": "sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "peer": true, + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.2.tgz", + "integrity": "sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==", + "license": "MIT", + "peer": true, + "dependencies": { + "ip-address": "^10.2.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT", + "peer": true + }, + "node_modules/fast-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/fast-xml-builder": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.2.0.tgz", + "integrity": "sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "path-expression-matcher": "^1.5.0", + "xml-naming": "^0.1.0" + } + }, + "node_modules/fast-xml-parser": { + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.7.3.tgz", + "integrity": "sha512-C0AaNuC+mscy6vrAQKAc/rMq+zAPHodfHGZu4sGVehvAQt/JLG1O5zEcYcXSY5zSqr4YVgxsB+pHXTq0i7eDlg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "@nodable/entities": "^2.1.0", + "fast-xml-builder": "^1.1.7", + "path-expression-matcher": "^1.5.0", + "strnum": "^2.2.3" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "peer": true, + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "peer": true, + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "peer": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hono": { + "version": "4.12.25", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.25.tgz", + "integrity": "sha512-2NFaIyNVgJmBs/ecmtGzlmluTFs5cHEWGTdu0t1HBwYzoGXOL5nUQBRMXsXWla5i4KkG//QMzVP88m1+I3fdAQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "peer": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC", + "peer": true + }, + "node_modules/ip-address": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT", + "peer": true + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC", + "peer": true + }, + "node_modules/jose": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.3.tgz", + "integrity": "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==", + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT", + "peer": true + }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "license": "BSD-2-Clause", + "peer": true + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "peer": true, + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT", + "peer": true + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/obug": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.3.tgz", + "integrity": "sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "peer": true, + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "peer": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-expression-matcher": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.5.0.tgz", + "integrity": "sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "license": "MIT", + "peer": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pkce-challenge": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", + "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "peer": true, + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.15.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", + "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "peer": true, + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rolldown": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz", + "integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.133.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.3", + "@rolldown/binding-darwin-arm64": "1.0.3", + "@rolldown/binding-darwin-x64": "1.0.3", + "@rolldown/binding-freebsd-x64": "1.0.3", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.3", + "@rolldown/binding-linux-arm64-gnu": "1.0.3", + "@rolldown/binding-linux-arm64-musl": "1.0.3", + "@rolldown/binding-linux-ppc64-gnu": "1.0.3", + "@rolldown/binding-linux-s390x-gnu": "1.0.3", + "@rolldown/binding-linux-x64-gnu": "1.0.3", + "@rolldown/binding-linux-x64-musl": "1.0.3", + "@rolldown/binding-openharmony-arm64": "1.0.3", + "@rolldown/binding-wasm32-wasi": "1.0.3", + "@rolldown/binding-win32-arm64-msvc": "1.0.3", + "@rolldown/binding-win32-x64-msvc": "1.0.3" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT", + "peer": true + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "peer": true, + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC", + "peer": true + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "peer": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "peer": true, + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "peer": true, + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "peer": true, + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/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/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/std-env": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", + "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/strnum": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.4.0.tgz", + "integrity": "sha512-sHrVyWWdq28RbhjuJdZsA1SnGRJV6NiXbk6AXBxDOsgAcA+lmpUZCYjOdLBxkXMwis6RRe7dlZt4VlIWFVzkmg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "anynum": "^1.0.0" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type-is": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", + "license": "MIT", + "peer": true, + "dependencies": { + "content-type": "^2.0.0", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/type-is/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/uuid": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.0.tgz", + "integrity": "sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist-node/bin/uuid" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vite": { + "version": "8.0.16", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz", + "integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.15", + "rolldown": "1.0.3", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.18", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.8.tgz", + "integrity": "sha512-flY6ScbCIt9HThs+C5HS7jvGOB560DJtk/Z15IQROTA6zEy49Nh8T/dofWTQL+n3vswqn87sbJNiuqw1SDp5Ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.8", + "@vitest/mocker": "4.1.8", + "@vitest/pretty-format": "4.1.8", + "@vitest/runner": "4.1.8", + "@vitest/snapshot": "4.1.8", + "@vitest/spy": "4.1.8", + "@vitest/utils": "4.1.8", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.8", + "@vitest/browser-preview": "4.1.8", + "@vitest/browser-webdriverio": "4.1.8", + "@vitest/coverage-istanbul": "4.1.8", + "@vitest/coverage-v8": "4.1.8", + "@vitest/ui": "4.1.8", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "peer": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "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==", + "license": "ISC", + "peer": true + }, + "node_modules/xml-naming": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.1.0.tgz", + "integrity": "sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.25.2", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", + "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", + "license": "ISC", + "peer": true, + "peerDependencies": { + "zod": "^3.25.28 || ^4" + } + } + } +} diff --git a/strands-command/scripts/typescript/package.json b/strands-command/scripts/typescript/package.json new file mode 100644 index 0000000..7b4da51 --- /dev/null +++ b/strands-command/scripts/typescript/package.json @@ -0,0 +1,22 @@ +{ + "name": "strands-command-ts", + "private": true, + "type": "module", + "main": "dist/runner.js", + "scripts": { + "build": "tsc", + "test": "vitest run", + "test:watch": "vitest", + "runner": "node dist/runner.js", + "finalize": "node dist/writeExecutor.js" + }, + "dependencies": { + "@strands-agents/sdk": "^1.4.0", + "zod": "^4.1.12" + }, + "devDependencies": { + "@types/node": "^20.0.0", + "typescript": "^5.5.0", + "vitest": "^4.1.6" + } +} diff --git a/strands-command/scripts/typescript/tsconfig.json b/strands-command/scripts/typescript/tsconfig.json new file mode 100644 index 0000000..3bb8a93 --- /dev/null +++ b/strands-command/scripts/typescript/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "Bundler", + "outDir": "dist", + "rootDir": "src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "resolveJsonModule": true, + "declaration": false + }, + "include": ["src"], + "exclude": ["dist", "node_modules", "tests"] +} diff --git a/strands-command/scripts/typescript/vitest.config.ts b/strands-command/scripts/typescript/vitest.config.ts new file mode 100644 index 0000000..e874d99 --- /dev/null +++ b/strands-command/scripts/typescript/vitest.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + include: ['tests/**/*.test.ts'], + environment: 'node', + // Several test files create/delete the same .artifact JSONL path; parallel + // file execution would race on it. + fileParallelism: false, + }, +}) From 20544cc3d24ba086749f51786b60e6ddcc485b5f Mon Sep 17 00:00:00 2001 From: Jonathan Segev Date: Fri, 12 Jun 2026 10:46:26 -0400 Subject: [PATCH 02/29] test(strands-ts): add vitest smoke test --- strands-command/scripts/typescript/src/version.ts | 1 + strands-command/scripts/typescript/tests/version.test.ts | 8 ++++++++ 2 files changed, 9 insertions(+) create mode 100644 strands-command/scripts/typescript/src/version.ts create mode 100644 strands-command/scripts/typescript/tests/version.test.ts diff --git a/strands-command/scripts/typescript/src/version.ts b/strands-command/scripts/typescript/src/version.ts new file mode 100644 index 0000000..35ecd11 --- /dev/null +++ b/strands-command/scripts/typescript/src/version.ts @@ -0,0 +1 @@ +export const RUNNER_NAME = 'strands-ts' diff --git a/strands-command/scripts/typescript/tests/version.test.ts b/strands-command/scripts/typescript/tests/version.test.ts new file mode 100644 index 0000000..a2c54cb --- /dev/null +++ b/strands-command/scripts/typescript/tests/version.test.ts @@ -0,0 +1,8 @@ +import { describe, it, expect } from 'vitest' +import { RUNNER_NAME } from '../src/version' + +describe('version', () => { + it('exposes the runner name', () => { + expect(RUNNER_NAME).toBe('strands-ts') + }) +}) From 45065964f896550c95201156da51bb9f00f545ed Mon Sep 17 00:00:00 2001 From: Jonathan Segev Date: Fri, 12 Jun 2026 10:57:43 -0400 Subject: [PATCH 03/29] fix(strands-ts): use NodeNext module resolution for direct node execution --- strands-command/scripts/typescript/tsconfig.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/strands-command/scripts/typescript/tsconfig.json b/strands-command/scripts/typescript/tsconfig.json index 3bb8a93..21c112d 100644 --- a/strands-command/scripts/typescript/tsconfig.json +++ b/strands-command/scripts/typescript/tsconfig.json @@ -1,8 +1,8 @@ { "compilerOptions": { "target": "ES2022", - "module": "ESNext", - "moduleResolution": "Bundler", + "module": "NodeNext", + "moduleResolution": "NodeNext", "outDir": "dist", "rootDir": "src", "strict": true, From deae6768877f839f547154434ca07898256c38da Mon Sep 17 00:00:00 2001 From: Jonathan Segev Date: Fri, 12 Jun 2026 11:06:34 -0400 Subject: [PATCH 04/29] feat(strands-ts): add Finding/ReviewOutput zod schemas --- .../scripts/typescript/src/findings.ts | 22 ++++++++++++++ .../scripts/typescript/tests/findings.test.ts | 30 +++++++++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 strands-command/scripts/typescript/src/findings.ts create mode 100644 strands-command/scripts/typescript/tests/findings.test.ts diff --git a/strands-command/scripts/typescript/src/findings.ts b/strands-command/scripts/typescript/src/findings.ts new file mode 100644 index 0000000..febe556 --- /dev/null +++ b/strands-command/scripts/typescript/src/findings.ts @@ -0,0 +1,22 @@ +import { z } from 'zod' + +export const LENSES = ['adherence', 'api', 'bug', 'history', 'test'] as const + +export const FindingSchema = z.object({ + lens: z.string(), + description: z.string(), + file: z.string(), + line: z.number().int(), + startLine: z.number().int().optional(), + reason: z.string(), + score: z.number().int().min(0).max(100), +}) + +export type Finding = z.infer + +// The orchestrator emits this as structuredOutput. +export const ReviewOutputSchema = z.object({ + findings: z.array(FindingSchema), +}) + +export type ReviewOutput = z.infer diff --git a/strands-command/scripts/typescript/tests/findings.test.ts b/strands-command/scripts/typescript/tests/findings.test.ts new file mode 100644 index 0000000..d7ba5c0 --- /dev/null +++ b/strands-command/scripts/typescript/tests/findings.test.ts @@ -0,0 +1,30 @@ +import { describe, it, expect } from 'vitest' +import { FindingSchema, ReviewOutputSchema } from '../src/findings' + +describe('FindingSchema', () => { + it('accepts a complete finding', () => { + const f = FindingSchema.parse({ + lens: 'bug', description: 'off-by-one', file: 'a.py', + line: 10, startLine: 8, reason: 'loop bound', score: 90, + }) + expect(f.startLine).toBe(8) + }) + + it('defaults optional startLine to undefined', () => { + const f = FindingSchema.parse({ + lens: 'api', description: 'd', file: 'x.ts', line: 1, reason: 'r', score: 50, + }) + expect(f.startLine).toBeUndefined() + }) + + it('rejects an out-of-range score', () => { + expect(() => FindingSchema.parse({ + lens: 'bug', description: 'd', file: 'x', line: 1, reason: 'r', score: 150, + })).toThrow() + }) + + it('parses a review output wrapping a findings array', () => { + const out = ReviewOutputSchema.parse({ findings: [] }) + expect(out.findings).toEqual([]) + }) +}) From b169320449cab221a107bc7522b5dc67161cc652 Mon Sep 17 00:00:00 2001 From: Jonathan Segev Date: Fri, 12 Jun 2026 11:07:36 -0400 Subject: [PATCH 05/29] feat(strands-ts): add deterministic score/dedupe/cap filter --- .../scripts/typescript/src/scoreAndFilter.ts | 20 +++++++++++++ .../typescript/tests/scoreAndFilter.test.ts | 29 +++++++++++++++++++ 2 files changed, 49 insertions(+) create mode 100644 strands-command/scripts/typescript/src/scoreAndFilter.ts create mode 100644 strands-command/scripts/typescript/tests/scoreAndFilter.test.ts diff --git a/strands-command/scripts/typescript/src/scoreAndFilter.ts b/strands-command/scripts/typescript/src/scoreAndFilter.ts new file mode 100644 index 0000000..905642a --- /dev/null +++ b/strands-command/scripts/typescript/src/scoreAndFilter.ts @@ -0,0 +1,20 @@ +import type { Finding } from './findings.js' + +export const THRESHOLD = 80 +export const MAX_COMMENTS = 15 + +export function scoreAndFilter(findings: Finding[]): Finding[] { + const passing = findings.filter((f) => f.score >= THRESHOLD) + + // Dedupe on file+line+description, keeping the highest score. + const best = new Map() + for (const f of passing) { + const key = `${f.file}:${f.line}:${f.description}` + const existing = best.get(key) + if (!existing || f.score > existing.score) best.set(key, f) + } + + return [...best.values()] + .sort((a, b) => b.score - a.score) + .slice(0, MAX_COMMENTS) +} diff --git a/strands-command/scripts/typescript/tests/scoreAndFilter.test.ts b/strands-command/scripts/typescript/tests/scoreAndFilter.test.ts new file mode 100644 index 0000000..257f30f --- /dev/null +++ b/strands-command/scripts/typescript/tests/scoreAndFilter.test.ts @@ -0,0 +1,29 @@ +import { describe, it, expect } from 'vitest' +import { scoreAndFilter, THRESHOLD, MAX_COMMENTS } from '../src/scoreAndFilter' +import type { Finding } from '../src/findings' + +function f(score: number, line = 1, file = 'a.py', description = 'd'): Finding { + return { lens: 'bug', description, file, line, reason: 'r', score } +} + +describe('scoreAndFilter', () => { + it('drops findings below the threshold', () => { + const kept = scoreAndFilter([f(79, 1), f(80, 2), f(100, 3)]) + expect(kept.map((x) => x.score)).toEqual([100, 80]) // sorted desc, 79 dropped + }) + + it('dedupes same file+line+description keeping the highest score', () => { + const kept = scoreAndFilter([f(90), f(95)]) + expect(kept).toHaveLength(1) + expect(kept[0].score).toBe(95) + }) + + it('caps the number of findings', () => { + const many = Array.from({ length: 40 }, (_, i) => f(90, i)) + expect(scoreAndFilter(many)).toHaveLength(MAX_COMMENTS) + }) + + it('exposes a threshold of 80', () => { + expect(THRESHOLD).toBe(80) + }) +}) From 7b9d53685a60e11e6ea253088d67f373ca3d1502 Mon Sep 17 00:00:00 2001 From: Jonathan Segev Date: Fri, 12 Jun 2026 11:08:18 -0400 Subject: [PATCH 06/29] feat(strands-ts): add deferred-write safeguard wrapper --- .../typescript/src/tools/deferredWrite.ts | 35 +++++++++++++++++++ .../typescript/tests/deferredWrite.test.ts | 33 +++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 strands-command/scripts/typescript/src/tools/deferredWrite.ts create mode 100644 strands-command/scripts/typescript/tests/deferredWrite.test.ts diff --git a/strands-command/scripts/typescript/src/tools/deferredWrite.ts b/strands-command/scripts/typescript/src/tools/deferredWrite.ts new file mode 100644 index 0000000..5bdf7b5 --- /dev/null +++ b/strands-command/scripts/typescript/src/tools/deferredWrite.ts @@ -0,0 +1,35 @@ +import { appendFileSync, mkdirSync } from 'node:fs' +import { dirname } from 'node:path' + +export const ARTIFACT_PATH = '.artifact/write_operations.jsonl' + +export interface WriteOperation { + timestamp: string + function: string + kwargs: Record +} + +export interface WriteMode { + write: boolean +} + +export function writeEnabled(): WriteMode { + return { write: process.env.GITHUB_WRITE === 'true' } +} + +export async function recordOrCall( + mode: WriteMode, + fnName: string, + kwargs: Record, + call: () => Promise, +): Promise { + if (mode.write) return call() + const entry: WriteOperation = { + timestamp: new Date().toISOString(), + function: fnName, + kwargs, + } + mkdirSync(dirname(ARTIFACT_PATH), { recursive: true }) + appendFileSync(ARTIFACT_PATH, JSON.stringify(entry) + '\n') + return `Operation deferred: ${fnName}` +} diff --git a/strands-command/scripts/typescript/tests/deferredWrite.test.ts b/strands-command/scripts/typescript/tests/deferredWrite.test.ts new file mode 100644 index 0000000..ed22dc5 --- /dev/null +++ b/strands-command/scripts/typescript/tests/deferredWrite.test.ts @@ -0,0 +1,33 @@ +import { describe, it, expect, beforeEach, afterEach } from 'vitest' +import { readFileSync, rmSync, existsSync } from 'node:fs' +import { recordOrCall, ARTIFACT_PATH } from '../src/tools/deferredWrite' + +describe('recordOrCall', () => { + beforeEach(() => { if (existsSync(ARTIFACT_PATH)) rmSync(ARTIFACT_PATH) }) + afterEach(() => { if (existsSync(ARTIFACT_PATH)) rmSync(ARTIFACT_PATH) }) + + it('defers (records, does not call) when write disabled', async () => { + let called = false + const result = await recordOrCall( + { write: false }, 'addPrComment', { prNumber: 1, body: 'hi' }, + async () => { called = true; return 'posted' }, + ) + expect(called).toBe(false) + expect(result).toMatch(/deferred/i) + const line = JSON.parse(readFileSync(ARTIFACT_PATH, 'utf8').trim()) + expect(line.function).toBe('addPrComment') + expect(line.kwargs).toEqual({ prNumber: 1, body: 'hi' }) + expect(typeof line.timestamp).toBe('string') + }) + + it('calls through when write enabled', async () => { + let called = false + const result = await recordOrCall( + { write: true }, 'addPrComment', { prNumber: 1, body: 'hi' }, + async () => { called = true; return 'posted' }, + ) + expect(called).toBe(true) + expect(result).toBe('posted') + expect(existsSync(ARTIFACT_PATH)).toBe(false) + }) +}) From d1fe60f1f3bde3b44476089ddcdcf6cb006fbe8a Mon Sep 17 00:00:00 2001 From: Jonathan Segev Date: Fri, 12 Jun 2026 11:26:13 -0400 Subject: [PATCH 07/29] fix(strands-ts): async artifact writes, schema comment, test coverage --- strands-command/scripts/typescript/src/findings.ts | 2 ++ .../scripts/typescript/src/tools/deferredWrite.ts | 8 +++++--- .../scripts/typescript/tests/deferredWrite.test.ts | 14 ++++++++++++++ .../typescript/tests/scoreAndFilter.test.ts | 4 ++++ 4 files changed, 25 insertions(+), 3 deletions(-) diff --git a/strands-command/scripts/typescript/src/findings.ts b/strands-command/scripts/typescript/src/findings.ts index febe556..9c5ca22 100644 --- a/strands-command/scripts/typescript/src/findings.ts +++ b/strands-command/scripts/typescript/src/findings.ts @@ -3,6 +3,8 @@ import { z } from 'zod' export const LENSES = ['adherence', 'api', 'bug', 'history', 'test'] as const export const FindingSchema = z.object({ + // Deliberately a free string, not z.enum(LENSES): the custom_reviewer + // meta-agent emits findings under ad-hoc lens names. lens: z.string(), description: z.string(), file: z.string(), diff --git a/strands-command/scripts/typescript/src/tools/deferredWrite.ts b/strands-command/scripts/typescript/src/tools/deferredWrite.ts index 5bdf7b5..3f271a7 100644 --- a/strands-command/scripts/typescript/src/tools/deferredWrite.ts +++ b/strands-command/scripts/typescript/src/tools/deferredWrite.ts @@ -1,4 +1,4 @@ -import { appendFileSync, mkdirSync } from 'node:fs' +import { appendFile, mkdir } from 'node:fs/promises' import { dirname } from 'node:path' export const ARTIFACT_PATH = '.artifact/write_operations.jsonl' @@ -29,7 +29,9 @@ export async function recordOrCall( function: fnName, kwargs, } - mkdirSync(dirname(ARTIFACT_PATH), { recursive: true }) - appendFileSync(ARTIFACT_PATH, JSON.stringify(entry) + '\n') + await mkdir(dirname(ARTIFACT_PATH), { recursive: true }) + // Single-process runner appends small lines; concurrent interleaving is not + // a concern here. Revisit if the runner ever becomes multi-process. + await appendFile(ARTIFACT_PATH, JSON.stringify(entry) + '\n') return `Operation deferred: ${fnName}` } diff --git a/strands-command/scripts/typescript/tests/deferredWrite.test.ts b/strands-command/scripts/typescript/tests/deferredWrite.test.ts index ed22dc5..37c0450 100644 --- a/strands-command/scripts/typescript/tests/deferredWrite.test.ts +++ b/strands-command/scripts/typescript/tests/deferredWrite.test.ts @@ -30,4 +30,18 @@ describe('recordOrCall', () => { expect(result).toBe('posted') expect(existsSync(ARTIFACT_PATH)).toBe(false) }) + + it('propagates call errors in write mode', async () => { + await expect(recordOrCall( + { write: true }, 'addPrComment', {}, async () => { throw new Error('boom') }, + )).rejects.toThrow('boom') + }) + + it('appends one JSONL line per deferred call', async () => { + await recordOrCall({ write: false }, 'a', { n: 1 }, async () => 'x') + await recordOrCall({ write: false }, 'b', { n: 2 }, async () => 'y') + const lines = readFileSync(ARTIFACT_PATH, 'utf8').trim().split('\n') + expect(lines).toHaveLength(2) + expect(lines.map((l) => JSON.parse(l).function)).toEqual(['a', 'b']) + }) }) diff --git a/strands-command/scripts/typescript/tests/scoreAndFilter.test.ts b/strands-command/scripts/typescript/tests/scoreAndFilter.test.ts index 257f30f..8bc0616 100644 --- a/strands-command/scripts/typescript/tests/scoreAndFilter.test.ts +++ b/strands-command/scripts/typescript/tests/scoreAndFilter.test.ts @@ -26,4 +26,8 @@ describe('scoreAndFilter', () => { it('exposes a threshold of 80', () => { expect(THRESHOLD).toBe(80) }) + + it('returns empty for empty input', () => { + expect(scoreAndFilter([])).toEqual([]) + }) }) From 57670b104c38f9ac88b8dc0edfb4556b06d52163 Mon Sep 17 00:00:00 2001 From: Jonathan Segev Date: Fri, 12 Jun 2026 12:46:52 -0400 Subject: [PATCH 08/29] feat(strands-ts): add github read tools and deferred addPrComment --- .../scripts/typescript/src/tools/github.ts | 119 ++++++++++++++++++ .../scripts/typescript/tests/github.test.ts | 36 ++++++ 2 files changed, 155 insertions(+) create mode 100644 strands-command/scripts/typescript/src/tools/github.ts create mode 100644 strands-command/scripts/typescript/tests/github.test.ts diff --git a/strands-command/scripts/typescript/src/tools/github.ts b/strands-command/scripts/typescript/src/tools/github.ts new file mode 100644 index 0000000..f7c7ebd --- /dev/null +++ b/strands-command/scripts/typescript/src/tools/github.ts @@ -0,0 +1,119 @@ +// src/tools/github.ts +import { tool } from '@strands-agents/sdk' +import { z } from 'zod' +import { recordOrCall, type WriteMode } from './deferredWrite.js' + +function repoOrEnv(repo?: string): string { + const r = repo ?? process.env.GITHUB_REPOSITORY + if (!r) throw new Error('GITHUB_REPOSITORY not set') + return r +} + +async function githubRequest( + method: string, + endpoint: string, + repo?: string, + body?: unknown, +): Promise { + const r = repoOrEnv(repo) + const token = process.env.GITHUB_TOKEN + if (!token) throw new Error('GITHUB_TOKEN not set') + const res = await fetch(`https://api.github.com/repos/${r}/${endpoint}`, { + method, + headers: { + Authorization: `Bearer ${token}`, + Accept: 'application/vnd.github.v3+json', + }, + body: body ? JSON.stringify(body) : undefined, + }) + if (!res.ok) throw new Error(`GitHub ${method} ${endpoint} failed: ${res.status}`) + return res.json() +} + +// Indirection seam for tests: ESM namespace exports are sealed, so tests spy on +// this object's property instead of the module binding. All internal calls go +// through _http.request. +export const _http = { request: githubRequest } + +// ---- Read helpers ---- +export async function getPrComments(prNumber: number, repo?: string): Promise { + const data = await _http.request('GET', `issues/${prNumber}/comments`, repo, undefined) + return JSON.stringify(data) +} + +export async function getPrDiffRaw(prNumber: number, repo?: string): Promise { + const data = await _http.request('GET', `pulls/${prNumber}/files`, repo, undefined) + return JSON.stringify(data) +} + +export async function getFileContentsRaw(path: string, ref: string, repo?: string): Promise { + const data = await _http.request('GET', `contents/${path}?ref=${ref}`, repo, undefined) + return JSON.stringify(data) +} + +export async function getFileHistoryRaw(path: string, repo?: string): Promise { + // Recent commits touching this file — the history lens's data source + // (no shell/git in the TS runner; history comes from the API). + const data = await _http.request('GET', `commits?path=${encodeURIComponent(path)}&per_page=20`, repo, undefined) + return JSON.stringify(data) +} + +// ---- Write fn (shared by agent tool + writeExecutor) ---- +export interface AddPrCommentArgs { + prNumber: number + body: string + path?: string + line?: number + startLine?: number + repo?: string +} + +export async function addPrComment(mode: WriteMode, args: AddPrCommentArgs): Promise { + return recordOrCall(mode, 'addPrComment', { ...args }, async () => { + const endpoint = args.path + ? `pulls/${args.prNumber}/comments` + : `issues/${args.prNumber}/comments` + const body: Record = { body: args.body } + if (args.path) { + body.path = args.path + body.line = args.line + body.side = 'RIGHT' + if (args.startLine !== undefined) { + body.start_line = args.startLine + body.start_side = 'RIGHT' + } + } + const res = await _http.request('POST', endpoint, args.repo, body) + return JSON.stringify(res) + }) as Promise +} + +// ---- Agent-facing tool() wrappers (read-only; agent never posts directly) ---- +export function readTools(repo: string) { + return [ + tool({ + name: 'get_pr_diff', + description: 'Get the list of changed files and their diffs for a PR.', + inputSchema: z.object({ prNumber: z.number().int() }), + callback: async (input) => getPrDiffRaw(input.prNumber, repo), + }), + tool({ + name: 'get_file_contents', + description: 'Get the full contents of a file at a git ref.', + inputSchema: z.object({ path: z.string(), ref: z.string() }), + callback: async (input) => getFileContentsRaw(input.path, input.ref, repo), + }), + tool({ + name: 'get_pr_comments', + description: 'Get existing comments on a PR.', + inputSchema: z.object({ prNumber: z.number().int() }), + callback: async (input) => getPrComments(input.prNumber, repo), + }), + tool({ + name: 'get_file_history', + description: 'Get recent commits (messages, authors, dates) that touched a file.', + inputSchema: z.object({ path: z.string() }), + callback: async (input) => getFileHistoryRaw(input.path, repo), + }), + ] +} diff --git a/strands-command/scripts/typescript/tests/github.test.ts b/strands-command/scripts/typescript/tests/github.test.ts new file mode 100644 index 0000000..a7c2fd2 --- /dev/null +++ b/strands-command/scripts/typescript/tests/github.test.ts @@ -0,0 +1,36 @@ +// tests/github.test.ts +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest' +import { existsSync, rmSync, readFileSync } from 'node:fs' +import { getPrComments, addPrComment, _http } from '../src/tools/github' +import { ARTIFACT_PATH } from '../src/tools/deferredWrite' + +// _http is the indirection seam: { request } object whose property tests replace. + +describe('github tools', () => { + beforeEach(() => { if (existsSync(ARTIFACT_PATH)) rmSync(ARTIFACT_PATH) }) + afterEach(() => { vi.restoreAllMocks(); if (existsSync(ARTIFACT_PATH)) rmSync(ARTIFACT_PATH) }) + + it('getPrComments calls the issue comments endpoint', async () => { + const spy = vi.spyOn(_http, 'request').mockResolvedValue([{ id: 1, body: 'x' }]) + const out = await getPrComments(7, 'o/r') + expect(spy).toHaveBeenCalledWith('GET', 'issues/7/comments', 'o/r', undefined) + expect(out).toContain('x') + }) + + it('addPrComment defers when write disabled', async () => { + const spy = vi.spyOn(_http, 'request').mockResolvedValue({ id: 1 }) + const res = await addPrComment({ write: false }, { prNumber: 7, body: 'hi', repo: 'o/r' }) + expect(spy).not.toHaveBeenCalled() + expect(res).toMatch(/deferred/i) + const line = JSON.parse(readFileSync(ARTIFACT_PATH, 'utf8').trim()) + expect(line.function).toBe('addPrComment') + expect(line.kwargs.prNumber).toBe(7) + }) + + it('addPrComment posts when write enabled', async () => { + const spy = vi.spyOn(_http, 'request').mockResolvedValue({ id: 99 }) + const res = await addPrComment({ write: true }, { prNumber: 7, body: 'hi', repo: 'o/r' }) + expect(spy).toHaveBeenCalledOnce() + expect(res).toContain('99') + }) +}) From c6d22c1c3a302242464e171330e0aa9dbd23cbc1 Mon Sep 17 00:00:00 2001 From: Jonathan Segev Date: Fri, 12 Jun 2026 13:10:19 -0400 Subject: [PATCH 09/29] feat(strands-ts): add writeExecutor finalize replay --- .../scripts/typescript/src/writeExecutor.ts | 58 +++++++++++++++++++ .../typescript/tests/writeExecutor.test.ts | 52 +++++++++++++++++ 2 files changed, 110 insertions(+) create mode 100644 strands-command/scripts/typescript/src/writeExecutor.ts create mode 100644 strands-command/scripts/typescript/tests/writeExecutor.test.ts diff --git a/strands-command/scripts/typescript/src/writeExecutor.ts b/strands-command/scripts/typescript/src/writeExecutor.ts new file mode 100644 index 0000000..1053293 --- /dev/null +++ b/strands-command/scripts/typescript/src/writeExecutor.ts @@ -0,0 +1,58 @@ +// src/writeExecutor.ts +import { existsSync, readFileSync } from 'node:fs' +import { addPrComment } from './tools/github.js' +import { ARTIFACT_PATH, type WriteOperation } from './tools/deferredWrite.js' + +// function name -> write fn. Each fn is called as fn({write:true}, kwargs). +// This allowlist bounds what a (potentially agent-influenced) artifact can do. +type WriteFn = (mode: { write: true }, kwargs: Record) => Promise + +const DEFAULT_WRITE_FNS: Record = { + addPrComment: (mode, kwargs) => addPrComment(mode, kwargs as any), +} + +export interface ReplayResult { total: number; ok: number; failed: number } + +export async function replayOperations( + path: string = ARTIFACT_PATH, + writeFns: Record = DEFAULT_WRITE_FNS, +): Promise { + if (!existsSync(path)) return { total: 0, ok: 0, failed: 0 } + const expectedRepo = process.env.GITHUB_REPOSITORY + const lines = readFileSync(path, 'utf8').split('\n').map((l) => l.trim()).filter(Boolean) + let ok = 0 + let failed = 0 + for (const line of lines) { + try { + const op = JSON.parse(line) as WriteOperation + const fn = writeFns[op.function] + if (!fn) { console.error(`Unknown function: ${op.function}`); failed++; continue } + // Repo guard: the artifact is produced while an agent runs; never let a + // recorded op write outside the repo this workflow serves. + const target = op.kwargs?.repo + if (target !== undefined && target !== expectedRepo) { + console.error(`Rejected op targeting foreign repo: ${String(target)}`) + failed++ + continue + } + await fn({ write: true }, op.kwargs) + ok++ + } catch (e) { + console.error(`Replay error: ${String(e)}`) + failed++ + } + } + return { total: lines.length, ok, failed } +} + +async function main(): Promise { + const path = process.argv[2] ?? ARTIFACT_PATH + const { total, ok, failed } = await replayOperations(path) + console.log(`Replay complete: total=${total} ok=${ok} failed=${failed}`) + if (failed > 0) process.exitCode = 1 +} + +// Run as a script (finalize step) but not when imported by tests. +if (import.meta.url === `file://${process.argv[1]}`) { + void main() +} diff --git a/strands-command/scripts/typescript/tests/writeExecutor.test.ts b/strands-command/scripts/typescript/tests/writeExecutor.test.ts new file mode 100644 index 0000000..6948629 --- /dev/null +++ b/strands-command/scripts/typescript/tests/writeExecutor.test.ts @@ -0,0 +1,52 @@ +// tests/writeExecutor.test.ts +import { describe, it, expect, vi, afterEach, beforeEach } from 'vitest' +import { writeFileSync, rmSync, existsSync, mkdirSync } from 'node:fs' +import { replayOperations } from '../src/writeExecutor' + +const TMP = '.artifact/test_ops.jsonl' + +describe('replayOperations', () => { + beforeEach(() => { process.env.GITHUB_REPOSITORY = 'o/r'; mkdirSync('.artifact', { recursive: true }) }) + afterEach(() => { vi.restoreAllMocks(); if (existsSync(TMP)) rmSync(TMP) }) + + it('replays each line via the matching write fn in write mode', async () => { + const fake = vi.fn().mockResolvedValue('ok') + writeFileSync(TMP, [ + JSON.stringify({ timestamp: 't', function: 'addPrComment', kwargs: { prNumber: 1, body: 'a', repo: 'o/r' } }), + JSON.stringify({ timestamp: 't', function: 'addPrComment', kwargs: { prNumber: 1, body: 'b', repo: 'o/r' } }), + ].join('\n') + '\n') + + const { total, ok, failed } = await replayOperations(TMP, { addPrComment: fake }) + expect(total).toBe(2) + expect(ok).toBe(2) + expect(failed).toBe(0) + expect(fake).toHaveBeenCalledTimes(2) + expect(fake.mock.calls[0][0]).toEqual({ write: true }) // forced write mode + expect(fake.mock.calls[0][1]).toEqual({ prNumber: 1, body: 'a', repo: 'o/r' }) + }) + + it('rejects an operation targeting a different repo', async () => { + const fake = vi.fn().mockResolvedValue('ok') + writeFileSync(TMP, JSON.stringify( + { timestamp: 't', function: 'addPrComment', kwargs: { prNumber: 1, body: 'a', repo: 'evil/elsewhere' } }, + ) + '\n') + const { total, ok, failed } = await replayOperations(TMP, { addPrComment: fake }) + expect(total).toBe(1) + expect(ok).toBe(0) + expect(failed).toBe(1) + expect(fake).not.toHaveBeenCalled() + }) + + it('skips unknown function names without throwing', async () => { + writeFileSync(TMP, JSON.stringify({ timestamp: 't', function: 'nope', kwargs: {} }) + '\n') + const { total, ok, failed } = await replayOperations(TMP, {}) + expect(total).toBe(1) + expect(ok).toBe(0) + expect(failed).toBe(1) + }) + + it('returns zero counts when the file is missing', async () => { + const { total } = await replayOperations('.artifact/does_not_exist.jsonl', {}) + expect(total).toBe(0) + }) +}) From 920518f6d36bb1183ffb6f9eb4efce6236a6b95f Mon Sep 17 00:00:00 2001 From: Jonathan Segev Date: Fri, 12 Jun 2026 13:19:18 -0400 Subject: [PATCH 10/29] fix(strands-ts): encode contents url, pin replay repo, pagination + tests --- .../scripts/typescript/src/tools/github.ts | 7 ++++--- .../scripts/typescript/src/writeExecutor.ts | 6 ++++-- .../scripts/typescript/tests/github.test.ts | 12 +++++++++++- .../scripts/typescript/tests/writeExecutor.test.ts | 10 ++++++++++ 4 files changed, 29 insertions(+), 6 deletions(-) diff --git a/strands-command/scripts/typescript/src/tools/github.ts b/strands-command/scripts/typescript/src/tools/github.ts index f7c7ebd..bb1c6eb 100644 --- a/strands-command/scripts/typescript/src/tools/github.ts +++ b/strands-command/scripts/typescript/src/tools/github.ts @@ -37,17 +37,18 @@ export const _http = { request: githubRequest } // ---- Read helpers ---- export async function getPrComments(prNumber: number, repo?: string): Promise { - const data = await _http.request('GET', `issues/${prNumber}/comments`, repo, undefined) + const data = await _http.request('GET', `issues/${prNumber}/comments?per_page=100`, repo, undefined) return JSON.stringify(data) } export async function getPrDiffRaw(prNumber: number, repo?: string): Promise { - const data = await _http.request('GET', `pulls/${prNumber}/files`, repo, undefined) + const data = await _http.request('GET', `pulls/${prNumber}/files?per_page=100`, repo, undefined) return JSON.stringify(data) } export async function getFileContentsRaw(path: string, ref: string, repo?: string): Promise { - const data = await _http.request('GET', `contents/${path}?ref=${ref}`, repo, undefined) + const safePath = path.split('/').map(encodeURIComponent).join('/') + const data = await _http.request('GET', `contents/${safePath}?ref=${encodeURIComponent(ref)}`, repo, undefined) return JSON.stringify(data) } diff --git a/strands-command/scripts/typescript/src/writeExecutor.ts b/strands-command/scripts/typescript/src/writeExecutor.ts index 1053293..07e4ab5 100644 --- a/strands-command/scripts/typescript/src/writeExecutor.ts +++ b/strands-command/scripts/typescript/src/writeExecutor.ts @@ -28,14 +28,16 @@ export async function replayOperations( const fn = writeFns[op.function] if (!fn) { console.error(`Unknown function: ${op.function}`); failed++; continue } // Repo guard: the artifact is produced while an agent runs; never let a - // recorded op write outside the repo this workflow serves. + // recorded op write outside the repo this workflow serves. Undefined + // repo is pinned to the expected repo rather than trusted to fallbacks. const target = op.kwargs?.repo if (target !== undefined && target !== expectedRepo) { console.error(`Rejected op targeting foreign repo: ${String(target)}`) failed++ continue } - await fn({ write: true }, op.kwargs) + const kwargs = { ...op.kwargs, repo: expectedRepo } + await fn({ write: true }, kwargs) ok++ } catch (e) { console.error(`Replay error: ${String(e)}`) diff --git a/strands-command/scripts/typescript/tests/github.test.ts b/strands-command/scripts/typescript/tests/github.test.ts index a7c2fd2..d820347 100644 --- a/strands-command/scripts/typescript/tests/github.test.ts +++ b/strands-command/scripts/typescript/tests/github.test.ts @@ -13,7 +13,7 @@ describe('github tools', () => { it('getPrComments calls the issue comments endpoint', async () => { const spy = vi.spyOn(_http, 'request').mockResolvedValue([{ id: 1, body: 'x' }]) const out = await getPrComments(7, 'o/r') - expect(spy).toHaveBeenCalledWith('GET', 'issues/7/comments', 'o/r', undefined) + expect(spy).toHaveBeenCalledWith('GET', 'issues/7/comments?per_page=100', 'o/r', undefined) expect(out).toContain('x') }) @@ -33,4 +33,14 @@ describe('github tools', () => { expect(spy).toHaveBeenCalledOnce() expect(res).toContain('99') }) + + it('addPrComment inline range posts to pulls endpoint with start_line', async () => { + const spy = vi.spyOn(_http, 'request').mockResolvedValue({ id: 5 }) + await addPrComment({ write: true }, { + prNumber: 7, body: 'hi', path: 'a.ts', line: 10, startLine: 8, repo: 'o/r', + }) + expect(spy).toHaveBeenCalledWith('POST', 'pulls/7/comments', 'o/r', { + body: 'hi', path: 'a.ts', line: 10, side: 'RIGHT', start_line: 8, start_side: 'RIGHT', + }) + }) }) diff --git a/strands-command/scripts/typescript/tests/writeExecutor.test.ts b/strands-command/scripts/typescript/tests/writeExecutor.test.ts index 6948629..7a80156 100644 --- a/strands-command/scripts/typescript/tests/writeExecutor.test.ts +++ b/strands-command/scripts/typescript/tests/writeExecutor.test.ts @@ -25,6 +25,16 @@ describe('replayOperations', () => { expect(fake.mock.calls[0][1]).toEqual({ prNumber: 1, body: 'a', repo: 'o/r' }) }) + it('pins undefined kwargs.repo to the expected repo', async () => { + const fake = vi.fn().mockResolvedValue('ok') + writeFileSync(TMP, JSON.stringify( + { timestamp: 't', function: 'addPrComment', kwargs: { prNumber: 1, body: 'a' } }, + ) + '\n') + const { ok } = await replayOperations(TMP, { addPrComment: fake }) + expect(ok).toBe(1) + expect(fake.mock.calls[0][1].repo).toBe('o/r') + }) + it('rejects an operation targeting a different repo', async () => { const fake = vi.fn().mockResolvedValue('ok') writeFileSync(TMP, JSON.stringify( From d82da69293c4f6b4d95db2fc91d58cacc76d1c9a Mon Sep 17 00:00:00 2001 From: Jonathan Segev Date: Fri, 12 Jun 2026 14:00:27 -0400 Subject: [PATCH 11/29] feat(strands-ts): add model factory, lens SOPs, and SOP loader --- .../typescript/sops/lenses/adherence.sop.md | 5 ++ .../scripts/typescript/sops/lenses/api.sop.md | 5 ++ .../scripts/typescript/sops/lenses/bug.sop.md | 5 ++ .../typescript/sops/lenses/history.sop.md | 5 ++ .../typescript/sops/lenses/test.sop.md | 5 ++ .../scripts/typescript/src/models.ts | 70 +++++++++++++++++++ .../typescript/src/prompts/sopLoader.ts | 25 +++++++ .../scripts/typescript/tests/models.test.ts | 42 +++++++++++ .../typescript/tests/sopLoader.test.ts | 26 +++++++ 9 files changed, 188 insertions(+) create mode 100644 strands-command/scripts/typescript/sops/lenses/adherence.sop.md create mode 100644 strands-command/scripts/typescript/sops/lenses/api.sop.md create mode 100644 strands-command/scripts/typescript/sops/lenses/bug.sop.md create mode 100644 strands-command/scripts/typescript/sops/lenses/history.sop.md create mode 100644 strands-command/scripts/typescript/sops/lenses/test.sop.md create mode 100644 strands-command/scripts/typescript/src/models.ts create mode 100644 strands-command/scripts/typescript/src/prompts/sopLoader.ts create mode 100644 strands-command/scripts/typescript/tests/models.test.ts create mode 100644 strands-command/scripts/typescript/tests/sopLoader.test.ts diff --git a/strands-command/scripts/typescript/sops/lenses/adherence.sop.md b/strands-command/scripts/typescript/sops/lenses/adherence.sop.md new file mode 100644 index 0000000..207ac1f --- /dev/null +++ b/strands-command/scripts/typescript/sops/lenses/adherence.sop.md @@ -0,0 +1,5 @@ +You are the ADHERENCE reviewer for a Strands SDK PR. Check tenets/DECISIONS/terminology, structured-logging format, and Callable-vs-Protocol that CI cannot catch. Cite the doc+line when governance docs are present; degrade to general API sanity when absent. + +Return ONLY a JSON array of findings. Each: {lens, description, file, line, startLine?, reason}. +Return [] if nothing clears the bar. Do not flag lint/type/format/CI-catchable issues, +pre-existing issues, or unmodified lines. No praise. No prose outside the JSON. diff --git a/strands-command/scripts/typescript/sops/lenses/api.sop.md b/strands-command/scripts/typescript/sops/lenses/api.sop.md new file mode 100644 index 0000000..bdbaf81 --- /dev/null +++ b/strands-command/scripts/typescript/sops/lenses/api.sop.md @@ -0,0 +1,5 @@ +You are the API bar-raising reviewer. Both harness-sdk and evals are SDKs: flag changes that break or weaken the public shape (signatures, removed/renamed symbols, unstable surface not staged in experimental). Cite API_BAR_RAISING/DECISIONS when present, else first-principles. + +Return ONLY a JSON array of findings. Each: {lens, description, file, line, startLine?, reason}. +Return [] if nothing clears the bar. Do not flag lint/type/format/CI-catchable issues, +pre-existing issues, or unmodified lines. No praise. No prose outside the JSON. diff --git a/strands-command/scripts/typescript/sops/lenses/bug.sop.md b/strands-command/scripts/typescript/sops/lenses/bug.sop.md new file mode 100644 index 0000000..b2ab9f1 --- /dev/null +++ b/strands-command/scripts/typescript/sops/lenses/bug.sop.md @@ -0,0 +1,5 @@ +You are the BUG reviewer. Shallow scan the diff for real, impactful correctness bugs. Large bugs only, ignore nitpicks/false positives. For evals also verify its invariants (evaluator contract, prompt-version modules, detector fallbacks, mapper completeness, no private strands._*, banned deps, justified lazy imports). + +Return ONLY a JSON array of findings. Each: {lens, description, file, line, startLine?, reason}. +Return [] if nothing clears the bar. Do not flag lint/type/format/CI-catchable issues, +pre-existing issues, or unmodified lines. No praise. No prose outside the JSON. diff --git a/strands-command/scripts/typescript/sops/lenses/history.sop.md b/strands-command/scripts/typescript/sops/lenses/history.sop.md new file mode 100644 index 0000000..4604396 --- /dev/null +++ b/strands-command/scripts/typescript/sops/lenses/history.sop.md @@ -0,0 +1,5 @@ +You are the HISTORY reviewer. You receive commit history (messages/authors/dates) for the changed files and prior PR comments in your context. Flag regressions of intentional past changes and recurring review feedback that applies again. + +Return ONLY a JSON array of findings. Each: {lens, description, file, line, startLine?, reason}. +Return [] if nothing clears the bar. Do not flag lint/type/format/CI-catchable issues, +pre-existing issues, or unmodified lines. No praise. No prose outside the JSON. diff --git a/strands-command/scripts/typescript/sops/lenses/test.sop.md b/strands-command/scripts/typescript/sops/lenses/test.sop.md new file mode 100644 index 0000000..ee3c5cf --- /dev/null +++ b/strands-command/scripts/typescript/sops/lenses/test.sop.md @@ -0,0 +1,5 @@ +You are the TEST reviewer. Check changed behavior has tests mirroring src/, whole-object asserts, correct TS env-suffix + fixtures. Do NOT flag coverage percentage. + +Return ONLY a JSON array of findings. Each: {lens, description, file, line, startLine?, reason}. +Return [] if nothing clears the bar. Do not flag lint/type/format/CI-catchable issues, +pre-existing issues, or unmodified lines. No praise. No prose outside the JSON. diff --git a/strands-command/scripts/typescript/src/models.ts b/strands-command/scripts/typescript/src/models.ts new file mode 100644 index 0000000..af0f318 --- /dev/null +++ b/strands-command/scripts/typescript/src/models.ts @@ -0,0 +1,70 @@ +// src/models.ts +import { BedrockModel } from '@strands-agents/sdk' + +// Tier aliases — friendly names for the common cases. +export const MODEL_IDS = { + haiku: 'global.anthropic.claude-haiku-4-5-20251001', + sonnet: 'global.anthropic.claude-sonnet-4-6', + opus: 'global.anthropic.claude-opus-4-8', + fable: 'global.anthropic.claude-fable-5', +} as const + +export type ModelTier = keyof typeof MODEL_IDS + +const DEFAULT_MAX_TOKENS: Record = { haiku: 8000, sonnet: 16000, opus: 16000, fable: 16000 } +const FALLBACK_MAX_TOKENS = 16000 + +// A model choice is either a tier alias ("haiku" | "sonnet" | "opus" | "fable") +// or a raw Bedrock model id (anything containing a dot). +export type ModelChoice = ModelTier | (string & {}) + +// Per-agent user config: STRANDS_TS_AGENTS env var — JSON map of +// agentKey -> { model?: ModelChoice, sop?: string (path relative to the SOP dir) }. +// Set by the workflow input; explicit human config always wins. +export interface AgentOverride { + model?: string + sop?: string +} + +export function agentOverrides(): Record { + const raw = process.env.STRANDS_TS_AGENTS + if (!raw) return {} + try { + return JSON.parse(raw) as Record + } catch { + // Malformed config must not kill a review run. + console.error('STRANDS_TS_AGENTS is not valid JSON; ignoring') + return {} + } +} + +/** + * Resolve which model a given agent (orchestrator or a specialist key like + * "bug"/"adherence") should use. Precedence: + * 1. User config (STRANDS_TS_AGENTS[key].model) + * 2. Agent choice (the orchestrator may pick a tier per task complexity) + * 3. Default tier for that agent. + */ +export function resolveModelChoice( + agentKey: string, + agentChoice: ModelChoice | undefined, + defaultTier: ModelChoice, +): ModelChoice { + const fromConfig = agentOverrides()[agentKey]?.model + if (typeof fromConfig === 'string' && fromConfig.length > 0) return fromConfig + return agentChoice ?? defaultTier +} + +export function makeModel(choice: ModelChoice): BedrockModel { + const isTier = choice in MODEL_IDS + if (!isTier && !choice.includes('.')) { + throw new Error(`Unknown model tier or id: ${choice}`) + } + const modelId = isTier ? MODEL_IDS[choice as ModelTier] : choice + const maxTokens = isTier ? DEFAULT_MAX_TOKENS[choice as ModelTier] : FALLBACK_MAX_TOKENS + return new BedrockModel({ + modelId, + maxTokens, + region: process.env.AWS_REGION ?? 'us-west-2', + }) +} diff --git a/strands-command/scripts/typescript/src/prompts/sopLoader.ts b/strands-command/scripts/typescript/src/prompts/sopLoader.ts new file mode 100644 index 0000000..301ac40 --- /dev/null +++ b/strands-command/scripts/typescript/src/prompts/sopLoader.ts @@ -0,0 +1,25 @@ +// src/prompts/sopLoader.ts +import { readFileSync } from 'node:fs' +import { join, normalize } from 'node:path' +import { fileURLToPath } from 'node:url' +import { agentOverrides } from '../models.js' + +const SOP_DIR = fileURLToPath(new URL('../../sops/', import.meta.url)) + +/** Load an agent's SOP: user-override path (relative to sops/, traversal-safe) or the default. */ +export function loadSop(agentKey: string, defaultRelPath: string): string { + const override = agentOverrides()[agentKey]?.sop + const rel = override ?? defaultRelPath + const full = normalize(join(SOP_DIR, rel)) + if (!full.startsWith(normalize(SOP_DIR))) { + throw new Error(`SOP path escapes sops/ dir: ${rel}`) + } + return readFileSync(full, 'utf8') +} + +export function scorerRubric(): string { + return ( + '0: false positive/pre-existing. 25: maybe real, unverified. 50: verified but nitpick/infrequent. ' + + '75: verified, impactful, or doc-mandated. 100: certain, frequent, evidence confirms.' + ) +} diff --git a/strands-command/scripts/typescript/tests/models.test.ts b/strands-command/scripts/typescript/tests/models.test.ts new file mode 100644 index 0000000..abe6714 --- /dev/null +++ b/strands-command/scripts/typescript/tests/models.test.ts @@ -0,0 +1,42 @@ +import { describe, it, expect, beforeEach } from 'vitest' +import { makeModel, MODEL_IDS, resolveModelChoice } from '../src/models' + +describe('makeModel', () => { + it('maps tiers to distinct pinned model ids', () => { + expect(MODEL_IDS.haiku).not.toBe(MODEL_IDS.sonnet) + expect(MODEL_IDS.sonnet).toMatch(/sonnet/) + }) + it('throws on an unknown tier that is not a model id', () => { + expect(() => makeModel('gpt')).toThrow() + }) + it('accepts a raw Bedrock model id passthrough', () => { + expect(() => makeModel('global.anthropic.claude-opus-4-8')).not.toThrow() + }) +}) + +describe('resolveModelChoice (precedence: user config > agent choice > default)', () => { + beforeEach(() => { delete process.env.STRANDS_TS_AGENTS }) + + it('falls back to the default tier', () => { + expect(resolveModelChoice('bug', undefined, 'sonnet')).toBe('sonnet') + }) + + it('agent choice overrides the default', () => { + expect(resolveModelChoice('bug', 'haiku', 'sonnet')).toBe('haiku') + }) + + it('user config (STRANDS_TS_AGENTS JSON) overrides agent choice', () => { + process.env.STRANDS_TS_AGENTS = '{"bug":{"model":"global.anthropic.claude-opus-4-8"}}' + expect(resolveModelChoice('bug', 'haiku', 'sonnet')).toBe('global.anthropic.claude-opus-4-8') + }) + + it('user config for other keys does not affect this one', () => { + process.env.STRANDS_TS_AGENTS = '{"adherence":{"model":"haiku"}}' + expect(resolveModelChoice('bug', undefined, 'sonnet')).toBe('sonnet') + }) + + it('malformed config JSON is ignored, not fatal', () => { + process.env.STRANDS_TS_AGENTS = 'not json' + expect(resolveModelChoice('bug', 'haiku', 'sonnet')).toBe('haiku') + }) +}) diff --git a/strands-command/scripts/typescript/tests/sopLoader.test.ts b/strands-command/scripts/typescript/tests/sopLoader.test.ts new file mode 100644 index 0000000..66e27ba --- /dev/null +++ b/strands-command/scripts/typescript/tests/sopLoader.test.ts @@ -0,0 +1,26 @@ +import { describe, it, expect, beforeEach } from 'vitest' +import { loadSop, scorerRubric } from '../src/prompts/sopLoader' + +describe('loadSop', () => { + beforeEach(() => { delete process.env.STRANDS_TS_AGENTS }) + + it('loads the default SOP for a lens', () => { + const sop = loadSop('bug', 'lenses/bug.sop.md') + expect(sop).toContain('BUG reviewer') + expect(sop).toContain('JSON') + }) + + it('user config sop override wins', () => { + process.env.STRANDS_TS_AGENTS = '{"bug":{"sop":"lenses/test.sop.md"}}' + expect(loadSop('bug', 'lenses/bug.sop.md')).toContain('TEST reviewer') + }) + + it('rejects path traversal in overrides', () => { + process.env.STRANDS_TS_AGENTS = '{"bug":{"sop":"../../package.json"}}' + expect(() => loadSop('bug', 'lenses/bug.sop.md')).toThrow(/escapes/) + }) + + it('rubric covers all bands', () => { + for (const band of ['0', '25', '50', '75', '100']) expect(scorerRubric()).toContain(band) + }) +}) From 39e628b6b7ee911695c787ebdee5afc391300c8d Mon Sep 17 00:00:00 2001 From: Jonathan Segev Date: Fri, 12 Jun 2026 14:05:59 -0400 Subject: [PATCH 12/29] feat(strands-ts): add specialist and orchestrator agent builders --- .../scripts/typescript/sops/reviewer.sop.md | 21 ++++++ .../typescript/src/agents/orchestrator.ts | 20 ++++++ .../typescript/src/agents/specialists.ts | 64 +++++++++++++++++++ .../scripts/typescript/tests/agents.test.ts | 24 +++++++ 4 files changed, 129 insertions(+) create mode 100644 strands-command/scripts/typescript/sops/reviewer.sop.md create mode 100644 strands-command/scripts/typescript/src/agents/orchestrator.ts create mode 100644 strands-command/scripts/typescript/src/agents/specialists.ts create mode 100644 strands-command/scripts/typescript/tests/agents.test.ts diff --git a/strands-command/scripts/typescript/sops/reviewer.sop.md b/strands-command/scripts/typescript/sops/reviewer.sop.md new file mode 100644 index 0000000..cac9868 --- /dev/null +++ b/strands-command/scripts/typescript/sops/reviewer.sop.md @@ -0,0 +1,21 @@ +You are the PR review orchestrator. Steps: + +1. Call get_pr_diff to read the change. Use get_file_contents for fuller context and + get_file_history + get_pr_comments to gather history context for the changed files. +2. Dispatch ALL five reviewer tools (adherence, api, bug, history, test), passing each the + PR number and the context it needs (give the history lens the commit history and prior + comments). You may set modelTier per dispatch to match task complexity: "haiku" for + small/mechanical changes, "sonnet" (default) for typical changes, "opus" or "fable" for + large, subtle, or high-risk changes. A user-provided agent config, if present, overrides + your choice. +3. PREFER the five tuned reviewer tools — their SOPs have been refined. Only if the PR + raises a concern none of them covers (e.g. a domain-specific invariant), you may + additionally dispatch custom_reviewer with a focused system prompt you write and a + model tier. Do not use custom_reviewer to duplicate an existing lens. +4. Collect their findings. Assign each finding an integer score 0-100 using this rubric: + {{RUBRIC}} + For tenet/DECISIONS-flagged issues, confirm the cited doc actually says it before + scoring > 25. +5. Emit your final answer as structured output matching the required schema (a findings + array). Do not post comments yourself; posting happens downstream. Do not include + praise or nitpicks. diff --git a/strands-command/scripts/typescript/src/agents/orchestrator.ts b/strands-command/scripts/typescript/src/agents/orchestrator.ts new file mode 100644 index 0000000..e9aeb8e --- /dev/null +++ b/strands-command/scripts/typescript/src/agents/orchestrator.ts @@ -0,0 +1,20 @@ +// src/agents/orchestrator.ts +import { Agent } from '@strands-agents/sdk' +import { buildSpecialistTools } from './specialists.js' +import { readTools } from '../tools/github.js' +import { loadSop, scorerRubric } from '../prompts/sopLoader.js' +import { ReviewOutputSchema } from '../findings.js' +import { makeModel, resolveModelChoice } from '../models.js' + +export function buildOrchestrator(repo: string): Agent { + // "orchestrator" is the user-config key for this agent in STRANDS_TS_AGENTS; + // the orchestrator itself has no agent-choice (nothing upstream picks for it). + const choice = resolveModelChoice('orchestrator', undefined, 'sonnet') + const sop = loadSop('orchestrator', 'reviewer.sop.md').replace('{{RUBRIC}}', scorerRubric()) + return new Agent({ + model: makeModel(choice), + systemPrompt: sop, + tools: [...buildSpecialistTools(), ...readTools(repo)], + structuredOutputSchema: ReviewOutputSchema, + }) +} diff --git a/strands-command/scripts/typescript/src/agents/specialists.ts b/strands-command/scripts/typescript/src/agents/specialists.ts new file mode 100644 index 0000000..1fa078c --- /dev/null +++ b/strands-command/scripts/typescript/src/agents/specialists.ts @@ -0,0 +1,64 @@ +// src/agents/specialists.ts +import { Agent, tool } from '@strands-agents/sdk' +import { z } from 'zod' +import { LENSES } from '../findings.js' +import { loadSop } from '../prompts/sopLoader.js' +import { makeModel, resolveModelChoice } from '../models.js' + +const TIER_ENUM = z.enum(['haiku', 'sonnet', 'opus', 'fable']) + +async function runSpecialist(systemPrompt: string, model: ReturnType, prompt: string): Promise { + // Model + Agent constructed per call: the orchestrator may emit parallel tool + // calls, and sharing instances across concurrent invocations is not + // guaranteed safe by the SDK. + const agent = new Agent({ model, printer: false, systemPrompt }) + const result = await agent.invoke(prompt) + return result.lastMessage.content.map((b) => (b.type === 'textBlock' ? b.text : '')).join('') +} + +export function buildSpecialistTools() { + const lensTools = LENSES.map((lens) => + tool({ + name: `${lens}_reviewer`, + description: + `Review the PR through the ${lens} lens using its tuned SOP; returns a JSON array of ` + + `findings. Optionally pass modelTier ("haiku" simple, "sonnet" default, "opus"/"fable" ` + + `large or subtle) to match model strength to task complexity.`, + inputSchema: z.object({ + prNumber: z.number().int(), + context: z.string().describe('Diff and any extra context for this lens'), + modelTier: TIER_ENUM.optional().describe('Model strength for this dispatch; omit for default'), + }), + callback: async (input) => { + // Precedence: user config (STRANDS_TS_AGENTS) > orchestrator's modelTier > sonnet. + const choice = resolveModelChoice(lens, input.modelTier, 'sonnet') + const sop = loadSop(lens, `lenses/${lens}.sop.md`) + return runSpecialist(sop, makeModel(choice), `PR #${input.prNumber}\n\n${input.context}`) + }, + }), + ) + + // Meta-agent escape hatch: the orchestrator authors a focused prompt itself + // when no tuned SOP covers the concern. SOPs remain the preferred path (the + // orchestrator SOP says so); this tool is for genuinely uncovered cases. + const customReviewer = tool({ + name: 'custom_reviewer', + description: + 'Dispatch a custom one-off reviewer when NO tuned lens covers a concern. You write its ' + + 'system prompt and pick its model. Prefer the tuned *_reviewer tools whenever they apply.', + inputSchema: z.object({ + systemPrompt: z.string().min(50) + .describe('Focused reviewer system prompt; must demand the same JSON findings output contract'), + prNumber: z.number().int(), + context: z.string().describe('Diff and any extra context'), + modelTier: TIER_ENUM.optional().describe('Model strength; omit for default'), + }), + callback: async (input) => { + // "custom" is the user-config key, so humans can also pin its model. + const choice = resolveModelChoice('custom', input.modelTier, 'sonnet') + return runSpecialist(input.systemPrompt, makeModel(choice), `PR #${input.prNumber}\n\n${input.context}`) + }, + }) + + return [...lensTools, customReviewer] +} diff --git a/strands-command/scripts/typescript/tests/agents.test.ts b/strands-command/scripts/typescript/tests/agents.test.ts new file mode 100644 index 0000000..78311cd --- /dev/null +++ b/strands-command/scripts/typescript/tests/agents.test.ts @@ -0,0 +1,24 @@ +// tests/agents.test.ts +import { describe, it, expect } from 'vitest' +import { buildSpecialistTools } from '../src/agents/specialists' +import { buildOrchestrator } from '../src/agents/orchestrator' +import { LENSES } from '../src/findings' + +describe('specialist tools', () => { + it('builds one tool per lens plus the custom_reviewer meta-agent', () => { + const tools = buildSpecialistTools() + expect(tools).toHaveLength(LENSES.length + 1) + const names = tools.map((t) => t.name) + for (const lens of LENSES) expect(names).toContain(`${lens}_reviewer`) + expect(names).toContain('custom_reviewer') + }) +}) + +describe('orchestrator', () => { + it('builds an Agent wired with specialists + read tools', () => { + const agent = buildOrchestrator('o/r') + const toolNames = agent.tools.map((t) => t.name) + expect(toolNames).toContain('bug_reviewer') + expect(toolNames).toContain('get_pr_diff') + }) +}) From 2f56b25c935744aad2b48ca956a32f22f17afdb5 Mon Sep 17 00:00:00 2001 From: Jonathan Segev Date: Fri, 12 Jun 2026 14:18:58 -0400 Subject: [PATCH 13/29] feat(strands-ts): add reviewer mode, registry, runner entry --- .../scripts/typescript/src/format.ts | 18 ++++++++++++ .../scripts/typescript/src/modes/registry.ts | 17 +++++++++++ .../scripts/typescript/src/modes/reviewer.ts | 29 +++++++++++++++++++ .../scripts/typescript/src/runner.ts | 27 +++++++++++++++++ .../scripts/typescript/tests/format.test.ts | 17 +++++++++++ .../scripts/typescript/tests/registry.test.ts | 11 +++++++ 6 files changed, 119 insertions(+) create mode 100644 strands-command/scripts/typescript/src/format.ts create mode 100644 strands-command/scripts/typescript/src/modes/registry.ts create mode 100644 strands-command/scripts/typescript/src/modes/reviewer.ts create mode 100644 strands-command/scripts/typescript/src/runner.ts create mode 100644 strands-command/scripts/typescript/tests/format.test.ts create mode 100644 strands-command/scripts/typescript/tests/registry.test.ts diff --git a/strands-command/scripts/typescript/src/format.ts b/strands-command/scripts/typescript/src/format.ts new file mode 100644 index 0000000..beb5524 --- /dev/null +++ b/strands-command/scripts/typescript/src/format.ts @@ -0,0 +1,18 @@ +import type { Finding } from './findings.js' + +export const NO_ISSUES_TEMPLATE = + '### Code review\n\nNo issues found. Checked for bugs and guideline compliance.' + +function permalink(repo: string, sha: string, file: string, line: number, startLine?: number): string { + const lo = startLine ?? Math.max(1, line - 1) + const hi = Math.max(line + 1, lo) + return `https://github.com/${repo}/blob/${sha}/${file}#L${lo}-L${hi}` +} + +export function formatReview(findings: Finding[], repo: string, sha: string): string { + if (findings.length === 0) return NO_ISSUES_TEMPLATE + const lines = findings.map((f, i) => + `${i + 1}. ${f.description} (${f.reason})\n\n${permalink(repo, sha, f.file, f.line, f.startLine)}`, + ) + return `### Code review\n\nFound ${findings.length} issue(s):\n\n${lines.join('\n\n')}` +} diff --git a/strands-command/scripts/typescript/src/modes/registry.ts b/strands-command/scripts/typescript/src/modes/registry.ts new file mode 100644 index 0000000..f7ab395 --- /dev/null +++ b/strands-command/scripts/typescript/src/modes/registry.ts @@ -0,0 +1,17 @@ +import { runReviewer, type ModeContext } from './reviewer.js' + +export type ModeHandler = (ctx: ModeContext) => Promise + +const REGISTRY: Record = { + reviewer: runReviewer, +} + +// Map a /strands-ts word to a mode handler. +const COMMAND_TO_MODE: Record = { + review: 'reviewer', +} + +export function resolveMode(command: string): ModeHandler | undefined { + const mode = COMMAND_TO_MODE[command.trim().toLowerCase()] + return mode ? REGISTRY[mode] : undefined +} diff --git a/strands-command/scripts/typescript/src/modes/reviewer.ts b/strands-command/scripts/typescript/src/modes/reviewer.ts new file mode 100644 index 0000000..e333d19 --- /dev/null +++ b/strands-command/scripts/typescript/src/modes/reviewer.ts @@ -0,0 +1,29 @@ +import { buildOrchestrator } from '../agents/orchestrator.js' +import { scoreAndFilter } from '../scoreAndFilter.js' +import { formatReview } from '../format.js' +import { addPrComment } from '../tools/github.js' +import { writeEnabled } from '../tools/deferredWrite.js' +import { ReviewOutputSchema } from '../findings.js' + +export interface ModeContext { + prNumber: number + repo: string + headSha: string +} + +export async function runReviewer(ctx: ModeContext): Promise { + const orchestrator = buildOrchestrator(ctx.repo) + const result = await orchestrator.invoke( + `Review pull request #${ctx.prNumber} in ${ctx.repo}.`, + ) + const parsed = ReviewOutputSchema.safeParse(result.structuredOutput) + if (!parsed.success) { + // Designed silence means VERIFIED nothing to report. Malformed output is a + // failure, not a clean review — fail loudly so the workflow run goes red + // instead of posting a misleading "No issues found". + throw new Error(`Reviewer structured output failed validation: ${parsed.error.message}`) + } + const kept = scoreAndFilter(parsed.data.findings) + const body = formatReview(kept, ctx.repo, ctx.headSha) + await addPrComment(writeEnabled(), { prNumber: ctx.prNumber, body, repo: ctx.repo }) +} diff --git a/strands-command/scripts/typescript/src/runner.ts b/strands-command/scripts/typescript/src/runner.ts new file mode 100644 index 0000000..588dd2d --- /dev/null +++ b/strands-command/scripts/typescript/src/runner.ts @@ -0,0 +1,27 @@ +import { resolveMode } from './modes/registry.js' + +function parseCommand(raw: string): string { + // Accept "/strands-ts review ..." or "review ..."; take the first word after the trigger. + const cleaned = raw.replace(/^\/strands-ts\s*/i, '').trim() + return cleaned.split(/\s+/)[0] ?? '' +} + +async function main(): Promise { + const raw = process.env.INPUT_TASK ?? process.argv.slice(2).join(' ') + const command = parseCommand(raw) + const handler = resolveMode(command) + if (!handler) throw new Error(`Unknown /strands-ts command: "${command}"`) + + const prNumber = Number(process.env.PR_NUMBER) + const repo = process.env.GITHUB_REPOSITORY + const headSha = process.env.PR_HEAD_SHA + if (!prNumber || !repo || !headSha) { + throw new Error('PR_NUMBER, GITHUB_REPOSITORY, and PR_HEAD_SHA must be set') + } + await handler({ prNumber, repo, headSha }) +} + +// Run as a script but not when imported. +if (import.meta.url === `file://${process.argv[1]}`) { + void main() +} diff --git a/strands-command/scripts/typescript/tests/format.test.ts b/strands-command/scripts/typescript/tests/format.test.ts new file mode 100644 index 0000000..c620130 --- /dev/null +++ b/strands-command/scripts/typescript/tests/format.test.ts @@ -0,0 +1,17 @@ +import { describe, it, expect } from 'vitest' +import { formatReview, NO_ISSUES_TEMPLATE } from '../src/format' +import type { Finding } from '../src/findings' + +describe('formatReview', () => { + it('renders the no-issues template when empty (designed silence)', () => { + expect(formatReview([], 'o/r', 'abc123')).toContain(NO_ISSUES_TEMPLATE) + }) + it('renders findings with full-SHA permalinks', () => { + const findings: Finding[] = [ + { lens: 'bug', description: 'off-by-one', file: 'a.ts', line: 10, reason: 'loop', score: 90 }, + ] + const out = formatReview(findings, 'o/r', 'abc123def') + expect(out).toContain('off-by-one') + expect(out).toContain('https://github.com/o/r/blob/abc123def/a.ts#L') + }) +}) diff --git a/strands-command/scripts/typescript/tests/registry.test.ts b/strands-command/scripts/typescript/tests/registry.test.ts new file mode 100644 index 0000000..daba1f8 --- /dev/null +++ b/strands-command/scripts/typescript/tests/registry.test.ts @@ -0,0 +1,11 @@ +import { describe, it, expect } from 'vitest' +import { resolveMode } from '../src/modes/registry' + +describe('resolveMode', () => { + it('resolves the reviewer mode for "review"', () => { + expect(resolveMode('review')).toBeDefined() + }) + it('returns undefined for an unknown command', () => { + expect(resolveMode('frobnicate')).toBeUndefined() + }) +}) From 26fe579da3692362c268705d86ffb9aed71327d1 Mon Sep 17 00:00:00 2001 From: Jonathan Segev Date: Fri, 12 Jun 2026 14:28:15 -0400 Subject: [PATCH 14/29] fix(strands-ts): harden sop path guard, tier lookup, finding range validation --- strands-command/scripts/typescript/src/findings.ts | 2 ++ strands-command/scripts/typescript/src/models.ts | 2 +- strands-command/scripts/typescript/src/prompts/sopLoader.ts | 5 +++-- strands-command/scripts/typescript/tests/findings.test.ts | 6 ++++++ strands-command/scripts/typescript/tests/models.test.ts | 3 +++ strands-command/scripts/typescript/tests/registry.test.ts | 3 +++ strands-command/scripts/typescript/tests/sopLoader.test.ts | 5 +++++ 7 files changed, 23 insertions(+), 3 deletions(-) diff --git a/strands-command/scripts/typescript/src/findings.ts b/strands-command/scripts/typescript/src/findings.ts index 9c5ca22..498f449 100644 --- a/strands-command/scripts/typescript/src/findings.ts +++ b/strands-command/scripts/typescript/src/findings.ts @@ -12,6 +12,8 @@ export const FindingSchema = z.object({ startLine: z.number().int().optional(), reason: z.string(), score: z.number().int().min(0).max(100), +}).refine((f) => f.startLine === undefined || f.startLine <= f.line, { + message: 'startLine must be <= line', }) export type Finding = z.infer diff --git a/strands-command/scripts/typescript/src/models.ts b/strands-command/scripts/typescript/src/models.ts index af0f318..b7bbc17 100644 --- a/strands-command/scripts/typescript/src/models.ts +++ b/strands-command/scripts/typescript/src/models.ts @@ -56,7 +56,7 @@ export function resolveModelChoice( } export function makeModel(choice: ModelChoice): BedrockModel { - const isTier = choice in MODEL_IDS + const isTier = Object.prototype.hasOwnProperty.call(MODEL_IDS, choice) if (!isTier && !choice.includes('.')) { throw new Error(`Unknown model tier or id: ${choice}`) } diff --git a/strands-command/scripts/typescript/src/prompts/sopLoader.ts b/strands-command/scripts/typescript/src/prompts/sopLoader.ts index 301ac40..e0672be 100644 --- a/strands-command/scripts/typescript/src/prompts/sopLoader.ts +++ b/strands-command/scripts/typescript/src/prompts/sopLoader.ts @@ -1,6 +1,6 @@ // src/prompts/sopLoader.ts import { readFileSync } from 'node:fs' -import { join, normalize } from 'node:path' +import { join, normalize, sep } from 'node:path' import { fileURLToPath } from 'node:url' import { agentOverrides } from '../models.js' @@ -10,8 +10,9 @@ const SOP_DIR = fileURLToPath(new URL('../../sops/', import.meta.url)) export function loadSop(agentKey: string, defaultRelPath: string): string { const override = agentOverrides()[agentKey]?.sop const rel = override ?? defaultRelPath + const base = normalize(SOP_DIR).replace(/\/+$/, '') + sep const full = normalize(join(SOP_DIR, rel)) - if (!full.startsWith(normalize(SOP_DIR))) { + if (!full.startsWith(base)) { throw new Error(`SOP path escapes sops/ dir: ${rel}`) } return readFileSync(full, 'utf8') diff --git a/strands-command/scripts/typescript/tests/findings.test.ts b/strands-command/scripts/typescript/tests/findings.test.ts index d7ba5c0..d5c2603 100644 --- a/strands-command/scripts/typescript/tests/findings.test.ts +++ b/strands-command/scripts/typescript/tests/findings.test.ts @@ -23,6 +23,12 @@ describe('FindingSchema', () => { })).toThrow() }) + it('rejects startLine greater than line', () => { + expect(() => FindingSchema.parse({ + lens: 'bug', description: 'd', file: 'x', line: 5, startLine: 20, reason: 'r', score: 90, + })).toThrow() + }) + it('parses a review output wrapping a findings array', () => { const out = ReviewOutputSchema.parse({ findings: [] }) expect(out.findings).toEqual([]) diff --git a/strands-command/scripts/typescript/tests/models.test.ts b/strands-command/scripts/typescript/tests/models.test.ts index abe6714..ed2ed3b 100644 --- a/strands-command/scripts/typescript/tests/models.test.ts +++ b/strands-command/scripts/typescript/tests/models.test.ts @@ -12,6 +12,9 @@ describe('makeModel', () => { it('accepts a raw Bedrock model id passthrough', () => { expect(() => makeModel('global.anthropic.claude-opus-4-8')).not.toThrow() }) + it('rejects prototype-chain keys as tiers', () => { + expect(() => makeModel('constructor')).toThrow() + }) }) describe('resolveModelChoice (precedence: user config > agent choice > default)', () => { diff --git a/strands-command/scripts/typescript/tests/registry.test.ts b/strands-command/scripts/typescript/tests/registry.test.ts index daba1f8..5b3e7ca 100644 --- a/strands-command/scripts/typescript/tests/registry.test.ts +++ b/strands-command/scripts/typescript/tests/registry.test.ts @@ -8,4 +8,7 @@ describe('resolveMode', () => { it('returns undefined for an unknown command', () => { expect(resolveMode('frobnicate')).toBeUndefined() }) + it('returns undefined for an empty command', () => { + expect(resolveMode('')).toBeUndefined() + }) }) diff --git a/strands-command/scripts/typescript/tests/sopLoader.test.ts b/strands-command/scripts/typescript/tests/sopLoader.test.ts index 66e27ba..5c0de3b 100644 --- a/strands-command/scripts/typescript/tests/sopLoader.test.ts +++ b/strands-command/scripts/typescript/tests/sopLoader.test.ts @@ -20,6 +20,11 @@ describe('loadSop', () => { expect(() => loadSop('bug', 'lenses/bug.sop.md')).toThrow(/escapes/) }) + it('rejects sibling-directory prefix bypass', () => { + process.env.STRANDS_TS_AGENTS = '{"bug":{"sop":"../sopsevil/x.sop.md"}}' + expect(() => loadSop('bug', 'lenses/bug.sop.md')).toThrow(/escapes/) + }) + it('rubric covers all bands', () => { for (const band of ['0', '25', '50', '75', '100']) expect(scorerRubric()).toContain(band) }) From b86367ea752b4f167adbbfbe9d436ab7f9bc53cb Mon Sep 17 00:00:00 2001 From: Jonathan Segev Date: Fri, 12 Jun 2026 14:50:22 -0400 Subject: [PATCH 15/29] feat(strands-ts): add read-only runner action --- .../actions/strands-ts-runner/action.yml | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 strands-command/actions/strands-ts-runner/action.yml diff --git a/strands-command/actions/strands-ts-runner/action.yml b/strands-command/actions/strands-ts-runner/action.yml new file mode 100644 index 0000000..8a6d4a3 --- /dev/null +++ b/strands-command/actions/strands-ts-runner/action.yml @@ -0,0 +1,85 @@ +name: 'Strands TS Runner' +description: 'Run the read-only Strands TypeScript agent against a pull request' + +inputs: + command: + description: 'The comment body that triggered the run (e.g. "/strands-ts review")' + required: true + pr_number: + description: 'Pull request number the agent should review' + required: true + pr_head_sha: + description: 'Head SHA of the pull request' + required: true + aws_role_arn: + description: 'AWS IAM role ARN to assume via OIDC for Bedrock access' + required: true + agents_config: + description: 'Optional per-agent config: JSON map of agent key -> {model?, sop?}' + required: false + default: '' + +runs: + using: 'composite' + steps: + # The TypeScript project ships inside this action's repository, which GitHub + # downloads (at the ref the consumer pinned) to run the action. Resolve its + # absolute path so consumer workflows don't need to check out devtools. + - name: Resolve TypeScript project directory + id: dirs + shell: bash + run: echo "ts_dir=$(cd "${{ github.action_path }}/../../scripts/typescript" && pwd)" >> "$GITHUB_OUTPUT" + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install dependencies + shell: bash + working-directory: ${{ steps.dirs.outputs.ts_dir }} + run: npm ci + + - name: Build + shell: bash + working-directory: ${{ steps.dirs.outputs.ts_dir }} + run: npx tsc + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + role-to-assume: ${{ inputs.aws_role_arn }} + role-session-name: GitHubActions-StrandsTS-${{ github.run_id }} + aws-region: us-west-2 + mask-aws-account-id: true + + - name: Run agent (read-only) + shell: bash + working-directory: ${{ steps.dirs.outputs.ts_dir }} + env: + # Read-only: write tool calls are deferred to .artifact/write_operations.jsonl + GITHUB_WRITE: 'false' + + # GitHub Configuration + GITHUB_TOKEN: ${{ github.token }} + GITHUB_REPOSITORY: ${{ github.repository }} + + # Task Configuration + # inputs.command is attacker-influenced PR comment text. Pass it (and the + # other inputs) via env only — never interpolate into the run script body. + INPUT_TASK: ${{ inputs.command }} + PR_NUMBER: ${{ inputs.pr_number }} + PR_HEAD_SHA: ${{ inputs.pr_head_sha }} + STRANDS_TS_AGENTS: ${{ inputs.agents_config }} + + # AWS Configuration + AWS_REGION: 'us-west-2' + run: node dist/runner.js + + - name: Upload artifact for write operations + uses: actions/upload-artifact@v4 + with: + name: strands-ts-write-operations + path: ${{ steps.dirs.outputs.ts_dir }}/.artifact/write_operations.jsonl + retention-days: 1 + if-no-files-found: ignore From 457f3308af780502d74ba3e20ef960a4ee2950b5 Mon Sep 17 00:00:00 2001 From: Jonathan Segev Date: Fri, 12 Jun 2026 14:50:26 -0400 Subject: [PATCH 16/29] feat(strands-ts): add finalize action and example consumer workflow --- .../actions/strands-ts-finalize/action.yml | 44 ++++++++++++ .../examples/strands-ts-command.yml | 68 +++++++++++++++++++ 2 files changed, 112 insertions(+) create mode 100644 strands-command/actions/strands-ts-finalize/action.yml create mode 100644 strands-command/examples/strands-ts-command.yml diff --git a/strands-command/actions/strands-ts-finalize/action.yml b/strands-command/actions/strands-ts-finalize/action.yml new file mode 100644 index 0000000..307d1e2 --- /dev/null +++ b/strands-command/actions/strands-ts-finalize/action.yml @@ -0,0 +1,44 @@ +name: 'Strands TS Finalize' +description: 'Replay deferred write operations recorded by the read-only Strands TypeScript agent' + +runs: + using: 'composite' + steps: + # The TypeScript project ships inside this action's repository, which GitHub + # downloads (at the ref the consumer pinned) to run the action. Resolve its + # absolute path so consumer workflows don't need to check out devtools. + - name: Resolve TypeScript project directory + id: dirs + shell: bash + run: echo "ts_dir=$(cd "${{ github.action_path }}/../../scripts/typescript" && pwd)" >> "$GITHUB_OUTPUT" + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install dependencies + shell: bash + working-directory: ${{ steps.dirs.outputs.ts_dir }} + run: npm ci + + - name: Build + shell: bash + working-directory: ${{ steps.dirs.outputs.ts_dir }} + run: npx tsc + + # No artifact means the agent run deferred nothing — there is nothing to post. + - name: Download artifact with write operations + uses: actions/download-artifact@v4 + with: + name: strands-ts-write-operations + path: ${{ steps.dirs.outputs.ts_dir }}/.artifact + continue-on-error: true + + - name: Replay deferred writes + shell: bash + working-directory: ${{ steps.dirs.outputs.ts_dir }} + env: + GITHUB_TOKEN: ${{ github.token }} + GITHUB_REPOSITORY: ${{ github.repository }} + run: node dist/writeExecutor.js .artifact/write_operations.jsonl diff --git a/strands-command/examples/strands-ts-command.yml b/strands-command/examples/strands-ts-command.yml new file mode 100644 index 0000000..1de6c3e --- /dev/null +++ b/strands-command/examples/strands-ts-command.yml @@ -0,0 +1,68 @@ +# Example consumer workflow for the /strands-ts command. +# Copy into your repo's .github/workflows/ and adjust secrets/vars. +name: Strands-TS Command Handler + +on: + issue_comment: + types: [created] + +# SECURITY: no workflow-level write permissions. The read/write split is enforced +# per job — the agent-running job must NEVER hold a write-capable token, or the +# deferred-write safeguard is decorative. +permissions: {} + +jobs: + authorization-check: + if: ${{ github.event.issue.pull_request && startsWith(github.event.comment.body, '/strands-ts') }} + name: Check access + permissions: + contents: read + runs-on: ubuntu-latest + outputs: + approval-env: ${{ steps.auth.outputs.approval-env }} + steps: + - name: Check Authorization + id: auth + uses: strands-agents/devtools/authorization-check@main + with: + username: ${{ github.event.comment.user.login }} + allowed-roles: 'maintain,triage,write,admin' + + execute-readonly-agent: + needs: [authorization-check] + environment: ${{ needs.authorization-check.outputs.approval-env }} + runs-on: ubuntu-latest + timeout-minutes: 20 # bounded execution: cap a hung multi-agent run + permissions: + contents: read + pull-requests: read + id-token: write # AWS OIDC role assumption only + steps: + - uses: actions/checkout@v4 + - name: Resolve PR head SHA + id: pr + env: + GH_TOKEN: ${{ github.token }} + run: echo "sha=$(gh pr view ${{ github.event.issue.number }} --json headRefOid -q .headRefOid)" >> "$GITHUB_OUTPUT" + - name: Run Strands-TS Agent + uses: strands-agents/devtools/strands-command/actions/strands-ts-runner@main + with: + command: ${{ github.event.comment.body }} + pr_number: ${{ github.event.issue.number }} + pr_head_sha: ${{ steps.pr.outputs.sha }} + aws_role_arn: ${{ secrets.AWS_ROLE_ARN }} + # Per-repo agent config (Actions variable, optional). JSON map of + # agent key -> {model?, sop?}. + agents_config: ${{ vars.STRANDS_TS_AGENTS || '' }} + + finalize: + needs: [execute-readonly-agent] + if: ${{ always() && needs.execute-readonly-agent.result != 'cancelled' }} + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write # the ONLY job that can write; replays vetted artifact ops + steps: + - uses: actions/checkout@v4 + - name: Replay deferred writes + uses: strands-agents/devtools/strands-command/actions/strands-ts-finalize@main From 2435eba9afe130110402f655bc8d610b9506e912 Mon Sep 17 00:00:00 2001 From: Jonathan Segev Date: Fri, 12 Jun 2026 15:10:48 -0400 Subject: [PATCH 17/29] fix(strands-ts): gate finalize on agent success, env-pass pr number --- strands-command/examples/strands-ts-command.yml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/strands-command/examples/strands-ts-command.yml b/strands-command/examples/strands-ts-command.yml index 1de6c3e..567e93e 100644 --- a/strands-command/examples/strands-ts-command.yml +++ b/strands-command/examples/strands-ts-command.yml @@ -43,7 +43,9 @@ jobs: id: pr env: GH_TOKEN: ${{ github.token }} - run: echo "sha=$(gh pr view ${{ github.event.issue.number }} --json headRefOid -q .headRefOid)" >> "$GITHUB_OUTPUT" + # Event payload data goes through env, never into the script body. + PR_NUM: ${{ github.event.issue.number }} + run: echo "sha=$(gh pr view "$PR_NUM" --json headRefOid -q .headRefOid)" >> "$GITHUB_OUTPUT" - name: Run Strands-TS Agent uses: strands-agents/devtools/strands-command/actions/strands-ts-runner@main with: @@ -57,7 +59,10 @@ jobs: finalize: needs: [execute-readonly-agent] - if: ${{ always() && needs.execute-readonly-agent.result != 'cancelled' }} + # Only replay when the agent run succeeded: a crashed run may leave a + # partial artifact, and replaying it would post incomplete reviews while + # making the failure look green. + if: ${{ needs.execute-readonly-agent.result == 'success' }} runs-on: ubuntu-latest permissions: contents: read From cf5dade1f620b7b897844aceac5c923f3e0f06c3 Mon Sep 17 00:00:00 2001 From: Jonathan Segev Date: Fri, 12 Jun 2026 15:39:24 -0400 Subject: [PATCH 18/29] fix(strands-ts): decode file contents, give orchestrator the head ref --- .../scripts/typescript/sops/reviewer.sop.md | 2 +- .../scripts/typescript/src/modes/reviewer.ts | 3 ++- .../scripts/typescript/src/tools/github.ts | 5 +++++ .../scripts/typescript/tests/github.test.ts | 11 ++++++++++- 4 files changed, 18 insertions(+), 3 deletions(-) diff --git a/strands-command/scripts/typescript/sops/reviewer.sop.md b/strands-command/scripts/typescript/sops/reviewer.sop.md index cac9868..2c9058f 100644 --- a/strands-command/scripts/typescript/sops/reviewer.sop.md +++ b/strands-command/scripts/typescript/sops/reviewer.sop.md @@ -1,6 +1,6 @@ You are the PR review orchestrator. Steps: -1. Call get_pr_diff to read the change. Use get_file_contents for fuller context and +1. Call get_pr_diff to read the change. Use get_file_contents (with the PR head commit you were given as the ref) for fuller context and get_file_history + get_pr_comments to gather history context for the changed files. 2. Dispatch ALL five reviewer tools (adherence, api, bug, history, test), passing each the PR number and the context it needs (give the history lens the commit history and prior diff --git a/strands-command/scripts/typescript/src/modes/reviewer.ts b/strands-command/scripts/typescript/src/modes/reviewer.ts index e333d19..e20faff 100644 --- a/strands-command/scripts/typescript/src/modes/reviewer.ts +++ b/strands-command/scripts/typescript/src/modes/reviewer.ts @@ -14,7 +14,8 @@ export interface ModeContext { export async function runReviewer(ctx: ModeContext): Promise { const orchestrator = buildOrchestrator(ctx.repo) const result = await orchestrator.invoke( - `Review pull request #${ctx.prNumber} in ${ctx.repo}.`, + `Review pull request #${ctx.prNumber} in ${ctx.repo}. The PR head commit is ${ctx.headSha}; ` + + `use it as the ref when fetching file contents.`, ) const parsed = ReviewOutputSchema.safeParse(result.structuredOutput) if (!parsed.success) { diff --git a/strands-command/scripts/typescript/src/tools/github.ts b/strands-command/scripts/typescript/src/tools/github.ts index bb1c6eb..7f5c01a 100644 --- a/strands-command/scripts/typescript/src/tools/github.ts +++ b/strands-command/scripts/typescript/src/tools/github.ts @@ -49,6 +49,11 @@ export async function getPrDiffRaw(prNumber: number, repo?: string): Promise { const safePath = path.split('/').map(encodeURIComponent).join('/') const data = await _http.request('GET', `contents/${safePath}?ref=${encodeURIComponent(ref)}`, repo, undefined) + // The contents API returns base64; decode so the reviewing agent sees real text. + if (data && typeof data === 'object' && 'content' in data && typeof (data as any).content === 'string') { + const decoded = Buffer.from((data as any).content, 'base64').toString('utf8') + return JSON.stringify({ path, ref, content: decoded }) + } return JSON.stringify(data) } diff --git a/strands-command/scripts/typescript/tests/github.test.ts b/strands-command/scripts/typescript/tests/github.test.ts index d820347..7ddc1ca 100644 --- a/strands-command/scripts/typescript/tests/github.test.ts +++ b/strands-command/scripts/typescript/tests/github.test.ts @@ -1,7 +1,7 @@ // tests/github.test.ts import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest' import { existsSync, rmSync, readFileSync } from 'node:fs' -import { getPrComments, addPrComment, _http } from '../src/tools/github' +import { getPrComments, addPrComment, getFileContentsRaw, _http } from '../src/tools/github' import { ARTIFACT_PATH } from '../src/tools/deferredWrite' // _http is the indirection seam: { request } object whose property tests replace. @@ -17,6 +17,15 @@ describe('github tools', () => { expect(out).toContain('x') }) + it('getFileContentsRaw decodes base64 content', async () => { + vi.spyOn(_http, 'request').mockResolvedValue({ + content: Buffer.from('hello world', 'utf8').toString('base64'), + encoding: 'base64', + }) + const out = await getFileContentsRaw('a.ts', 'abc123', 'o/r') + expect(out).toContain('hello world') + }) + it('addPrComment defers when write disabled', async () => { const spy = vi.spyOn(_http, 'request').mockResolvedValue({ id: 1 }) const res = await addPrComment({ write: false }, { prNumber: 7, body: 'hi', repo: 'o/r' }) From 0c3505f907a0a19967ebc1bd4767b7c69aee2db8 Mon Sep 17 00:00:00 2001 From: Jonathan Segev Date: Fri, 12 Jun 2026 18:36:09 -0400 Subject: [PATCH 19/29] fix(strands-ts): reject dot-segment paths, require commit_id for inline comments --- .../scripts/typescript/src/tools/github.ts | 15 ++++++++- .../scripts/typescript/tests/github.test.ts | 33 +++++++++++++++++-- 2 files changed, 44 insertions(+), 4 deletions(-) diff --git a/strands-command/scripts/typescript/src/tools/github.ts b/strands-command/scripts/typescript/src/tools/github.ts index 7f5c01a..0277091 100644 --- a/strands-command/scripts/typescript/src/tools/github.ts +++ b/strands-command/scripts/typescript/src/tools/github.ts @@ -47,7 +47,11 @@ export async function getPrDiffRaw(prNumber: number, repo?: string): Promise { - const safePath = path.split('/').map(encodeURIComponent).join('/') + const segments = path.split('/') + if (segments.some((s) => s === '..' || s === '.' || s === '')) { + throw new Error(`Invalid file path: ${path}`) + } + const safePath = segments.map(encodeURIComponent).join('/') const data = await _http.request('GET', `contents/${safePath}?ref=${encodeURIComponent(ref)}`, repo, undefined) // The contents API returns base64; decode so the reviewing agent sees real text. if (data && typeof data === 'object' && 'content' in data && typeof (data as any).content === 'string') { @@ -60,6 +64,10 @@ export async function getFileContentsRaw(path: string, ref: string, repo?: strin export async function getFileHistoryRaw(path: string, repo?: string): Promise { // Recent commits touching this file — the history lens's data source // (no shell/git in the TS runner; history comes from the API). + const segments = path.split('/') + if (segments.some((s) => s === '..' || s === '.' || s === '')) { + throw new Error(`Invalid file path: ${path}`) + } const data = await _http.request('GET', `commits?path=${encodeURIComponent(path)}&per_page=20`, repo, undefined) return JSON.stringify(data) } @@ -71,6 +79,7 @@ export interface AddPrCommentArgs { path?: string line?: number startLine?: number + commitId?: string repo?: string } @@ -81,6 +90,10 @@ export async function addPrComment(mode: WriteMode, args: AddPrCommentArgs): Pro : `issues/${args.prNumber}/comments` const body: Record = { body: args.body } if (args.path) { + if (!args.commitId) { + throw new Error('commitId is required for inline PR comments') + } + body.commit_id = args.commitId body.path = args.path body.line = args.line body.side = 'RIGHT' diff --git a/strands-command/scripts/typescript/tests/github.test.ts b/strands-command/scripts/typescript/tests/github.test.ts index 7ddc1ca..da0599e 100644 --- a/strands-command/scripts/typescript/tests/github.test.ts +++ b/strands-command/scripts/typescript/tests/github.test.ts @@ -1,7 +1,7 @@ // tests/github.test.ts import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest' import { existsSync, rmSync, readFileSync } from 'node:fs' -import { getPrComments, addPrComment, getFileContentsRaw, _http } from '../src/tools/github' +import { getPrComments, addPrComment, getFileContentsRaw, getFileHistoryRaw, _http } from '../src/tools/github' import { ARTIFACT_PATH } from '../src/tools/deferredWrite' // _http is the indirection seam: { request } object whose property tests replace. @@ -46,10 +46,37 @@ describe('github tools', () => { it('addPrComment inline range posts to pulls endpoint with start_line', async () => { const spy = vi.spyOn(_http, 'request').mockResolvedValue({ id: 5 }) await addPrComment({ write: true }, { - prNumber: 7, body: 'hi', path: 'a.ts', line: 10, startLine: 8, repo: 'o/r', + prNumber: 7, body: 'hi', path: 'a.ts', line: 10, startLine: 8, commitId: 'deadbeef', repo: 'o/r', }) expect(spy).toHaveBeenCalledWith('POST', 'pulls/7/comments', 'o/r', { - body: 'hi', path: 'a.ts', line: 10, side: 'RIGHT', start_line: 8, start_side: 'RIGHT', + body: 'hi', commit_id: 'deadbeef', path: 'a.ts', line: 10, side: 'RIGHT', start_line: 8, start_side: 'RIGHT', }) }) + + it('addPrComment inline without commitId throws', async () => { + const spy = vi.spyOn(_http, 'request').mockResolvedValue({ id: 1 }) + await expect(addPrComment({ write: true }, { + prNumber: 7, body: 'hi', path: 'a.ts', line: 10, repo: 'o/r', + })).rejects.toThrow(/commitId/) + expect(spy).not.toHaveBeenCalled() + }) + + it('getFileContentsRaw rejects dot-segment traversal paths', async () => { + const spy = vi.spyOn(_http, 'request').mockResolvedValue({}) + await expect(getFileContentsRaw('../../../../user', 'abc', 'o/r')).rejects.toThrow(/Invalid file path/) + expect(spy).not.toHaveBeenCalled() + }) + + it('getFileContentsRaw accepts normal nested paths', async () => { + vi.spyOn(_http, 'request').mockResolvedValue({ + content: Buffer.from('x', 'utf8').toString('base64'), encoding: 'base64', + }) + await expect(getFileContentsRaw('src/tools/github.ts', 'abc', 'o/r')).resolves.toContain('x') + }) + + it('getFileHistoryRaw rejects dot-segment paths', async () => { + const spy = vi.spyOn(_http, 'request').mockResolvedValue([]) + await expect(getFileHistoryRaw('..', 'o/r')).rejects.toThrow(/Invalid file path/) + expect(spy).not.toHaveBeenCalled() + }) }) From a5e18d98c606f67b8e5e53a52109558de9639262 Mon Sep 17 00:00:00 2001 From: Jonathan Segev Date: Fri, 12 Jun 2026 19:48:32 -0400 Subject: [PATCH 20/29] fix(strands-ts): paginate listings, harden entry guards, cover runner seams --- .../scripts/typescript/src/runner.ts | 20 +++++-- .../scripts/typescript/src/tools/github.ts | 22 ++++++-- .../scripts/typescript/src/writeExecutor.ts | 19 +++++-- .../scripts/typescript/tests/github.test.ts | 13 ++++- .../scripts/typescript/tests/reviewer.test.ts | 54 +++++++++++++++++++ .../scripts/typescript/tests/runner.test.ts | 14 +++++ 6 files changed, 131 insertions(+), 11 deletions(-) create mode 100644 strands-command/scripts/typescript/tests/reviewer.test.ts create mode 100644 strands-command/scripts/typescript/tests/runner.test.ts diff --git a/strands-command/scripts/typescript/src/runner.ts b/strands-command/scripts/typescript/src/runner.ts index 588dd2d..e4740cc 100644 --- a/strands-command/scripts/typescript/src/runner.ts +++ b/strands-command/scripts/typescript/src/runner.ts @@ -1,6 +1,8 @@ +import { realpathSync } from 'node:fs' +import { fileURLToPath } from 'node:url' import { resolveMode } from './modes/registry.js' -function parseCommand(raw: string): string { +export function parseCommand(raw: string): string { // Accept "/strands-ts review ..." or "review ..."; take the first word after the trigger. const cleaned = raw.replace(/^\/strands-ts\s*/i, '').trim() return cleaned.split(/\s+/)[0] ?? '' @@ -22,6 +24,18 @@ async function main(): Promise { } // Run as a script but not when imported. -if (import.meta.url === `file://${process.argv[1]}`) { - void main() +function isMain(): boolean { + if (!process.argv[1]) return false + try { + return realpathSync(fileURLToPath(import.meta.url)) === realpathSync(process.argv[1]) + } catch { + return false + } +} + +if (isMain()) { + main().catch((e) => { + console.error(String(e)) + process.exit(1) + }) } diff --git a/strands-command/scripts/typescript/src/tools/github.ts b/strands-command/scripts/typescript/src/tools/github.ts index 0277091..4c81c42 100644 --- a/strands-command/scripts/typescript/src/tools/github.ts +++ b/strands-command/scripts/typescript/src/tools/github.ts @@ -36,14 +36,28 @@ async function githubRequest( export const _http = { request: githubRequest } // ---- Read helpers ---- +const PAGE_SIZE = 100 +const MAX_PAGES = 10 + +async function paginate(endpointBase: string, repo?: string): Promise { + const all: unknown[] = [] + for (let page = 1; page <= MAX_PAGES; page++) { + const data = await _http.request('GET', `${endpointBase}&page=${page}`, repo, undefined) + if (!Array.isArray(data)) return data === null || data === undefined ? all : [...all, data] + all.push(...data) + if (data.length < PAGE_SIZE) return all + } + // Hard bound hit: surface it rather than silently truncating. + all.push({ warning: `pagination capped at ${MAX_PAGES * PAGE_SIZE} items; more exist` }) + return all +} + export async function getPrComments(prNumber: number, repo?: string): Promise { - const data = await _http.request('GET', `issues/${prNumber}/comments?per_page=100`, repo, undefined) - return JSON.stringify(data) + return JSON.stringify(await paginate(`issues/${prNumber}/comments?per_page=${PAGE_SIZE}`, repo)) } export async function getPrDiffRaw(prNumber: number, repo?: string): Promise { - const data = await _http.request('GET', `pulls/${prNumber}/files?per_page=100`, repo, undefined) - return JSON.stringify(data) + return JSON.stringify(await paginate(`pulls/${prNumber}/files?per_page=${PAGE_SIZE}`, repo)) } export async function getFileContentsRaw(path: string, ref: string, repo?: string): Promise { diff --git a/strands-command/scripts/typescript/src/writeExecutor.ts b/strands-command/scripts/typescript/src/writeExecutor.ts index 07e4ab5..8e36d57 100644 --- a/strands-command/scripts/typescript/src/writeExecutor.ts +++ b/strands-command/scripts/typescript/src/writeExecutor.ts @@ -1,5 +1,6 @@ // src/writeExecutor.ts -import { existsSync, readFileSync } from 'node:fs' +import { existsSync, readFileSync, realpathSync } from 'node:fs' +import { fileURLToPath } from 'node:url' import { addPrComment } from './tools/github.js' import { ARTIFACT_PATH, type WriteOperation } from './tools/deferredWrite.js' @@ -55,6 +56,18 @@ async function main(): Promise { } // Run as a script (finalize step) but not when imported by tests. -if (import.meta.url === `file://${process.argv[1]}`) { - void main() +function isMain(): boolean { + if (!process.argv[1]) return false + try { + return realpathSync(fileURLToPath(import.meta.url)) === realpathSync(process.argv[1]) + } catch { + return false + } +} + +if (isMain()) { + main().catch((e) => { + console.error(String(e)) + process.exit(1) + }) } diff --git a/strands-command/scripts/typescript/tests/github.test.ts b/strands-command/scripts/typescript/tests/github.test.ts index da0599e..bf2fb27 100644 --- a/strands-command/scripts/typescript/tests/github.test.ts +++ b/strands-command/scripts/typescript/tests/github.test.ts @@ -13,10 +13,21 @@ describe('github tools', () => { it('getPrComments calls the issue comments endpoint', async () => { const spy = vi.spyOn(_http, 'request').mockResolvedValue([{ id: 1, body: 'x' }]) const out = await getPrComments(7, 'o/r') - expect(spy).toHaveBeenCalledWith('GET', 'issues/7/comments?per_page=100', 'o/r', undefined) + expect(spy).toHaveBeenCalledWith('GET', 'issues/7/comments?per_page=100&page=1', 'o/r', undefined) expect(out).toContain('x') }) + it('paginates past the first full page', async () => { + const page1 = Array.from({ length: 100 }, (_, i) => ({ id: i })) + const page2 = [{ id: 100 }] + const spy = vi.spyOn(_http, 'request') + .mockResolvedValueOnce(page1) + .mockResolvedValueOnce(page2) + const out = JSON.parse(await getPrComments(7, 'o/r')) + expect(spy).toHaveBeenCalledTimes(2) + expect(out).toHaveLength(101) + }) + it('getFileContentsRaw decodes base64 content', async () => { vi.spyOn(_http, 'request').mockResolvedValue({ content: Buffer.from('hello world', 'utf8').toString('base64'), diff --git a/strands-command/scripts/typescript/tests/reviewer.test.ts b/strands-command/scripts/typescript/tests/reviewer.test.ts new file mode 100644 index 0000000..035680c --- /dev/null +++ b/strands-command/scripts/typescript/tests/reviewer.test.ts @@ -0,0 +1,54 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' +import { existsSync, rmSync, readFileSync } from 'node:fs' +import { ARTIFACT_PATH } from '../src/tools/deferredWrite' + +vi.mock('../src/agents/orchestrator', () => ({ + buildOrchestrator: vi.fn(), +})) + +import { buildOrchestrator } from '../src/agents/orchestrator' +import { runReviewer } from '../src/modes/reviewer' + +const ctx = { prNumber: 7, repo: 'o/r', headSha: 'abc123' } + +function mockAgent(structuredOutput: unknown) { + return { invoke: vi.fn().mockResolvedValue({ structuredOutput }) } +} + +describe('runReviewer', () => { + beforeEach(() => { + process.env.GITHUB_WRITE = 'false' + if (existsSync(ARTIFACT_PATH)) rmSync(ARTIFACT_PATH) + }) + afterEach(() => { + vi.restoreAllMocks() + delete process.env.GITHUB_WRITE + if (existsSync(ARTIFACT_PATH)) rmSync(ARTIFACT_PATH) + }) + + it('defers a formatted comment for valid findings above threshold', async () => { + vi.mocked(buildOrchestrator).mockReturnValue(mockAgent({ + findings: [{ lens: 'bug', description: 'real bug', file: 'a.ts', line: 3, reason: 'r', score: 95 }], + }) as any) + await runReviewer(ctx) + const line = JSON.parse(readFileSync(ARTIFACT_PATH, 'utf8').trim()) + expect(line.function).toBe('addPrComment') + expect(line.kwargs.body).toContain('real bug') + expect(line.kwargs.body).toContain('abc123') + }) + + it('defers the designed-silence template when all findings are filtered out', async () => { + vi.mocked(buildOrchestrator).mockReturnValue(mockAgent({ + findings: [{ lens: 'bug', description: 'weak', file: 'a.ts', line: 3, reason: 'r', score: 40 }], + }) as any) + await runReviewer(ctx) + const line = JSON.parse(readFileSync(ARTIFACT_PATH, 'utf8').trim()) + expect(line.kwargs.body).toContain('No issues found') + }) + + it('throws on malformed structured output without deferring anything', async () => { + vi.mocked(buildOrchestrator).mockReturnValue(mockAgent({ nonsense: true }) as any) + await expect(runReviewer(ctx)).rejects.toThrow(/structured output/) + expect(existsSync(ARTIFACT_PATH)).toBe(false) + }) +}) diff --git a/strands-command/scripts/typescript/tests/runner.test.ts b/strands-command/scripts/typescript/tests/runner.test.ts new file mode 100644 index 0000000..8ee3dcd --- /dev/null +++ b/strands-command/scripts/typescript/tests/runner.test.ts @@ -0,0 +1,14 @@ +import { describe, it, expect } from 'vitest' +import { parseCommand } from '../src/runner' + +describe('parseCommand', () => { + it('strips the /strands-ts trigger', () => { + expect(parseCommand('/strands-ts review please')).toBe('review') + }) + it('accepts a bare command', () => { + expect(parseCommand('review')).toBe('review') + }) + it('returns empty for the lone trigger', () => { + expect(parseCommand('/strands-ts')).toBe('') + }) +}) From 3fdbef274a859b64f3f0846479cf6d6c49942362 Mon Sep 17 00:00:00 2001 From: Jonathan Segev Date: Fri, 12 Jun 2026 20:05:00 -0400 Subject: [PATCH 21/29] refactor(strands-ts): restructure SOPs to devtools house style --- .../typescript/sops/lenses/adherence.sop.md | 30 ++++++-- .../scripts/typescript/sops/lenses/api.sop.md | 30 ++++++-- .../scripts/typescript/sops/lenses/bug.sop.md | 30 ++++++-- .../typescript/sops/lenses/history.sop.md | 29 ++++++-- .../typescript/sops/lenses/test.sop.md | 31 +++++++-- .../scripts/typescript/sops/reviewer.sop.md | 68 +++++++++++++------ 6 files changed, 177 insertions(+), 41 deletions(-) diff --git a/strands-command/scripts/typescript/sops/lenses/adherence.sop.md b/strands-command/scripts/typescript/sops/lenses/adherence.sop.md index 207ac1f..51ef99f 100644 --- a/strands-command/scripts/typescript/sops/lenses/adherence.sop.md +++ b/strands-command/scripts/typescript/sops/lenses/adherence.sop.md @@ -1,5 +1,27 @@ -You are the ADHERENCE reviewer for a Strands SDK PR. Check tenets/DECISIONS/terminology, structured-logging format, and Callable-vs-Protocol that CI cannot catch. Cite the doc+line when governance docs are present; degrade to general API sanity when absent. +# Adherence Reviewer -Return ONLY a JSON array of findings. Each: {lens, description, file, line, startLine?, reason}. -Return [] if nothing clears the bar. Do not flag lint/type/format/CI-catchable issues, -pre-existing issues, or unmodified lines. No praise. No prose outside the JSON. +## Role + +You are the ADHERENCE reviewer for a Strands SDK PR. Your scope is adherence to project governance and conventions that CI cannot catch. + +## Steps + +### 1. Review the Change Through the Adherence Lens + +Examine the diff for governance and convention violations. + +**Constraints:** +- You MUST check tenets/DECISIONS/terminology, structured-logging format, and Callable-vs-Protocol issues that CI cannot catch +- You MUST cite the doc and line when governance docs are present +- You SHOULD degrade to general API sanity checks when governance docs are absent + +### 2. Emit Findings + +Report what you found as the JSON output contract. + +**Constraints:** +- You MUST return ONLY a JSON array of findings, each shaped {lens, description, file, line, startLine?, reason} +- You MUST return [] if nothing clears the bar +- You MUST NOT flag lint/type/format/CI-catchable issues because CI already enforces them +- You MUST NOT flag pre-existing issues or unmodified lines because review scope is the change itself +- You MUST NOT include praise or prose outside the JSON diff --git a/strands-command/scripts/typescript/sops/lenses/api.sop.md b/strands-command/scripts/typescript/sops/lenses/api.sop.md index bdbaf81..0dfb2d5 100644 --- a/strands-command/scripts/typescript/sops/lenses/api.sop.md +++ b/strands-command/scripts/typescript/sops/lenses/api.sop.md @@ -1,5 +1,27 @@ -You are the API bar-raising reviewer. Both harness-sdk and evals are SDKs: flag changes that break or weaken the public shape (signatures, removed/renamed symbols, unstable surface not staged in experimental). Cite API_BAR_RAISING/DECISIONS when present, else first-principles. +# API Reviewer -Return ONLY a JSON array of findings. Each: {lens, description, file, line, startLine?, reason}. -Return [] if nothing clears the bar. Do not flag lint/type/format/CI-catchable issues, -pre-existing issues, or unmodified lines. No praise. No prose outside the JSON. +## Role + +You are the API bar-raising reviewer. Both harness-sdk and evals are SDKs, so your scope is the public API surface of the change. + +## Steps + +### 1. Review the Change Through the API Lens + +Examine the diff for damage to the public shape. + +**Constraints:** +- You MUST flag changes that break or weaken the public shape (signatures, removed/renamed symbols, unstable surface not staged in experimental) +- You MUST cite API_BAR_RAISING/DECISIONS when present +- You SHOULD reason from first principles when those docs are absent + +### 2. Emit Findings + +Report what you found as the JSON output contract. + +**Constraints:** +- You MUST return ONLY a JSON array of findings, each shaped {lens, description, file, line, startLine?, reason} +- You MUST return [] if nothing clears the bar +- You MUST NOT flag lint/type/format/CI-catchable issues because CI already enforces them +- You MUST NOT flag pre-existing issues or unmodified lines because review scope is the change itself +- You MUST NOT include praise or prose outside the JSON diff --git a/strands-command/scripts/typescript/sops/lenses/bug.sop.md b/strands-command/scripts/typescript/sops/lenses/bug.sop.md index b2ab9f1..f75718a 100644 --- a/strands-command/scripts/typescript/sops/lenses/bug.sop.md +++ b/strands-command/scripts/typescript/sops/lenses/bug.sop.md @@ -1,5 +1,27 @@ -You are the BUG reviewer. Shallow scan the diff for real, impactful correctness bugs. Large bugs only, ignore nitpicks/false positives. For evals also verify its invariants (evaluator contract, prompt-version modules, detector fallbacks, mapper completeness, no private strands._*, banned deps, justified lazy imports). +# Bug Reviewer -Return ONLY a JSON array of findings. Each: {lens, description, file, line, startLine?, reason}. -Return [] if nothing clears the bar. Do not flag lint/type/format/CI-catchable issues, -pre-existing issues, or unmodified lines. No praise. No prose outside the JSON. +## Role + +You are the BUG reviewer. Your scope is real, impactful correctness bugs introduced by the change. + +## Steps + +### 1. Review the Change Through the Bug Lens + +Shallow scan the diff for correctness bugs. + +**Constraints:** +- You MUST scan the diff for real, impactful correctness bugs +- You MUST report large bugs only, ignoring nitpicks and false positives +- You MUST, for evals, also verify its invariants (evaluator contract, prompt-version modules, detector fallbacks, mapper completeness, no private strands._*, banned deps, justified lazy imports) + +### 2. Emit Findings + +Report what you found as the JSON output contract. + +**Constraints:** +- You MUST return ONLY a JSON array of findings, each shaped {lens, description, file, line, startLine?, reason} +- You MUST return [] if nothing clears the bar +- You MUST NOT flag lint/type/format/CI-catchable issues because CI already enforces them +- You MUST NOT flag pre-existing issues or unmodified lines because review scope is the change itself +- You MUST NOT include praise or prose outside the JSON diff --git a/strands-command/scripts/typescript/sops/lenses/history.sop.md b/strands-command/scripts/typescript/sops/lenses/history.sop.md index 4604396..a378aef 100644 --- a/strands-command/scripts/typescript/sops/lenses/history.sop.md +++ b/strands-command/scripts/typescript/sops/lenses/history.sop.md @@ -1,5 +1,26 @@ -You are the HISTORY reviewer. You receive commit history (messages/authors/dates) for the changed files and prior PR comments in your context. Flag regressions of intentional past changes and recurring review feedback that applies again. +# History Reviewer -Return ONLY a JSON array of findings. Each: {lens, description, file, line, startLine?, reason}. -Return [] if nothing clears the bar. Do not flag lint/type/format/CI-catchable issues, -pre-existing issues, or unmodified lines. No praise. No prose outside the JSON. +## Role + +You are the HISTORY reviewer. You receive commit history (messages/authors/dates) for the changed files and prior PR comments in your context, and your scope is conflicts between the change and that history. + +## Steps + +### 1. Review the Change Through the History Lens + +Compare the diff against the commit history and prior review feedback. + +**Constraints:** +- You MUST flag regressions of intentional past changes +- You MUST flag recurring review feedback that applies again to this change + +### 2. Emit Findings + +Report what you found as the JSON output contract. + +**Constraints:** +- You MUST return ONLY a JSON array of findings, each shaped {lens, description, file, line, startLine?, reason} +- You MUST return [] if nothing clears the bar +- You MUST NOT flag lint/type/format/CI-catchable issues because CI already enforces them +- You MUST NOT flag pre-existing issues or unmodified lines because review scope is the change itself +- You MUST NOT include praise or prose outside the JSON diff --git a/strands-command/scripts/typescript/sops/lenses/test.sop.md b/strands-command/scripts/typescript/sops/lenses/test.sop.md index ee3c5cf..155ffdd 100644 --- a/strands-command/scripts/typescript/sops/lenses/test.sop.md +++ b/strands-command/scripts/typescript/sops/lenses/test.sop.md @@ -1,5 +1,28 @@ -You are the TEST reviewer. Check changed behavior has tests mirroring src/, whole-object asserts, correct TS env-suffix + fixtures. Do NOT flag coverage percentage. +# Test Reviewer -Return ONLY a JSON array of findings. Each: {lens, description, file, line, startLine?, reason}. -Return [] if nothing clears the bar. Do not flag lint/type/format/CI-catchable issues, -pre-existing issues, or unmodified lines. No praise. No prose outside the JSON. +## Role + +You are the TEST reviewer. Your scope is test coverage and test quality for the behavior changed in the PR. + +## Steps + +### 1. Review the Change Through the Test Lens + +Examine the diff for missing or weak tests. + +**Constraints:** +- You MUST check that changed behavior has tests mirroring src/ +- You MUST check for whole-object asserts +- You MUST check for the correct TS env-suffix and fixtures +- You MUST NOT flag coverage percentage + +### 2. Emit Findings + +Report what you found as the JSON output contract. + +**Constraints:** +- You MUST return ONLY a JSON array of findings, each shaped {lens, description, file, line, startLine?, reason} +- You MUST return [] if nothing clears the bar +- You MUST NOT flag lint/type/format/CI-catchable issues because CI already enforces them +- You MUST NOT flag pre-existing issues or unmodified lines because review scope is the change itself +- You MUST NOT include praise or prose outside the JSON diff --git a/strands-command/scripts/typescript/sops/reviewer.sop.md b/strands-command/scripts/typescript/sops/reviewer.sop.md index 2c9058f..2e55733 100644 --- a/strands-command/scripts/typescript/sops/reviewer.sop.md +++ b/strands-command/scripts/typescript/sops/reviewer.sop.md @@ -1,21 +1,47 @@ -You are the PR review orchestrator. Steps: - -1. Call get_pr_diff to read the change. Use get_file_contents (with the PR head commit you were given as the ref) for fuller context and - get_file_history + get_pr_comments to gather history context for the changed files. -2. Dispatch ALL five reviewer tools (adherence, api, bug, history, test), passing each the - PR number and the context it needs (give the history lens the commit history and prior - comments). You may set modelTier per dispatch to match task complexity: "haiku" for - small/mechanical changes, "sonnet" (default) for typical changes, "opus" or "fable" for - large, subtle, or high-risk changes. A user-provided agent config, if present, overrides - your choice. -3. PREFER the five tuned reviewer tools — their SOPs have been refined. Only if the PR - raises a concern none of them covers (e.g. a domain-specific invariant), you may - additionally dispatch custom_reviewer with a focused system prompt you write and a - model tier. Do not use custom_reviewer to duplicate an existing lens. -4. Collect their findings. Assign each finding an integer score 0-100 using this rubric: - {{RUBRIC}} - For tenet/DECISIONS-flagged issues, confirm the cited doc actually says it before - scoring > 25. -5. Emit your final answer as structured output matching the required schema (a findings - array). Do not post comments yourself; posting happens downstream. Do not include - praise or nitpicks. +# PR Review Orchestrator + +## Role + +You are the PR review orchestrator. You gather the full context for a pull request, dispatch the specialized reviewer lenses against the change, score their findings against a rubric, and emit the consolidated result as structured output. + +## Steps + +### 1. Gather Context + +Read the change and collect the context the reviewers will need. + +**Constraints:** +- You MUST call get_pr_diff to read the change +- You SHOULD use get_file_contents (with the PR head commit you were given as the ref) for fuller context +- You SHOULD use get_file_history and get_pr_comments to gather history context for the changed files + +### 2. Dispatch Reviewers + +Dispatch the reviewer lenses against the change. + +**Constraints:** +- You MUST dispatch ALL five reviewer tools (adherence, api, bug, history, test), passing each the PR number and the context it needs +- You MUST give the history lens the commit history and prior comments +- You MAY set modelTier per dispatch to match task complexity: "haiku" for small/mechanical changes, "sonnet" (default) for typical changes, "opus" or "fable" for large, subtle, or high-risk changes +- You MUST let a user-provided agent config, if present, override your modelTier choice +- You SHOULD prefer the five tuned reviewer tools because their SOPs have been refined +- You MAY additionally dispatch custom_reviewer with a focused system prompt you write and a model tier, but only if the PR raises a concern none of the five lenses covers (e.g. a domain-specific invariant) +- You MUST NOT use custom_reviewer to duplicate an existing lens + +### 3. Score Findings + +Collect the reviewers' findings and score each one. + +**Constraints:** +- You MUST assign each finding an integer score 0-100 using this rubric: + {{RUBRIC}} +- You MUST confirm, for tenet/DECISIONS-flagged issues, that the cited doc actually says it before scoring > 25 + +### 4. Emit Final Answer + +Produce the consolidated review result. + +**Constraints:** +- You MUST emit your final answer as structured output matching the required schema (a findings array) +- You MUST NOT post comments yourself because posting happens downstream +- You MUST NOT include praise or nitpicks From cfd45fec4b7aecb55d2877a2bb98784fc0d6f60d Mon Sep 17 00:00:00 2001 From: Jonathan Segev Date: Fri, 12 Jun 2026 20:19:16 -0400 Subject: [PATCH 22/29] feat(strands-ts): post inline review comments per finding --- .../scripts/typescript/src/format.ts | 4 +++ .../scripts/typescript/src/modes/reviewer.ts | 21 ++++++++++-- .../scripts/typescript/tests/format.test.ts | 8 ++++- .../scripts/typescript/tests/reviewer.test.ts | 33 +++++++++++++++---- 4 files changed, 57 insertions(+), 9 deletions(-) diff --git a/strands-command/scripts/typescript/src/format.ts b/strands-command/scripts/typescript/src/format.ts index beb5524..ca1389c 100644 --- a/strands-command/scripts/typescript/src/format.ts +++ b/strands-command/scripts/typescript/src/format.ts @@ -9,6 +9,10 @@ function permalink(repo: string, sha: string, file: string, line: number, startL return `https://github.com/${repo}/blob/${sha}/${file}#L${lo}-L${hi}` } +export function inlineBody(f: Finding): string { + return `**${f.lens}**: ${f.description}\n\n${f.reason} (confidence: ${f.score})` +} + export function formatReview(findings: Finding[], repo: string, sha: string): string { if (findings.length === 0) return NO_ISSUES_TEMPLATE const lines = findings.map((f, i) => diff --git a/strands-command/scripts/typescript/src/modes/reviewer.ts b/strands-command/scripts/typescript/src/modes/reviewer.ts index e20faff..425dbd0 100644 --- a/strands-command/scripts/typescript/src/modes/reviewer.ts +++ b/strands-command/scripts/typescript/src/modes/reviewer.ts @@ -1,6 +1,6 @@ import { buildOrchestrator } from '../agents/orchestrator.js' import { scoreAndFilter } from '../scoreAndFilter.js' -import { formatReview } from '../format.js' +import { formatReview, inlineBody } from '../format.js' import { addPrComment } from '../tools/github.js' import { writeEnabled } from '../tools/deferredWrite.js' import { ReviewOutputSchema } from '../findings.js' @@ -25,6 +25,23 @@ export async function runReviewer(ctx: ModeContext): Promise { throw new Error(`Reviewer structured output failed validation: ${parsed.error.message}`) } const kept = scoreAndFilter(parsed.data.findings) + const mode = writeEnabled() + // Inline comments can fail at replay time if a finding targets a line outside + // the diff (GitHub 422). The summary comment independently lists every + // finding, so a failed inline degrades gracefully rather than losing the + // finding. (writeExecutor already continues past per-op failures and reports + // counts.) + for (const finding of kept) { + await addPrComment(mode, { + prNumber: ctx.prNumber, + body: inlineBody(finding), + path: finding.file, + line: finding.line, + startLine: finding.startLine, + commitId: ctx.headSha, + repo: ctx.repo, + }) + } const body = formatReview(kept, ctx.repo, ctx.headSha) - await addPrComment(writeEnabled(), { prNumber: ctx.prNumber, body, repo: ctx.repo }) + await addPrComment(mode, { prNumber: ctx.prNumber, body, repo: ctx.repo }) } diff --git a/strands-command/scripts/typescript/tests/format.test.ts b/strands-command/scripts/typescript/tests/format.test.ts index c620130..eaa9147 100644 --- a/strands-command/scripts/typescript/tests/format.test.ts +++ b/strands-command/scripts/typescript/tests/format.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from 'vitest' -import { formatReview, NO_ISSUES_TEMPLATE } from '../src/format' +import { formatReview, inlineBody, NO_ISSUES_TEMPLATE } from '../src/format' import type { Finding } from '../src/findings' describe('formatReview', () => { @@ -14,4 +14,10 @@ describe('formatReview', () => { expect(out).toContain('off-by-one') expect(out).toContain('https://github.com/o/r/blob/abc123def/a.ts#L') }) + it('inlineBody renders lens, description, reason, confidence', () => { + const out = inlineBody({ lens: 'bug', description: 'off-by-one', file: 'a.ts', line: 10, reason: 'loop', score: 90 }) + expect(out).toContain('bug') + expect(out).toContain('off-by-one') + expect(out).toContain('90') + }) }) diff --git a/strands-command/scripts/typescript/tests/reviewer.test.ts b/strands-command/scripts/typescript/tests/reviewer.test.ts index 035680c..8c789ab 100644 --- a/strands-command/scripts/typescript/tests/reviewer.test.ts +++ b/strands-command/scripts/typescript/tests/reviewer.test.ts @@ -31,10 +31,30 @@ describe('runReviewer', () => { findings: [{ lens: 'bug', description: 'real bug', file: 'a.ts', line: 3, reason: 'r', score: 95 }], }) as any) await runReviewer(ctx) - const line = JSON.parse(readFileSync(ARTIFACT_PATH, 'utf8').trim()) - expect(line.function).toBe('addPrComment') - expect(line.kwargs.body).toContain('real bug') - expect(line.kwargs.body).toContain('abc123') + const lines = readFileSync(ARTIFACT_PATH, 'utf8').trim().split('\n').map((l) => JSON.parse(l)) + expect(lines).toHaveLength(2) + // First line: the inline comment anchored to the finding's location. + expect(lines[0].function).toBe('addPrComment') + expect(lines[0].kwargs.path).toBe('a.ts') + expect(lines[0].kwargs.line).toBe(3) + expect(lines[0].kwargs.commitId).toBe('abc123') + expect(lines[0].kwargs.body).toContain('real bug') + // Last line: the summary comment (no path). + const summary = lines[lines.length - 1] + expect(summary.function).toBe('addPrComment') + expect(summary.kwargs.path).toBeUndefined() + expect(summary.kwargs.body).toContain('real bug') + expect(summary.kwargs.body).toContain('abc123') + }) + + it('passes startLine through to the inline comment', async () => { + vi.mocked(buildOrchestrator).mockReturnValue(mockAgent({ + findings: [{ lens: 'bug', description: 'range bug', file: 'b.ts', line: 9, startLine: 7, reason: 'r', score: 88 }], + }) as any) + await runReviewer(ctx) + const lines = readFileSync(ARTIFACT_PATH, 'utf8').trim().split('\n').map((l) => JSON.parse(l)) + expect(lines[0].kwargs.startLine).toBe(7) + expect(lines[0].kwargs.commitId).toBe('abc123') }) it('defers the designed-silence template when all findings are filtered out', async () => { @@ -42,8 +62,9 @@ describe('runReviewer', () => { findings: [{ lens: 'bug', description: 'weak', file: 'a.ts', line: 3, reason: 'r', score: 40 }], }) as any) await runReviewer(ctx) - const line = JSON.parse(readFileSync(ARTIFACT_PATH, 'utf8').trim()) - expect(line.kwargs.body).toContain('No issues found') + const lines = readFileSync(ARTIFACT_PATH, 'utf8').trim().split('\n').map((l) => JSON.parse(l)) + expect(lines).toHaveLength(1) + expect(lines[0].kwargs.body).toContain('No issues found') }) it('throws on malformed structured output without deferring anything', async () => { From a301b095120df1098f060e25cb01e4c7a1573233 Mon Sep 17 00:00:00 2001 From: Jonathan Segev Date: Sat, 13 Jun 2026 22:27:33 -0400 Subject: [PATCH 23/29] fix(strands-ts): treat inline-comment 422 as skipped, not a run failure --- .../scripts/typescript/src/tools/github.ts | 9 ++- .../scripts/typescript/src/writeExecutor.ts | 58 ++++++++++++------- .../typescript/tests/writeExecutor.test.ts | 24 ++++++++ 3 files changed, 69 insertions(+), 22 deletions(-) diff --git a/strands-command/scripts/typescript/src/tools/github.ts b/strands-command/scripts/typescript/src/tools/github.ts index 4c81c42..3fa5e85 100644 --- a/strands-command/scripts/typescript/src/tools/github.ts +++ b/strands-command/scripts/typescript/src/tools/github.ts @@ -3,6 +3,13 @@ import { tool } from '@strands-agents/sdk' import { z } from 'zod' import { recordOrCall, type WriteMode } from './deferredWrite.js' +export class GitHubApiError extends Error { + constructor(public readonly status: number, method: string, endpoint: string) { + super(`GitHub ${method} ${endpoint} failed: ${status}`) + this.name = 'GitHubApiError' + } +} + function repoOrEnv(repo?: string): string { const r = repo ?? process.env.GITHUB_REPOSITORY if (!r) throw new Error('GITHUB_REPOSITORY not set') @@ -26,7 +33,7 @@ async function githubRequest( }, body: body ? JSON.stringify(body) : undefined, }) - if (!res.ok) throw new Error(`GitHub ${method} ${endpoint} failed: ${res.status}`) + if (!res.ok) throw new GitHubApiError(res.status, method, endpoint) return res.json() } diff --git a/strands-command/scripts/typescript/src/writeExecutor.ts b/strands-command/scripts/typescript/src/writeExecutor.ts index 8e36d57..edad7cd 100644 --- a/strands-command/scripts/typescript/src/writeExecutor.ts +++ b/strands-command/scripts/typescript/src/writeExecutor.ts @@ -1,7 +1,7 @@ // src/writeExecutor.ts import { existsSync, readFileSync, realpathSync } from 'node:fs' import { fileURLToPath } from 'node:url' -import { addPrComment } from './tools/github.js' +import { addPrComment, GitHubApiError } from './tools/github.js' import { ARTIFACT_PATH, type WriteOperation } from './tools/deferredWrite.js' // function name -> write fn. Each fn is called as fn({write:true}, kwargs). @@ -12,46 +12,62 @@ const DEFAULT_WRITE_FNS: Record = { addPrComment: (mode, kwargs) => addPrComment(mode, kwargs as any), } -export interface ReplayResult { total: number; ok: number; failed: number } +export interface ReplayResult { total: number; ok: number; failed: number; skipped: number } export async function replayOperations( path: string = ARTIFACT_PATH, writeFns: Record = DEFAULT_WRITE_FNS, ): Promise { - if (!existsSync(path)) return { total: 0, ok: 0, failed: 0 } + if (!existsSync(path)) return { total: 0, ok: 0, failed: 0, skipped: 0 } const expectedRepo = process.env.GITHUB_REPOSITORY const lines = readFileSync(path, 'utf8').split('\n').map((l) => l.trim()).filter(Boolean) let ok = 0 let failed = 0 + let skipped = 0 for (const line of lines) { + let op: WriteOperation try { - const op = JSON.parse(line) as WriteOperation - const fn = writeFns[op.function] - if (!fn) { console.error(`Unknown function: ${op.function}`); failed++; continue } - // Repo guard: the artifact is produced while an agent runs; never let a - // recorded op write outside the repo this workflow serves. Undefined - // repo is pinned to the expected repo rather than trusted to fallbacks. - const target = op.kwargs?.repo - if (target !== undefined && target !== expectedRepo) { - console.error(`Rejected op targeting foreign repo: ${String(target)}`) - failed++ - continue - } - const kwargs = { ...op.kwargs, repo: expectedRepo } - await fn({ write: true }, kwargs) - ok++ + op = JSON.parse(line) as WriteOperation } catch (e) { console.error(`Replay error: ${String(e)}`) failed++ + continue + } + const fn = writeFns[op.function] + if (!fn) { console.error(`Unknown function: ${op.function}`); failed++; continue } + // Repo guard: the artifact is produced while an agent runs; never let a + // recorded op write outside the repo this workflow serves. Undefined + // repo is pinned to the expected repo rather than trusted to fallbacks. + const target = op.kwargs?.repo + if (target !== undefined && target !== expectedRepo) { + console.error(`Rejected op targeting foreign repo: ${String(target)}`) + failed++ + continue + } + const kwargs = { ...op.kwargs, repo: expectedRepo } + try { + await fn({ write: true }, kwargs) + ok++ + } catch (e) { + // A 422 from GitHub on an inline comment means the anchored line isn't in + // the diff hunk — an expected degradation, since the finding is still + // preserved in the summary comment. Skip it rather than fail the run. + if (e instanceof GitHubApiError && e.status === 422) { + console.warn(`Skipped (422, line likely not in diff): ${op.function}`) + skipped++ + } else { + console.error(`Replay error: ${String(e)}`) + failed++ + } } } - return { total: lines.length, ok, failed } + return { total: lines.length, ok, failed, skipped } } async function main(): Promise { const path = process.argv[2] ?? ARTIFACT_PATH - const { total, ok, failed } = await replayOperations(path) - console.log(`Replay complete: total=${total} ok=${ok} failed=${failed}`) + const { total, ok, failed, skipped } = await replayOperations(path) + console.log(`Replay complete: total=${total} ok=${ok} failed=${failed} skipped=${skipped}`) if (failed > 0) process.exitCode = 1 } diff --git a/strands-command/scripts/typescript/tests/writeExecutor.test.ts b/strands-command/scripts/typescript/tests/writeExecutor.test.ts index 7a80156..61821eb 100644 --- a/strands-command/scripts/typescript/tests/writeExecutor.test.ts +++ b/strands-command/scripts/typescript/tests/writeExecutor.test.ts @@ -2,6 +2,7 @@ import { describe, it, expect, vi, afterEach, beforeEach } from 'vitest' import { writeFileSync, rmSync, existsSync, mkdirSync } from 'node:fs' import { replayOperations } from '../src/writeExecutor' +import { GitHubApiError } from '../src/tools/github' const TMP = '.artifact/test_ops.jsonl' @@ -59,4 +60,27 @@ describe('replayOperations', () => { const { total } = await replayOperations('.artifact/does_not_exist.jsonl', {}) expect(total).toBe(0) }) + + it('counts a 422 GitHubApiError as skipped, not failed, without throwing', async () => { + const fake = vi.fn().mockRejectedValue(new GitHubApiError(422, 'POST', 'pulls/3/comments')) + writeFileSync(TMP, JSON.stringify( + { timestamp: 't', function: 'addPrComment', kwargs: { prNumber: 3, body: 'a', repo: 'o/r' } }, + ) + '\n') + const { ok, failed, skipped } = await replayOperations(TMP, { addPrComment: fake }) + expect(ok).toBe(0) + expect(failed).toBe(0) + expect(skipped).toBe(1) + expect(fake).toHaveBeenCalledOnce() + }) + + it('counts a non-422 GitHubApiError as failed', async () => { + const fake = vi.fn().mockRejectedValue(new GitHubApiError(500, 'POST', 'pulls/3/comments')) + writeFileSync(TMP, JSON.stringify( + { timestamp: 't', function: 'addPrComment', kwargs: { prNumber: 3, body: 'a', repo: 'o/r' } }, + ) + '\n') + const { ok, failed, skipped } = await replayOperations(TMP, { addPrComment: fake }) + expect(ok).toBe(0) + expect(failed).toBe(1) + expect(skipped).toBe(0) + }) }) From 4c83da7cb488d7c293e92756697f0778671060e1 Mon Sep 17 00:00:00 2001 From: Jonathan Segev Date: Sat, 13 Jun 2026 22:33:51 -0400 Subject: [PATCH 24/29] fix(strands-ts): orchestrator fetches governance docs for the adherence lens --- strands-command/scripts/typescript/sops/reviewer.sop.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/strands-command/scripts/typescript/sops/reviewer.sop.md b/strands-command/scripts/typescript/sops/reviewer.sop.md index 2e55733..a37990c 100644 --- a/strands-command/scripts/typescript/sops/reviewer.sop.md +++ b/strands-command/scripts/typescript/sops/reviewer.sop.md @@ -14,6 +14,9 @@ Read the change and collect the context the reviewers will need. - You MUST call get_pr_diff to read the change - You SHOULD use get_file_contents (with the PR head commit you were given as the ref) for fuller context - You SHOULD use get_file_history and get_pr_comments to gather history context for the changed files +- You MUST attempt to fetch governance/convention docs with get_file_contents at the PR head ref, trying CONVENTIONS.md, CONTRIBUTING.md, AGENTS.md, TENETS.md, DECISIONS.md, and CLAUDE.md, since these pre-exist on the base branch and never appear in the diff +- You MUST try these paths both at the repo root AND in directories inferred from the changed files' paths (e.g. for a change to `pkg/src/foo.py`, also try `pkg/CONVENTIONS.md` and `pkg/src/CONVENTIONS.md`) +- You SHOULD treat a 404 or error return from get_file_contents as "that doc does not exist" and move on without retrying ### 2. Dispatch Reviewers @@ -22,6 +25,8 @@ Dispatch the reviewer lenses against the change. **Constraints:** - You MUST dispatch ALL five reviewer tools (adherence, api, bug, history, test), passing each the PR number and the context it needs - You MUST give the history lens the commit history and prior comments +- You MUST include the full text of every governance doc you found in the adherence_reviewer's `context` argument, because the adherence lens has no tools and can only see what you give it +- You SHOULD, when no governance docs were found, tell the adherence lens explicitly that none exist so it knowingly applies general API/convention sanity rather than silently finding nothing - You MAY set modelTier per dispatch to match task complexity: "haiku" for small/mechanical changes, "sonnet" (default) for typical changes, "opus" or "fable" for large, subtle, or high-risk changes - You MUST let a user-provided agent config, if present, override your modelTier choice - You SHOULD prefer the five tuned reviewer tools because their SOPs have been refined From 14eabf692214affafe8a6f4f3881b1c157d9b404 Mon Sep 17 00:00:00 2001 From: Jonathan Segev Date: Sat, 13 Jun 2026 23:35:19 -0400 Subject: [PATCH 25/29] fix(strands-ts): keep masking-test findings separate, clamp inline anchors --- .../scripts/typescript/sops/reviewer.sop.md | 2 ++ strands-command/scripts/typescript/src/format.ts | 11 +++++++++++ .../scripts/typescript/src/modes/reviewer.ts | 13 +++++++------ .../scripts/typescript/tests/format.test.ts | 16 ++++++++++++++++ 4 files changed, 36 insertions(+), 6 deletions(-) diff --git a/strands-command/scripts/typescript/sops/reviewer.sop.md b/strands-command/scripts/typescript/sops/reviewer.sop.md index a37990c..b29c75a 100644 --- a/strands-command/scripts/typescript/sops/reviewer.sop.md +++ b/strands-command/scripts/typescript/sops/reviewer.sop.md @@ -41,6 +41,8 @@ Collect the reviewers' findings and score each one. - You MUST assign each finding an integer score 0-100 using this rubric: {{RUBRIC}} - You MUST confirm, for tenet/DECISIONS-flagged issues, that the cited doc actually says it before scoring > 25 +- You MUST keep a test that was changed to match buggy or regressed behavior as its OWN separate finding (lens: test), distinct from the code finding it masks, because a test edited to expect the wrong result removes the safety net and is independently dangerous +- You MUST NOT merge findings that have different root causes or live in different files just because they describe the same incident; dedupe only true duplicates (same file, same line, same issue) ### 4. Emit Final Answer diff --git a/strands-command/scripts/typescript/src/format.ts b/strands-command/scripts/typescript/src/format.ts index ca1389c..35fe952 100644 --- a/strands-command/scripts/typescript/src/format.ts +++ b/strands-command/scripts/typescript/src/format.ts @@ -9,6 +9,17 @@ function permalink(repo: string, sha: string, file: string, line: number, startL return `https://github.com/${repo}/blob/${sha}/${file}#L${lo}-L${hi}` } +// Clamp an inline-comment anchor: a finding's `line` is the changed line; only +// keep a multi-line range when it's a small, forward span (start < line, within +// 10 lines), otherwise anchor to the single line so GitHub doesn't 422 on a +// range that reaches over unchanged context. +export function inlineAnchor(line: number, startLine?: number): { line: number; startLine?: number } { + if (startLine === undefined || startLine >= line || line - startLine > 10) { + return { line } + } + return { line, startLine } +} + export function inlineBody(f: Finding): string { return `**${f.lens}**: ${f.description}\n\n${f.reason} (confidence: ${f.score})` } diff --git a/strands-command/scripts/typescript/src/modes/reviewer.ts b/strands-command/scripts/typescript/src/modes/reviewer.ts index 425dbd0..57ffd80 100644 --- a/strands-command/scripts/typescript/src/modes/reviewer.ts +++ b/strands-command/scripts/typescript/src/modes/reviewer.ts @@ -1,6 +1,6 @@ import { buildOrchestrator } from '../agents/orchestrator.js' import { scoreAndFilter } from '../scoreAndFilter.js' -import { formatReview, inlineBody } from '../format.js' +import { formatReview, inlineBody, inlineAnchor } from '../format.js' import { addPrComment } from '../tools/github.js' import { writeEnabled } from '../tools/deferredWrite.js' import { ReviewOutputSchema } from '../findings.js' @@ -31,13 +31,14 @@ export async function runReviewer(ctx: ModeContext): Promise { // finding, so a failed inline degrades gracefully rather than losing the // finding. (writeExecutor already continues past per-op failures and reports // counts.) - for (const finding of kept) { + for (const f of kept) { + const anchor = inlineAnchor(f.line, f.startLine) await addPrComment(mode, { prNumber: ctx.prNumber, - body: inlineBody(finding), - path: finding.file, - line: finding.line, - startLine: finding.startLine, + body: inlineBody(f), + path: f.file, + line: anchor.line, + startLine: anchor.startLine, commitId: ctx.headSha, repo: ctx.repo, }) diff --git a/strands-command/scripts/typescript/tests/format.test.ts b/strands-command/scripts/typescript/tests/format.test.ts index eaa9147..6717aa3 100644 --- a/strands-command/scripts/typescript/tests/format.test.ts +++ b/strands-command/scripts/typescript/tests/format.test.ts @@ -1,7 +1,23 @@ import { describe, it, expect } from 'vitest' import { formatReview, inlineBody, NO_ISSUES_TEMPLATE } from '../src/format' +import { inlineAnchor } from '../src/format' import type { Finding } from '../src/findings' +describe('inlineAnchor', () => { + it('keeps a small forward range', () => { + expect(inlineAnchor(10, 8)).toEqual({ line: 10, startLine: 8 }) + }) + it('drops startLine when undefined', () => { + expect(inlineAnchor(10)).toEqual({ line: 10 }) + }) + it('drops a backwards range (start >= line)', () => { + expect(inlineAnchor(10, 20)).toEqual({ line: 10 }) + }) + it('drops a wide back-reach over unchanged context', () => { + expect(inlineAnchor(62, 47)).toEqual({ line: 62 }) + }) +}) + describe('formatReview', () => { it('renders the no-issues template when empty (designed silence)', () => { expect(formatReview([], 'o/r', 'abc123')).toContain(NO_ISSUES_TEMPLATE) From 1a41528f1538de3f0674434867fdb98084020405 Mon Sep 17 00:00:00 2001 From: Jonathan Segev Date: Sat, 13 Jun 2026 23:57:19 -0400 Subject: [PATCH 26/29] fix(strands-ts): score verified/doc-cited findings at the posting threshold --- .../scripts/typescript/src/prompts/sopLoader.ts | 12 ++++++++++-- .../scripts/typescript/tests/sopLoader.test.ts | 8 +++++++- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/strands-command/scripts/typescript/src/prompts/sopLoader.ts b/strands-command/scripts/typescript/src/prompts/sopLoader.ts index e0672be..19109af 100644 --- a/strands-command/scripts/typescript/src/prompts/sopLoader.ts +++ b/strands-command/scripts/typescript/src/prompts/sopLoader.ts @@ -19,8 +19,16 @@ export function loadSop(agentKey: string, defaultRelPath: string): string { } export function scorerRubric(): string { + // Findings scoring >= 80 are posted; < 80 are filtered. The bands are tuned so + // a VERIFIED, doc-cited convention violation clears the bar (it is exactly the + // kind of issue this reviewer exists to surface), while unverified or stylistic + // findings not backed by a doc stay below it. return ( - '0: false positive/pre-existing. 25: maybe real, unverified. 50: verified but nitpick/infrequent. ' + - '75: verified, impactful, or doc-mandated. 100: certain, frequent, evidence confirms.' + '0: false positive, or a pre-existing issue not introduced by this change. ' + + '25: possibly real but unverified, or a stylistic preference NOT backed by a cited doc. ' + + '50: verified but a minor nitpick / rare in practice / low impact. ' + + '80: verified and impactful, OR a violation of an explicitly cited convention/governance doc, ' + + 'OR a verified breaking change. These MUST be surfaced. ' + + '100: certain, frequent, and directly evidence-confirmed.' ) } diff --git a/strands-command/scripts/typescript/tests/sopLoader.test.ts b/strands-command/scripts/typescript/tests/sopLoader.test.ts index 5c0de3b..09b892a 100644 --- a/strands-command/scripts/typescript/tests/sopLoader.test.ts +++ b/strands-command/scripts/typescript/tests/sopLoader.test.ts @@ -26,6 +26,12 @@ describe('loadSop', () => { }) it('rubric covers all bands', () => { - for (const band of ['0', '25', '50', '75', '100']) expect(scorerRubric()).toContain(band) + for (const band of ['0', '25', '50', '80', '100']) expect(scorerRubric()).toContain(band) + }) + + it('rubric places verified/doc-cited findings at the 80 posting threshold', () => { + // Regression guard: convention violations must not be scored just below the + // filter cutoff. The 80 band must explicitly cover doc-cited violations. + expect(scorerRubric()).toMatch(/80:[^.]*cited convention/) }) }) From 5313a601ab24032106bd7ed5e007c17990847217 Mon Sep 17 00:00:00 2001 From: Jonathan Segev Date: Sun, 14 Jun 2026 00:04:08 -0400 Subject: [PATCH 27/29] feat(strands-ts): default to opus tier and add strands-running label lifecycle --- .../examples/strands-ts-command.yml | 33 ++++++++++++++++++- .../typescript/src/agents/orchestrator.ts | 4 +-- .../typescript/src/agents/specialists.ts | 8 ++--- .../scripts/typescript/src/models.ts | 5 +++ 4 files changed, 43 insertions(+), 7 deletions(-) diff --git a/strands-command/examples/strands-ts-command.yml b/strands-command/examples/strands-ts-command.yml index 567e93e..67b84a3 100644 --- a/strands-command/examples/strands-ts-command.yml +++ b/strands-command/examples/strands-ts-command.yml @@ -28,8 +28,23 @@ jobs: username: ${{ github.event.comment.user.login }} allowed-roles: 'maintain,triage,write,admin' - execute-readonly-agent: + # Visible in-progress indicator + soft concurrency signal. Runs in its own + # write-capable job so the agent job below stays tokenless. + mark-running: needs: [authorization-check] + runs-on: ubuntu-latest + permissions: + issues: write # PR labels are managed via the issues API + pull-requests: write + steps: + - name: Add strands-running label + env: + GH_TOKEN: ${{ github.token }} + PR_NUM: ${{ github.event.issue.number }} + run: gh pr edit "$PR_NUM" --repo "${{ github.repository }}" --add-label strands-running || true + + execute-readonly-agent: + needs: [authorization-check, mark-running] environment: ${{ needs.authorization-check.outputs.approval-env }} runs-on: ubuntu-latest timeout-minutes: 20 # bounded execution: cap a hung multi-agent run @@ -71,3 +86,19 @@ jobs: - uses: actions/checkout@v4 - name: Replay deferred writes uses: strands-agents/devtools/strands-command/actions/strands-ts-finalize@main + + # Always clear the label once the run is done (success, failure, or skip), + # mirroring the Python command's remove-on-finalize behavior. + clear-running: + needs: [mark-running, execute-readonly-agent, finalize] + if: ${{ always() && needs.mark-running.result == 'success' }} + runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: write + steps: + - name: Remove strands-running label + env: + GH_TOKEN: ${{ github.token }} + PR_NUM: ${{ github.event.issue.number }} + run: gh pr edit "$PR_NUM" --repo "${{ github.repository }}" --remove-label strands-running || true diff --git a/strands-command/scripts/typescript/src/agents/orchestrator.ts b/strands-command/scripts/typescript/src/agents/orchestrator.ts index e9aeb8e..072c786 100644 --- a/strands-command/scripts/typescript/src/agents/orchestrator.ts +++ b/strands-command/scripts/typescript/src/agents/orchestrator.ts @@ -4,12 +4,12 @@ import { buildSpecialistTools } from './specialists.js' import { readTools } from '../tools/github.js' import { loadSop, scorerRubric } from '../prompts/sopLoader.js' import { ReviewOutputSchema } from '../findings.js' -import { makeModel, resolveModelChoice } from '../models.js' +import { makeModel, resolveModelChoice, DEFAULT_TIER } from '../models.js' export function buildOrchestrator(repo: string): Agent { // "orchestrator" is the user-config key for this agent in STRANDS_TS_AGENTS; // the orchestrator itself has no agent-choice (nothing upstream picks for it). - const choice = resolveModelChoice('orchestrator', undefined, 'sonnet') + const choice = resolveModelChoice('orchestrator', undefined, DEFAULT_TIER) const sop = loadSop('orchestrator', 'reviewer.sop.md').replace('{{RUBRIC}}', scorerRubric()) return new Agent({ model: makeModel(choice), diff --git a/strands-command/scripts/typescript/src/agents/specialists.ts b/strands-command/scripts/typescript/src/agents/specialists.ts index 1fa078c..d27af72 100644 --- a/strands-command/scripts/typescript/src/agents/specialists.ts +++ b/strands-command/scripts/typescript/src/agents/specialists.ts @@ -3,7 +3,7 @@ import { Agent, tool } from '@strands-agents/sdk' import { z } from 'zod' import { LENSES } from '../findings.js' import { loadSop } from '../prompts/sopLoader.js' -import { makeModel, resolveModelChoice } from '../models.js' +import { makeModel, resolveModelChoice, DEFAULT_TIER } from '../models.js' const TIER_ENUM = z.enum(['haiku', 'sonnet', 'opus', 'fable']) @@ -22,7 +22,7 @@ export function buildSpecialistTools() { name: `${lens}_reviewer`, description: `Review the PR through the ${lens} lens using its tuned SOP; returns a JSON array of ` + - `findings. Optionally pass modelTier ("haiku" simple, "sonnet" default, "opus"/"fable" ` + + `findings. Optionally pass modelTier ("haiku" simple, "sonnet" mid, "opus" default / "fable" ` + `large or subtle) to match model strength to task complexity.`, inputSchema: z.object({ prNumber: z.number().int(), @@ -31,7 +31,7 @@ export function buildSpecialistTools() { }), callback: async (input) => { // Precedence: user config (STRANDS_TS_AGENTS) > orchestrator's modelTier > sonnet. - const choice = resolveModelChoice(lens, input.modelTier, 'sonnet') + const choice = resolveModelChoice(lens, input.modelTier, DEFAULT_TIER) const sop = loadSop(lens, `lenses/${lens}.sop.md`) return runSpecialist(sop, makeModel(choice), `PR #${input.prNumber}\n\n${input.context}`) }, @@ -55,7 +55,7 @@ export function buildSpecialistTools() { }), callback: async (input) => { // "custom" is the user-config key, so humans can also pin its model. - const choice = resolveModelChoice('custom', input.modelTier, 'sonnet') + const choice = resolveModelChoice('custom', input.modelTier, DEFAULT_TIER) return runSpecialist(input.systemPrompt, makeModel(choice), `PR #${input.prNumber}\n\n${input.context}`) }, }) diff --git a/strands-command/scripts/typescript/src/models.ts b/strands-command/scripts/typescript/src/models.ts index b7bbc17..36184ce 100644 --- a/strands-command/scripts/typescript/src/models.ts +++ b/strands-command/scripts/typescript/src/models.ts @@ -18,6 +18,11 @@ const FALLBACK_MAX_TOKENS = 16000 // or a raw Bedrock model id (anything containing a dot). export type ModelChoice = ModelTier | (string & {}) +// Default tier for any agent when neither user config nor an agent-chosen tier +// applies. Opus by default: review quality is worth the cost, and per-dispatch +// downgrades (e.g. "haiku" for trivial changes) remain available. +export const DEFAULT_TIER: ModelTier = 'opus' + // Per-agent user config: STRANDS_TS_AGENTS env var — JSON map of // agentKey -> { model?: ModelChoice, sop?: string (path relative to the SOP dir) }. // Set by the workflow input; explicit human config always wins. From f19f37d40f558083cbbf7fc8805256dfa113a71c Mon Sep 17 00:00:00 2001 From: Jonathan Segev Date: Sun, 14 Jun 2026 00:22:02 -0400 Subject: [PATCH 28/29] refactor(strands-ts): remove dead scaffolding and low-value tests Drop the version.ts smoke-test artifact (RUNNER_NAME had no production use) and the unused ReviewOutput type export. Remove three change-detector tests that asserted config data or a constant's value rather than behavior. --- strands-command/scripts/typescript/src/findings.ts | 2 -- strands-command/scripts/typescript/src/version.ts | 1 - strands-command/scripts/typescript/tests/models.test.ts | 6 +----- .../scripts/typescript/tests/scoreAndFilter.test.ts | 6 +----- strands-command/scripts/typescript/tests/version.test.ts | 8 -------- 5 files changed, 2 insertions(+), 21 deletions(-) delete mode 100644 strands-command/scripts/typescript/src/version.ts delete mode 100644 strands-command/scripts/typescript/tests/version.test.ts diff --git a/strands-command/scripts/typescript/src/findings.ts b/strands-command/scripts/typescript/src/findings.ts index 498f449..0cb3012 100644 --- a/strands-command/scripts/typescript/src/findings.ts +++ b/strands-command/scripts/typescript/src/findings.ts @@ -22,5 +22,3 @@ export type Finding = z.infer export const ReviewOutputSchema = z.object({ findings: z.array(FindingSchema), }) - -export type ReviewOutput = z.infer diff --git a/strands-command/scripts/typescript/src/version.ts b/strands-command/scripts/typescript/src/version.ts deleted file mode 100644 index 35ecd11..0000000 --- a/strands-command/scripts/typescript/src/version.ts +++ /dev/null @@ -1 +0,0 @@ -export const RUNNER_NAME = 'strands-ts' diff --git a/strands-command/scripts/typescript/tests/models.test.ts b/strands-command/scripts/typescript/tests/models.test.ts index ed2ed3b..fbc3b55 100644 --- a/strands-command/scripts/typescript/tests/models.test.ts +++ b/strands-command/scripts/typescript/tests/models.test.ts @@ -1,11 +1,7 @@ import { describe, it, expect, beforeEach } from 'vitest' -import { makeModel, MODEL_IDS, resolveModelChoice } from '../src/models' +import { makeModel, resolveModelChoice } from '../src/models' describe('makeModel', () => { - it('maps tiers to distinct pinned model ids', () => { - expect(MODEL_IDS.haiku).not.toBe(MODEL_IDS.sonnet) - expect(MODEL_IDS.sonnet).toMatch(/sonnet/) - }) it('throws on an unknown tier that is not a model id', () => { expect(() => makeModel('gpt')).toThrow() }) diff --git a/strands-command/scripts/typescript/tests/scoreAndFilter.test.ts b/strands-command/scripts/typescript/tests/scoreAndFilter.test.ts index 8bc0616..5e8197d 100644 --- a/strands-command/scripts/typescript/tests/scoreAndFilter.test.ts +++ b/strands-command/scripts/typescript/tests/scoreAndFilter.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from 'vitest' -import { scoreAndFilter, THRESHOLD, MAX_COMMENTS } from '../src/scoreAndFilter' +import { scoreAndFilter, MAX_COMMENTS } from '../src/scoreAndFilter' import type { Finding } from '../src/findings' function f(score: number, line = 1, file = 'a.py', description = 'd'): Finding { @@ -23,10 +23,6 @@ describe('scoreAndFilter', () => { expect(scoreAndFilter(many)).toHaveLength(MAX_COMMENTS) }) - it('exposes a threshold of 80', () => { - expect(THRESHOLD).toBe(80) - }) - it('returns empty for empty input', () => { expect(scoreAndFilter([])).toEqual([]) }) diff --git a/strands-command/scripts/typescript/tests/version.test.ts b/strands-command/scripts/typescript/tests/version.test.ts deleted file mode 100644 index a2c54cb..0000000 --- a/strands-command/scripts/typescript/tests/version.test.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { describe, it, expect } from 'vitest' -import { RUNNER_NAME } from '../src/version' - -describe('version', () => { - it('exposes the runner name', () => { - expect(RUNNER_NAME).toBe('strands-ts') - }) -}) From 14a756f4e42452f5daf9dc1ef2413a8d7ed169ab Mon Sep 17 00:00:00 2001 From: Jonathan Segev Date: Sun, 14 Jun 2026 00:32:20 -0400 Subject: [PATCH 29/29] refactor(strands-ts): collapse maxTokens config and share path guard Replace the near-uniform per-tier maxTokens map + fallback with a single default and a haiku special-case. Extract the duplicated path-traversal guard into one safeSegments() helper so the security check has a single definition. --- .../scripts/typescript/src/models.ts | 7 ++++--- .../scripts/typescript/src/tools/github.ts | 21 +++++++++++-------- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/strands-command/scripts/typescript/src/models.ts b/strands-command/scripts/typescript/src/models.ts index 36184ce..742434a 100644 --- a/strands-command/scripts/typescript/src/models.ts +++ b/strands-command/scripts/typescript/src/models.ts @@ -11,8 +11,9 @@ export const MODEL_IDS = { export type ModelTier = keyof typeof MODEL_IDS -const DEFAULT_MAX_TOKENS: Record = { haiku: 8000, sonnet: 16000, opus: 16000, fable: 16000 } -const FALLBACK_MAX_TOKENS = 16000 +// haiku gets a smaller budget; every other tier (and raw model ids) use the default. +const MAX_TOKENS = 16000 +const HAIKU_MAX_TOKENS = 8000 // A model choice is either a tier alias ("haiku" | "sonnet" | "opus" | "fable") // or a raw Bedrock model id (anything containing a dot). @@ -66,7 +67,7 @@ export function makeModel(choice: ModelChoice): BedrockModel { throw new Error(`Unknown model tier or id: ${choice}`) } const modelId = isTier ? MODEL_IDS[choice as ModelTier] : choice - const maxTokens = isTier ? DEFAULT_MAX_TOKENS[choice as ModelTier] : FALLBACK_MAX_TOKENS + const maxTokens = choice === 'haiku' ? HAIKU_MAX_TOKENS : MAX_TOKENS return new BedrockModel({ modelId, maxTokens, diff --git a/strands-command/scripts/typescript/src/tools/github.ts b/strands-command/scripts/typescript/src/tools/github.ts index 3fa5e85..c46015a 100644 --- a/strands-command/scripts/typescript/src/tools/github.ts +++ b/strands-command/scripts/typescript/src/tools/github.ts @@ -46,6 +46,16 @@ export const _http = { request: githubRequest } const PAGE_SIZE = 100 const MAX_PAGES = 10 +// Reject path-traversal segments from an agent-supplied path before it reaches +// an API URL. Returns the split segments for the caller to encode/join. +function safeSegments(path: string): string[] { + const segments = path.split('/') + if (segments.some((s) => s === '..' || s === '.' || s === '')) { + throw new Error(`Invalid file path: ${path}`) + } + return segments +} + async function paginate(endpointBase: string, repo?: string): Promise { const all: unknown[] = [] for (let page = 1; page <= MAX_PAGES; page++) { @@ -68,11 +78,7 @@ export async function getPrDiffRaw(prNumber: number, repo?: string): Promise { - const segments = path.split('/') - if (segments.some((s) => s === '..' || s === '.' || s === '')) { - throw new Error(`Invalid file path: ${path}`) - } - const safePath = segments.map(encodeURIComponent).join('/') + const safePath = safeSegments(path).map(encodeURIComponent).join('/') const data = await _http.request('GET', `contents/${safePath}?ref=${encodeURIComponent(ref)}`, repo, undefined) // The contents API returns base64; decode so the reviewing agent sees real text. if (data && typeof data === 'object' && 'content' in data && typeof (data as any).content === 'string') { @@ -85,10 +91,7 @@ export async function getFileContentsRaw(path: string, ref: string, repo?: strin export async function getFileHistoryRaw(path: string, repo?: string): Promise { // Recent commits touching this file — the history lens's data source // (no shell/git in the TS runner; history comes from the API). - const segments = path.split('/') - if (segments.some((s) => s === '..' || s === '.' || s === '')) { - throw new Error(`Invalid file path: ${path}`) - } + safeSegments(path) const data = await _http.request('GET', `commits?path=${encodeURIComponent(path)}&per_page=20`, repo, undefined) return JSON.stringify(data) }