Merge remote-tracking branch 'upstream/main' into feat/image-outputs
This commit is contained in:
2
.github/APPROVED_CONTRIBUTORS
vendored
2
.github/APPROVED_CONTRIBUTORS
vendored
@@ -198,3 +198,5 @@ louis030195 pr
|
||||
technocidal pr
|
||||
|
||||
pandada8 pr
|
||||
|
||||
npupko issue
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -16,6 +16,7 @@ packages/*/dist-firefox/
|
||||
.vscode/
|
||||
.zed/
|
||||
.idea/
|
||||
.claude/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
413
package-lock.json
generated
413
package-lock.json
generated
@@ -16,7 +16,6 @@
|
||||
"packages/coding-agent/examples/extensions/sandbox"
|
||||
],
|
||||
"dependencies": {
|
||||
"@mariozechner/jiti": "^2.6.5",
|
||||
"@mariozechner/pi-coding-agent": "^0.30.2",
|
||||
"get-east-asian-width": "^1.4.0"
|
||||
},
|
||||
@@ -27,6 +26,7 @@
|
||||
"@typescript/native-preview": "7.0.0-dev.20260120.1",
|
||||
"concurrently": "^9.2.1",
|
||||
"husky": "^9.1.7",
|
||||
"jiti": "^2.7.0",
|
||||
"shx": "^0.4.0",
|
||||
"tsx": "^4.20.3",
|
||||
"typescript": "^5.9.2"
|
||||
@@ -55,15 +55,6 @@
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@anthropic-ai/sandbox-runtime/node_modules/commander": {
|
||||
"version": "12.1.0",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz",
|
||||
"integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@anthropic-ai/sandbox-runtime/node_modules/zod": {
|
||||
"version": "3.25.76",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
|
||||
@@ -233,9 +224,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/client-bedrock-runtime": {
|
||||
"version": "3.1041.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/client-bedrock-runtime/-/client-bedrock-runtime-3.1041.0.tgz",
|
||||
"integrity": "sha512-1QehYO3jhdvNQ5mOKtwIiNV04y4aywaNZw9HzCp7SSYCX4yy+AGXc2hhYjCiMDUvQPIELuvbR8MXw81NGAj8ZQ==",
|
||||
"version": "3.1044.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/client-bedrock-runtime/-/client-bedrock-runtime-3.1044.0.tgz",
|
||||
"integrity": "sha512-uwJB0pVZIey+kYag8xeUirMvuaDhhEHuYgSsapOKT5XCAije5pLNlg160eECJdhX9JEf0IthIM3IiHKSEkFtMw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-crypto/sha256-browser": "5.2.0",
|
||||
@@ -250,7 +241,7 @@
|
||||
"@aws-sdk/middleware-user-agent": "^3.972.38",
|
||||
"@aws-sdk/middleware-websocket": "^3.972.16",
|
||||
"@aws-sdk/region-config-resolver": "^3.972.13",
|
||||
"@aws-sdk/token-providers": "3.1041.0",
|
||||
"@aws-sdk/token-providers": "3.1044.0",
|
||||
"@aws-sdk/types": "^3.973.8",
|
||||
"@aws-sdk/util-endpoints": "^3.996.8",
|
||||
"@aws-sdk/util-user-agent-browser": "^3.972.10",
|
||||
@@ -455,6 +446,24 @@
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers": {
|
||||
"version": "3.1041.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1041.0.tgz",
|
||||
"integrity": "sha512-Th7kPI6YPtvJUcdznooXJMy+9rQWjmEF81LxaJssngBzuysK4a/x+l8kjm1zb7nYsUPbndnBdUnwng/3PLvtGw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/core": "^3.974.8",
|
||||
"@aws-sdk/nested-clients": "^3.997.6",
|
||||
"@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": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/credential-provider-web-identity": {
|
||||
"version": "3.972.38",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.38.tgz",
|
||||
@@ -699,9 +708,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/token-providers": {
|
||||
"version": "3.1041.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1041.0.tgz",
|
||||
"integrity": "sha512-Th7kPI6YPtvJUcdznooXJMy+9rQWjmEF81LxaJssngBzuysK4a/x+l8kjm1zb7nYsUPbndnBdUnwng/3PLvtGw==",
|
||||
"version": "3.1044.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1044.0.tgz",
|
||||
"integrity": "sha512-uvpWTfpzOM9qVrR0kdAjzBbvv2ERW7S1CKeBeRkHyHy21Ext5fNReIVrbAzhjuVC7UxRyZQ8PHYW/3T83hWlbg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/core": "^3.974.8",
|
||||
@@ -1444,9 +1453,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@google/genai": {
|
||||
"version": "1.51.0",
|
||||
"resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.51.0.tgz",
|
||||
"integrity": "sha512-vTZZF3CSimN7cn2zsLpW2p5WF0eZa5Gz69ITMPCNHpPrDlAstOfGifSfi0p/s9Z9400f7xJRkgvkQNrcM7pJ6w==",
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.52.0.tgz",
|
||||
"integrity": "sha512-gwSvbpiN/17O9TbsqSsE/OzZcpv5Fo4RQjdngGgogtuB9RsyJ8ZHhX5KjHj1bp5N9snN2eK8LDGXSaWW2hof8Q==",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
@@ -1765,19 +1774,6 @@
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@mariozechner/jiti": {
|
||||
"version": "2.6.5",
|
||||
"resolved": "https://registry.npmjs.org/@mariozechner/jiti/-/jiti-2.6.5.tgz",
|
||||
"integrity": "sha512-faGUlTcXka5l7rv0lP3K3vGW/ejRuOS24RR2aSFWREUQqzjgdsuWNo/IiPqL3kWRGt6Ahl2+qcDAwtdeWeuGUw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"std-env": "^3.10.0",
|
||||
"yoctocolors": "^2.1.2"
|
||||
},
|
||||
"bin": {
|
||||
"jiti": "lib/jiti-cli.mjs"
|
||||
}
|
||||
},
|
||||
"node_modules/@mariozechner/mini-lit": {
|
||||
"version": "0.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@mariozechner/mini-lit/-/mini-lit-0.2.1.tgz",
|
||||
@@ -2554,9 +2550,9 @@
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||
"version": "4.60.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.2.tgz",
|
||||
"integrity": "sha512-dnlp69efPPg6Uaw2dVqzWRfAWRnYVb1XJ8CyyhIbZeaq4CA5/mLeZ1IEt9QqQxmbdvagjLIm2ZL8BxXv5lH4Yw==",
|
||||
"version": "4.60.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.3.tgz",
|
||||
"integrity": "sha512-x35CNW/ANXG3hE/EZpRU8MXX1JDN86hBb2wMGAtltkz7pc6cxgjpy1OMMfDosOQ+2hWqIkag/fGok1Yady9nGw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -2567,9 +2563,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm64": {
|
||||
"version": "4.60.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.2.tgz",
|
||||
"integrity": "sha512-OqZTwDRDchGRHHm/hwLOL7uVPB9aUvI0am/eQuWMNyFHf5PSEQmyEeYYheA0EPPKUO/l0uigCp+iaTjoLjVoHg==",
|
||||
"version": "4.60.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.3.tgz",
|
||||
"integrity": "sha512-xw3xtkDApIOGayehp2+Rz4zimfkaX65r4t47iy+ymQB2G4iJCBBfj0ogVg5jpvjpn8UWn/+q9tprxleYeNp3Hw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -2580,9 +2576,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-arm64": {
|
||||
"version": "4.60.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.2.tgz",
|
||||
"integrity": "sha512-UwRE7CGpvSVEQS8gUMBe1uADWjNnVgP3Iusyda1nSRwNDCsRjnGc7w6El6WLQsXmZTbLZx9cecegumcitNfpmA==",
|
||||
"version": "4.60.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.3.tgz",
|
||||
"integrity": "sha512-vo6Y5Qfpx7/5EaamIwi0WqW2+zfiusVihKatLvtN1VFVy3D13uERk/6gZLU1UiHRL6fDXqj/ELIeVRGnvcTE1g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -2593,9 +2589,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-x64": {
|
||||
"version": "4.60.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.2.tgz",
|
||||
"integrity": "sha512-gjEtURKLCC5VXm1I+2i1u9OhxFsKAQJKTVB8WvDAHF+oZlq0GTVFOlTlO1q3AlCTE/DF32c16ESvfgqR7343/g==",
|
||||
"version": "4.60.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.3.tgz",
|
||||
"integrity": "sha512-D+0QGcZhBzTN82weOnsSlY7V7+RMmPuF1CkbxyMAGE8+ZHeUjyb76ZiWmBlCu//AQQONvxcqRbwZTajZKqjuOw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -2606,9 +2602,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-freebsd-arm64": {
|
||||
"version": "4.60.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.2.tgz",
|
||||
"integrity": "sha512-Bcl6CYDeAgE70cqZaMojOi/eK63h5Me97ZqAQoh77VPjMysA/4ORQBRGo3rRy45x4MzVlU9uZxs8Uwy7ZaKnBw==",
|
||||
"version": "4.60.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.3.tgz",
|
||||
"integrity": "sha512-6HnvHCT7fDyj6R0Ph7A6x8dQS/S38MClRWeDLqc0MdfWkxjiu1HSDYrdPhqSILzjTIC/pnXbbJbo+ft+gy/9hQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -2619,9 +2615,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-freebsd-x64": {
|
||||
"version": "4.60.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.2.tgz",
|
||||
"integrity": "sha512-LU+TPda3mAE2QB0/Hp5VyeKJivpC6+tlOXd1VMoXV/YFMvk/MNk5iXeBfB4MQGRWyOYVJ01625vjkr0Az98OJQ==",
|
||||
"version": "4.60.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.3.tgz",
|
||||
"integrity": "sha512-KHLgC3WKlUYW3ShFKnnosZDOJ0xjg9zp7au3sIm2bs/tGBeC2ipmvRh/N7JKi0t9Ue20C0dpEshi8WUubg+cnA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -2632,9 +2628,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
||||
"version": "4.60.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.2.tgz",
|
||||
"integrity": "sha512-2QxQrM+KQ7DAW4o22j+XZ6RKdxjLD7BOWTP0Bv0tmjdyhXSsr2Ul1oJDQqh9Zf5qOwTuTc7Ek83mOFaKnodPjg==",
|
||||
"version": "4.60.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.3.tgz",
|
||||
"integrity": "sha512-DV6fJoxEYWJOvaZIsok7KrYl0tPvga5OZ2yvKHNNYyk/2roMLqQAbGhr78EQ5YhHpnhLKJD3S1WFusAkmUuV5g==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -2645,9 +2641,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
|
||||
"version": "4.60.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.2.tgz",
|
||||
"integrity": "sha512-TbziEu2DVsTEOPif2mKWkMeDMLoYjx95oESa9fkQQK7r/Orta0gnkcDpzwufEcAO2BLBsD7mZkXGFqEdMRRwfw==",
|
||||
"version": "4.60.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.3.tgz",
|
||||
"integrity": "sha512-mQKoJAzvuOs6F+TZybQO4GOTSMUu7v0WdxEk24krQ/uUxXoPTtHjuaUuPmFhtBcM4K0ons8nrE3JyhTuCFtT/w==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -2658,9 +2654,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
||||
"version": "4.60.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.2.tgz",
|
||||
"integrity": "sha512-bO/rVDiDUuM2YfuCUwZ1t1cP+/yqjqz+Xf2VtkdppefuOFS2OSeAfgafaHNkFn0t02hEyXngZkxtGqXcXwO8Rg==",
|
||||
"version": "4.60.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.3.tgz",
|
||||
"integrity": "sha512-Whjj2qoiJ6+OOJMGptTYazaJvjOJm+iKHpXQM1P3LzGjt7Ff++Tp7nH4N8J/BUA7R9IHfDyx4DJIflifwnbmIA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -2671,9 +2667,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
||||
"version": "4.60.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.2.tgz",
|
||||
"integrity": "sha512-hr26p7e93Rl0Za+JwW7EAnwAvKkehh12BU1Llm9Ykiibg4uIr2rbpxG9WCf56GuvidlTG9KiiQT/TXT1yAWxTA==",
|
||||
"version": "4.60.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.3.tgz",
|
||||
"integrity": "sha512-4YTNHKqGng5+yiZt3mg77nmyuCfmNfX4fPmyUapBcIk+BdwSwmCWGXOUxhXbBEkFHtoN5boLj/5NON+u5QC9tg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -2684,9 +2680,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-loong64-gnu": {
|
||||
"version": "4.60.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.2.tgz",
|
||||
"integrity": "sha512-pOjB/uSIyDt+ow3k/RcLvUAOGpysT2phDn7TTUB3n75SlIgZzM6NKAqlErPhoFU+npgY3/n+2HYIQVbF70P9/A==",
|
||||
"version": "4.60.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.3.tgz",
|
||||
"integrity": "sha512-SU3kNlhkpI4UqlUc2VXPGK9o886ZsSeGfMAX2ba2b8DKmMXq4AL7KUrkSWVbb7koVqx41Yczx6dx5PNargIrEA==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
@@ -2697,9 +2693,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-loong64-musl": {
|
||||
"version": "4.60.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.2.tgz",
|
||||
"integrity": "sha512-2/w+q8jszv9Ww1c+6uJT3OwqhdmGP2/4T17cu8WuwyUuuaCDDJ2ojdyYwZzCxx0GcsZBhzi3HmH+J5pZNXnd+Q==",
|
||||
"version": "4.60.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.3.tgz",
|
||||
"integrity": "sha512-6lDLl5h4TXpB1mTf2rQWnAk/LcXrx9vBfu/DT5TIPhvMhRWaZ5MxkIc8u4lJAmBo6klTe1ywXIUHFjylW505sg==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
@@ -2710,9 +2706,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
|
||||
"version": "4.60.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.2.tgz",
|
||||
"integrity": "sha512-11+aL5vKheYgczxtPVVRhdptAM2H7fcDR5Gw4/bTcteuZBlH4oP9f5s9zYO9aGZvoGeBpqXI/9TZZihZ609wKw==",
|
||||
"version": "4.60.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.3.tgz",
|
||||
"integrity": "sha512-BMo8bOw8evlup/8G+cj5xWtPyp93xPdyoSN16Zy90Q2QZ0ZYRhCt6ZJSwbrRzG9HApFabjwj2p25TUPDWrhzqQ==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
@@ -2723,9 +2719,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-ppc64-musl": {
|
||||
"version": "4.60.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.2.tgz",
|
||||
"integrity": "sha512-i16fokAGK46IVZuV8LIIwMdtqhin9hfYkCh8pf8iC3QU3LpwL+1FSFGej+O7l3E/AoknL6Dclh2oTdnRMpTzFQ==",
|
||||
"version": "4.60.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.3.tgz",
|
||||
"integrity": "sha512-E0L8X1dZN1/Rph+5VPF6Xj2G7JJvMACVXtamTJIDrVI44Y3K+G8gQaMEAavbqCGTa16InptiVrX6eM6pmJ+7qA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
@@ -2736,9 +2732,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
||||
"version": "4.60.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.2.tgz",
|
||||
"integrity": "sha512-49FkKS6RGQoriDSK/6E2GkAsAuU5kETFCh7pG4yD/ylj9rKhTmO3elsnmBvRD4PgJPds5W2PkhC82aVwmUcJ7A==",
|
||||
"version": "4.60.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.3.tgz",
|
||||
"integrity": "sha512-oZJ/WHaVfHUiRAtmTAeo3DcevNsVvH8mbvodjZy7D5QKvCefO371SiKRpxoDcCxB3PTRTLayWBkvmDQKTcX/sw==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
@@ -2749,9 +2745,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-musl": {
|
||||
"version": "4.60.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.2.tgz",
|
||||
"integrity": "sha512-mjYNkHPfGpUR00DuM1ZZIgs64Hpf4bWcz9Z41+4Q+pgDx73UwWdAYyf6EG/lRFldmdHHzgrYyge5akFUW0D3mQ==",
|
||||
"version": "4.60.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.3.tgz",
|
||||
"integrity": "sha512-Dhbyh7j9FybM3YaTgaHmVALwA8AkUwTPccyCQ79TG9AJUsMQqgN1DDEZNr4+QUfwiWvLDumW5vdwzoeUF+TNxQ==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
@@ -2762,9 +2758,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-s390x-gnu": {
|
||||
"version": "4.60.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.2.tgz",
|
||||
"integrity": "sha512-ALyvJz965BQk8E9Al/JDKKDLH2kfKFLTGMlgkAbbYtZuJt9LU8DW3ZoDMCtQpXAltZxwBHevXz5u+gf0yA0YoA==",
|
||||
"version": "4.60.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.3.tgz",
|
||||
"integrity": "sha512-cJd1X5XhHHlltkaypz1UcWLA8AcoIi1aWhsvaWDskD1oz2eKCypnqvTQ8ykMNI0RSmm7NkTdSqSSD7zM0xa6Ig==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
@@ -2775,9 +2771,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
||||
"version": "4.60.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.2.tgz",
|
||||
"integrity": "sha512-UQjrkIdWrKI626Du8lCQ6MJp/6V1LAo2bOK9OTu4mSn8GGXIkPXk/Vsp4bLHCd9Z9Iz2OTEaokUE90VweJgIYQ==",
|
||||
"version": "4.60.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.3.tgz",
|
||||
"integrity": "sha512-DAZDBHQfG2oQuhY7mc6I3/qB4LU2fQCjRvxbDwd/Jdvb9fypP4IJ4qmtu6lNjes6B531AI8cg1aKC2di97bUxA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -2788,9 +2784,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-musl": {
|
||||
"version": "4.60.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.2.tgz",
|
||||
"integrity": "sha512-bTsRGj6VlSdn/XD4CGyzMnzaBs9bsRxy79eTqTCBsA8TMIEky7qg48aPkvJvFe1HyzQ5oMZdg7AnVlWQSKLTnw==",
|
||||
"version": "4.60.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.3.tgz",
|
||||
"integrity": "sha512-cRxsE8c13mZOh3vP+wLDxpQBRrOHDIGOWyDL93Sy0Ga8y515fBcC2pjUfFwUe5T7tqvTvWbCpg1URM/AXdWIXA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -2801,9 +2797,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-openbsd-x64": {
|
||||
"version": "4.60.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.2.tgz",
|
||||
"integrity": "sha512-6d4Z3534xitaA1FcMWP7mQPq5zGwBmGbhphh2DwaA1aNIXUu3KTOfwrWpbwI4/Gr0uANo7NTtaykFyO2hPuFLg==",
|
||||
"version": "4.60.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.3.tgz",
|
||||
"integrity": "sha512-QaWcIgRxqEdQdhJqW4DJctsH6HCmo5vHxY0krHSX4jMtOqfzC+dqDGuHM87bu4H8JBeibWx7jFz+h6/4C8wA5Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -2814,9 +2810,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-openharmony-arm64": {
|
||||
"version": "4.60.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.2.tgz",
|
||||
"integrity": "sha512-NetAg5iO2uN7eB8zE5qrZ3CSil+7IJt4WDFLcC75Ymywq1VZVD6qJ6EvNLjZ3rEm6gB7XW5JdT60c6MN35Z85Q==",
|
||||
"version": "4.60.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.3.tgz",
|
||||
"integrity": "sha512-AaXwSvUi3QIPtroAUw1t5yHGIyqKEXwH54WUocFolZhpGDruJcs8c+xPNDRn4XiQsS7MEwnYsHW2l0MBLDMkWg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -2827,9 +2823,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
||||
"version": "4.60.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.2.tgz",
|
||||
"integrity": "sha512-NCYhOotpgWZ5kdxCZsv6Iudx0wX8980Q/oW4pNFNihpBKsDbEA1zpkfxJGC0yugsUuyDZ7gL37dbzwhR0VI7pQ==",
|
||||
"version": "4.60.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.3.tgz",
|
||||
"integrity": "sha512-65LAKM/bAWDqKNEelHlcHvm2V+Vfb8C6INFxQXRHCvaVN1rJfwr4NvdP4FyzUaLqWfaCGaadf6UbTm8xJeYfEg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -2840,9 +2836,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
||||
"version": "4.60.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.2.tgz",
|
||||
"integrity": "sha512-RXsaOqXxfoUBQoOgvmmijVxJnW2IGB0eoMO7F8FAjaj0UTywUO/luSqimWBJn04WNgUkeNhh7fs7pESXajWmkg==",
|
||||
"version": "4.60.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.3.tgz",
|
||||
"integrity": "sha512-EEM2gyhBF5MFnI6vMKdX1LAosE627RGBzIoGMdLloPZkXrUN0Ckqgr2Qi8+J3zip/8NVVro3/FjB+tjhZUgUHA==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@@ -2853,9 +2849,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-x64-gnu": {
|
||||
"version": "4.60.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.2.tgz",
|
||||
"integrity": "sha512-qdAzEULD+/hzObedtmV6iBpdL5TIbKVztGiK7O3/KYSf+HIzU257+MX1EXJcyIiDbMAqmbwaufcYPvyRryeZtA==",
|
||||
"version": "4.60.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.3.tgz",
|
||||
"integrity": "sha512-E5Eb5H/DpxaoXH++Qkv28RcUJboMopmdDUALBczvHMf7hNIxaDZqwY5lK12UK1BHacSmvupoEWGu+n993Z0y1A==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -2866,9 +2862,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
||||
"version": "4.60.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.2.tgz",
|
||||
"integrity": "sha512-Nd/SgG27WoA9e+/TdK74KnHz852TLa94ovOYySo/yMPuTmpckK/jIF2jSwS3g7ELSKXK13/cVdmg1Z/DaCWKxA==",
|
||||
"version": "4.60.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.3.tgz",
|
||||
"integrity": "sha512-hPt/bgL5cE+Qp+/TPHBqptcAgPzgj46mPcg/16zNUmbQk0j+mOEQV/+Lqu8QRtDV3Ek95Q6FeFITpuhl6OTsAA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -3853,9 +3849,10 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
||||
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
|
||||
"version": "1.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
|
||||
"integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/hosted-git-info": {
|
||||
@@ -4582,12 +4579,12 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/commander": {
|
||||
"version": "8.3.0",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz",
|
||||
"integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==",
|
||||
"version": "12.1.0",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz",
|
||||
"integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/concurrently": {
|
||||
@@ -5110,9 +5107,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/fast-xml-builder": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.5.tgz",
|
||||
"integrity": "sha512-4TJn/8FKLeslLAH3dnohXqE3QSoxkhvaMzepOIZytwJXZO69Bfz0HBdDHzOTOon6G59Zrk6VQ2bEiv1t61rfkA==",
|
||||
"version": "1.1.9",
|
||||
"resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.9.tgz",
|
||||
"integrity": "sha512-jcyKVSEX13iseJqg7n/KWw+xnu/7fdrZ333Fac54KjHDIELVCfDDJXYIm6DTJ0Su4gSzrhqiK0DzY/wZbF40mw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -5589,13 +5586,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/is-core-module": {
|
||||
"version": "2.16.1",
|
||||
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
|
||||
"integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
|
||||
"version": "2.16.2",
|
||||
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz",
|
||||
"integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"hasown": "^2.0.2"
|
||||
"hasown": "^2.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
@@ -5670,9 +5667,9 @@
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/jiti": {
|
||||
"version": "2.6.1",
|
||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
|
||||
"integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==",
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz",
|
||||
"integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"jiti": "lib/jiti-cli.mjs"
|
||||
@@ -5795,10 +5792,19 @@
|
||||
"katex": "cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/katex/node_modules/commander": {
|
||||
"version": "8.3.0",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz",
|
||||
"integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
}
|
||||
},
|
||||
"node_modules/koffi": {
|
||||
"version": "2.16.1",
|
||||
"resolved": "https://registry.npmjs.org/koffi/-/koffi-2.16.1.tgz",
|
||||
"integrity": "sha512-0Ie6CfD026dNfWSosDw9dPxPzO9Rlyo0N8m5r05S8YjytIpuilzMFDMY4IDy/8xQsTwpuVinhncD+S8n3bcYZQ==",
|
||||
"version": "2.16.2",
|
||||
"resolved": "https://registry.npmjs.org/koffi/-/koffi-2.16.2.tgz",
|
||||
"integrity": "sha512-owU0MRwv6xkrVqCd+33uw6BaYppkTRXbO/rVdJNI2dvZG0gzyRhYwW25eWtc5pauwK8TGh3AbkFONSezdykfSA==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
@@ -6115,9 +6121,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lru-cache": {
|
||||
"version": "11.3.5",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.5.tgz",
|
||||
"integrity": "sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw==",
|
||||
"version": "11.3.6",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.6.tgz",
|
||||
"integrity": "sha512-Gf/KoL3C/MlI7Bt0PGI9I+TeTC/I6r/csU58N4BSNc4lppLBeKsOdFYkK+dX0ABDUMJNfCHTyPpzwwO21Awd3A==",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
@@ -6322,9 +6328,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/node-abi": {
|
||||
"version": "3.90.0",
|
||||
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.90.0.tgz",
|
||||
"integrity": "sha512-pZNQT7UnYlMwMBy5N1lV5X/YLTbZM5ncytN3xL7CHEzhDN8uVe0u55yaPUJICIJjaCW8NrM5BFdqr7HLweStNA==",
|
||||
"version": "3.92.0",
|
||||
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.92.0.tgz",
|
||||
"integrity": "sha512-KdHvFWZjEKDf0cakgFjebl371GPsISX2oZHcuyKqM7DtogIsHrqKeLTo8wBHxaXRAQlY2PsPlZmfo+9ZCxEREQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -6657,9 +6663,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.13",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.13.tgz",
|
||||
"integrity": "sha512-qif0+jGGZoLWdHey3UFHHWP0H7Gbmsk8T5VEqyYFbWqPr1XqvLGBbk/sl8V5exGmcYJklJOhOQq1pV9IcsiFag==",
|
||||
"version": "8.5.14",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz",
|
||||
"integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
@@ -6932,9 +6938,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/rollup": {
|
||||
"version": "4.60.2",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.2.tgz",
|
||||
"integrity": "sha512-J9qZyW++QK/09NyN/zeO0dG/1GdGfyp9lV8ajHnRVLfo/uFsbji5mHnDgn/qYdUHyCkM2N+8VyspgZclfAh0eQ==",
|
||||
"version": "4.60.3",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.3.tgz",
|
||||
"integrity": "sha512-pAQK9HalE84QSm4Po3EmWIZPd3FnjkShVkiMlz1iligWYkWQ7wHYd1PF/T7QZ5TVSD6uSTon5gBVMSM4JfBV+A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree": "1.0.8"
|
||||
@@ -6947,34 +6953,40 @@
|
||||
"npm": ">=8.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@rollup/rollup-android-arm-eabi": "4.60.2",
|
||||
"@rollup/rollup-android-arm64": "4.60.2",
|
||||
"@rollup/rollup-darwin-arm64": "4.60.2",
|
||||
"@rollup/rollup-darwin-x64": "4.60.2",
|
||||
"@rollup/rollup-freebsd-arm64": "4.60.2",
|
||||
"@rollup/rollup-freebsd-x64": "4.60.2",
|
||||
"@rollup/rollup-linux-arm-gnueabihf": "4.60.2",
|
||||
"@rollup/rollup-linux-arm-musleabihf": "4.60.2",
|
||||
"@rollup/rollup-linux-arm64-gnu": "4.60.2",
|
||||
"@rollup/rollup-linux-arm64-musl": "4.60.2",
|
||||
"@rollup/rollup-linux-loong64-gnu": "4.60.2",
|
||||
"@rollup/rollup-linux-loong64-musl": "4.60.2",
|
||||
"@rollup/rollup-linux-ppc64-gnu": "4.60.2",
|
||||
"@rollup/rollup-linux-ppc64-musl": "4.60.2",
|
||||
"@rollup/rollup-linux-riscv64-gnu": "4.60.2",
|
||||
"@rollup/rollup-linux-riscv64-musl": "4.60.2",
|
||||
"@rollup/rollup-linux-s390x-gnu": "4.60.2",
|
||||
"@rollup/rollup-linux-x64-gnu": "4.60.2",
|
||||
"@rollup/rollup-linux-x64-musl": "4.60.2",
|
||||
"@rollup/rollup-openbsd-x64": "4.60.2",
|
||||
"@rollup/rollup-openharmony-arm64": "4.60.2",
|
||||
"@rollup/rollup-win32-arm64-msvc": "4.60.2",
|
||||
"@rollup/rollup-win32-ia32-msvc": "4.60.2",
|
||||
"@rollup/rollup-win32-x64-gnu": "4.60.2",
|
||||
"@rollup/rollup-win32-x64-msvc": "4.60.2",
|
||||
"@rollup/rollup-android-arm-eabi": "4.60.3",
|
||||
"@rollup/rollup-android-arm64": "4.60.3",
|
||||
"@rollup/rollup-darwin-arm64": "4.60.3",
|
||||
"@rollup/rollup-darwin-x64": "4.60.3",
|
||||
"@rollup/rollup-freebsd-arm64": "4.60.3",
|
||||
"@rollup/rollup-freebsd-x64": "4.60.3",
|
||||
"@rollup/rollup-linux-arm-gnueabihf": "4.60.3",
|
||||
"@rollup/rollup-linux-arm-musleabihf": "4.60.3",
|
||||
"@rollup/rollup-linux-arm64-gnu": "4.60.3",
|
||||
"@rollup/rollup-linux-arm64-musl": "4.60.3",
|
||||
"@rollup/rollup-linux-loong64-gnu": "4.60.3",
|
||||
"@rollup/rollup-linux-loong64-musl": "4.60.3",
|
||||
"@rollup/rollup-linux-ppc64-gnu": "4.60.3",
|
||||
"@rollup/rollup-linux-ppc64-musl": "4.60.3",
|
||||
"@rollup/rollup-linux-riscv64-gnu": "4.60.3",
|
||||
"@rollup/rollup-linux-riscv64-musl": "4.60.3",
|
||||
"@rollup/rollup-linux-s390x-gnu": "4.60.3",
|
||||
"@rollup/rollup-linux-x64-gnu": "4.60.3",
|
||||
"@rollup/rollup-linux-x64-musl": "4.60.3",
|
||||
"@rollup/rollup-openbsd-x64": "4.60.3",
|
||||
"@rollup/rollup-openharmony-arm64": "4.60.3",
|
||||
"@rollup/rollup-win32-arm64-msvc": "4.60.3",
|
||||
"@rollup/rollup-win32-ia32-msvc": "4.60.3",
|
||||
"@rollup/rollup-win32-x64-gnu": "4.60.3",
|
||||
"@rollup/rollup-win32-x64-msvc": "4.60.3",
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/rollup/node_modules/@types/estree": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
||||
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/run-parallel": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
|
||||
@@ -7247,6 +7259,7 @@
|
||||
"version": "3.10.0",
|
||||
"resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz",
|
||||
"integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/string_decoder": {
|
||||
@@ -7343,9 +7356,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/strnum": {
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.3.tgz",
|
||||
"integrity": "sha512-oKx6RUCuHfT3oyVjtnrmn19H1SiCqgJSg+54XqURKp5aCMbrXrhLjRN9TjuwMjiYstZ0MzDrHqkGZ5dFTKd+zg==",
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/strnum/-/strnum-2.3.0.tgz",
|
||||
"integrity": "sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -7674,9 +7687,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/typebox": {
|
||||
"version": "1.1.37",
|
||||
"resolved": "https://registry.npmjs.org/typebox/-/typebox-1.1.37.tgz",
|
||||
"integrity": "sha512-jb7jp6KvOvvy5sd+11AfJ0/e0F0AS9RcOXd55oGi2ZnRHIGmFvrTaNF+ZidRmGBmmNTkM5KKl0Z37KzxJ+owEQ==",
|
||||
"version": "1.1.38",
|
||||
"resolved": "https://registry.npmjs.org/typebox/-/typebox-1.1.38.tgz",
|
||||
"integrity": "sha512-pZ0aQPmMmXoUvSbeuWf/Hzsc+avNw/Zd6VeE8CFgkVGWyuHPJvqeJJDeJqLve+K70LvjYIoleGcoJHPT17cWoA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
@@ -7749,9 +7762,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "7.3.2",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.2.tgz",
|
||||
"integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==",
|
||||
"version": "7.3.3",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.3.tgz",
|
||||
"integrity": "sha512-/4XH147Ui7OGTjg3HbdWe5arnZQSbfuRzdr9Ec7TQi5I7R+ir0Rlc9GIvD4v0XZurELqA035KVXJXpR61xhiTA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"esbuild": "^0.27.0",
|
||||
@@ -8143,22 +8156,10 @@
|
||||
"fd-slicer": "~1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/yoctocolors": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.2.tgz",
|
||||
"integrity": "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/zod": {
|
||||
"version": "4.4.2",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-4.4.2.tgz",
|
||||
"integrity": "sha512-IynmDyxsEsb9RKzO3J9+4SxXnl2FTFSzNBaKKaMV6tsSk0rw9gYw9gs+JFCq/qk2LCZ78KDwyj+Z289TijSkUw==",
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz",
|
||||
"integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
@@ -8175,10 +8176,10 @@
|
||||
},
|
||||
"packages/agent": {
|
||||
"name": "@mariozechner/pi-agent-core",
|
||||
"version": "0.72.1",
|
||||
"version": "0.73.1",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@mariozechner/pi-ai": "^0.72.1",
|
||||
"@mariozechner/pi-ai": "^0.73.1",
|
||||
"typebox": "^1.1.24"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -8209,7 +8210,7 @@
|
||||
},
|
||||
"packages/ai": {
|
||||
"name": "@mariozechner/pi-ai",
|
||||
"version": "0.72.1",
|
||||
"version": "0.73.1",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.91.1",
|
||||
@@ -8255,13 +8256,12 @@
|
||||
},
|
||||
"packages/coding-agent": {
|
||||
"name": "@mariozechner/pi-coding-agent",
|
||||
"version": "0.72.1",
|
||||
"version": "0.73.1",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@mariozechner/jiti": "^2.6.2",
|
||||
"@mariozechner/pi-agent-core": "^0.72.1",
|
||||
"@mariozechner/pi-ai": "^0.72.1",
|
||||
"@mariozechner/pi-tui": "^0.72.1",
|
||||
"@mariozechner/pi-agent-core": "^0.73.1",
|
||||
"@mariozechner/pi-ai": "^0.73.1",
|
||||
"@mariozechner/pi-tui": "^0.73.1",
|
||||
"@silvia-odwyer/photon-node": "^0.3.4",
|
||||
"chalk": "^5.5.0",
|
||||
"cli-highlight": "^2.1.11",
|
||||
@@ -8271,6 +8271,7 @@
|
||||
"glob": "^13.0.1",
|
||||
"hosted-git-info": "^9.0.2",
|
||||
"ignore": "^7.0.5",
|
||||
"jiti": "^2.7.0",
|
||||
"marked": "^15.0.12",
|
||||
"minimatch": "^10.2.3",
|
||||
"proper-lockfile": "^4.1.2",
|
||||
@@ -8302,25 +8303,25 @@
|
||||
},
|
||||
"packages/coding-agent/examples/extensions/custom-provider-anthropic": {
|
||||
"name": "pi-extension-custom-provider-anthropic",
|
||||
"version": "0.72.1",
|
||||
"version": "0.73.1",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.52.0"
|
||||
}
|
||||
},
|
||||
"packages/coding-agent/examples/extensions/custom-provider-gitlab-duo": {
|
||||
"name": "pi-extension-custom-provider-gitlab-duo",
|
||||
"version": "0.72.1"
|
||||
"version": "0.73.1"
|
||||
},
|
||||
"packages/coding-agent/examples/extensions/sandbox": {
|
||||
"name": "pi-extension-sandbox",
|
||||
"version": "1.2.1",
|
||||
"version": "1.3.1",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sandbox-runtime": "^0.0.26"
|
||||
}
|
||||
},
|
||||
"packages/coding-agent/examples/extensions/with-deps": {
|
||||
"name": "pi-extension-with-deps",
|
||||
"version": "0.72.1",
|
||||
"version": "0.73.1",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
@@ -8356,7 +8357,7 @@
|
||||
},
|
||||
"packages/tui": {
|
||||
"name": "@mariozechner/pi-tui",
|
||||
"version": "0.72.1",
|
||||
"version": "0.73.1",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/mime-types": "^2.1.4",
|
||||
@@ -8378,12 +8379,12 @@
|
||||
},
|
||||
"packages/web-ui": {
|
||||
"name": "@mariozechner/pi-web-ui",
|
||||
"version": "0.72.1",
|
||||
"version": "0.73.1",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lmstudio/sdk": "^1.5.0",
|
||||
"@mariozechner/pi-ai": "^0.72.1",
|
||||
"@mariozechner/pi-tui": "^0.72.1",
|
||||
"@mariozechner/pi-ai": "^0.73.1",
|
||||
"@mariozechner/pi-tui": "^0.73.1",
|
||||
"docx-preview": "^0.3.7",
|
||||
"jszip": "^3.10.1",
|
||||
"lucide": "^0.544.0",
|
||||
@@ -8405,7 +8406,7 @@
|
||||
},
|
||||
"packages/web-ui/example": {
|
||||
"name": "pi-web-ui-example",
|
||||
"version": "0.72.1",
|
||||
"version": "0.73.1",
|
||||
"dependencies": {
|
||||
"@mariozechner/mini-lit": "^0.2.0",
|
||||
"@mariozechner/pi-ai": "file:../../ai",
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
"@typescript/native-preview": "7.0.0-dev.20260120.1",
|
||||
"concurrently": "^9.2.1",
|
||||
"husky": "^9.1.7",
|
||||
"jiti": "^2.7.0",
|
||||
"tsx": "^4.20.3",
|
||||
"typescript": "^5.9.2",
|
||||
"shx": "^0.4.0"
|
||||
@@ -48,7 +49,6 @@
|
||||
},
|
||||
"version": "0.0.3",
|
||||
"dependencies": {
|
||||
"@mariozechner/jiti": "^2.6.5",
|
||||
"@mariozechner/pi-coding-agent": "^0.30.2",
|
||||
"get-east-asian-width": "^1.4.0"
|
||||
},
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.73.1] - 2026-05-07
|
||||
|
||||
## [0.73.0] - 2026-05-04
|
||||
|
||||
## [0.72.1] - 2026-05-02
|
||||
|
||||
### Changed
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@mariozechner/pi-agent-core",
|
||||
"version": "0.72.1",
|
||||
"version": "0.73.1",
|
||||
"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.72.1",
|
||||
"@mariozechner/pi-ai": "^0.73.1",
|
||||
"typebox": "^1.1.24"
|
||||
},
|
||||
"keywords": [
|
||||
|
||||
@@ -2,13 +2,36 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
- Switched the built-in `xiaomi` provider endpoint from Token Plan AMS (`https://token-plan-ams.xiaomimimo.com/anthropic`) to API billing (`https://api.xiaomimimo.com/anthropic`). `XIAOMI_API_KEY` now refers to the API billing key from [platform.xiaomimimo.com](https://platform.xiaomimimo.com). Users still on Token Plan must move to the appropriate `xiaomi-token-plan-*` provider and set the corresponding env var.
|
||||
## [0.73.1] - 2026-05-07
|
||||
|
||||
### Added
|
||||
|
||||
- Added Xiaomi MiMo Token Plan regional providers with per-region env vars: `xiaomi-token-plan-cn` (`XIAOMI_TOKEN_PLAN_CN_API_KEY`), `xiaomi-token-plan-ams` (`XIAOMI_TOKEN_PLAN_AMS_API_KEY`), and `xiaomi-token-plan-sgp` (`XIAOMI_TOKEN_PLAN_SGP_API_KEY`).
|
||||
- Added OAuth login flow metadata so clients can present interactive provider choices during login ([#4190](https://github.com/earendil-works/pi-mono/pull/4190) by [@mitsuhiko](https://github.com/mitsuhiko)).
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed OpenAI Responses reasoning text streaming for LM Studio and other compatible providers that emit `response.reasoning_text.delta` events ([#4191](https://github.com/badlogic/pi-mono/pull/4191) by [@yaanfpv](https://github.com/yaanfpv)).
|
||||
- Fixed OpenAI Codex OAuth refresh failures writing directly to stderr while the TUI is active ([#4141](https://github.com/badlogic/pi-mono/issues/4141)).
|
||||
- Fixed OpenAI-compatible chat completion streams that interleave content and tool-call deltas in the same choice.
|
||||
- Fixed the Kimi K2 P6 model alias to normalize to `kimi-for-coding` ([#4218](https://github.com/earendil-works/pi-mono/issues/4218)).
|
||||
- Fixed OpenAI Codex Responses requests to send a non-empty system prompt ([#4184](https://github.com/earendil-works/pi-mono/issues/4184)).
|
||||
|
||||
## [0.73.0] - 2026-05-04
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
- Switched the built-in `xiaomi` provider endpoint from Token Plan AMS (`https://token-plan-ams.xiaomimimo.com/anthropic`) to API billing (`https://api.xiaomimimo.com/anthropic`). `XIAOMI_API_KEY` now refers to the API billing key from [platform.xiaomimimo.com](https://platform.xiaomimimo.com). Users still on Token Plan must move to the appropriate `xiaomi-token-plan-*` provider and set the corresponding env var ([#4112](https://github.com/badlogic/pi-mono/pull/4112) by [@Phoen1xCode](https://github.com/Phoen1xCode)).
|
||||
|
||||
### Added
|
||||
|
||||
- Added Xiaomi MiMo Token Plan regional providers with per-region env vars: `xiaomi-token-plan-cn` (`XIAOMI_TOKEN_PLAN_CN_API_KEY`), `xiaomi-token-plan-ams` (`XIAOMI_TOKEN_PLAN_AMS_API_KEY`), and `xiaomi-token-plan-sgp` (`XIAOMI_TOKEN_PLAN_SGP_API_KEY`) ([#4112](https://github.com/badlogic/pi-mono/pull/4112) by [@Phoen1xCode](https://github.com/Phoen1xCode)).
|
||||
- Added `registerSessionResourceCleanup()` and `cleanupSessionResources()` so providers can register cleanup hooks for session-scoped resources.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed generated OpenAI-compatible model metadata for Qwen 3.5/3.6 and MiniMax M2.7 to match models.dev and OpenCode Go ([#4110](https://github.com/badlogic/pi-mono/pull/4110) by [@jsynowiec](https://github.com/jsynowiec)).
|
||||
- Fixed Bedrock Converse thinking effort mapping to preserve native `xhigh` for Claude Opus 4.7.
|
||||
- Fixed OpenAI Codex Responses WebSocket transport to fall back to SSE when setup fails before streaming starts, and attach transport diagnostics to the assistant message ([#4133](https://github.com/badlogic/pi-mono/issues/4133)).
|
||||
|
||||
## [0.72.1] - 2026-05-02
|
||||
|
||||
|
||||
@@ -386,6 +386,8 @@ All streaming events emitted during assistant message generation:
|
||||
| `done` | Stream complete | `reason`: Stop reason ("stop", "length", "toolUse"), `message`: Final assistant message |
|
||||
| `error` | Error occurred | `reason`: Error type ("error" or "aborted"), `error`: AssistantMessage with partial content |
|
||||
|
||||
Streaming events for different content blocks are not guaranteed to be contiguous. Providers may emit deltas for text, thinking, and tool calls in the same upstream chunk, and pi may surface corresponding events interleaved, for example `text_start`, `text_delta`, `toolcall_start`, `text_delta`, `toolcall_delta`. Consumers must use `contentIndex` to associate each delta/end event with its block and must not assume that a block's `*_start`/`*_delta`/`*_end` sequence is uninterrupted by events for other blocks.
|
||||
|
||||
## Image Input
|
||||
|
||||
Models with vision capabilities can process images. You can check if a model supports images via the `input` property. If you pass images to a non-vision model, they are silently ignored.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@mariozechner/pi-ai",
|
||||
"version": "0.72.1",
|
||||
"version": "0.73.1",
|
||||
"description": "Unified LLM API with automatic model discovery and provider configuration",
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
|
||||
@@ -857,15 +857,17 @@ async function loadModelsDevData(): Promise<Model<any>[]> {
|
||||
const kimiModels = data["kimi-for-coding"].models as Record<string, ModelsDevModel>;
|
||||
const hasCanonicalModel = Object.prototype.hasOwnProperty.call(kimiModels, "kimi-for-coding");
|
||||
|
||||
const kimiAliases = new Set(["k2p5", "k2p6"]);
|
||||
|
||||
for (const [modelId, model] of Object.entries(kimiModels)) {
|
||||
const m = model as ModelsDevModel;
|
||||
if (m.tool_call !== true) continue;
|
||||
// models.dev still exposes deprecated "k2p5" in some snapshots.
|
||||
// Normalize to the canonical model id and drop duplicates when canonical exists.
|
||||
if (modelId === "k2p5" && hasCanonicalModel) continue;
|
||||
// models.dev may expose versioned aliases (e.g. k2p5/k2p6).
|
||||
// Normalize aliases to the canonical model id and drop duplicates when canonical exists.
|
||||
if (kimiAliases.has(modelId) && hasCanonicalModel) continue;
|
||||
|
||||
const normalizedId = modelId === "k2p5" ? "kimi-for-coding" : modelId;
|
||||
const normalizedName = modelId === "k2p5" ? "Kimi For Coding" : m.name || normalizedId;
|
||||
const normalizedId = kimiAliases.has(modelId) ? "kimi-for-coding" : modelId;
|
||||
const normalizedName = kimiAliases.has(modelId) ? "Kimi For Coding" : m.name || normalizedId;
|
||||
|
||||
models.push({
|
||||
id: normalizedId,
|
||||
|
||||
@@ -38,6 +38,8 @@ export type {
|
||||
OAuthProviderId,
|
||||
OAuthProviderInfo,
|
||||
OAuthProviderInterface,
|
||||
OAuthSelectOption,
|
||||
OAuthSelectPrompt,
|
||||
} from "./utils/oauth/types.js";
|
||||
export * from "./utils/overflow.js";
|
||||
export * from "./utils/typebox-helpers.js";
|
||||
|
||||
@@ -5819,24 +5819,6 @@ export const MODELS = {
|
||||
} satisfies Model<"openai-completions">,
|
||||
},
|
||||
"kimi-coding": {
|
||||
"k2p6": {
|
||||
id: "k2p6",
|
||||
name: "Kimi K2.6",
|
||||
api: "anthropic-messages",
|
||||
provider: "kimi-coding",
|
||||
baseUrl: "https://api.kimi.com/coding",
|
||||
headers: {"User-Agent":"KimiCLI/1.5"},
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
cost: {
|
||||
input: 0,
|
||||
output: 0,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 262144,
|
||||
maxTokens: 32768,
|
||||
} satisfies Model<"anthropic-messages">,
|
||||
"kimi-for-coding": {
|
||||
id: "kimi-for-coding",
|
||||
name: "Kimi For Coding",
|
||||
@@ -7604,9 +7586,9 @@ export const MODELS = {
|
||||
"big-pickle": {
|
||||
id: "big-pickle",
|
||||
name: "Big Pickle",
|
||||
api: "anthropic-messages",
|
||||
api: "openai-completions",
|
||||
provider: "opencode",
|
||||
baseUrl: "https://opencode.ai/zen",
|
||||
baseUrl: "https://opencode.ai/zen/v1",
|
||||
reasoning: true,
|
||||
input: ["text"],
|
||||
cost: {
|
||||
@@ -7617,7 +7599,7 @@ export const MODELS = {
|
||||
},
|
||||
contextWindow: 200000,
|
||||
maxTokens: 128000,
|
||||
} satisfies Model<"anthropic-messages">,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"claude-haiku-4-5": {
|
||||
id: "claude-haiku-4-5",
|
||||
name: "Claude Haiku 4.5",
|
||||
@@ -7872,9 +7854,9 @@ export const MODELS = {
|
||||
thinkingLevelMap: {"off":null},
|
||||
input: ["text", "image"],
|
||||
cost: {
|
||||
input: 0,
|
||||
output: 0,
|
||||
cacheRead: 0,
|
||||
input: 0.05,
|
||||
output: 0.4,
|
||||
cacheRead: 0.005,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 400000,
|
||||
@@ -8360,55 +8342,21 @@ export const MODELS = {
|
||||
} satisfies Model<"openai-completions">,
|
||||
"kimi-k2.6": {
|
||||
id: "kimi-k2.6",
|
||||
name: "Kimi K2.6 (3x limits)",
|
||||
name: "Kimi K2.6",
|
||||
api: "openai-completions",
|
||||
provider: "opencode-go",
|
||||
baseUrl: "https://opencode.ai/zen/go/v1",
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
cost: {
|
||||
input: 0.32,
|
||||
output: 1.34,
|
||||
cacheRead: 0.054,
|
||||
input: 0.95,
|
||||
output: 4,
|
||||
cacheRead: 0.16,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 262144,
|
||||
maxTokens: 65536,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"mimo-v2-omni": {
|
||||
id: "mimo-v2-omni",
|
||||
name: "MiMo V2 Omni",
|
||||
api: "openai-completions",
|
||||
provider: "opencode-go",
|
||||
baseUrl: "https://opencode.ai/zen/go/v1",
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
cost: {
|
||||
input: 0.4,
|
||||
output: 2,
|
||||
cacheRead: 0.08,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 262144,
|
||||
maxTokens: 128000,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"mimo-v2-pro": {
|
||||
id: "mimo-v2-pro",
|
||||
name: "MiMo V2 Pro",
|
||||
api: "openai-completions",
|
||||
provider: "opencode-go",
|
||||
baseUrl: "https://opencode.ai/zen/go/v1",
|
||||
reasoning: true,
|
||||
input: ["text"],
|
||||
cost: {
|
||||
input: 1,
|
||||
output: 3,
|
||||
cacheRead: 0.2,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 1048576,
|
||||
maxTokens: 128000,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"mimo-v2.5": {
|
||||
id: "mimo-v2.5",
|
||||
name: "MiMo V2.5",
|
||||
@@ -8549,23 +8497,6 @@ export const MODELS = {
|
||||
contextWindow: 131072,
|
||||
maxTokens: 131072,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"allenai/olmo-3.1-32b-instruct": {
|
||||
id: "allenai/olmo-3.1-32b-instruct",
|
||||
name: "AllenAI: Olmo 3.1 32B Instruct",
|
||||
api: "openai-completions",
|
||||
provider: "openrouter",
|
||||
baseUrl: "https://openrouter.ai/api/v1",
|
||||
reasoning: false,
|
||||
input: ["text"],
|
||||
cost: {
|
||||
input: 0.19999999999999998,
|
||||
output: 0.6,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 65536,
|
||||
maxTokens: 16384,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"amazon/nova-2-lite-v1": {
|
||||
id: "amazon/nova-2-lite-v1",
|
||||
name: "Amazon: Nova 2 Lite",
|
||||
@@ -8700,7 +8631,7 @@ export const MODELS = {
|
||||
cacheWrite: 3.75,
|
||||
},
|
||||
contextWindow: 200000,
|
||||
maxTokens: 128000,
|
||||
maxTokens: 64000,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"anthropic/claude-3.7-sonnet:thinking": {
|
||||
id: "anthropic/claude-3.7-sonnet:thinking",
|
||||
@@ -8977,6 +8908,23 @@ export const MODELS = {
|
||||
contextWindow: 2000000,
|
||||
maxTokens: 30000,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"baidu/cobuddy:free": {
|
||||
id: "baidu/cobuddy:free",
|
||||
name: "Baidu Qianfan: CoBuddy (free)",
|
||||
api: "openai-completions",
|
||||
provider: "openrouter",
|
||||
baseUrl: "https://openrouter.ai/api/v1",
|
||||
reasoning: true,
|
||||
input: ["text"],
|
||||
cost: {
|
||||
input: 0,
|
||||
output: 0,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 131072,
|
||||
maxTokens: 65536,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"baidu/ernie-4.5-21b-a3b": {
|
||||
id: "baidu/ernie-4.5-21b-a3b",
|
||||
name: "Baidu: ERNIE 4.5 21B A3B",
|
||||
@@ -9207,8 +9155,8 @@ export const MODELS = {
|
||||
reasoning: true,
|
||||
input: ["text"],
|
||||
cost: {
|
||||
input: 0.21,
|
||||
output: 0.7899999999999999,
|
||||
input: 0.27,
|
||||
output: 0.95,
|
||||
cacheRead: 0.13,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
@@ -9318,7 +9266,7 @@ export const MODELS = {
|
||||
cacheRead: 0.024999999999999998,
|
||||
cacheWrite: 0.08333333333333334,
|
||||
},
|
||||
contextWindow: 1000000,
|
||||
contextWindow: 1048576,
|
||||
maxTokens: 8192,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"google/gemini-2.0-flash-lite-001": {
|
||||
@@ -9695,23 +9643,6 @@ export const MODELS = {
|
||||
contextWindow: 256000,
|
||||
maxTokens: 80000,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"meta-llama/llama-3-8b-instruct": {
|
||||
id: "meta-llama/llama-3-8b-instruct",
|
||||
name: "Meta: Llama 3 8B Instruct",
|
||||
api: "openai-completions",
|
||||
provider: "openrouter",
|
||||
baseUrl: "https://openrouter.ai/api/v1",
|
||||
reasoning: false,
|
||||
input: ["text"],
|
||||
cost: {
|
||||
input: 0.03,
|
||||
output: 0.04,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 8192,
|
||||
maxTokens: 16384,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"meta-llama/llama-3.1-70b-instruct": {
|
||||
id: "meta-llama/llama-3.1-70b-instruct",
|
||||
name: "Meta: Llama 3.1 70B Instruct",
|
||||
@@ -10103,6 +10034,23 @@ export const MODELS = {
|
||||
contextWindow: 131072,
|
||||
maxTokens: 4096,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"mistralai/mistral-medium-3-5": {
|
||||
id: "mistralai/mistral-medium-3-5",
|
||||
name: "Mistral: Mistral Medium 3.5",
|
||||
api: "openai-completions",
|
||||
provider: "openrouter",
|
||||
baseUrl: "https://openrouter.ai/api/v1",
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
cost: {
|
||||
input: 1.5,
|
||||
output: 7.5,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 262144,
|
||||
maxTokens: 4096,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"mistralai/mistral-medium-3.1": {
|
||||
id: "mistralai/mistral-medium-3.1",
|
||||
name: "Mistral: Mistral Medium 3.1",
|
||||
@@ -10333,13 +10281,13 @@ export const MODELS = {
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
cost: {
|
||||
input: 0.74,
|
||||
output: 3.49,
|
||||
cacheRead: 0.14,
|
||||
input: 0.75,
|
||||
output: 3.5,
|
||||
cacheRead: 0.15,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 262142,
|
||||
maxTokens: 262142,
|
||||
contextWindow: 262144,
|
||||
maxTokens: 16384,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"nex-agi/deepseek-v3.1-nex-n1": {
|
||||
id: "nex-agi/deepseek-v3.1-nex-n1",
|
||||
@@ -11254,6 +11202,23 @@ export const MODELS = {
|
||||
contextWindow: 128000,
|
||||
maxTokens: 16384,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"openai/gpt-chat-latest": {
|
||||
id: "openai/gpt-chat-latest",
|
||||
name: "OpenAI: GPT Chat Latest",
|
||||
api: "openai-completions",
|
||||
provider: "openrouter",
|
||||
baseUrl: "https://openrouter.ai/api/v1",
|
||||
reasoning: false,
|
||||
input: ["text", "image"],
|
||||
cost: {
|
||||
input: 5,
|
||||
output: 30,
|
||||
cacheRead: 0.5,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 400000,
|
||||
maxTokens: 128000,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"openai/gpt-oss-120b": {
|
||||
id: "openai/gpt-oss-120b",
|
||||
name: "OpenAI: gpt-oss-120b",
|
||||
@@ -11807,13 +11772,13 @@ export const MODELS = {
|
||||
reasoning: true,
|
||||
input: ["text"],
|
||||
cost: {
|
||||
input: 0.08,
|
||||
output: 0.28,
|
||||
input: 0.09,
|
||||
output: 0.44999999999999996,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 40960,
|
||||
maxTokens: 16384,
|
||||
maxTokens: 20000,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"qwen/qwen3-30b-a3b-instruct-2507": {
|
||||
id: "qwen/qwen3-30b-a3b-instruct-2507",
|
||||
@@ -11943,7 +11908,7 @@ export const MODELS = {
|
||||
reasoning: false,
|
||||
input: ["text"],
|
||||
cost: {
|
||||
input: 0.12,
|
||||
input: 0.11,
|
||||
output: 0.7999999999999999,
|
||||
cacheRead: 0.07,
|
||||
cacheWrite: 0,
|
||||
@@ -12232,13 +12197,13 @@ export const MODELS = {
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
cost: {
|
||||
input: 0.1625,
|
||||
output: 1.3,
|
||||
cacheRead: 0,
|
||||
input: 0.14,
|
||||
output: 1,
|
||||
cacheRead: 0.049999999999999996,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 262144,
|
||||
maxTokens: 65536,
|
||||
maxTokens: 81920,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"qwen/qwen3.5-397b-a17b": {
|
||||
id: "qwen/qwen3.5-397b-a17b",
|
||||
@@ -12266,13 +12231,13 @@ export const MODELS = {
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
cost: {
|
||||
input: 0.09999999999999999,
|
||||
input: 0.04,
|
||||
output: 0.15,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 262144,
|
||||
maxTokens: 4096,
|
||||
maxTokens: 81920,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"qwen/qwen3.5-flash-02-23": {
|
||||
id: "qwen/qwen3.5-flash-02-23",
|
||||
@@ -12342,6 +12307,23 @@ export const MODELS = {
|
||||
contextWindow: 262144,
|
||||
maxTokens: 81920,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"qwen/qwen3.6-35b-a3b": {
|
||||
id: "qwen/qwen3.6-35b-a3b",
|
||||
name: "Qwen: Qwen3.6 35B A3B",
|
||||
api: "openai-completions",
|
||||
provider: "openrouter",
|
||||
baseUrl: "https://openrouter.ai/api/v1",
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
cost: {
|
||||
input: 0.15,
|
||||
output: 1,
|
||||
cacheRead: 0.049999999999999996,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 262144,
|
||||
maxTokens: 262144,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"qwen/qwen3.6-flash": {
|
||||
id: "qwen/qwen3.6-flash",
|
||||
name: "Qwen: Qwen3.6 Flash",
|
||||
@@ -12986,7 +12968,7 @@ export const MODELS = {
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 202752,
|
||||
maxTokens: 16384,
|
||||
maxTokens: 4096,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"z-ai/glm-5-turbo": {
|
||||
id: "z-ai/glm-5-turbo",
|
||||
@@ -13133,13 +13115,13 @@ export const MODELS = {
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
cost: {
|
||||
input: 0.74,
|
||||
output: 3.49,
|
||||
cacheRead: 0.14,
|
||||
input: 0.75,
|
||||
output: 3.5,
|
||||
cacheRead: 0.15,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 262142,
|
||||
maxTokens: 262142,
|
||||
contextWindow: 262144,
|
||||
maxTokens: 16384,
|
||||
} satisfies Model<"openai-completions">,
|
||||
"~openai/gpt-latest": {
|
||||
id: "~openai/gpt-latest",
|
||||
@@ -14573,23 +14555,6 @@ export const MODELS = {
|
||||
contextWindow: 131072,
|
||||
maxTokens: 131072,
|
||||
} satisfies Model<"anthropic-messages">,
|
||||
"moonshotai/kimi-k2-0905": {
|
||||
id: "moonshotai/kimi-k2-0905",
|
||||
name: "Kimi K2 0905",
|
||||
api: "anthropic-messages",
|
||||
provider: "vercel-ai-gateway",
|
||||
baseUrl: "https://ai-gateway.vercel.sh",
|
||||
reasoning: false,
|
||||
input: ["text"],
|
||||
cost: {
|
||||
input: 0.6,
|
||||
output: 2.5,
|
||||
cacheRead: 0.3,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 256000,
|
||||
maxTokens: 128000,
|
||||
} satisfies Model<"anthropic-messages">,
|
||||
"moonshotai/kimi-k2-thinking": {
|
||||
id: "moonshotai/kimi-k2-thinking",
|
||||
name: "Kimi K2 Thinking",
|
||||
@@ -15546,8 +15511,8 @@ export const MODELS = {
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
cost: {
|
||||
input: 2,
|
||||
output: 6,
|
||||
input: 1.25,
|
||||
output: 2.5,
|
||||
cacheRead: 0.19999999999999998,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
@@ -15563,8 +15528,8 @@ export const MODELS = {
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
cost: {
|
||||
input: 2,
|
||||
output: 6,
|
||||
input: 1.25,
|
||||
output: 2.5,
|
||||
cacheRead: 0.19999999999999998,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
@@ -15580,8 +15545,8 @@ export const MODELS = {
|
||||
reasoning: false,
|
||||
input: ["text", "image"],
|
||||
cost: {
|
||||
input: 2,
|
||||
output: 6,
|
||||
input: 1.25,
|
||||
output: 2.5,
|
||||
cacheRead: 0.19999999999999998,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
@@ -15597,8 +15562,8 @@ export const MODELS = {
|
||||
reasoning: false,
|
||||
input: ["text", "image"],
|
||||
cost: {
|
||||
input: 2,
|
||||
output: 6,
|
||||
input: 1.25,
|
||||
output: 2.5,
|
||||
cacheRead: 0.19999999999999998,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
@@ -15614,8 +15579,8 @@ export const MODELS = {
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
cost: {
|
||||
input: 2,
|
||||
output: 6,
|
||||
input: 1.25,
|
||||
output: 2.5,
|
||||
cacheRead: 0.19999999999999998,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
@@ -15631,8 +15596,8 @@ export const MODELS = {
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
cost: {
|
||||
input: 2,
|
||||
output: 6,
|
||||
input: 1.25,
|
||||
output: 2.5,
|
||||
cacheRead: 0.19999999999999998,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
@@ -16405,8 +16370,8 @@ export const MODELS = {
|
||||
cacheRead: 0.01,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 256000,
|
||||
maxTokens: 64000,
|
||||
contextWindow: 262144,
|
||||
maxTokens: 65536,
|
||||
} satisfies Model<"anthropic-messages">,
|
||||
"mimo-v2-omni": {
|
||||
id: "mimo-v2-omni",
|
||||
@@ -16422,8 +16387,8 @@ export const MODELS = {
|
||||
cacheRead: 0.08,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 256000,
|
||||
maxTokens: 128000,
|
||||
contextWindow: 262144,
|
||||
maxTokens: 131072,
|
||||
} satisfies Model<"anthropic-messages">,
|
||||
"mimo-v2-pro": {
|
||||
id: "mimo-v2-pro",
|
||||
@@ -16439,8 +16404,8 @@ export const MODELS = {
|
||||
cacheRead: 0.2,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 1000000,
|
||||
maxTokens: 128000,
|
||||
contextWindow: 1048576,
|
||||
maxTokens: 131072,
|
||||
} satisfies Model<"anthropic-messages">,
|
||||
"mimo-v2.5": {
|
||||
id: "mimo-v2.5",
|
||||
@@ -16449,7 +16414,7 @@ export const MODELS = {
|
||||
provider: "xiaomi",
|
||||
baseUrl: "https://api.xiaomimimo.com/anthropic",
|
||||
reasoning: true,
|
||||
input: ["text"],
|
||||
input: ["text", "image"],
|
||||
cost: {
|
||||
input: 0.4,
|
||||
output: 2,
|
||||
@@ -16466,7 +16431,7 @@ export const MODELS = {
|
||||
provider: "xiaomi",
|
||||
baseUrl: "https://api.xiaomimimo.com/anthropic",
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
input: ["text"],
|
||||
cost: {
|
||||
input: 1,
|
||||
output: 3,
|
||||
@@ -16492,8 +16457,8 @@ export const MODELS = {
|
||||
cacheRead: 0.01,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 256000,
|
||||
maxTokens: 64000,
|
||||
contextWindow: 262144,
|
||||
maxTokens: 65536,
|
||||
} satisfies Model<"anthropic-messages">,
|
||||
"mimo-v2-omni": {
|
||||
id: "mimo-v2-omni",
|
||||
@@ -16509,8 +16474,8 @@ export const MODELS = {
|
||||
cacheRead: 0.08,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 256000,
|
||||
maxTokens: 128000,
|
||||
contextWindow: 262144,
|
||||
maxTokens: 131072,
|
||||
} satisfies Model<"anthropic-messages">,
|
||||
"mimo-v2-pro": {
|
||||
id: "mimo-v2-pro",
|
||||
@@ -16526,8 +16491,8 @@ export const MODELS = {
|
||||
cacheRead: 0.2,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 1000000,
|
||||
maxTokens: 128000,
|
||||
contextWindow: 1048576,
|
||||
maxTokens: 131072,
|
||||
} satisfies Model<"anthropic-messages">,
|
||||
"mimo-v2.5": {
|
||||
id: "mimo-v2.5",
|
||||
@@ -16536,7 +16501,7 @@ export const MODELS = {
|
||||
provider: "xiaomi-token-plan-ams",
|
||||
baseUrl: "https://token-plan-ams.xiaomimimo.com/anthropic",
|
||||
reasoning: true,
|
||||
input: ["text"],
|
||||
input: ["text", "image"],
|
||||
cost: {
|
||||
input: 0.4,
|
||||
output: 2,
|
||||
@@ -16553,7 +16518,7 @@ export const MODELS = {
|
||||
provider: "xiaomi-token-plan-ams",
|
||||
baseUrl: "https://token-plan-ams.xiaomimimo.com/anthropic",
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
input: ["text"],
|
||||
cost: {
|
||||
input: 1,
|
||||
output: 3,
|
||||
@@ -16579,8 +16544,8 @@ export const MODELS = {
|
||||
cacheRead: 0.01,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 256000,
|
||||
maxTokens: 64000,
|
||||
contextWindow: 262144,
|
||||
maxTokens: 65536,
|
||||
} satisfies Model<"anthropic-messages">,
|
||||
"mimo-v2-omni": {
|
||||
id: "mimo-v2-omni",
|
||||
@@ -16596,8 +16561,8 @@ export const MODELS = {
|
||||
cacheRead: 0.08,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 256000,
|
||||
maxTokens: 128000,
|
||||
contextWindow: 262144,
|
||||
maxTokens: 131072,
|
||||
} satisfies Model<"anthropic-messages">,
|
||||
"mimo-v2-pro": {
|
||||
id: "mimo-v2-pro",
|
||||
@@ -16613,8 +16578,8 @@ export const MODELS = {
|
||||
cacheRead: 0.2,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 1000000,
|
||||
maxTokens: 128000,
|
||||
contextWindow: 1048576,
|
||||
maxTokens: 131072,
|
||||
} satisfies Model<"anthropic-messages">,
|
||||
"mimo-v2.5": {
|
||||
id: "mimo-v2.5",
|
||||
@@ -16623,7 +16588,7 @@ export const MODELS = {
|
||||
provider: "xiaomi-token-plan-cn",
|
||||
baseUrl: "https://token-plan-cn.xiaomimimo.com/anthropic",
|
||||
reasoning: true,
|
||||
input: ["text"],
|
||||
input: ["text", "image"],
|
||||
cost: {
|
||||
input: 0.4,
|
||||
output: 2,
|
||||
@@ -16640,7 +16605,7 @@ export const MODELS = {
|
||||
provider: "xiaomi-token-plan-cn",
|
||||
baseUrl: "https://token-plan-cn.xiaomimimo.com/anthropic",
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
input: ["text"],
|
||||
cost: {
|
||||
input: 1,
|
||||
output: 3,
|
||||
@@ -16666,8 +16631,8 @@ export const MODELS = {
|
||||
cacheRead: 0.01,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 256000,
|
||||
maxTokens: 64000,
|
||||
contextWindow: 262144,
|
||||
maxTokens: 65536,
|
||||
} satisfies Model<"anthropic-messages">,
|
||||
"mimo-v2-omni": {
|
||||
id: "mimo-v2-omni",
|
||||
@@ -16683,8 +16648,8 @@ export const MODELS = {
|
||||
cacheRead: 0.08,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 256000,
|
||||
maxTokens: 128000,
|
||||
contextWindow: 262144,
|
||||
maxTokens: 131072,
|
||||
} satisfies Model<"anthropic-messages">,
|
||||
"mimo-v2-pro": {
|
||||
id: "mimo-v2-pro",
|
||||
@@ -16700,8 +16665,8 @@ export const MODELS = {
|
||||
cacheRead: 0.2,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 1000000,
|
||||
maxTokens: 128000,
|
||||
contextWindow: 1048576,
|
||||
maxTokens: 131072,
|
||||
} satisfies Model<"anthropic-messages">,
|
||||
"mimo-v2.5": {
|
||||
id: "mimo-v2.5",
|
||||
@@ -16710,7 +16675,7 @@ export const MODELS = {
|
||||
provider: "xiaomi-token-plan-sgp",
|
||||
baseUrl: "https://token-plan-sgp.xiaomimimo.com/anthropic",
|
||||
reasoning: true,
|
||||
input: ["text"],
|
||||
input: ["text", "image"],
|
||||
cost: {
|
||||
input: 0.4,
|
||||
output: 2,
|
||||
@@ -16727,7 +16692,7 @@ export const MODELS = {
|
||||
provider: "xiaomi-token-plan-sgp",
|
||||
baseUrl: "https://token-plan-sgp.xiaomimimo.com/anthropic",
|
||||
reasoning: true,
|
||||
input: ["text", "image"],
|
||||
input: ["text"],
|
||||
cost: {
|
||||
input: 1,
|
||||
output: 3,
|
||||
@@ -16813,7 +16778,7 @@ export const MODELS = {
|
||||
} satisfies Model<"openai-completions">,
|
||||
"glm-5v-turbo": {
|
||||
id: "glm-5v-turbo",
|
||||
name: "glm-5v-turbo",
|
||||
name: "GLM-5V-Turbo",
|
||||
api: "openai-completions",
|
||||
provider: "zai",
|
||||
baseUrl: "https://api.z.ai/api/coding/paas/v4",
|
||||
|
||||
@@ -352,7 +352,7 @@ function buildRequestBody(
|
||||
model: model.id,
|
||||
store: false,
|
||||
stream: true,
|
||||
instructions: context.systemPrompt,
|
||||
instructions: context.systemPrompt || "You are a helpful assistant.",
|
||||
input: messages,
|
||||
text: { verbosity: options?.textVerbosity || "low" },
|
||||
include: ["reasoning.encrypted_content"],
|
||||
|
||||
@@ -160,45 +160,104 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions", OpenA
|
||||
partialArgs?: string;
|
||||
streamIndex?: number;
|
||||
}
|
||||
type StreamingBlock = TextContent | ThinkingContent | StreamingToolCallBlock;
|
||||
type StreamingToolCallDelta = NonNullable<ChatCompletionChunk.Choice.Delta["tool_calls"]>[number];
|
||||
|
||||
let currentBlock: TextContent | ThinkingContent | StreamingToolCallBlock | null = null;
|
||||
const blocks = output.content;
|
||||
const getContentIndex = (block: typeof currentBlock) => (block ? blocks.indexOf(block) : -1);
|
||||
const currentContentIndex = () => getContentIndex(currentBlock);
|
||||
const finishCurrentBlock = (block?: typeof currentBlock) => {
|
||||
if (block) {
|
||||
const contentIndex = getContentIndex(block);
|
||||
if (contentIndex === -1) {
|
||||
return;
|
||||
}
|
||||
if (block.type === "text") {
|
||||
stream.push({
|
||||
type: "text_end",
|
||||
contentIndex,
|
||||
content: block.text,
|
||||
partial: output,
|
||||
});
|
||||
} else if (block.type === "thinking") {
|
||||
stream.push({
|
||||
type: "thinking_end",
|
||||
contentIndex,
|
||||
content: block.thinking,
|
||||
partial: output,
|
||||
});
|
||||
} else if (block.type === "toolCall") {
|
||||
block.arguments = parseStreamingJson(block.partialArgs);
|
||||
// Finalize in-place and strip the scratch buffers so replay only
|
||||
// carries parsed arguments.
|
||||
delete block.partialArgs;
|
||||
delete block.streamIndex;
|
||||
stream.push({
|
||||
type: "toolcall_end",
|
||||
contentIndex,
|
||||
toolCall: block,
|
||||
partial: output,
|
||||
});
|
||||
}
|
||||
let textBlock: TextContent | null = null;
|
||||
let thinkingBlock: ThinkingContent | null = null;
|
||||
const toolCallBlocksByIndex = new Map<number, StreamingToolCallBlock>();
|
||||
const toolCallBlocksById = new Map<string, StreamingToolCallBlock>();
|
||||
const blocks = output.content as StreamingBlock[];
|
||||
const getContentIndex = (block: StreamingBlock) => blocks.indexOf(block);
|
||||
const finishBlock = (block: StreamingBlock) => {
|
||||
const contentIndex = getContentIndex(block);
|
||||
if (contentIndex === -1) {
|
||||
return;
|
||||
}
|
||||
if (block.type === "text") {
|
||||
stream.push({
|
||||
type: "text_end",
|
||||
contentIndex,
|
||||
content: block.text,
|
||||
partial: output,
|
||||
});
|
||||
} else if (block.type === "thinking") {
|
||||
stream.push({
|
||||
type: "thinking_end",
|
||||
contentIndex,
|
||||
content: block.thinking,
|
||||
partial: output,
|
||||
});
|
||||
} else if (block.type === "toolCall") {
|
||||
block.arguments = parseStreamingJson(block.partialArgs);
|
||||
// Finalize in-place and strip the scratch buffers so replay only
|
||||
// carries parsed arguments.
|
||||
delete block.partialArgs;
|
||||
delete block.streamIndex;
|
||||
stream.push({
|
||||
type: "toolcall_end",
|
||||
contentIndex,
|
||||
toolCall: block,
|
||||
partial: output,
|
||||
});
|
||||
}
|
||||
};
|
||||
const ensureTextBlock = () => {
|
||||
if (!textBlock) {
|
||||
textBlock = { type: "text", text: "" };
|
||||
blocks.push(textBlock);
|
||||
stream.push({ type: "text_start", contentIndex: getContentIndex(textBlock), partial: output });
|
||||
}
|
||||
return textBlock;
|
||||
};
|
||||
const ensureThinkingBlock = (thinkingSignature: string) => {
|
||||
if (!thinkingBlock) {
|
||||
thinkingBlock = {
|
||||
type: "thinking",
|
||||
thinking: "",
|
||||
thinkingSignature,
|
||||
};
|
||||
blocks.push(thinkingBlock);
|
||||
stream.push({ type: "thinking_start", contentIndex: getContentIndex(thinkingBlock), partial: output });
|
||||
}
|
||||
return thinkingBlock;
|
||||
};
|
||||
const ensureToolCallBlock = (toolCall: StreamingToolCallDelta) => {
|
||||
const streamIndex = typeof toolCall.index === "number" ? toolCall.index : undefined;
|
||||
let block = streamIndex !== undefined ? toolCallBlocksByIndex.get(streamIndex) : undefined;
|
||||
if (!block && toolCall.id) {
|
||||
block = toolCallBlocksById.get(toolCall.id);
|
||||
}
|
||||
if (!block) {
|
||||
block = {
|
||||
type: "toolCall",
|
||||
id: toolCall.id || "",
|
||||
name: toolCall.function?.name || "",
|
||||
arguments: {},
|
||||
partialArgs: "",
|
||||
streamIndex,
|
||||
};
|
||||
if (streamIndex !== undefined) {
|
||||
toolCallBlocksByIndex.set(streamIndex, block);
|
||||
}
|
||||
if (toolCall.id) {
|
||||
toolCallBlocksById.set(toolCall.id, block);
|
||||
}
|
||||
blocks.push(block);
|
||||
stream.push({
|
||||
type: "toolcall_start",
|
||||
contentIndex: getContentIndex(block),
|
||||
partial: output,
|
||||
});
|
||||
}
|
||||
if (streamIndex !== undefined && block.streamIndex === undefined) {
|
||||
block.streamIndex = streamIndex;
|
||||
toolCallBlocksByIndex.set(streamIndex, block);
|
||||
}
|
||||
if (toolCall.id) {
|
||||
toolCallBlocksById.set(toolCall.id, block);
|
||||
}
|
||||
return block;
|
||||
};
|
||||
|
||||
for await (const chunk of openaiStream) {
|
||||
@@ -237,22 +296,14 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions", OpenA
|
||||
choice.delta.content !== undefined &&
|
||||
choice.delta.content.length > 0
|
||||
) {
|
||||
if (!currentBlock || currentBlock.type !== "text") {
|
||||
finishCurrentBlock(currentBlock);
|
||||
currentBlock = { type: "text", text: "" };
|
||||
output.content.push(currentBlock);
|
||||
stream.push({ type: "text_start", contentIndex: currentContentIndex(), partial: output });
|
||||
}
|
||||
|
||||
if (currentBlock.type === "text") {
|
||||
currentBlock.text += choice.delta.content;
|
||||
stream.push({
|
||||
type: "text_delta",
|
||||
contentIndex: currentContentIndex(),
|
||||
delta: choice.delta.content,
|
||||
partial: output,
|
||||
});
|
||||
}
|
||||
const block = ensureTextBlock();
|
||||
block.text += choice.delta.content;
|
||||
stream.push({
|
||||
type: "text_delta",
|
||||
contentIndex: getContentIndex(block),
|
||||
delta: choice.delta.content,
|
||||
partial: output,
|
||||
});
|
||||
}
|
||||
|
||||
// Some endpoints return reasoning in reasoning_content (llama.cpp),
|
||||
@@ -260,38 +311,24 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions", OpenA
|
||||
// Use the first non-empty reasoning field to avoid duplication
|
||||
// (e.g., chutes.ai returns both reasoning_content and reasoning with same content)
|
||||
const reasoningFields = ["reasoning_content", "reasoning", "reasoning_text"];
|
||||
const deltaFields = choice.delta as Record<string, unknown>;
|
||||
let foundReasoningField: string | null = null;
|
||||
for (const field of reasoningFields) {
|
||||
if (
|
||||
(choice.delta as any)[field] !== null &&
|
||||
(choice.delta as any)[field] !== undefined &&
|
||||
(choice.delta as any)[field].length > 0
|
||||
) {
|
||||
if (!foundReasoningField) {
|
||||
foundReasoningField = field;
|
||||
break;
|
||||
}
|
||||
const value = deltaFields[field];
|
||||
if (typeof value === "string" && value.length > 0) {
|
||||
foundReasoningField = field;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (foundReasoningField) {
|
||||
if (!currentBlock || currentBlock.type !== "thinking") {
|
||||
finishCurrentBlock(currentBlock);
|
||||
currentBlock = {
|
||||
type: "thinking",
|
||||
thinking: "",
|
||||
thinkingSignature: foundReasoningField,
|
||||
};
|
||||
output.content.push(currentBlock);
|
||||
stream.push({ type: "thinking_start", contentIndex: currentContentIndex(), partial: output });
|
||||
}
|
||||
|
||||
if (currentBlock.type === "thinking") {
|
||||
const delta = (choice.delta as any)[foundReasoningField];
|
||||
currentBlock.thinking += delta;
|
||||
const delta = deltaFields[foundReasoningField];
|
||||
if (typeof delta === "string" && delta.length > 0) {
|
||||
const block = ensureThinkingBlock(foundReasoningField);
|
||||
block.thinking += delta;
|
||||
stream.push({
|
||||
type: "thinking_delta",
|
||||
contentIndex: currentContentIndex(),
|
||||
contentIndex: getContentIndex(block),
|
||||
delta,
|
||||
partial: output,
|
||||
});
|
||||
@@ -300,52 +337,27 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions", OpenA
|
||||
|
||||
if (choice?.delta?.tool_calls) {
|
||||
for (const toolCall of choice.delta.tool_calls) {
|
||||
const streamIndex = typeof toolCall.index === "number" ? toolCall.index : undefined;
|
||||
const sameToolCall =
|
||||
currentBlock?.type === "toolCall" &&
|
||||
((streamIndex !== undefined && currentBlock.streamIndex === streamIndex) ||
|
||||
(streamIndex === undefined && toolCall.id && currentBlock.id === toolCall.id));
|
||||
|
||||
if (!sameToolCall) {
|
||||
finishCurrentBlock(currentBlock);
|
||||
currentBlock = {
|
||||
type: "toolCall",
|
||||
id: toolCall.id || "",
|
||||
name: toolCall.function?.name || "",
|
||||
arguments: {},
|
||||
partialArgs: "",
|
||||
streamIndex,
|
||||
};
|
||||
output.content.push(currentBlock);
|
||||
stream.push({
|
||||
type: "toolcall_start",
|
||||
contentIndex: getContentIndex(currentBlock),
|
||||
partial: output,
|
||||
});
|
||||
const block = ensureToolCallBlock(toolCall);
|
||||
if (!block.id && toolCall.id) {
|
||||
block.id = toolCall.id;
|
||||
toolCallBlocksById.set(toolCall.id, block);
|
||||
}
|
||||
if (!block.name && toolCall.function?.name) {
|
||||
block.name = toolCall.function.name;
|
||||
}
|
||||
|
||||
const currentToolCallBlock = currentBlock?.type === "toolCall" ? currentBlock : null;
|
||||
if (currentToolCallBlock) {
|
||||
if (!currentToolCallBlock.id && toolCall.id) currentToolCallBlock.id = toolCall.id;
|
||||
if (!currentToolCallBlock.name && toolCall.function?.name) {
|
||||
currentToolCallBlock.name = toolCall.function.name;
|
||||
}
|
||||
if (currentToolCallBlock.streamIndex === undefined && streamIndex !== undefined) {
|
||||
currentToolCallBlock.streamIndex = streamIndex;
|
||||
}
|
||||
let delta = "";
|
||||
if (toolCall.function?.arguments) {
|
||||
delta = toolCall.function.arguments;
|
||||
currentToolCallBlock.partialArgs += toolCall.function.arguments;
|
||||
currentToolCallBlock.arguments = parseStreamingJson(currentToolCallBlock.partialArgs);
|
||||
}
|
||||
stream.push({
|
||||
type: "toolcall_delta",
|
||||
contentIndex: getContentIndex(currentToolCallBlock),
|
||||
delta,
|
||||
partial: output,
|
||||
});
|
||||
let delta = "";
|
||||
if (toolCall.function?.arguments) {
|
||||
delta = toolCall.function.arguments;
|
||||
block.partialArgs = (block.partialArgs ?? "") + toolCall.function.arguments;
|
||||
block.arguments = parseStreamingJson(block.partialArgs);
|
||||
}
|
||||
stream.push({
|
||||
type: "toolcall_delta",
|
||||
contentIndex: getContentIndex(block),
|
||||
delta,
|
||||
partial: output,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -365,7 +377,9 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions", OpenA
|
||||
}
|
||||
}
|
||||
|
||||
finishCurrentBlock(currentBlock);
|
||||
for (const block of blocks) {
|
||||
finishBlock(block);
|
||||
}
|
||||
if (options?.signal?.aborted) {
|
||||
throw new Error("Request was aborted");
|
||||
}
|
||||
|
||||
@@ -354,6 +354,16 @@ export async function processResponsesStream<TApi extends Api>(
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (event.type === "response.reasoning_text.delta") {
|
||||
if (currentItem?.type === "reasoning" && currentBlock?.type === "thinking") {
|
||||
currentBlock.thinking += event.delta;
|
||||
stream.push({
|
||||
type: "thinking_delta",
|
||||
contentIndex: blockIndex(),
|
||||
delta: event.delta,
|
||||
partial: output,
|
||||
});
|
||||
}
|
||||
} else if (event.type === "response.content_part.added") {
|
||||
if (currentItem?.type === "message") {
|
||||
currentItem.content = currentItem.content || [];
|
||||
@@ -429,7 +439,9 @@ export async function processResponsesStream<TApi extends Api>(
|
||||
const item = event.item;
|
||||
|
||||
if (item.type === "reasoning" && currentBlock?.type === "thinking") {
|
||||
currentBlock.thinking = item.summary?.map((s) => s.text).join("\n\n") || "";
|
||||
const summaryText = item.summary?.map((s) => s.text).join("\n\n") || "";
|
||||
const contentText = item.content?.map((c) => c.text).join("\n\n") || "";
|
||||
currentBlock.thinking = summaryText || contentText || currentBlock.thinking;
|
||||
currentBlock.thinkingSignature = JSON.stringify(item);
|
||||
stream.push({
|
||||
type: "thinking_end",
|
||||
|
||||
@@ -30,7 +30,7 @@ const SCOPE = "openid profile email offline_access";
|
||||
const JWT_CLAIM_PATH = "https://api.openai.com/auth";
|
||||
|
||||
type TokenSuccess = { type: "success"; access: string; refresh: string; expires: number };
|
||||
type TokenFailure = { type: "failed" };
|
||||
type TokenFailure = { type: "failed"; message: string; status?: number };
|
||||
type TokenResult = TokenSuccess | TokenFailure;
|
||||
|
||||
type JwtPayload = {
|
||||
@@ -108,8 +108,11 @@ async function exchangeAuthorizationCode(
|
||||
|
||||
if (!response.ok) {
|
||||
const text = await response.text().catch(() => "");
|
||||
console.error("[openai-codex] code->token failed:", response.status, text);
|
||||
return { type: "failed" };
|
||||
return {
|
||||
type: "failed",
|
||||
status: response.status,
|
||||
message: `OpenAI Codex token exchange failed (${response.status}): ${text || response.statusText}`,
|
||||
};
|
||||
}
|
||||
|
||||
const json = (await response.json()) as {
|
||||
@@ -119,8 +122,10 @@ async function exchangeAuthorizationCode(
|
||||
};
|
||||
|
||||
if (!json.access_token || !json.refresh_token || typeof json.expires_in !== "number") {
|
||||
console.error("[openai-codex] token response missing fields:", json);
|
||||
return { type: "failed" };
|
||||
return {
|
||||
type: "failed",
|
||||
message: `OpenAI Codex token exchange response missing fields: ${JSON.stringify(json)}`,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -145,8 +150,11 @@ async function refreshAccessToken(refreshToken: string): Promise<TokenResult> {
|
||||
|
||||
if (!response.ok) {
|
||||
const text = await response.text().catch(() => "");
|
||||
console.error("[openai-codex] Token refresh failed:", response.status, text);
|
||||
return { type: "failed" };
|
||||
return {
|
||||
type: "failed",
|
||||
status: response.status,
|
||||
message: `OpenAI Codex token refresh failed (${response.status}): ${text || response.statusText}`,
|
||||
};
|
||||
}
|
||||
|
||||
const json = (await response.json()) as {
|
||||
@@ -156,8 +164,10 @@ async function refreshAccessToken(refreshToken: string): Promise<TokenResult> {
|
||||
};
|
||||
|
||||
if (!json.access_token || !json.refresh_token || typeof json.expires_in !== "number") {
|
||||
console.error("[openai-codex] Token refresh response missing fields:", json);
|
||||
return { type: "failed" };
|
||||
return {
|
||||
type: "failed",
|
||||
message: `OpenAI Codex token refresh response missing fields: ${JSON.stringify(json)}`,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -167,8 +177,10 @@ async function refreshAccessToken(refreshToken: string): Promise<TokenResult> {
|
||||
expires: Date.now() + json.expires_in * 1000,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("[openai-codex] Token refresh error:", error);
|
||||
return { type: "failed" };
|
||||
return {
|
||||
type: "failed",
|
||||
message: `OpenAI Codex token refresh error: ${error instanceof Error ? error.message : String(error)}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -258,12 +270,7 @@ function startLocalOAuthServer(state: string): Promise<OAuthServerInfo> {
|
||||
waitForCode: () => waitForCodePromise,
|
||||
});
|
||||
})
|
||||
.on("error", (err: NodeJS.ErrnoException) => {
|
||||
console.error(
|
||||
`[openai-codex] Failed to bind http://${CALLBACK_HOST}:1455 (`,
|
||||
err.code,
|
||||
") Falling back to manual paste.",
|
||||
);
|
||||
.on("error", (_err: NodeJS.ErrnoException) => {
|
||||
settleWait?.(null);
|
||||
resolve({
|
||||
close: () => {
|
||||
@@ -386,7 +393,7 @@ export async function loginOpenAICodex(options: {
|
||||
|
||||
const tokenResult = await exchangeAuthorizationCode(code, verifier);
|
||||
if (tokenResult.type !== "success") {
|
||||
throw new Error("Token exchange failed");
|
||||
throw new Error(tokenResult.message);
|
||||
}
|
||||
|
||||
const accountId = getAccountId(tokenResult.access);
|
||||
@@ -411,7 +418,7 @@ export async function loginOpenAICodex(options: {
|
||||
export async function refreshOpenAICodexToken(refreshToken: string): Promise<OAuthCredentials> {
|
||||
const result = await refreshAccessToken(refreshToken);
|
||||
if (result.type !== "success") {
|
||||
throw new Error("Failed to refresh OpenAI Codex token");
|
||||
throw new Error(result.message);
|
||||
}
|
||||
|
||||
const accountId = getAccountId(result.access);
|
||||
|
||||
@@ -23,11 +23,23 @@ export type OAuthAuthInfo = {
|
||||
instructions?: string;
|
||||
};
|
||||
|
||||
export type OAuthSelectOption = {
|
||||
id: string;
|
||||
label: string;
|
||||
};
|
||||
|
||||
export type OAuthSelectPrompt = {
|
||||
message: string;
|
||||
options: OAuthSelectOption[];
|
||||
};
|
||||
|
||||
export interface OAuthLoginCallbacks {
|
||||
onAuth: (info: OAuthAuthInfo) => void;
|
||||
onPrompt: (prompt: OAuthPrompt) => Promise<string>;
|
||||
onProgress?: (message: string) => void;
|
||||
onManualCodeInput?: () => Promise<string>;
|
||||
/** Show an interactive selector and return the selected option id, or undefined on cancel. */
|
||||
onSelect?: (prompt: OAuthSelectPrompt) => Promise<string | undefined>;
|
||||
signal?: AbortSignal;
|
||||
}
|
||||
|
||||
|
||||
32
packages/ai/test/openai-codex-oauth.test.ts
Normal file
32
packages/ai/test/openai-codex-oauth.test.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { refreshOpenAICodexToken } from "../src/utils/oauth/openai-codex.js";
|
||||
|
||||
describe("OpenAI Codex OAuth", () => {
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it("does not write token refresh failures to stderr", async () => {
|
||||
const consoleError = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn(async (): Promise<Response> => {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
error: {
|
||||
message: "Could not validate your token. Please try signing in again.",
|
||||
type: "invalid_request_error",
|
||||
},
|
||||
}),
|
||||
{ status: 401, statusText: "Unauthorized", headers: { "Content-Type": "application/json" } },
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
await expect(refreshOpenAICodexToken("invalid-refresh-token")).rejects.toThrow(
|
||||
/OpenAI Codex token refresh failed \(401\).*Could not validate your token/,
|
||||
);
|
||||
expect(consoleError).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -552,6 +552,238 @@ describe("openai-completions tool_choice", () => {
|
||||
expect(toolCall).not.toHaveProperty("partialArgs");
|
||||
});
|
||||
|
||||
it("accumulates mixed content, reasoning, and parallel tool call deltas independently", async () => {
|
||||
mockState.chunks = [
|
||||
{
|
||||
id: "chatcmpl-mixed-deltas",
|
||||
choices: [
|
||||
{
|
||||
delta: {
|
||||
content: "answer 1",
|
||||
reasoning_content: "think 1",
|
||||
tool_calls: [
|
||||
{
|
||||
index: 0,
|
||||
id: "tc_read_initial",
|
||||
type: "function",
|
||||
function: { name: "read", arguments: '{"path":"README' },
|
||||
},
|
||||
{
|
||||
index: 1,
|
||||
id: "tc_grep_initial",
|
||||
type: "function",
|
||||
function: { name: "grep", arguments: '{"pattern":"TODO' },
|
||||
},
|
||||
{
|
||||
id: "tc_list_no_index",
|
||||
type: "function",
|
||||
function: { name: "list", arguments: '{"path":"packages' },
|
||||
},
|
||||
{
|
||||
id: "tc_write_no_index",
|
||||
type: "function",
|
||||
function: { name: "write", arguments: '{"path":"out' },
|
||||
},
|
||||
],
|
||||
},
|
||||
finish_reason: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "chatcmpl-mixed-deltas",
|
||||
choices: [
|
||||
{
|
||||
delta: {
|
||||
content: " answer 2",
|
||||
tool_calls: [
|
||||
{
|
||||
index: 1,
|
||||
id: "tc_grep_changed",
|
||||
type: "function",
|
||||
function: { arguments: '","path":"src' },
|
||||
},
|
||||
{
|
||||
id: "tc_write_no_index",
|
||||
type: "function",
|
||||
function: { arguments: '.txt","content":"ok"}' },
|
||||
},
|
||||
{
|
||||
id: "tc_list_no_index",
|
||||
type: "function",
|
||||
function: { arguments: '/ai"}' },
|
||||
},
|
||||
],
|
||||
},
|
||||
finish_reason: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "chatcmpl-mixed-deltas",
|
||||
choices: [
|
||||
{
|
||||
delta: {
|
||||
content: "\n",
|
||||
reasoning_content: " think 2",
|
||||
tool_calls: [
|
||||
{
|
||||
index: 0,
|
||||
id: "tc_read_changed",
|
||||
type: "function",
|
||||
function: { arguments: '.md"}' },
|
||||
},
|
||||
{
|
||||
index: 1,
|
||||
type: "function",
|
||||
function: { arguments: '"}' },
|
||||
},
|
||||
],
|
||||
},
|
||||
finish_reason: "tool_calls",
|
||||
},
|
||||
],
|
||||
usage: {
|
||||
prompt_tokens: 10,
|
||||
completion_tokens: 8,
|
||||
prompt_tokens_details: { cached_tokens: 0 },
|
||||
completion_tokens_details: { reasoning_tokens: 2 },
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const { compat: _compat, ...baseModel } = getModel("openai", "gpt-4o-mini")!;
|
||||
const model = { ...baseModel, api: "openai-completions" } as const;
|
||||
const tools: Tool[] = [
|
||||
{
|
||||
name: "read",
|
||||
description: "Read a file",
|
||||
parameters: Type.Object({ path: Type.String() }),
|
||||
},
|
||||
{
|
||||
name: "grep",
|
||||
description: "Search a file",
|
||||
parameters: Type.Object({ pattern: Type.String(), path: Type.String() }),
|
||||
},
|
||||
{
|
||||
name: "list",
|
||||
description: "List a directory",
|
||||
parameters: Type.Object({ path: Type.String() }),
|
||||
},
|
||||
{
|
||||
name: "write",
|
||||
description: "Write a file",
|
||||
parameters: Type.Object({ path: Type.String(), content: Type.String() }),
|
||||
},
|
||||
];
|
||||
const s = streamSimple(
|
||||
model,
|
||||
{
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: "Think, answer, and use tools.",
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
],
|
||||
tools,
|
||||
},
|
||||
{ apiKey: "test" },
|
||||
);
|
||||
|
||||
const eventTypes: string[] = [];
|
||||
const toolEventsByContentIndex = new Map<number, string[]>();
|
||||
for await (const event of s) {
|
||||
eventTypes.push(event.type);
|
||||
if (event.type === "toolcall_start" || event.type === "toolcall_delta" || event.type === "toolcall_end") {
|
||||
const events = toolEventsByContentIndex.get(event.contentIndex) ?? [];
|
||||
events.push(event.type);
|
||||
toolEventsByContentIndex.set(event.contentIndex, events);
|
||||
}
|
||||
}
|
||||
|
||||
const response = await s.result();
|
||||
expect(response.stopReason).toBe("toolUse");
|
||||
expect(eventTypes.filter((type) => type === "text_start")).toHaveLength(1);
|
||||
expect(eventTypes.filter((type) => type === "text_delta")).toHaveLength(3);
|
||||
expect(eventTypes.filter((type) => type === "text_end")).toHaveLength(1);
|
||||
expect(eventTypes.filter((type) => type === "thinking_start")).toHaveLength(1);
|
||||
expect(eventTypes.filter((type) => type === "thinking_delta")).toHaveLength(2);
|
||||
expect(eventTypes.filter((type) => type === "thinking_end")).toHaveLength(1);
|
||||
expect(eventTypes.filter((type) => type === "toolcall_start")).toHaveLength(4);
|
||||
expect(eventTypes.filter((type) => type === "toolcall_delta")).toHaveLength(9);
|
||||
expect(eventTypes.filter((type) => type === "toolcall_end")).toHaveLength(4);
|
||||
expect(toolEventsByContentIndex.get(2)).toEqual([
|
||||
"toolcall_start",
|
||||
"toolcall_delta",
|
||||
"toolcall_delta",
|
||||
"toolcall_end",
|
||||
]);
|
||||
expect(toolEventsByContentIndex.get(3)).toEqual([
|
||||
"toolcall_start",
|
||||
"toolcall_delta",
|
||||
"toolcall_delta",
|
||||
"toolcall_delta",
|
||||
"toolcall_end",
|
||||
]);
|
||||
expect(toolEventsByContentIndex.get(4)).toEqual([
|
||||
"toolcall_start",
|
||||
"toolcall_delta",
|
||||
"toolcall_delta",
|
||||
"toolcall_end",
|
||||
]);
|
||||
expect(toolEventsByContentIndex.get(5)).toEqual([
|
||||
"toolcall_start",
|
||||
"toolcall_delta",
|
||||
"toolcall_delta",
|
||||
"toolcall_end",
|
||||
]);
|
||||
|
||||
expect(response.content).toHaveLength(6);
|
||||
expect(response.content[0]).toEqual({ type: "text", text: "answer 1 answer 2\n" });
|
||||
expect(response.content[1]).toEqual({
|
||||
type: "thinking",
|
||||
thinking: "think 1 think 2",
|
||||
thinkingSignature: "reasoning_content",
|
||||
});
|
||||
const readCall = response.content[2];
|
||||
const grepCall = response.content[3];
|
||||
const listCall = response.content[4];
|
||||
const writeCall = response.content[5];
|
||||
expect(readCall.type).toBe("toolCall");
|
||||
expect(grepCall.type).toBe("toolCall");
|
||||
expect(listCall.type).toBe("toolCall");
|
||||
expect(writeCall.type).toBe("toolCall");
|
||||
if (
|
||||
readCall.type !== "toolCall" ||
|
||||
grepCall.type !== "toolCall" ||
|
||||
listCall.type !== "toolCall" ||
|
||||
writeCall.type !== "toolCall"
|
||||
) {
|
||||
throw new Error("Expected toolCall content");
|
||||
}
|
||||
expect(readCall.id).toBe("tc_read_initial");
|
||||
expect(readCall.name).toBe("read");
|
||||
expect(readCall.arguments).toEqual({ path: "README.md" });
|
||||
expect(readCall).not.toHaveProperty("streamIndex");
|
||||
expect(readCall).not.toHaveProperty("partialArgs");
|
||||
expect(grepCall.id).toBe("tc_grep_initial");
|
||||
expect(grepCall.name).toBe("grep");
|
||||
expect(grepCall.arguments).toEqual({ pattern: "TODO", path: "src" });
|
||||
expect(grepCall).not.toHaveProperty("streamIndex");
|
||||
expect(grepCall).not.toHaveProperty("partialArgs");
|
||||
expect(listCall.id).toBe("tc_list_no_index");
|
||||
expect(listCall.name).toBe("list");
|
||||
expect(listCall.arguments).toEqual({ path: "packages/ai" });
|
||||
expect(listCall).not.toHaveProperty("streamIndex");
|
||||
expect(listCall).not.toHaveProperty("partialArgs");
|
||||
expect(writeCall.id).toBe("tc_write_no_index");
|
||||
expect(writeCall.name).toBe("write");
|
||||
expect(writeCall.arguments).toEqual({ path: "out.txt", content: "ok" });
|
||||
expect(writeCall).not.toHaveProperty("streamIndex");
|
||||
expect(writeCall).not.toHaveProperty("partialArgs");
|
||||
});
|
||||
|
||||
it("does not double-count reasoning tokens in completion usage", async () => {
|
||||
mockState.chunks = [
|
||||
{
|
||||
|
||||
@@ -2,13 +2,53 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Breaking Changes
|
||||
## [0.73.1] - 2026-05-07
|
||||
|
||||
- Switched the built-in `xiaomi` provider from Token Plan AMS to Xiaomi's API billing endpoint, and renamed its `/login` display from "Xiaomi MiMo Token Plan" to "Xiaomi MiMo". `XIAOMI_API_KEY` now refers to the API billing key from [platform.xiaomimimo.com](https://platform.xiaomimimo.com). Users on Token Plan should switch to the appropriate `xiaomi-token-plan-*` provider and set the corresponding env var.
|
||||
### New Features
|
||||
|
||||
- **Self-update support for the npm scope migration**: `pi update --self` now supports the upcoming package rename from `@mariozechner/pi-coding-agent` to `@earendil-works/pi-coding-agent`. After the new package is published, existing global installs can update through the normal self-update flow; pi will uninstall the old global package and install the package name returned by the version check endpoint.
|
||||
- **Interactive OAuth login selection**: OAuth providers can now present multiple login choices in `/login`, enabling provider-specific interactive authentication flows. See [Providers](docs/providers.md).
|
||||
- **JSONC-style `models.json` parsing**: `models.json` now allows comments and trailing commas, making custom provider and model configuration easier to maintain. See [Providers](docs/providers.md) and [Custom Providers](docs/custom-provider.md).
|
||||
|
||||
### Added
|
||||
|
||||
- Added three Xiaomi MiMo Token Plan regional providers visible in `/login`: `xiaomi-token-plan-cn` (`XIAOMI_TOKEN_PLAN_CN_API_KEY`), `xiaomi-token-plan-ams` (`XIAOMI_TOKEN_PLAN_AMS_API_KEY`), `xiaomi-token-plan-sgp` (`XIAOMI_TOKEN_PLAN_SGP_API_KEY`). Each defaults to `mimo-v2.5-pro`.
|
||||
- Added interactive login selection support so OAuth providers can present multiple login choices ([#4190](https://github.com/earendil-works/pi-mono/pull/4190) by [@mitsuhiko](https://github.com/mitsuhiko)).
|
||||
|
||||
### Changed
|
||||
|
||||
- Changed `pi update --self` to honor the active package name returned by the Pi version check endpoint, defaulting to the current package when omitted and uninstalling the old global package before installing a renamed package.
|
||||
- Changed extension loading to use upstream `jiti` 2.7 instead of the `@mariozechner/jiti` fork ([#4244](https://github.com/earendil-works/pi-mono/pull/4244) by [@pi0](https://github.com/pi0)).
|
||||
- Changed `models.json` parsing to allow comments and trailing commas ([#4162](https://github.com/earendil-works/pi-mono/pull/4162) by [@julien-c](https://github.com/julien-c)).
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed `pi -p` treating prompts that start with YAML frontmatter as extension flags instead of user messages ([#4163](https://github.com/badlogic/pi-mono/issues/4163)).
|
||||
- Fixed pending tool results not updating in the live TUI after toggling thinking block visibility while the tool is running ([#4167](https://github.com/badlogic/pi-mono/issues/4167)).
|
||||
- Fixed `/copy` reporting success on Linux without writing the clipboard on Wayland-only compositors (Hyprland, Niri, ...) by skipping the X11-only native addon on Linux and routing through `wl-copy`/`xclip`/`xsel` instead ([#4177](https://github.com/badlogic/pi-mono/issues/4177)).
|
||||
- Fixed HTML session exports to strip skill wrapper XML from rendered user messages ([#4234](https://github.com/earendil-works/pi-mono/pull/4234) by [@aliou](https://github.com/aliou)).
|
||||
- Fixed OpenAI-compatible chat completion streams that interleave content and tool-call deltas in the same choice.
|
||||
- Fixed OpenAI Codex OAuth refresh failures writing directly to stderr while the TUI is active ([#4141](https://github.com/badlogic/pi-mono/issues/4141)).
|
||||
- Fixed OpenAI Codex Responses requests to send a non-empty system prompt ([#4184](https://github.com/earendil-works/pi-mono/issues/4184)).
|
||||
- Fixed Kimi For Coding model resolution for the Kimi K2 P6 alias ([#4218](https://github.com/earendil-works/pi-mono/issues/4218)).
|
||||
- Fixed Kitty inline image redraws to stay within TUI-owned terminal regions and avoid writing below the active viewport.
|
||||
- Fixed Kitty inline image rendering by letting the terminal allocate image ids and bounding parsed image ids to valid values.
|
||||
- Fixed inline image capability detection to disable inline images in cmux terminals.
|
||||
|
||||
## [0.73.0] - 2026-05-04
|
||||
|
||||
### New Features
|
||||
|
||||
- **Xiaomi MiMo API billing and regional Token Plan providers** - `xiaomi` now uses API billing, with separate `xiaomi-token-plan-{cn,ams,sgp}` providers. See [docs/providers.md#api-keys](docs/providers.md#api-keys) and [README.md#providers--models](README.md#providers--models). ([#4112](https://github.com/badlogic/pi-mono/pull/4112) by [@Phoen1xCode](https://github.com/Phoen1xCode))
|
||||
- **Incremental bash output streaming** - Bash tool output now appears while commands run instead of only after completion. ([#4145](https://github.com/badlogic/pi-mono/issues/4145))
|
||||
- **Compact read rendering** - Interactive `read` output for Pi docs, context files, and skills is collapsed by default and shows selected line ranges.
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
- Switched the built-in `xiaomi` provider from Token Plan AMS to Xiaomi's API billing endpoint, and renamed its `/login` display from "Xiaomi MiMo Token Plan" to "Xiaomi MiMo". `XIAOMI_API_KEY` now refers to the API billing key from [platform.xiaomimimo.com](https://platform.xiaomimimo.com). Users on Token Plan should switch to the appropriate `xiaomi-token-plan-*` provider and set the corresponding env var ([#4112](https://github.com/badlogic/pi-mono/pull/4112) by [@Phoen1xCode](https://github.com/Phoen1xCode)).
|
||||
|
||||
### Added
|
||||
|
||||
- Added three Xiaomi MiMo Token Plan regional providers visible in `/login`: `xiaomi-token-plan-cn` (`XIAOMI_TOKEN_PLAN_CN_API_KEY`), `xiaomi-token-plan-ams` (`XIAOMI_TOKEN_PLAN_AMS_API_KEY`), `xiaomi-token-plan-sgp` (`XIAOMI_TOKEN_PLAN_SGP_API_KEY`). Each defaults to `mimo-v2.5-pro` ([#4112](https://github.com/badlogic/pi-mono/pull/4112) by [@Phoen1xCode](https://github.com/Phoen1xCode)).
|
||||
|
||||
### Changed
|
||||
|
||||
@@ -16,7 +56,14 @@
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed generated OpenAI-compatible model metadata for Qwen 3.5/3.6 and MiniMax M2.7, so those models work through the built-in provider catalog ([#4110](https://github.com/badlogic/pi-mono/pull/4110) by [@jsynowiec](https://github.com/jsynowiec)).
|
||||
- Fixed Bedrock Claude Opus 4.7 `xhigh` thinking requests by preserving the provider's native effort value.
|
||||
- Fixed OpenAI Codex WebSocket transport to fall back to SSE when setup fails before streaming starts, and surface transport diagnostics in the assistant message ([#4133](https://github.com/badlogic/pi-mono/issues/4133)).
|
||||
- Fixed OpenAI Codex WebSocket transport keeping `--print` and JSON mode processes alive after the response by closing cached WebSocket sessions during session shutdown ([#4103](https://github.com/badlogic/pi-mono/issues/4103)).
|
||||
- Fixed compact `read` tool calls to render directly and include selected line ranges in interactive output.
|
||||
- Fixed interactive sessions to exit when terminal input is lost instead of continuing in a broken state.
|
||||
- Fixed bash tool output to stream incrementally while commands run instead of waiting for command completion ([#4145](https://github.com/badlogic/pi-mono/issues/4145)).
|
||||
- Fixed selector and autocomplete fuzzy ranking to prioritize exact matches.
|
||||
|
||||
## [0.72.1] - 2026-05-02
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "pi-extension-custom-provider",
|
||||
"version": "0.72.1",
|
||||
"version": "0.73.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "pi-extension-custom-provider",
|
||||
"version": "0.72.1",
|
||||
"version": "0.73.1",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.52.0"
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "pi-extension-custom-provider-anthropic",
|
||||
"private": true,
|
||||
"version": "0.72.1",
|
||||
"version": "0.73.1",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"clean": "echo 'nothing to clean'",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "pi-extension-custom-provider-gitlab-duo",
|
||||
"private": true,
|
||||
"version": "0.72.1",
|
||||
"version": "0.73.1",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"clean": "echo 'nothing to clean'",
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "pi-extension-sandbox",
|
||||
"version": "1.2.1",
|
||||
"version": "1.3.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "pi-extension-sandbox",
|
||||
"version": "1.2.1",
|
||||
"version": "1.3.1",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sandbox-runtime": "^0.0.26"
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "pi-extension-sandbox",
|
||||
"private": true,
|
||||
"version": "1.2.1",
|
||||
"version": "1.3.1",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"clean": "echo 'nothing to clean'",
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "pi-extension-with-deps",
|
||||
"version": "0.72.1",
|
||||
"version": "0.73.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "pi-extension-with-deps",
|
||||
"version": "0.72.1",
|
||||
"version": "0.73.1",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "pi-extension-with-deps",
|
||||
"private": true,
|
||||
"version": "0.72.1",
|
||||
"version": "0.73.1",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"clean": "echo 'nothing to clean'",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@mariozechner/pi-coding-agent",
|
||||
"version": "0.72.1",
|
||||
"version": "0.73.1",
|
||||
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
||||
"type": "module",
|
||||
"piConfig": {
|
||||
@@ -38,10 +38,9 @@
|
||||
"prepublishOnly": "npm run clean && npm run build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mariozechner/jiti": "^2.6.2",
|
||||
"@mariozechner/pi-agent-core": "^0.72.1",
|
||||
"@mariozechner/pi-ai": "^0.72.1",
|
||||
"@mariozechner/pi-tui": "^0.72.1",
|
||||
"@mariozechner/pi-agent-core": "^0.73.1",
|
||||
"@mariozechner/pi-ai": "^0.73.1",
|
||||
"@mariozechner/pi-tui": "^0.73.1",
|
||||
"@silvia-odwyer/photon-node": "^0.3.4",
|
||||
"chalk": "^5.5.0",
|
||||
"cli-highlight": "^2.1.11",
|
||||
@@ -51,6 +50,7 @@
|
||||
"glob": "^13.0.1",
|
||||
"hosted-git-info": "^9.0.2",
|
||||
"ignore": "^7.0.5",
|
||||
"jiti": "^2.7.0",
|
||||
"marked": "^15.0.12",
|
||||
"minimatch": "^10.2.3",
|
||||
"proper-lockfile": "^4.1.2",
|
||||
|
||||
@@ -122,6 +122,11 @@ export function parseArgs(args: string[]): Args {
|
||||
}
|
||||
} else if (arg === "--print" || arg === "-p") {
|
||||
result.print = true;
|
||||
const next = args[i + 1];
|
||||
if (next !== undefined && !next.startsWith("@") && (!next.startsWith("-") || next.startsWith("---"))) {
|
||||
result.messages.push(next);
|
||||
i++;
|
||||
}
|
||||
} else if (arg === "--export" && i + 1 < args.length) {
|
||||
result.export = args[++i];
|
||||
} else if ((arg === "--extension" || arg === "-e") && i + 1 < args.length) {
|
||||
|
||||
@@ -28,12 +28,36 @@ export const isBunRuntime = !!process.versions.bun;
|
||||
|
||||
export type InstallMethod = "bun-binary" | "npm" | "pnpm" | "yarn" | "bun" | "unknown";
|
||||
|
||||
export interface SelfUpdateCommand {
|
||||
interface SelfUpdateCommandStep {
|
||||
command: string;
|
||||
args: string[];
|
||||
display: string;
|
||||
}
|
||||
|
||||
export interface SelfUpdateCommand extends SelfUpdateCommandStep {
|
||||
steps?: SelfUpdateCommandStep[];
|
||||
}
|
||||
|
||||
function makeSelfUpdateCommand(
|
||||
installStep: SelfUpdateCommandStep,
|
||||
uninstallStep?: SelfUpdateCommandStep,
|
||||
): SelfUpdateCommand {
|
||||
if (!uninstallStep) return installStep;
|
||||
return {
|
||||
...installStep,
|
||||
display: `${uninstallStep.display} && ${installStep.display}`,
|
||||
steps: [uninstallStep, installStep],
|
||||
};
|
||||
}
|
||||
|
||||
function makeSelfUpdateCommandStep(command: string, args: string[]): SelfUpdateCommandStep {
|
||||
return {
|
||||
command,
|
||||
args,
|
||||
display: [command, ...args].map((arg) => (/\s/.test(arg) ? `"${arg}"` : arg)).join(" "),
|
||||
};
|
||||
}
|
||||
|
||||
export function detectInstallMethod(): InstallMethod {
|
||||
if (isBunBinary) {
|
||||
return "bun-binary";
|
||||
@@ -83,24 +107,44 @@ function getInferredNpmInstall(packageName: string): { root: string; prefix: str
|
||||
|
||||
function getSelfUpdateCommandForMethod(
|
||||
method: InstallMethod,
|
||||
packageName: string,
|
||||
installedPackageName: string,
|
||||
updatePackageName = installedPackageName,
|
||||
npmCommand?: string[],
|
||||
): SelfUpdateCommand | undefined {
|
||||
switch (method) {
|
||||
case "bun-binary":
|
||||
return undefined;
|
||||
case "pnpm":
|
||||
return { command: "pnpm", args: ["install", "-g", packageName], display: `pnpm install -g ${packageName}` };
|
||||
return makeSelfUpdateCommand(
|
||||
makeSelfUpdateCommandStep("pnpm", ["install", "-g", updatePackageName]),
|
||||
updatePackageName === installedPackageName
|
||||
? undefined
|
||||
: makeSelfUpdateCommandStep("pnpm", ["remove", "-g", installedPackageName]),
|
||||
);
|
||||
case "yarn":
|
||||
return { command: "yarn", args: ["global", "add", packageName], display: `yarn global add ${packageName}` };
|
||||
return makeSelfUpdateCommand(
|
||||
makeSelfUpdateCommandStep("yarn", ["global", "add", updatePackageName]),
|
||||
updatePackageName === installedPackageName
|
||||
? undefined
|
||||
: makeSelfUpdateCommandStep("yarn", ["global", "remove", installedPackageName]),
|
||||
);
|
||||
case "bun":
|
||||
return { command: "bun", args: ["install", "-g", packageName], display: `bun install -g ${packageName}` };
|
||||
return makeSelfUpdateCommand(
|
||||
makeSelfUpdateCommandStep("bun", ["install", "-g", updatePackageName]),
|
||||
updatePackageName === installedPackageName
|
||||
? undefined
|
||||
: makeSelfUpdateCommandStep("bun", ["uninstall", "-g", installedPackageName]),
|
||||
);
|
||||
case "npm": {
|
||||
const [command = "npm", ...npmArgs] = npmCommand ?? [];
|
||||
const inferred = npmCommand?.length ? undefined : getInferredNpmInstall(packageName);
|
||||
const args = [...npmArgs, ...(inferred ? ["--prefix", inferred.prefix] : []), "install", "-g", packageName];
|
||||
const display = [command, ...args].map((arg) => (/\s/.test(arg) ? `"${arg}"` : arg)).join(" ");
|
||||
return { command, args, display };
|
||||
const inferred = npmCommand?.length ? undefined : getInferredNpmInstall(installedPackageName);
|
||||
const prefixArgs = [...npmArgs, ...(inferred ? ["--prefix", inferred.prefix] : [])];
|
||||
const installStep = makeSelfUpdateCommandStep(command, [...prefixArgs, "install", "-g", updatePackageName]);
|
||||
const uninstallStep =
|
||||
updatePackageName === installedPackageName
|
||||
? undefined
|
||||
: makeSelfUpdateCommandStep(command, [...prefixArgs, "uninstall", "-g", installedPackageName]);
|
||||
return makeSelfUpdateCommand(installStep, uninstallStep);
|
||||
}
|
||||
case "unknown":
|
||||
return undefined;
|
||||
@@ -210,28 +254,36 @@ function isManagedByGlobalPackageManager(method: InstallMethod, packageName: str
|
||||
);
|
||||
}
|
||||
|
||||
export function getSelfUpdateCommand(packageName: string, npmCommand?: string[]): SelfUpdateCommand | undefined {
|
||||
export function getSelfUpdateCommand(
|
||||
packageName: string,
|
||||
npmCommand?: string[],
|
||||
updatePackageName = packageName,
|
||||
): SelfUpdateCommand | undefined {
|
||||
const method = detectInstallMethod();
|
||||
const command = getSelfUpdateCommandForMethod(method, packageName, npmCommand);
|
||||
const command = getSelfUpdateCommandForMethod(method, packageName, updatePackageName, npmCommand);
|
||||
if (!command || !isManagedByGlobalPackageManager(method, packageName, npmCommand) || !isSelfUpdatePathWritable()) {
|
||||
return undefined;
|
||||
}
|
||||
return command;
|
||||
}
|
||||
|
||||
export function getSelfUpdateUnavailableInstruction(packageName: string, npmCommand?: string[]): string {
|
||||
export function getSelfUpdateUnavailableInstruction(
|
||||
packageName: string,
|
||||
npmCommand?: string[],
|
||||
updatePackageName = packageName,
|
||||
): string {
|
||||
const method = detectInstallMethod();
|
||||
if (method === "bun-binary") {
|
||||
return `Download from: https://github.com/badlogic/pi-mono/releases/latest`;
|
||||
}
|
||||
const command = getSelfUpdateCommandForMethod(method, packageName, npmCommand);
|
||||
const command = getSelfUpdateCommandForMethod(method, packageName, updatePackageName, npmCommand);
|
||||
if (command) {
|
||||
if (isManagedByGlobalPackageManager(method, packageName, npmCommand) && !isSelfUpdatePathWritable()) {
|
||||
return `This installation is managed by a global ${method} install, but the install path is not writable. Update it yourself with: ${command.display}`;
|
||||
}
|
||||
return `This installation is not managed by a global ${method} install. Update it with the package manager, wrapper, or source checkout that provides it.`;
|
||||
}
|
||||
return `Update ${packageName} using the package manager, wrapper, or source checkout that provides this installation.`;
|
||||
return `Update ${updatePackageName} using the package manager, wrapper, or source checkout that provides this installation.`;
|
||||
}
|
||||
|
||||
export function getUpdateInstruction(packageName: string): string {
|
||||
|
||||
@@ -204,6 +204,10 @@
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.tree-role-skill {
|
||||
color: var(--customMessageLabel);
|
||||
}
|
||||
|
||||
.tree-role-assistant {
|
||||
color: var(--success);
|
||||
}
|
||||
@@ -383,7 +387,8 @@
|
||||
}
|
||||
|
||||
.user-message:hover .copy-link-btn,
|
||||
.assistant-message:hover .copy-link-btn {
|
||||
.assistant-message:hover .copy-link-btn,
|
||||
.skill-user-entry:hover .copy-link-btn {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@@ -786,6 +791,45 @@
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Skill invocation - matches compaction style (clickable, collapsed by default) */
|
||||
.skill-invocation {
|
||||
background: var(--customMessageBg);
|
||||
border-radius: 4px;
|
||||
padding: var(--line-height);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.skill-invocation-label {
|
||||
color: var(--customMessageLabel);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.skill-invocation-collapsed {
|
||||
color: var(--customMessageText);
|
||||
}
|
||||
|
||||
.skill-invocation-content {
|
||||
display: none;
|
||||
color: var(--customMessageText);
|
||||
margin-top: var(--line-height);
|
||||
}
|
||||
|
||||
.skill-invocation.expanded .skill-invocation-collapsed {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.skill-invocation.expanded .skill-invocation-content {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.skill-invocation + .user-message {
|
||||
margin-top: var(--line-height);
|
||||
}
|
||||
|
||||
.skill-user-entry {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Branch summary */
|
||||
.branch-summary {
|
||||
background: var(--customMessageBg);
|
||||
|
||||
@@ -313,6 +313,22 @@
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a skill block from message text.
|
||||
* Returns null if the text doesn't contain a skill block.
|
||||
* Matches the format: <skill name="..." location="...">\n...\n</skill>\n\nuser message
|
||||
*/
|
||||
function parseSkillBlock(text) {
|
||||
const match = text.match(/^<skill name="([^"]+)" location="([^"]+)">\n([\s\S]*?)\n<\/skill>(?:\n\n([\s\S]+))?$/);
|
||||
if (!match) return null;
|
||||
return {
|
||||
name: match[1],
|
||||
location: match[2],
|
||||
content: match[3],
|
||||
userMessage: match[4]?.trim() || undefined,
|
||||
};
|
||||
}
|
||||
|
||||
function getSearchableText(entry, label) {
|
||||
const parts = [];
|
||||
if (label) parts.push(label);
|
||||
@@ -613,7 +629,16 @@
|
||||
case 'message': {
|
||||
const msg = entry.message;
|
||||
if (msg.role === 'user') {
|
||||
const content = truncate(normalize(extractContent(msg.content)));
|
||||
const rawContent = extractContent(msg.content);
|
||||
const skillBlock = parseSkillBlock(rawContent);
|
||||
if (skillBlock) {
|
||||
let treeHtml = labelHtml + `<span class="tree-role-skill">skill:</span> ${escapeHtml(skillBlock.name)}`;
|
||||
if (skillBlock.userMessage) {
|
||||
treeHtml += ` · <span class="tree-role-user">user:</span> ${escapeHtml(truncate(normalize(skillBlock.userMessage)))}`;
|
||||
}
|
||||
return treeHtml;
|
||||
}
|
||||
const content = truncate(normalize(rawContent));
|
||||
return labelHtml + `<span class="tree-role-user">user:</span> ${escapeHtml(content)}`;
|
||||
}
|
||||
if (msg.role === 'assistant') {
|
||||
@@ -1140,8 +1165,46 @@
|
||||
const msg = entry.message;
|
||||
|
||||
if (msg.role === 'user') {
|
||||
let html = `<div class="user-message" id="${entryDomId}">${copyBtnHtml}${tsHtml}`;
|
||||
const content = msg.content;
|
||||
const text = typeof content === 'string' ? content :
|
||||
content.filter(c => c.type === 'text').map(c => c.text).join('\n');
|
||||
const skillBlock = parseSkillBlock(text);
|
||||
|
||||
if (skillBlock) {
|
||||
// Collect images from content array
|
||||
const images = Array.isArray(content) ? content.filter(c => c.type === 'image') : [];
|
||||
const hasUserContent = skillBlock.userMessage || images.length > 0;
|
||||
let html = `<div class="skill-user-entry" id="${entryDomId}">${copyBtnHtml}${tsHtml}`;
|
||||
|
||||
// Skill invocation (collapsed by default, click to expand)
|
||||
html += `<div class="skill-invocation" onclick="if(window.getSelection().toString())return;this.classList.toggle('expanded')">
|
||||
<div class="skill-invocation-label">[skill] ${escapeHtml(skillBlock.name)}</div>
|
||||
<div class="skill-invocation-collapsed">${escapeHtml(skillBlock.name)} (click to expand)</div>
|
||||
<div class="skill-invocation-content markdown-content">${safeMarkedParse(skillBlock.content)}</div>
|
||||
</div>`;
|
||||
|
||||
// User message (separate block if present)
|
||||
if (hasUserContent) {
|
||||
html += '<div class="user-message">';
|
||||
if (images.length > 0) {
|
||||
html += '<div class="message-images">';
|
||||
for (const img of images) {
|
||||
html += `<img src="data:${escapeHtml(img.mimeType || 'image/png')};base64,${escapeHtml(img.data || '')}" class="message-image" />`;
|
||||
}
|
||||
html += '</div>';
|
||||
}
|
||||
if (skillBlock.userMessage) {
|
||||
html += `<div class="markdown-content">${safeMarkedParse(skillBlock.userMessage)}</div>`;
|
||||
}
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
return html;
|
||||
}
|
||||
|
||||
// No skill block - normal user message
|
||||
let html = `<div class="user-message" id="${entryDomId}">${copyBtnHtml}${tsHtml}`;
|
||||
|
||||
if (Array.isArray(content)) {
|
||||
const images = content.filter(c => c.type === 'image');
|
||||
@@ -1154,8 +1217,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
const text = typeof content === 'string' ? content :
|
||||
content.filter(c => c.type === 'text').map(c => c.text).join('\n');
|
||||
if (text.trim()) {
|
||||
html += `<div class="markdown-content">${safeMarkedParse(text)}</div>`;
|
||||
}
|
||||
@@ -1716,6 +1777,9 @@
|
||||
document.querySelectorAll('.compaction').forEach(el => {
|
||||
el.classList.toggle('expanded', toolOutputsExpanded);
|
||||
});
|
||||
document.querySelectorAll('.skill-invocation').forEach(el => {
|
||||
el.classList.toggle('expanded', toolOutputsExpanded);
|
||||
});
|
||||
};
|
||||
|
||||
const attachHeaderHandlers = () => {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
/**
|
||||
* Extension loader - loads TypeScript extension modules using jiti.
|
||||
*
|
||||
* Uses @mariozechner/jiti fork with virtualModules support for compiled Bun binaries.
|
||||
*/
|
||||
|
||||
import * as fs from "node:fs";
|
||||
@@ -9,12 +8,12 @@ import { createRequire } from "node:module";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { createJiti } from "@mariozechner/jiti";
|
||||
import * as _bundledPiAgentCore from "@mariozechner/pi-agent-core";
|
||||
import * as _bundledPiAi from "@mariozechner/pi-ai";
|
||||
import * as _bundledPiAiOauth from "@mariozechner/pi-ai/oauth";
|
||||
import type { KeyId } from "@mariozechner/pi-tui";
|
||||
import * as _bundledPiTui from "@mariozechner/pi-tui";
|
||||
import { createJiti } from "jiti/static";
|
||||
// Static imports of packages that extensions may use.
|
||||
// These MUST be static so Bun bundles them into the compiled binary.
|
||||
// The virtualModules option then makes them available to extensions.
|
||||
|
||||
@@ -213,6 +213,13 @@ function formatValidationPath(error: TLocalizedValidationError): string {
|
||||
return path || "root";
|
||||
}
|
||||
|
||||
/** Strip `//` line comments and trailing commas from JSON, leaving string literals untouched. */
|
||||
function stripJsonComments(input: string): string {
|
||||
return input
|
||||
.replace(/"(?:\\.|[^"\\])*"|\/\/[^\n]*/g, (m) => (m[0] === '"' ? m : ""))
|
||||
.replace(/"(?:\\.|[^"\\])*"|,(\s*[}\]])/g, (m, tail) => tail ?? (m[0] === '"' ? m : ""));
|
||||
}
|
||||
|
||||
/** Provider override config (baseUrl, compat) without request auth/headers */
|
||||
interface ProviderOverride {
|
||||
baseUrl?: string;
|
||||
@@ -450,7 +457,7 @@ export class ModelRegistry {
|
||||
|
||||
try {
|
||||
const content = readFileSync(modelsJsonPath, "utf-8");
|
||||
const parsed = JSON.parse(content) as unknown;
|
||||
const parsed = JSON.parse(stripJsonComments(content)) as unknown;
|
||||
|
||||
if (!validateModelsConfig.Check(parsed)) {
|
||||
const errors =
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import { randomBytes } from "node:crypto";
|
||||
import { createWriteStream, existsSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { existsSync } from "node:fs";
|
||||
import type { AgentTool } from "@mariozechner/pi-agent-core";
|
||||
import { Container, Text, truncateToWidth } from "@mariozechner/pi-tui";
|
||||
import { spawn } from "child_process";
|
||||
@@ -18,17 +15,10 @@ import {
|
||||
untrackDetachedChildPid,
|
||||
} from "../../utils/shell.js";
|
||||
import type { ToolDefinition, ToolRenderResultOptions } from "../extensions/types.js";
|
||||
import { OutputAccumulator } from "./output-accumulator.js";
|
||||
import { getTextOutput, invalidArgText, str } from "./render-utils.js";
|
||||
import { wrapToolDefinition } from "./tool-definition-wrapper.js";
|
||||
import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, type TruncationResult, truncateTail } from "./truncate.js";
|
||||
|
||||
/**
|
||||
* Generate a unique temp file path for bash output.
|
||||
*/
|
||||
function getTempFilePath(): string {
|
||||
const id = randomBytes(8).toString("hex");
|
||||
return join(tmpdir(), `pi-bash-${id}.log`);
|
||||
}
|
||||
import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, type TruncationResult } from "./truncate.js";
|
||||
|
||||
const bashSchema = Type.Object({
|
||||
command: Type.String({ description: "Bash command to execute" }),
|
||||
@@ -161,6 +151,7 @@ export interface BashToolOptions {
|
||||
}
|
||||
|
||||
const BASH_PREVIEW_LINES = 5;
|
||||
const BASH_UPDATE_THROTTLE_MS = 100;
|
||||
|
||||
type BashRenderState = {
|
||||
startedAt: number | undefined;
|
||||
@@ -292,118 +283,119 @@ export function createBashToolDefinition(
|
||||
) {
|
||||
const resolvedCommand = commandPrefix ? `${commandPrefix}\n${command}` : command;
|
||||
const spawnContext = resolveSpawnContext(resolvedCommand, cwd, spawnHook);
|
||||
const output = new OutputAccumulator({ tempFilePrefix: "pi-bash" });
|
||||
let updateTimer: NodeJS.Timeout | undefined;
|
||||
let updateDirty = false;
|
||||
let lastUpdateAt = 0;
|
||||
|
||||
const emitOutputUpdate = () => {
|
||||
if (!onUpdate || !updateDirty) return;
|
||||
updateDirty = false;
|
||||
lastUpdateAt = Date.now();
|
||||
const snapshot = output.snapshot({ persistIfTruncated: true });
|
||||
onUpdate({
|
||||
content: [{ type: "text", text: snapshot.content || "" }],
|
||||
details: {
|
||||
truncation: snapshot.truncation.truncated ? snapshot.truncation : undefined,
|
||||
fullOutputPath: snapshot.fullOutputPath,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const clearUpdateTimer = () => {
|
||||
if (updateTimer) {
|
||||
clearTimeout(updateTimer);
|
||||
updateTimer = undefined;
|
||||
}
|
||||
};
|
||||
|
||||
const scheduleOutputUpdate = () => {
|
||||
if (!onUpdate) return;
|
||||
updateDirty = true;
|
||||
const delay = BASH_UPDATE_THROTTLE_MS - (Date.now() - lastUpdateAt);
|
||||
if (delay <= 0) {
|
||||
clearUpdateTimer();
|
||||
emitOutputUpdate();
|
||||
return;
|
||||
}
|
||||
updateTimer ??= setTimeout(() => {
|
||||
updateTimer = undefined;
|
||||
emitOutputUpdate();
|
||||
}, delay);
|
||||
};
|
||||
|
||||
if (onUpdate) {
|
||||
onUpdate({ content: [], details: undefined });
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
let tempFilePath: string | undefined;
|
||||
let tempFileStream: ReturnType<typeof createWriteStream> | undefined;
|
||||
let totalBytes = 0;
|
||||
const chunks: Buffer[] = [];
|
||||
let chunksBytes = 0;
|
||||
const maxChunksBytes = DEFAULT_MAX_BYTES * 2;
|
||||
|
||||
const ensureTempFile = () => {
|
||||
if (tempFilePath) return;
|
||||
tempFilePath = getTempFilePath();
|
||||
tempFileStream = createWriteStream(tempFilePath);
|
||||
for (const chunk of chunks) tempFileStream.write(chunk);
|
||||
};
|
||||
const handleData = (data: Buffer) => {
|
||||
output.append(data);
|
||||
scheduleOutputUpdate();
|
||||
};
|
||||
|
||||
const handleData = (data: Buffer) => {
|
||||
totalBytes += data.length;
|
||||
// Start writing to a temp file once output exceeds the in-memory threshold.
|
||||
if (totalBytes > DEFAULT_MAX_BYTES) {
|
||||
ensureTempFile();
|
||||
}
|
||||
// Write to temp file if we have one.
|
||||
if (tempFileStream) tempFileStream.write(data);
|
||||
// Keep a rolling buffer of recent output for tail truncation.
|
||||
chunks.push(data);
|
||||
chunksBytes += data.length;
|
||||
// Trim old chunks if the rolling buffer grows too large.
|
||||
while (chunksBytes > maxChunksBytes && chunks.length > 1) {
|
||||
const removed = chunks.shift()!;
|
||||
chunksBytes -= removed.length;
|
||||
}
|
||||
// Stream partial output using the rolling tail buffer.
|
||||
if (onUpdate) {
|
||||
const fullBuffer = Buffer.concat(chunks);
|
||||
const fullText = fullBuffer.toString("utf-8");
|
||||
const truncation = truncateTail(fullText);
|
||||
if (truncation.truncated) {
|
||||
ensureTempFile();
|
||||
}
|
||||
onUpdate({
|
||||
content: [{ type: "text", text: truncation.content || "" }],
|
||||
details: {
|
||||
truncation: truncation.truncated ? truncation : undefined,
|
||||
fullOutputPath: tempFilePath,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
const finishOutput = async () => {
|
||||
output.finish();
|
||||
clearUpdateTimer();
|
||||
emitOutputUpdate();
|
||||
const snapshot = output.snapshot({ persistIfTruncated: true });
|
||||
await output.closeTempFile();
|
||||
return snapshot;
|
||||
};
|
||||
|
||||
ops.exec(spawnContext.command, spawnContext.cwd, {
|
||||
onData: handleData,
|
||||
signal,
|
||||
timeout,
|
||||
env: spawnContext.env,
|
||||
})
|
||||
.then(({ exitCode }) => {
|
||||
// Combine the rolling buffer chunks.
|
||||
const fullBuffer = Buffer.concat(chunks);
|
||||
const fullOutput = fullBuffer.toString("utf-8");
|
||||
// Apply tail truncation for the final display payload.
|
||||
const truncation = truncateTail(fullOutput);
|
||||
if (truncation.truncated) {
|
||||
ensureTempFile();
|
||||
}
|
||||
// Close temp file stream before building the final result.
|
||||
if (tempFileStream) tempFileStream.end();
|
||||
let outputText = truncation.content || "(no output)";
|
||||
let details: BashToolDetails | undefined;
|
||||
if (truncation.truncated) {
|
||||
// Build truncation details and an actionable notice.
|
||||
details = { truncation, fullOutputPath: tempFilePath };
|
||||
const startLine = truncation.totalLines - truncation.outputLines + 1;
|
||||
const endLine = truncation.totalLines;
|
||||
if (truncation.lastLinePartial) {
|
||||
// Edge case: the last line alone is larger than the byte limit.
|
||||
const lastLineSize = formatSize(Buffer.byteLength(fullOutput.split("\n").pop() || "", "utf-8"));
|
||||
outputText += `\n\n[Showing last ${formatSize(truncation.outputBytes)} of line ${endLine} (line is ${lastLineSize}). Full output: ${tempFilePath}]`;
|
||||
} else if (truncation.truncatedBy === "lines") {
|
||||
outputText += `\n\n[Showing lines ${startLine}-${endLine} of ${truncation.totalLines}. Full output: ${tempFilePath}]`;
|
||||
} else {
|
||||
outputText += `\n\n[Showing lines ${startLine}-${endLine} of ${truncation.totalLines} (${formatSize(DEFAULT_MAX_BYTES)} limit). Full output: ${tempFilePath}]`;
|
||||
}
|
||||
}
|
||||
if (exitCode !== 0 && exitCode !== null) {
|
||||
outputText += `\n\nCommand exited with code ${exitCode}`;
|
||||
reject(new Error(outputText));
|
||||
} else {
|
||||
resolve({ content: [{ type: "text", text: outputText }], details });
|
||||
}
|
||||
})
|
||||
.catch((err: Error) => {
|
||||
// Close temp file stream and include buffered output in the error message.
|
||||
if (tempFileStream) tempFileStream.end();
|
||||
const fullBuffer = Buffer.concat(chunks);
|
||||
let output = fullBuffer.toString("utf-8");
|
||||
if (err.message === "aborted") {
|
||||
if (output) output += "\n\n";
|
||||
output += "Command aborted";
|
||||
reject(new Error(output));
|
||||
} else if (err.message.startsWith("timeout:")) {
|
||||
const timeoutSecs = err.message.split(":")[1];
|
||||
if (output) output += "\n\n";
|
||||
output += `Command timed out after ${timeoutSecs} seconds`;
|
||||
reject(new Error(output));
|
||||
} else {
|
||||
reject(err);
|
||||
}
|
||||
const formatOutput = (snapshot: Awaited<ReturnType<typeof finishOutput>>, emptyText = "(no output)") => {
|
||||
const truncation = snapshot.truncation;
|
||||
let text = snapshot.content || emptyText;
|
||||
let details: BashToolDetails | undefined;
|
||||
if (truncation.truncated) {
|
||||
details = { truncation, fullOutputPath: snapshot.fullOutputPath };
|
||||
const startLine = truncation.totalLines - truncation.outputLines + 1;
|
||||
const endLine = truncation.totalLines;
|
||||
if (truncation.lastLinePartial) {
|
||||
const lastLineSize = formatSize(output.getLastLineBytes());
|
||||
text += `\n\n[Showing last ${formatSize(truncation.outputBytes)} of line ${endLine} (line is ${lastLineSize}). Full output: ${snapshot.fullOutputPath}]`;
|
||||
} else if (truncation.truncatedBy === "lines") {
|
||||
text += `\n\n[Showing lines ${startLine}-${endLine} of ${truncation.totalLines}. Full output: ${snapshot.fullOutputPath}]`;
|
||||
} else {
|
||||
text += `\n\n[Showing lines ${startLine}-${endLine} of ${truncation.totalLines} (${formatSize(DEFAULT_MAX_BYTES)} limit). Full output: ${snapshot.fullOutputPath}]`;
|
||||
}
|
||||
}
|
||||
return { text, details };
|
||||
};
|
||||
|
||||
const appendStatus = (text: string, status: string) => `${text ? `${text}\n\n` : ""}${status}`;
|
||||
|
||||
try {
|
||||
let exitCode: number | null;
|
||||
try {
|
||||
const result = await ops.exec(spawnContext.command, spawnContext.cwd, {
|
||||
onData: handleData,
|
||||
signal,
|
||||
timeout,
|
||||
env: spawnContext.env,
|
||||
});
|
||||
});
|
||||
exitCode = result.exitCode;
|
||||
} catch (err) {
|
||||
const snapshot = await finishOutput();
|
||||
const { text } = formatOutput(snapshot, "");
|
||||
if (err instanceof Error && err.message === "aborted") {
|
||||
throw new Error(appendStatus(text, "Command aborted"));
|
||||
}
|
||||
if (err instanceof Error && err.message.startsWith("timeout:")) {
|
||||
const timeoutSecs = err.message.split(":")[1];
|
||||
throw new Error(appendStatus(text, `Command timed out after ${timeoutSecs} seconds`));
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
|
||||
const snapshot = await finishOutput();
|
||||
const { text: outputText, details } = formatOutput(snapshot);
|
||||
if (exitCode !== 0 && exitCode !== null) {
|
||||
throw new Error(appendStatus(outputText, `Command exited with code ${exitCode}`));
|
||||
}
|
||||
return { content: [{ type: "text", text: outputText }], details };
|
||||
} finally {
|
||||
clearUpdateTimer();
|
||||
}
|
||||
},
|
||||
renderCall(args, _theme, context) {
|
||||
const state = context.state;
|
||||
|
||||
216
packages/coding-agent/src/core/tools/output-accumulator.ts
Normal file
216
packages/coding-agent/src/core/tools/output-accumulator.ts
Normal file
@@ -0,0 +1,216 @@
|
||||
import { randomBytes } from "node:crypto";
|
||||
import { createWriteStream, type WriteStream } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, type TruncationResult, truncateTail } from "./truncate.js";
|
||||
|
||||
export interface OutputAccumulatorOptions {
|
||||
maxLines?: number;
|
||||
maxBytes?: number;
|
||||
tempFilePrefix?: string;
|
||||
}
|
||||
|
||||
export interface OutputSnapshot {
|
||||
content: string;
|
||||
truncation: TruncationResult;
|
||||
fullOutputPath?: string;
|
||||
}
|
||||
|
||||
function defaultTempFilePath(prefix: string): string {
|
||||
const id = randomBytes(8).toString("hex");
|
||||
return join(tmpdir(), `${prefix}-${id}.log`);
|
||||
}
|
||||
|
||||
function byteLength(text: string): number {
|
||||
return Buffer.byteLength(text, "utf-8");
|
||||
}
|
||||
|
||||
/**
|
||||
* Incrementally tracks streaming output with bounded memory.
|
||||
*
|
||||
* Appends decode chunks with a streaming UTF-8 decoder, keeps only a decoded
|
||||
* tail for display snapshots, and opens a temp file when the full output needs
|
||||
* to be preserved.
|
||||
*/
|
||||
export class OutputAccumulator {
|
||||
private readonly maxLines: number;
|
||||
private readonly maxBytes: number;
|
||||
private readonly maxRollingBytes: number;
|
||||
private readonly tempFilePrefix: string;
|
||||
private readonly decoder = new TextDecoder();
|
||||
|
||||
private rawChunks: Buffer[] = [];
|
||||
private tailText = "";
|
||||
private tailBytes = 0;
|
||||
private tailStartsAtLineBoundary = true;
|
||||
private totalRawBytes = 0;
|
||||
private totalDecodedBytes = 0;
|
||||
private totalLines = 1;
|
||||
private currentLineBytes = 0;
|
||||
private finished = false;
|
||||
|
||||
private tempFilePath: string | undefined;
|
||||
private tempFileStream: WriteStream | undefined;
|
||||
|
||||
constructor(options: OutputAccumulatorOptions = {}) {
|
||||
this.maxLines = options.maxLines ?? DEFAULT_MAX_LINES;
|
||||
this.maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;
|
||||
this.maxRollingBytes = Math.max(this.maxBytes * 2, 1);
|
||||
this.tempFilePrefix = options.tempFilePrefix ?? "pi-output";
|
||||
}
|
||||
|
||||
append(data: Buffer): void {
|
||||
if (this.finished) {
|
||||
throw new Error("Cannot append to a finished output accumulator");
|
||||
}
|
||||
|
||||
this.totalRawBytes += data.length;
|
||||
this.appendDecodedText(this.decoder.decode(data, { stream: true }));
|
||||
|
||||
if (this.tempFileStream || this.shouldUseTempFile()) {
|
||||
this.ensureTempFile();
|
||||
this.tempFileStream?.write(data);
|
||||
} else if (data.length > 0) {
|
||||
this.rawChunks.push(data);
|
||||
}
|
||||
}
|
||||
|
||||
finish(): void {
|
||||
if (this.finished) {
|
||||
return;
|
||||
}
|
||||
this.finished = true;
|
||||
this.appendDecodedText(this.decoder.decode());
|
||||
if (this.shouldUseTempFile()) {
|
||||
this.ensureTempFile();
|
||||
}
|
||||
}
|
||||
|
||||
snapshot(options: { persistIfTruncated?: boolean } = {}): OutputSnapshot {
|
||||
const tailTruncation = truncateTail(this.getSnapshotText(), {
|
||||
maxLines: this.maxLines,
|
||||
maxBytes: this.maxBytes,
|
||||
});
|
||||
const truncated = this.totalLines > this.maxLines || this.totalDecodedBytes > this.maxBytes;
|
||||
const truncatedBy = truncated
|
||||
? (tailTruncation.truncatedBy ?? (this.totalDecodedBytes > this.maxBytes ? "bytes" : "lines"))
|
||||
: null;
|
||||
const truncation: TruncationResult = {
|
||||
...tailTruncation,
|
||||
truncated,
|
||||
truncatedBy,
|
||||
totalLines: this.totalLines,
|
||||
totalBytes: this.totalDecodedBytes,
|
||||
maxLines: this.maxLines,
|
||||
maxBytes: this.maxBytes,
|
||||
};
|
||||
|
||||
if (options.persistIfTruncated && truncation.truncated) {
|
||||
this.ensureTempFile();
|
||||
}
|
||||
|
||||
return {
|
||||
content: truncation.content,
|
||||
truncation,
|
||||
fullOutputPath: this.tempFilePath,
|
||||
};
|
||||
}
|
||||
|
||||
async closeTempFile(): Promise<void> {
|
||||
if (!this.tempFileStream) {
|
||||
return;
|
||||
}
|
||||
|
||||
const stream = this.tempFileStream;
|
||||
this.tempFileStream = undefined;
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const onError = (error: Error) => {
|
||||
stream.off("finish", onFinish);
|
||||
reject(error);
|
||||
};
|
||||
const onFinish = () => {
|
||||
stream.off("error", onError);
|
||||
resolve();
|
||||
};
|
||||
stream.once("error", onError);
|
||||
stream.once("finish", onFinish);
|
||||
stream.end();
|
||||
});
|
||||
}
|
||||
|
||||
getLastLineBytes(): number {
|
||||
return this.currentLineBytes;
|
||||
}
|
||||
|
||||
private appendDecodedText(text: string): void {
|
||||
if (text.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bytes = byteLength(text);
|
||||
this.totalDecodedBytes += bytes;
|
||||
this.tailText += text;
|
||||
this.tailBytes += bytes;
|
||||
if (this.tailBytes > this.maxRollingBytes * 2) {
|
||||
this.trimTail();
|
||||
}
|
||||
|
||||
let newlines = 0;
|
||||
let lastNewline = -1;
|
||||
for (let i = text.indexOf("\n"); i !== -1; i = text.indexOf("\n", i + 1)) {
|
||||
newlines++;
|
||||
lastNewline = i;
|
||||
}
|
||||
if (newlines === 0) {
|
||||
this.currentLineBytes += bytes;
|
||||
} else {
|
||||
this.totalLines += newlines;
|
||||
this.currentLineBytes = byteLength(text.slice(lastNewline + 1));
|
||||
}
|
||||
}
|
||||
|
||||
private trimTail(): void {
|
||||
const buffer = Buffer.from(this.tailText, "utf-8");
|
||||
if (buffer.length <= this.maxRollingBytes) {
|
||||
this.tailBytes = buffer.length;
|
||||
return;
|
||||
}
|
||||
|
||||
let start = buffer.length - this.maxRollingBytes;
|
||||
while (start < buffer.length && (buffer[start] & 0xc0) === 0x80) {
|
||||
start++;
|
||||
}
|
||||
|
||||
this.tailStartsAtLineBoundary = start === 0 ? this.tailStartsAtLineBoundary : buffer[start - 1] === 0x0a;
|
||||
this.tailText = buffer.subarray(start).toString("utf-8");
|
||||
this.tailBytes = byteLength(this.tailText);
|
||||
}
|
||||
|
||||
private getSnapshotText(): string {
|
||||
if (this.tailStartsAtLineBoundary) {
|
||||
return this.tailText;
|
||||
}
|
||||
|
||||
const firstNewline = this.tailText.indexOf("\n");
|
||||
return firstNewline === -1 ? this.tailText : this.tailText.slice(firstNewline + 1);
|
||||
}
|
||||
|
||||
private shouldUseTempFile(): boolean {
|
||||
return (
|
||||
this.totalRawBytes > this.maxBytes || this.totalDecodedBytes > this.maxBytes || this.totalLines > this.maxLines
|
||||
);
|
||||
}
|
||||
|
||||
private ensureTempFile(): void {
|
||||
if (this.tempFilePath) {
|
||||
return;
|
||||
}
|
||||
this.tempFilePath = defaultTempFilePath(this.tempFilePrefix);
|
||||
this.tempFileStream = createWriteStream(this.tempFilePath);
|
||||
for (const chunk of this.rawChunks) {
|
||||
this.tempFileStream.write(chunk);
|
||||
}
|
||||
this.rawChunks = [];
|
||||
}
|
||||
}
|
||||
@@ -87,7 +87,8 @@ export class LoginDialogComponent extends Container implements Focusable {
|
||||
showAuth(url: string, instructions?: string): void {
|
||||
this.contentContainer.clear();
|
||||
this.contentContainer.addChild(new Spacer(1));
|
||||
this.contentContainer.addChild(new Text(theme.fg("accent", url), 1, 0));
|
||||
const linkedUrl = `\x1b]8;;${url}\x07${url}\x1b]8;;\x07`;
|
||||
this.contentContainer.addChild(new Text(theme.fg("accent", linkedUrl), 1, 0));
|
||||
|
||||
const clickHint = process.platform === "darwin" ? "Cmd+click to open" : "Ctrl+click to open";
|
||||
const hyperlink = `\x1b]8;;${url}\x07${clickHint}\x1b]8;;\x07`;
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
type Message,
|
||||
type Model,
|
||||
type OAuthProviderId,
|
||||
type OAuthSelectPrompt,
|
||||
} from "@mariozechner/pi-ai";
|
||||
import type {
|
||||
AutocompleteItem,
|
||||
@@ -2632,6 +2633,7 @@ export class InteractiveMode {
|
||||
|
||||
switch (event.type) {
|
||||
case "agent_start":
|
||||
this.pendingTools.clear();
|
||||
if (this.settingsManager.getShowTerminalProgress()) {
|
||||
this.ui.terminal.setProgress(true);
|
||||
}
|
||||
@@ -3099,6 +3101,7 @@ export class InteractiveMode {
|
||||
options: { updateFooter?: boolean; populateHistory?: boolean } = {},
|
||||
): void {
|
||||
this.pendingTools.clear();
|
||||
const renderedPendingTools = new Map<string, ToolExecutionComponent>();
|
||||
|
||||
if (options.updateFooter) {
|
||||
this.footer.invalidate();
|
||||
@@ -3140,16 +3143,16 @@ export class InteractiveMode {
|
||||
}
|
||||
component.updateResult({ content: [{ type: "text", text: errorMessage }], isError: true });
|
||||
} else {
|
||||
this.pendingTools.set(content.id, component);
|
||||
renderedPendingTools.set(content.id, component);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (message.role === "toolResult") {
|
||||
// Match tool results to pending tool components
|
||||
const component = this.pendingTools.get(message.toolCallId);
|
||||
const component = renderedPendingTools.get(message.toolCallId);
|
||||
if (component) {
|
||||
component.updateResult(message);
|
||||
this.pendingTools.delete(message.toolCallId);
|
||||
renderedPendingTools.delete(message.toolCallId);
|
||||
}
|
||||
} else {
|
||||
// All other messages use standard rendering
|
||||
@@ -3157,7 +3160,9 @@ export class InteractiveMode {
|
||||
}
|
||||
}
|
||||
|
||||
this.pendingTools.clear();
|
||||
for (const [toolCallId, component] of renderedPendingTools) {
|
||||
this.pendingTools.set(toolCallId, component);
|
||||
}
|
||||
this.ui.requestRender();
|
||||
}
|
||||
|
||||
@@ -4645,6 +4650,34 @@ export class InteractiveMode {
|
||||
}
|
||||
}
|
||||
|
||||
private showOAuthLoginSelect(dialog: LoginDialogComponent, prompt: OAuthSelectPrompt): Promise<string | undefined> {
|
||||
return new Promise((resolve) => {
|
||||
const restoreDialog = () => {
|
||||
this.editorContainer.clear();
|
||||
this.editorContainer.addChild(dialog);
|
||||
this.ui.setFocus(dialog);
|
||||
this.ui.requestRender();
|
||||
};
|
||||
const labels = prompt.options.map((option) => option.label);
|
||||
const selector = new ExtensionSelectorComponent(
|
||||
prompt.message,
|
||||
labels,
|
||||
(optionLabel) => {
|
||||
restoreDialog();
|
||||
resolve(prompt.options.find((option) => option.label === optionLabel)?.id);
|
||||
},
|
||||
() => {
|
||||
restoreDialog();
|
||||
resolve(undefined);
|
||||
},
|
||||
);
|
||||
this.editorContainer.clear();
|
||||
this.editorContainer.addChild(selector);
|
||||
this.ui.setFocus(selector);
|
||||
this.ui.requestRender();
|
||||
});
|
||||
}
|
||||
|
||||
private async showLoginDialog(providerId: string, providerName: string): Promise<void> {
|
||||
const providerInfo = this.session.modelRegistry.authStorage
|
||||
.getOAuthProviders()
|
||||
@@ -4722,6 +4755,8 @@ export class InteractiveMode {
|
||||
dialog.showProgress(message);
|
||||
},
|
||||
|
||||
onSelect: (prompt: OAuthSelectPrompt) => this.showOAuthLoginSelect(dialog, prompt),
|
||||
|
||||
onManualCodeInput: () => manualCodePromise,
|
||||
|
||||
signal: dialog.signal,
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
import { DefaultPackageManager } from "./core/package-manager.js";
|
||||
import { SettingsManager } from "./core/settings-manager.js";
|
||||
import { shouldUseWindowsShell } from "./utils/child-process.js";
|
||||
import { getLatestPiVersion, isNewerPackageVersion } from "./utils/version-check.js";
|
||||
import { getLatestPiRelease, isNewerPackageVersion } from "./utils/version-check.js";
|
||||
|
||||
export type PackageCommand = "install" | "remove" | "update" | "list";
|
||||
|
||||
@@ -273,9 +273,9 @@ function updateTargetIncludesExtensions(target: UpdateTarget): boolean {
|
||||
return target.type === "all" || target.type === "extensions";
|
||||
}
|
||||
|
||||
function printSelfUpdateUnavailable(npmCommand?: string[]): void {
|
||||
function printSelfUpdateUnavailable(npmCommand?: string[], updatePackageName = PACKAGE_NAME): void {
|
||||
console.error(`error: ${APP_NAME} cannot self-update this installation.`);
|
||||
console.error(getSelfUpdateUnavailableInstruction(PACKAGE_NAME, npmCommand));
|
||||
console.error(getSelfUpdateUnavailableInstruction(PACKAGE_NAME, npmCommand, updatePackageName));
|
||||
|
||||
const entrypoint = process.argv[1];
|
||||
if (entrypoint) {
|
||||
@@ -288,47 +288,53 @@ function printSelfUpdateFallback(command: SelfUpdateCommand): void {
|
||||
console.error(chalk.dim(`If this keeps failing, run this command yourself: ${command.display}`));
|
||||
}
|
||||
|
||||
async function shouldRunSelfUpdate(force: boolean): Promise<boolean> {
|
||||
interface SelfUpdatePlan {
|
||||
packageName: string;
|
||||
shouldRun: boolean;
|
||||
}
|
||||
|
||||
async function getSelfUpdatePlan(force: boolean): Promise<SelfUpdatePlan> {
|
||||
if (force) {
|
||||
return true;
|
||||
return { packageName: PACKAGE_NAME, shouldRun: true };
|
||||
}
|
||||
|
||||
let latestVersion: string | undefined;
|
||||
try {
|
||||
latestVersion = await getLatestPiVersion(VERSION);
|
||||
const latestRelease = await getLatestPiRelease(VERSION);
|
||||
const packageName = latestRelease?.packageName ?? PACKAGE_NAME;
|
||||
if (!latestRelease || packageName !== PACKAGE_NAME || isNewerPackageVersion(latestRelease.version, VERSION)) {
|
||||
return { packageName, shouldRun: true };
|
||||
}
|
||||
} catch {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!latestVersion || isNewerPackageVersion(latestVersion, VERSION)) {
|
||||
return true;
|
||||
return { packageName: PACKAGE_NAME, shouldRun: true };
|
||||
}
|
||||
|
||||
console.log(chalk.green(`${APP_NAME} is already up to date (v${VERSION})`));
|
||||
return false;
|
||||
return { packageName: PACKAGE_NAME, shouldRun: false };
|
||||
}
|
||||
|
||||
async function runSelfUpdate(command: SelfUpdateCommand): Promise<void> {
|
||||
console.log(chalk.dim(`Updating ${APP_NAME} with ${command.display}...`));
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
// Windows package managers are commonly .cmd shims. Use the shell so Node can execute them.
|
||||
const child = spawn(command.command, command.args, {
|
||||
stdio: "inherit",
|
||||
shell: shouldUseWindowsShell(command.command),
|
||||
for (const step of command.steps ?? [command]) {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
// Windows package managers are commonly .cmd shims. Use the shell so Node can execute them.
|
||||
const child = spawn(step.command, step.args, {
|
||||
stdio: "inherit",
|
||||
shell: shouldUseWindowsShell(step.command),
|
||||
});
|
||||
child.on("error", (error) => {
|
||||
reject(error);
|
||||
});
|
||||
child.on("close", (code, signal) => {
|
||||
if (code === 0) {
|
||||
resolve();
|
||||
} else if (signal) {
|
||||
reject(new Error(`${step.display} terminated by signal ${signal}`));
|
||||
} else {
|
||||
reject(new Error(`${step.display} exited with code ${code ?? "unknown"}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
child.on("error", (error) => {
|
||||
reject(error);
|
||||
});
|
||||
child.on("close", (code, signal) => {
|
||||
if (code === 0) {
|
||||
resolve();
|
||||
} else if (signal) {
|
||||
reject(new Error(`${command.display} terminated by signal ${signal}`));
|
||||
} else {
|
||||
reject(new Error(`${command.display} exited with code ${code ?? "unknown"}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function handleConfigCommand(args: string[]): Promise<boolean> {
|
||||
@@ -480,13 +486,18 @@ export async function handlePackageCommand(args: string[]): Promise<boolean> {
|
||||
}
|
||||
}
|
||||
if (updateTargetIncludesSelf(target)) {
|
||||
const selfUpdateCommand = getSelfUpdateCommand(PACKAGE_NAME, selfUpdateNpmCommand);
|
||||
if (!selfUpdateCommand) {
|
||||
printSelfUpdateUnavailable(selfUpdateNpmCommand);
|
||||
process.exitCode = 1;
|
||||
const selfUpdatePlan = await getSelfUpdatePlan(options.force);
|
||||
if (!selfUpdatePlan.shouldRun) {
|
||||
return true;
|
||||
}
|
||||
if (!(await shouldRunSelfUpdate(options.force))) {
|
||||
const selfUpdateCommand = getSelfUpdateCommand(
|
||||
PACKAGE_NAME,
|
||||
selfUpdateNpmCommand,
|
||||
selfUpdatePlan.packageName,
|
||||
);
|
||||
if (!selfUpdateCommand) {
|
||||
printSelfUpdateUnavailable(selfUpdateNpmCommand, selfUpdatePlan.packageName);
|
||||
process.exitCode = 1;
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
|
||||
@@ -35,11 +35,20 @@ function emitOsc52(text: string): boolean {
|
||||
export async function copyToClipboard(text: string): Promise<void> {
|
||||
let copied = false;
|
||||
|
||||
const p = platform();
|
||||
|
||||
// Prefer direct clipboard writes. Emitting OSC 52 first can make terminals
|
||||
// write the same native clipboard concurrently with the addon, and very large
|
||||
// OSC 52 payloads can desynchronize terminal rendering.
|
||||
//
|
||||
// On Linux, skip the native addon. The underlying `clipboard-rs` crate is
|
||||
// X11-only and does not retain selection ownership after `set_text`
|
||||
// resolves, so on Wayland-only compositors (Hyprland, Niri, ...) and even
|
||||
// some X11 sessions the call resolves successfully without populating the
|
||||
// clipboard. The platform tools below (wl-copy, xclip, xsel) properly
|
||||
// daemonize and keep ownership.
|
||||
try {
|
||||
if (clipboard) {
|
||||
if (clipboard && p !== "linux") {
|
||||
await clipboard.setText(text);
|
||||
copied = true;
|
||||
}
|
||||
@@ -52,7 +61,6 @@ export async function copyToClipboard(text: string): Promise<void> {
|
||||
return;
|
||||
}
|
||||
|
||||
const p = platform();
|
||||
const options: NativeClipboardExecOptions = { input: text, timeout: 5000, stdio: ["pipe", "ignore", "ignore"] };
|
||||
|
||||
if (!copied) {
|
||||
|
||||
@@ -3,6 +3,11 @@ import { getPiUserAgent } from "./pi-user-agent.js";
|
||||
const LATEST_VERSION_URL = "https://pi.dev/api/latest-version";
|
||||
const DEFAULT_VERSION_CHECK_TIMEOUT_MS = 10000;
|
||||
|
||||
export interface LatestPiRelease {
|
||||
version: string;
|
||||
packageName?: string;
|
||||
}
|
||||
|
||||
interface ParsedVersion {
|
||||
major: number;
|
||||
minor: number;
|
||||
@@ -47,10 +52,10 @@ export function isNewerPackageVersion(candidateVersion: string, currentVersion:
|
||||
return candidateVersion.trim() !== currentVersion.trim();
|
||||
}
|
||||
|
||||
export async function getLatestPiVersion(
|
||||
export async function getLatestPiRelease(
|
||||
currentVersion: string,
|
||||
options: { timeoutMs?: number } = {},
|
||||
): Promise<string | undefined> {
|
||||
): Promise<LatestPiRelease | undefined> {
|
||||
if (process.env.PI_SKIP_VERSION_CHECK || process.env.PI_OFFLINE) return undefined;
|
||||
|
||||
const response = await fetch(LATEST_VERSION_URL, {
|
||||
@@ -62,8 +67,20 @@ export async function getLatestPiVersion(
|
||||
});
|
||||
if (!response.ok) return undefined;
|
||||
|
||||
const data = (await response.json()) as { version?: unknown };
|
||||
return typeof data.version === "string" && data.version.trim() ? data.version.trim() : undefined;
|
||||
const data = (await response.json()) as { packageName?: unknown; version?: unknown };
|
||||
if (typeof data.version !== "string" || !data.version.trim()) {
|
||||
return undefined;
|
||||
}
|
||||
const packageName =
|
||||
typeof data.packageName === "string" && data.packageName.trim() ? data.packageName.trim() : undefined;
|
||||
return { version: data.version.trim(), packageName };
|
||||
}
|
||||
|
||||
export async function getLatestPiVersion(
|
||||
currentVersion: string,
|
||||
options: { timeoutMs?: number } = {},
|
||||
): Promise<string | undefined> {
|
||||
return (await getLatestPiRelease(currentVersion, options))?.version;
|
||||
}
|
||||
|
||||
export async function checkForNewPiVersion(currentVersion: string): Promise<string | undefined> {
|
||||
|
||||
@@ -43,6 +43,21 @@ describe("parseArgs", () => {
|
||||
const result = parseArgs(["-p"]);
|
||||
expect(result.print).toBe(true);
|
||||
});
|
||||
|
||||
test("parses prompt after -p even when it starts with YAML frontmatter", () => {
|
||||
const prompt = "---\ntitle: hello\n---\nSay hi.";
|
||||
const result = parseArgs(["-p", prompt]);
|
||||
expect(result.print).toBe(true);
|
||||
expect(result.messages).toEqual([prompt]);
|
||||
expect(result.unknownFlags.size).toBe(0);
|
||||
});
|
||||
|
||||
test("does not consume options after -p as prompts", () => {
|
||||
const result = parseArgs(["-p", "--provider", "openai", "Say hi."]);
|
||||
expect(result.print).toBe(true);
|
||||
expect(result.provider).toBe("openai");
|
||||
expect(result.messages).toEqual(["Say hi."]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("--continue flag", () => {
|
||||
|
||||
@@ -54,6 +54,49 @@ function createNpmPrefixInstall(template = "pi-prefix-"): { prefix: string; pack
|
||||
return { prefix, packageDir };
|
||||
}
|
||||
|
||||
function createPnpmGlobalInstall(): { root: string; packageDir: string } {
|
||||
const temp = mkdtempSync(join(tmpdir(), "pi-pnpm-"));
|
||||
const binDir = join(temp, "bin");
|
||||
const root = join(temp, "pnpm", "global", "5", "node_modules");
|
||||
const packageDir = join(root, "@mariozechner", "pi-coding-agent");
|
||||
mkdirSync(packageDir, { recursive: true });
|
||||
mkdirSync(binDir, { recursive: true });
|
||||
writeFileSync(join(binDir, process.platform === "win32" ? "pnpm.cmd" : "pnpm"), createFakePnpmScript(root));
|
||||
chmodSync(join(binDir, process.platform === "win32" ? "pnpm.cmd" : "pnpm"), 0o755);
|
||||
tempDir = temp;
|
||||
process.env.PATH = `${binDir}${delimiter}${originalPath ?? ""}`;
|
||||
process.env.PI_PACKAGE_DIR = packageDir;
|
||||
setExecPath(
|
||||
join(
|
||||
root,
|
||||
".pnpm",
|
||||
"@mariozechner+pi-coding-agent@0.0.0",
|
||||
"node_modules",
|
||||
"@mariozechner",
|
||||
"pi-coding-agent",
|
||||
"dist",
|
||||
"cli.js",
|
||||
),
|
||||
);
|
||||
return { root, packageDir };
|
||||
}
|
||||
|
||||
function createYarnGlobalInstall(): { globalDir: string; packageDir: string } {
|
||||
const temp = mkdtempSync(join(tmpdir(), "pi-yarn-"));
|
||||
const binDir = join(temp, "bin");
|
||||
const globalDir = join(temp, "yarn", "global");
|
||||
const packageDir = join(globalDir, "node_modules", "@mariozechner", "pi-coding-agent");
|
||||
mkdirSync(packageDir, { recursive: true });
|
||||
mkdirSync(binDir, { recursive: true });
|
||||
writeFileSync(join(binDir, process.platform === "win32" ? "yarn.cmd" : "yarn"), createFakeYarnScript(globalDir));
|
||||
chmodSync(join(binDir, process.platform === "win32" ? "yarn.cmd" : "yarn"), 0o755);
|
||||
tempDir = temp;
|
||||
process.env.PATH = `${binDir}${delimiter}${originalPath ?? ""}`;
|
||||
process.env.PI_PACKAGE_DIR = packageDir;
|
||||
setExecPath(join(globalDir, ".yarn", "@mariozechner", "pi-coding-agent", "dist", "cli.js"));
|
||||
return { globalDir, packageDir };
|
||||
}
|
||||
|
||||
function createBunGlobalInstall(): { packageDir: string } {
|
||||
const temp = mkdtempSync(join(tmpdir(), "pi-bun-"));
|
||||
const prefix = join(temp, ".bun");
|
||||
@@ -72,6 +115,22 @@ function createBunGlobalInstall(): { packageDir: string } {
|
||||
return { packageDir };
|
||||
}
|
||||
|
||||
function createFakePnpmScript(root: string): string {
|
||||
if (process.platform === "win32") {
|
||||
return `@echo off\r\nif "%1"=="root" if "%2"=="-g" echo ${root}\r\n`;
|
||||
}
|
||||
const escapedRoot = root.replaceAll("'", "'\\''");
|
||||
return `#!/bin/sh\nif [ "$1" = "root" ] && [ "$2" = "-g" ]; then\n\tprintf '%s\\n' '${escapedRoot}'\n\texit 0\nfi\nexit 1\n`;
|
||||
}
|
||||
|
||||
function createFakeYarnScript(globalDir: string): string {
|
||||
if (process.platform === "win32") {
|
||||
return `@echo off\r\nif "%1"=="global" if "%2"=="dir" echo ${globalDir}\r\n`;
|
||||
}
|
||||
const escapedGlobalDir = globalDir.replaceAll("'", "'\\''");
|
||||
return `#!/bin/sh\nif [ "$1" = "global" ] && [ "$2" = "dir" ]; then\n\tprintf '%s\\n' '${escapedGlobalDir}'\n\texit 0\nfi\nexit 1\n`;
|
||||
}
|
||||
|
||||
function createFakeBunScript(bunBin: string): string {
|
||||
if (process.platform === "win32") {
|
||||
return `@echo off\r\nif "%1"=="pm" if "%2"=="bin" if "%3"=="-g" echo ${bunBin}\r\n`;
|
||||
@@ -115,6 +174,30 @@ describe("detectInstallMethod", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test("self-updates renamed packages from the current install prefix", () => {
|
||||
const { prefix } = createNpmPrefixInstall();
|
||||
|
||||
const command = getSelfUpdateCommand("@mariozechner/pi-coding-agent", undefined, "@new-scope/pi");
|
||||
|
||||
expect(command).toEqual({
|
||||
command: "npm",
|
||||
args: ["--prefix", prefix, "install", "-g", "@new-scope/pi"],
|
||||
display: `npm --prefix ${prefix} uninstall -g @mariozechner/pi-coding-agent && npm --prefix ${prefix} install -g @new-scope/pi`,
|
||||
steps: [
|
||||
{
|
||||
command: "npm",
|
||||
args: ["--prefix", prefix, "uninstall", "-g", "@mariozechner/pi-coding-agent"],
|
||||
display: `npm --prefix ${prefix} uninstall -g @mariozechner/pi-coding-agent`,
|
||||
},
|
||||
{
|
||||
command: "npm",
|
||||
args: ["--prefix", prefix, "install", "-g", "@new-scope/pi"],
|
||||
display: `npm --prefix ${prefix} install -g @new-scope/pi`,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test("self-update respects configured npmCommand", () => {
|
||||
const { prefix } = createNpmPrefixInstall();
|
||||
|
||||
@@ -167,6 +250,81 @@ describe("detectInstallMethod", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test("self-updates renamed pnpm global installs by removing the old package first", () => {
|
||||
createPnpmGlobalInstall();
|
||||
|
||||
const command = getSelfUpdateCommand("@mariozechner/pi-coding-agent", undefined, "@new-scope/pi");
|
||||
|
||||
expect(detectInstallMethod()).toBe("pnpm");
|
||||
expect(command).toEqual({
|
||||
command: "pnpm",
|
||||
args: ["install", "-g", "@new-scope/pi"],
|
||||
display: "pnpm remove -g @mariozechner/pi-coding-agent && pnpm install -g @new-scope/pi",
|
||||
steps: [
|
||||
{
|
||||
command: "pnpm",
|
||||
args: ["remove", "-g", "@mariozechner/pi-coding-agent"],
|
||||
display: "pnpm remove -g @mariozechner/pi-coding-agent",
|
||||
},
|
||||
{
|
||||
command: "pnpm",
|
||||
args: ["install", "-g", "@new-scope/pi"],
|
||||
display: "pnpm install -g @new-scope/pi",
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test("self-updates renamed yarn global installs by removing the old package first", () => {
|
||||
createYarnGlobalInstall();
|
||||
|
||||
const command = getSelfUpdateCommand("@mariozechner/pi-coding-agent", undefined, "@new-scope/pi");
|
||||
|
||||
expect(detectInstallMethod()).toBe("yarn");
|
||||
expect(command).toEqual({
|
||||
command: "yarn",
|
||||
args: ["global", "add", "@new-scope/pi"],
|
||||
display: "yarn global remove @mariozechner/pi-coding-agent && yarn global add @new-scope/pi",
|
||||
steps: [
|
||||
{
|
||||
command: "yarn",
|
||||
args: ["global", "remove", "@mariozechner/pi-coding-agent"],
|
||||
display: "yarn global remove @mariozechner/pi-coding-agent",
|
||||
},
|
||||
{
|
||||
command: "yarn",
|
||||
args: ["global", "add", "@new-scope/pi"],
|
||||
display: "yarn global add @new-scope/pi",
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test("self-updates renamed bun global installs by removing the old package first", () => {
|
||||
createBunGlobalInstall();
|
||||
|
||||
const command = getSelfUpdateCommand("@mariozechner/pi-coding-agent", undefined, "@new-scope/pi");
|
||||
|
||||
expect(detectInstallMethod()).toBe("bun");
|
||||
expect(command).toEqual({
|
||||
command: "bun",
|
||||
args: ["install", "-g", "@new-scope/pi"],
|
||||
display: "bun uninstall -g @mariozechner/pi-coding-agent && bun install -g @new-scope/pi",
|
||||
steps: [
|
||||
{
|
||||
command: "bun",
|
||||
args: ["uninstall", "-g", "@mariozechner/pi-coding-agent"],
|
||||
display: "bun uninstall -g @mariozechner/pi-coding-agent",
|
||||
},
|
||||
{
|
||||
command: "bun",
|
||||
args: ["install", "-g", "@new-scope/pi"],
|
||||
display: "bun install -g @new-scope/pi",
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test("does not self-update when npm install path is not writable", () => {
|
||||
const { packageDir } = createNpmPrefixInstall();
|
||||
chmodSync(packageDir, 0o500);
|
||||
|
||||
40
packages/coding-agent/test/export-html-skill-block.test.ts
Normal file
40
packages/coding-agent/test/export-html-skill-block.test.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { readFileSync } from "fs";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
describe("export HTML skill block rendering", () => {
|
||||
const templateJs = readFileSync(new URL("../src/core/export-html/template.js", import.meta.url), "utf-8");
|
||||
|
||||
it("strips skill wrapper XML from user message rendering", () => {
|
||||
// Skill commands store a structural wrapper in the raw user message:
|
||||
// <skill name="..." location="...">\n...\n</skill>\n\nactual prompt
|
||||
// The export renderer must detect that wrapper and render only the user-visible prompt,
|
||||
// not the Pi-generated <skill>...</skill> XML tags.
|
||||
expect(templateJs).toMatch(/parseSkillBlock/);
|
||||
expect(templateJs).toMatch(/skillBlock\.userMessage/);
|
||||
});
|
||||
|
||||
it("renders skill invocation and user message as separate sibling blocks", () => {
|
||||
// The skill block and user message should render as separate entry-level elements,
|
||||
// matching the TUI layout where SkillInvocationMessageComponent and
|
||||
// UserMessageComponent are siblings, not nested.
|
||||
expect(templateJs).toMatch(/skill-invocation/);
|
||||
|
||||
// When a skill block has a userMessage, the user-message div must be emitted
|
||||
// as a separate block after the skill-invocation div, containing the user-authored text.
|
||||
// Verify the code checks hasUserContent so the user-message div is only omitted
|
||||
// when the skill block has no user prompt and no images.
|
||||
expect(templateJs).toMatch(/hasUserContent/);
|
||||
});
|
||||
|
||||
it("renders skill content as markdown, not raw text", () => {
|
||||
// The skill block body is markdown (from the SKILL.md file).
|
||||
// It should be rendered through safeMarkedParse, not escaped as raw text.
|
||||
expect(templateJs).toMatch(/safeMarkedParse\(skillBlock\.content\)/);
|
||||
});
|
||||
|
||||
it("shows skill name and user message in the sidebar tree", () => {
|
||||
// The sidebar tree should display both the skill name and the user prompt,
|
||||
// not just one or the other.
|
||||
expect(templateJs).toMatch(/tree-role-skill/);
|
||||
});
|
||||
});
|
||||
@@ -2,7 +2,7 @@ import { mkdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from "no
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { ENV_AGENT_DIR } from "../src/config.js";
|
||||
import { ENV_AGENT_DIR, PACKAGE_NAME } from "../src/config.js";
|
||||
import { main } from "../src/main.js";
|
||||
|
||||
describe("package commands", () => {
|
||||
@@ -36,6 +36,7 @@ describe("package commands", () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
process.chdir(originalCwd);
|
||||
process.exitCode = originalExitCode;
|
||||
if (originalAgentDir === undefined) {
|
||||
@@ -128,7 +129,7 @@ describe("package commands", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("uses global npmCommand for self updates", async () => {
|
||||
it("uses global npmCommand and current package name for forced self updates without checking the api", async () => {
|
||||
const globalPrefix = join(tempDir, "global-prefix");
|
||||
const projectPrefix = join(tempDir, "project-prefix");
|
||||
const selfPackageDir = join(globalPrefix, "lib", "node_modules", "@mariozechner", "pi-coding-agent");
|
||||
@@ -156,6 +157,8 @@ else fs.writeFileSync(${JSON.stringify(recordPath)},JSON.stringify(args));
|
||||
value: join(selfPackageDir, "dist", "cli.js"),
|
||||
configurable: true,
|
||||
});
|
||||
const fetchMock = vi.fn();
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
|
||||
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
||||
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
@@ -165,8 +168,10 @@ else fs.writeFileSync(${JSON.stringify(recordPath)},JSON.stringify(args));
|
||||
|
||||
expect(process.exitCode).toBeUndefined();
|
||||
expect(errorSpy).not.toHaveBeenCalled();
|
||||
expect(fetchMock).not.toHaveBeenCalled();
|
||||
const recordedArgs = JSON.parse(readFileSync(recordPath, "utf-8")) as string[];
|
||||
expect(recordedArgs).toContain(globalPrefix);
|
||||
expect(recordedArgs).toContain(PACKAGE_NAME);
|
||||
expect(recordedArgs).not.toContain(projectPrefix);
|
||||
} finally {
|
||||
logSpy.mockRestore();
|
||||
@@ -174,6 +179,155 @@ else fs.writeFileSync(${JSON.stringify(recordPath)},JSON.stringify(args));
|
||||
}
|
||||
});
|
||||
|
||||
it("uses the current package name when the update check omits packageName", async () => {
|
||||
const globalPrefix = join(tempDir, "global-prefix");
|
||||
const selfPackageDir = join(globalPrefix, "lib", "node_modules", "@mariozechner", "pi-coding-agent");
|
||||
const fakeNpmPath = join(tempDir, "fake-npm.cjs");
|
||||
const recordPath = join(tempDir, "self-update.json");
|
||||
mkdirSync(selfPackageDir, { recursive: true });
|
||||
writeFileSync(
|
||||
fakeNpmPath,
|
||||
`const fs=require("node:fs"),path=require("node:path"),args=process.argv.slice(2),prefix=args[args.indexOf("--prefix")+1];
|
||||
if(args.includes("root")) console.log(path.join(prefix,"lib","node_modules"));
|
||||
else fs.writeFileSync(${JSON.stringify(recordPath)},JSON.stringify(args));
|
||||
`,
|
||||
);
|
||||
writeFileSync(
|
||||
join(agentDir, "settings.json"),
|
||||
JSON.stringify({ npmCommand: [originalExecPath, fakeNpmPath, "--prefix", globalPrefix] }, null, 2),
|
||||
);
|
||||
process.env.PI_PACKAGE_DIR = selfPackageDir;
|
||||
Object.defineProperty(process, "execPath", {
|
||||
value: join(selfPackageDir, "dist", "cli.js"),
|
||||
configurable: true,
|
||||
});
|
||||
const fetchMock = vi.fn(async () => Response.json({ version: "0.73.1" }));
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
|
||||
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
||||
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
|
||||
try {
|
||||
await expect(main(["update", "--self"])).resolves.toBeUndefined();
|
||||
|
||||
expect(process.exitCode).toBeUndefined();
|
||||
expect(errorSpy).not.toHaveBeenCalled();
|
||||
expect(fetchMock).toHaveBeenCalledOnce();
|
||||
const recordedArgs = JSON.parse(readFileSync(recordPath, "utf-8")) as string[];
|
||||
expect(recordedArgs).toContain(PACKAGE_NAME);
|
||||
} finally {
|
||||
logSpy.mockRestore();
|
||||
errorSpy.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
it("installs the active package name from the update check during self-update", async () => {
|
||||
const globalPrefix = join(tempDir, "global-prefix");
|
||||
const selfPackageDir = join(globalPrefix, "lib", "node_modules", "@mariozechner", "pi-coding-agent");
|
||||
const fakeNpmPath = join(tempDir, "fake-npm.cjs");
|
||||
const recordPath = join(tempDir, "self-update.json");
|
||||
mkdirSync(selfPackageDir, { recursive: true });
|
||||
writeFileSync(
|
||||
fakeNpmPath,
|
||||
`const fs=require("node:fs"),path=require("node:path"),args=process.argv.slice(2),prefix=args[args.indexOf("--prefix")+1];
|
||||
if(args.includes("root")) console.log(path.join(prefix,"lib","node_modules"));
|
||||
else {
|
||||
const records=fs.existsSync(${JSON.stringify(recordPath)})?JSON.parse(fs.readFileSync(${JSON.stringify(recordPath)},"utf-8")):[];
|
||||
records.push(args);
|
||||
fs.writeFileSync(${JSON.stringify(recordPath)},JSON.stringify(records));
|
||||
}
|
||||
`,
|
||||
);
|
||||
writeFileSync(
|
||||
join(agentDir, "settings.json"),
|
||||
JSON.stringify({ npmCommand: [originalExecPath, fakeNpmPath, "--prefix", globalPrefix] }, null, 2),
|
||||
);
|
||||
process.env.PI_PACKAGE_DIR = selfPackageDir;
|
||||
Object.defineProperty(process, "execPath", {
|
||||
value: join(selfPackageDir, "dist", "cli.js"),
|
||||
configurable: true,
|
||||
});
|
||||
const activePackageName = PACKAGE_NAME === "@new-scope/pi" ? "@newer-scope/pi" : "@new-scope/pi";
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn(async () => Response.json({ packageName: activePackageName, version: "0.73.0" })),
|
||||
);
|
||||
|
||||
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
||||
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
|
||||
try {
|
||||
await expect(main(["update", "--self"])).resolves.toBeUndefined();
|
||||
|
||||
expect(process.exitCode).toBeUndefined();
|
||||
expect(errorSpy).not.toHaveBeenCalled();
|
||||
const recordedCalls = JSON.parse(readFileSync(recordPath, "utf-8")) as string[][];
|
||||
expect(recordedCalls).toEqual([
|
||||
expect.arrayContaining(["uninstall", "-g", PACKAGE_NAME]),
|
||||
expect.arrayContaining(["install", "-g", activePackageName]),
|
||||
]);
|
||||
} finally {
|
||||
logSpy.mockRestore();
|
||||
errorSpy.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
it("fails self-update when renamed npm package installation fails", async () => {
|
||||
const globalPrefix = join(tempDir, "global-prefix");
|
||||
const selfPackageDir = join(globalPrefix, "lib", "node_modules", "@mariozechner", "pi-coding-agent");
|
||||
const fakeNpmPath = join(tempDir, "fake-npm-fail.cjs");
|
||||
const recordPath = join(tempDir, "self-update-fail.json");
|
||||
mkdirSync(selfPackageDir, { recursive: true });
|
||||
writeFileSync(
|
||||
fakeNpmPath,
|
||||
`const fs=require("node:fs"),path=require("node:path"),args=process.argv.slice(2),prefix=args[args.indexOf("--prefix")+1];
|
||||
if(args.includes("root")) {
|
||||
console.log(path.join(prefix,"lib","node_modules"));
|
||||
process.exit(0);
|
||||
}
|
||||
const records=fs.existsSync(${JSON.stringify(recordPath)})?JSON.parse(fs.readFileSync(${JSON.stringify(recordPath)},"utf-8")):[];
|
||||
records.push(args);
|
||||
fs.writeFileSync(${JSON.stringify(recordPath)},JSON.stringify(records));
|
||||
if(args.includes("install")) process.exit(23);
|
||||
`,
|
||||
);
|
||||
writeFileSync(
|
||||
join(agentDir, "settings.json"),
|
||||
JSON.stringify({ npmCommand: [originalExecPath, fakeNpmPath, "--prefix", globalPrefix] }, null, 2),
|
||||
);
|
||||
process.env.PI_PACKAGE_DIR = selfPackageDir;
|
||||
Object.defineProperty(process, "execPath", {
|
||||
value: join(selfPackageDir, "dist", "cli.js"),
|
||||
configurable: true,
|
||||
});
|
||||
const activePackageName = PACKAGE_NAME === "@new-scope/pi" ? "@newer-scope/pi" : "@new-scope/pi";
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn(async () => Response.json({ packageName: activePackageName, version: "0.73.0" })),
|
||||
);
|
||||
|
||||
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
||||
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
|
||||
try {
|
||||
await expect(main(["update", "--self"])).resolves.toBeUndefined();
|
||||
|
||||
expect(process.exitCode).toBe(1);
|
||||
const stdout = logSpy.mock.calls.map(([message]) => String(message)).join("\n");
|
||||
const stderr = errorSpy.mock.calls.map(([message]) => String(message)).join("\n");
|
||||
expect(stdout).not.toContain(`Updated pi`);
|
||||
expect(stderr).toContain("exited with code 23");
|
||||
const recordedCalls = JSON.parse(readFileSync(recordPath, "utf-8")) as string[][];
|
||||
expect(recordedCalls).toEqual([
|
||||
expect.arrayContaining(["uninstall", "-g", PACKAGE_NAME]),
|
||||
expect.arrayContaining(["install", "-g", activePackageName]),
|
||||
]);
|
||||
} finally {
|
||||
logSpy.mockRestore();
|
||||
errorSpy.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
it("suggests the configured source when update input omits the npm prefix", async () => {
|
||||
const settingsPath = join(agentDir, "settings.json");
|
||||
writeFileSync(settingsPath, JSON.stringify({ packages: ["npm:pi-formatter"] }, null, 2));
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
import type { AgentMessage } from "@mariozechner/pi-agent-core";
|
||||
import type { AssistantMessage, ToolResultMessage, Usage } from "@mariozechner/pi-ai";
|
||||
import { Container, Text, type TUI } from "@mariozechner/pi-tui";
|
||||
import stripAnsi from "strip-ansi";
|
||||
import { beforeAll, describe, expect, test, vi } from "vitest";
|
||||
import type { AgentSessionEvent } from "../../../src/core/agent-session.js";
|
||||
import type { SessionContext } from "../../../src/core/session-manager.js";
|
||||
import type { ToolExecutionComponent } from "../../../src/modes/interactive/components/tool-execution.js";
|
||||
import { InteractiveMode } from "../../../src/modes/interactive/interactive-mode.js";
|
||||
import { initTheme } from "../../../src/modes/interactive/theme/theme.js";
|
||||
|
||||
const TOOL_CALL_ID = "tool-4167";
|
||||
const TOOL_NAME = "slow_tool";
|
||||
|
||||
const EMPTY_USAGE: Usage = {
|
||||
input: 0,
|
||||
output: 0,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
totalTokens: 0,
|
||||
cost: {
|
||||
input: 0,
|
||||
output: 0,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
total: 0,
|
||||
},
|
||||
};
|
||||
|
||||
type RenderSessionContextThis = {
|
||||
pendingTools: Map<string, ToolExecutionComponent>;
|
||||
chatContainer: Container;
|
||||
footer: { invalidate(): void };
|
||||
ui: TUI;
|
||||
settingsManager: {
|
||||
getShowImages(): boolean;
|
||||
getImageWidthCells(): number;
|
||||
};
|
||||
sessionManager: { getCwd(): string };
|
||||
session: { retryAttempt: number };
|
||||
toolOutputExpanded: boolean;
|
||||
isInitialized: boolean;
|
||||
updateEditorBorderColor(): void;
|
||||
getRegisteredToolDefinition(toolName: string): undefined;
|
||||
addMessageToChat(message: AgentMessage, options?: { populateHistory?: boolean }): void;
|
||||
};
|
||||
|
||||
type RenderSessionContext = (
|
||||
this: RenderSessionContextThis,
|
||||
sessionContext: SessionContext,
|
||||
options?: { updateFooter?: boolean; populateHistory?: boolean },
|
||||
) => void;
|
||||
|
||||
type HandleEvent = (this: RenderSessionContextThis, event: AgentSessionEvent) => Promise<void>;
|
||||
|
||||
function createFakeInteractiveModeThis(): RenderSessionContextThis {
|
||||
const chatContainer = new Container();
|
||||
return {
|
||||
pendingTools: new Map<string, ToolExecutionComponent>(),
|
||||
chatContainer,
|
||||
footer: { invalidate: vi.fn() },
|
||||
ui: { requestRender: vi.fn() } as unknown as TUI,
|
||||
settingsManager: {
|
||||
getShowImages: () => false,
|
||||
getImageWidthCells: () => 60,
|
||||
},
|
||||
sessionManager: { getCwd: () => process.cwd() },
|
||||
session: { retryAttempt: 0 },
|
||||
toolOutputExpanded: false,
|
||||
isInitialized: true,
|
||||
updateEditorBorderColor: vi.fn(),
|
||||
getRegisteredToolDefinition: (_toolName: string) => undefined,
|
||||
addMessageToChat(message: AgentMessage) {
|
||||
chatContainer.addChild(new Text(message.role, 0, 0));
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createAssistantToolCallMessage(): AssistantMessage {
|
||||
return {
|
||||
role: "assistant",
|
||||
content: [
|
||||
{
|
||||
type: "toolCall",
|
||||
id: TOOL_CALL_ID,
|
||||
name: TOOL_NAME,
|
||||
arguments: { delayMs: 10_000 },
|
||||
},
|
||||
],
|
||||
api: "test-api",
|
||||
provider: "test-provider",
|
||||
model: "test-model",
|
||||
usage: EMPTY_USAGE,
|
||||
stopReason: "toolUse",
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
}
|
||||
|
||||
function createToolResultMessage(text: string): ToolResultMessage {
|
||||
return {
|
||||
role: "toolResult",
|
||||
toolCallId: TOOL_CALL_ID,
|
||||
toolName: TOOL_NAME,
|
||||
content: [{ type: "text", text }],
|
||||
isError: false,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
}
|
||||
|
||||
function createSessionContext(messages: AgentMessage[]): SessionContext {
|
||||
return {
|
||||
messages,
|
||||
thinkingLevel: "off",
|
||||
model: null,
|
||||
};
|
||||
}
|
||||
|
||||
function renderChat(container: Container): string {
|
||||
return stripAnsi(container.render(120).join("\n"));
|
||||
}
|
||||
|
||||
describe("InteractiveMode.renderSessionContext", () => {
|
||||
beforeAll(() => {
|
||||
initTheme("dark");
|
||||
});
|
||||
|
||||
test("keeps unresolved rendered tool calls registered for live completion events", async () => {
|
||||
const fakeThis = createFakeInteractiveModeThis();
|
||||
const renderSessionContext = (
|
||||
InteractiveMode.prototype as unknown as { renderSessionContext: RenderSessionContext }
|
||||
).renderSessionContext;
|
||||
const handleEvent = (InteractiveMode.prototype as unknown as { handleEvent: HandleEvent }).handleEvent;
|
||||
|
||||
renderSessionContext.call(fakeThis, createSessionContext([createAssistantToolCallMessage()]));
|
||||
|
||||
expect(fakeThis.pendingTools.has(TOOL_CALL_ID)).toBe(true);
|
||||
|
||||
await handleEvent.call(fakeThis, {
|
||||
type: "tool_execution_end",
|
||||
toolCallId: TOOL_CALL_ID,
|
||||
toolName: TOOL_NAME,
|
||||
result: { content: [{ type: "text", text: "FINAL_RESULT" }], details: undefined },
|
||||
isError: false,
|
||||
});
|
||||
|
||||
expect(fakeThis.pendingTools.has(TOOL_CALL_ID)).toBe(false);
|
||||
expect(renderChat(fakeThis.chatContainer)).toContain("FINAL_RESULT");
|
||||
});
|
||||
|
||||
test("does not keep completed historical tool calls registered as pending", () => {
|
||||
const fakeThis = createFakeInteractiveModeThis();
|
||||
const renderSessionContext = (
|
||||
InteractiveMode.prototype as unknown as { renderSessionContext: RenderSessionContext }
|
||||
).renderSessionContext;
|
||||
|
||||
renderSessionContext.call(
|
||||
fakeThis,
|
||||
createSessionContext([createAssistantToolCallMessage(), createToolResultMessage("HISTORICAL_RESULT")]),
|
||||
);
|
||||
|
||||
expect(fakeThis.pendingTools.size).toBe(0);
|
||||
expect(renderChat(fakeThis.chatContainer)).toContain("HISTORICAL_RESULT");
|
||||
});
|
||||
});
|
||||
@@ -3,7 +3,7 @@ import { tmpdir } from "os";
|
||||
import { join } from "path";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { executeBashWithOperations } from "../src/core/bash-executor.js";
|
||||
import { createBashTool, createLocalBashOperations } from "../src/core/tools/bash.js";
|
||||
import { type BashOperations, createBashTool, createLocalBashOperations } from "../src/core/tools/bash.js";
|
||||
import { computeEditsDiff } from "../src/core/tools/edit-diff.js";
|
||||
import {
|
||||
createEditTool,
|
||||
@@ -449,6 +449,42 @@ describe("Coding Agent Tools", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("should include full output path for truncated timeout and abort errors", async () => {
|
||||
for (const testCase of [
|
||||
{ error: "timeout:5", expected: "Command timed out after 5 seconds" },
|
||||
{ error: "aborted", expected: "Command aborted" },
|
||||
]) {
|
||||
const operations: BashOperations = {
|
||||
exec: async (_command, _cwd, { onData }) => {
|
||||
for (let i = 1; i <= 3000; i++) {
|
||||
onData(Buffer.from(`${i}\n`, "utf-8"));
|
||||
}
|
||||
throw new Error(testCase.error);
|
||||
},
|
||||
};
|
||||
const bash = createBashTool(testDir, { operations });
|
||||
|
||||
let error: unknown;
|
||||
try {
|
||||
await bash.execute(`test-call-${testCase.error}`, { command: "chatty-fail" });
|
||||
} catch (err) {
|
||||
error = err;
|
||||
}
|
||||
|
||||
expect(error).toBeInstanceOf(Error);
|
||||
const message = (error as Error).message;
|
||||
expect(message).toContain(testCase.expected);
|
||||
expect(message).toMatch(/\[Showing lines \d+-\d+ of \d+\. Full output: /);
|
||||
expect(message).not.toContain("Full output: undefined");
|
||||
const fullOutputPath = message.match(/Full output: ([^\]\n]+)/)?.[1];
|
||||
expect(fullOutputPath).toBeDefined();
|
||||
expect(existsSync(fullOutputPath!)).toBe(true);
|
||||
const fullOutput = readFileSync(fullOutputPath!, "utf-8");
|
||||
expect(fullOutput).toContain("1\n2\n3");
|
||||
expect(fullOutput).toContain("2998\n2999\n3000");
|
||||
}
|
||||
});
|
||||
|
||||
it("should throw error when cwd does not exist", async () => {
|
||||
const nonexistentCwd = "/this/directory/definitely/does/not/exist/12345";
|
||||
|
||||
@@ -517,6 +553,42 @@ describe("Coding Agent Tools", () => {
|
||||
expect(getTextOutput(result).trim()).toBe("no-prefix");
|
||||
});
|
||||
|
||||
it("should coalesce streaming updates for chatty output", async () => {
|
||||
const operations: BashOperations = {
|
||||
exec: async (_command, _cwd, { onData }) => {
|
||||
for (let i = 0; i < 5000; i++) {
|
||||
onData(Buffer.from(`line ${i}\n`, "utf-8"));
|
||||
}
|
||||
return { exitCode: 0 };
|
||||
},
|
||||
};
|
||||
const updates: Array<{ content: Array<{ type: string; text?: string }>; details?: unknown }> = [];
|
||||
const bash = createBashTool(testDir, { operations });
|
||||
|
||||
const result = await bash.execute("test-call-chatty-updates", { command: "chatty" }, undefined, (update) =>
|
||||
updates.push(update),
|
||||
);
|
||||
|
||||
expect(updates.length).toBeLessThan(25);
|
||||
expect(getTextOutput(result)).toContain("line 4999");
|
||||
});
|
||||
|
||||
it("should decode UTF-8 characters split across output chunks", async () => {
|
||||
const euro = Buffer.from("€\n", "utf-8");
|
||||
const operations: BashOperations = {
|
||||
exec: async (_command, _cwd, { onData }) => {
|
||||
onData(euro.subarray(0, 1));
|
||||
onData(euro.subarray(1));
|
||||
return { exitCode: 0 };
|
||||
},
|
||||
};
|
||||
const bash = createBashTool(testDir, { operations });
|
||||
|
||||
const result = await bash.execute("test-call-split-utf8", { command: "split-utf8" });
|
||||
|
||||
expect(getTextOutput(result).trim()).toBe("€");
|
||||
});
|
||||
|
||||
it("should expose local bash operations for extension reuse", async () => {
|
||||
const ops = createLocalBashOperations();
|
||||
const chunks: Buffer[] = [];
|
||||
|
||||
@@ -2,6 +2,7 @@ import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
checkForNewPiVersion,
|
||||
comparePackageVersions,
|
||||
getLatestPiRelease,
|
||||
getLatestPiVersion,
|
||||
isNewerPackageVersion,
|
||||
} from "../src/utils/version-check.js";
|
||||
@@ -56,6 +57,13 @@ describe("version checks", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("returns the active package name from the version check api", async () => {
|
||||
const fetchMock = vi.fn(async () => Response.json({ packageName: "@new-scope/pi", version: "1.2.4" }));
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
|
||||
await expect(getLatestPiRelease("1.2.3")).resolves.toEqual({ packageName: "@new-scope/pi", version: "1.2.4" });
|
||||
});
|
||||
|
||||
it("skips api calls when version checks are disabled", async () => {
|
||||
process.env.PI_SKIP_VERSION_CHECK = "1";
|
||||
const fetchMock = vi.fn();
|
||||
|
||||
@@ -2,6 +2,21 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.73.1] - 2026-05-07
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed wrapped OSC 8 hyperlinks to preserve BEL terminators so OAuth login URLs remain clickable on every wrapped line.
|
||||
- Fixed Kitty inline image redraws to stay within TUI-owned terminal regions and avoid writing below the active viewport.
|
||||
- Fixed Kitty inline image rendering by letting the terminal allocate image ids and bounding parsed image ids to valid values.
|
||||
- Fixed inline image capability detection to disable inline images in cmux terminals.
|
||||
|
||||
## [0.73.0] - 2026-05-04
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed fuzzy ranking to prioritize exact matches in selector and autocomplete results.
|
||||
|
||||
## [0.72.1] - 2026-05-02
|
||||
|
||||
## [0.72.0] - 2026-05-01
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@mariozechner/pi-tui",
|
||||
"version": "0.72.1",
|
||||
"version": "0.73.1",
|
||||
"description": "Terminal User Interface library with differential rendering for efficient text-based applications",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {
|
||||
allocateImageId,
|
||||
getCapabilities,
|
||||
getImageDimensions,
|
||||
type ImageDimensions,
|
||||
@@ -66,9 +67,13 @@ export class Image implements Component {
|
||||
let lines: string[];
|
||||
|
||||
if (caps.images) {
|
||||
if (caps.images === "kitty" && this.imageId === undefined) {
|
||||
this.imageId = allocateImageId();
|
||||
}
|
||||
const result = renderImage(this.base64Data, this.dimensions, {
|
||||
maxWidthCells: maxWidth,
|
||||
imageId: this.imageId,
|
||||
moveCursor: false,
|
||||
});
|
||||
|
||||
if (result) {
|
||||
@@ -77,16 +82,19 @@ export class Image implements Component {
|
||||
this.imageId = result.imageId;
|
||||
}
|
||||
|
||||
// Return `rows` lines so TUI accounts for image height
|
||||
// First (rows-1) lines are empty (TUI clears them)
|
||||
// Last line: move cursor back up, then output image sequence
|
||||
// Return `rows` lines so TUI accounts for image height.
|
||||
// First (rows-1) lines are empty and cleared before the image is drawn.
|
||||
// Last line: move cursor back up, draw the image, then move back down
|
||||
// for Kitty (this component disables Kitty's terminal-side cursor movement)
|
||||
// so TUI cursor accounting stays inside the scroll area.
|
||||
lines = [];
|
||||
for (let i = 0; i < result.rows - 1; i++) {
|
||||
lines.push("");
|
||||
}
|
||||
// Move cursor up to first row, then output image
|
||||
const moveUp = result.rows > 1 ? `\x1b[${result.rows - 1}A` : "";
|
||||
lines.push(moveUp + result.sequence);
|
||||
const rowOffset = result.rows - 1;
|
||||
const moveUp = rowOffset > 0 ? `\x1b[${rowOffset}A` : "";
|
||||
const moveDown = caps.images === "kitty" && rowOffset > 0 ? `\x1b[${rowOffset}B` : "";
|
||||
lines.push(moveUp + result.sequence + moveDown);
|
||||
} else {
|
||||
const fallback = imageFallback(this.mimeType, this.dimensions, this.options.filename);
|
||||
lines = [this.theme.fallbackColor(fallback)];
|
||||
|
||||
@@ -60,6 +60,10 @@ export function fuzzyMatch(query: string, text: string): FuzzyMatch {
|
||||
return { matches: false, score: 0 };
|
||||
}
|
||||
|
||||
if (normalizedQuery === textLower) {
|
||||
score -= 100;
|
||||
}
|
||||
|
||||
return { matches: true, score };
|
||||
};
|
||||
|
||||
|
||||
@@ -22,6 +22,8 @@ export interface ImageRenderOptions {
|
||||
preserveAspectRatio?: boolean;
|
||||
/** Kitty image ID. If provided, reuses/replaces existing image with this ID. */
|
||||
imageId?: number;
|
||||
/** Whether Kitty should apply its default cursor movement after placement. */
|
||||
moveCursor?: boolean;
|
||||
}
|
||||
|
||||
let cachedCapabilities: TerminalCapabilities | null = null;
|
||||
@@ -128,12 +130,15 @@ export function encodeKitty(
|
||||
columns?: number;
|
||||
rows?: number;
|
||||
imageId?: number;
|
||||
/** Whether Kitty should apply its default cursor movement after placement. Default: true. */
|
||||
moveCursor?: boolean;
|
||||
} = {},
|
||||
): string {
|
||||
const CHUNK_SIZE = 4096;
|
||||
|
||||
const params: string[] = ["a=T", "f=100", "q=2"];
|
||||
|
||||
if (options.moveCursor === false) params.push("C=1");
|
||||
if (options.columns) params.push(`c=${options.columns}`);
|
||||
if (options.rows) params.push(`r=${options.rows}`);
|
||||
if (options.imageId) params.push(`i=${options.imageId}`);
|
||||
@@ -170,7 +175,7 @@ export function encodeKitty(
|
||||
* Uses uppercase 'I' to also free the image data.
|
||||
*/
|
||||
export function deleteKittyImage(imageId: number): string {
|
||||
return `\x1b_Ga=d,d=I,i=${imageId}\x1b\\`;
|
||||
return `\x1b_Ga=d,d=I,i=${imageId},q=2\x1b\\`;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -178,7 +183,7 @@ export function deleteKittyImage(imageId: number): string {
|
||||
* Uses uppercase 'A' to also free the image data.
|
||||
*/
|
||||
export function deleteAllKittyImages(): string {
|
||||
return `\x1b_Ga=d,d=A\x1b\\`;
|
||||
return "\x1b_Ga=d,d=A,q=2\x1b\\";
|
||||
}
|
||||
|
||||
export function encodeITerm2(
|
||||
@@ -374,8 +379,12 @@ export function renderImage(
|
||||
const rows = calculateImageRows(imageDimensions, maxWidth, getCellDimensions());
|
||||
|
||||
if (caps.images === "kitty") {
|
||||
// Only use imageId if explicitly provided - static images don't need IDs
|
||||
const sequence = encodeKitty(base64Data, { columns: maxWidth, rows, imageId: options.imageId });
|
||||
const sequence = encodeKitty(base64Data, {
|
||||
columns: maxWidth,
|
||||
rows,
|
||||
imageId: options.imageId,
|
||||
moveCursor: options.moveCursor,
|
||||
});
|
||||
return { sequence, rows, imageId: options.imageId };
|
||||
}
|
||||
|
||||
|
||||
@@ -8,9 +8,31 @@ import * as path from "node:path";
|
||||
import { performance } from "node:perf_hooks";
|
||||
import { isKeyRelease, matchesKey } from "./keys.js";
|
||||
import type { Terminal } from "./terminal.js";
|
||||
import { getCapabilities, isImageLine, setCellDimensions } from "./terminal-image.js";
|
||||
import { deleteKittyImage, getCapabilities, isImageLine, setCellDimensions } from "./terminal-image.js";
|
||||
import { extractSegments, normalizeTerminalOutput, sliceByColumn, sliceWithWidth, visibleWidth } from "./utils.js";
|
||||
|
||||
const KITTY_SEQUENCE_PREFIX = "\x1b_G";
|
||||
|
||||
function extractKittyImageIds(line: string): number[] {
|
||||
const sequenceStart = line.indexOf(KITTY_SEQUENCE_PREFIX);
|
||||
if (sequenceStart === -1) return [];
|
||||
|
||||
const paramsStart = sequenceStart + KITTY_SEQUENCE_PREFIX.length;
|
||||
const paramsEnd = line.indexOf(";", paramsStart);
|
||||
if (paramsEnd === -1) return [];
|
||||
|
||||
const params = line.slice(paramsStart, paramsEnd);
|
||||
for (const param of params.split(",")) {
|
||||
const [key, value] = param.split("=", 2);
|
||||
if (key !== "i" || value === undefined) continue;
|
||||
const id = Number(value);
|
||||
if (Number.isInteger(id) && id > 0 && id <= 0xffffffff) {
|
||||
return [id];
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Component interface - all components must implement this
|
||||
*/
|
||||
@@ -217,6 +239,7 @@ export class Container implements Component {
|
||||
export class TUI extends Container {
|
||||
public terminal: Terminal;
|
||||
private previousLines: string[] = [];
|
||||
private previousKittyImageIds = new Set<number>();
|
||||
private previousWidth = 0;
|
||||
private previousHeight = 0;
|
||||
private focusedComponent: Component | null = null;
|
||||
@@ -806,6 +829,48 @@ export class TUI extends Container {
|
||||
return lines;
|
||||
}
|
||||
|
||||
private collectKittyImageIds(lines: string[]): Set<number> {
|
||||
const ids = new Set<number>();
|
||||
for (const line of lines) {
|
||||
for (const id of extractKittyImageIds(line)) {
|
||||
ids.add(id);
|
||||
}
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
private deleteKittyImages(ids: Iterable<number>): string {
|
||||
let buffer = "";
|
||||
for (const id of ids) {
|
||||
buffer += deleteKittyImage(id);
|
||||
}
|
||||
return buffer;
|
||||
}
|
||||
|
||||
private expandLastChangedForKittyImages(firstChanged: number, lastChanged: number): number {
|
||||
let expandedLastChanged = lastChanged;
|
||||
for (let i = firstChanged; i < this.previousLines.length; i++) {
|
||||
if (extractKittyImageIds(this.previousLines[i]).length > 0) {
|
||||
expandedLastChanged = Math.max(expandedLastChanged, i);
|
||||
}
|
||||
}
|
||||
return expandedLastChanged;
|
||||
}
|
||||
|
||||
private deleteChangedKittyImages(firstChanged: number, lastChanged: number): string {
|
||||
if (firstChanged < 0 || lastChanged < firstChanged) return "";
|
||||
|
||||
const ids = new Set<number>();
|
||||
const maxLine = Math.min(lastChanged, this.previousLines.length - 1);
|
||||
for (let i = firstChanged; i <= maxLine; i++) {
|
||||
for (const id of extractKittyImageIds(this.previousLines[i] ?? "")) {
|
||||
ids.add(id);
|
||||
}
|
||||
}
|
||||
|
||||
return this.deleteKittyImages(ids);
|
||||
}
|
||||
|
||||
/** Splice overlay content into a base line at a specific column. Single-pass optimized. */
|
||||
private compositeLineAt(
|
||||
baseLine: string,
|
||||
@@ -918,7 +983,10 @@ export class TUI extends Container {
|
||||
const fullRender = (clear: boolean): void => {
|
||||
this.fullRedrawCount += 1;
|
||||
let buffer = "\x1b[?2026h"; // Begin synchronized output
|
||||
if (clear) buffer += "\x1b[2J\x1b[H\x1b[3J"; // Clear screen, home, then clear scrollback
|
||||
if (clear) {
|
||||
buffer += this.deleteKittyImages(this.previousKittyImageIds);
|
||||
buffer += "\x1b[2J\x1b[H\x1b[3J"; // Clear screen, home, then clear scrollback
|
||||
}
|
||||
for (let i = 0; i < newLines.length; i++) {
|
||||
if (i > 0) buffer += "\r\n";
|
||||
buffer += newLines[i];
|
||||
@@ -937,6 +1005,7 @@ export class TUI extends Container {
|
||||
this.previousViewportTop = Math.max(0, bufferLength - height);
|
||||
this.positionHardwareCursor(cursorPos, newLines.length);
|
||||
this.previousLines = newLines;
|
||||
this.previousKittyImageIds = this.collectKittyImageIds(newLines);
|
||||
this.previousWidth = width;
|
||||
this.previousHeight = height;
|
||||
};
|
||||
@@ -1003,6 +1072,9 @@ export class TUI extends Container {
|
||||
}
|
||||
lastChanged = newLines.length - 1;
|
||||
}
|
||||
if (firstChanged !== -1) {
|
||||
lastChanged = this.expandLastChangedForKittyImages(firstChanged, lastChanged);
|
||||
}
|
||||
const appendStart = appendedLines && firstChanged === this.previousLines.length && firstChanged > 0;
|
||||
|
||||
// No changes - but still need to update hardware cursor position if it moved
|
||||
@@ -1017,6 +1089,7 @@ export class TUI extends Container {
|
||||
if (firstChanged >= newLines.length) {
|
||||
if (this.previousLines.length > newLines.length) {
|
||||
let buffer = "\x1b[?2026h";
|
||||
buffer += this.deleteChangedKittyImages(firstChanged, lastChanged);
|
||||
// Move to end of new content (clamp to 0 for empty content)
|
||||
const targetRow = Math.max(0, newLines.length - 1);
|
||||
if (targetRow < prevViewportTop) {
|
||||
@@ -1052,6 +1125,7 @@ export class TUI extends Container {
|
||||
}
|
||||
this.positionHardwareCursor(cursorPos, newLines.length);
|
||||
this.previousLines = newLines;
|
||||
this.previousKittyImageIds = this.collectKittyImageIds(newLines);
|
||||
this.previousWidth = width;
|
||||
this.previousHeight = height;
|
||||
this.previousViewportTop = prevViewportTop;
|
||||
@@ -1069,6 +1143,7 @@ export class TUI extends Container {
|
||||
// Render from first changed line to end
|
||||
// Build buffer with all updates wrapped in synchronized output
|
||||
let buffer = "\x1b[?2026h"; // Begin synchronized output
|
||||
buffer += this.deleteChangedKittyImages(firstChanged, lastChanged);
|
||||
const prevViewportBottom = prevViewportTop + height - 1;
|
||||
const moveTargetRow = appendStart ? firstChanged - 1 : firstChanged;
|
||||
if (moveTargetRow > prevViewportBottom) {
|
||||
@@ -1199,6 +1274,7 @@ export class TUI extends Container {
|
||||
this.positionHardwareCursor(cursorPos, newLines.length);
|
||||
|
||||
this.previousLines = newLines;
|
||||
this.previousKittyImageIds = this.collectKittyImageIds(newLines);
|
||||
this.previousWidth = width;
|
||||
this.previousHeight = height;
|
||||
}
|
||||
|
||||
@@ -312,6 +312,42 @@ export function extractAnsiCode(str: string, pos: number): { code: string; lengt
|
||||
return null;
|
||||
}
|
||||
|
||||
type Osc8Terminator = "\x07" | "\x1b\\";
|
||||
|
||||
interface ActiveHyperlink {
|
||||
params: string;
|
||||
url: string;
|
||||
terminator: Osc8Terminator;
|
||||
}
|
||||
|
||||
function parseOsc8Hyperlink(ansiCode: string): ActiveHyperlink | null | undefined {
|
||||
if (!ansiCode.startsWith("\x1b]8;")) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const terminator: Osc8Terminator = ansiCode.endsWith("\x07") ? "\x07" : "\x1b\\";
|
||||
const body = ansiCode.slice(4, terminator === "\x07" ? -1 : -2);
|
||||
const separatorIndex = body.indexOf(";");
|
||||
if (separatorIndex === -1) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const params = body.slice(0, separatorIndex);
|
||||
const url = body.slice(separatorIndex + 1);
|
||||
if (!url) {
|
||||
return null;
|
||||
}
|
||||
return { params, url, terminator };
|
||||
}
|
||||
|
||||
function formatOsc8Hyperlink(hyperlink: ActiveHyperlink): string {
|
||||
return `\x1b]8;${hyperlink.params};${hyperlink.url}${hyperlink.terminator}`;
|
||||
}
|
||||
|
||||
function formatOsc8Close(terminator: Osc8Terminator): string {
|
||||
return `\x1b]8;;${terminator}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Track active ANSI SGR codes to preserve styling across line breaks.
|
||||
*/
|
||||
@@ -327,13 +363,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
|
||||
private activeHyperlink: ActiveHyperlink | null = null;
|
||||
|
||||
process(ansiCode: string): void {
|
||||
// OSC 8 hyperlink: \x1b]8;;<url>\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;
|
||||
// OSC 8 hyperlink: \x1b]8;;<url>\x1b\\ (open) or \x1b]8;;\x1b\\ (close).
|
||||
// Preserve the original terminator because some terminals only make BEL-terminated
|
||||
// links clickable. OAuth login URLs use BEL, so reopening wrapped lines with ST
|
||||
// made only the first physical line clickable in those terminals.
|
||||
const hyperlink = parseOsc8Hyperlink(ansiCode);
|
||||
if (hyperlink !== undefined) {
|
||||
this.activeHyperlink = hyperlink;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -495,7 +534,7 @@ class AnsiCodeTracker {
|
||||
|
||||
let result = codes.length > 0 ? `\x1b[${codes.join(";")}m` : "";
|
||||
if (this.activeHyperlink) {
|
||||
result += `\x1b]8;;${this.activeHyperlink}\x1b\\`;
|
||||
result += formatOsc8Hyperlink(this.activeHyperlink);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -528,7 +567,7 @@ class AnsiCodeTracker {
|
||||
result += "\x1b[24m"; // Underline off only
|
||||
}
|
||||
if (this.activeHyperlink) {
|
||||
result += "\x1b]8;;\x1b\\"; // Close hyperlink; re-opened at line start via getActiveCodes()
|
||||
result += formatOsc8Close(this.activeHyperlink.terminator); // Re-opened at line start via getActiveCodes()
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -83,6 +83,13 @@ describe("fuzzyFilter", () => {
|
||||
assert.strictEqual(result[0], "app");
|
||||
});
|
||||
|
||||
it("prioritizes exact matches over longer prefix matches", () => {
|
||||
const items = ["clone", "cl"];
|
||||
const result = fuzzyFilter(items, "cl", (x: string) => x);
|
||||
|
||||
assert.deepStrictEqual(result, ["cl", "clone"]);
|
||||
});
|
||||
|
||||
it("works with custom getText function", () => {
|
||||
const items = [
|
||||
{ name: "foo", id: 1 },
|
||||
|
||||
@@ -4,7 +4,19 @@
|
||||
|
||||
import assert from "node:assert";
|
||||
import { describe, it } from "node:test";
|
||||
import { detectCapabilities, hyperlink, isImageLine } from "../src/terminal-image.js";
|
||||
import { Image } from "../src/components/image.js";
|
||||
import {
|
||||
deleteAllKittyImages,
|
||||
deleteKittyImage,
|
||||
detectCapabilities,
|
||||
encodeKitty,
|
||||
hyperlink,
|
||||
isImageLine,
|
||||
renderImage,
|
||||
resetCapabilitiesCache,
|
||||
setCapabilities,
|
||||
setCellDimensions,
|
||||
} from "../src/terminal-image.js";
|
||||
|
||||
const ENV_KEYS = [
|
||||
"TERM",
|
||||
@@ -15,6 +27,7 @@ const ENV_KEYS = [
|
||||
"GHOSTTY_RESOURCES_DIR",
|
||||
"WEZTERM_PANE",
|
||||
"ITERM_SESSION_ID",
|
||||
"CMUX_WORKSPACE_ID",
|
||||
] as const;
|
||||
|
||||
function withEnv(overrides: Record<string, string | undefined>, fn: () => void): void {
|
||||
@@ -223,6 +236,14 @@ describe("detectCapabilities", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("does not disable Ghostty images solely because cmux is present", () => {
|
||||
withEnv({ TERM_PROGRAM: "ghostty", CMUX_WORKSPACE_ID: "workspace" }, () => {
|
||||
const caps = detectCapabilities();
|
||||
assert.strictEqual(caps.images, "kitty");
|
||||
assert.strictEqual(caps.hyperlinks, true);
|
||||
});
|
||||
});
|
||||
|
||||
it("enables hyperlinks for Kitty", () => {
|
||||
withEnv({ KITTY_WINDOW_ID: "1" }, () => {
|
||||
const caps = detectCapabilities();
|
||||
@@ -252,6 +273,71 @@ describe("detectCapabilities", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("Kitty image cursor movement", () => {
|
||||
it("can request no terminal-side cursor movement", () => {
|
||||
const sequence = encodeKitty("AAAA", { columns: 2, rows: 2, moveCursor: false });
|
||||
assert.ok(sequence.startsWith("\x1b_Ga=T,f=100,q=2,C=1,c=2,r=2;"));
|
||||
});
|
||||
|
||||
it("suppresses Kitty replies for delete commands", () => {
|
||||
assert.strictEqual(deleteKittyImage(42), "\x1b_Ga=d,d=I,i=42,q=2\x1b\\");
|
||||
assert.strictEqual(deleteAllKittyImages(), "\x1b_Ga=d,d=A,q=2\x1b\\");
|
||||
});
|
||||
|
||||
it("preserves renderImage's default terminal-side cursor movement", () => {
|
||||
setCapabilities({ images: "kitty", trueColor: true, hyperlinks: true });
|
||||
setCellDimensions({ widthPx: 10, heightPx: 10 });
|
||||
try {
|
||||
const result = renderImage("AAAA", { widthPx: 20, heightPx: 20 }, { maxWidthCells: 2 });
|
||||
assert.ok(result);
|
||||
assert.ok(!result.sequence.includes(",C=1,"));
|
||||
assert.strictEqual(result.rows, 2);
|
||||
} finally {
|
||||
resetCapabilitiesCache();
|
||||
setCellDimensions({ widthPx: 9, heightPx: 18 });
|
||||
}
|
||||
});
|
||||
|
||||
it("can opt renderImage into no terminal-side cursor movement", () => {
|
||||
setCapabilities({ images: "kitty", trueColor: true, hyperlinks: true });
|
||||
setCellDimensions({ widthPx: 10, heightPx: 10 });
|
||||
try {
|
||||
const result = renderImage("AAAA", { widthPx: 20, heightPx: 20 }, { maxWidthCells: 2, moveCursor: false });
|
||||
assert.ok(result);
|
||||
assert.ok(result.sequence.includes(",C=1,"));
|
||||
assert.strictEqual(result.rows, 2);
|
||||
} finally {
|
||||
resetCapabilitiesCache();
|
||||
setCellDimensions({ widthPx: 9, heightPx: 18 });
|
||||
}
|
||||
});
|
||||
|
||||
it("restores the cursor to the reserved image row after Kitty rendering", () => {
|
||||
setCapabilities({ images: "kitty", trueColor: true, hyperlinks: true });
|
||||
setCellDimensions({ widthPx: 10, heightPx: 10 });
|
||||
try {
|
||||
const image = new Image(
|
||||
"AAAA",
|
||||
"image/png",
|
||||
{ fallbackColor: (value) => value },
|
||||
{ maxWidthCells: 2 },
|
||||
{ widthPx: 20, heightPx: 20 },
|
||||
);
|
||||
const lines = image.render(4);
|
||||
const imageId = image.getImageId();
|
||||
assert.strictEqual(typeof imageId, "number");
|
||||
assert.deepStrictEqual(lines.slice(0, -1), [""]);
|
||||
assert.ok(lines[1].startsWith("\x1b[1A\x1b_G"));
|
||||
assert.ok(lines[1].includes(",C=1,"));
|
||||
assert.ok(lines[1].includes(`,i=${imageId}`));
|
||||
assert.ok(lines[1].endsWith("\x1b[1B"));
|
||||
} finally {
|
||||
resetCapabilitiesCache();
|
||||
setCellDimensions({ widthPx: 9, heightPx: 18 });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("hyperlink", () => {
|
||||
it("wraps text in OSC 8 open and close sequences", () => {
|
||||
const result = hyperlink("click me", "https://example.com");
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import assert from "node:assert";
|
||||
import { describe, it } from "node:test";
|
||||
import type { Terminal as XtermTerminalType } from "@xterm/headless";
|
||||
import { deleteKittyImage, encodeKitty } from "../src/terminal-image.js";
|
||||
import { type Component, TUI } from "../src/tui.js";
|
||||
import { VirtualTerminal } from "./virtual-terminal.js";
|
||||
|
||||
@@ -63,6 +64,87 @@ function getCellItalic(terminal: VirtualTerminal, row: number, col: number): num
|
||||
return cell.isItalic();
|
||||
}
|
||||
|
||||
describe("TUI Kitty image cleanup", () => {
|
||||
it("deletes changed image ids before drawing moved placements", async () => {
|
||||
const terminal = new LoggingVirtualTerminal(40, 10);
|
||||
const tui = new TUI(terminal);
|
||||
const component = new TestComponent();
|
||||
tui.addChild(component);
|
||||
|
||||
const oldImage = encodeKitty("AAAA", { columns: 2, rows: 2, imageId: 42, moveCursor: false });
|
||||
component.lines = ["top", oldImage];
|
||||
tui.start();
|
||||
await terminal.waitForRender();
|
||||
terminal.clearWrites();
|
||||
|
||||
const newImage = encodeKitty("BBBB", { columns: 2, rows: 1, imageId: 42, moveCursor: false });
|
||||
component.lines = [newImage, ""];
|
||||
tui.requestRender();
|
||||
await terminal.waitForRender();
|
||||
|
||||
const writes = terminal.getWrites();
|
||||
const deleteIndex = writes.indexOf(deleteKittyImage(42));
|
||||
const drawIndex = writes.indexOf(newImage);
|
||||
assert.ok(deleteIndex >= 0, "changed old image should be deleted");
|
||||
assert.ok(drawIndex >= 0, "new image should be drawn");
|
||||
assert.ok(deleteIndex < drawIndex, "old image must be deleted before the new placement is drawn");
|
||||
|
||||
tui.stop();
|
||||
});
|
||||
|
||||
it("redraws image lines when an earlier reserved image row changes", async () => {
|
||||
const terminal = new LoggingVirtualTerminal(40, 10);
|
||||
const tui = new TUI(terminal);
|
||||
const component = new TestComponent();
|
||||
tui.addChild(component);
|
||||
|
||||
const image = encodeKitty("AAAA", { columns: 2, rows: 2, imageId: 88, moveCursor: false });
|
||||
component.lines = ["", image];
|
||||
tui.start();
|
||||
await terminal.waitForRender();
|
||||
terminal.clearWrites();
|
||||
|
||||
component.lines = ["covered", image];
|
||||
tui.requestRender();
|
||||
await terminal.waitForRender();
|
||||
|
||||
const writes = terminal.getWrites();
|
||||
const deleteIndex = writes.indexOf(deleteKittyImage(88));
|
||||
const drawIndex = writes.indexOf(image);
|
||||
assert.ok(deleteIndex >= 0, "image should be deleted when a reserved row changes");
|
||||
assert.ok(drawIndex >= 0, "unchanged image line should be redrawn after deleting the placement");
|
||||
assert.ok(deleteIndex < drawIndex, "old placement must be deleted before the image line is redrawn");
|
||||
assert.ok(!writes.includes("\x1b[2J"), "reserved row changes should not force a full redraw");
|
||||
|
||||
tui.stop();
|
||||
});
|
||||
|
||||
it("deletes previously rendered image ids during full redraws", async () => {
|
||||
const terminal = new LoggingVirtualTerminal(40, 10);
|
||||
const tui = new TUI(terminal);
|
||||
const component = new TestComponent();
|
||||
tui.addChild(component);
|
||||
|
||||
component.lines = [encodeKitty("AAAA", { columns: 2, rows: 2, imageId: 77, moveCursor: false })];
|
||||
tui.start();
|
||||
await terminal.waitForRender();
|
||||
terminal.clearWrites();
|
||||
|
||||
component.lines = ["plain text"];
|
||||
tui.requestRender(true);
|
||||
await terminal.waitForRender();
|
||||
|
||||
const writes = terminal.getWrites();
|
||||
const deleteIndex = writes.indexOf(deleteKittyImage(77));
|
||||
const clearIndex = writes.indexOf("\x1b[2J");
|
||||
assert.ok(deleteIndex >= 0, "previous image should be deleted during full redraw");
|
||||
assert.ok(clearIndex >= 0, "full redraw should clear the screen");
|
||||
assert.ok(deleteIndex < clearIndex, "old image should be deleted before the screen is cleared");
|
||||
|
||||
tui.stop();
|
||||
});
|
||||
});
|
||||
|
||||
describe("TUI resize handling", () => {
|
||||
it("triggers full re-render when terminal height changes", async () => {
|
||||
await withEnv({ TERMUX_VERSION: undefined }, async () => {
|
||||
|
||||
@@ -191,6 +191,21 @@ describe("wrapTextWithAnsi with OSC 8 hyperlinks", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("preserves BEL terminators when wrapping OAuth-style hyperlinks", () => {
|
||||
const url = `https://example.com/oauth/${"a".repeat(32)}`;
|
||||
const input = `\x1b]8;;${url}\x07${url}\x1b]8;;\x07`;
|
||||
const lines = wrapTextWithAnsi(input, 20);
|
||||
|
||||
assert.ok(lines.length > 1);
|
||||
for (const line of lines) {
|
||||
assert.ok(line.includes(`\x1b]8;;${url}\x07`), `Line "${line}" does not reopen the hyperlink with BEL`);
|
||||
assert.ok(!line.includes(`\x1b]8;;${url}\x1b\\`), `Line "${line}" reopens the hyperlink with ST`);
|
||||
}
|
||||
for (const line of lines.slice(0, -1)) {
|
||||
assert.ok(line.endsWith("\x1b]8;;\x07"), `Line "${line}" does not close the hyperlink with BEL`);
|
||||
}
|
||||
});
|
||||
|
||||
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`;
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.73.1] - 2026-05-07
|
||||
|
||||
## [0.73.0] - 2026-05-04
|
||||
|
||||
## [0.72.1] - 2026-05-02
|
||||
|
||||
## [0.72.0] - 2026-05-01
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "pi-web-ui-example",
|
||||
"version": "0.72.1",
|
||||
"version": "0.73.1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@mariozechner/pi-web-ui",
|
||||
"version": "0.72.1",
|
||||
"version": "0.73.1",
|
||||
"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.72.1",
|
||||
"@mariozechner/pi-tui": "^0.72.1",
|
||||
"@mariozechner/pi-ai": "^0.73.1",
|
||||
"@mariozechner/pi-tui": "^0.73.1",
|
||||
"typebox": "^1.1.24",
|
||||
"docx-preview": "^0.3.7",
|
||||
"jszip": "^3.10.1",
|
||||
|
||||
232
scripts/tool-stats.ts
Executable file
232
scripts/tool-stats.ts
Executable file
@@ -0,0 +1,232 @@
|
||||
#!/usr/bin/env npx tsx
|
||||
|
||||
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import { homedir, tmpdir } from "node:os";
|
||||
import { join, resolve } from "node:path";
|
||||
import { spawn } from "node:child_process";
|
||||
|
||||
interface TextContent { type: "text"; text: string }
|
||||
interface ImageContent { type: "image"; data: string; mimeType?: string }
|
||||
interface ToolCallContent { type: "toolCall"; id: string; name: string; arguments?: Record<string, unknown> }
|
||||
type Content = TextContent | ImageContent | ToolCallContent | { type: string; [key: string]: unknown };
|
||||
interface Message { role?: string; content?: string | Content[]; toolCallId?: string; toolName?: string; details?: unknown }
|
||||
interface Entry { type?: string; message?: Message }
|
||||
interface ToolStats { calls: number; results: number; estimatedTokens: number; samples: number[]; errors: number }
|
||||
interface BashCommandStats { calls: number; estimatedTokens: number; samples: number[] }
|
||||
interface ToolCallInfo { toolName: string; bashCommand?: string }
|
||||
|
||||
const BUCKETS = [0, 50, 100, 250, 500, 1000, 2000, 4000, 8000, 16000, 32000, Number.POSITIVE_INFINITY];
|
||||
|
||||
function parseArgs(): { sessionsDir: string; output: string } {
|
||||
let sessionsDir = join(homedir(), ".pi", "agent", "sessions");
|
||||
let output = join(tmpdir(), "pi-tool-stats.html");
|
||||
const args = process.argv.slice(2);
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
const arg = args[i];
|
||||
if (arg === "--sessions-dir" && args[i + 1]) sessionsDir = resolve(args[++i]);
|
||||
else if ((arg === "--output" || arg === "-o") && args[i + 1]) output = resolve(args[++i]);
|
||||
else if (arg === "--help" || arg === "-h") {
|
||||
console.log(`Usage: scripts/tool-stats.ts [--sessions-dir <dir>] [--output <file.html>]`);
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
return { sessionsDir, output };
|
||||
}
|
||||
|
||||
function jsonlFiles(dir: string): string[] {
|
||||
const out: string[] = [];
|
||||
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
||||
const path = join(dir, entry.name);
|
||||
if (entry.isDirectory()) out.push(...jsonlFiles(path));
|
||||
else if (entry.isFile() && entry.name.endsWith(".jsonl")) out.push(path);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function getStats<T>(map: Map<string, T>, key: string, create: () => T): T {
|
||||
let stats = map.get(key);
|
||||
if (!stats) {
|
||||
stats = create();
|
||||
map.set(key, stats);
|
||||
}
|
||||
return stats;
|
||||
}
|
||||
|
||||
function createToolStats(): ToolStats {
|
||||
return { calls: 0, results: 0, estimatedTokens: 0, samples: [], errors: 0 };
|
||||
}
|
||||
|
||||
function createBashStats(): BashCommandStats {
|
||||
return { calls: 0, estimatedTokens: 0, samples: [] };
|
||||
}
|
||||
|
||||
function estimateTokenCount(text: string): number {
|
||||
return Math.ceil(text.length / 4);
|
||||
}
|
||||
|
||||
function contentText(content: Message["content"]): string {
|
||||
if (typeof content === "string") return content;
|
||||
if (!Array.isArray(content)) return "";
|
||||
return content.map((block) => {
|
||||
if (block.type === "text" && "text" in block && typeof block.text === "string") return block.text;
|
||||
if (block.type === "image" && "data" in block && typeof block.data === "string") return block.data;
|
||||
return JSON.stringify(block);
|
||||
}).join("\n");
|
||||
}
|
||||
|
||||
function getBashCommand(args: Record<string, unknown> | undefined): string | undefined {
|
||||
const command = args?.command;
|
||||
return typeof command === "string" ? command : undefined;
|
||||
}
|
||||
|
||||
function commandKey(command: string): string {
|
||||
const first = command.split(/\n|&&|\|\||;|\|/)[0]?.trim() ?? command.trim();
|
||||
const match = first.match(/^(?:\w+=\S+\s+)*(?:sudo\s+)?([^\s]+)(?:\s+([^\s]+))?/);
|
||||
if (!match) return "unknown";
|
||||
const bin = match[1] ?? "unknown";
|
||||
const sub = match[2] && !match[2].startsWith("-") ? ` ${match[2]}` : "";
|
||||
return `${bin}${sub}`;
|
||||
}
|
||||
|
||||
function bucketCounts(samples: number[]): number[] {
|
||||
const counts = new Array(BUCKETS.length - 1).fill(0) as number[];
|
||||
for (const sample of samples) {
|
||||
const index = BUCKETS.findIndex((max, i) => i > 0 && sample <= max) - 1;
|
||||
counts[Math.max(0, index)]++;
|
||||
}
|
||||
return counts;
|
||||
}
|
||||
|
||||
function bucketLabels(): string[] {
|
||||
return BUCKETS.slice(0, -1).map((min, i) => {
|
||||
const max = BUCKETS[i + 1];
|
||||
return Number.isFinite(max) ? `${min}-${max}` : `${min}+`;
|
||||
});
|
||||
}
|
||||
|
||||
const { sessionsDir, output } = parseArgs();
|
||||
if (!existsSync(sessionsDir)) throw new Error(`Sessions directory not found: ${sessionsDir}`);
|
||||
|
||||
const tools = new Map<string, ToolStats>();
|
||||
const bashCommands = new Map<string, BashCommandStats>();
|
||||
const callsById = new Map<string, ToolCallInfo>();
|
||||
let parseErrors = 0;
|
||||
const files = jsonlFiles(sessionsDir);
|
||||
|
||||
for (const file of files) {
|
||||
for (const line of readFileSync(file, "utf8").split("\n")) {
|
||||
if (!line.trim()) continue;
|
||||
let entry: Entry;
|
||||
try { entry = JSON.parse(line) as Entry; } catch { parseErrors++; continue; }
|
||||
if (entry.type !== "message") continue;
|
||||
const message = entry.message;
|
||||
if (!message) continue;
|
||||
if (message.role === "assistant" && Array.isArray(message.content)) {
|
||||
for (const block of message.content) {
|
||||
if (block.type !== "toolCall" || !("name" in block) || typeof block.name !== "string") continue;
|
||||
const stats = getStats(tools, block.name, createToolStats);
|
||||
stats.calls++;
|
||||
const bashCommand = block.name === "bash" ? getBashCommand(block.arguments) : undefined;
|
||||
callsById.set(block.id, { toolName: block.name, bashCommand });
|
||||
if (bashCommand) getStats(bashCommands, commandKey(bashCommand), createBashStats).calls++;
|
||||
}
|
||||
} else if (message.role === "toolResult" && message.toolName) {
|
||||
const text = contentText(message.content);
|
||||
const tokens = estimateTokenCount(text);
|
||||
const stats = getStats(tools, message.toolName, createToolStats);
|
||||
stats.results++;
|
||||
stats.estimatedTokens += tokens;
|
||||
stats.samples.push(tokens);
|
||||
if ("isError" in message && message.isError === true) stats.errors++;
|
||||
const call = message.toolCallId ? callsById.get(message.toolCallId) : undefined;
|
||||
if (call?.bashCommand) {
|
||||
const bash = getStats(bashCommands, commandKey(call.bashCommand), createBashStats);
|
||||
bash.estimatedTokens += tokens;
|
||||
bash.samples.push(tokens);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const toolRows = [...tools.entries()].map(([name, s]) => ({ name, ...s, avg: s.results ? s.estimatedTokens / s.results : 0, histogram: bucketCounts(s.samples) })).sort((a, b) => b.estimatedTokens - a.estimatedTokens);
|
||||
const bashRows = [...bashCommands.entries()].map(([name, s]) => ({ name, ...s, avg: s.samples.length ? s.estimatedTokens / s.samples.length : 0, histogram: bucketCounts(s.samples) })).sort((a, b) => b.estimatedTokens - a.estimatedTokens).slice(0, 50);
|
||||
const data = { generatedAt: new Date().toISOString(), sessionsDir, files: files.length, parseErrors, bucketLabels: bucketLabels(), tools: toolRows, bashCommands: bashRows };
|
||||
|
||||
const html = `<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Pi Tool Stats</title>
|
||||
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.9/dist/chart.umd.min.js"></script>
|
||||
</head>
|
||||
<body class="bg-zinc-950 text-zinc-100 p-6">
|
||||
<main class="max-w-7xl mx-auto space-y-6">
|
||||
<h1 class="text-3xl font-bold">Pi Tool Stats</h1>
|
||||
<p class="text-zinc-400">${data.files} session files from <code>${sessionsDir}</code>. Generated ${data.generatedAt}.</p>
|
||||
<section class="grid md:grid-cols-2 gap-6">
|
||||
<div class="bg-zinc-900 rounded p-4"><h2 class="font-semibold mb-3">Estimated result tokens by tool</h2><canvas id="tokens"></canvas></div>
|
||||
<div class="bg-zinc-900 rounded p-4"><h2 class="font-semibold mb-3">Tool calls</h2><canvas id="calls"></canvas></div>
|
||||
</section>
|
||||
<section class="grid md:grid-cols-2 gap-6">
|
||||
<div class="bg-zinc-900 rounded p-4">
|
||||
<div class="flex items-center justify-between gap-4 mb-3">
|
||||
<h2 class="font-semibold">Tool result token histogram</h2>
|
||||
<select id="toolSelect" class="bg-zinc-800 rounded px-2 py-1 text-sm"></select>
|
||||
</div>
|
||||
<p id="toolSummary" class="text-sm text-zinc-400 mb-3"></p>
|
||||
<canvas id="toolHistogram" height="120"></canvas>
|
||||
</div>
|
||||
<div class="bg-zinc-900 rounded p-4">
|
||||
<div class="flex items-center justify-between gap-4 mb-3">
|
||||
<h2 class="font-semibold">Bash result token histogram</h2>
|
||||
<select id="bashSelect" class="bg-zinc-800 rounded px-2 py-1 text-sm"></select>
|
||||
</div>
|
||||
<p id="bashSummary" class="text-sm text-zinc-400 mb-3"></p>
|
||||
<canvas id="bashHistogram" height="120"></canvas>
|
||||
</div>
|
||||
</section>
|
||||
<section class="bg-zinc-900 rounded p-4"><h2 class="font-semibold mb-3">Tools</h2><div id="tools"></div></section>
|
||||
<section class="bg-zinc-900 rounded p-4">
|
||||
<h2 class="font-semibold mb-3">Bash common commands (best effort)</h2>
|
||||
<div id="bash" class="mt-4"></div>
|
||||
</section>
|
||||
</main>
|
||||
<script>
|
||||
const data=${JSON.stringify(data)};
|
||||
function fmt(n){return Math.round(n).toLocaleString()}
|
||||
function esc(s){return String(s).replace(/[&<>\"]/g,c=>({'&':'&','<':'<','>':'>','"':'"'}[c]))}
|
||||
function table(rows,el){
|
||||
document.getElementById(el).innerHTML='<table class="w-full text-sm"><thead class="text-zinc-400"><tr><th class="text-left p-2">Name</th><th class="text-right p-2">Calls</th><th class="text-right p-2">Results</th><th class="text-right p-2">Est. tokens</th><th class="text-right p-2">Avg/result</th><th class="text-left p-2 w-64">Histogram</th></tr></thead><tbody>'+rows.map((r,i)=>'<tr class="border-t border-zinc-800 hover:bg-zinc-800/50 cursor-pointer" data-row="'+i+'"><td class="p-2 font-mono">'+esc(r.name)+'</td><td class="p-2 text-right">'+fmt(r.calls)+'</td><td class="p-2 text-right">'+fmt(r.results??r.samples.length)+'</td><td class="p-2 text-right">'+fmt(r.estimatedTokens)+'</td><td class="p-2 text-right">'+fmt(r.avg)+'</td><td class="p-2"><canvas id="'+el+'Hist'+i+'" height="34"></canvas></td></tr>').join('')+'</tbody></table>';
|
||||
rows.forEach((r,i)=>new Chart(document.getElementById(el+'Hist'+i),{type:'bar',data:{labels:data.bucketLabels,datasets:[{data:r.histogram,label:r.name,backgroundColor:'#60a5fa'}]},options:{responsive:true,maintainAspectRatio:false,plugins:{legend:{display:false},tooltip:{callbacks:{title:(items)=>data.bucketLabels[items[0].dataIndex]+' tokens'}}},scales:{x:{display:false},y:{display:false}}}}));
|
||||
document.querySelectorAll('#'+el+' tr[data-row]').forEach(row=>row.addEventListener('click',()=>document.getElementById(el==='tools'?'toolSelect':'bashSelect').value=row.dataset.row,document.getElementById(el==='tools'?'toolSelect':'bashSelect').dispatchEvent(new Event('change'))));
|
||||
}
|
||||
function fillSelect(id,rows){document.getElementById(id).innerHTML=rows.map((r,i)=>'<option value="'+i+'">'+esc(r.name)+'</option>').join('')}
|
||||
function summary(r){return fmt(r.calls)+' calls, '+fmt(r.results??r.samples.length)+' results, '+fmt(r.estimatedTokens)+' estimated result tokens, '+fmt(r.avg)+' avg/result'}
|
||||
function singleHistogram(canvasId,summaryId,selectId,rows){
|
||||
let chart;
|
||||
function update(){
|
||||
const row=rows[Number(document.getElementById(selectId).value)]??rows[0];
|
||||
document.getElementById(summaryId).textContent=row?summary(row):'No data';
|
||||
if(chart)chart.destroy();
|
||||
chart=new Chart(document.getElementById(canvasId),{type:'bar',data:{labels:data.bucketLabels,datasets:[{label:row?.name??'',data:row?.histogram??[],backgroundColor:'#60a5fa'}]},options:{plugins:{legend:{display:false},tooltip:{callbacks:{title:(items)=>data.bucketLabels[items[0].dataIndex]+' tokens',label:(item)=>fmt(item.raw)+' results'}}},scales:{y:{beginAtZero:true,title:{display:true,text:'result count'}},x:{title:{display:true,text:'estimated tokens/result'}}}}});
|
||||
}
|
||||
document.getElementById(selectId).addEventListener('change',update);
|
||||
update();
|
||||
}
|
||||
table(data.tools,'tools');
|
||||
table(data.bashCommands,'bash');
|
||||
fillSelect('toolSelect',data.tools);
|
||||
fillSelect('bashSelect',data.bashCommands);
|
||||
singleHistogram('toolHistogram','toolSummary','toolSelect',data.tools);
|
||||
singleHistogram('bashHistogram','bashSummary','bashSelect',data.bashCommands);
|
||||
new Chart(document.getElementById('tokens'),{type:'bar',data:{labels:data.tools.map(r=>r.name),datasets:[{label:'estimated tokens',data:data.tools.map(r=>r.estimatedTokens)}]},options:{plugins:{legend:{display:false}}}});
|
||||
new Chart(document.getElementById('calls'),{type:'bar',data:{labels:data.tools.map(r=>r.name),datasets:[{label:'calls',data:data.tools.map(r=>r.calls)}]},options:{plugins:{legend:{display:false}}}});
|
||||
</script>
|
||||
</body>
|
||||
</html>`;
|
||||
|
||||
mkdirSync(resolve(output, ".."), { recursive: true });
|
||||
writeFileSync(output, html);
|
||||
console.log(`Wrote ${output}`);
|
||||
spawn(process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open", process.platform === "win32" ? ["/c", "start", output] : [output], { detached: true, stdio: "ignore" }).unref();
|
||||
Reference in New Issue
Block a user