diff --git a/.pi/prompts/is.md b/.pi/prompts/is.md index cf7da408..3dee06a5 100644 --- a/.pi/prompts/is.md +++ b/.pi/prompts/is.md @@ -1,5 +1,6 @@ --- description: Analyze GitHub issues (bugs or feature requests) +argument-hint: "" --- Analyze GitHub issue(s): $ARGUMENTS diff --git a/.pi/prompts/pr.md b/.pi/prompts/pr.md index cda6ad79..b8ce2da6 100644 --- a/.pi/prompts/pr.md +++ b/.pi/prompts/pr.md @@ -1,5 +1,6 @@ --- description: Review PRs from URLs with structured issue and code analysis +argument-hint: "" --- You are given one or more GitHub PR URLs: $@ diff --git a/.pi/prompts/wr.md b/.pi/prompts/wr.md index fa0250bc..65108e9c 100644 --- a/.pi/prompts/wr.md +++ b/.pi/prompts/wr.md @@ -1,5 +1,6 @@ --- description: Finish the current task end-to-end with changelog, commit, and push +argument-hint: "[instructions]" --- Wrap it. diff --git a/package-lock.json b/package-lock.json index 60c6044b..d6ef41ff 100644 --- a/package-lock.json +++ b/package-lock.json @@ -64,9 +64,9 @@ } }, "node_modules/@anthropic-ai/sdk": { - "version": "0.73.0", - "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.73.0.tgz", - "integrity": "sha512-URURVzhxXGJDGUGFunIOtBlSl7KWvZiAAKY/ttTkZAkXT9bTPqdk2eK0b8qqSxXpikh3QKPnPYpiyX98zf5ebw==", + "version": "0.90.0", + "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.90.0.tgz", + "integrity": "sha512-MzZtPabJF1b0FTDl6Z6H5ljphPwACLGP13lu8MTiB8jXaW/YXlpOp+Po2cVou3MPM5+f5toyLnul9whKCy7fBg==", "license": "MIT", "dependencies": { "json-schema-to-ts": "^3.1.1" @@ -223,56 +223,56 @@ } }, "node_modules/@aws-sdk/client-bedrock-runtime": { - "version": "3.1030.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-bedrock-runtime/-/client-bedrock-runtime-3.1030.0.tgz", - "integrity": "sha512-5Lnyx6mQPsIdld5Xr9FJqu8Hi9RVY6SgE8Rysmn4r3lRY2vNohNEu+gCtdXRDkkv/PgK9OnbA0sUPFU9rBRMYA==", + "version": "3.1031.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-bedrock-runtime/-/client-bedrock-runtime-3.1031.0.tgz", + "integrity": "sha512-ZgiSo2wslPXlv7wK4m2ULu2VfimbVRRlho0DqXhlvZGEqvtC209cMOxfPZWJ79Fz9sf0IzmWFkDtvMYjnwyLfw==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.973.27", - "@aws-sdk/credential-provider-node": "^3.972.30", - "@aws-sdk/eventstream-handler-node": "^3.972.13", - "@aws-sdk/middleware-eventstream": "^3.972.9", - "@aws-sdk/middleware-host-header": "^3.972.9", - "@aws-sdk/middleware-logger": "^3.972.9", - "@aws-sdk/middleware-recursion-detection": "^3.972.10", - "@aws-sdk/middleware-user-agent": "^3.972.29", - "@aws-sdk/middleware-websocket": "^3.972.15", - "@aws-sdk/region-config-resolver": "^3.972.11", - "@aws-sdk/token-providers": "3.1030.0", - "@aws-sdk/types": "^3.973.7", - "@aws-sdk/util-endpoints": "^3.996.6", - "@aws-sdk/util-user-agent-browser": "^3.972.9", - "@aws-sdk/util-user-agent-node": "^3.973.15", - "@smithy/config-resolver": "^4.4.14", - "@smithy/core": "^3.23.14", - "@smithy/eventstream-serde-browser": "^4.2.13", - "@smithy/eventstream-serde-config-resolver": "^4.3.13", - "@smithy/eventstream-serde-node": "^4.2.13", - "@smithy/fetch-http-handler": "^5.3.16", - "@smithy/hash-node": "^4.2.13", - "@smithy/invalid-dependency": "^4.2.13", - "@smithy/middleware-content-length": "^4.2.13", - "@smithy/middleware-endpoint": "^4.4.29", - "@smithy/middleware-retry": "^4.5.0", - "@smithy/middleware-serde": "^4.2.17", - "@smithy/middleware-stack": "^4.2.13", - "@smithy/node-config-provider": "^4.3.13", - "@smithy/node-http-handler": "^4.5.2", - "@smithy/protocol-http": "^5.3.13", - "@smithy/smithy-client": "^4.12.9", - "@smithy/types": "^4.14.0", - "@smithy/url-parser": "^4.2.13", + "@aws-sdk/core": "^3.974.0", + "@aws-sdk/credential-provider-node": "^3.972.31", + "@aws-sdk/eventstream-handler-node": "^3.972.14", + "@aws-sdk/middleware-eventstream": "^3.972.10", + "@aws-sdk/middleware-host-header": "^3.972.10", + "@aws-sdk/middleware-logger": "^3.972.10", + "@aws-sdk/middleware-recursion-detection": "^3.972.11", + "@aws-sdk/middleware-user-agent": "^3.972.30", + "@aws-sdk/middleware-websocket": "^3.972.16", + "@aws-sdk/region-config-resolver": "^3.972.12", + "@aws-sdk/token-providers": "3.1031.0", + "@aws-sdk/types": "^3.973.8", + "@aws-sdk/util-endpoints": "^3.996.7", + "@aws-sdk/util-user-agent-browser": "^3.972.10", + "@aws-sdk/util-user-agent-node": "^3.973.16", + "@smithy/config-resolver": "^4.4.16", + "@smithy/core": "^3.23.15", + "@smithy/eventstream-serde-browser": "^4.2.14", + "@smithy/eventstream-serde-config-resolver": "^4.3.14", + "@smithy/eventstream-serde-node": "^4.2.14", + "@smithy/fetch-http-handler": "^5.3.17", + "@smithy/hash-node": "^4.2.14", + "@smithy/invalid-dependency": "^4.2.14", + "@smithy/middleware-content-length": "^4.2.14", + "@smithy/middleware-endpoint": "^4.4.30", + "@smithy/middleware-retry": "^4.5.3", + "@smithy/middleware-serde": "^4.2.18", + "@smithy/middleware-stack": "^4.2.14", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/node-http-handler": "^4.5.3", + "@smithy/protocol-http": "^5.3.14", + "@smithy/smithy-client": "^4.12.11", + "@smithy/types": "^4.14.1", + "@smithy/url-parser": "^4.2.14", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", - "@smithy/util-defaults-mode-browser": "^4.3.45", - "@smithy/util-defaults-mode-node": "^4.2.49", - "@smithy/util-endpoints": "^3.3.4", - "@smithy/util-middleware": "^4.2.13", - "@smithy/util-retry": "^4.3.0", - "@smithy/util-stream": "^4.5.22", + "@smithy/util-defaults-mode-browser": "^4.3.47", + "@smithy/util-defaults-mode-node": "^4.2.52", + "@smithy/util-endpoints": "^3.4.1", + "@smithy/util-middleware": "^4.2.14", + "@smithy/util-retry": "^4.3.2", + "@smithy/util-stream": "^4.5.23", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, @@ -281,22 +281,22 @@ } }, "node_modules/@aws-sdk/core": { - "version": "3.973.27", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.973.27.tgz", - "integrity": "sha512-CUZ5m8hwMCH6OYI4Li/WgMfIEx10Q2PLI9Y3XOUTPGZJ53aZ0007jCv+X/ywsaERyKPdw5MRZWk877roQksQ4A==", + "version": "3.974.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.974.0.tgz", + "integrity": "sha512-8j+dMtyDqNXFmi09CBdz8TY6Ltf2jhfHuP6ZvG4zVjndRc6JF0aeBUbRwQLndbptFCsdctRQgdNWecy4TIfXAw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.7", - "@aws-sdk/xml-builder": "^3.972.17", - "@smithy/core": "^3.23.14", - "@smithy/node-config-provider": "^4.3.13", - "@smithy/property-provider": "^4.2.13", - "@smithy/protocol-http": "^5.3.13", - "@smithy/signature-v4": "^5.3.13", - "@smithy/smithy-client": "^4.12.9", - "@smithy/types": "^4.14.0", + "@aws-sdk/types": "^3.973.8", + "@aws-sdk/xml-builder": "^3.972.18", + "@smithy/core": "^3.23.15", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/property-provider": "^4.2.14", + "@smithy/protocol-http": "^5.3.14", + "@smithy/signature-v4": "^5.3.14", + "@smithy/smithy-client": "^4.12.11", + "@smithy/types": "^4.14.1", "@smithy/util-base64": "^4.3.2", - "@smithy/util-middleware": "^4.2.13", + "@smithy/util-middleware": "^4.2.14", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, @@ -305,15 +305,15 @@ } }, "node_modules/@aws-sdk/credential-provider-env": { - "version": "3.972.25", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.25.tgz", - "integrity": "sha512-6QfI0wv4jpG5CrdO/AO0JfZ2ux+tKwJPrUwmvxXF50vI5KIypKVGNF6b4vlkYEnKumDTI1NX2zUBi8JoU5QU3A==", + "version": "3.972.26", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.26.tgz", + "integrity": "sha512-WBHAMxyPdgeJY6ZGLvq9mJwzZ+GaNUROQbfdVshtMsDVBrZTj5ZuFjKclSjSHvKSHJ4Y4O2yvI/aA/hrJbYfng==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.27", - "@aws-sdk/types": "^3.973.7", - "@smithy/property-provider": "^4.2.13", - "@smithy/types": "^4.14.0", + "@aws-sdk/core": "^3.974.0", + "@aws-sdk/types": "^3.973.8", + "@smithy/property-provider": "^4.2.14", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -321,20 +321,20 @@ } }, "node_modules/@aws-sdk/credential-provider-http": { - "version": "3.972.27", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.27.tgz", - "integrity": "sha512-3V3Usj9Gs93h865DqN4M2NWJhC5kXU9BvZskfN3+69omuYlE3TZxOEcVQtBGLOloJB7BVfJKXVLqeNhOzHqSlQ==", + "version": "3.972.28", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.28.tgz", + "integrity": "sha512-+1DwCjjpo1WoiZTN08yGitI3nUwZUSQWVWFrW4C46HqZwACjcUQ7C66tnKPBTVxrEYYDOP11A6Afmu1L6ylt3g==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.27", - "@aws-sdk/types": "^3.973.7", - "@smithy/fetch-http-handler": "^5.3.16", - "@smithy/node-http-handler": "^4.5.2", - "@smithy/property-provider": "^4.2.13", - "@smithy/protocol-http": "^5.3.13", - "@smithy/smithy-client": "^4.12.9", - "@smithy/types": "^4.14.0", - "@smithy/util-stream": "^4.5.22", + "@aws-sdk/core": "^3.974.0", + "@aws-sdk/types": "^3.973.8", + "@smithy/fetch-http-handler": "^5.3.17", + "@smithy/node-http-handler": "^4.5.3", + "@smithy/property-provider": "^4.2.14", + "@smithy/protocol-http": "^5.3.14", + "@smithy/smithy-client": "^4.12.11", + "@smithy/types": "^4.14.1", + "@smithy/util-stream": "^4.5.23", "tslib": "^2.6.2" }, "engines": { @@ -342,24 +342,24 @@ } }, "node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.972.29", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.29.tgz", - "integrity": "sha512-SiBuAnXecCbT/OpAf3vqyI/AVE3mTaYr9ShXLybxZiPLBiPCCOIWSGAtYYGQWMRvobBTiqOewaB+wcgMMZI2Aw==", + "version": "3.972.30", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.30.tgz", + "integrity": "sha512-Fg1oJcoijwOZjTxdbx+ubqbQl8YEQ4Cwhjw6TWzQjuDEvQYNhnCXW2pN7eKtdTrdE4a6+5TVKGSm2I+i2BKIQg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.27", - "@aws-sdk/credential-provider-env": "^3.972.25", - "@aws-sdk/credential-provider-http": "^3.972.27", - "@aws-sdk/credential-provider-login": "^3.972.29", - "@aws-sdk/credential-provider-process": "^3.972.25", - "@aws-sdk/credential-provider-sso": "^3.972.29", - "@aws-sdk/credential-provider-web-identity": "^3.972.29", - "@aws-sdk/nested-clients": "^3.996.19", - "@aws-sdk/types": "^3.973.7", - "@smithy/credential-provider-imds": "^4.2.13", - "@smithy/property-provider": "^4.2.13", - "@smithy/shared-ini-file-loader": "^4.4.8", - "@smithy/types": "^4.14.0", + "@aws-sdk/core": "^3.974.0", + "@aws-sdk/credential-provider-env": "^3.972.26", + "@aws-sdk/credential-provider-http": "^3.972.28", + "@aws-sdk/credential-provider-login": "^3.972.30", + "@aws-sdk/credential-provider-process": "^3.972.26", + "@aws-sdk/credential-provider-sso": "^3.972.30", + "@aws-sdk/credential-provider-web-identity": "^3.972.30", + "@aws-sdk/nested-clients": "^3.996.20", + "@aws-sdk/types": "^3.973.8", + "@smithy/credential-provider-imds": "^4.2.14", + "@smithy/property-provider": "^4.2.14", + "@smithy/shared-ini-file-loader": "^4.4.9", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -367,18 +367,18 @@ } }, "node_modules/@aws-sdk/credential-provider-login": { - "version": "3.972.29", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.29.tgz", - "integrity": "sha512-OGOslTbOlxXexKMqhxCEbBQbUIfuhGxU5UXw3Fm56ypXHvrXH4aTt/xb5Y884LOoteP1QST1lVZzHfcTnWhiPQ==", + "version": "3.972.30", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.30.tgz", + "integrity": "sha512-nchIrrI/7dgjG1bW/DEWOJc00K9n+kkl6B8Mk0KO6d4GfWBOXlVr9uHp7CJR9FIrjmov5SGjHXG2q9XAtkRw6Q==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.27", - "@aws-sdk/nested-clients": "^3.996.19", - "@aws-sdk/types": "^3.973.7", - "@smithy/property-provider": "^4.2.13", - "@smithy/protocol-http": "^5.3.13", - "@smithy/shared-ini-file-loader": "^4.4.8", - "@smithy/types": "^4.14.0", + "@aws-sdk/core": "^3.974.0", + "@aws-sdk/nested-clients": "^3.996.20", + "@aws-sdk/types": "^3.973.8", + "@smithy/property-provider": "^4.2.14", + "@smithy/protocol-http": "^5.3.14", + "@smithy/shared-ini-file-loader": "^4.4.9", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -386,22 +386,22 @@ } }, "node_modules/@aws-sdk/credential-provider-node": { - "version": "3.972.30", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.30.tgz", - "integrity": "sha512-FMnAnWxc8PG+ZrZ2OBKzY4luCUJhe9CG0B9YwYr4pzrYGLXBS2rl+UoUvjGbAwiptxRL6hyA3lFn03Bv1TLqTw==", + "version": "3.972.31", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.31.tgz", + "integrity": "sha512-99OHVQ6eZ5DTxiOWgHdjBMvLqv7xoY4jLK6nZ1NcNSQbAnYZkQNIHi/VqInc9fnmg7of9si/z+waE6YL9OQIlw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/credential-provider-env": "^3.972.25", - "@aws-sdk/credential-provider-http": "^3.972.27", - "@aws-sdk/credential-provider-ini": "^3.972.29", - "@aws-sdk/credential-provider-process": "^3.972.25", - "@aws-sdk/credential-provider-sso": "^3.972.29", - "@aws-sdk/credential-provider-web-identity": "^3.972.29", - "@aws-sdk/types": "^3.973.7", - "@smithy/credential-provider-imds": "^4.2.13", - "@smithy/property-provider": "^4.2.13", - "@smithy/shared-ini-file-loader": "^4.4.8", - "@smithy/types": "^4.14.0", + "@aws-sdk/credential-provider-env": "^3.972.26", + "@aws-sdk/credential-provider-http": "^3.972.28", + "@aws-sdk/credential-provider-ini": "^3.972.30", + "@aws-sdk/credential-provider-process": "^3.972.26", + "@aws-sdk/credential-provider-sso": "^3.972.30", + "@aws-sdk/credential-provider-web-identity": "^3.972.30", + "@aws-sdk/types": "^3.973.8", + "@smithy/credential-provider-imds": "^4.2.14", + "@smithy/property-provider": "^4.2.14", + "@smithy/shared-ini-file-loader": "^4.4.9", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -409,16 +409,16 @@ } }, "node_modules/@aws-sdk/credential-provider-process": { - "version": "3.972.25", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.25.tgz", - "integrity": "sha512-HR7ynNRdNhNsdVCOCegy1HsfsRzozCOPtD3RzzT1JouuaHobWyRfJzCBue/3jP7gECHt+kQyZUvwg/cYLWurNQ==", + "version": "3.972.26", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.26.tgz", + "integrity": "sha512-jibxNld3m+vbmQwn98hcQ+fLIVrx3cQuhZlSs1/hix48SjDS5/pjMLwpmtLD/lFnd6ve1AL4o1bZg3X1WRa2SQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.27", - "@aws-sdk/types": "^3.973.7", - "@smithy/property-provider": "^4.2.13", - "@smithy/shared-ini-file-loader": "^4.4.8", - "@smithy/types": "^4.14.0", + "@aws-sdk/core": "^3.974.0", + "@aws-sdk/types": "^3.973.8", + "@smithy/property-provider": "^4.2.14", + "@smithy/shared-ini-file-loader": "^4.4.9", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -426,36 +426,18 @@ } }, "node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.972.29", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.29.tgz", - "integrity": "sha512-HWv4SEq3jZDYPlwryZVef97+U8CxxRos5mK8sgGO1dQaFZpV5giZLzqGE5hkDmh2csYcBO2uf5XHjPTpZcJlig==", + "version": "3.972.30", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.30.tgz", + "integrity": "sha512-honYIM17F/+QSWJRE84T4u//ofqEi7rLbnwmIpu7fgFX5PML78wbtdSAy5Xwyve3TLpE9/f9zQx0aBVxSjAOPw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.27", - "@aws-sdk/nested-clients": "^3.996.19", - "@aws-sdk/token-providers": "3.1026.0", - "@aws-sdk/types": "^3.973.7", - "@smithy/property-provider": "^4.2.13", - "@smithy/shared-ini-file-loader": "^4.4.8", - "@smithy/types": "^4.14.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers": { - "version": "3.1026.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1026.0.tgz", - "integrity": "sha512-Ieq/HiRrbEtrYP387Nes0XlR7H1pJiJOZKv+QyQzMYpvTiDs0VKy2ZB3E2Zf+aFovWmeE7lRE4lXyF7dYM6GgA==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "^3.973.27", - "@aws-sdk/nested-clients": "^3.996.19", - "@aws-sdk/types": "^3.973.7", - "@smithy/property-provider": "^4.2.13", - "@smithy/shared-ini-file-loader": "^4.4.8", - "@smithy/types": "^4.14.0", + "@aws-sdk/core": "^3.974.0", + "@aws-sdk/nested-clients": "^3.996.20", + "@aws-sdk/token-providers": "3.1031.0", + "@aws-sdk/types": "^3.973.8", + "@smithy/property-provider": "^4.2.14", + "@smithy/shared-ini-file-loader": "^4.4.9", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -463,17 +445,17 @@ } }, "node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.972.29", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.29.tgz", - "integrity": "sha512-PdMBza1WEKEUPFEmMGCfnU2RYCz9MskU2e8JxjyUOsMKku7j9YaDKvbDi2dzC0ihFoM6ods2SbhfAAro+Gwlew==", + "version": "3.972.30", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.30.tgz", + "integrity": "sha512-CyL4oWUlONQRN2SsYMVrA9Z3i3QfLWTQctI8tuKbjNGCVVDCnJf/yMbSJCOZgpPFRtxh7dgQwvpqwmJm+iytmw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.27", - "@aws-sdk/nested-clients": "^3.996.19", - "@aws-sdk/types": "^3.973.7", - "@smithy/property-provider": "^4.2.13", - "@smithy/shared-ini-file-loader": "^4.4.8", - "@smithy/types": "^4.14.0", + "@aws-sdk/core": "^3.974.0", + "@aws-sdk/nested-clients": "^3.996.20", + "@aws-sdk/types": "^3.973.8", + "@smithy/property-provider": "^4.2.14", + "@smithy/shared-ini-file-loader": "^4.4.9", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -481,14 +463,14 @@ } }, "node_modules/@aws-sdk/eventstream-handler-node": { - "version": "3.972.13", - "resolved": "https://registry.npmjs.org/@aws-sdk/eventstream-handler-node/-/eventstream-handler-node-3.972.13.tgz", - "integrity": "sha512-2Pi1kD0MDkMAxDHqvpi/hKMs9hXUYbj2GLEjCwy+0jzfLChAsF50SUYnOeTI+RztA+Ic4pnLAdB03f1e8nggxQ==", + "version": "3.972.14", + "resolved": "https://registry.npmjs.org/@aws-sdk/eventstream-handler-node/-/eventstream-handler-node-3.972.14.tgz", + "integrity": "sha512-m4X56gxG76/CKfxNVbOFuYwnAZcHgS6HOH8lgp15HoGHIAVTcZfZrXvcYzJFOMLEJgVn+JHBu6EiNV+xSNXXFg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.7", - "@smithy/eventstream-codec": "^4.2.13", - "@smithy/types": "^4.14.0", + "@aws-sdk/types": "^3.973.8", + "@smithy/eventstream-codec": "^4.2.14", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -496,14 +478,14 @@ } }, "node_modules/@aws-sdk/middleware-eventstream": { - "version": "3.972.9", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-eventstream/-/middleware-eventstream-3.972.9.tgz", - "integrity": "sha512-ypgOvpWxQTCnQyDHGxnTviqqANE7FIIzII7VczJnTPCJcJlu17hMQXnvE47aKSKsawVJAaaRsyOEbHQuLJF9ng==", + "version": "3.972.10", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-eventstream/-/middleware-eventstream-3.972.10.tgz", + "integrity": "sha512-QUqLs7Af1II9X4fCRAu+EGHG3KHyOp4RkuLhRKoA3NuFlh6TL8i+zXBl8w2LUxqm44B/Kom45hgSlwA1SpTsXQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.7", - "@smithy/protocol-http": "^5.3.13", - "@smithy/types": "^4.14.0", + "@aws-sdk/types": "^3.973.8", + "@smithy/protocol-http": "^5.3.14", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -511,14 +493,14 @@ } }, "node_modules/@aws-sdk/middleware-host-header": { - "version": "3.972.9", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.972.9.tgz", - "integrity": "sha512-je5vRdNw4SkuTnmRbFZLdye4sQ0faLt8kwka5wnnSU30q1mHO4X+idGEJOOE+Tn1ME7Oryn05xxkDvIb3UaLaQ==", + "version": "3.972.10", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.972.10.tgz", + "integrity": "sha512-IJSsIMeVQ8MMCPbuh1AbltkFhLBLXn7aejzfX5YKT/VLDHn++Dcz8886tXckE+wQssyPUhaXrJhdakO2VilRhg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.7", - "@smithy/protocol-http": "^5.3.13", - "@smithy/types": "^4.14.0", + "@aws-sdk/types": "^3.973.8", + "@smithy/protocol-http": "^5.3.14", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -526,13 +508,13 @@ } }, "node_modules/@aws-sdk/middleware-logger": { - "version": "3.972.9", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.972.9.tgz", - "integrity": "sha512-HsVgDrruhqI28RkaXALm8grJ7Agc1wF6Et0xh6pom8NdO2VdO/SD9U/tPwUjewwK/pVoka+EShBxyCvgsPCtog==", + "version": "3.972.10", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.972.10.tgz", + "integrity": "sha512-OOuGvvz1Dm20SjZo5oEBePFqxt5nf8AwkNDSyUHvD9/bfNASmstcYxFAHUowy4n6Io7mWUZ04JURZwSBvyQanQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.7", - "@smithy/types": "^4.14.0", + "@aws-sdk/types": "^3.973.8", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -540,15 +522,15 @@ } }, "node_modules/@aws-sdk/middleware-recursion-detection": { - "version": "3.972.10", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.972.10.tgz", - "integrity": "sha512-RVQQbq5orQ/GHUnXvqEOj2HHPBJm+mM+ySwZKS5UaLBwra5ugRtiH09PLUoOZRl7a1YzaOzXSuGbn9iD5j60WQ==", + "version": "3.972.11", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.972.11.tgz", + "integrity": "sha512-+zz6f79Kj9V5qFK2P+D8Ehjnw4AhphAlCAsPjUqEcInA9umtSSKMrHbSagEeOIsDNuvVrH98bjRHcyQukTrhaQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.7", + "@aws-sdk/types": "^3.973.8", "@aws/lambda-invoke-store": "^0.2.2", - "@smithy/protocol-http": "^5.3.13", - "@smithy/types": "^4.14.0", + "@smithy/protocol-http": "^5.3.14", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -556,18 +538,18 @@ } }, "node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.972.29", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.29.tgz", - "integrity": "sha512-f/sIRzuTfEjg6NsbMYvye2VsmnQoNgntntleQyx5uGacUYzszbfIlO3GcI6G6daWUmTm0IDZc11qMHWwF0o0mQ==", + "version": "3.972.30", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.30.tgz", + "integrity": "sha512-lCz6JfelhjD6Eco1urXM2rOYRaxROSqeoY6IEKx+soegFJOajmIBCMHTAWuJl25Wf9IAST+i0/yOk9G3rMV26A==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.27", - "@aws-sdk/types": "^3.973.7", - "@aws-sdk/util-endpoints": "^3.996.6", - "@smithy/core": "^3.23.14", - "@smithy/protocol-http": "^5.3.13", - "@smithy/types": "^4.14.0", - "@smithy/util-retry": "^4.3.0", + "@aws-sdk/core": "^3.974.0", + "@aws-sdk/types": "^3.973.8", + "@aws-sdk/util-endpoints": "^3.996.7", + "@smithy/core": "^3.23.15", + "@smithy/protocol-http": "^5.3.14", + "@smithy/types": "^4.14.1", + "@smithy/util-retry": "^4.3.2", "tslib": "^2.6.2" }, "engines": { @@ -575,19 +557,19 @@ } }, "node_modules/@aws-sdk/middleware-websocket": { - "version": "3.972.15", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-websocket/-/middleware-websocket-3.972.15.tgz", - "integrity": "sha512-hsZ35FORQsN5hwNdMD6zWmHCphbXkDxO6j+xwCUiuMb0O6gzS/PWgttQNl1OAn7h/uqZAMUG4yOS0wY/yhAieg==", + "version": "3.972.16", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-websocket/-/middleware-websocket-3.972.16.tgz", + "integrity": "sha512-86+S9oCyRVGzoMRpQhxkArp7kD2K75GPmaNevd9B6EyNhWoNvnCZZ3WbgN4j7ZT+jvtvBCGZvI2XHsWZJ+BRIg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.7", - "@aws-sdk/util-format-url": "^3.972.9", - "@smithy/eventstream-codec": "^4.2.13", - "@smithy/eventstream-serde-browser": "^4.2.13", - "@smithy/fetch-http-handler": "^5.3.16", - "@smithy/protocol-http": "^5.3.13", - "@smithy/signature-v4": "^5.3.13", - "@smithy/types": "^4.14.0", + "@aws-sdk/types": "^3.973.8", + "@aws-sdk/util-format-url": "^3.972.10", + "@smithy/eventstream-codec": "^4.2.14", + "@smithy/eventstream-serde-browser": "^4.2.14", + "@smithy/fetch-http-handler": "^5.3.17", + "@smithy/protocol-http": "^5.3.14", + "@smithy/signature-v4": "^5.3.14", + "@smithy/types": "^4.14.1", "@smithy/util-base64": "^4.3.2", "@smithy/util-hex-encoding": "^4.2.2", "@smithy/util-utf8": "^4.2.2", @@ -598,47 +580,47 @@ } }, "node_modules/@aws-sdk/nested-clients": { - "version": "3.996.19", - "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.996.19.tgz", - "integrity": "sha512-uFkmCDXvmQYLanlYdOFS0+MQWkrj9wPMt/ZCc/0J0fjPim6F5jBVBmEomvGY/j77ILW6GTPwN22Jc174Mhkw6Q==", + "version": "3.996.20", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.996.20.tgz", + "integrity": "sha512-bzPdsNQnCh6TvvUmTHLZlL8qgyME6mNiUErcRMyJPywIl1BEu2VZRShel3mUoSh89bOBEXEWtjocDMolFxd/9A==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.973.27", - "@aws-sdk/middleware-host-header": "^3.972.9", - "@aws-sdk/middleware-logger": "^3.972.9", - "@aws-sdk/middleware-recursion-detection": "^3.972.10", - "@aws-sdk/middleware-user-agent": "^3.972.29", - "@aws-sdk/region-config-resolver": "^3.972.11", - "@aws-sdk/types": "^3.973.7", - "@aws-sdk/util-endpoints": "^3.996.6", - "@aws-sdk/util-user-agent-browser": "^3.972.9", - "@aws-sdk/util-user-agent-node": "^3.973.15", - "@smithy/config-resolver": "^4.4.14", - "@smithy/core": "^3.23.14", - "@smithy/fetch-http-handler": "^5.3.16", - "@smithy/hash-node": "^4.2.13", - "@smithy/invalid-dependency": "^4.2.13", - "@smithy/middleware-content-length": "^4.2.13", - "@smithy/middleware-endpoint": "^4.4.29", - "@smithy/middleware-retry": "^4.5.0", - "@smithy/middleware-serde": "^4.2.17", - "@smithy/middleware-stack": "^4.2.13", - "@smithy/node-config-provider": "^4.3.13", - "@smithy/node-http-handler": "^4.5.2", - "@smithy/protocol-http": "^5.3.13", - "@smithy/smithy-client": "^4.12.9", - "@smithy/types": "^4.14.0", - "@smithy/url-parser": "^4.2.13", + "@aws-sdk/core": "^3.974.0", + "@aws-sdk/middleware-host-header": "^3.972.10", + "@aws-sdk/middleware-logger": "^3.972.10", + "@aws-sdk/middleware-recursion-detection": "^3.972.11", + "@aws-sdk/middleware-user-agent": "^3.972.30", + "@aws-sdk/region-config-resolver": "^3.972.12", + "@aws-sdk/types": "^3.973.8", + "@aws-sdk/util-endpoints": "^3.996.7", + "@aws-sdk/util-user-agent-browser": "^3.972.10", + "@aws-sdk/util-user-agent-node": "^3.973.16", + "@smithy/config-resolver": "^4.4.16", + "@smithy/core": "^3.23.15", + "@smithy/fetch-http-handler": "^5.3.17", + "@smithy/hash-node": "^4.2.14", + "@smithy/invalid-dependency": "^4.2.14", + "@smithy/middleware-content-length": "^4.2.14", + "@smithy/middleware-endpoint": "^4.4.30", + "@smithy/middleware-retry": "^4.5.3", + "@smithy/middleware-serde": "^4.2.18", + "@smithy/middleware-stack": "^4.2.14", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/node-http-handler": "^4.5.3", + "@smithy/protocol-http": "^5.3.14", + "@smithy/smithy-client": "^4.12.11", + "@smithy/types": "^4.14.1", + "@smithy/url-parser": "^4.2.14", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", - "@smithy/util-defaults-mode-browser": "^4.3.45", - "@smithy/util-defaults-mode-node": "^4.2.49", - "@smithy/util-endpoints": "^3.3.4", - "@smithy/util-middleware": "^4.2.13", - "@smithy/util-retry": "^4.3.0", + "@smithy/util-defaults-mode-browser": "^4.3.47", + "@smithy/util-defaults-mode-node": "^4.2.52", + "@smithy/util-endpoints": "^3.4.1", + "@smithy/util-middleware": "^4.2.14", + "@smithy/util-retry": "^4.3.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, @@ -647,15 +629,15 @@ } }, "node_modules/@aws-sdk/region-config-resolver": { - "version": "3.972.11", - "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.972.11.tgz", - "integrity": "sha512-6Q8B1dcx6BBqUTY1Mc/eROKA0FImEEY5VPSd6AGPEUf0ErjExz4snVqa9kNJSoVDV1rKaNf3qrWojgcKW+SdDg==", + "version": "3.972.12", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.972.12.tgz", + "integrity": "sha512-QQI43Mxd53nBij0pm8HXC+t4IOC6gnhhZfzxE0OATQyO6QfPV4e+aTIRRuAJKA6Nig/cR8eLwPryqYTX9ZrjAQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.7", - "@smithy/config-resolver": "^4.4.14", - "@smithy/node-config-provider": "^4.3.13", - "@smithy/types": "^4.14.0", + "@aws-sdk/types": "^3.973.8", + "@smithy/config-resolver": "^4.4.16", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -663,17 +645,17 @@ } }, "node_modules/@aws-sdk/token-providers": { - "version": "3.1030.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1030.0.tgz", - "integrity": "sha512-gUuCLTnEiUgpxHEnJSidxZZlQ+rQwc/mrijz6DxeMijTwS3/e3UfJvL8C1YDvcbt8MkkXj92h0MpYtfhR+EGeg==", + "version": "3.1031.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1031.0.tgz", + "integrity": "sha512-zj/PvnbQK/2KJNln5K2QRI9HSsy+B4emz2gbQyUHkk6l7Lidu83P/9tfmC2cJXkcC3vdmyKH2DP3Iw/FDfKQuQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.27", - "@aws-sdk/nested-clients": "^3.996.19", - "@aws-sdk/types": "^3.973.7", - "@smithy/property-provider": "^4.2.13", - "@smithy/shared-ini-file-loader": "^4.4.8", - "@smithy/types": "^4.14.0", + "@aws-sdk/core": "^3.974.0", + "@aws-sdk/nested-clients": "^3.996.20", + "@aws-sdk/types": "^3.973.8", + "@smithy/property-provider": "^4.2.14", + "@smithy/shared-ini-file-loader": "^4.4.9", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -681,12 +663,12 @@ } }, "node_modules/@aws-sdk/types": { - "version": "3.973.7", - "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.7.tgz", - "integrity": "sha512-reXRwoJ6CfChoqAsBszUYajAF8Z2LRE+CRcKocvFSMpIiLOtYU3aJ9trmn6VVPAzbbY5LXF+FfmUslbXk1SYFg==", + "version": "3.973.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.8.tgz", + "integrity": "sha512-gjlAdtHMbtR9X5iIhVUvbVcy55KnznpC6bkDUWW9z915bi0ckdUr5cjf16Kp6xq0bP5HBD2xzgbL9F9Quv5vUw==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.14.0", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -694,15 +676,15 @@ } }, "node_modules/@aws-sdk/util-endpoints": { - "version": "3.996.6", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.996.6.tgz", - "integrity": "sha512-2nUQ+2ih7CShuKHpGSIYvvAIOHy52dOZguYG36zptBukhw6iFwcvGfG0tes0oZFWQqEWvgZe9HLWaNlvXGdOrg==", + "version": "3.996.7", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.996.7.tgz", + "integrity": "sha512-ty4LQxN1QC+YhUP28NfEgZDEGXkyqOQy+BDriBozqHsrYO4JMgiPhfizqOGF7P+euBTZ5Ez6SKlLAMCLo8tzmw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.7", - "@smithy/types": "^4.14.0", - "@smithy/url-parser": "^4.2.13", - "@smithy/util-endpoints": "^3.3.4", + "@aws-sdk/types": "^3.973.8", + "@smithy/types": "^4.14.1", + "@smithy/url-parser": "^4.2.14", + "@smithy/util-endpoints": "^3.4.1", "tslib": "^2.6.2" }, "engines": { @@ -710,14 +692,14 @@ } }, "node_modules/@aws-sdk/util-format-url": { - "version": "3.972.9", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-format-url/-/util-format-url-3.972.9.tgz", - "integrity": "sha512-fNJXHrs0ZT7Wx0KGIqKv7zLxlDXt2vqjx9z6oKUQFmpE5o4xxnSryvVHfHpIifYHWKz94hFccIldJ0YSZjlCBw==", + "version": "3.972.10", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-format-url/-/util-format-url-3.972.10.tgz", + "integrity": "sha512-DEKiHNJVtNxdyTeQspzY+15Po/kHm6sF0Cs4HV9Q2+lplB63+DrvdeiSoOSdWEWAoO2RcY1veoXVDz2tWxWCgQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.7", - "@smithy/querystring-builder": "^4.2.13", - "@smithy/types": "^4.14.0", + "@aws-sdk/types": "^3.973.8", + "@smithy/querystring-builder": "^4.2.14", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -737,27 +719,27 @@ } }, "node_modules/@aws-sdk/util-user-agent-browser": { - "version": "3.972.9", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.972.9.tgz", - "integrity": "sha512-sn/LMzTbGjYqCCF24390WxPd6hkpoSptiUn5DzVp4cD71yqw+yGEGm1YCxyEoPXyc8qciM8UzLJcZBFslxo5Uw==", + "version": "3.972.10", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.972.10.tgz", + "integrity": "sha512-FAzqXvfEssGdSIz8ejatan0bOdx1qefBWKF/gWmVBXIP1HkS7v/wjjaqrAGGKvyihrXTXW00/2/1nTJtxpXz7g==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.7", - "@smithy/types": "^4.14.0", + "@aws-sdk/types": "^3.973.8", + "@smithy/types": "^4.14.1", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.973.15", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.973.15.tgz", - "integrity": "sha512-fYn3s9PtKdgQkczGZCFMgkNEe8aq1JCVbnRqjqN9RSVW43xn2RV9xdcZ3z01a48Jpkuh/xCmBKJxdLOo4Ozg7w==", + "version": "3.973.16", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.973.16.tgz", + "integrity": "sha512-ccvu0FNCI0C6OqmxI/tWn7BD8qGooWuURssiIM+6vbksFO8opXR4JOGtGYPj8QYzN/vfwNYrcK344PPbYuvzRg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/middleware-user-agent": "^3.972.29", - "@aws-sdk/types": "^3.973.7", - "@smithy/node-config-provider": "^4.3.13", - "@smithy/types": "^4.14.0", + "@aws-sdk/middleware-user-agent": "^3.972.30", + "@aws-sdk/types": "^3.973.8", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/types": "^4.14.1", "@smithy/util-config-provider": "^4.2.2", "tslib": "^2.6.2" }, @@ -774,12 +756,12 @@ } }, "node_modules/@aws-sdk/xml-builder": { - "version": "3.972.17", - "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.17.tgz", - "integrity": "sha512-Ra7hjqAZf1OXRRMueB13qex7mFJRDK/pgCvdSFemXBT8KCGnQDPoKzHY1SjN+TjJVmnpSF14W5tJ1vDamFu+Gg==", + "version": "3.972.18", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.18.tgz", + "integrity": "sha512-BMDNVG1ETXRhl1tnisQiYBef3RShJ1kfZA7x7afivTFMLirfHNTb6U71K569HNXhSXbQZsweHvSDZ6euBw8hPA==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.14.0", + "@smithy/types": "^4.14.1", "fast-xml-parser": "5.5.8", "tslib": "^2.6.2" }, @@ -2901,16 +2883,16 @@ } }, "node_modules/@smithy/config-resolver": { - "version": "4.4.15", - "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.15.tgz", - "integrity": "sha512-BJdMBY5YO9iHh+lPLYdHv6LbX+J8IcPCYMl1IJdBt2KDWNHwONHrPVHk3ttYBqJd9wxv84wlbN0f7GlQzcQtNQ==", + "version": "4.4.16", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.16.tgz", + "integrity": "sha512-GFlGPNLZKrGfqWpqVb31z7hvYCA9ZscfX1buYnvvMGcRYsQQnhH+4uN6mWWflcD5jB4OXP/LBrdpukEdjl41tg==", "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.3.13", - "@smithy/types": "^4.14.0", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/types": "^4.14.1", "@smithy/util-config-provider": "^4.2.2", - "@smithy/util-endpoints": "^3.4.0", - "@smithy/util-middleware": "^4.2.13", + "@smithy/util-endpoints": "^3.4.1", + "@smithy/util-middleware": "^4.2.14", "tslib": "^2.6.2" }, "engines": { @@ -2918,18 +2900,18 @@ } }, "node_modules/@smithy/core": { - "version": "3.23.14", - "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.23.14.tgz", - "integrity": "sha512-vJ0IhpZxZAkFYOegMKSrxw7ujhhT2pass/1UEcZ4kfl5srTAqtPU5I7MdYQoreVas3204ykCiNhY1o7Xlz6Yyg==", + "version": "3.23.15", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.23.15.tgz", + "integrity": "sha512-E7GVCgsQttzfujEZb6Qep005wWf4xiL4x06apFEtzQMWYBPggZh/0cnOxPficw5cuK/YjjkehKoIN4YUaSh0UQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^5.3.13", - "@smithy/types": "^4.14.0", - "@smithy/url-parser": "^4.2.13", + "@smithy/protocol-http": "^5.3.14", + "@smithy/types": "^4.14.1", + "@smithy/url-parser": "^4.2.14", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", - "@smithy/util-middleware": "^4.2.13", - "@smithy/util-stream": "^4.5.22", + "@smithy/util-middleware": "^4.2.14", + "@smithy/util-stream": "^4.5.23", "@smithy/util-utf8": "^4.2.2", "@smithy/uuid": "^1.1.2", "tslib": "^2.6.2" @@ -2939,15 +2921,15 @@ } }, "node_modules/@smithy/credential-provider-imds": { - "version": "4.2.13", - "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.13.tgz", - "integrity": "sha512-wboCPijzf6RJKLOvnjDAiBxGSmSnGXj35o5ZAWKDaHa/cvQ5U3ZJ13D4tMCE8JG4dxVAZFy/P0x/V9CwwdfULQ==", + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.14.tgz", + "integrity": "sha512-Au28zBN48ZAoXdooGUHemuVBrkE+Ie6RPmGNIAJsFqj33Vhb6xAgRifUydZ2aY+M+KaMAETAlKk5NC5h1G7wpg==", "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.3.13", - "@smithy/property-provider": "^4.2.13", - "@smithy/types": "^4.14.0", - "@smithy/url-parser": "^4.2.13", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/property-provider": "^4.2.14", + "@smithy/types": "^4.14.1", + "@smithy/url-parser": "^4.2.14", "tslib": "^2.6.2" }, "engines": { @@ -2955,13 +2937,13 @@ } }, "node_modules/@smithy/eventstream-codec": { - "version": "4.2.13", - "resolved": "https://registry.npmjs.org/@smithy/eventstream-codec/-/eventstream-codec-4.2.13.tgz", - "integrity": "sha512-vYahwBAtRaAcFbOmE9aLr12z7RiHYDSLcnogSdxfm7kKfsNa3wH+NU5r7vTeB5rKvLsWyPjVX8iH94brP7umiQ==", + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-codec/-/eventstream-codec-4.2.14.tgz", + "integrity": "sha512-erZq0nOIpzfeZdCyzZjdJb4nVSKLUmSkaQUVkRGQTXs30gyUGeKnrYEg+Xe1W5gE3aReS7IgsvANwVPxSzY6Pw==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/crc32": "5.2.0", - "@smithy/types": "^4.14.0", + "@smithy/types": "^4.14.1", "@smithy/util-hex-encoding": "^4.2.2", "tslib": "^2.6.2" }, @@ -2970,13 +2952,13 @@ } }, "node_modules/@smithy/eventstream-serde-browser": { - "version": "4.2.13", - "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-browser/-/eventstream-serde-browser-4.2.13.tgz", - "integrity": "sha512-wwybfcOX0tLqCcBP378TIU9IqrDuZq/tDV48LlZNydMpCnqnYr+hWBAYbRE+rFFf/p7IkDJySM3bgiMKP2ihPg==", + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-browser/-/eventstream-serde-browser-4.2.14.tgz", + "integrity": "sha512-8IelTCtTctWRbb+0Dcy+C0aICh1qa0qWXqgjcXDmMuCvPJRnv26hiDZoAau2ILOniki65mCPKqOQs/BaWvO4CQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/eventstream-serde-universal": "^4.2.13", - "@smithy/types": "^4.14.0", + "@smithy/eventstream-serde-universal": "^4.2.14", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -2984,12 +2966,12 @@ } }, "node_modules/@smithy/eventstream-serde-config-resolver": { - "version": "4.3.13", - "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-4.3.13.tgz", - "integrity": "sha512-ied1lO559PtAsMJzg2TKRlctLnEi1PfkNeMMpdwXDImk1zV9uvS/Oxoy/vcy9uv1GKZAjDAB5xT6ziE9fzm5wA==", + "version": "4.3.14", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-4.3.14.tgz", + "integrity": "sha512-sqHiHpYRYo3FJlaIxD1J8PhbcmJAm7IuM16mVnwSkCToD7g00IBZzKuiLNMGmftULmEUX6/UAz8/NN5uMP8bVA==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.14.0", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -2997,13 +2979,13 @@ } }, "node_modules/@smithy/eventstream-serde-node": { - "version": "4.2.13", - "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-node/-/eventstream-serde-node-4.2.13.tgz", - "integrity": "sha512-hFyK+ORJrxAN3RYoaD6+gsGDQjeix8HOEkosoajvXYZ4VeqonM3G4jd9IIRm/sWGXUKmudkY9KdYjzosUqdM8A==", + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-node/-/eventstream-serde-node-4.2.14.tgz", + "integrity": "sha512-Ht/8BuGlKfFTy0H3+8eEu0vdpwGztCnaLLXtpXNdQqiR7Hj4vFScU3T436vRAjATglOIPjJXronY+1WxxNLSiw==", "license": "Apache-2.0", "dependencies": { - "@smithy/eventstream-serde-universal": "^4.2.13", - "@smithy/types": "^4.14.0", + "@smithy/eventstream-serde-universal": "^4.2.14", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -3011,13 +2993,13 @@ } }, "node_modules/@smithy/eventstream-serde-universal": { - "version": "4.2.13", - "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-universal/-/eventstream-serde-universal-4.2.13.tgz", - "integrity": "sha512-kRrq4EKLGeOxhC2CBEhRNcu1KSzNJzYY7RK3S7CxMPgB5dRrv55WqQOtRwQxQLC04xqORFLUgnDlc6xrNUULaA==", + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-universal/-/eventstream-serde-universal-4.2.14.tgz", + "integrity": "sha512-lWyt4T2XQZUZgK3tQ3Wn0w3XBvZsK/vjTuJl6bXbnGZBHH0ZUSONTYiK9TgjTTzU54xQr3DRFwpjmhp0oLm3gg==", "license": "Apache-2.0", "dependencies": { - "@smithy/eventstream-codec": "^4.2.13", - "@smithy/types": "^4.14.0", + "@smithy/eventstream-codec": "^4.2.14", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -3025,14 +3007,14 @@ } }, "node_modules/@smithy/fetch-http-handler": { - "version": "5.3.16", - "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.16.tgz", - "integrity": "sha512-nYDRUIvNd4mFmuXraRWt6w5UsZTNqtj4hXJA/iiOD4tuseIdLP9Lq38teH/SZTcIFCa2f+27o7hYpIsWktJKEQ==", + "version": "5.3.17", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.17.tgz", + "integrity": "sha512-bXOvQzaSm6MnmLaWA1elgfQcAtN4UP3vXqV97bHuoOrHQOJiLT3ds6o9eo5bqd0TJfRFpzdGnDQdW3FACiAVdw==", "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^5.3.13", - "@smithy/querystring-builder": "^4.2.13", - "@smithy/types": "^4.14.0", + "@smithy/protocol-http": "^5.3.14", + "@smithy/querystring-builder": "^4.2.14", + "@smithy/types": "^4.14.1", "@smithy/util-base64": "^4.3.2", "tslib": "^2.6.2" }, @@ -3041,12 +3023,12 @@ } }, "node_modules/@smithy/hash-node": { - "version": "4.2.13", - "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.2.13.tgz", - "integrity": "sha512-4/oy9h0jjmY80a2gOIo75iLl8TOPhmtx4E2Hz+PfMjvx/vLtGY4TMU/35WRyH2JHPfT5CVB38u4JRow7gnmzJA==", + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.2.14.tgz", + "integrity": "sha512-8ZBDY2DD4wr+GGjTpPtiglEsqr0lUP+KHqgZcWczFf6qeZ/YRjMIOoQWVQlmwu7EtxKTd8YXD8lblmYcpBIA1g==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.14.0", + "@smithy/types": "^4.14.1", "@smithy/util-buffer-from": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" @@ -3056,12 +3038,12 @@ } }, "node_modules/@smithy/invalid-dependency": { - "version": "4.2.13", - "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.2.13.tgz", - "integrity": "sha512-jvC0RB/8BLj2SMIkY0Npl425IdnxZJxInpZJbu563zIRnVjpDMXevU3VMCRSabaLB0kf/eFIOusdGstrLJ8IDg==", + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.2.14.tgz", + "integrity": "sha512-c21qJiTSb25xvvOp+H2TNZzPCngrvl5vIPqPB8zQ/DmJF4QWXO19x1dWfMJZ6wZuuWUPPm0gV8C0cU3+ifcWuw==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.14.0", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -3081,13 +3063,13 @@ } }, "node_modules/@smithy/middleware-content-length": { - "version": "4.2.13", - "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.13.tgz", - "integrity": "sha512-IPMLm/LE4AZwu6qiE8Rr8vJsWhs9AtOdySRXrOM7xnvclp77Tyh7hMs/FRrMf26kgIe67vFJXXOSmVxS7oKeig==", + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.14.tgz", + "integrity": "sha512-xhHq7fX4/3lv5NHxLUk3OeEvl0xZ+Ek3qIbWaCL4f9JwgDZEclPBElljaZCAItdGPQl/kSM4LPMOpy1MYgprpw==", "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^5.3.13", - "@smithy/types": "^4.14.0", + "@smithy/protocol-http": "^5.3.14", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -3095,18 +3077,18 @@ } }, "node_modules/@smithy/middleware-endpoint": { - "version": "4.4.29", - "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.29.tgz", - "integrity": "sha512-R9Q/58U+qBiSARGWbAbFLczECg/RmysRksX6Q8BaQEpt75I7LI6WGDZnjuC9GXSGKljEbA7N118LhGaMbfrTXw==", + "version": "4.4.30", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.30.tgz", + "integrity": "sha512-qS2XqhKeXmdZ4nEQ4cOxIczSP/Y91wPAHYuRwmWDCh975B7/57uxsm5d6sisnUThn2u2FwzMdJNM7AbO1YPsPg==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.23.14", - "@smithy/middleware-serde": "^4.2.17", - "@smithy/node-config-provider": "^4.3.13", - "@smithy/shared-ini-file-loader": "^4.4.8", - "@smithy/types": "^4.14.0", - "@smithy/url-parser": "^4.2.13", - "@smithy/util-middleware": "^4.2.13", + "@smithy/core": "^3.23.15", + "@smithy/middleware-serde": "^4.2.18", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/shared-ini-file-loader": "^4.4.9", + "@smithy/types": "^4.14.1", + "@smithy/url-parser": "^4.2.14", + "@smithy/util-middleware": "^4.2.14", "tslib": "^2.6.2" }, "engines": { @@ -3114,19 +3096,19 @@ } }, "node_modules/@smithy/middleware-retry": { - "version": "4.5.1", - "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.5.1.tgz", - "integrity": "sha512-/zY+Gp7Qj2D2hVm3irkCyONER7E9MiX3cUUm/k2ZmhkzZkrPgwVS4aJ5NriZUEN/M0D1hhjrgjUmX04HhRwdWA==", + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.5.3.tgz", + "integrity": "sha512-TE8dJNi6JuxzGSxMCVd3i9IEWDndCl3bmluLsBNDWok8olgj65OfkndMhl9SZ7m14c+C5SQn/PcUmrDl57rSFw==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.23.14", - "@smithy/node-config-provider": "^4.3.13", - "@smithy/protocol-http": "^5.3.13", - "@smithy/service-error-classification": "^4.2.13", - "@smithy/smithy-client": "^4.12.9", - "@smithy/types": "^4.14.0", - "@smithy/util-middleware": "^4.2.13", - "@smithy/util-retry": "^4.3.1", + "@smithy/core": "^3.23.15", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/protocol-http": "^5.3.14", + "@smithy/service-error-classification": "^4.2.14", + "@smithy/smithy-client": "^4.12.11", + "@smithy/types": "^4.14.1", + "@smithy/util-middleware": "^4.2.14", + "@smithy/util-retry": "^4.3.2", "@smithy/uuid": "^1.1.2", "tslib": "^2.6.2" }, @@ -3135,14 +3117,14 @@ } }, "node_modules/@smithy/middleware-serde": { - "version": "4.2.17", - "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.17.tgz", - "integrity": "sha512-0T2mcaM6v9W1xku86Dk0bEW7aEseG6KenFkPK98XNw0ZhOqOiD1MrMsdnQw9QsL3/Oa85T53iSMlm0SZdSuIEQ==", + "version": "4.2.18", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.18.tgz", + "integrity": "sha512-M6CSgnp3v4tYz9ynj2JHbA60woBZcGqEwNjTKjBsNHPV26R1ZX52+0wW8WsZU18q45jD0tw2wL22S17Ze9LpEw==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.23.14", - "@smithy/protocol-http": "^5.3.13", - "@smithy/types": "^4.14.0", + "@smithy/core": "^3.23.15", + "@smithy/protocol-http": "^5.3.14", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -3150,12 +3132,12 @@ } }, "node_modules/@smithy/middleware-stack": { - "version": "4.2.13", - "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.2.13.tgz", - "integrity": "sha512-g72jN/sGDLyTanrCLH9fhg3oysO3f7tQa6eWWsMyn2BiYNCgjF24n4/I9wff/5XidFvjj9ilipAoQrurTUrLvw==", + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.2.14.tgz", + "integrity": "sha512-2dvkUKLuFdKsCRmOE4Mn63co0Djtsm+JMh0bYZQupN1pJwMeE8FmQmRLLzzEMN0dnNi7CDCYYH8F0EVwWiPBeA==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.14.0", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -3163,14 +3145,14 @@ } }, "node_modules/@smithy/node-config-provider": { - "version": "4.3.13", - "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.13.tgz", - "integrity": "sha512-iGxQ04DsKXLckbgnX4ipElrOTk+IHgTyu0q0WssZfYhDm9CQWHmu6cOeI5wmWRxpXbBDhIIfXMWz5tPEtcVqbw==", + "version": "4.3.14", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.14.tgz", + "integrity": "sha512-S+gFjyo/weSVL0P1b9Ts8C/CwIfNCgUPikk3sl6QVsfE/uUuO+QsF+NsE/JkpvWqqyz1wg7HFdiaZuj5CoBMRg==", "license": "Apache-2.0", "dependencies": { - "@smithy/property-provider": "^4.2.13", - "@smithy/shared-ini-file-loader": "^4.4.8", - "@smithy/types": "^4.14.0", + "@smithy/property-provider": "^4.2.14", + "@smithy/shared-ini-file-loader": "^4.4.9", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -3178,14 +3160,14 @@ } }, "node_modules/@smithy/node-http-handler": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.5.2.tgz", - "integrity": "sha512-/oD7u8M0oj2ZTFw7GkuuHWpIxtWdLlnyNkbrWcyVYhd5RJNDuczdkb0wfnQICyNFrVPlr8YHOhamjNy3zidhmA==", + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.5.3.tgz", + "integrity": "sha512-lc5jFL++x17sPhIwMWJ3YOnqmSjw/2Po6VLDlUIXvxVWRuJwRXnJ4jOBBLB0cfI5BB5ehIl02Fxr1PDvk/kxDw==", "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^5.3.13", - "@smithy/querystring-builder": "^4.2.13", - "@smithy/types": "^4.14.0", + "@smithy/protocol-http": "^5.3.14", + "@smithy/querystring-builder": "^4.2.14", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -3193,12 +3175,12 @@ } }, "node_modules/@smithy/property-provider": { - "version": "4.2.13", - "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.13.tgz", - "integrity": "sha512-bGzUCthxRmezuxkbu9wD33wWg9KX3hJpCXpQ93vVkPrHn9ZW6KNNdY5xAUWNuRCwQ+VyboFuWirG1lZhhkcyRQ==", + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.14.tgz", + "integrity": "sha512-WuM31CgfsnQ/10i7NYr0PyxqknD72Y5uMfUMVSniPjbEPceiTErb4eIqJQ+pdxNEAUEWrewrGjIRjVbVHsxZiQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.14.0", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -3206,12 +3188,12 @@ } }, "node_modules/@smithy/protocol-http": { - "version": "5.3.13", - "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.13.tgz", - "integrity": "sha512-+HsmuJUF4u8POo6s8/a2Yb/AQ5t/YgLovCuHF9oxbocqv+SZ6gd8lC2duBFiCA/vFHoHQhoq7QjqJqZC6xOxxg==", + "version": "5.3.14", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.14.tgz", + "integrity": "sha512-dN5F8kHx8RNU0r+pCwNmFZyz6ChjMkzShy/zup6MtkRmmix4vZzJdW+di7x//b1LiynIev88FM18ie+wwPcQtQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.14.0", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -3219,12 +3201,12 @@ } }, "node_modules/@smithy/querystring-builder": { - "version": "4.2.13", - "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.13.tgz", - "integrity": "sha512-tG4aOYFCZdPMjbgfhnIQ322H//ojujldp1SrHPHpBSb3NqgUp3dwiUGRJzie87hS1DYwWGqDuPaowoDF+rYCbQ==", + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.14.tgz", + "integrity": "sha512-XYA5Z0IqTeF+5XDdh4BBmSA0HvbgVZIyv4cmOoUheDNR57K1HgBp9ukUMx3Cr3XpDHHpLBnexPE3LAtDsZkj2A==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.14.0", + "@smithy/types": "^4.14.1", "@smithy/util-uri-escape": "^4.2.2", "tslib": "^2.6.2" }, @@ -3233,12 +3215,12 @@ } }, "node_modules/@smithy/querystring-parser": { - "version": "4.2.13", - "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.13.tgz", - "integrity": "sha512-hqW3Q4P+CDzUyQ87GrboGMeD7XYNMOF+CuTwu936UQRB/zeYn3jys8C3w+wMkDfY7CyyyVwZQ5cNFoG0x1pYmA==", + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.14.tgz", + "integrity": "sha512-hr+YyqBD23GVvRxGGrcc/oOeNlK3PzT5Fu4dzrDXxzS1LpFiuL2PQQqKPs87M79aW7ziMs+nvB3qdw77SqE7Lw==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.14.0", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -3246,24 +3228,24 @@ } }, "node_modules/@smithy/service-error-classification": { - "version": "4.2.13", - "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.2.13.tgz", - "integrity": "sha512-a0s8XZMfOC/qpqq7RCPvJlk93rWFrElH6O++8WJKz0FqnA4Y7fkNi/0mnGgSH1C4x6MFsuBA8VKu4zxFrMe5Vw==", + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.2.14.tgz", + "integrity": "sha512-vVimoUnGxlx4eLLQbZImdOZFOe+Zh+5ACntv8VxZuGP72LdWu5GV3oEmCahSEReBgRJoWjypFkrehSj7BWx1HQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.14.0" + "@smithy/types": "^4.14.1" }, "engines": { "node": ">=18.0.0" } }, "node_modules/@smithy/shared-ini-file-loader": { - "version": "4.4.8", - "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.4.8.tgz", - "integrity": "sha512-VZCZx2bZasxdqxVgEAhREvDSlkatTPnkdWy1+Kiy8w7kYPBosW0V5IeDwzDUMvWBt56zpK658rx1cOBFOYaPaw==", + "version": "4.4.9", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.4.9.tgz", + "integrity": "sha512-495/V2I15SHgedSJoDPD23JuSfKAp726ZI1V0wtjB07Wh7q/0tri/0e0DLefZCHgxZonrGKt/OCTpAtP1wE1kQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.14.0", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -3271,16 +3253,16 @@ } }, "node_modules/@smithy/signature-v4": { - "version": "5.3.13", - "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.13.tgz", - "integrity": "sha512-YpYSyM0vMDwKbHD/JA7bVOF6kToVRpa+FM5ateEVRpsTNu564g1muBlkTubXhSKKYXInhpADF46FPyrZcTLpXg==", + "version": "5.3.14", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.14.tgz", + "integrity": "sha512-1D9Y/nmlVjCeSivCbhZ7hgEpmHyY1h0GvpSZt3l0xcD9JjmjVC1CHOozS6+Gh+/ldMH8JuJ6cujObQqfayAVFA==", "license": "Apache-2.0", "dependencies": { "@smithy/is-array-buffer": "^4.2.2", - "@smithy/protocol-http": "^5.3.13", - "@smithy/types": "^4.14.0", + "@smithy/protocol-http": "^5.3.14", + "@smithy/types": "^4.14.1", "@smithy/util-hex-encoding": "^4.2.2", - "@smithy/util-middleware": "^4.2.13", + "@smithy/util-middleware": "^4.2.14", "@smithy/util-uri-escape": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" @@ -3290,17 +3272,17 @@ } }, "node_modules/@smithy/smithy-client": { - "version": "4.12.9", - "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.12.9.tgz", - "integrity": "sha512-ovaLEcTU5olSeHcRXcxV6viaKtpkHZumn6Ps0yn7dRf2rRSfy794vpjOtrWDO0d1auDSvAqxO+lyhERSXQ03EQ==", + "version": "4.12.11", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.12.11.tgz", + "integrity": "sha512-wzz/Wa1CH/Tlhxh0s4DQPEcXSxSVfJ59AZcUh9Gu0c6JTlKuwGf4o/3P2TExv0VbtPFt8odIBG+eQGK2+vTECg==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.23.14", - "@smithy/middleware-endpoint": "^4.4.29", - "@smithy/middleware-stack": "^4.2.13", - "@smithy/protocol-http": "^5.3.13", - "@smithy/types": "^4.14.0", - "@smithy/util-stream": "^4.5.22", + "@smithy/core": "^3.23.15", + "@smithy/middleware-endpoint": "^4.4.30", + "@smithy/middleware-stack": "^4.2.14", + "@smithy/protocol-http": "^5.3.14", + "@smithy/types": "^4.14.1", + "@smithy/util-stream": "^4.5.23", "tslib": "^2.6.2" }, "engines": { @@ -3308,9 +3290,9 @@ } }, "node_modules/@smithy/types": { - "version": "4.14.0", - "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.14.0.tgz", - "integrity": "sha512-OWgntFLW88kx2qvf/c/67Vno1yuXm/f9M7QFAtVkkO29IJXGBIg0ycEaBTH0kvCtwmvZxRujrgP5a86RvsXJAQ==", + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.14.1.tgz", + "integrity": "sha512-59b5HtSVrVR/eYNei3BUj3DCPKD/G7EtDDe7OEJE7i7FtQFugYo6MxbotS8mVJkLNVf8gYaAlEBwwtJ9HzhWSg==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -3320,13 +3302,13 @@ } }, "node_modules/@smithy/url-parser": { - "version": "4.2.13", - "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.2.13.tgz", - "integrity": "sha512-2G03yoboIRZlZze2+PT4GZEjgwQsJjUgn6iTsvxA02bVceHR6vp4Cuk7TUnPFWKF+ffNUk3kj4COwkENS2K3vw==", + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.2.14.tgz", + "integrity": "sha512-p06BiBigJ8bTA3MgnOfCtDUWnAMY0YfedO/GRpmc7p+wg3KW8vbXy1xwSu5ASy0wV7rRYtlfZOIKH4XqfhjSQQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/querystring-parser": "^4.2.13", - "@smithy/types": "^4.14.0", + "@smithy/querystring-parser": "^4.2.14", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -3397,14 +3379,14 @@ } }, "node_modules/@smithy/util-defaults-mode-browser": { - "version": "4.3.45", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.45.tgz", - "integrity": "sha512-ag9sWc6/nWZAuK3Wm9KlFJUnRkXLrXn33RFjIAmCTFThqLHY+7wCst10BGq56FxslsDrjhSie46c8OULS+BiIw==", + "version": "4.3.47", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.47.tgz", + "integrity": "sha512-zlIuXai3/SHjQUQ8y3g/woLvrH573SK2wNjcDaHu5e9VOcC0JwM1MI0Sq0GZJyN3BwSUneIhpjZ18nsiz5AtQw==", "license": "Apache-2.0", "dependencies": { - "@smithy/property-provider": "^4.2.13", - "@smithy/smithy-client": "^4.12.9", - "@smithy/types": "^4.14.0", + "@smithy/property-provider": "^4.2.14", + "@smithy/smithy-client": "^4.12.11", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -3412,17 +3394,17 @@ } }, "node_modules/@smithy/util-defaults-mode-node": { - "version": "4.2.50", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.50.tgz", - "integrity": "sha512-xpjncL5XozFA3No7WypTsPU1du0fFS8flIyO+Wh2nhCy7bpEapvU7BR55Bg+wrfw+1cRA+8G8UsTjaxgzrMzXg==", + "version": "4.2.52", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.52.tgz", + "integrity": "sha512-cQBz8g68Vnw1W2meXlkb3D/hXJU+Taiyj9P8qLJtjREEV9/Td65xi4A/H1sRQ8EIgX5qbZbvdYPKygKLholZ3w==", "license": "Apache-2.0", "dependencies": { - "@smithy/config-resolver": "^4.4.15", - "@smithy/credential-provider-imds": "^4.2.13", - "@smithy/node-config-provider": "^4.3.13", - "@smithy/property-provider": "^4.2.13", - "@smithy/smithy-client": "^4.12.9", - "@smithy/types": "^4.14.0", + "@smithy/config-resolver": "^4.4.16", + "@smithy/credential-provider-imds": "^4.2.14", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/property-provider": "^4.2.14", + "@smithy/smithy-client": "^4.12.11", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -3430,13 +3412,13 @@ } }, "node_modules/@smithy/util-endpoints": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.4.0.tgz", - "integrity": "sha512-QQHGPKkw6NPcU6TJ1rNEEa201srPtZiX4k61xL163vvs9sTqW/XKz+UEuJ00uvPqoN+5Rs4Ka1UJ7+Mp03IXJw==", + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.4.1.tgz", + "integrity": "sha512-wMxNDZJrgS5mQV9oxCs4TWl5767VMgOfqfZ3JHyCkMtGC2ykW9iPqMvFur695Otcc5yxLG8OKO/80tsQBxrhXg==", "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.3.13", - "@smithy/types": "^4.14.0", + "@smithy/node-config-provider": "^4.3.14", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -3456,12 +3438,12 @@ } }, "node_modules/@smithy/util-middleware": { - "version": "4.2.13", - "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.13.tgz", - "integrity": "sha512-GTooyrlmRTqvUen4eK7/K1p6kryF7bnDfq6XsAbIsf2mo51B/utaH+XThY6dKgNCWzMAaH/+OLmqaBuLhLWRow==", + "version": "4.2.14", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.14.tgz", + "integrity": "sha512-1Su2vj9RYNDEv/V+2E+jXkkwGsgR7dc4sfHn9Z7ruzQHJIEni9zzw5CauvRXlFJfmgcqYP8fWa0dkh2Q2YaQyw==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.14.0", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -3469,13 +3451,13 @@ } }, "node_modules/@smithy/util-retry": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.3.1.tgz", - "integrity": "sha512-FwmicpgWOkP5kZUjN3y+3JIom8NLGqSAJBeoIgK0rIToI817TEBHCrd0A2qGeKQlgDeP+Jzn4i0H/NLAXGy9uQ==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.3.2.tgz", + "integrity": "sha512-2+KTsJEwTi63NUv4uR9IQ+IFT1yu6Rf6JuoBK2WKaaJ/TRvOiOVGcXAsEqX/TQN2thR9yII21kPUJq1UV/WI2A==", "license": "Apache-2.0", "dependencies": { - "@smithy/service-error-classification": "^4.2.13", - "@smithy/types": "^4.14.0", + "@smithy/service-error-classification": "^4.2.14", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -3483,14 +3465,14 @@ } }, "node_modules/@smithy/util-stream": { - "version": "4.5.22", - "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.22.tgz", - "integrity": "sha512-3H8iq/0BfQjUs2/4fbHZ9aG9yNzcuZs24LPkcX1Q7Z+qpqaGM8+qbGmE8zo9m2nCRgamyvS98cHdcWvR6YUsew==", + "version": "4.5.23", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.23.tgz", + "integrity": "sha512-N6on1+ngJ3RznZOnDWNveIwnTSlqxNnXuNAh7ez889ZZaRdXoNRTXKgmYOLe6dB0gCmAVtuRScE1hymQFl4hpg==", "license": "Apache-2.0", "dependencies": { - "@smithy/fetch-http-handler": "^5.3.16", - "@smithy/node-http-handler": "^4.5.2", - "@smithy/types": "^4.14.0", + "@smithy/fetch-http-handler": "^5.3.17", + "@smithy/node-http-handler": "^4.5.3", + "@smithy/types": "^4.14.1", "@smithy/util-base64": "^4.3.2", "@smithy/util-buffer-from": "^4.2.2", "@smithy/util-hex-encoding": "^4.2.2", @@ -8555,10 +8537,10 @@ }, "packages/agent": { "name": "@mariozechner/pi-agent-core", - "version": "0.67.3", + "version": "0.67.6", "license": "MIT", "dependencies": { - "@mariozechner/pi-ai": "^0.67.3" + "@mariozechner/pi-ai": "^0.67.6" }, "devDependencies": { "@types/node": "^24.3.0", @@ -8588,11 +8570,11 @@ }, "packages/ai": { "name": "@mariozechner/pi-ai", - "version": "0.67.3", + "version": "0.67.6", "license": "MIT", "dependencies": { - "@anthropic-ai/sdk": "^0.73.0", - "@aws-sdk/client-bedrock-runtime": "^3.983.0", + "@anthropic-ai/sdk": "^0.90.0", + "@aws-sdk/client-bedrock-runtime": "^3.1030.0", "@google/genai": "^1.40.0", "@mistralai/mistralai": "1.14.1", "@sinclair/typebox": "^0.34.41", @@ -8636,13 +8618,13 @@ }, "packages/coding-agent": { "name": "@mariozechner/pi-coding-agent", - "version": "0.67.3", + "version": "0.67.6", "license": "MIT", "dependencies": { "@mariozechner/jiti": "^2.6.2", - "@mariozechner/pi-agent-core": "^0.67.3", - "@mariozechner/pi-ai": "^0.67.3", - "@mariozechner/pi-tui": "^0.67.3", + "@mariozechner/pi-agent-core": "^0.67.6", + "@mariozechner/pi-ai": "^0.67.6", + "@mariozechner/pi-tui": "^0.67.6", "@silvia-odwyer/photon-node": "^0.3.4", "ajv": "^8.17.1", "chalk": "^5.5.0", @@ -8683,22 +8665,22 @@ }, "packages/coding-agent/examples/extensions/custom-provider-anthropic": { "name": "pi-extension-custom-provider-anthropic", - "version": "1.18.3", + "version": "1.18.6", "dependencies": { "@anthropic-ai/sdk": "^0.52.0" } }, "packages/coding-agent/examples/extensions/custom-provider-gitlab-duo": { "name": "pi-extension-custom-provider-gitlab-duo", - "version": "1.18.3" + "version": "1.18.6" }, "packages/coding-agent/examples/extensions/custom-provider-qwen-cli": { "name": "pi-extension-custom-provider-qwen-cli", - "version": "1.17.3" + "version": "1.17.6" }, "packages/coding-agent/examples/extensions/with-deps": { "name": "pi-extension-with-deps", - "version": "1.31.3", + "version": "1.31.6", "dependencies": { "ms": "^2.1.3" }, @@ -8734,13 +8716,13 @@ }, "packages/mom": { "name": "@mariozechner/pi-mom", - "version": "0.67.3", + "version": "0.67.6", "license": "MIT", "dependencies": { "@anthropic-ai/sandbox-runtime": "^0.0.16", - "@mariozechner/pi-agent-core": "^0.67.3", - "@mariozechner/pi-ai": "^0.67.3", - "@mariozechner/pi-coding-agent": "^0.67.3", + "@mariozechner/pi-agent-core": "^0.67.6", + "@mariozechner/pi-ai": "^0.67.6", + "@mariozechner/pi-coding-agent": "^0.67.6", "@sinclair/typebox": "^0.34.0", "@slack/socket-mode": "^2.0.0", "@slack/web-api": "^7.0.0", @@ -8779,10 +8761,10 @@ }, "packages/pods": { "name": "@mariozechner/pi", - "version": "0.67.3", + "version": "0.67.6", "license": "MIT", "dependencies": { - "@mariozechner/pi-agent-core": "^0.67.3", + "@mariozechner/pi-agent-core": "^0.67.6", "chalk": "^5.5.0" }, "bin": { @@ -8795,7 +8777,7 @@ }, "packages/tui": { "name": "@mariozechner/pi-tui", - "version": "0.67.3", + "version": "0.67.6", "license": "MIT", "dependencies": { "@types/mime-types": "^2.1.4", @@ -8842,12 +8824,12 @@ }, "packages/web-ui": { "name": "@mariozechner/pi-web-ui", - "version": "0.67.3", + "version": "0.67.6", "license": "MIT", "dependencies": { "@lmstudio/sdk": "^1.5.0", - "@mariozechner/pi-ai": "^0.67.3", - "@mariozechner/pi-tui": "^0.67.3", + "@mariozechner/pi-ai": "^0.67.6", + "@mariozechner/pi-tui": "^0.67.6", "docx-preview": "^0.3.7", "jszip": "^3.10.1", "lucide": "^0.544.0", @@ -8868,7 +8850,7 @@ }, "packages/web-ui/example": { "name": "pi-web-ui-example", - "version": "1.55.3", + "version": "1.55.6", "dependencies": { "@mariozechner/mini-lit": "^0.2.0", "@mariozechner/pi-ai": "file:../../ai", diff --git a/packages/agent/CHANGELOG.md b/packages/agent/CHANGELOG.md index b16bfac0..5855835f 100644 --- a/packages/agent/CHANGELOG.md +++ b/packages/agent/CHANGELOG.md @@ -2,6 +2,16 @@ ## [Unreleased] +### Fixed + +- Fixed parallel tool-call finalization to convert `afterToolCall` hook throws into error tool results instead of aborting the batch ([#3084](https://github.com/badlogic/pi-mono/issues/3084)) + +## [0.67.6] - 2026-04-16 + +## [0.67.5] - 2026-04-16 + +## [0.67.4] - 2026-04-16 + ## [0.67.3] - 2026-04-15 ## [0.67.2] - 2026-04-14 diff --git a/packages/agent/package.json b/packages/agent/package.json index 353e9993..76ebbe8d 100644 --- a/packages/agent/package.json +++ b/packages/agent/package.json @@ -1,6 +1,6 @@ { "name": "@mariozechner/pi-agent-core", - "version": "0.67.3", + "version": "0.67.6", "description": "General-purpose agent with transport abstraction, state management, and attachment support", "type": "module", "main": "./dist/index.js", @@ -17,7 +17,7 @@ "prepublishOnly": "npm run clean && npm run build" }, "dependencies": { - "@mariozechner/pi-ai": "^0.67.3" + "@mariozechner/pi-ai": "^0.67.6" }, "keywords": [ "ai", diff --git a/packages/agent/src/agent-loop.ts b/packages/agent/src/agent-loop.ts index 586a1b7d..8482c5ce 100644 --- a/packages/agent/src/agent-loop.ts +++ b/packages/agent/src/agent-loop.ts @@ -571,23 +571,28 @@ async function finalizeExecutedToolCall( let isError = executed.isError; if (config.afterToolCall) { - const afterResult = await config.afterToolCall( - { - assistantMessage, - toolCall: prepared.toolCall, - args: prepared.args, - result, - isError, - context: currentContext, - }, - signal, - ); - if (afterResult) { - result = { - content: afterResult.content ?? result.content, - details: afterResult.details ?? result.details, - }; - isError = afterResult.isError ?? isError; + try { + const afterResult = await config.afterToolCall( + { + assistantMessage, + toolCall: prepared.toolCall, + args: prepared.args, + result, + isError, + context: currentContext, + }, + signal, + ); + if (afterResult) { + result = { + content: afterResult.content ?? result.content, + details: afterResult.details ?? result.details, + }; + isError = afterResult.isError ?? isError; + } + } catch (error) { + result = createErrorToolResult(error instanceof Error ? error.message : String(error)); + isError = true; } } diff --git a/packages/agent/src/agent.ts b/packages/agent/src/agent.ts index b7d69546..1017239b 100644 --- a/packages/agent/src/agent.ts +++ b/packages/agent/src/agent.ts @@ -98,6 +98,7 @@ export interface AgentOptions { streamFn?: StreamFn; getApiKey?: (provider: string) => Promise | string | undefined; onPayload?: SimpleStreamOptions["onPayload"]; + onResponse?: SimpleStreamOptions["onResponse"]; beforeToolCall?: (context: BeforeToolCallContext, signal?: AbortSignal) => Promise; afterToolCall?: (context: AfterToolCallContext, signal?: AbortSignal) => Promise; steeringMode?: QueueMode; @@ -165,6 +166,7 @@ export class Agent { public streamFn: StreamFn; public getApiKey?: (provider: string) => Promise | string | undefined; public onPayload?: SimpleStreamOptions["onPayload"]; + public onResponse?: SimpleStreamOptions["onResponse"]; public beforeToolCall?: ( context: BeforeToolCallContext, signal?: AbortSignal, @@ -192,6 +194,7 @@ export class Agent { this.streamFn = options.streamFn ?? streamSimple; this.getApiKey = options.getApiKey; this.onPayload = options.onPayload; + this.onResponse = options.onResponse; this.beforeToolCall = options.beforeToolCall; this.afterToolCall = options.afterToolCall; this.steeringQueue = new PendingMessageQueue(options.steeringMode ?? "one-at-a-time"); @@ -411,6 +414,7 @@ export class Agent { reasoning: this._state.thinkingLevel === "off" ? undefined : this._state.thinkingLevel, sessionId: this.sessionId, onPayload: this.onPayload, + onResponse: this.onResponse, transport: this.transport, thinkingBudgets: this.thinkingBudgets, maxRetryDelayMs: this.maxRetryDelayMs, diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 186c607d..15128307 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -2,8 +2,28 @@ ## [Unreleased] +## [0.67.6] - 2026-04-16 + +### Added + +- Added `onResponse` to `StreamOptions` so callers can inspect provider HTTP status and headers after each response arrives and before the response stream is consumed ([#3128](https://github.com/badlogic/pi-mono/issues/3128)) +- Added `thinkingDisplay` (`"summarized" | "omitted"`) to `AnthropicOptions` and `BedrockOptions`, wiring it through to the Anthropic/Bedrock `thinking` config. Defaults to `"summarized"` so Claude Opus 4.7 and Mythos Preview keep returning thinking text; set it to `"omitted"` to skip thinking streaming for faster time-to-first-text-token. + +### Fixed + +- Fixed OpenAI Responses prompt caching for non-`api.openai.com` base URLs (OpenAI-compatible proxies such as litellm, theclawbay) by sending the `session_id` and `x-client-request-id` cache-affinity headers unconditionally when a `sessionId` is provided, matching the official Codex CLI behavior ([#3264](https://github.com/badlogic/pi-mono/pull/3264) by [@vegarsti](https://github.com/vegarsti)) + +## [0.67.5] - 2026-04-16 + +### Fixed + +- Fixed Opus 4.7 adaptive thinking configuration across Anthropic and Bedrock providers by recognizing Opus 4.7 adaptive-thinking support and mapping `xhigh` reasoning to provider-supported effort values ([#3286](https://github.com/badlogic/pi-mono/pull/3286) by [@markusylisiurunen](https://github.com/markusylisiurunen)) + +## [0.67.4] - 2026-04-16 + ### Changed +- Added `claude-opus-4-7` model for Anthropic, OpenRouter. - Changed Anthropic prompt caching to add a `cache_control` breakpoint on the last tool definition, so tool schemas can be cached independently from transcript updates while preserving existing cache retention behavior ([#3260](https://github.com/badlogic/pi-mono/issues/3260)) - Changed Kimi Coding model generation to normalize deprecated `k2p5` to `kimi-for-coding` from models.dev data and removed the old static fallback model list ([#3242](https://github.com/badlogic/pi-mono/issues/3242)) diff --git a/packages/ai/package.json b/packages/ai/package.json index 66877c85..7fddf4c3 100644 --- a/packages/ai/package.json +++ b/packages/ai/package.json @@ -1,6 +1,6 @@ { "name": "@mariozechner/pi-ai", - "version": "0.67.3", + "version": "0.67.6", "description": "Unified LLM API with automatic model discovery and provider configuration", "type": "module", "main": "./dist/index.js", @@ -72,8 +72,8 @@ "prepublishOnly": "npm run clean && npm run build" }, "dependencies": { - "@anthropic-ai/sdk": "^0.73.0", - "@aws-sdk/client-bedrock-runtime": "^3.983.0", + "@anthropic-ai/sdk": "^0.90.0", + "@aws-sdk/client-bedrock-runtime": "^3.1030.0", "@google/genai": "^1.40.0", "@mistralai/mistralai": "1.14.1", "@sinclair/typebox": "^0.34.41", diff --git a/packages/ai/scripts/generate-models.ts b/packages/ai/scripts/generate-models.ts index 902d798c..ea5e14a1 100644 --- a/packages/ai/scripts/generate-models.ts +++ b/packages/ai/scripts/generate-models.ts @@ -769,6 +769,27 @@ async function generateModels() { }); } + // Add missing Claude Opus 4.7 + if (!allModels.some(m => m.provider === "anthropic" && m.id === "claude-opus-4-7")) { + allModels.push({ + id: "claude-opus-4-7", + name: "Claude Opus 4.7", + api: "anthropic-messages", + baseUrl: "https://api.anthropic.com", + provider: "anthropic", + reasoning: true, + input: ["text", "image"], + cost: { + input: 5, + output: 25, + cacheRead: 0.5, + cacheWrite: 6.25, + }, + contextWindow: 1000000, + maxTokens: 128000, + }); + } + // Add missing Claude Sonnet 4.6 if (!allModels.some(m => m.provider === "anthropic" && m.id === "claude-sonnet-4-6")) { allModels.push({ diff --git a/packages/ai/src/index.ts b/packages/ai/src/index.ts index 1c885a85..6a1ee2c0 100644 --- a/packages/ai/src/index.ts +++ b/packages/ai/src/index.ts @@ -4,8 +4,8 @@ export { Type } from "@sinclair/typebox"; export * from "./api-registry.js"; export * from "./env-api-keys.js"; export * from "./models.js"; -export type { BedrockOptions } from "./providers/amazon-bedrock.js"; -export type { AnthropicOptions } from "./providers/anthropic.js"; +export type { BedrockOptions, BedrockThinkingDisplay } from "./providers/amazon-bedrock.js"; +export type { AnthropicEffort, AnthropicOptions, AnthropicThinkingDisplay } from "./providers/anthropic.js"; export type { AzureOpenAIResponsesOptions } from "./providers/azure-openai-responses.js"; export * from "./providers/faux.js"; export type { GoogleOptions } from "./providers/google.js"; diff --git a/packages/ai/src/models.generated.ts b/packages/ai/src/models.generated.ts index 757d4c68..15412260 100644 --- a/packages/ai/src/models.generated.ts +++ b/packages/ai/src/models.generated.ts @@ -260,6 +260,23 @@ export const MODELS = { contextWindow: 1000000, maxTokens: 128000, } satisfies Model<"bedrock-converse-stream">, + "anthropic.claude-opus-4-7": { + id: "anthropic.claude-opus-4-7", + name: "Claude Opus 4.7", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + input: ["text", "image"], + cost: { + input: 5, + output: 25, + cacheRead: 0.5, + cacheWrite: 6.25, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"bedrock-converse-stream">, "anthropic.claude-sonnet-4-20250514-v1:0": { id: "anthropic.claude-sonnet-4-20250514-v1:0", name: "Claude Sonnet 4", @@ -413,6 +430,23 @@ export const MODELS = { contextWindow: 1000000, maxTokens: 128000, } satisfies Model<"bedrock-converse-stream">, + "eu.anthropic.claude-opus-4-7": { + id: "eu.anthropic.claude-opus-4-7", + name: "Claude Opus 4.7 (EU)", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + input: ["text", "image"], + cost: { + input: 5, + output: 25, + cacheRead: 0.5, + cacheWrite: 6.25, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"bedrock-converse-stream">, "eu.anthropic.claude-sonnet-4-20250514-v1:0": { id: "eu.anthropic.claude-sonnet-4-20250514-v1:0", name: "Claude Sonnet 4 (EU)", @@ -515,6 +549,23 @@ export const MODELS = { contextWindow: 1000000, maxTokens: 128000, } satisfies Model<"bedrock-converse-stream">, + "global.anthropic.claude-opus-4-7": { + id: "global.anthropic.claude-opus-4-7", + name: "Claude Opus 4.7 (Global)", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + input: ["text", "image"], + cost: { + input: 5, + output: 25, + cacheRead: 0.5, + cacheWrite: 6.25, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"bedrock-converse-stream">, "global.anthropic.claude-sonnet-4-20250514-v1:0": { id: "global.anthropic.claude-sonnet-4-20250514-v1:0", name: "Claude Sonnet 4 (Global)", @@ -1348,6 +1399,23 @@ export const MODELS = { contextWindow: 1000000, maxTokens: 128000, } satisfies Model<"bedrock-converse-stream">, + "us.anthropic.claude-opus-4-7": { + id: "us.anthropic.claude-opus-4-7", + name: "Claude Opus 4.7 (US)", + api: "bedrock-converse-stream", + provider: "amazon-bedrock", + baseUrl: "https://bedrock-runtime.us-east-1.amazonaws.com", + reasoning: true, + input: ["text", "image"], + cost: { + input: 5, + output: 25, + cacheRead: 0.5, + cacheWrite: 6.25, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"bedrock-converse-stream">, "us.anthropic.claude-sonnet-4-20250514-v1:0": { id: "us.anthropic.claude-sonnet-4-20250514-v1:0", name: "Claude Sonnet 4 (US)", @@ -1775,6 +1843,23 @@ export const MODELS = { contextWindow: 1000000, maxTokens: 128000, } satisfies Model<"anthropic-messages">, + "claude-opus-4-7": { + id: "claude-opus-4-7", + name: "Claude Opus 4.7", + api: "anthropic-messages", + provider: "anthropic", + baseUrl: "https://api.anthropic.com", + reasoning: true, + input: ["text", "image"], + cost: { + input: 5, + output: 25, + cacheRead: 0.5, + cacheWrite: 6.25, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"anthropic-messages">, "claude-sonnet-4-0": { id: "claude-sonnet-4-0", name: "Claude Sonnet 4 (latest)", @@ -6193,6 +6278,23 @@ export const MODELS = { contextWindow: 1000000, maxTokens: 128000, } satisfies Model<"anthropic-messages">, + "claude-opus-4-7": { + id: "claude-opus-4-7", + name: "Claude Opus 4.7", + api: "anthropic-messages", + provider: "opencode", + baseUrl: "https://opencode.ai/zen", + reasoning: true, + input: ["text", "image"], + cost: { + input: 5, + output: 25, + cacheRead: 0.5, + cacheWrite: 6.25, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"anthropic-messages">, "claude-sonnet-4": { id: "claude-sonnet-4", name: "Claude Sonnet 4", @@ -7115,6 +7217,23 @@ export const MODELS = { contextWindow: 1000000, maxTokens: 128000, } satisfies Model<"openai-completions">, + "anthropic/claude-opus-4.7": { + id: "anthropic/claude-opus-4.7", + name: "Anthropic: Claude Opus 4.7", + api: "openai-completions", + provider: "openrouter", + baseUrl: "https://openrouter.ai/api/v1", + reasoning: true, + input: ["text", "image"], + cost: { + input: 5, + output: 25, + cacheRead: 0.5, + cacheWrite: 6.25, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"openai-completions">, "anthropic/claude-sonnet-4": { id: "anthropic/claude-sonnet-4", name: "Anthropic: Claude Sonnet 4", @@ -7753,13 +7872,13 @@ export const MODELS = { reasoning: true, input: ["text", "image"], cost: { - input: 0.08, - output: 0.35, - cacheRead: 0.01, + input: 0.07, + output: 0.39999999999999997, + cacheRead: 0.04, cacheWrite: 0, }, contextWindow: 262144, - maxTokens: 4096, + maxTokens: 262144, } satisfies Model<"openai-completions">, "google/gemma-4-26b-a4b-it:free": { id: "google/gemma-4-26b-a4b-it:free", @@ -8456,7 +8575,7 @@ export const MODELS = { cacheWrite: 0, }, contextWindow: 131072, - maxTokens: 131072, + maxTokens: 32768, } satisfies Model<"openai-completions">, "moonshotai/kimi-k2-0905": { id: "moonshotai/kimi-k2-0905", @@ -9779,7 +9898,7 @@ export const MODELS = { input: 0.26, output: 0.78, cacheRead: 0.052000000000000005, - cacheWrite: 0, + cacheWrite: 0.325, }, contextWindow: 1000000, maxTokens: 32768, @@ -9796,7 +9915,7 @@ export const MODELS = { input: 0.26, output: 0.78, cacheRead: 0, - cacheWrite: 0, + cacheWrite: 0.325, }, contextWindow: 1000000, maxTokens: 32768, @@ -9813,7 +9932,7 @@ export const MODELS = { input: 0.26, output: 0.78, cacheRead: 0, - cacheWrite: 0, + cacheWrite: 0.325, }, contextWindow: 1000000, maxTokens: 32768, @@ -9912,13 +10031,13 @@ export const MODELS = { reasoning: true, input: ["text"], cost: { - input: 0.14950000000000002, - output: 1.495, + input: 0.13, + output: 0.6, cacheRead: 0, cacheWrite: 0, }, - contextWindow: 131072, - maxTokens: 4096, + contextWindow: 262144, + maxTokens: 262144, } satisfies Model<"openai-completions">, "qwen/qwen3-30b-a3b": { id: "qwen/qwen3-30b-a3b", @@ -10051,7 +10170,7 @@ export const MODELS = { input: 0.195, output: 0.975, cacheRead: 0.039, - cacheWrite: 0, + cacheWrite: 0.24375, }, contextWindow: 1000000, maxTokens: 65536, @@ -10085,7 +10204,7 @@ export const MODELS = { input: 0.65, output: 3.25, cacheRead: 0.13, - cacheWrite: 0, + cacheWrite: 0.8125, }, contextWindow: 1000000, maxTokens: 65536, @@ -10119,7 +10238,7 @@ export const MODELS = { input: 0.78, output: 3.9, cacheRead: 0.156, - cacheWrite: 0, + cacheWrite: 0.975, }, contextWindow: 262144, maxTokens: 32768, @@ -10373,7 +10492,7 @@ export const MODELS = { cost: { input: 0.39, output: 2.34, - cacheRead: 0, + cacheRead: 0.195, cacheWrite: 0, }, contextWindow: 262144, @@ -10408,7 +10527,7 @@ export const MODELS = { input: 0.065, output: 0.26, cacheRead: 0, - cacheWrite: 0, + cacheWrite: 0.08125, }, contextWindow: 1000000, maxTokens: 65536, @@ -10425,7 +10544,7 @@ export const MODELS = { input: 0.26, output: 1.56, cacheRead: 0, - cacheWrite: 0, + cacheWrite: 0.325, }, contextWindow: 1000000, maxTokens: 65536, @@ -10442,7 +10561,7 @@ export const MODELS = { input: 0.325, output: 1.95, cacheRead: 0, - cacheWrite: 0, + cacheWrite: 0.40625, }, contextWindow: 1000000, maxTokens: 65536, @@ -11452,6 +11571,23 @@ export const MODELS = { contextWindow: 1000000, maxTokens: 128000, } satisfies Model<"anthropic-messages">, + "anthropic/claude-opus-4.7": { + id: "anthropic/claude-opus-4.7", + name: "Claude Opus 4.7", + api: "anthropic-messages", + provider: "vercel-ai-gateway", + baseUrl: "https://ai-gateway.vercel.sh", + reasoning: true, + input: ["text", "image"], + cost: { + input: 5, + output: 25, + cacheRead: 0.5, + cacheWrite: 6.25, + }, + contextWindow: 1000000, + maxTokens: 128000, + } satisfies Model<"anthropic-messages">, "anthropic/claude-sonnet-4": { id: "anthropic/claude-sonnet-4", name: "Claude Sonnet 4", @@ -13618,15 +13754,15 @@ export const MODELS = { provider: "vercel-ai-gateway", baseUrl: "https://ai-gateway.vercel.sh", reasoning: true, - input: ["text"], + input: ["text", "image"], cost: { input: 1.4, output: 4.4, cacheRead: 0.26, cacheWrite: 0, }, - contextWindow: 202800, - maxTokens: 64000, + contextWindow: 202752, + maxTokens: 202752, } satisfies Model<"anthropic-messages">, "zai/glm-5v-turbo": { id: "zai/glm-5v-turbo", diff --git a/packages/ai/src/models.ts b/packages/ai/src/models.ts index 101c83f2..8944d07e 100644 --- a/packages/ai/src/models.ts +++ b/packages/ai/src/models.ts @@ -50,14 +50,19 @@ export function calculateCost(model: Model, usage: Usage * * Supported today: * - GPT-5.2 / GPT-5.3 / GPT-5.4 model families - * - Opus 4.6 models (xhigh maps to adaptive effort "max" on Anthropic-compatible providers) + * - Opus 4.6+ models (xhigh maps to adaptive effort "max" on Anthropic-compatible providers) */ export function supportsXhigh(model: Model): boolean { if (model.id.includes("gpt-5.2") || model.id.includes("gpt-5.3") || model.id.includes("gpt-5.4")) { return true; } - if (model.id.includes("opus-4-6") || model.id.includes("opus-4.6")) { + if ( + model.id.includes("opus-4-6") || + model.id.includes("opus-4.6") || + model.id.includes("opus-4-7") || + model.id.includes("opus-4.7") + ) { return true; } diff --git a/packages/ai/src/providers/amazon-bedrock.ts b/packages/ai/src/providers/amazon-bedrock.ts index a0aa55c3..79c98038 100644 --- a/packages/ai/src/providers/amazon-bedrock.ts +++ b/packages/ai/src/providers/amazon-bedrock.ts @@ -15,11 +15,14 @@ import { type ConverseStreamMetadataEvent, ImageFormat, type Message, + type ServiceInputTypes, + type ServiceOutputTypes, type SystemContentBlock, type ToolChoice, type ToolConfiguration, ToolResultStatus, } from "@aws-sdk/client-bedrock-runtime"; +import type { FinalizeRequestMiddleware } from "@smithy/types"; import { calculateCost } from "../models.js"; import type { @@ -46,6 +49,8 @@ import { sanitizeSurrogates } from "../utils/sanitize-unicode.js"; import { adjustMaxTokensForThinking, buildBaseOptions, clampReasoning } from "./simple-options.js"; import { transformMessages } from "./transform-messages.js"; +export type BedrockThinkingDisplay = "summarized" | "omitted"; + export interface BedrockOptions extends StreamOptions { region?: string; profile?: string; @@ -56,11 +61,28 @@ export interface BedrockOptions extends StreamOptions { thinkingBudgets?: ThinkingBudgets; /* Only supported by Claude 4.x models, see https://docs.aws.amazon.com/bedrock/latest/userguide/claude-messages-extended-thinking.html#claude-messages-extended-thinking-tool-use-interleaved */ interleavedThinking?: boolean; + /** + * Controls how Claude's thinking content is returned in responses. + * - "summarized": Thinking blocks contain summarized thinking text (default here). + * - "omitted": Thinking content is redacted but the signature still travels back + * for multi-turn continuity, reducing time-to-first-text-token. + * + * Note: Anthropic's API default for Claude Opus 4.7 and Mythos Preview is + * "omitted". We default to "summarized" here to keep behavior consistent with + * older Claude 4 models. Only applies to Claude models on Bedrock. + */ + thinkingDisplay?: BedrockThinkingDisplay; /** Key-value pairs attached to the inference request for cost allocation tagging. * Keys: max 64 chars, no `aws:` prefix. Values: max 256 chars. Max 50 pairs. * Tags appear in AWS Cost Explorer split cost allocation data. * @see https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_ConverseStream.html */ requestMetadata?: Record; + /** Bearer token for Bedrock API key authentication. + * When set, bypasses SigV4 signing and sends Authorization: Bearer instead. + * Requires `bedrock:CallWithBearerToken` IAM permission on the token's identity. + * Set via AWS_BEARER_TOKEN_BEDROCK env var or pass directly. + * @see https://docs.aws.amazon.com/service-authorization/latest/reference/list_amazonbedrock.html */ + bearerToken?: string; } type Block = (TextContent | ThinkingContent | ToolCall) & { index?: number; partialJson?: string }; @@ -97,6 +119,9 @@ export const streamBedrock: StreamFunction<"bedrock-converse-stream", BedrockOpt profile: options.profile, }; + // Resolve bearer token for API key auth (bypasses SigV4) + const bearerToken = options.bearerToken || process.env.AWS_BEARER_TOKEN_BEDROCK || undefined; + // in Node.js/Bun environment only if (typeof process !== "undefined" && (process.versions?.node || process.versions?.bun)) { // Region resolution: explicit option > env vars > SDK default chain. @@ -117,6 +142,15 @@ export const streamBedrock: StreamFunction<"bedrock-converse-stream", BedrockOpt }; } + // Bearer token auth: use API key instead of SigV4 signing. + // Requires bedrock:CallWithBearerToken IAM permission. + if (bearerToken && process.env.AWS_BEDROCK_SKIP_AUTH !== "1") { + config.credentials = { + accessKeyId: "bearer-token-auth", + secretAccessKey: "bearer-token-auth", + }; + } + if ( process.env.HTTP_PROXY || process.env.HTTPS_PROXY || @@ -151,6 +185,34 @@ export const streamBedrock: StreamFunction<"bedrock-converse-stream", BedrockOpt try { const client = new BedrockRuntimeClient(config); + // Inject bearer token middleware after SigV4 signing + if (bearerToken) { + const bearerTokenAuthMiddleware: FinalizeRequestMiddleware = + (next) => async (args) => { + const request = args.request; + if ( + typeof request === "object" && + request !== null && + "headers" in request && + typeof request.headers === "object" && + request.headers !== null + ) { + const headers = request.headers as Record; + headers.authorization = `Bearer ${bearerToken}`; + delete headers["x-amz-date"]; + delete headers["x-amz-security-token"]; + delete headers["x-amz-content-sha256"]; + } + return next(args); + }; + + client.middlewareStack.addRelativeTo(bearerTokenAuthMiddleware, { + relation: "after", + toMiddleware: "awsAuthMiddleware", + name: "bearerTokenAuth", + }); + } + const cacheRetention = resolveCacheRetention(options.cacheRetention); let commandInput = { modelId: model.id, @@ -168,6 +230,13 @@ export const streamBedrock: StreamFunction<"bedrock-converse-stream", BedrockOpt const command = new ConverseStreamCommand(commandInput); const response = await client.send(command, { abortSignal: options.signal }); + if (response.$metadata.httpStatusCode !== undefined) { + const responseHeaders: Record = {}; + if (response.$metadata.requestId) { + responseHeaders["x-amzn-requestid"] = response.$metadata.requestId; + } + await options?.onResponse?.({ status: response.$metadata.httpStatusCode, headers: responseHeaders }, model); + } for await (const item of response.stream!) { if (item.messageStart) { @@ -423,12 +492,14 @@ function handleContentBlockStop( } /** - * Check if the model supports adaptive thinking (Opus 4.6 and Sonnet 4.6). + * Check if the model supports adaptive thinking (Opus 4.6+, Sonnet 4.6). */ function supportsAdaptiveThinking(modelId: string): boolean { return ( modelId.includes("opus-4-6") || modelId.includes("opus-4.6") || + modelId.includes("opus-4-7") || + modelId.includes("opus-4.7") || modelId.includes("sonnet-4-6") || modelId.includes("sonnet-4.6") ); @@ -437,7 +508,7 @@ function supportsAdaptiveThinking(modelId: string): boolean { function mapThinkingLevelToEffort( level: SimpleStreamOptions["reasoning"], modelId: string, -): "low" | "medium" | "high" | "max" { +): "low" | "medium" | "high" | "xhigh" | "max" { switch (level) { case "minimal": case "low": @@ -447,7 +518,13 @@ function mapThinkingLevelToEffort( case "high": return "high"; case "xhigh": - return modelId.includes("opus-4-6") || modelId.includes("opus-4.6") ? "max" : "high"; + if (modelId.includes("opus-4-6") || modelId.includes("opus-4.6")) { + return "max"; + } + if (modelId.includes("opus-4-7") || modelId.includes("opus-4.7")) { + return "xhigh"; + } + return "high"; default: return "high"; } @@ -744,9 +821,12 @@ function buildAdditionalModelRequestFields( } if (model.id.includes("anthropic.claude") || model.id.includes("anthropic/claude")) { + // Default to "summarized" so Opus 4.7 and Mythos Preview behave like + // older Claude 4 models (whose API default is also "summarized"). + const display: BedrockThinkingDisplay = options.thinkingDisplay ?? "summarized"; const result: Record = supportsAdaptiveThinking(model.id) ? { - thinking: { type: "adaptive" }, + thinking: { type: "adaptive", display }, output_config: { effort: mapThinkingLevelToEffort(options.reasoning, model.id) }, } : (() => { @@ -766,6 +846,7 @@ function buildAdditionalModelRequestFields( thinking: { type: "enabled", budget_tokens: budget, + display, }, }; })(); diff --git a/packages/ai/src/providers/anthropic.ts b/packages/ai/src/providers/anthropic.ts index 53d29e45..47686327 100644 --- a/packages/ai/src/providers/anthropic.ts +++ b/packages/ai/src/providers/anthropic.ts @@ -26,6 +26,7 @@ import type { ToolResultMessage, } from "../types.js"; import { AssistantMessageEventStream } from "../utils/event-stream.js"; +import { headersToRecord } from "../utils/headers.js"; import { parseStreamingJson } from "../utils/json-parse.js"; import { sanitizeSurrogates } from "../utils/sanitize-unicode.js"; @@ -153,7 +154,9 @@ function convertContentBlocks(content: (TextContent | ImageContent)[]): return blocks; } -export type AnthropicEffort = "low" | "medium" | "high" | "max"; +export type AnthropicEffort = "low" | "medium" | "high" | "xhigh" | "max"; + +export type AnthropicThinkingDisplay = "summarized" | "omitted"; export interface AnthropicOptions extends StreamOptions { /** @@ -168,15 +171,28 @@ export interface AnthropicOptions extends StreamOptions { */ thinkingBudgetTokens?: number; /** - * Effort level for adaptive thinking (Opus 4.6 and Sonnet 4.6). + * Effort level for adaptive thinking (Opus 4.6+ and Sonnet 4.6). * Controls how much thinking Claude allocates: * - "max": Always thinks with no constraints (Opus 4.6 only) + * - "xhigh": Highest reasoning level (Opus 4.7) * - "high": Always thinks, deep reasoning (default) * - "medium": Moderate thinking, may skip for simple queries * - "low": Minimal thinking, skips for simple tasks * Ignored for older models. */ effort?: AnthropicEffort; + /** + * Controls how thinking content is returned in API responses. + * - "summarized": Thinking blocks contain summarized thinking text (default here). + * - "omitted": Thinking blocks return an empty thinking field; the encrypted + * signature still travels back for multi-turn continuity. Use for faster + * time-to-first-text-token when your UI does not surface thinking. + * + * Note: Anthropic's API default for Claude Opus 4.7 and Claude Mythos Preview + * is "omitted". We default to "summarized" here to keep behavior consistent + * with older Claude 4 models. Set this explicitly to "omitted" to opt in. + */ + thinkingDisplay?: AnthropicThinkingDisplay; interleavedThinking?: boolean; toolChoice?: "auto" | "any" | "none" | { type: "tool"; name: string }; /** @@ -257,7 +273,10 @@ export const streamAnthropic: StreamFunction<"anthropic-messages", AnthropicOpti if (nextParams !== undefined) { params = nextParams as MessageCreateParamsStreaming; } - const anthropicStream = client.messages.stream({ ...params, stream: true }, { signal: options?.signal }); + const { data: anthropicStream, response } = await client.messages + .stream({ ...params, stream: true }, { signal: options?.signal }) + .withResponse(); + await options?.onResponse?.({ status: response.status, headers: headersToRecord(response.headers) }, model); stream.push({ type: "start", partial: output }); type Block = (ThinkingContent | TextContent | (ToolCall & { partialJson: string })) & { index: number }; @@ -448,13 +467,15 @@ export const streamAnthropic: StreamFunction<"anthropic-messages", AnthropicOpti }; /** - * Check if a model supports adaptive thinking (Opus 4.6 and Sonnet 4.6) + * Check if a model supports adaptive thinking (Opus 4.6+, Sonnet 4.6) */ function supportsAdaptiveThinking(modelId: string): boolean { - // Opus 4.6 and Sonnet 4.6 model IDs (with or without date suffix) + // Adaptive-thinking model IDs (with or without date suffix) return ( modelId.includes("opus-4-6") || modelId.includes("opus-4.6") || + modelId.includes("opus-4-7") || + modelId.includes("opus-4.7") || modelId.includes("sonnet-4-6") || modelId.includes("sonnet-4.6") ); @@ -462,7 +483,7 @@ function supportsAdaptiveThinking(modelId: string): boolean { /** * Map ThinkingLevel to Anthropic effort levels for adaptive thinking. - * Note: effort "max" is only valid on Opus 4.6. + * Note: effort "max" is only valid on Opus 4.6, while Opus 4.7 supports "xhigh". */ function mapThinkingLevelToEffort(level: SimpleStreamOptions["reasoning"], modelId: string): AnthropicEffort { switch (level) { @@ -475,7 +496,13 @@ function mapThinkingLevelToEffort(level: SimpleStreamOptions["reasoning"], model case "high": return "high"; case "xhigh": - return modelId.includes("opus-4-6") || modelId.includes("opus-4.6") ? "max" : "high"; + if (modelId.includes("opus-4-6") || modelId.includes("opus-4.6")) { + return "max"; + } + if (modelId.includes("opus-4-7") || modelId.includes("opus-4.7")) { + return "xhigh"; + } + return "high"; default: return "high"; } @@ -661,21 +688,31 @@ function buildParams( params.tools = convertTools(context.tools, isOAuthToken, cacheControl); } - // Configure thinking mode: adaptive (Opus 4.6 and Sonnet 4.6), + // Configure thinking mode: adaptive (Opus 4.6+ and Sonnet 4.6), // budget-based (older models), or explicitly disabled. if (model.reasoning) { if (options?.thinkingEnabled) { + // Default to "summarized" so Opus 4.7 and Mythos Preview behave like + // older Claude 4 models (whose API default is also "summarized"). + const display: AnthropicThinkingDisplay = options.thinkingDisplay ?? "summarized"; if (supportsAdaptiveThinking(model.id)) { - // Adaptive thinking: Claude decides when and how much to think - params.thinking = { type: "adaptive" }; + // Adaptive thinking: Claude decides when and how much to think. + params.thinking = { type: "adaptive", display }; if (options.effort) { - params.output_config = { effort: options.effort }; + // The Anthropic SDK types can lag newly supported effort values such as "xhigh". + params.output_config = + options.effort === "xhigh" + ? ({ effort: options.effort } as unknown as NonNullable< + MessageCreateParamsStreaming["output_config"] + >) + : { effort: options.effort }; } } else { // Budget-based thinking for older models params.thinking = { type: "enabled", budget_tokens: options.thinkingBudgetTokens || 1024, + display, }; } } else if (options?.thinkingEnabled === false) { diff --git a/packages/ai/src/providers/azure-openai-responses.ts b/packages/ai/src/providers/azure-openai-responses.ts index 9dc5261e..b9981935 100644 --- a/packages/ai/src/providers/azure-openai-responses.ts +++ b/packages/ai/src/providers/azure-openai-responses.ts @@ -12,6 +12,7 @@ import type { StreamOptions, } from "../types.js"; import { AssistantMessageEventStream } from "../utils/event-stream.js"; +import { headersToRecord } from "../utils/headers.js"; import { convertResponsesMessages, convertResponsesTools, processResponsesStream } from "./openai-responses-shared.js"; import { buildBaseOptions, clampReasoning } from "./simple-options.js"; @@ -90,10 +91,10 @@ export const streamAzureOpenAIResponses: StreamFunction<"azure-openai-responses" if (nextParams !== undefined) { params = nextParams as ResponseCreateParamsStreaming; } - const openaiStream = await client.responses.create( - params, - options?.signal ? { signal: options.signal } : undefined, - ); + const { data: openaiStream, response } = await client.responses + .create(params, options?.signal ? { signal: options.signal } : undefined) + .withResponse(); + await options?.onResponse?.({ status: response.status, headers: headersToRecord(response.headers) }, model); stream.push({ type: "start", partial: output }); await processResponsesStream(openaiStream, output, stream, model); diff --git a/packages/ai/src/providers/faux.ts b/packages/ai/src/providers/faux.ts index 328fd87f..7bba1b72 100644 --- a/packages/ai/src/providers/faux.ts +++ b/packages/ai/src/providers/faux.ts @@ -435,6 +435,7 @@ export function registerFauxProvider(options: RegisterFauxProviderOptions = {}): queueMicrotask(async () => { try { + await streamOptions?.onResponse?.({ status: 200, headers: {} }, requestModel); if (!step) { let message = createErrorMessage( new Error("No more faux responses queued"), diff --git a/packages/ai/src/providers/google-gemini-cli.ts b/packages/ai/src/providers/google-gemini-cli.ts index 259ca069..fa6f98f1 100644 --- a/packages/ai/src/providers/google-gemini-cli.ts +++ b/packages/ai/src/providers/google-gemini-cli.ts @@ -21,6 +21,7 @@ import type { ToolCall, } from "../types.js"; import { AssistantMessageEventStream } from "../utils/event-stream.js"; +import { headersToRecord } from "../utils/headers.js"; import { sanitizeSurrogates } from "../utils/sanitize-unicode.js"; import { convertMessages, @@ -408,6 +409,10 @@ export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli", GoogleGe body: requestBodyJson, signal: options?.signal, }); + await options?.onResponse?.( + { status: response.status, headers: headersToRecord(response.headers) }, + model, + ); if (response.ok) { break; // Success, exit retry loop @@ -757,6 +762,10 @@ export const streamGoogleGeminiCli: StreamFunction<"google-gemini-cli", GoogleGe body: requestBodyJson, signal: options?.signal, }); + await options?.onResponse?.( + { status: currentResponse.status, headers: headersToRecord(currentResponse.headers) }, + model, + ); if (!currentResponse.ok) { const retryErrorText = await currentResponse.text(); diff --git a/packages/ai/src/providers/openai-codex-responses.ts b/packages/ai/src/providers/openai-codex-responses.ts index 4cca39b7..248443ec 100644 --- a/packages/ai/src/providers/openai-codex-responses.ts +++ b/packages/ai/src/providers/openai-codex-responses.ts @@ -33,6 +33,7 @@ import type { Usage, } from "../types.js"; import { AssistantMessageEventStream } from "../utils/event-stream.js"; +import { headersToRecord } from "../utils/headers.js"; import { convertResponsesMessages, convertResponsesTools, processResponsesStream } from "./openai-responses-shared.js"; import { buildBaseOptions, clampReasoning } from "./simple-options.js"; @@ -214,6 +215,10 @@ export const streamOpenAICodexResponses: StreamFunction<"openai-codex-responses" body: bodyJson, signal: options?.signal, }); + await options?.onResponse?.( + { status: response.status, headers: headersToRecord(response.headers) }, + model, + ); if (response.ok) { break; @@ -529,14 +534,6 @@ function getWebSocketConstructor(): WebSocketConstructor | null { return ctor as unknown as WebSocketConstructor; } -function headersToRecord(headers: Headers): Record { - const out: Record = {}; - for (const [key, value] of headers.entries()) { - out[key] = value; - } - return out; -} - function getWebSocketReadyState(socket: WebSocketLike): number | undefined { const readyState = (socket as { readyState?: unknown }).readyState; return typeof readyState === "number" ? readyState : undefined; diff --git a/packages/ai/src/providers/openai-completions.ts b/packages/ai/src/providers/openai-completions.ts index 6656540f..e2ab39b1 100644 --- a/packages/ai/src/providers/openai-completions.ts +++ b/packages/ai/src/providers/openai-completions.ts @@ -27,6 +27,7 @@ import type { ToolResultMessage, } from "../types.js"; import { AssistantMessageEventStream } from "../utils/event-stream.js"; +import { headersToRecord } from "../utils/headers.js"; import { parseStreamingJson } from "../utils/json-parse.js"; import { sanitizeSurrogates } from "../utils/sanitize-unicode.js"; import { buildCopilotDynamicHeaders, hasCopilotVisionInput } from "./github-copilot-headers.js"; @@ -91,7 +92,10 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions", OpenA if (nextParams !== undefined) { params = nextParams as OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming; } - const openaiStream = await client.chat.completions.create(params, { signal: options?.signal }); + const { data: openaiStream, response } = await client.chat.completions + .create(params, { signal: options?.signal }) + .withResponse(); + await options?.onResponse?.({ status: response.status, headers: headersToRecord(response.headers) }, model); stream.push({ type: "start", partial: output }); let currentBlock: TextContent | ThinkingContent | (ToolCall & { partialArgs?: string }) | null = null; diff --git a/packages/ai/src/providers/openai-responses.ts b/packages/ai/src/providers/openai-responses.ts index 35bfeffe..b8a991b6 100644 --- a/packages/ai/src/providers/openai-responses.ts +++ b/packages/ai/src/providers/openai-responses.ts @@ -14,6 +14,7 @@ import type { Usage, } from "../types.js"; import { AssistantMessageEventStream } from "../utils/event-stream.js"; +import { headersToRecord } from "../utils/headers.js"; import { buildCopilotDynamicHeaders, hasCopilotVisionInput } from "./github-copilot-headers.js"; import { convertResponsesMessages, convertResponsesTools, processResponsesStream } from "./openai-responses-shared.js"; import { buildBaseOptions, clampReasoning } from "./simple-options.js"; @@ -96,10 +97,10 @@ export const streamOpenAIResponses: StreamFunction<"openai-responses", OpenAIRes if (nextParams !== undefined) { params = nextParams as ResponseCreateParamsStreaming; } - const openaiStream = await client.responses.create( - params, - options?.signal ? { signal: options.signal } : undefined, - ); + const { data: openaiStream, response } = await client.responses + .create(params, options?.signal ? { signal: options.signal } : undefined) + .withResponse(); + await options?.onResponse?.({ status: response.status, headers: headersToRecord(response.headers) }, model); stream.push({ type: "start", partial: output }); await processResponsesStream(openaiStream, output, stream, model, { @@ -178,7 +179,7 @@ function createClient( Object.assign(headers, copilotHeaders); } - if (sessionId && model.provider === "openai" && model.baseUrl.includes("api.openai.com")) { + if (sessionId) { headers.session_id = sessionId; headers["x-client-request-id"] = sessionId; } diff --git a/packages/ai/src/providers/simple-options.ts b/packages/ai/src/providers/simple-options.ts index 71c15847..6e478e64 100644 --- a/packages/ai/src/providers/simple-options.ts +++ b/packages/ai/src/providers/simple-options.ts @@ -10,6 +10,7 @@ export function buildBaseOptions(model: Model, options?: SimpleStreamOption sessionId: options?.sessionId, headers: options?.headers, onPayload: options?.onPayload, + onResponse: options?.onResponse, maxRetryDelayMs: options?.maxRetryDelayMs, metadata: options?.metadata, }; diff --git a/packages/ai/src/types.ts b/packages/ai/src/types.ts index 3c2a0d1a..79281c6c 100644 --- a/packages/ai/src/types.ts +++ b/packages/ai/src/types.ts @@ -57,6 +57,11 @@ export type CacheRetention = "none" | "short" | "long"; export type Transport = "sse" | "websocket" | "auto"; +export interface ProviderResponse { + status: number; + headers: Record; +} + export interface StreamOptions { temperature?: number; maxTokens?: number; @@ -83,6 +88,11 @@ export interface StreamOptions { * Return undefined to keep the payload unchanged. */ onPayload?: (payload: unknown, model: Model) => unknown | undefined | Promise; + /** + * Optional callback invoked after an HTTP response is received and before + * its body stream is consumed. + */ + onResponse?: (response: ProviderResponse, model: Model) => void | Promise; /** * Optional custom HTTP headers to include in API requests. * Merged with provider defaults; can override default headers. diff --git a/packages/ai/src/utils/headers.ts b/packages/ai/src/utils/headers.ts new file mode 100644 index 00000000..fae8a036 --- /dev/null +++ b/packages/ai/src/utils/headers.ts @@ -0,0 +1,7 @@ +export function headersToRecord(headers: Headers): Record { + const result: Record = {}; + for (const [key, value] of headers.entries()) { + result[key] = value; + } + return result; +} diff --git a/packages/ai/test/anthropic-opus-4-7-smoke.test.ts b/packages/ai/test/anthropic-opus-4-7-smoke.test.ts new file mode 100644 index 00000000..29626d1f --- /dev/null +++ b/packages/ai/test/anthropic-opus-4-7-smoke.test.ts @@ -0,0 +1,72 @@ +import { describe, expect, it } from "vitest"; +import { getModel } from "../src/models.js"; +import { streamSimple } from "../src/stream.js"; +import type { Context } from "../src/types.js"; + +interface AnthropicThinkingPayload { + thinking?: { type: string }; + output_config?: { effort?: string }; +} + +function makeContext(): Context { + return { + systemPrompt: "You are a precise assistant. Follow the user's instructions exactly.", + messages: [ + { + role: "user", + content: + "Compute 48291 * 7317 and 90844 - 17729, add the results, and determine whether the sum is divisible by 11. Reply with exactly this format and nothing else: sum=; divisibleBy11=", + timestamp: Date.now(), + }, + ], + }; +} + +describe.skipIf(!process.env.ANTHROPIC_API_KEY)("Anthropic Opus 4.7 smoke", () => { + it("streams Claude Opus 4.7 with reasoning enabled", { retry: 2, timeout: 30000 }, async () => { + const model = getModel("anthropic", "claude-opus-4-7"); + let capturedPayload: AnthropicThinkingPayload | undefined; + const s = streamSimple(model, makeContext(), { + reasoning: "high", + maxTokens: 1024, + onPayload: (payload) => { + capturedPayload = payload as AnthropicThinkingPayload; + return payload; + }, + }); + + let sawThinking = false; + + for await (const event of s) { + if (event.type === "thinking_start" || event.type === "thinking_delta" || event.type === "thinking_end") { + sawThinking = true; + } + } + + const response = await s.result(); + expect(response.stopReason, response.errorMessage).toBe("stop"); + expect(response.errorMessage).toBeFalsy(); + expect(capturedPayload?.thinking).toEqual({ type: "adaptive" }); + expect(capturedPayload?.output_config).toEqual({ effort: "high" }); + expect(sawThinking).toBe(true); + + const thinkingBlock = response.content.find((block) => block.type === "thinking"); + expect(thinkingBlock?.type).toBe("thinking"); + if (!thinkingBlock || thinkingBlock.type !== "thinking") { + throw new Error("Expected thinking block from Claude Opus 4.7"); + } + expect(typeof thinkingBlock.thinkingSignature).toBe("string"); + const thinkingSignature = thinkingBlock.thinkingSignature; + if (!thinkingSignature) { + throw new Error("Expected thinking signature from Claude Opus 4.7"); + } + expect(thinkingSignature.length).toBeGreaterThan(0); + + const text = response.content + .filter((block) => block.type === "text") + .map((block) => block.text) + .join("") + .trim(); + expect(text).toBe("sum=353418362; divisibleBy11=yes"); + }); +}); diff --git a/packages/ai/test/anthropic-thinking-disable.test.ts b/packages/ai/test/anthropic-thinking-disable.test.ts index 8b154d48..7f406b30 100644 --- a/packages/ai/test/anthropic-thinking-disable.test.ts +++ b/packages/ai/test/anthropic-thinking-disable.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from "vitest"; import { getModel } from "../src/models.js"; import { streamSimple } from "../src/stream.js"; -import type { Context, Model } from "../src/types.js"; +import type { Context, Model, SimpleStreamOptions } from "../src/types.js"; interface AnthropicThinkingPayload { thinking?: { type: string; budget_tokens?: number }; @@ -14,7 +14,10 @@ function makePayloadCaptureContext(): Context { }; } -async function capturePayload(model: Model<"anthropic-messages">): Promise { +async function capturePayload( + model: Model<"anthropic-messages">, + options?: SimpleStreamOptions, +): Promise { let capturedPayload: AnthropicThinkingPayload | undefined; const payloadCaptureModel: Model<"anthropic-messages"> = { ...model, @@ -22,6 +25,7 @@ async function capturePayload(model: Model<"anthropic-messages">): Promise { capturedPayload = payload as AnthropicThinkingPayload; @@ -113,6 +117,27 @@ describe("Anthropic thinking disable payload", () => { expect(payload.thinking).toEqual({ type: "disabled" }); expect(payload.output_config).toBeUndefined(); }); + + it("sends thinking.type=disabled for Claude Opus 4.7 when thinking is off", async () => { + const payload = await capturePayload(getModel("anthropic", "claude-opus-4-7")); + + expect(payload.thinking).toEqual({ type: "disabled" }); + expect(payload.output_config).toBeUndefined(); + }); + + it("uses adaptive thinking for Claude Opus 4.7 when reasoning is enabled", async () => { + const payload = await capturePayload(getModel("anthropic", "claude-opus-4-7"), { reasoning: "high" }); + + expect(payload.thinking).toEqual({ type: "adaptive" }); + expect(payload.output_config).toEqual({ effort: "high" }); + }); + + it("maps xhigh reasoning to effort=xhigh for Claude Opus 4.7", async () => { + const payload = await capturePayload(getModel("anthropic", "claude-opus-4-7"), { reasoning: "xhigh" }); + + expect(payload.thinking).toEqual({ type: "adaptive" }); + expect(payload.output_config).toEqual({ effort: "xhigh" }); + }); }); describe.skipIf(!process.env.ANTHROPIC_API_KEY)("Anthropic thinking disable E2E", () => { diff --git a/packages/ai/test/bedrock-thinking-payload.test.ts b/packages/ai/test/bedrock-thinking-payload.test.ts new file mode 100644 index 00000000..b9f7c86f --- /dev/null +++ b/packages/ai/test/bedrock-thinking-payload.test.ts @@ -0,0 +1,78 @@ +import { describe, expect, it } from "vitest"; +import { getModel } from "../src/models.js"; +import { streamBedrock } from "../src/providers/amazon-bedrock.js"; +import type { Context, Model, SimpleStreamOptions } from "../src/types.js"; + +interface BedrockThinkingPayload { + additionalModelRequestFields?: { + thinking?: { type: string; budget_tokens?: number }; + output_config?: { effort?: string }; + anthropic_beta?: string[]; + }; +} + +function makeContext(): Context { + return { + messages: [{ role: "user", content: "Hello", timestamp: Date.now() }], + }; +} + +async function capturePayload( + model: Model<"bedrock-converse-stream">, + options?: SimpleStreamOptions, +): Promise { + let capturedPayload: BedrockThinkingPayload | undefined; + const s = streamBedrock(model, makeContext(), { + ...options, + reasoning: options?.reasoning ?? "high", + signal: AbortSignal.abort(), + onPayload: (payload) => { + capturedPayload = payload as BedrockThinkingPayload; + return payload; + }, + }); + + for await (const event of s) { + if (event.type === "error") { + break; + } + } + + if (!capturedPayload) { + throw new Error("Expected Bedrock payload to be captured before request abort"); + } + + return capturedPayload; +} + +describe("Bedrock thinking payload", () => { + it("uses adaptive thinking for Claude Opus 4.7 when reasoning is enabled", async () => { + const baseModel = getModel("amazon-bedrock", "global.anthropic.claude-opus-4-6-v1"); + const model: Model<"bedrock-converse-stream"> = { + ...baseModel, + id: "global.anthropic.claude-opus-4-7-v1", + name: "Claude Opus 4.7 (Global)", + }; + + const payload = await capturePayload(model); + + expect(payload.additionalModelRequestFields?.thinking).toEqual({ type: "adaptive" }); + expect(payload.additionalModelRequestFields?.output_config).toEqual({ effort: "high" }); + expect(payload.additionalModelRequestFields?.anthropic_beta).toBeUndefined(); + }); + + it("maps xhigh reasoning to effort=xhigh for Claude Opus 4.7", async () => { + const baseModel = getModel("amazon-bedrock", "global.anthropic.claude-opus-4-6-v1"); + const model: Model<"bedrock-converse-stream"> = { + ...baseModel, + id: "global.anthropic.claude-opus-4-7-v1", + name: "Claude Opus 4.7 (Global)", + }; + + const payload = await capturePayload(model, { reasoning: "xhigh" }); + + expect(payload.additionalModelRequestFields?.thinking).toEqual({ type: "adaptive" }); + expect(payload.additionalModelRequestFields?.output_config).toEqual({ effort: "xhigh" }); + expect(payload.additionalModelRequestFields?.anthropic_beta).toBeUndefined(); + }); +}); diff --git a/packages/ai/test/openai-completions-tool-choice.test.ts b/packages/ai/test/openai-completions-tool-choice.test.ts index 62185d3e..485e973e 100644 --- a/packages/ai/test/openai-completions-tool-choice.test.ts +++ b/packages/ai/test/openai-completions-tool-choice.test.ts @@ -24,9 +24,9 @@ vi.mock("openai", () => { class FakeOpenAI { chat = { completions: { - create: async (params: unknown) => { + create: (params: unknown) => { mockState.lastParams = params; - return { + const stream = { async *[Symbol.asyncIterator]() { const chunks = mockState.chunks ?? [ { @@ -44,6 +44,17 @@ vi.mock("openai", () => { } }, }; + const promise = Promise.resolve(stream) as Promise & { + withResponse: () => Promise<{ + data: typeof stream; + response: { status: number; headers: Headers }; + }>; + }; + promise.withResponse = async () => ({ + data: stream, + response: { status: 200, headers: new Headers() }, + }); + return promise; }, }, }; diff --git a/packages/ai/test/openai-responses-copilot-provider.test.ts b/packages/ai/test/openai-responses-copilot-provider.test.ts index df1ccb8a..ef55603b 100644 --- a/packages/ai/test/openai-responses-copilot-provider.test.ts +++ b/packages/ai/test/openai-responses-copilot-provider.test.ts @@ -1,6 +1,7 @@ import { afterEach, describe, expect, it, vi } from "vitest"; import { getModel } from "../src/models.js"; import { streamOpenAIResponses } from "../src/providers/openai-responses.js"; +import type { Model } from "../src/types.js"; type CapturedHeaders = Headers | string[][] | Record | undefined; @@ -22,6 +23,7 @@ function getHeader(headers: CapturedHeaders, name: string): string | null { async function captureOpenAIResponseHeaders( options: Parameters[2], + model: Model<"openai-responses"> = getModel("openai", "gpt-5.4"), ): Promise<{ sessionId: string | null; clientRequestId: string | null }> { const captured = { sessionId: null as string | null, clientRequestId: null as string | null }; vi.spyOn(globalThis, "fetch").mockImplementation(async (_input, init) => { @@ -34,7 +36,7 @@ async function captureOpenAIResponseHeaders( }); const stream = streamOpenAIResponses( - getModel("openai", "gpt-5.4"), + model, { systemPrompt: "sys", messages: [{ role: "user", content: "hi", timestamp: Date.now() }], @@ -95,6 +97,17 @@ describe("openai-responses provider defaults", () => { expect(captured).toEqual({ sessionId: "session-123", clientRequestId: "session-123" }); }); + it("sets cache-affinity headers for proxy OpenAI Responses requests with a sessionId", async () => { + const proxyModel: Model<"openai-responses"> = { + ...getModel("openai", "gpt-5.4"), + provider: "opencode", + baseUrl: "https://proxy.example.com/v1", + }; + const captured = await captureOpenAIResponseHeaders({ sessionId: "session-123" }, proxyModel); + + expect(captured).toEqual({ sessionId: "session-123", clientRequestId: "session-123" }); + }); + it("lets explicit headers override the default OpenAI cache-affinity headers", async () => { const captured = await captureOpenAIResponseHeaders({ sessionId: "session-123", diff --git a/packages/ai/test/supports-xhigh.test.ts b/packages/ai/test/supports-xhigh.test.ts index b3e5aaf6..c0c4db0c 100644 --- a/packages/ai/test/supports-xhigh.test.ts +++ b/packages/ai/test/supports-xhigh.test.ts @@ -8,6 +8,12 @@ describe("supportsXhigh", () => { expect(supportsXhigh(model!)).toBe(true); }); + it("returns true for Anthropic Opus 4.7 on anthropic-messages API", () => { + const model = getModel("anthropic", "claude-opus-4-7"); + expect(model).toBeDefined(); + expect(supportsXhigh(model!)).toBe(true); + }); + it("returns false for non-Opus Anthropic models", () => { const model = getModel("anthropic", "claude-sonnet-4-5"); expect(model).toBeDefined(); diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 269b4e29..b2bc5ff4 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,16 +2,75 @@ ## [Unreleased] +### Fixed + +- Fixed missing root exports for `RpcClient` and RPC protocol types from `@mariozechner/pi-coding-agent`, so ESM consumers can import them from the main package entrypoint ([#3275](https://github.com/badlogic/pi-mono/issues/3275)) + +## [0.67.6] - 2026-04-16 + +### New Features + +- Prompt templates support an `argument-hint` frontmatter field that renders before the description in the `/` autocomplete dropdown, using `` for required and `[square]` for optional arguments. See [docs/prompt-templates.md#argument-hints](docs/prompt-templates.md#argument-hints). +- New `after_provider_response` extension hook lets extensions inspect provider HTTP status codes and headers immediately after each response is received and before stream consumption begins. See [docs/extensions.md](docs/extensions.md). +- Compact interactive startup header with a comma-separated view of loaded AGENTS.md files, prompt templates, skills, and extensions. Press `Ctrl+O` to toggle the expanded listing. +- Markdown links in assistant output now render as OSC 8 hyperlinks on terminals that advertise support; unknown terminals and tmux/screen default to plain text so URLs are never silently dropped. + ### Added -- Added `--no-context-files` (`-nc`) to disable `AGENTS.md` and `CLAUDE.md` context file discovery and loading ([#3253](https://github.com/badlogic/pi-mono/issues/3253)) +- Added `argument-hint` frontmatter field for prompt templates, displayed before the description in the autocomplete dropdown ([#2780](https://github.com/badlogic/pi-mono/pull/2780) by [@andresvi94](https://github.com/andresvi94)) +- Added `after_provider_response` extension hook so extensions can inspect provider HTTP status codes and headers after each provider response is received and before stream consumption begins ([#3128](https://github.com/badlogic/pi-mono/issues/3128)) +- Added OSC 8 hyperlink rendering for markdown links when the terminal advertises support ([#3248](https://github.com/badlogic/pi-mono/pull/3248) by [@ofa1](https://github.com/ofa1)) + +### Changed + +- Changed interactive startup header to a compact, comma-separated view of loaded AGENTS.md files, prompt templates, skills, and extensions, with `Ctrl+O` to toggle the expanded listing ([#3267](https://github.com/badlogic/pi-mono/pull/3267)) +- Tightened hyperlink capability detection to default `hyperlinks: false` for unknown terminals and force it off under tmux/screen (including nested sessions), preventing markdown link URLs from disappearing on terminals that silently swallow OSC 8 sequences ([#3248](https://github.com/badlogic/pi-mono/pull/3248)) ### Fixed - Fixed interactive user message rendering to keep bottom padding visible in terminals affected by OSC 133 prompt markers without adding an extra blank line before the following assistant message ([#3090](https://github.com/badlogic/pi-mono/issues/3090)) +- Fixed `--verbose` startup output to begin with expanded startup help and loaded resource listings after the compact startup header change ([#3147](https://github.com/badlogic/pi-mono/issues/3147)) +- Fixed `find` tool returning no results for path-based glob patterns such as `src/**/*.spec.ts` or `some/parent/child/**` by switching fd into full-path mode and normalizing the pattern when it contains a `/` ([#3302](https://github.com/badlogic/pi-mono/issues/3302)) +- Fixed `find` tool applying nested `.gitignore` rules across sibling directories (e.g. rules from `a/.gitignore` hiding matching files under `b/`) by dropping the manual `--ignore-file` collection and delegating to fd's hierarchical `.gitignore` handling via `--no-require-git` ([#3303](https://github.com/badlogic/pi-mono/issues/3303)) +- Fixed OpenAI Responses prompt caching for non-`api.openai.com` base URLs (OpenAI-compatible proxies such as litellm, theclawbay) by sending the `session_id` and `x-client-request-id` cache-affinity headers unconditionally when a `sessionId` is provided, matching the official Codex CLI behavior ([#3264](https://github.com/badlogic/pi-mono/pull/3264) by [@vegarsti](https://github.com/vegarsti)) +- Fixed the `preset` example extension to snapshot the active model, thinking level, and tool set on the first preset application and restore that state when cycling back to `(none)`, instead of falling back to a hardcoded default tool list ([#3272](https://github.com/badlogic/pi-mono/pull/3272) by [@stembi](https://github.com/stembi)) + +## [0.67.5] - 2026-04-16 + +### Fixed + +- Fixed Opus 4.7 adaptive thinking configuration across Anthropic and Bedrock providers by recognizing Opus 4.7 adaptive-thinking support and mapping `xhigh` reasoning to provider-supported effort values ([#3286](https://github.com/badlogic/pi-mono/pull/3286) by [@markusylisiurunen](https://github.com/markusylisiurunen)) +- Fixed Zellij `Shift+Enter` regressions by reverting the Zellij-specific Kitty keyboard query bypass and restoring the previous keyboard negotiation behavior ([#3259](https://github.com/badlogic/pi-mono/issues/3259)) + +## [0.67.4] - 2026-04-16 + +### New Features + +- `--no-context-files` (`-nc`) disables automatic `AGENTS.md` / `CLAUDE.md` discovery when you need a clean run without project context injection. See [README.md#context-files](README.md#context-files). +- `loadProjectContextFiles()` is now exported as a standalone utility for extensions and SDK-style integrations that need to inspect the same context-file resolution order used by the CLI. See [README.md#context-files](README.md#context-files). +- New `after_provider_response` extension hook lets extensions inspect provider HTTP status codes and headers immediately after response creation and before stream consumption. See [docs/extensions.md](docs/extensions.md). + +### Added + +- Added `--no-context-files` (`-nc`) to disable `AGENTS.md` and `CLAUDE.md` context file discovery and loading ([#3253](https://github.com/badlogic/pi-mono/issues/3253)) +- Exported `loadProjectContextFiles()` as a standalone utility so extensions can discover project context files without instantiating a full `DefaultResourceLoader` ([#3142](https://github.com/badlogic/pi-mono/issues/3142)) +- Added `after_provider_response` extension hook so extensions can inspect provider HTTP status codes and headers after each provider response is received and before stream consumption begins ([#3128](https://github.com/badlogic/pi-mono/issues/3128)) + +### Changed + +- Added `claude-opus-4-7` model for Anthropic. +- Changed Anthropic prompt caching to add a `cache_control` breakpoint on the last tool definition, so tool schemas can be cached independently from transcript updates while preserving existing cache retention behavior ([#3260](https://github.com/badlogic/pi-mono/issues/3260)) + +### Fixed + +- Fixed markdown strikethrough parsing in interactive rendering and HTML export to require strict double-tilde delimiters (`~~text~~`) with non-whitespace boundaries. +- Fixed shutdown handling to kill tracked detached `bash` tool child processes on exit signals, preventing orphaned background processes. +>>>>>>> 165603189b60231ed8b88274729471cee676a0c0 - Fixed flaky `edit-tool-no-full-redraw` TUI tests by waiting for asynchronous preview and preflight error rendering instead of relying on fixed render ticks. - Fixed `kimi-coding` default model selection to use `kimi-for-coding` instead of `kimi-k2-thinking` ([#3242](https://github.com/badlogic/pi-mono/issues/3242)) - Fixed `ctrl+z` on native Windows to avoid crashing interactive mode, disable the default suspend binding there, and show a status message when suspend is invoked manually ([#3191](https://github.com/badlogic/pi-mono/issues/3191)) +- Fixed `find` tool cancellation and responsiveness on broad searches by making `.gitignore` discovery and `fd` execution fully abort-aware and non-blocking ([#3148](https://github.com/badlogic/pi-mono/issues/3148)) +- Fixed `grep` broad-search stalls when `context=0` by formatting match lines from ripgrep JSON output instead of doing synchronous per-match file reads ([#3205](https://github.com/badlogic/pi-mono/issues/3205)) ## [0.67.3] - 2026-04-15 @@ -31,7 +90,6 @@ - Fixed the `plan-mode` example extension to allow `eza` in the read-only bash allowlist instead of the deprecated `exa` command ([#3240](https://github.com/badlogic/pi-mono/pull/3240) by [@rwachtler](https://github.com/rwachtler)) - Fixed `google-vertex` API key resolution to treat `gcp-vertex-credentials` as an Application Default Credentials marker instead of a literal API key, so marker-based setups correctly fall back to ADC ([#3221](https://github.com/badlogic/pi-mono/pull/3221) by [@deepkilo](https://github.com/deepkilo)) - Fixed RPC `prompt` to wait for prompt preflight success before emitting its single authoritative response, while still treating handled and queued prompts as success ([#3049](https://github.com/badlogic/pi-mono/issues/3049)) -- Fixed Alt keybindings inside Zellij by skipping the Kitty keyboard protocol query there and enabling xterm `modifyOtherKeys` mode 2 directly ([#3163](https://github.com/badlogic/pi-mono/issues/3163)) - Fixed `/scoped-models` reordering to propagate into the `/model` scoped tab, preserving the user-defined scoped model order instead of re-sorting it ([#3217](https://github.com/badlogic/pi-mono/issues/3217)) - Fixed `session_shutdown` to fire on `SIGHUP` and `SIGTERM` in interactive, print, and RPC modes so extensions can run shutdown cleanup on those signal-driven exits ([#3212](https://github.com/badlogic/pi-mono/issues/3212)) - Fixed screenshot path parsing to handle lower case am/pm in macOS screenshot filenames ([#3194](https://github.com/badlogic/pi-mono/pull/3194) by [@jay-aye-see-kay](https://github.com/jay-aye-see-kay)) diff --git a/packages/coding-agent/docs/extensions.md b/packages/coding-agent/docs/extensions.md index af1a2513..7175bab4 100644 --- a/packages/coding-agent/docs/extensions.md +++ b/packages/coding-agent/docs/extensions.md @@ -246,6 +246,7 @@ user sends prompt ──────────────────── │ ├─► turn_start │ │ │ ├─► context (can modify messages) │ │ │ ├─► before_provider_request (can inspect or replace payload) + │ ├─► after_provider_response (status + headers, before stream consume) │ │ │ │ │ │ LLM responds, may call tools: │ │ │ │ ├─► tool_execution_start │ │ @@ -534,6 +535,22 @@ pi.on("before_provider_request", (event, ctx) => { This is mainly useful for debugging provider serialization and cache behavior. +#### after_provider_response + +Fired after an HTTP response is received and before its stream body is consumed. Handlers run in extension load order. + +```typescript +pi.on("after_provider_response", (event, ctx) => { + // event.status - HTTP status code + // event.headers - normalized response headers + if (event.status === 429) { + console.log("rate limited", event.headers["retry-after"]); + } +}); +``` + +Header availability depends on provider and transport. Providers that abstract HTTP responses may not expose headers. + ### Model Events #### model_select @@ -2231,7 +2248,7 @@ All examples in [examples/extensions/](../examples/extensions/). | `dirty-repo-guard.ts` | Warn on dirty git repo | `on("session_before_*")`, `exec` | | `input-transform.ts` | Transform user input | `on("input")` | | `model-status.ts` | React to model changes | `on("model_select")`, `setStatus` | -| `provider-payload.ts` | Inspect or patch provider payloads | `on("before_provider_request")` | +| `provider-payload.ts` | Inspect payloads and provider response headers | `on("before_provider_request")`, `on("after_provider_response")` | | `system-prompt-header.ts` | Display system prompt info | `on("agent_start")`, `getSystemPrompt` | | `claude-rules.ts` | Load rules from files | `on("session_start")`, `on("before_agent_start")` | | `file-trigger.ts` | File watcher triggers messages | `sendMessage` | diff --git a/packages/coding-agent/docs/prompt-templates.md b/packages/coding-agent/docs/prompt-templates.md index d6e540bb..056d2c9a 100644 --- a/packages/coding-agent/docs/prompt-templates.md +++ b/packages/coding-agent/docs/prompt-templates.md @@ -30,6 +30,27 @@ Review the staged changes (`git diff --cached`). Focus on: - The filename becomes the command name. `review.md` becomes `/review`. - `description` is optional. If missing, the first non-empty line is used. +- `argument-hint` is optional. When set, the hint is displayed before the description in the autocomplete dropdown. + +### Argument Hints + +Use `argument-hint` in frontmatter to show expected arguments in autocomplete. Use `` for required arguments and `[square brackets]` for optional ones: + +```markdown +--- +description: Review PRs from URLs with structured issue and code analysis +argument-hint: "" +--- +``` + +This renders in the autocomplete dropdown as: + +``` +→ pr — Review PRs from URLs with structured issue and code analysis + is — Analyze GitHub issues (bugs or feature requests) + wr [instructions] — Finish the current task end-to-end + cl — Audit changelog entries before release +``` ## Usage diff --git a/packages/coding-agent/docs/terminal-setup.md b/packages/coding-agent/docs/terminal-setup.md index fd80e231..74b0df2a 100644 --- a/packages/coding-agent/docs/terminal-setup.md +++ b/packages/coding-agent/docs/terminal-setup.md @@ -32,12 +32,6 @@ If you want `Shift+Enter` to keep working in tmux via that remap, add `ctrl+j` t } ``` -## Zellij - -Pi detects Zellij automatically and skips the Kitty keyboard protocol query there. -Zellij currently forwards that query to the outer terminal but still sends Alt as legacy ESC-prefixed sequences, which can break Alt keybindings if applications enable Kitty mode. -Pi uses xterm `modifyOtherKeys` mode 2 inside Zellij instead, so no extra Zellij config is required. - ## WezTerm Create `~/.wezterm.lua`: diff --git a/packages/coding-agent/examples/extensions/custom-provider-anthropic/package-lock.json b/packages/coding-agent/examples/extensions/custom-provider-anthropic/package-lock.json index 735ed093..6637c89f 100644 --- a/packages/coding-agent/examples/extensions/custom-provider-anthropic/package-lock.json +++ b/packages/coding-agent/examples/extensions/custom-provider-anthropic/package-lock.json @@ -1,12 +1,12 @@ { "name": "pi-extension-custom-provider", - "version": "1.18.3", + "version": "1.18.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pi-extension-custom-provider", - "version": "1.18.3", + "version": "1.18.6", "dependencies": { "@anthropic-ai/sdk": "^0.52.0" } diff --git a/packages/coding-agent/examples/extensions/custom-provider-anthropic/package.json b/packages/coding-agent/examples/extensions/custom-provider-anthropic/package.json index 21c572ca..da6667c2 100644 --- a/packages/coding-agent/examples/extensions/custom-provider-anthropic/package.json +++ b/packages/coding-agent/examples/extensions/custom-provider-anthropic/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-custom-provider-anthropic", "private": true, - "version": "1.18.3", + "version": "1.18.6", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/package.json b/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/package.json index 6694a0a5..2f6eda05 100644 --- a/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/package.json +++ b/packages/coding-agent/examples/extensions/custom-provider-gitlab-duo/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-custom-provider-gitlab-duo", "private": true, - "version": "1.18.3", + "version": "1.18.6", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/examples/extensions/custom-provider-qwen-cli/package.json b/packages/coding-agent/examples/extensions/custom-provider-qwen-cli/package.json index 7ef3cb4b..249715e1 100644 --- a/packages/coding-agent/examples/extensions/custom-provider-qwen-cli/package.json +++ b/packages/coding-agent/examples/extensions/custom-provider-qwen-cli/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-custom-provider-qwen-cli", "private": true, - "version": "1.17.3", + "version": "1.17.6", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/examples/extensions/preset.ts b/packages/coding-agent/examples/extensions/preset.ts index d644632d..a0cf663e 100644 --- a/packages/coding-agent/examples/extensions/preset.ts +++ b/packages/coding-agent/examples/extensions/preset.ts @@ -40,6 +40,7 @@ import { existsSync, readFileSync } from "node:fs"; import { join } from "node:path"; +import type { Api, Model } from "@mariozechner/pi-ai"; import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent"; import { DynamicBorder, getAgentDir } from "@mariozechner/pi-coding-agent"; import { Container, Key, type SelectItem, SelectList, Text } from "@mariozechner/pi-tui"; @@ -97,10 +98,17 @@ function loadPresets(cwd: string): PresetsConfig { return { ...globalPresets, ...projectPresets }; } +interface OriginalState { + model: Model | undefined; + thinkingLevel: "off" | "minimal" | "low" | "medium" | "high" | "xhigh"; + tools: string[]; +} + export default function presetExtension(pi: ExtensionAPI) { let presets: PresetsConfig = {}; let activePresetName: string | undefined; let activePreset: Preset | undefined; + let originalState: OriginalState | undefined; // Register --preset CLI flag pi.registerFlag("preset", { @@ -112,6 +120,15 @@ export default function presetExtension(pi: ExtensionAPI) { * Apply a preset configuration. */ async function applyPreset(name: string, preset: Preset, ctx: ExtensionContext): Promise { + // Snapshot state before the first preset is applied (i.e. only when transitioning from no-preset) + if (activePresetName === undefined) { + originalState = { + model: ctx.model, + thinkingLevel: pi.getThinkingLevel(), + tools: pi.getActiveTools(), + }; + } + // Apply model if specified if (preset.provider && preset.model) { const model = ctx.modelRegistry.find(preset.provider, preset.model); @@ -248,10 +265,18 @@ export default function presetExtension(pi: ExtensionAPI) { if (!result) return; if (result === "(none)") { - // Clear preset and restore defaults + // Clear preset and restore original state activePresetName = undefined; activePreset = undefined; - pi.setActiveTools(["read", "bash", "edit", "write"]); + if (originalState) { + if (originalState.model) { + await pi.setModel(originalState.model); + } + pi.setThinkingLevel(originalState.thinkingLevel); + pi.setActiveTools(originalState.tools); + } else { + pi.setActiveTools(["read", "bash", "edit", "write"]); + } ctx.ui.notify("Preset cleared, defaults restored", "info"); updateStatus(ctx); return; @@ -296,7 +321,15 @@ export default function presetExtension(pi: ExtensionAPI) { if (nextName === "(none)") { activePresetName = undefined; activePreset = undefined; - pi.setActiveTools(["read", "bash", "edit", "write"]); + if (originalState) { + if (originalState.model) { + await pi.setModel(originalState.model); + } + pi.setThinkingLevel(originalState.thinkingLevel); + pi.setActiveTools(originalState.tools); + } else { + pi.setActiveTools(["read", "bash", "edit", "write"]); + } ctx.ui.notify("Preset cleared, defaults restored", "info"); updateStatus(ctx); return; diff --git a/packages/coding-agent/examples/extensions/provider-payload.ts b/packages/coding-agent/examples/extensions/provider-payload.ts index 55d8cf59..738a4412 100644 --- a/packages/coding-agent/examples/extensions/provider-payload.ts +++ b/packages/coding-agent/examples/extensions/provider-payload.ts @@ -11,4 +11,8 @@ export default function (pi: ExtensionAPI) { // Optional: replace the payload instead of only logging it. // return { ...event.payload, temperature: 0 }; }); + + pi.on("after_provider_response", (event) => { + appendFileSync(logFile, `[${event.status}] ${JSON.stringify(event.headers)}\n\n`, "utf8"); + }); } diff --git a/packages/coding-agent/examples/extensions/with-deps/package-lock.json b/packages/coding-agent/examples/extensions/with-deps/package-lock.json index d6c6656f..22b9706a 100644 --- a/packages/coding-agent/examples/extensions/with-deps/package-lock.json +++ b/packages/coding-agent/examples/extensions/with-deps/package-lock.json @@ -1,12 +1,12 @@ { "name": "pi-extension-with-deps", - "version": "1.31.3", + "version": "1.31.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pi-extension-with-deps", - "version": "1.31.3", + "version": "1.31.6", "dependencies": { "ms": "^2.1.3" }, diff --git a/packages/coding-agent/examples/extensions/with-deps/package.json b/packages/coding-agent/examples/extensions/with-deps/package.json index dc5ce841..e9c63bf1 100644 --- a/packages/coding-agent/examples/extensions/with-deps/package.json +++ b/packages/coding-agent/examples/extensions/with-deps/package.json @@ -1,7 +1,7 @@ { "name": "pi-extension-with-deps", "private": true, - "version": "1.31.3", + "version": "1.31.6", "type": "module", "scripts": { "clean": "echo 'nothing to clean'", diff --git a/packages/coding-agent/package.json b/packages/coding-agent/package.json index 4543b262..ad21c8b8 100644 --- a/packages/coding-agent/package.json +++ b/packages/coding-agent/package.json @@ -1,6 +1,6 @@ { "name": "@mariozechner/pi-coding-agent", - "version": "0.67.3", + "version": "0.67.6", "description": "Coding agent CLI with read, bash, edit, write tools and session management", "type": "module", "piConfig": { @@ -40,9 +40,9 @@ }, "dependencies": { "@mariozechner/jiti": "^2.6.2", - "@mariozechner/pi-agent-core": "^0.67.3", - "@mariozechner/pi-ai": "^0.67.3", - "@mariozechner/pi-tui": "^0.67.3", + "@mariozechner/pi-agent-core": "^0.67.6", + "@mariozechner/pi-ai": "^0.67.6", + "@mariozechner/pi-tui": "^0.67.6", "@silvia-odwyer/photon-node": "^0.3.4", "ajv": "^8.17.1", "chalk": "^5.5.0", diff --git a/packages/coding-agent/src/core/agent-session-runtime.ts b/packages/coding-agent/src/core/agent-session-runtime.ts index 58e1fd57..327140bd 100644 --- a/packages/coding-agent/src/core/agent-session-runtime.ts +++ b/packages/coding-agent/src/core/agent-session-runtime.ts @@ -33,6 +33,16 @@ export type CreateAgentSessionRuntimeFactory = (options: { sessionStartEvent?: SessionStartEvent; }) => Promise; +export class SessionImportFileNotFoundError extends Error { + readonly filePath: string; + + constructor(filePath: string) { + super(`File not found: ${filePath}`); + this.name = "SessionImportFileNotFoundError"; + this.filePath = filePath; + } +} + function extractUserMessageText(content: string | Array<{ type: string; text?: string }>): string { if (typeof content === "string") { return content; @@ -251,7 +261,7 @@ export class AgentSessionRuntime { async importFromJsonl(inputPath: string, cwdOverride?: string): Promise<{ cancelled: boolean }> { const resolvedPath = resolve(inputPath); if (!existsSync(resolvedPath)) { - throw new Error(`File not found: ${resolvedPath}`); + throw new SessionImportFileNotFoundError(resolvedPath); } const sessionDir = this.session.sessionManager.getSessionDir(); diff --git a/packages/coding-agent/src/core/export-html/template.js b/packages/coding-agent/src/core/export-html/template.js index 6b53516b..57bdb811 100644 --- a/packages/coding-agent/src/core/export-html/template.js +++ b/packages/coding-agent/src/core/export-html/template.js @@ -1448,9 +1448,23 @@ } // Configure marked with syntax highlighting and HTML escaping for text + const strictStrikethroughRegex = /^(~~)(?=[^\s~])((?:\\.|[^\\])*?(?:\\.|[^\s~\\]))\1(?=[^~]|$)/; + marked.use({ breaks: true, gfm: true, + tokenizer: { + del(src) { + const match = strictStrikethroughRegex.exec(src); + if (!match) return undefined; + return { + type: 'del', + raw: match[0], + text: match[2], + tokens: this.lexer.inlineTokens(match[2]) + }; + } + }, renderer: { // Code blocks: syntax highlight, no HTML escaping code(token) { diff --git a/packages/coding-agent/src/core/extensions/index.ts b/packages/coding-agent/src/core/extensions/index.ts index cca8c914..686bd15e 100644 --- a/packages/coding-agent/src/core/extensions/index.ts +++ b/packages/coding-agent/src/core/extensions/index.ts @@ -20,6 +20,7 @@ export type { } from "./runner.js"; export { ExtensionRunner } from "./runner.js"; export type { + AfterProviderResponseEvent, AgentEndEvent, AgentStartEvent, // Re-exports diff --git a/packages/coding-agent/src/core/extensions/types.ts b/packages/coding-agent/src/core/extensions/types.ts index f986db85..28af6703 100644 --- a/packages/coding-agent/src/core/extensions/types.ts +++ b/packages/coding-agent/src/core/extensions/types.ts @@ -543,6 +543,13 @@ export interface BeforeProviderRequestEvent { payload: unknown; } +/** Fired after a provider response is received and before the response stream is consumed. */ +export interface AfterProviderResponseEvent { + type: "after_provider_response"; + status: number; + headers: Record; +} + /** Fired after user submits prompt but before agent loop. */ export interface BeforeAgentStartEvent { type: "before_agent_start"; @@ -863,6 +870,7 @@ export type ExtensionEvent = | SessionEvent | ContextEvent | BeforeProviderRequestEvent + | AfterProviderResponseEvent | BeforeAgentStartEvent | AgentStartEvent | AgentEndEvent @@ -1010,6 +1018,7 @@ export interface ExtensionAPI { event: "before_provider_request", handler: ExtensionHandler, ): void; + on(event: "after_provider_response", handler: ExtensionHandler): void; on(event: "before_agent_start", handler: ExtensionHandler): void; on(event: "agent_start", handler: ExtensionHandler): void; on(event: "agent_end", handler: ExtensionHandler): void; diff --git a/packages/coding-agent/src/core/prompt-templates.ts b/packages/coding-agent/src/core/prompt-templates.ts index 7c5dbc3f..4850351f 100644 --- a/packages/coding-agent/src/core/prompt-templates.ts +++ b/packages/coding-agent/src/core/prompt-templates.ts @@ -11,6 +11,7 @@ import { createSyntheticSourceInfo, type SourceInfo } from "./source-info.js"; export interface PromptTemplate { name: string; description: string; + argumentHint?: string; content: string; sourceInfo: SourceInfo; filePath: string; // Absolute path to the template file @@ -121,6 +122,7 @@ function loadTemplateFromFile(filePath: string, sourceInfo: SourceInfo): PromptT return { name, description, + ...(frontmatter["argument-hint"] && { argumentHint: frontmatter["argument-hint"] }), content: body, sourceInfo, filePath, diff --git a/packages/coding-agent/src/core/resource-loader.ts b/packages/coding-agent/src/core/resource-loader.ts index 9f21102d..c1c49b23 100644 --- a/packages/coding-agent/src/core/resource-loader.ts +++ b/packages/coding-agent/src/core/resource-loader.ts @@ -73,7 +73,7 @@ function loadContextFileFromDir(dir: string): { path: string; content: string } return null; } -function loadProjectContextFiles( +export function loadProjectContextFiles( options: { cwd?: string; agentDir?: string } = {}, ): Array<{ path: string; content: string }> { const resolvedCwd = options.cwd ?? process.cwd(); diff --git a/packages/coding-agent/src/core/sdk.ts b/packages/coding-agent/src/core/sdk.ts index 7e4512cb..5831e1ff 100644 --- a/packages/coding-agent/src/core/sdk.ts +++ b/packages/coding-agent/src/core/sdk.ts @@ -314,6 +314,17 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {} } return runner.emitBeforeProviderRequest(payload); }, + onResponse: async (response, _model) => { + const runner = extensionRunnerRef.current; + if (!runner?.hasHandlers("after_provider_response")) { + return; + } + await runner.emit({ + type: "after_provider_response", + status: response.status, + headers: response.headers, + }); + }, sessionId: sessionManager.getSessionId(), transformContext: async (messages) => { const runner = extensionRunnerRef.current; diff --git a/packages/coding-agent/src/core/tools/find.ts b/packages/coding-agent/src/core/tools/find.ts index dd71434f..0a8cbc2a 100644 --- a/packages/coding-agent/src/core/tools/find.ts +++ b/packages/coding-agent/src/core/tools/find.ts @@ -1,9 +1,9 @@ +import { createInterface } from "node:readline"; import type { AgentTool } from "@mariozechner/pi-agent-core"; import { Text } from "@mariozechner/pi-tui"; import { type Static, Type } from "@sinclair/typebox"; -import { spawnSync } from "child_process"; +import { spawn } from "child_process"; import { existsSync } from "fs"; -import { globSync } from "glob"; import path from "path"; import { keyHint } from "../../modes/interactive/components/keybinding-hints.js"; import { ensureTool } from "../../utils/tools-manager.js"; @@ -133,7 +133,19 @@ export function createFindToolDefinition( return; } - const onAbort = () => reject(new Error("Operation aborted")); + let settled = false; + let stopChild: (() => void) | undefined; + const settle = (fn: () => void) => { + if (settled) return; + settled = true; + signal?.removeEventListener("abort", onAbort); + stopChild = undefined; + fn(); + }; + const onAbort = () => { + stopChild?.(); + settle(() => reject(new Error("Operation aborted"))); + }; signal?.addEventListener("abort", onAbort, { once: true }); (async () => { @@ -145,19 +157,28 @@ export function createFindToolDefinition( // If custom operations provide glob(), use that instead of fd. if (customOps?.glob) { if (!(await ops.exists(searchPath))) { - reject(new Error(`Path not found: ${searchPath}`)); + settle(() => reject(new Error(`Path not found: ${searchPath}`))); + return; + } + if (signal?.aborted) { + settle(() => reject(new Error("Operation aborted"))); return; } const results = await ops.glob(pattern, searchPath, { ignore: ["**/node_modules/**", "**/.git/**"], limit: effectiveLimit, }); - signal?.removeEventListener("abort", onAbort); + if (signal?.aborted) { + settle(() => reject(new Error("Operation aborted"))); + return; + } if (results.length === 0) { - resolve({ - content: [{ type: "text", text: "No files found matching pattern" }], - details: undefined, - }); + settle(() => + resolve({ + content: [{ type: "text", text: "No files found matching pattern" }], + details: undefined, + }), + ); return; } @@ -183,111 +204,150 @@ export function createFindToolDefinition( if (notices.length > 0) { resultOutput += `\n\n[${notices.join(". ")}]`; } - resolve({ - content: [{ type: "text", text: resultOutput }], - details: Object.keys(details).length > 0 ? details : undefined, - }); + settle(() => + resolve({ + content: [{ type: "text", text: resultOutput }], + details: Object.keys(details).length > 0 ? details : undefined, + }), + ); return; } // Default implementation uses fd. const fdPath = await ensureTool("fd", true); + if (signal?.aborted) { + settle(() => reject(new Error("Operation aborted"))); + return; + } if (!fdPath) { - reject(new Error("fd is not available and could not be downloaded")); + settle(() => reject(new Error("fd is not available and could not be downloaded"))); return; } - // Build fd arguments. + // Build fd arguments. --no-require-git makes fd apply hierarchical .gitignore + // semantics whether or not the search path is inside a git repository, without + // leaking sibling-directory rules the way --ignore-file (a global source) would. const args: string[] = [ "--glob", "--color=never", "--hidden", + "--no-require-git", "--max-results", String(effectiveLimit), ]; - // Include .gitignore files from the search tree. - const gitignoreFiles = new Set(); - const rootGitignore = path.join(searchPath, ".gitignore"); - if (existsSync(rootGitignore)) gitignoreFiles.add(rootGitignore); - try { - const nestedGitignores = globSync("**/.gitignore", { - cwd: searchPath, - dot: true, - absolute: true, - ignore: ["**/node_modules/**", "**/.git/**"], - }); - for (const file of nestedGitignores) gitignoreFiles.add(file); - } catch { - // ignore - } - for (const gitignorePath of gitignoreFiles) args.push("--ignore-file", gitignorePath); - args.push(pattern, searchPath); - const result = spawnSync(fdPath, args, { encoding: "utf-8", maxBuffer: 10 * 1024 * 1024 }); - signal?.removeEventListener("abort", onAbort); - if (result.error) { - reject(new Error(`Failed to run fd: ${result.error.message}`)); - return; + // fd --glob matches against the basename unless --full-path is set; in --full-path + // mode it matches against the absolute candidate path, so a path-containing + // pattern like 'src/**/*.spec.ts' needs a leading '**/' to match anything. + let effectivePattern = pattern; + if (pattern.includes("/")) { + args.push("--full-path"); + if (!pattern.startsWith("/") && !pattern.startsWith("**/") && pattern !== "**") { + effectivePattern = `**/${pattern}`; + } } + args.push(effectivePattern, searchPath); - const output = result.stdout?.trim() || ""; - if (result.status !== 0) { - const errorMsg = result.stderr?.trim() || `fd exited with code ${result.status}`; - if (!output) { - reject(new Error(errorMsg)); + const child = spawn(fdPath, args, { stdio: ["ignore", "pipe", "pipe"] }); + const rl = createInterface({ input: child.stdout }); + let stderr = ""; + const lines: string[] = []; + + stopChild = () => { + if (!child.killed) { + child.kill(); + } + }; + + const cleanup = () => { + rl.close(); + }; + + child.stderr?.on("data", (chunk) => { + stderr += chunk.toString(); + }); + + rl.on("line", (line) => { + lines.push(line); + }); + + child.on("error", (error) => { + cleanup(); + settle(() => reject(new Error(`Failed to run fd: ${error.message}`))); + }); + + child.on("close", (code) => { + cleanup(); + if (signal?.aborted) { + settle(() => reject(new Error("Operation aborted"))); return; } - } - if (!output) { - resolve({ - content: [{ type: "text", text: "No files found matching pattern" }], - details: undefined, - }); + const output = lines.join("\n"); + if (code !== 0) { + const errorMsg = stderr.trim() || `fd exited with code ${code}`; + if (!output) { + settle(() => reject(new Error(errorMsg))); + return; + } + } + if (!output) { + settle(() => + resolve({ + content: [{ type: "text", text: "No files found matching pattern" }], + details: undefined, + }), + ); + return; + } + + const relativized: string[] = []; + for (const rawLine of lines) { + const line = rawLine.replace(/\r$/, "").trim(); + if (!line) continue; + const hadTrailingSlash = line.endsWith("/") || line.endsWith("\\"); + let relativePath = line; + if (line.startsWith(searchPath)) { + relativePath = line.slice(searchPath.length + 1); + } else { + relativePath = path.relative(searchPath, line); + } + if (hadTrailingSlash && !relativePath.endsWith("/")) relativePath += "/"; + relativized.push(toPosixPath(relativePath)); + } + + const resultLimitReached = relativized.length >= effectiveLimit; + const rawOutput = relativized.join("\n"); + const truncation = truncateHead(rawOutput, { maxLines: Number.MAX_SAFE_INTEGER }); + let resultOutput = truncation.content; + const details: FindToolDetails = {}; + const notices: string[] = []; + if (resultLimitReached) { + notices.push( + `${effectiveLimit} results limit reached. Use limit=${effectiveLimit * 2} for more, or refine pattern`, + ); + details.resultLimitReached = effectiveLimit; + } + if (truncation.truncated) { + notices.push(`${formatSize(DEFAULT_MAX_BYTES)} limit reached`); + details.truncation = truncation; + } + if (notices.length > 0) { + resultOutput += `\n\n[${notices.join(". ")}]`; + } + settle(() => + resolve({ + content: [{ type: "text", text: resultOutput }], + details: Object.keys(details).length > 0 ? details : undefined, + }), + ); + }); + } catch (e) { + if (signal?.aborted) { + settle(() => reject(new Error("Operation aborted"))); return; } - - const lines = output.split("\n"); - const relativized: string[] = []; - for (const rawLine of lines) { - const line = rawLine.replace(/\r$/, "").trim(); - if (!line) continue; - const hadTrailingSlash = line.endsWith("/") || line.endsWith("\\"); - let relativePath = line; - if (line.startsWith(searchPath)) { - relativePath = line.slice(searchPath.length + 1); - } else { - relativePath = path.relative(searchPath, line); - } - if (hadTrailingSlash && !relativePath.endsWith("/")) relativePath += "/"; - relativized.push(toPosixPath(relativePath)); - } - - const resultLimitReached = relativized.length >= effectiveLimit; - const rawOutput = relativized.join("\n"); - const truncation = truncateHead(rawOutput, { maxLines: Number.MAX_SAFE_INTEGER }); - let resultOutput = truncation.content; - const details: FindToolDetails = {}; - const notices: string[] = []; - if (resultLimitReached) { - notices.push( - `${effectiveLimit} results limit reached. Use limit=${effectiveLimit * 2} for more, or refine pattern`, - ); - details.resultLimitReached = effectiveLimit; - } - if (truncation.truncated) { - notices.push(`${formatSize(DEFAULT_MAX_BYTES)} limit reached`); - details.truncation = truncation; - } - if (notices.length > 0) { - resultOutput += `\n\n[${notices.join(". ")}]`; - } - resolve({ - content: [{ type: "text", text: resultOutput }], - details: Object.keys(details).length > 0 ? details : undefined, - }); - } catch (e: any) { - signal?.removeEventListener("abort", onAbort); - reject(e); + const error = e instanceof Error ? e : new Error(String(e)); + settle(() => reject(error)); } })(); }); diff --git a/packages/coding-agent/src/core/tools/grep.ts b/packages/coding-agent/src/core/tools/grep.ts index 6802027e..a9c07282 100644 --- a/packages/coding-agent/src/core/tools/grep.ts +++ b/packages/coding-agent/src/core/tools/grep.ts @@ -267,7 +267,7 @@ export function createGrepToolDefinition( }; // Collect matches during streaming, then format them after rg exits. - const matches: Array<{ filePath: string; lineNumber: number }> = []; + const matches: Array<{ filePath: string; lineNumber: number; lineText?: string }> = []; rl.on("line", (line) => { if (!line.trim() || matchCount >= effectiveLimit) return; let event: any; @@ -280,7 +280,9 @@ export function createGrepToolDefinition( matchCount++; const filePath = event.data?.path?.text; const lineNumber = event.data?.line_number; - if (filePath && typeof lineNumber === "number") matches.push({ filePath, lineNumber }); + const lineText = event.data?.lines?.text; + if (filePath && typeof lineNumber === "number") + matches.push({ filePath, lineNumber, lineText }); if (matchCount >= effectiveLimit) { matchLimitReached = true; stopChild(true); @@ -312,8 +314,19 @@ export function createGrepToolDefinition( // Format matches after streaming finishes so custom readFile() backends can be async. for (const match of matches) { - const block = await formatBlock(match.filePath, match.lineNumber); - outputLines.push(...block); + if (contextValue === 0 && match.lineText !== undefined) { + const relativePath = formatPath(match.filePath); + const sanitized = match.lineText + .replace(/\r\n/g, "\n") + .replace(/\r/g, "") + .replace(/\n$/, ""); + const { text: truncatedText, wasTruncated } = truncateLine(sanitized); + if (wasTruncated) linesTruncated = true; + outputLines.push(`${relativePath}:${match.lineNumber}: ${truncatedText}`); + } else { + const block = await formatBlock(match.filePath, match.lineNumber); + outputLines.push(...block); + } } const rawOutput = outputLines.join("\n"); diff --git a/packages/coding-agent/src/index.ts b/packages/coding-agent/src/index.ts index a221998a..edf754a9 100644 --- a/packages/coding-agent/src/index.ts +++ b/packages/coding-agent/src/index.ts @@ -153,7 +153,7 @@ export type { } from "./core/package-manager.js"; export { DefaultPackageManager } from "./core/package-manager.js"; export type { ResourceCollision, ResourceDiagnostic, ResourceLoader } from "./core/resource-loader.js"; -export { DefaultResourceLoader } from "./core/resource-loader.js"; +export { DefaultResourceLoader, loadProjectContextFiles } from "./core/resource-loader.js"; // SDK for programmatic usage export { AgentSessionRuntime, @@ -296,7 +296,14 @@ export { type MainOptions, main } from "./main.js"; export { InteractiveMode, type InteractiveModeOptions, + type ModelInfo, type PrintModeOptions, + RpcClient, + type RpcClientOptions, + type RpcCommand, + type RpcEventListener, + type RpcResponse, + type RpcSessionState, runPrintMode, runRpcMode, } from "./modes/index.js"; diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts index c1f5089d..c088c7f3 100644 --- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts @@ -47,7 +47,7 @@ import { VERSION, } from "../../config.js"; import { type AgentSession, type AgentSessionEvent, parseSkillBlock } from "../../core/agent-session.js"; -import type { AgentSessionRuntime } from "../../core/agent-session-runtime.js"; +import { type AgentSessionRuntime, SessionImportFileNotFoundError } from "../../core/agent-session-runtime.js"; import type { ExtensionContext, ExtensionRunner, @@ -58,7 +58,7 @@ import type { import { FooterDataProvider, type ReadonlyFooterDataProvider } from "../../core/footer-data-provider.js"; import { type AppKeybinding, KeybindingsManager } from "../../core/keybindings.js"; import { createCompactionSummaryMessage } from "../../core/messages.js"; -import { findExactModelReferenceMatch, resolveModelScope } from "../../core/model-resolver.js"; +import { defaultModelPerProvider, findExactModelReferenceMatch, resolveModelScope } from "../../core/model-resolver.js"; import { DefaultPackageManager } from "../../core/package-manager.js"; import type { ResourceDiagnostic } from "../../core/resource-loader.js"; import { formatMissingSessionCwdPrompt, MissingSessionCwdError } from "../../core/session-cwd.js"; @@ -126,6 +126,22 @@ function isExpandable(obj: unknown): obj is Expandable { return typeof obj === "object" && obj !== null && "setExpanded" in obj && typeof obj.setExpanded === "function"; } +class ExpandableText extends Text implements Expandable { + constructor( + private readonly getCollapsedText: () => string, + private readonly getExpandedText: () => string, + expanded = false, + paddingX = 0, + paddingY = 0, + ) { + super(expanded ? getExpandedText() : getCollapsedText(), paddingX, paddingY); + } + + setExpanded(expanded: boolean): void { + this.setText(expanded ? this.getExpandedText() : this.getCollapsedText()); + } +} + type CompactionQueuedMessage = { text: string; mode: "steer" | "followUp"; @@ -143,6 +159,14 @@ function isTruthyEnvFlag(value: string | undefined): boolean { return value === "1" || value.toLowerCase() === "true" || value.toLowerCase() === "yes"; } +function isUnknownModel(model: Model | undefined): boolean { + return !!model && model.provider === "unknown" && model.id === "unknown" && model.api === "unknown"; +} + +function hasDefaultModelProvider(providerId: string): providerId is keyof typeof defaultModelPerProvider { + return providerId in defaultModelPerProvider; +} + /** * Options for InteractiveMode initialization. */ @@ -408,6 +432,7 @@ export class InteractiveMode { const templateCommands: SlashCommand[] = this.session.promptTemplates.map((cmd) => ({ name: cmd.name, description: this.prefixAutocompleteDescription(cmd.description, cmd.sourceInfo), + ...(cmd.argumentHint && { argumentHint: cmd.argumentHint }), })); // Convert extension commands to SlashCommand format @@ -499,7 +524,7 @@ export class InteractiveMode { // Build startup instructions using keybinding hint helpers const hint = (keybinding: AppKeybinding, description: string) => keyHint(keybinding, description); - const instructions = [ + const expandedInstructions = [ hint("app.interrupt", "to interrupt"), hint("app.clear", "to clear"), rawKeyHint(`${keyText("app.clear")} twice`, "to exit"), @@ -520,11 +545,28 @@ export class InteractiveMode { hint("app.clipboard.pasteImage", "to paste image"), rawKeyHint("drop files", "to attach"), ].join("\n"); + const compactInstructions = [ + hint("app.interrupt", "interrupt"), + rawKeyHint(`${keyText("app.clear")}/${keyText("app.exit")}`, "clear/exit"), + rawKeyHint("/", "commands"), + rawKeyHint("!", "bash"), + hint("app.tools.expand", "more"), + ].join(theme.fg("muted", " · ")); + const compactOnboarding = theme.fg( + "dim", + `Press ${keyText("app.tools.expand")} to show full startup help and loaded resources.`, + ); const onboarding = theme.fg( "dim", `Pi can explain its own features and look up its docs. Ask it how to use or extend Pi.`, ); - this.builtInHeader = new Text(`${logo}\n${instructions}\n\n${onboarding}`, 1, 0); + this.builtInHeader = new ExpandableText( + () => `${logo}\n${compactInstructions}\n${compactOnboarding}\n\n${onboarding}`, + () => `${logo}\n${expandedInstructions}\n\n${onboarding}`, + this.getStartupExpansionState(), + 1, + 0, + ); // Setup UI layout this.headerContainer.addChild(new Spacer(1)); @@ -836,6 +878,27 @@ export class InteractiveMode { return result; } + private formatContextPath(p: string): string { + const cwd = path.resolve(this.sessionManager.getCwd()); + const absolutePath = path.isAbsolute(p) ? path.resolve(p) : path.resolve(cwd, p); + const relativePath = path.relative(cwd, absolutePath); + const isInsideCwd = + relativePath === "" || + (!relativePath.startsWith("..") && + !relativePath.startsWith(`..${path.sep}`) && + !path.isAbsolute(relativePath)); + + if (isInsideCwd) { + return relativePath || "."; + } + + return this.formatDisplayPath(absolutePath); + } + + private getStartupExpansionState(): boolean { + return this.options.verbose || this.toolOutputExpanded; + } + /** * Get a short path relative to the package root for display. */ @@ -1057,6 +1120,38 @@ export class InteractiveMode { } const sectionHeader = (name: string, color: ThemeColor = "mdHeading") => theme.fg(color, `[${name}]`); + const compactPath = (resourcePath: string, sourceInfo?: SourceInfo): string => { + const shortPath = this.getShortPath(resourcePath, sourceInfo); + const normalizedPath = shortPath.replace(/\\/g, "/"); + const segments = normalizedPath.split("/").filter((segment) => segment.length > 0 && segment !== "~"); + if (segments.length > 0) { + return segments[segments.length - 1]!; + } + return shortPath; + }; + const formatCompactList = (items: string[], options?: { sort?: boolean }): string => { + const labels = items.map((item) => item.trim()).filter((item) => item.length > 0); + if (options?.sort !== false) { + labels.sort((a, b) => a.localeCompare(b)); + } + return theme.fg("dim", ` ${labels.join(", ")}`); + }; + const addLoadedSection = ( + name: string, + collapsedBody: string, + expandedBody = collapsedBody, + color: ThemeColor = "mdHeading", + ): void => { + const section = new ExpandableText( + () => `${sectionHeader(name, color)}\n${collapsedBody}`, + () => `${sectionHeader(name, color)}\n${expandedBody}`, + this.getStartupExpansionState(), + 0, + 0, + ); + this.chatContainer.addChild(section); + this.chatContainer.addChild(new Spacer(1)); + }; const skillsResult = this.session.resourceLoader.getSkills(); const promptsResult = this.session.resourceLoader.getPrompts(); @@ -1096,8 +1191,11 @@ export class InteractiveMode { const contextList = contextFiles .map((f) => theme.fg("dim", ` ${this.formatDisplayPath(f.path)}`)) .join("\n"); - this.chatContainer.addChild(new Text(`${sectionHeader("Context")}\n${contextList}`, 0, 0)); - this.chatContainer.addChild(new Spacer(1)); + const contextCompactList = formatCompactList( + contextFiles.map((contextFile) => this.formatContextPath(contextFile.path)), + { sort: false }, + ); + addLoadedSection("Context", contextCompactList, contextList); } const skills = skillsResult.skills; @@ -1109,8 +1207,8 @@ export class InteractiveMode { formatPath: (item) => this.formatDisplayPath(item.path), formatPackagePath: (item) => this.getShortPath(item.path, item.sourceInfo), }); - this.chatContainer.addChild(new Text(`${sectionHeader("Skills")}\n${skillList}`, 0, 0)); - this.chatContainer.addChild(new Spacer(1)); + const skillCompactList = formatCompactList(skills.map((skill) => skill.name)); + addLoadedSection("Skills", skillCompactList, skillList); } const templates = this.session.promptTemplates; @@ -1129,8 +1227,8 @@ export class InteractiveMode { return template ? `/${template.name}` : this.formatDisplayPath(item.path); }, }); - this.chatContainer.addChild(new Text(`${sectionHeader("Prompts")}\n${templateList}`, 0, 0)); - this.chatContainer.addChild(new Spacer(1)); + const promptCompactList = formatCompactList(templates.map((template) => `/${template.name}`)); + addLoadedSection("Prompts", promptCompactList, templateList); } if (extensions.length > 0) { @@ -1139,8 +1237,10 @@ export class InteractiveMode { formatPath: (item) => this.formatDisplayPath(item.path), formatPackagePath: (item) => this.getShortPath(item.path, item.sourceInfo), }); - this.chatContainer.addChild(new Text(`${sectionHeader("Extensions", "mdHeading")}\n${extList}`, 0, 0)); - this.chatContainer.addChild(new Spacer(1)); + const extensionCompactList = formatCompactList( + extensions.map((extension) => compactPath(extension.path, extension.sourceInfo)), + ); + addLoadedSection("Extensions", extensionCompactList, extList, "mdHeading"); } // Show loaded themes (excluding built-in) @@ -1157,8 +1257,12 @@ export class InteractiveMode { formatPath: (item) => this.formatDisplayPath(item.path), formatPackagePath: (item) => this.getShortPath(item.path, item.sourceInfo), }); - this.chatContainer.addChild(new Text(`${sectionHeader("Themes")}\n${themeList}`, 0, 0)); - this.chatContainer.addChild(new Spacer(1)); + const themeCompactList = formatCompactList( + customThemes.map( + (loadedTheme) => loadedTheme.name ?? compactPath(loadedTheme.sourcePath!, loadedTheme.sourceInfo), + ), + ); + addLoadedSection("Themes", themeCompactList, themeList); } } @@ -1607,6 +1711,9 @@ export class InteractiveMode { if (factory) { // Create and add custom header this.customHeader = factory(this.ui, theme); + if (isExpandable(this.customHeader)) { + this.customHeader.setExpanded(this.toolOutputExpanded); + } if (index !== -1) { this.headerContainer.children[index] = this.customHeader; } else { @@ -1616,6 +1723,9 @@ export class InteractiveMode { } else { // Restore built-in header this.customHeader = undefined; + if (isExpandable(this.builtInHeader)) { + this.builtInHeader.setExpanded(this.toolOutputExpanded); + } if (index !== -1) { this.headerContainer.children[index] = this.builtInHeader; } @@ -2168,12 +2278,12 @@ export class InteractiveMode { await this.handleModelCommand(searchTerm); return; } - if (text.startsWith("/export")) { + if (text === "/export" || text.startsWith("/export ")) { await this.handleExportCommand(text); this.editor.setText(""); return; } - if (text.startsWith("/import")) { + if (text === "/import" || text.startsWith("/import ")) { await this.handleImportCommand(text); this.editor.setText(""); return; @@ -3080,6 +3190,10 @@ export class InteractiveMode { private setToolsExpanded(expanded: boolean): void { this.toolOutputExpanded = expanded; + const activeHeader = this.customHeader ?? this.builtInHeader; + if (isExpandable(activeHeader)) { + activeHeader.setExpanded(expanded); + } for (const child of this.chatContainer.children) { if (isExpandable(child)) { child.setExpanded(expanded); @@ -4023,6 +4137,7 @@ export class InteractiveMode { private async showLoginDialog(providerId: string): Promise { const providerInfo = this.session.modelRegistry.authStorage.getOAuthProviders().find((p) => p.id === providerId); const providerName = providerInfo?.name || providerId; + const previousModel = this.session.model; // Providers that use callback servers (can paste redirect URL) const usesCallbackServer = providerInfo?.usesCallbackServer ?? false; @@ -4098,8 +4213,50 @@ export class InteractiveMode { // Success restoreEditor(); this.session.modelRegistry.refresh(); + + let selectedModel: Model | undefined; + let selectionError: string | undefined; + if (isUnknownModel(previousModel)) { + const availableModels = this.session.modelRegistry.getAvailable(); + const providerModels = availableModels.filter((model) => model.provider === providerId); + if (!hasDefaultModelProvider(providerId)) { + selectionError = `Logged in to ${providerName}, but no default model is configured for provider "${providerId}". Use /model to select a model.`; + } else if (providerModels.length === 0) { + selectionError = `Logged in to ${providerName}, but no models are available for that provider. Use /model to select a model.`; + } else { + const defaultModelId = defaultModelPerProvider[providerId]; + selectedModel = providerModels.find((model) => model.id === defaultModelId); + if (!selectedModel) { + selectionError = `Logged in to ${providerName}, but its default model "${defaultModelId}" is not available. Use /model to select a model.`; + } else { + try { + await this.session.setModel(selectedModel); + } catch (error: unknown) { + selectedModel = undefined; + const errorMessage = error instanceof Error ? error.message : String(error); + selectionError = `Logged in to ${providerName}, but selecting its default model failed: ${errorMessage}. Use /model to select a model.`; + } + } + } + } + await this.updateAvailableProviderCount(); - this.showStatus(`Logged in to ${providerName}. Credentials saved to ${getAuthPath()}`); + this.footer.invalidate(); + this.updateEditorBorderColor(); + if (selectedModel) { + this.showStatus( + `Logged in to ${providerName}. Selected ${selectedModel.id}. Credentials saved to ${getAuthPath()}`, + ); + void this.maybeWarnAboutAnthropicSubscriptionAuth(selectedModel); + this.checkDaxnutsEasterEgg(selectedModel); + } else { + this.showStatus(`Logged in to ${providerName}. Credentials saved to ${getAuthPath()}`); + if (selectionError) { + this.showError(selectionError); + } else { + void this.maybeWarnAboutAnthropicSubscriptionAuth(); + } + } } catch (error: unknown) { restoreEditor(); const errorMsg = error instanceof Error ? error.message : String(error); @@ -4152,6 +4309,10 @@ export class InteractiveMode { try { await this.session.reload(); this.keybindings.reload(); + const activeHeader = this.customHeader ?? this.builtInHeader; + if (isExpandable(activeHeader)) { + activeHeader.setExpanded(this.toolOutputExpanded); + } setRegisteredThemes(this.session.resourceLoader.getThemes().themes); this.hideThinkingBlock = this.settingsManager.getHideThinkingBlock(); const themeName = this.settingsManager.getTheme(); @@ -4192,8 +4353,7 @@ export class InteractiveMode { } private async handleExportCommand(text: string): Promise { - const parts = text.split(/\s+/); - const outputPath = parts.length > 1 ? parts[1] : undefined; + const outputPath = this.getPathCommandArgument(text, "/export"); try { if (outputPath?.endsWith(".jsonl")) { @@ -4208,13 +4368,41 @@ export class InteractiveMode { } } + private getPathCommandArgument(text: string, command: "/export" | "/import"): string | undefined { + if (text === command) { + return undefined; + } + if (!text.startsWith(`${command} `)) { + return undefined; + } + + const argsString = text.slice(command.length + 1).trimStart(); + if (!argsString) { + return undefined; + } + + const firstChar = argsString[0]; + if (firstChar === '"' || firstChar === "'") { + const closingQuoteIndex = argsString.indexOf(firstChar, 1); + if (closingQuoteIndex < 0) { + return undefined; + } + return argsString.slice(1, closingQuoteIndex); + } + + const firstWhitespaceIndex = argsString.search(/\s/); + if (firstWhitespaceIndex < 0) { + return argsString; + } + return argsString.slice(0, firstWhitespaceIndex); + } + private async handleImportCommand(text: string): Promise { - const parts = text.split(/\s+/); - if (parts.length < 2 || !parts[1]) { + const inputPath = this.getPathCommandArgument(text, "/import"); + if (!inputPath) { this.showError("Usage: /import "); return; } - const inputPath = parts[1]; const confirmed = await this.showExtensionConfirm("Import session", `Replace current session with ${inputPath}?`); if (!confirmed) { @@ -4253,6 +4441,10 @@ export class InteractiveMode { this.showStatus(`Session imported from: ${inputPath}`); return; } + if (error instanceof SessionImportFileNotFoundError) { + this.showError(`Failed to import session: ${error.message}`); + return; + } await this.handleFatalRuntimeError("Failed to import session", error); } } diff --git a/packages/coding-agent/test/interactive-mode-import-command.test.ts b/packages/coding-agent/test/interactive-mode-import-command.test.ts new file mode 100644 index 00000000..85575ec0 --- /dev/null +++ b/packages/coding-agent/test/interactive-mode-import-command.test.ts @@ -0,0 +1,144 @@ +import { describe, expect, it, vi } from "vitest"; +import { SessionImportFileNotFoundError } from "../src/core/agent-session-runtime.js"; +import { InteractiveMode } from "../src/modes/interactive/interactive-mode.js"; + +type PathCommand = "/export" | "/import"; + +type InteractiveModePrototype = { + getPathCommandArgument(this: unknown, text: string, command: PathCommand): string | undefined; + handleImportCommand(this: ImportCommandContext, text: string): Promise; +}; + +type ImportCommandContext = { + loadingAnimation?: { stop: () => void }; + statusContainer: { clear: () => void }; + runtimeHost: { importFromJsonl: (inputPath: string, cwdOverride?: string) => Promise<{ cancelled: boolean }> }; + showError: (message: string) => void; + showStatus: (message: string) => void; + showExtensionConfirm: (title: string, message: string) => Promise; + handleRuntimeSessionChange: () => Promise; + renderCurrentSessionState: () => void; + handleFatalRuntimeError: (prefix: string, error: unknown) => Promise; + promptForMissingSessionCwd: (error: unknown) => Promise; + getPathCommandArgument: (text: string, command: PathCommand) => string | undefined; +}; + +const interactiveModePrototype = InteractiveMode.prototype as unknown as InteractiveModePrototype; + +describe("InteractiveMode /import parsing", () => { + it("strips quotes from /import path arguments", () => { + expect(interactiveModePrototype.getPathCommandArgument('/import "path/to/session.jsonl"', "/import")).toBe( + "path/to/session.jsonl", + ); + expect( + interactiveModePrototype.getPathCommandArgument('/import "path with spaces/session.jsonl"', "/import"), + ).toBe("path with spaces/session.jsonl"); + }); + + it("preserves apostrophes in unquoted /import path arguments", () => { + expect(interactiveModePrototype.getPathCommandArgument("/import john's/session.jsonl", "/import")).toBe( + "john's/session.jsonl", + ); + }); + + it("enforces command token boundaries", () => { + expect(interactiveModePrototype.getPathCommandArgument("/important /tmp/session.jsonl", "/import")).toBe( + undefined, + ); + expect(interactiveModePrototype.getPathCommandArgument("/exporter out.html", "/export")).toBe(undefined); + expect(interactiveModePrototype.getPathCommandArgument("/import /tmp/session.jsonl", "/import")).toBe( + "/tmp/session.jsonl", + ); + }); + + it("passes unquoted path to runtimeHost.importFromJsonl", async () => { + const importFromJsonl = vi.fn(async () => ({ cancelled: false })); + const showExtensionConfirm = vi.fn(async () => true); + const showStatus = vi.fn(); + const showError = vi.fn(); + + const context: ImportCommandContext = { + statusContainer: { clear: vi.fn() }, + runtimeHost: { importFromJsonl }, + showError, + showStatus, + showExtensionConfirm, + handleRuntimeSessionChange: vi.fn(async () => {}), + renderCurrentSessionState: vi.fn(), + handleFatalRuntimeError: vi.fn(async () => { + throw new Error("unexpected fatal error"); + }), + promptForMissingSessionCwd: vi.fn(async () => undefined), + getPathCommandArgument: interactiveModePrototype.getPathCommandArgument, + }; + + await interactiveModePrototype.handleImportCommand.call(context, '/import "path/to/session.jsonl"'); + + expect(showExtensionConfirm).toHaveBeenCalledWith( + "Import session", + "Replace current session with path/to/session.jsonl?", + ); + expect(importFromJsonl).toHaveBeenCalledWith("path/to/session.jsonl"); + expect(showError).not.toHaveBeenCalled(); + expect(showStatus).toHaveBeenCalledWith("Session imported from: path/to/session.jsonl"); + }); + + it("passes unquoted apostrophe path to runtimeHost.importFromJsonl unchanged", async () => { + const importFromJsonl = vi.fn(async () => ({ cancelled: false })); + const showExtensionConfirm = vi.fn(async () => true); + const showStatus = vi.fn(); + const showError = vi.fn(); + + const context: ImportCommandContext = { + statusContainer: { clear: vi.fn() }, + runtimeHost: { importFromJsonl }, + showError, + showStatus, + showExtensionConfirm, + handleRuntimeSessionChange: vi.fn(async () => {}), + renderCurrentSessionState: vi.fn(), + handleFatalRuntimeError: vi.fn(async () => { + throw new Error("unexpected fatal error"); + }), + promptForMissingSessionCwd: vi.fn(async () => undefined), + getPathCommandArgument: interactiveModePrototype.getPathCommandArgument, + }; + + await interactiveModePrototype.handleImportCommand.call(context, "/import john's/session.jsonl"); + + expect(importFromJsonl).toHaveBeenCalledWith("john's/session.jsonl"); + expect(showError).not.toHaveBeenCalled(); + expect(showStatus).toHaveBeenCalledWith("Session imported from: john's/session.jsonl"); + }); + + it("shows a non-fatal error when /import path does not exist", async () => { + const importFromJsonl = vi.fn(async () => { + throw new SessionImportFileNotFoundError("/tmp/missing-session.jsonl"); + }); + const showExtensionConfirm = vi.fn(async () => true); + const showStatus = vi.fn(); + const showError = vi.fn(); + const handleFatalRuntimeError = vi.fn(async () => { + throw new Error("unexpected fatal error"); + }); + + const context: ImportCommandContext = { + statusContainer: { clear: vi.fn() }, + runtimeHost: { importFromJsonl }, + showError, + showStatus, + showExtensionConfirm, + handleRuntimeSessionChange: vi.fn(async () => {}), + renderCurrentSessionState: vi.fn(), + handleFatalRuntimeError, + promptForMissingSessionCwd: vi.fn(async () => undefined), + getPathCommandArgument: interactiveModePrototype.getPathCommandArgument, + }; + + await interactiveModePrototype.handleImportCommand.call(context, "/import /tmp/missing-session.jsonl"); + + expect(showError).toHaveBeenCalledWith("Failed to import session: File not found: /tmp/missing-session.jsonl"); + expect(showStatus).not.toHaveBeenCalled(); + expect(handleFatalRuntimeError).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/coding-agent/test/interactive-mode-status.test.ts b/packages/coding-agent/test/interactive-mode-status.test.ts index 3d148495..e2af115b 100644 --- a/packages/coding-agent/test/interactive-mode-status.test.ts +++ b/packages/coding-agent/test/interactive-mode-status.test.ts @@ -1,3 +1,5 @@ +import { homedir } from "node:os"; +import * as path from "node:path"; import { Container } from "@mariozechner/pi-tui"; import { beforeAll, describe, expect, test, vi } from "vitest"; import { InteractiveMode } from "../src/modes/interactive/interactive-mode.js"; @@ -60,6 +62,27 @@ describe("InteractiveMode.showStatus", () => { }); }); +describe("InteractiveMode.setToolsExpanded", () => { + test("applies expansion state to the active header and chat entries", () => { + const header = { setExpanded: vi.fn() }; + const chatChild = { setExpanded: vi.fn() }; + const fakeThis: any = { + toolOutputExpanded: false, + customHeader: undefined, + builtInHeader: header, + chatContainer: { children: [chatChild] }, + ui: { requestRender: vi.fn() }, + }; + + (InteractiveMode as any).prototype.setToolsExpanded.call(fakeThis, true); + + expect(fakeThis.toolOutputExpanded).toBe(true); + expect(header.setExpanded).toHaveBeenCalledWith(true); + expect(chatChild.setExpanded).toHaveBeenCalledWith(true); + expect(fakeThis.ui.requestRender).toHaveBeenCalledTimes(1); + }); +}); + describe("InteractiveMode.createExtensionUIContext setTheme", () => { test("persists theme changes to settings manager", () => { initTheme("dark"); @@ -116,31 +139,41 @@ describe("InteractiveMode.showLoadedResources", () => { function createShowLoadedResourcesThis(options: { quietStartup: boolean; verbose?: boolean; - skills?: Array<{ filePath: string }>; + toolOutputExpanded?: boolean; + cwd?: string; + contextFiles?: Array<{ path: string; content?: string }>; + extensions?: Array<{ path: string }>; + skills?: Array<{ filePath: string; name: string }>; skillDiagnostics?: Array<{ type: "warning" | "error" | "collision"; message: string }>; }) { const fakeThis: any = { options: { verbose: options.verbose ?? false }, + toolOutputExpanded: options.toolOutputExpanded ?? false, chatContainer: new Container(), settingsManager: { getQuietStartup: () => options.quietStartup, }, + sessionManager: { + getCwd: () => options.cwd ?? "/tmp/project", + }, session: { promptTemplates: [], extensionRunner: undefined, resourceLoader: { getPathMetadata: () => new Map(), - getAgentsFiles: () => ({ agentsFiles: [] }), + getAgentsFiles: () => ({ agentsFiles: options.contextFiles ?? [] }), getSkills: () => ({ skills: options.skills ?? [], diagnostics: options.skillDiagnostics ?? [], }), getPrompts: () => ({ prompts: [], diagnostics: [] }), - getExtensions: () => ({ extensions: [], errors: [], runtime: {} }), + getExtensions: () => ({ extensions: options.extensions ?? [], errors: [], runtime: {} }), getThemes: () => ({ themes: [], diagnostics: [] }), }, }, - formatDisplayPath: (p: string) => p, + formatDisplayPath: (p: string) => (InteractiveMode as any).prototype.formatDisplayPath.call(fakeThis, p), + formatContextPath: (p: string) => (InteractiveMode as any).prototype.formatContextPath.call(fakeThis, p), + getStartupExpansionState: () => (InteractiveMode as any).prototype.getStartupExpansionState.call(fakeThis), buildScopeGroups: () => [], formatScopeGroups: () => "resource-list", getShortPath: (p: string) => p, @@ -151,10 +184,117 @@ describe("InteractiveMode.showLoadedResources", () => { return fakeThis; } + test("shows a compact resource listing by default", () => { + const fakeThis = createShowLoadedResourcesThis({ + quietStartup: false, + skills: [{ filePath: "/tmp/skill/SKILL.md", name: "commit" }], + }); + + (InteractiveMode as any).prototype.showLoadedResources.call(fakeThis, { + force: false, + }); + + const output = renderAll(fakeThis.chatContainer); + expect(output).toContain("[Skills]"); + expect(output).toContain("commit"); + expect(output).not.toContain("resource-list"); + }); + + test("shows full resource listing when expanded", () => { + const fakeThis = createShowLoadedResourcesThis({ + quietStartup: false, + toolOutputExpanded: true, + skills: [{ filePath: "/tmp/skill/SKILL.md", name: "commit" }], + }); + + (InteractiveMode as any).prototype.showLoadedResources.call(fakeThis, { + force: false, + }); + + const output = renderAll(fakeThis.chatContainer); + expect(output).toContain("[Skills]"); + expect(output).toContain("resource-list"); + expect(output).not.toContain("commit"); + }); + + test("shows full resource listing on verbose startup even when tool output is collapsed", () => { + const fakeThis = createShowLoadedResourcesThis({ + quietStartup: true, + verbose: true, + toolOutputExpanded: false, + skills: [{ filePath: "/tmp/skill/SKILL.md", name: "commit" }], + }); + + (InteractiveMode as any).prototype.showLoadedResources.call(fakeThis, { + force: false, + }); + + const output = renderAll(fakeThis.chatContainer); + expect(output).toContain("[Skills]"); + expect(output).toContain("resource-list"); + expect(output).not.toContain("commit"); + }); + + test("abbreviates extensions in compact listing", () => { + const fakeThis = createShowLoadedResourcesThis({ + quietStartup: false, + extensions: [{ path: "/tmp/extensions/answer.ts" }, { path: "/tmp/extensions/btw.ts" }], + }); + + (InteractiveMode as any).prototype.showLoadedResources.call(fakeThis, { + force: false, + }); + + const output = renderAll(fakeThis.chatContainer); + expect(output).toContain("[Extensions]"); + expect(output).toContain("answer.ts, btw.ts"); + expect(output).not.toContain("extensions/answer.ts"); + }); + + test("shows context paths relative to cwd while preserving full external paths", () => { + const home = homedir(); + const cwd = path.join(home, "Development", "pi-mono"); + const fakeThis = createShowLoadedResourcesThis({ + quietStartup: false, + cwd, + contextFiles: [{ path: path.join(home, ".pi", "agent", "AGENTS.md") }, { path: path.join(cwd, "AGENTS.md") }], + }); + + (InteractiveMode as any).prototype.showLoadedResources.call(fakeThis, { + force: false, + }); + + const output = renderAll(fakeThis.chatContainer).replace(/\\/g, "/"); + expect(output).toContain("[Context]"); + expect(output).toContain("~/.pi/agent/AGENTS.md, AGENTS.md"); + expect(output).not.toContain(`${cwd.replace(/\\/g, "/")}/AGENTS.md`); + }); + + test("shows full context paths when expanded", () => { + const home = homedir(); + const cwd = path.join(home, "Development", "pi-mono"); + const fakeThis = createShowLoadedResourcesThis({ + quietStartup: false, + toolOutputExpanded: true, + cwd, + contextFiles: [{ path: path.join(home, ".pi", "agent", "AGENTS.md") }, { path: path.join(cwd, "AGENTS.md") }], + }); + + (InteractiveMode as any).prototype.showLoadedResources.call(fakeThis, { + force: false, + }); + + const output = renderAll(fakeThis.chatContainer).replace(/\\/g, "/"); + expect(output).toContain("[Context]"); + expect(output).toContain("~/.pi/agent/AGENTS.md"); + expect(output).toContain("~/Development/pi-mono/AGENTS.md"); + expect(output).not.toContain("~/.pi/agent/AGENTS.md, AGENTS.md"); + }); + test("does not show verbose listing on quiet startup during reload", () => { const fakeThis = createShowLoadedResourcesThis({ quietStartup: true, - skills: [{ filePath: "/tmp/skill/SKILL.md" }], + skills: [{ filePath: "/tmp/skill/SKILL.md", name: "commit" }], }); (InteractiveMode as any).prototype.showLoadedResources.call(fakeThis, { @@ -169,7 +309,7 @@ describe("InteractiveMode.showLoadedResources", () => { test("still shows diagnostics on quiet startup when requested", () => { const fakeThis = createShowLoadedResourcesThis({ quietStartup: true, - skills: [{ filePath: "/tmp/skill/SKILL.md" }], + skills: [{ filePath: "/tmp/skill/SKILL.md", name: "commit" }], skillDiagnostics: [{ type: "warning", message: "duplicate skill name" }], }); diff --git a/packages/coding-agent/test/prompt-templates.test.ts b/packages/coding-agent/test/prompt-templates.test.ts index 168f7407..bef43f44 100644 --- a/packages/coding-agent/test/prompt-templates.test.ts +++ b/packages/coding-agent/test/prompt-templates.test.ts @@ -8,8 +8,11 @@ * - Edge cases and integration between parsing and substitution */ -import { describe, expect, test } from "vitest"; -import { parseCommandArgs, substituteArgs } from "../src/core/prompt-templates.js"; +import { mkdirSync, rmSync, writeFileSync } from "fs"; +import { tmpdir } from "os"; +import { join } from "path"; +import { afterAll, describe, expect, test } from "vitest"; +import { loadPromptTemplates, parseCommandArgs, substituteArgs } from "../src/core/prompt-templates.js"; // ============================================================================ // substituteArgs @@ -379,3 +382,123 @@ describe("parseCommandArgs + substituteArgs integration", () => { expect(substituteArgs(template1, args)).toBe(substituteArgs(template2, args)); }); }); + +// ============================================================================ +// loadPromptTemplates - argument-hint frontmatter +// ============================================================================ + +describe("loadPromptTemplates - argument-hint", () => { + const testDir = join(tmpdir(), `pi-test-prompts-${Date.now()}`); + + function writeTemplate(name: string, content: string) { + mkdirSync(testDir, { recursive: true }); + writeFileSync(join(testDir, `${name}.md`), content); + } + + test("should parse required argument-hint from frontmatter", () => { + writeTemplate( + "pr", + `--- +description: Review PRs from URLs with structured issue and code analysis +argument-hint: "" +--- +You are given one or more GitHub PR URLs: $@`, + ); + + const templates = loadPromptTemplates({ + promptPaths: [testDir], + includeDefaults: false, + }); + + const pr = templates.find((t) => t.name === "pr"); + expect(pr).toBeDefined(); + expect(pr!.argumentHint).toBe(""); + expect(pr!.description).toBe("Review PRs from URLs with structured issue and code analysis"); + }); + + test("should parse optional argument-hint from frontmatter", () => { + writeTemplate( + "wr", + `--- +description: Finish the current task end-to-end with changelog, commit, and push +argument-hint: "[instructions]" +--- +Wrap it. Additional instructions: $ARGUMENTS`, + ); + + const templates = loadPromptTemplates({ + promptPaths: [testDir], + includeDefaults: false, + }); + + const wr = templates.find((t) => t.name === "wr"); + expect(wr).toBeDefined(); + expect(wr!.argumentHint).toBe("[instructions]"); + expect(wr!.description).toBe("Finish the current task end-to-end with changelog, commit, and push"); + }); + + test("should leave argumentHint undefined when not specified", () => { + writeTemplate( + "cl", + `--- +description: Audit changelog entries before release +--- +Audit changelog entries for all commits since the last release.`, + ); + + const templates = loadPromptTemplates({ + promptPaths: [testDir], + includeDefaults: false, + }); + + const cl = templates.find((t) => t.name === "cl"); + expect(cl).toBeDefined(); + expect(cl!.argumentHint).toBeUndefined(); + }); + + test("should ignore empty argument-hint", () => { + writeTemplate( + "empty-hint", + `--- +description: A command with empty hint +argument-hint: "" +--- +Do something`, + ); + + const templates = loadPromptTemplates({ + promptPaths: [testDir], + includeDefaults: false, + }); + + const tmpl = templates.find((t) => t.name === "empty-hint"); + expect(tmpl).toBeDefined(); + expect(tmpl!.argumentHint).toBeUndefined(); + }); + + test("should preserve argument-hint with special characters", () => { + writeTemplate( + "is", + `--- +description: Analyze GitHub issues (bugs or feature requests) +argument-hint: "" +--- +Analyze GitHub issue(s): $ARGUMENTS`, + ); + + const templates = loadPromptTemplates({ + promptPaths: [testDir], + includeDefaults: false, + }); + + const is = templates.find((t) => t.name === "is"); + expect(is).toBeDefined(); + expect(is!.argumentHint).toBe(""); + }); + + afterAll(() => { + try { + rmSync(testDir, { recursive: true, force: true }); + } catch {} + }); +}); diff --git a/packages/coding-agent/test/suite/harness.ts b/packages/coding-agent/test/suite/harness.ts index c1cdb81b..865b8780 100644 --- a/packages/coding-agent/test/suite/harness.ts +++ b/packages/coding-agent/test/suite/harness.ts @@ -142,6 +142,17 @@ export async function createHarness(options: HarnessOptions = {}): Promise { + const runner = extensionRunnerRef.current; + if (!runner?.hasHandlers("after_provider_response")) { + return; + } + await runner.emit({ + type: "after_provider_response", + status: response.status, + headers: response.headers, + }); + }, transformContext: async (messages: AgentMessage[]) => { const runner = extensionRunnerRef.current; if (!runner) return messages; diff --git a/packages/coding-agent/test/suite/regressions/3302-find-path-glob.test.ts b/packages/coding-agent/test/suite/regressions/3302-find-path-glob.test.ts new file mode 100644 index 00000000..0f19cb14 --- /dev/null +++ b/packages/coding-agent/test/suite/regressions/3302-find-path-glob.test.ts @@ -0,0 +1,72 @@ +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { createFindToolDefinition } from "../../../src/core/tools/find.js"; + +/** + * Regression test for https://github.com/badlogic/pi-mono/issues/3302 + * + * The `find` tool advertises glob patterns like `src/**\/*.spec.ts`, but the + * default fd-backed implementation used `fd --glob ` without + * `--full-path`, which makes fd match only against the basename. Any pattern + * containing a `/` therefore silently returned no matches. + * + * The fix switches fd into full-path mode when the pattern contains a `/` + * and prepends `**\/` so the pattern can match against the absolute candidate + * path that fd feeds to the matcher. + */ +describe("issue #3302 find returns no results for path-based glob patterns", () => { + let tempRoot: string; + + beforeEach(() => { + tempRoot = mkdtempSync(join(tmpdir(), "pi-3302-")); + mkdirSync(join(tempRoot, "some", "parent", "child"), { recursive: true }); + mkdirSync(join(tempRoot, "src", "foo", "bar"), { recursive: true }); + writeFileSync(join(tempRoot, "some", "parent", "child", "file.ext"), ""); + writeFileSync(join(tempRoot, "some", "parent", "child", "test.spec.ts"), ""); + writeFileSync(join(tempRoot, "src", "foo", "bar", "example.spec.ts"), ""); + }); + + afterEach(() => { + rmSync(tempRoot, { recursive: true, force: true }); + }); + + async function runFind(pattern: string): Promise { + const def = createFindToolDefinition(tempRoot); + // The find tool implementation does not touch ctx; pass a minimal stub. + const ctx = {} as Parameters[4]; + const result = (await def.execute("call-1", { pattern }, undefined, undefined, ctx)) as { + content: Array<{ type: string; text?: string }>; + }; + const text = result.content[0]?.text ?? ""; + if (text === "No files found matching pattern") return []; + return text + .split("\n") + .map((l) => l.trim()) + .filter((l) => l.length > 0 && !l.startsWith("[")); + } + + it("basename pattern still matches (regression-safe)", async () => { + const files = await runFind("*.spec.ts"); + expect(files.sort()).toEqual(["some/parent/child/test.spec.ts", "src/foo/bar/example.spec.ts"]); + }); + + it("directory-prefixed pattern with ** tail matches subtree", async () => { + const files = await runFind("some/parent/child/**"); + // Matches files (and possibly directories) under the subtree. Assert the two files are present. + expect(files).toContain("some/parent/child/file.ext"); + expect(files).toContain("some/parent/child/test.spec.ts"); + }); + + it("leading ** wildcard with path segments matches", async () => { + const files = await runFind("**/parent/child/*"); + expect(files.sort()).toContain("some/parent/child/file.ext"); + expect(files.sort()).toContain("some/parent/child/test.spec.ts"); + }); + + it("src/**/*.spec.ts matches nested spec file", async () => { + const files = await runFind("src/**/*.spec.ts"); + expect(files).toEqual(["src/foo/bar/example.spec.ts"]); + }); +}); diff --git a/packages/coding-agent/test/suite/regressions/3303-find-nested-gitignore.test.ts b/packages/coding-agent/test/suite/regressions/3303-find-nested-gitignore.test.ts new file mode 100644 index 00000000..15c8658a --- /dev/null +++ b/packages/coding-agent/test/suite/regressions/3303-find-nested-gitignore.test.ts @@ -0,0 +1,83 @@ +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { createFindToolDefinition } from "../../../src/core/tools/find.js"; + +/** + * Regression test for https://github.com/badlogic/pi-mono/issues/3303 + * + * The `find` tool previously collected every `.gitignore` under the search + * path and passed them to `fd` via `--ignore-file`. fd treats `--ignore-file` + * entries as a single global ignore source, so rules from `a/.gitignore` + * also filtered files under sibling `b/`. The fix switches to fd's + * hierarchical `.gitignore` handling via `--no-require-git` and drops the + * manual collection. + */ +describe("issue #3303 nested .gitignore rules leak into sibling directories", () => { + let tempRoot: string; + + async function runFind(pattern: string): Promise { + const def = createFindToolDefinition(tempRoot); + const ctx = {} as Parameters[4]; + const result = (await def.execute("call-1", { pattern }, undefined, undefined, ctx)) as { + content: Array<{ type: string; text?: string }>; + }; + const text = result.content[0]?.text ?? ""; + if (text === "No files found matching pattern") return []; + return text + .split("\n") + .map((l) => l.trim()) + .filter((l) => l.length > 0 && !l.startsWith("[")) + .sort(); + } + + afterEach(() => { + if (tempRoot) rmSync(tempRoot, { recursive: true, force: true }); + }); + + describe("flat sibling case", () => { + beforeEach(() => { + tempRoot = mkdtempSync(join(tmpdir(), "pi-3303-flat-")); + mkdirSync(join(tempRoot, "a"), { recursive: true }); + mkdirSync(join(tempRoot, "b"), { recursive: true }); + writeFileSync(join(tempRoot, "a", ".gitignore"), "ignored.txt\n"); + writeFileSync(join(tempRoot, "a", "ignored.txt"), ""); + writeFileSync(join(tempRoot, "a", "kept.txt"), ""); + writeFileSync(join(tempRoot, "b", "ignored.txt"), ""); + writeFileSync(join(tempRoot, "b", "kept.txt"), ""); + writeFileSync(join(tempRoot, "root.txt"), ""); + }); + + it("applies a/.gitignore only inside a/ and leaves b/ untouched", async () => { + const files = await runFind("**/*.txt"); + expect(files).toEqual(["a/kept.txt", "b/ignored.txt", "b/kept.txt", "root.txt"]); + }); + }); + + describe("deeply nested case", () => { + beforeEach(() => { + tempRoot = mkdtempSync(join(tmpdir(), "pi-3303-deep-")); + mkdirSync(join(tempRoot, "a", "deep"), { recursive: true }); + mkdirSync(join(tempRoot, "b"), { recursive: true }); + writeFileSync(join(tempRoot, "a", ".gitignore"), "ignored.txt\n"); + writeFileSync(join(tempRoot, "a", "deep", ".gitignore"), "secret.txt\n"); + writeFileSync(join(tempRoot, "a", "ignored.txt"), ""); + writeFileSync(join(tempRoot, "a", "kept.txt"), ""); + writeFileSync(join(tempRoot, "a", "deep", "ignored.txt"), ""); + writeFileSync(join(tempRoot, "a", "deep", "secret.txt"), ""); + writeFileSync(join(tempRoot, "a", "deep", "kept.txt"), ""); + writeFileSync(join(tempRoot, "b", "ignored.txt"), ""); + writeFileSync(join(tempRoot, "b", "kept.txt"), ""); + writeFileSync(join(tempRoot, "root.txt"), ""); + }); + + it("scopes each .gitignore to its own subtree", async () => { + const files = await runFind("**/*.txt"); + // a/.gitignore ignores 'ignored.txt' within a/ and a/deep/. + // a/deep/.gitignore additionally ignores 'secret.txt' within a/deep/. + // b/ is untouched by either. + expect(files).toEqual(["a/deep/kept.txt", "a/kept.txt", "b/ignored.txt", "b/kept.txt", "root.txt"]); + }); + }); +}); diff --git a/packages/coding-agent/test/tools.test.ts b/packages/coding-agent/test/tools.test.ts index 7e989839..5cb12279 100644 --- a/packages/coding-agent/test/tools.test.ts +++ b/packages/coding-agent/test/tools.test.ts @@ -557,6 +557,15 @@ describe("Coding Agent Tools", () => { expect(output).toContain("kept.txt"); expect(output).not.toContain("ignored.txt"); }); + + it("should surface fd glob parse errors", async () => { + await expect( + findTool.execute("test-call-15", { + pattern: "[", + path: testDir, + }), + ).rejects.toThrow(/error parsing glob|fd exited with code 1|fd error/i); + }); }); describe("ls tool", () => { diff --git a/packages/mom/CHANGELOG.md b/packages/mom/CHANGELOG.md index 3a637a7a..a03de420 100644 --- a/packages/mom/CHANGELOG.md +++ b/packages/mom/CHANGELOG.md @@ -2,6 +2,12 @@ ## [Unreleased] +## [0.67.6] - 2026-04-16 + +## [0.67.5] - 2026-04-16 + +## [0.67.4] - 2026-04-16 + ## [0.67.3] - 2026-04-15 ## [0.67.2] - 2026-04-14 diff --git a/packages/mom/package.json b/packages/mom/package.json index 351732de..0e8c6013 100644 --- a/packages/mom/package.json +++ b/packages/mom/package.json @@ -1,6 +1,6 @@ { "name": "@mariozechner/pi-mom", - "version": "0.67.3", + "version": "0.67.6", "description": "Slack bot that delegates messages to the pi coding agent", "type": "module", "bin": { @@ -20,9 +20,9 @@ }, "dependencies": { "@anthropic-ai/sandbox-runtime": "^0.0.16", - "@mariozechner/pi-agent-core": "^0.67.3", - "@mariozechner/pi-ai": "^0.67.3", - "@mariozechner/pi-coding-agent": "^0.67.3", + "@mariozechner/pi-agent-core": "^0.67.6", + "@mariozechner/pi-ai": "^0.67.6", + "@mariozechner/pi-coding-agent": "^0.67.6", "@sinclair/typebox": "^0.34.0", "@slack/socket-mode": "^2.0.0", "@slack/web-api": "^7.0.0", diff --git a/packages/pods/package.json b/packages/pods/package.json index 0910bbff..e25d1ad9 100644 --- a/packages/pods/package.json +++ b/packages/pods/package.json @@ -1,6 +1,6 @@ { "name": "@mariozechner/pi", - "version": "0.67.3", + "version": "0.67.6", "description": "CLI tool for managing vLLM deployments on GPU pods", "type": "module", "bin": { @@ -33,7 +33,7 @@ "node": ">=20.0.0" }, "dependencies": { - "@mariozechner/pi-agent-core": "^0.67.3", + "@mariozechner/pi-agent-core": "^0.67.6", "chalk": "^5.5.0" }, "devDependencies": {} diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md index d3c938bf..e16ada79 100644 --- a/packages/tui/CHANGELOG.md +++ b/packages/tui/CHANGELOG.md @@ -2,11 +2,28 @@ ## [Unreleased] -## [0.67.3] - 2026-04-15 +## [0.67.6] - 2026-04-16 + +### Added + +- Added OSC 8 hyperlink rendering for markdown links when the terminal advertises support. Introduces a public `hyperlink(text, url)` helper and a `setCapabilities()` test override in `packages/tui` ([#3248](https://github.com/badlogic/pi-mono/pull/3248) by [@ofa1](https://github.com/ofa1)). +- Added `argumentHint` to `SlashCommand` interface, displayed before the description in the autocomplete dropdown ([#2780](https://github.com/badlogic/pi-mono/pull/2780) by [@andresvi94](https://github.com/andresvi94)) + +### Changed + +- Tightened `detectCapabilities()` to default `hyperlinks: false` for unknown terminals and to force `hyperlinks: false` under tmux/screen (including nested sessions where the outer terminal would otherwise advertise OSC 8). Prevents markdown link URLs from disappearing on terminals that silently swallow OSC 8 sequences ([#3248](https://github.com/badlogic/pi-mono/pull/3248)). + +## [0.67.5] - 2026-04-16 ### Fixed -- Fixed Alt keybindings inside Zellij by skipping the Kitty keyboard protocol query there and enabling xterm `modifyOtherKeys` mode 2 directly ([#3163](https://github.com/badlogic/pi-mono/issues/3163)) +- Fixed Zellij `Shift+Enter` regressions by reverting the Zellij-specific Kitty keyboard query bypass and restoring the previous keyboard negotiation behavior ([#3259](https://github.com/badlogic/pi-mono/issues/3259)) + +## [0.67.4] - 2026-04-16 + +### Fixed + +- Fixed markdown strikethrough parsing to require strict double-tilde delimiters (`~~text~~`) with non-whitespace boundaries, preventing accidental strikethrough from loose tilde usage. ## [0.67.2] - 2026-04-14 diff --git a/packages/tui/package.json b/packages/tui/package.json index 355d2b45..8bc88115 100644 --- a/packages/tui/package.json +++ b/packages/tui/package.json @@ -1,6 +1,6 @@ { "name": "@mariozechner/pi-tui", - "version": "0.67.3", + "version": "0.67.6", "description": "Terminal User Interface library with differential rendering for efficient text-based applications", "type": "module", "main": "dist/index.js", diff --git a/packages/tui/src/autocomplete.ts b/packages/tui/src/autocomplete.ts index 792f119b..4ff1b6b7 100644 --- a/packages/tui/src/autocomplete.ts +++ b/packages/tui/src/autocomplete.ts @@ -223,6 +223,7 @@ type Awaitable = T | Promise; export interface SlashCommand { name: string; description?: string; + argumentHint?: string; // Function to get argument completions for this command // Returns null if no argument completion is available getArgumentCompletions?(argumentPrefix: string): Awaitable; @@ -303,11 +304,17 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider { if (spaceIndex === -1) { const prefix = textBeforeCursor.slice(1); - const commandItems = this.commands.map((cmd) => ({ - name: "name" in cmd ? cmd.name : cmd.value, - label: "name" in cmd ? cmd.name : cmd.label, - description: cmd.description, - })); + const commandItems = this.commands.map((cmd) => { + const name = "name" in cmd ? cmd.name : cmd.value; + const hint = "argumentHint" in cmd && cmd.argumentHint ? cmd.argumentHint : undefined; + const desc = cmd.description ?? ""; + const fullDesc = hint ? (desc ? `${hint} — ${desc}` : hint) : desc; + return { + name, + label: name, + description: fullDesc || undefined, + }; + }); const filtered = fuzzyFilter(commandItems, prefix, (item) => item.name).map((item) => ({ value: item.name, diff --git a/packages/tui/src/components/markdown.ts b/packages/tui/src/components/markdown.ts index 3cdf6373..7bdde68f 100644 --- a/packages/tui/src/components/markdown.ts +++ b/packages/tui/src/components/markdown.ts @@ -1,8 +1,32 @@ -import { marked, type Token } from "marked"; -import { isImageLine } from "../terminal-image.js"; +import { Marked, type Token, Tokenizer, type Tokens } from "marked"; +import { getCapabilities, hyperlink, isImageLine } from "../terminal-image.js"; import type { Component } from "../tui.js"; import { applyBackgroundToLine, visibleWidth, wrapTextWithAnsi } from "../utils.js"; +const STRICT_STRIKETHROUGH_REGEX = /^(~~)(?=[^\s~])((?:\\.|[^\\])*?(?:\\.|[^\s~\\]))\1(?=[^~]|$)/; + +class StrictStrikethroughTokenizer extends Tokenizer { + override del(src: string): Tokens.Del | undefined { + const match = STRICT_STRIKETHROUGH_REGEX.exec(src); + if (!match) { + return undefined; + } + + const text = match[2]; + return { + type: "del", + raw: match[0], + text, + tokens: this.lexer.inlineTokens(text), + }; + } +} + +const markdownParser = new Marked(); +markdownParser.setOptions({ + tokenizer: new StrictStrikethroughTokenizer(), +}); + /** * Default text styling for markdown content. * Applied to all text unless overridden by markdown formatting. @@ -112,7 +136,7 @@ export class Markdown implements Component { const normalizedText = this.text.replace(/\t/g, " "); // Parse markdown to HTML-like tokens - const tokens = marked.lexer(normalizedText); + const tokens = markdownParser.lexer(normalizedText); // Convert tokens to styled terminal output const renderedLines: string[] = []; @@ -464,18 +488,22 @@ export class Markdown implements Component { case "link": { const linkText = this.renderInlineTokens(token.tokens || [], resolvedStyleContext); - // If link text matches href, only show the link once - // Compare raw text (token.text) not styled text (linkText) since linkText has ANSI codes - // For mailto: links, strip the prefix before comparing (autolinked emails have - // text="foo@bar.com" but href="mailto:foo@bar.com") - const hrefForComparison = token.href.startsWith("mailto:") ? token.href.slice(7) : token.href; - if (token.text === token.href || token.text === hrefForComparison) { - result += this.theme.link(this.theme.underline(linkText)) + stylePrefix; + const styledLink = this.theme.link(this.theme.underline(linkText)); + if (getCapabilities().hyperlinks) { + // OSC 8: render as a clickable hyperlink. The URL is not printed inline, + // so we always show only the link text regardless of whether it matches href. + result += hyperlink(styledLink, token.href) + stylePrefix; } else { - result += - this.theme.link(this.theme.underline(linkText)) + - this.theme.linkUrl(` (${token.href})`) + - stylePrefix; + // Fallback: print URL in parentheses when text differs from href. + // Compare raw token.text (not styled) against href for the equality check. + // For mailto: links strip the prefix (autolinked emails use text="foo@bar.com" + // but href="mailto:foo@bar.com"). + const hrefForComparison = token.href.startsWith("mailto:") ? token.href.slice(7) : token.href; + if (token.text === token.href || token.text === hrefForComparison) { + result += styledLink + stylePrefix; + } else { + result += styledLink + this.theme.linkUrl(` (${token.href})`) + stylePrefix; + } } break; } diff --git a/packages/tui/src/index.ts b/packages/tui/src/index.ts index 025ecde4..aa3dd68b 100644 --- a/packages/tui/src/index.ts +++ b/packages/tui/src/index.ts @@ -78,12 +78,14 @@ export { getJpegDimensions, getPngDimensions, getWebpDimensions, + hyperlink, type ImageDimensions, type ImageProtocol, type ImageRenderOptions, imageFallback, renderImage, resetCapabilitiesCache, + setCapabilities, setCellDimensions, type TerminalCapabilities, } from "./terminal-image.js"; diff --git a/packages/tui/src/terminal-image.ts b/packages/tui/src/terminal-image.ts index e706fedc..b300d8cf 100644 --- a/packages/tui/src/terminal-image.ts +++ b/packages/tui/src/terminal-image.ts @@ -42,6 +42,16 @@ export function detectCapabilities(): TerminalCapabilities { const term = process.env.TERM?.toLowerCase() || ""; const colorTerm = process.env.COLORTERM?.toLowerCase() || ""; + // tmux and screen swallow OSC 8 by default (passthrough is opt-in and wraps + // sequences differently). Force hyperlinks off whenever we detect them, even + // when the outer terminal would otherwise support OSC 8. Image protocols are + // also unreliable under tmux/screen, so leave `images: null` for safety. + const inTmuxOrScreen = !!process.env.TMUX || term.startsWith("tmux") || term.startsWith("screen"); + if (inTmuxOrScreen) { + const trueColor = colorTerm === "truecolor" || colorTerm === "24bit"; + return { images: null, trueColor, hyperlinks: false }; + } + if (process.env.KITTY_WINDOW_ID || termProgram === "kitty") { return { images: "kitty", trueColor: true, hyperlinks: true }; } @@ -66,8 +76,12 @@ export function detectCapabilities(): TerminalCapabilities { return { images: null, trueColor: true, hyperlinks: true }; } + // Unknown terminal: be conservative. OSC 8 is rendered invisibly as "just + // text" on terminals that swallow it, which means the URL disappears from + // the rendered output. Default to the legacy `text (url)` behavior unless we + // have positively identified a hyperlink-capable terminal above. const trueColor = colorTerm === "truecolor" || colorTerm === "24bit"; - return { images: null, trueColor, hyperlinks: true }; + return { images: null, trueColor, hyperlinks: false }; } export function getCapabilities(): TerminalCapabilities { @@ -81,6 +95,11 @@ export function resetCapabilitiesCache(): void { cachedCapabilities = null; } +/** Override the cached capabilities. Useful in tests to exercise both code paths. */ +export function setCapabilities(caps: TerminalCapabilities): void { + cachedCapabilities = caps; +} + const KITTY_PREFIX = "\x1b_G"; const ITERM2_PREFIX = "\x1b]1337;File="; @@ -372,6 +391,20 @@ export function renderImage( return null; } +/** + * Wrap text in an OSC 8 hyperlink sequence. + * The text is rendered as a clickable hyperlink in terminals that support OSC 8 + * (Ghostty, Kitty, WezTerm, iTerm2, VSCode, and others). + * In terminals that do not support OSC 8, the escape sequences are ignored + * and only the plain text is displayed. + * + * @param text - The visible text to display + * @param url - The URL to link to + */ +export function hyperlink(text: string, url: string): string { + return `\x1b]8;;${url}\x1b\\${text}\x1b]8;;\x1b\\`; +} + export function imageFallback(mimeType: string, dimensions?: ImageDimensions, filename?: string): string { const parts: string[] = []; if (filename) parts.push(filename); diff --git a/packages/tui/src/terminal.ts b/packages/tui/src/terminal.ts index 1ef9b451..d98d1954 100644 --- a/packages/tui/src/terminal.ts +++ b/packages/tui/src/terminal.ts @@ -184,17 +184,6 @@ export class ProcessTerminal implements Terminal { private queryAndEnableKittyProtocol(): void { this.setupStdinBuffer(); process.stdin.on("data", this.stdinDataHandler!); - - // Zellij forwards the Kitty query to the outer terminal, which can make - // Pi enable its Kitty parser even though Zellij still sends Alt as - // legacy ESC-prefixed sequences. Skip the Kitty query there and use - // modifyOtherKeys directly instead. - if (process.env.ZELLIJ) { - process.stdout.write("\x1b[>4;2m"); - this._modifyOtherKeysActive = true; - return; - } - process.stdout.write("\x1b[?u"); setTimeout(() => { if (!this._kittyProtocolActive && !this._modifyOtherKeysActive) { diff --git a/packages/tui/src/utils.ts b/packages/tui/src/utils.ts index ad67efdd..7c4ea7c1 100644 --- a/packages/tui/src/utils.ts +++ b/packages/tui/src/utils.ts @@ -311,8 +311,16 @@ class AnsiCodeTracker { private strikethrough = false; private fgColor: string | null = null; // Stores the full code like "31" or "38;5;240" private bgColor: string | null = null; // Stores the full code like "41" or "48;5;240" + private activeHyperlink: string | null = null; // Active OSC 8 hyperlink URL, or null process(ansiCode: string): void { + // OSC 8 hyperlink: \x1b]8;;\x1b\\ (open) or \x1b]8;;\x1b\\ (close) + if (ansiCode.startsWith("\x1b]8;")) { + const m = ansiCode.match(/^\x1b\]8;[^;]*;([^\x1b\x07]*)/); + this.activeHyperlink = m?.[1] ? m[1] : null; + return; + } + if (!ansiCode.endsWith("m")) { return; } @@ -447,11 +455,13 @@ class AnsiCodeTracker { this.strikethrough = false; this.fgColor = null; this.bgColor = null; + // SGR reset does not affect OSC 8 hyperlink state } /** Clear all state for reuse. */ clear(): void { this.reset(); + this.activeHyperlink = null; } getActiveCodes(): string { @@ -467,8 +477,11 @@ class AnsiCodeTracker { if (this.fgColor) codes.push(this.fgColor); if (this.bgColor) codes.push(this.bgColor); - if (codes.length === 0) return ""; - return `\x1b[${codes.join(";")}m`; + let result = codes.length > 0 ? `\x1b[${codes.join(";")}m` : ""; + if (this.activeHyperlink) { + result += `\x1b]8;;${this.activeHyperlink}\x1b\\`; + } + return result; } hasActiveCodes(): boolean { @@ -482,22 +495,26 @@ class AnsiCodeTracker { this.hidden || this.strikethrough || this.fgColor !== null || - this.bgColor !== null + this.bgColor !== null || + this.activeHyperlink !== null ); } /** - * Get reset codes for attributes that need to be turned off at line end, - * specifically underline which bleeds into padding. - * Returns empty string if no problematic attributes are active. + * Get reset codes for attributes that need to be turned off at line end. + * Underline must be closed to prevent bleeding into padding. + * Active OSC 8 hyperlinks must be closed and re-opened on the next line. + * Returns empty string if no attributes need closing. */ getLineEndReset(): string { - // Only underline causes visual bleeding into padding - // Other attributes like colors don't visually bleed to padding + let result = ""; if (this.underline) { - return "\x1b[24m"; // Underline off only + result += "\x1b[24m"; // Underline off only } - return ""; + if (this.activeHyperlink) { + result += "\x1b]8;;\x1b\\"; // Close hyperlink; re-opened at line start via getActiveCodes() + } + return result; } } diff --git a/packages/tui/test/markdown.test.ts b/packages/tui/test/markdown.test.ts index e8f0d023..029a614e 100644 --- a/packages/tui/test/markdown.test.ts +++ b/packages/tui/test/markdown.test.ts @@ -1,8 +1,9 @@ import assert from "node:assert"; -import { describe, it } from "node:test"; +import { afterEach, describe, it } from "node:test"; import type { Terminal as XtermTerminalType } from "@xterm/headless"; import { Chalk } from "chalk"; import { Markdown } from "../src/components/markdown.js"; +import { resetCapabilitiesCache, setCapabilities } from "../src/terminal-image.js"; import { type Component, TUI } from "../src/tui.js"; import { defaultMarkdownTheme } from "./test-themes.js"; import { VirtualTerminal } from "./virtual-terminal.js"; @@ -325,6 +326,8 @@ describe("Markdown component", () => { }); it("should wrap long unbroken tokens inside table cells (not only at line start)", () => { + // Pin to no-hyperlinks so width checks work on plain text without OSC 8 sequences. + setCapabilities({ images: null, trueColor: false, hyperlinks: false }); const url = "https://example.com/this/is/a/very/long/url/that/should/wrap"; const markdown = new Markdown( `| Value | @@ -337,6 +340,7 @@ describe("Markdown component", () => { const width = 30; const lines = markdown.render(width); + resetCapabilitiesCache(); const plainLines = lines.map((line) => line.replace(/\x1b\[[0-9;]*m/g, "").trimEnd()); for (const line of plainLines) { @@ -1061,8 +1065,39 @@ bar`, }); }); + describe("Strikethrough syntax", () => { + it("should render ~~text~~ as strikethrough", () => { + const markdown = new Markdown("Use ~~strikethrough~~ here", 0, 0, defaultMarkdownTheme); + + const lines = markdown.render(80); + const joinedOutput = lines.join("\n"); + const joinedPlain = lines.map((line) => line.replace(/\x1b\[[0-9;]*m/g, "")).join(" "); + + assert.ok(joinedOutput.includes("\x1b[9m"), "Should apply strikethrough styling"); + assert.ok(joinedPlain.includes("strikethrough"), "Should include struck text content"); + assert.ok(!joinedPlain.includes("~~strikethrough~~"), "Should not render delimiters as text"); + }); + + it("should keep ~text~ as plain text", () => { + const markdown = new Markdown("Use ~strikethrough~ literally", 0, 0, defaultMarkdownTheme); + + const lines = markdown.render(80); + const joinedOutput = lines.join("\n"); + const joinedPlain = lines.map((line) => line.replace(/\x1b\[[0-9;]*m/g, "")).join(" "); + + assert.ok(joinedPlain.includes("~strikethrough~"), "Single-tilde delimiters should remain visible"); + assert.ok(!joinedOutput.includes("\x1b[9m"), "Single-tilde text should not use strikethrough styling"); + }); + }); + describe("Links", () => { + afterEach(() => { + resetCapabilitiesCache(); + }); + it("should not duplicate URL for autolinked emails", () => { + // Hyperlinks capability does not affect the mailto: display check. + setCapabilities({ images: null, trueColor: false, hyperlinks: false }); const markdown = new Markdown("Contact user@example.com for help", 0, 0, defaultMarkdownTheme); const lines = markdown.render(80); @@ -1075,6 +1110,7 @@ bar`, }); it("should not duplicate URL for bare URLs", () => { + setCapabilities({ images: null, trueColor: false, hyperlinks: false }); const markdown = new Markdown("Visit https://example.com for more", 0, 0, defaultMarkdownTheme); const lines = markdown.render(80); @@ -1086,29 +1122,79 @@ bar`, assert.strictEqual(urlCount, 1, "URL should appear exactly once"); }); - it("should show URL for explicit markdown links with different text", () => { + it("should show URL in parentheses when hyperlinks are not supported", () => { + setCapabilities({ images: null, trueColor: false, hyperlinks: false }); const markdown = new Markdown("[click here](https://example.com)", 0, 0, defaultMarkdownTheme); const lines = markdown.render(80); const plainLines = lines.map((line) => line.replace(/\x1b\[[0-9;]*m/g, "")); const joinedPlain = plainLines.join(" "); - // Should show both link text and URL assert.ok(joinedPlain.includes("click here"), "Should contain link text"); assert.ok(joinedPlain.includes("(https://example.com)"), "Should show URL in parentheses"); }); - it("should show URL for explicit mailto links with different text", () => { + it("should show mailto URL in parentheses when hyperlinks are not supported", () => { + setCapabilities({ images: null, trueColor: false, hyperlinks: false }); const markdown = new Markdown("[Email me](mailto:test@example.com)", 0, 0, defaultMarkdownTheme); const lines = markdown.render(80); const plainLines = lines.map((line) => line.replace(/\x1b\[[0-9;]*m/g, "")); const joinedPlain = plainLines.join(" "); - // Should show both link text and mailto URL assert.ok(joinedPlain.includes("Email me"), "Should contain link text"); assert.ok(joinedPlain.includes("(mailto:test@example.com)"), "Should show mailto URL in parentheses"); }); + + it("should emit OSC 8 hyperlink sequence when terminal supports hyperlinks", () => { + setCapabilities({ images: null, trueColor: false, hyperlinks: true }); + const markdown = new Markdown("[click here](https://example.com)", 0, 0, defaultMarkdownTheme); + + const lines = markdown.render(80); + const joined = lines.join(""); + + // OSC 8 open: ESC ] 8 ; ; ESC \ + assert.ok(joined.includes("\x1b]8;;https://example.com\x1b\\"), "Should contain OSC 8 open sequence"); + // OSC 8 close: ESC ] 8 ; ; ESC \ + assert.ok(joined.includes("\x1b]8;;\x1b\\"), "Should contain OSC 8 close sequence"); + // Visible text is present + const plainLines = lines.map((line) => line.replace(/\x1b[^a-zA-Z]*[a-zA-Z]|\x1b\].*?\x1b\\/g, "")); + assert.ok(plainLines.join("").includes("click here"), "Should contain link text"); + // URL is NOT printed inline as plain text + const rawPlain = lines.map((line) => + line.replace(/\x1b\]8;;[^\x1b]*\x1b\\/g, "").replace(/\x1b\[[0-9;]*m/g, ""), + ); + assert.ok(!rawPlain.join("").includes("(https://example.com)"), "URL should not appear inline in parentheses"); + }); + + it("should use OSC 8 for mailto links when terminal supports hyperlinks", () => { + setCapabilities({ images: null, trueColor: false, hyperlinks: true }); + const markdown = new Markdown("[Email me](mailto:test@example.com)", 0, 0, defaultMarkdownTheme); + + const lines = markdown.render(80); + const joined = lines.join(""); + + assert.ok( + joined.includes("\x1b]8;;mailto:test@example.com\x1b\\"), + "Should contain OSC 8 open with mailto URL", + ); + assert.ok(joined.includes("\x1b]8;;\x1b\\"), "Should contain OSC 8 close sequence"); + }); + + it("should use OSC 8 for bare URLs when terminal supports hyperlinks", () => { + setCapabilities({ images: null, trueColor: false, hyperlinks: true }); + const markdown = new Markdown("Visit https://example.com for more", 0, 0, defaultMarkdownTheme); + + const lines = markdown.render(80); + const joined = lines.join(""); + + assert.ok(joined.includes("\x1b]8;;https://example.com\x1b\\"), "Should contain OSC 8 hyperlink"); + // URL should not also appear as raw parenthetical text + const rawPlain = lines.map((line) => + line.replace(/\x1b\]8;;[^\x1b]*\x1b\\/g, "").replace(/\x1b\[[0-9;]*m/g, ""), + ); + assert.ok(!rawPlain.join("").includes("(https://example.com)"), "URL should not appear twice"); + }); }); describe("HTML-like tags in text", () => { diff --git a/packages/tui/test/terminal-image.test.ts b/packages/tui/test/terminal-image.test.ts index b573837c..dd67e6bf 100644 --- a/packages/tui/test/terminal-image.test.ts +++ b/packages/tui/test/terminal-image.test.ts @@ -4,7 +4,38 @@ import assert from "node:assert"; import { describe, it } from "node:test"; -import { isImageLine } from "../src/terminal-image.js"; +import { detectCapabilities, hyperlink, isImageLine } from "../src/terminal-image.js"; + +const ENV_KEYS = [ + "TERM", + "TERM_PROGRAM", + "COLORTERM", + "TMUX", + "KITTY_WINDOW_ID", + "GHOSTTY_RESOURCES_DIR", + "WEZTERM_PANE", + "ITERM_SESSION_ID", +] as const; + +function withEnv(overrides: Record, fn: () => void): void { + const saved: Record = {}; + for (const key of ENV_KEYS) { + saved[key] = process.env[key]; + delete process.env[key]; + } + try { + for (const [k, v] of Object.entries(overrides)) { + if (v === undefined) delete process.env[k]; + else process.env[k] = v; + } + fn(); + } finally { + for (const key of ENV_KEYS) { + if (saved[key] === undefined) delete process.env[key]; + else process.env[key] = saved[key]; + } + } +} describe("isImageLine", () => { describe("iTerm2 image protocol", () => { @@ -151,3 +182,98 @@ describe("isImageLine", () => { }); }); }); + +describe("detectCapabilities", () => { + it("defaults to hyperlinks: false for unknown terminals", () => { + withEnv({}, () => { + const caps = detectCapabilities(); + assert.strictEqual(caps.hyperlinks, false); + assert.strictEqual(caps.images, null); + }); + }); + + it("forces hyperlinks: false under tmux even if outer terminal supports OSC 8", () => { + withEnv({ TMUX: "/tmp/tmux-1000/default,1234,0", TERM_PROGRAM: "ghostty" }, () => { + const caps = detectCapabilities(); + assert.strictEqual(caps.hyperlinks, false); + assert.strictEqual(caps.images, null); + }); + }); + + it("forces hyperlinks: false when TERM starts with 'tmux'", () => { + withEnv({ TERM: "tmux-256color", TERM_PROGRAM: "iterm.app" }, () => { + const caps = detectCapabilities(); + assert.strictEqual(caps.hyperlinks, false); + assert.strictEqual(caps.images, null); + }); + }); + + it("forces hyperlinks: false when TERM starts with 'screen'", () => { + withEnv({ TERM: "screen-256color" }, () => { + const caps = detectCapabilities(); + assert.strictEqual(caps.hyperlinks, false); + assert.strictEqual(caps.images, null); + }); + }); + + it("enables hyperlinks for Ghostty", () => { + withEnv({ TERM_PROGRAM: "ghostty" }, () => { + const caps = detectCapabilities(); + assert.strictEqual(caps.hyperlinks, true); + }); + }); + + it("enables hyperlinks for Kitty", () => { + withEnv({ KITTY_WINDOW_ID: "1" }, () => { + const caps = detectCapabilities(); + assert.strictEqual(caps.hyperlinks, true); + }); + }); + + it("enables hyperlinks for WezTerm", () => { + withEnv({ WEZTERM_PANE: "0" }, () => { + const caps = detectCapabilities(); + assert.strictEqual(caps.hyperlinks, true); + }); + }); + + it("enables hyperlinks for iTerm2", () => { + withEnv({ TERM_PROGRAM: "iterm.app" }, () => { + const caps = detectCapabilities(); + assert.strictEqual(caps.hyperlinks, true); + }); + }); + + it("enables hyperlinks for VSCode", () => { + withEnv({ TERM_PROGRAM: "vscode" }, () => { + const caps = detectCapabilities(); + assert.strictEqual(caps.hyperlinks, true); + }); + }); +}); + +describe("hyperlink", () => { + it("wraps text in OSC 8 open and close sequences", () => { + const result = hyperlink("click me", "https://example.com"); + assert.strictEqual(result, "\x1b]8;;https://example.com\x1b\\click me\x1b]8;;\x1b\\"); + }); + + it("preserves ANSI styling inside the hyperlink", () => { + const styled = "\x1b[4m\x1b[34mclick me\x1b[0m"; + const result = hyperlink(styled, "https://example.com"); + assert.ok(result.startsWith("\x1b]8;;https://example.com\x1b\\")); + assert.ok(result.includes(styled)); + assert.ok(result.endsWith("\x1b]8;;\x1b\\")); + }); + + it("works with empty text", () => { + const result = hyperlink("", "https://example.com"); + assert.strictEqual(result, "\x1b]8;;https://example.com\x1b\\\x1b]8;;\x1b\\"); + }); + + it("works with file:// URIs", () => { + const result = hyperlink("README.md", "file:///home/user/README.md"); + assert.ok(result.includes("file:///home/user/README.md")); + assert.ok(result.includes("README.md")); + }); +}); diff --git a/packages/tui/test/terminal.test.ts b/packages/tui/test/terminal.test.ts deleted file mode 100644 index 77ecaf83..00000000 --- a/packages/tui/test/terminal.test.ts +++ /dev/null @@ -1,76 +0,0 @@ -import assert from "node:assert"; -import { describe, it } from "node:test"; -import { ProcessTerminal } from "../src/terminal.js"; - -function withEnv(name: string, value: string | undefined, fn: () => void): void { - const previous = process.env[name]; - if (value === undefined) delete process.env[name]; - else process.env[name] = value; - try { - fn(); - } finally { - if (previous === undefined) delete process.env[name]; - else process.env[name] = previous; - } -} - -describe("ProcessTerminal", () => { - it("should skip the Kitty query inside Zellij and enable modifyOtherKeys immediately", () => { - const terminal = new ProcessTerminal(); - const writes: string[] = []; - const stdinOnCalls: Array<{ event: string | symbol; listener: (...args: unknown[]) => void }> = []; - const stdinRemoveCalls: Array<{ event: string | symbol; listener: (...args: unknown[]) => void }> = []; - const stdoutRemoveCalls: Array<{ event: string | symbol; listener: (...args: unknown[]) => void }> = []; - - const originalStdoutWrite = process.stdout.write; - const originalStdinOn = process.stdin.on; - const originalStdinRemoveListener = process.stdin.removeListener; - const originalStdinPause = process.stdin.pause; - const originalStdoutRemoveListener = process.stdout.removeListener; - - process.stdout.write = ((chunk: string | Uint8Array) => { - writes.push(typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8")); - return true; - }) as typeof process.stdout.write; - process.stdin.on = ((event: string | symbol, listener: (...args: unknown[]) => void) => { - stdinOnCalls.push({ event, listener }); - return process.stdin; - }) as typeof process.stdin.on; - process.stdin.removeListener = ((event: string | symbol, listener: (...args: unknown[]) => void) => { - stdinRemoveCalls.push({ event, listener }); - return process.stdin; - }) as typeof process.stdin.removeListener; - process.stdin.pause = (() => process.stdin) as typeof process.stdin.pause; - process.stdout.removeListener = ((event: string | symbol, listener: (...args: unknown[]) => void) => { - stdoutRemoveCalls.push({ event, listener }); - return process.stdout; - }) as typeof process.stdout.removeListener; - - try { - withEnv("ZELLIJ", "1", () => { - ( - terminal as unknown as { - queryAndEnableKittyProtocol(): void; - } - ).queryAndEnableKittyProtocol(); - }); - - assert.deepStrictEqual(writes, ["\x1b[>4;2m"]); - assert.strictEqual(stdinOnCalls.length, 1); - assert.strictEqual(stdinOnCalls[0]?.event, "data"); - - terminal.stop(); - - assert.deepStrictEqual(writes, ["\x1b[>4;2m", "\x1b[?2004l", "\x1b[>4;0m"]); - assert.strictEqual(stdinRemoveCalls.length, 1); - assert.strictEqual(stdinRemoveCalls[0]?.event, "data"); - assert.strictEqual(stdoutRemoveCalls.length, 0); - } finally { - process.stdout.write = originalStdoutWrite; - process.stdin.on = originalStdinOn; - process.stdin.removeListener = originalStdinRemoveListener; - process.stdin.pause = originalStdinPause; - process.stdout.removeListener = originalStdoutRemoveListener; - } - }); -}); diff --git a/packages/tui/test/wrap-ansi.test.ts b/packages/tui/test/wrap-ansi.test.ts index 9fcc89ce..e0944ded 100644 --- a/packages/tui/test/wrap-ansi.test.ts +++ b/packages/tui/test/wrap-ansi.test.ts @@ -150,3 +150,58 @@ describe("wrapTextWithAnsi", () => { }); }); }); + +describe("wrapTextWithAnsi with OSC 8 hyperlinks", () => { + it("re-emits OSC 8 open at the start of continuation lines", () => { + // A hyperlink whose text is long enough to wrap + const url = "https://example.com"; + // OSC 8 open + text that is 10 visible chars + OSC 8 close + const input = `\x1b]8;;${url}\x1b\\0123456789\x1b]8;;\x1b\\`; + const lines = wrapTextWithAnsi(input, 6); + + // Every line that contains visible text from inside the hyperlink + // should start with the OSC 8 open sequence (or be preceded by it). + for (const line of lines) { + // If the line has visible content it must begin with the OSC 8 re-open + // OR it is the line where the close appeared with no following content. + const stripped = line.replace(/\x1b\]8;;[^\x1b\x07]*\x1b\\/g, "").replace(/\x1b\[[0-9;]*m/g, ""); + if (stripped.trim().length > 0) { + assert.ok( + line.startsWith(`\x1b]8;;${url}\x1b\\`) || line.includes(`\x1b]8;;${url}\x1b\\`), + `Line "${line}" has visible text but no OSC 8 re-open`, + ); + } + } + }); + + it("closes OSC 8 before each line break", () => { + const url = "https://example.com"; + const input = `\x1b]8;;${url}\x1b\\0123456789\x1b]8;;\x1b\\`; + const lines = wrapTextWithAnsi(input, 6); + + for (let i = 0; i < lines.length - 1; i++) { + const line = lines[i]; + // Every non-final line that is inside a hyperlink should end with the close + if (line.includes(`\x1b]8;;${url}\x1b\\`)) { + assert.ok( + line.endsWith("\x1b]8;;\x1b\\"), + `Non-final line "${line}" is inside a hyperlink but does not close it`, + ); + } + } + }); + + it("does not emit OSC 8 sequences on lines that are outside the hyperlink", () => { + const url = "https://example.com"; + const input = `before \x1b]8;;${url}\x1b\\link\x1b]8;;\x1b\\ after`; + const lines = wrapTextWithAnsi(input, 80); + + // With width 80 everything fits on one line; there should be exactly one + // OSC 8 open and one OSC 8 close. + assert.strictEqual(lines.length, 1); + const openCount = (lines[0].match(/\x1b\]8;;https:[^\x1b]+\x1b\\/g) ?? []).length; + const closeCount = (lines[0].match(/\x1b\]8;;\x1b\\/g) ?? []).length; + assert.strictEqual(openCount, 1); + assert.strictEqual(closeCount, 1); + }); +}); diff --git a/packages/web-ui/CHANGELOG.md b/packages/web-ui/CHANGELOG.md index 4bbc697e..8d9ada70 100644 --- a/packages/web-ui/CHANGELOG.md +++ b/packages/web-ui/CHANGELOG.md @@ -2,6 +2,12 @@ ## [Unreleased] +## [0.67.6] - 2026-04-16 + +## [0.67.5] - 2026-04-16 + +## [0.67.4] - 2026-04-16 + ## [0.67.3] - 2026-04-15 ## [0.67.2] - 2026-04-14 diff --git a/packages/web-ui/example/package.json b/packages/web-ui/example/package.json index 26df3602..f3c97350 100644 --- a/packages/web-ui/example/package.json +++ b/packages/web-ui/example/package.json @@ -1,6 +1,6 @@ { "name": "pi-web-ui-example", - "version": "1.55.3", + "version": "1.55.6", "private": true, "type": "module", "scripts": { diff --git a/packages/web-ui/package.json b/packages/web-ui/package.json index 3aa5a114..67591ede 100644 --- a/packages/web-ui/package.json +++ b/packages/web-ui/package.json @@ -1,6 +1,6 @@ { "name": "@mariozechner/pi-web-ui", - "version": "0.67.3", + "version": "0.67.6", "description": "Reusable web UI components for AI chat interfaces powered by @mariozechner/pi-ai", "type": "module", "main": "dist/index.js", @@ -18,8 +18,8 @@ }, "dependencies": { "@lmstudio/sdk": "^1.5.0", - "@mariozechner/pi-ai": "^0.67.3", - "@mariozechner/pi-tui": "^0.67.3", + "@mariozechner/pi-ai": "^0.67.6", + "@mariozechner/pi-tui": "^0.67.6", "docx-preview": "^0.3.7", "jszip": "^3.10.1", "lucide": "^0.544.0",