Merge remote-tracking branch 'upstream/main' into pr-898
# Conflicts: # packages/ai/CHANGELOG.md
This commit is contained in:
@@ -65,3 +65,4 @@ scripts/session-stats/edit-analysis.csv
|
||||
|
||||
# parallel-agent worktrees
|
||||
.wt/
|
||||
CPU*.md
|
||||
|
||||
Generated
+1
-1
@@ -2162,7 +2162,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pi-natives"
|
||||
version = "14.5.12"
|
||||
version = "14.5.13"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arboard",
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ exclude = ["crates/brush-core-vendored", "crates/brush-builtins-vendored"]
|
||||
resolver = "3"
|
||||
|
||||
[workspace.package]
|
||||
version = "14.5.12"
|
||||
version = "14.5.13"
|
||||
edition = "2024"
|
||||
license = "MIT"
|
||||
authors = ["Can Boluk"]
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
},
|
||||
"packages/agent": {
|
||||
"name": "@oh-my-pi/pi-agent-core",
|
||||
"version": "14.5.12",
|
||||
"version": "14.5.13",
|
||||
"dependencies": {
|
||||
"@oh-my-pi/pi-ai": "catalog:",
|
||||
"@oh-my-pi/pi-natives": "catalog:",
|
||||
@@ -29,7 +29,7 @@
|
||||
},
|
||||
"packages/ai": {
|
||||
"name": "@oh-my-pi/pi-ai",
|
||||
"version": "14.5.12",
|
||||
"version": "14.5.13",
|
||||
"bin": {
|
||||
"pi-ai": "./src/cli.ts",
|
||||
},
|
||||
@@ -56,7 +56,7 @@
|
||||
},
|
||||
"packages/coding-agent": {
|
||||
"name": "@oh-my-pi/pi-coding-agent",
|
||||
"version": "14.5.12",
|
||||
"version": "14.5.13",
|
||||
"bin": {
|
||||
"omp": "src/cli.ts",
|
||||
},
|
||||
@@ -92,7 +92,7 @@
|
||||
},
|
||||
"packages/natives": {
|
||||
"name": "@oh-my-pi/pi-natives",
|
||||
"version": "14.5.12",
|
||||
"version": "14.5.13",
|
||||
"devDependencies": {
|
||||
"@napi-rs/cli": "catalog:",
|
||||
"@types/bun": "catalog:",
|
||||
@@ -100,7 +100,7 @@
|
||||
},
|
||||
"packages/stats": {
|
||||
"name": "@oh-my-pi/omp-stats",
|
||||
"version": "14.5.12",
|
||||
"version": "14.5.13",
|
||||
"bin": {
|
||||
"omp-stats": "./src/index.ts",
|
||||
},
|
||||
@@ -125,7 +125,7 @@
|
||||
},
|
||||
"packages/swarm-extension": {
|
||||
"name": "@oh-my-pi/swarm-extension",
|
||||
"version": "14.5.12",
|
||||
"version": "14.5.13",
|
||||
"bin": {
|
||||
"omp-swarm": "src/cli.ts",
|
||||
},
|
||||
@@ -141,7 +141,7 @@
|
||||
},
|
||||
"packages/tui": {
|
||||
"name": "@oh-my-pi/pi-tui",
|
||||
"version": "14.5.12",
|
||||
"version": "14.5.13",
|
||||
"dependencies": {
|
||||
"@oh-my-pi/pi-natives": "catalog:",
|
||||
"@oh-my-pi/pi-utils": "catalog:",
|
||||
@@ -181,7 +181,7 @@
|
||||
},
|
||||
"packages/utils": {
|
||||
"name": "@oh-my-pi/pi-utils",
|
||||
"version": "14.5.12",
|
||||
"version": "14.5.13",
|
||||
"dependencies": {
|
||||
"beautiful-mermaid": "catalog:",
|
||||
"handlebars": "catalog:",
|
||||
@@ -212,13 +212,13 @@
|
||||
"@google/genai": "^1.50.1",
|
||||
"@mozilla/readability": "^0.6.0",
|
||||
"@napi-rs/cli": "3.6.2",
|
||||
"@oh-my-pi/omp-stats": "14.5.12",
|
||||
"@oh-my-pi/pi-agent-core": "14.5.12",
|
||||
"@oh-my-pi/pi-ai": "14.5.12",
|
||||
"@oh-my-pi/pi-coding-agent": "14.5.12",
|
||||
"@oh-my-pi/pi-natives": "14.5.12",
|
||||
"@oh-my-pi/pi-tui": "14.5.12",
|
||||
"@oh-my-pi/pi-utils": "14.5.12",
|
||||
"@oh-my-pi/omp-stats": "14.5.13",
|
||||
"@oh-my-pi/pi-agent-core": "14.5.13",
|
||||
"@oh-my-pi/pi-ai": "14.5.13",
|
||||
"@oh-my-pi/pi-coding-agent": "14.5.13",
|
||||
"@oh-my-pi/pi-natives": "14.5.13",
|
||||
"@oh-my-pi/pi-tui": "14.5.13",
|
||||
"@oh-my-pi/pi-utils": "14.5.13",
|
||||
"@puppeteer/browsers": "^2.13.0",
|
||||
"@sinclair/typebox": "^0.34.49",
|
||||
"@smithy/node-http-handler": "^4.6.1",
|
||||
@@ -279,7 +279,7 @@
|
||||
|
||||
"@aws-crypto/util": ["@aws-crypto/util@5.2.0", "", { "dependencies": { "@aws-sdk/types": "^3.222.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ=="],
|
||||
|
||||
"@aws-sdk/client-bedrock-runtime": ["@aws-sdk/client-bedrock-runtime@3.1039.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.974.7", "@aws-sdk/credential-provider-node": "^3.972.38", "@aws-sdk/eventstream-handler-node": "^3.972.14", "@aws-sdk/middleware-eventstream": "^3.972.10", "@aws-sdk/middleware-host-header": "^3.972.10", "@aws-sdk/middleware-logger": "^3.972.10", "@aws-sdk/middleware-recursion-detection": "^3.972.11", "@aws-sdk/middleware-user-agent": "^3.972.37", "@aws-sdk/middleware-websocket": "^3.972.16", "@aws-sdk/region-config-resolver": "^3.972.13", "@aws-sdk/token-providers": "3.1039.0", "@aws-sdk/types": "^3.973.8", "@aws-sdk/util-endpoints": "^3.996.8", "@aws-sdk/util-user-agent-browser": "^3.972.10", "@aws-sdk/util-user-agent-node": "^3.973.23", "@smithy/config-resolver": "^4.4.17", "@smithy/core": "^3.23.17", "@smithy/eventstream-serde-browser": "^4.2.14", "@smithy/eventstream-serde-config-resolver": "^4.3.14", "@smithy/eventstream-serde-node": "^4.2.14", "@smithy/fetch-http-handler": "^5.3.17", "@smithy/hash-node": "^4.2.14", "@smithy/invalid-dependency": "^4.2.14", "@smithy/middleware-content-length": "^4.2.14", "@smithy/middleware-endpoint": "^4.4.32", "@smithy/middleware-retry": "^4.5.7", "@smithy/middleware-serde": "^4.2.20", "@smithy/middleware-stack": "^4.2.14", "@smithy/node-config-provider": "^4.3.14", "@smithy/node-http-handler": "^4.6.1", "@smithy/protocol-http": "^5.3.14", "@smithy/smithy-client": "^4.12.13", "@smithy/types": "^4.14.1", "@smithy/url-parser": "^4.2.14", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", "@smithy/util-defaults-mode-browser": "^4.3.49", "@smithy/util-defaults-mode-node": "^4.2.54", "@smithy/util-endpoints": "^3.4.2", "@smithy/util-middleware": "^4.2.14", "@smithy/util-retry": "^4.3.6", "@smithy/util-stream": "^4.5.25", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-rpm9rGcv95ulprNIu/ruhreG4bSKq7oFrErM1Nkp9Cq/zzo/11Hw1/ffYKLM/PAcMGZ+5/zAHOCWBDQ3W1lIBw=="],
|
||||
"@aws-sdk/client-bedrock-runtime": ["@aws-sdk/client-bedrock-runtime@3.1040.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.974.7", "@aws-sdk/credential-provider-node": "^3.972.38", "@aws-sdk/eventstream-handler-node": "^3.972.14", "@aws-sdk/middleware-eventstream": "^3.972.10", "@aws-sdk/middleware-host-header": "^3.972.10", "@aws-sdk/middleware-logger": "^3.972.10", "@aws-sdk/middleware-recursion-detection": "^3.972.11", "@aws-sdk/middleware-user-agent": "^3.972.37", "@aws-sdk/middleware-websocket": "^3.972.16", "@aws-sdk/region-config-resolver": "^3.972.13", "@aws-sdk/token-providers": "3.1040.0", "@aws-sdk/types": "^3.973.8", "@aws-sdk/util-endpoints": "^3.996.8", "@aws-sdk/util-user-agent-browser": "^3.972.10", "@aws-sdk/util-user-agent-node": "^3.973.23", "@smithy/config-resolver": "^4.4.17", "@smithy/core": "^3.23.17", "@smithy/eventstream-serde-browser": "^4.2.14", "@smithy/eventstream-serde-config-resolver": "^4.3.14", "@smithy/eventstream-serde-node": "^4.2.14", "@smithy/fetch-http-handler": "^5.3.17", "@smithy/hash-node": "^4.2.14", "@smithy/invalid-dependency": "^4.2.14", "@smithy/middleware-content-length": "^4.2.14", "@smithy/middleware-endpoint": "^4.4.32", "@smithy/middleware-retry": "^4.5.7", "@smithy/middleware-serde": "^4.2.20", "@smithy/middleware-stack": "^4.2.14", "@smithy/node-config-provider": "^4.3.14", "@smithy/node-http-handler": "^4.6.1", "@smithy/protocol-http": "^5.3.14", "@smithy/smithy-client": "^4.12.13", "@smithy/types": "^4.14.1", "@smithy/url-parser": "^4.2.14", "@smithy/util-base64": "^4.3.2", "@smithy/util-body-length-browser": "^4.2.2", "@smithy/util-body-length-node": "^4.2.3", "@smithy/util-defaults-mode-browser": "^4.3.49", "@smithy/util-defaults-mode-node": "^4.2.54", "@smithy/util-endpoints": "^3.4.2", "@smithy/util-middleware": "^4.2.14", "@smithy/util-retry": "^4.3.6", "@smithy/util-stream": "^4.5.25", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-tFCqtci1gVGIRwgK3tmv2DV2EawXjBIQgwM/7KaeL4wHUMhNMUA+POUw6vGowtQb51ZaSDjK3KzI3MaQskOyuw=="],
|
||||
|
||||
"@aws-sdk/core": ["@aws-sdk/core@3.974.7", "", { "dependencies": { "@aws-sdk/types": "^3.973.8", "@aws-sdk/xml-builder": "^3.972.22", "@smithy/core": "^3.23.17", "@smithy/node-config-provider": "^4.3.14", "@smithy/property-provider": "^4.2.14", "@smithy/protocol-http": "^5.3.14", "@smithy/signature-v4": "^5.3.14", "@smithy/smithy-client": "^4.12.13", "@smithy/types": "^4.14.1", "@smithy/util-base64": "^4.3.2", "@smithy/util-middleware": "^4.2.14", "@smithy/util-retry": "^4.3.6", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-YhRC90ofz5oolTJZlA8voU/oUrCB2azi8Usx51k8hhB5LpWbYQMMXKUqSqkoL0Cru+RQJgWTHpAfEDDIwfUhJw=="],
|
||||
|
||||
@@ -321,7 +321,7 @@
|
||||
|
||||
"@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.996.24", "", { "dependencies": { "@aws-sdk/middleware-sdk-s3": "^3.972.36", "@aws-sdk/types": "^3.973.8", "@smithy/protocol-http": "^5.3.14", "@smithy/signature-v4": "^5.3.14", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-amP7tLikppN940wbBFISYqiuzVmpzMS9U3mcgtmVLjX4fdWI/SNCvrXv6ZxfVzTT4cT0rPKOLhFah2xLwzREWw=="],
|
||||
|
||||
"@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1039.0", "", { "dependencies": { "@aws-sdk/core": "^3.974.7", "@aws-sdk/nested-clients": "^3.997.5", "@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" } }, "sha512-NMSFL2HwkAOoCeLCQiqoOq5pT3vVbSjww2QZTuYgYknVwhhv125PSDzZIcL5EYnlxuPWjEOdauZK+FspkZDVdw=="],
|
||||
"@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1040.0", "", { "dependencies": { "@aws-sdk/core": "^3.974.7", "@aws-sdk/nested-clients": "^3.997.5", "@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" } }, "sha512-0KTpz2KqASQwzLOywV1bS2TX6Su0bARkATgpSu236BDM/D/6cMQ2EPiFwoRYwwvXsWSDn8KkKp9NV2ZWWA53Xw=="],
|
||||
|
||||
"@aws-sdk/types": ["@aws-sdk/types@3.973.8", "", { "dependencies": { "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-gjlAdtHMbtR9X5iIhVUvbVcy55KnznpC6bkDUWW9z915bi0ckdUr5cjf16Kp6xq0bP5HBD2xzgbL9F9Quv5vUw=="],
|
||||
|
||||
@@ -351,7 +351,7 @@
|
||||
|
||||
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
|
||||
|
||||
"@babel/parser": ["@babel/parser@7.29.2", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA=="],
|
||||
"@babel/parser": ["@babel/parser@7.29.3", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA=="],
|
||||
|
||||
"@babel/runtime": ["@babel/runtime@7.29.2", "", {}, "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g=="],
|
||||
|
||||
@@ -779,13 +779,13 @@
|
||||
|
||||
"async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="],
|
||||
|
||||
"b4a": ["b4a@1.8.0", "", { "peerDependencies": { "react-native-b4a": "*" }, "optionalPeers": ["react-native-b4a"] }, "sha512-qRuSmNSkGQaHwNbM7J78Wwy+ghLEYF1zNrSeMxj4Kgw6y33O3mXcQ6Ie9fRvfU/YnxWkOchPXbaLb73TkIsfdg=="],
|
||||
"b4a": ["b4a@1.8.1", "", { "peerDependencies": { "react-native-b4a": "*" }, "optionalPeers": ["react-native-b4a"] }, "sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw=="],
|
||||
|
||||
"bare-events": ["bare-events@2.8.2", "", { "peerDependencies": { "bare-abort-controller": "*" }, "optionalPeers": ["bare-abort-controller"] }, "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ=="],
|
||||
|
||||
"bare-fs": ["bare-fs@4.7.1", "", { "dependencies": { "bare-events": "^2.5.4", "bare-path": "^3.0.0", "bare-stream": "^2.6.4", "bare-url": "^2.2.2", "fast-fifo": "^1.3.2" }, "peerDependencies": { "bare-buffer": "*" }, "optionalPeers": ["bare-buffer"] }, "sha512-WDRsyVN52eAx/lBamKD6uyw8H4228h/x0sGGGegOamM2cd7Pag88GfMQalobXI+HaEUxpCkbKQUDOQqt9wawRw=="],
|
||||
|
||||
"bare-os": ["bare-os@3.9.0", "", {}, "sha512-JTjuZyNIDpw+GytMO4a6TK1VXdVKKJr6DRxEHasyuYyShV2deuiHJK/ahGZlebc+SG0/wJCB9XK8gprBGDFi/Q=="],
|
||||
"bare-os": ["bare-os@3.9.1", "", {}, "sha512-6M5XjcnsygQNPMCMPXSK379xrJFiZ/AEMNBmFEmQW8d/789VQATvriyi5r0HYTL9TkQ26rn3kgdTG3aisbrXkQ=="],
|
||||
|
||||
"bare-path": ["bare-path@3.0.0", "", { "dependencies": { "bare-os": "^3.0.1" } }, "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw=="],
|
||||
|
||||
@@ -993,7 +993,7 @@
|
||||
|
||||
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
|
||||
|
||||
"ip-address": ["ip-address@10.1.1", "", {}, "sha512-1FMu8/N15Ck1BL551Jf42NYIoin2unWjLQ2Fze/DXryJRl5twqtwNHlO39qERGbIOcKYWHdgRryhOC+NG4eaLw=="],
|
||||
"ip-address": ["ip-address@10.2.0", "", {}, "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA=="],
|
||||
|
||||
"is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
|
||||
|
||||
@@ -1095,7 +1095,7 @@
|
||||
|
||||
"mute-stream": ["mute-stream@3.0.0", "", {}, "sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw=="],
|
||||
|
||||
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
||||
"nanoid": ["nanoid@3.3.12", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ=="],
|
||||
|
||||
"neo-async": ["neo-async@2.6.2", "", {}, "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="],
|
||||
|
||||
@@ -1141,7 +1141,7 @@
|
||||
|
||||
"picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="],
|
||||
|
||||
"postcss": ["postcss@8.5.12", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA=="],
|
||||
"postcss": ["postcss@8.5.13", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-qif0+jGGZoLWdHey3UFHHWP0H7Gbmsk8T5VEqyYFbWqPr1XqvLGBbk/sl8V5exGmcYJklJOhOQq1pV9IcsiFag=="],
|
||||
|
||||
"prettier": ["prettier@3.8.3", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw=="],
|
||||
|
||||
@@ -1317,6 +1317,8 @@
|
||||
|
||||
"@aws-crypto/util/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="],
|
||||
|
||||
"@aws-sdk/credential-provider-sso/@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1039.0", "", { "dependencies": { "@aws-sdk/core": "^3.974.7", "@aws-sdk/nested-clients": "^3.997.5", "@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" } }, "sha512-NMSFL2HwkAOoCeLCQiqoOq5pT3vVbSjww2QZTuYgYknVwhhv125PSDzZIcL5EYnlxuPWjEOdauZK+FspkZDVdw=="],
|
||||
|
||||
"@bufbuild/protoplugin/typescript": ["typescript@5.4.5", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ=="],
|
||||
|
||||
"@puppeteer/browsers/proxy-agent": ["proxy-agent@6.5.0", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "^4.3.4", "http-proxy-agent": "^7.0.1", "https-proxy-agent": "^7.0.6", "lru-cache": "^7.14.1", "pac-proxy-agent": "^7.1.0", "proxy-from-env": "^1.1.0", "socks-proxy-agent": "^8.0.5" } }, "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A=="],
|
||||
|
||||
+7
-7
@@ -22,13 +22,13 @@
|
||||
"@google/genai": "^1.50.1",
|
||||
"@mozilla/readability": "^0.6.0",
|
||||
"@napi-rs/cli": "3.6.2",
|
||||
"@oh-my-pi/omp-stats": "14.5.12",
|
||||
"@oh-my-pi/pi-agent-core": "14.5.12",
|
||||
"@oh-my-pi/pi-ai": "14.5.12",
|
||||
"@oh-my-pi/pi-coding-agent": "14.5.12",
|
||||
"@oh-my-pi/pi-natives": "14.5.12",
|
||||
"@oh-my-pi/pi-tui": "14.5.12",
|
||||
"@oh-my-pi/pi-utils": "14.5.12",
|
||||
"@oh-my-pi/omp-stats": "14.5.13",
|
||||
"@oh-my-pi/pi-agent-core": "14.5.13",
|
||||
"@oh-my-pi/pi-ai": "14.5.13",
|
||||
"@oh-my-pi/pi-coding-agent": "14.5.13",
|
||||
"@oh-my-pi/pi-natives": "14.5.13",
|
||||
"@oh-my-pi/pi-tui": "14.5.13",
|
||||
"@oh-my-pi/pi-utils": "14.5.13",
|
||||
"@puppeteer/browsers": "^2.13.0",
|
||||
"@sinclair/typebox": "^0.34.49",
|
||||
"@smithy/node-http-handler": "^4.6.1",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"type": "module",
|
||||
"name": "@oh-my-pi/pi-agent-core",
|
||||
"version": "14.5.12",
|
||||
"version": "14.5.13",
|
||||
"description": "General-purpose agent with transport abstraction, state management, and attachment support",
|
||||
"homepage": "https://github.com/can1357/oh-my-pi",
|
||||
"author": "Can Boluk",
|
||||
|
||||
@@ -2,14 +2,16 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
- Removed `utils/oauth` re-exports from the package entrypoint, so OAuth helper imports from the root module must be updated
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed Vercel AI Gateway dynamic model discovery to use the versioned `/v1/models` catalog endpoint, restoring newly published models.
|
||||
|
||||
## [14.5.13] - 2026-05-01
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
- Removed `utils/oauth` re-exports from the package entrypoint, so OAuth helper imports from the root module must be updated
|
||||
|
||||
## [14.5.10] - 2026-04-30
|
||||
|
||||
### Added
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"type": "module",
|
||||
"name": "@oh-my-pi/pi-ai",
|
||||
"version": "14.5.12",
|
||||
"version": "14.5.13",
|
||||
"description": "Unified LLM API with automatic model discovery and provider configuration",
|
||||
"homepage": "https://github.com/can1357/oh-my-pi",
|
||||
"author": "Can Boluk",
|
||||
|
||||
+489
-6
@@ -16058,6 +16058,25 @@
|
||||
"contextWindow": 222222,
|
||||
"maxTokens": 8888
|
||||
},
|
||||
"x-ai/grok-4.3": {
|
||||
"id": "x-ai/grok-4.3",
|
||||
"name": "xAI: Grok 4.3",
|
||||
"api": "openai-completions",
|
||||
"provider": "kilo",
|
||||
"baseUrl": "https://api.kilo.ai/api/gateway",
|
||||
"reasoning": false,
|
||||
"input": [
|
||||
"text"
|
||||
],
|
||||
"cost": {
|
||||
"input": 0,
|
||||
"output": 0,
|
||||
"cacheRead": 0,
|
||||
"cacheWrite": 0
|
||||
},
|
||||
"contextWindow": 222222,
|
||||
"maxTokens": 8888
|
||||
},
|
||||
"x-ai/grok-code-fast-1": {
|
||||
"id": "x-ai/grok-code-fast-1",
|
||||
"name": "Grok Code Fast 1",
|
||||
@@ -24027,6 +24046,25 @@
|
||||
"contextWindow": 222222,
|
||||
"maxTokens": 8888
|
||||
},
|
||||
"ibm-granite/granite-4.1-8b": {
|
||||
"id": "ibm-granite/granite-4.1-8b",
|
||||
"name": "ibm-granite/granite-4.1-8b",
|
||||
"api": "openai-completions",
|
||||
"provider": "nanogpt",
|
||||
"baseUrl": "https://nano-gpt.com/api/v1",
|
||||
"reasoning": false,
|
||||
"input": [
|
||||
"text"
|
||||
],
|
||||
"cost": {
|
||||
"input": 0,
|
||||
"output": 0,
|
||||
"cacheRead": 0,
|
||||
"cacheWrite": 0
|
||||
},
|
||||
"contextWindow": 222222,
|
||||
"maxTokens": 8888
|
||||
},
|
||||
"inclusionai/ling-2.6-1t": {
|
||||
"id": "inclusionai/ling-2.6-1t",
|
||||
"name": "inclusionai/ling-2.6-1t",
|
||||
@@ -25827,7 +25865,7 @@
|
||||
"api": "openai-completions",
|
||||
"provider": "nanogpt",
|
||||
"baseUrl": "https://nano-gpt.com/api/v1",
|
||||
"reasoning": false,
|
||||
"reasoning": true,
|
||||
"input": [
|
||||
"text"
|
||||
],
|
||||
@@ -25838,7 +25876,12 @@
|
||||
"cacheWrite": 0
|
||||
},
|
||||
"contextWindow": 222222,
|
||||
"maxTokens": 8888
|
||||
"maxTokens": 8888,
|
||||
"thinking": {
|
||||
"mode": "effort",
|
||||
"minLevel": "minimal",
|
||||
"maxLevel": "xhigh"
|
||||
}
|
||||
},
|
||||
"mistral/mistral-vibe-cli-latest": {
|
||||
"id": "mistral/mistral-vibe-cli-latest",
|
||||
@@ -27948,6 +27991,25 @@
|
||||
"maxLevel": "xhigh"
|
||||
}
|
||||
},
|
||||
"owl": {
|
||||
"id": "owl",
|
||||
"name": "owl",
|
||||
"api": "openai-completions",
|
||||
"provider": "nanogpt",
|
||||
"baseUrl": "https://nano-gpt.com/api/v1",
|
||||
"reasoning": false,
|
||||
"input": [
|
||||
"text"
|
||||
],
|
||||
"cost": {
|
||||
"input": 0,
|
||||
"output": 0,
|
||||
"cacheRead": 0,
|
||||
"cacheWrite": 0
|
||||
},
|
||||
"contextWindow": 222222,
|
||||
"maxTokens": 8888
|
||||
},
|
||||
"pamanseau/OpenReasoning-Nemotron-32B": {
|
||||
"id": "pamanseau/OpenReasoning-Nemotron-32B",
|
||||
"name": "pamanseau/OpenReasoning-Nemotron-32B",
|
||||
@@ -28885,6 +28947,25 @@
|
||||
"maxLevel": "high"
|
||||
}
|
||||
},
|
||||
"Qwen3.5-27B-Anko": {
|
||||
"id": "Qwen3.5-27B-Anko",
|
||||
"name": "Qwen3.5-27B-Anko",
|
||||
"api": "openai-completions",
|
||||
"provider": "nanogpt",
|
||||
"baseUrl": "https://nano-gpt.com/api/v1",
|
||||
"reasoning": false,
|
||||
"input": [
|
||||
"text"
|
||||
],
|
||||
"cost": {
|
||||
"input": 0,
|
||||
"output": 0,
|
||||
"cacheRead": 0,
|
||||
"cacheWrite": 0
|
||||
},
|
||||
"contextWindow": 222222,
|
||||
"maxTokens": 8888
|
||||
},
|
||||
"Qwen3.5-27B-BlueStar-Derestricted": {
|
||||
"id": "Qwen3.5-27B-BlueStar-Derestricted",
|
||||
"name": "Qwen3.5-27B-BlueStar-Derestricted",
|
||||
@@ -28961,6 +29042,44 @@
|
||||
"contextWindow": 222222,
|
||||
"maxTokens": 8888
|
||||
},
|
||||
"Qwen3.5-27B-BlueStar-v3-Derestricted": {
|
||||
"id": "Qwen3.5-27B-BlueStar-v3-Derestricted",
|
||||
"name": "Qwen3.5-27B-BlueStar-v3-Derestricted",
|
||||
"api": "openai-completions",
|
||||
"provider": "nanogpt",
|
||||
"baseUrl": "https://nano-gpt.com/api/v1",
|
||||
"reasoning": false,
|
||||
"input": [
|
||||
"text"
|
||||
],
|
||||
"cost": {
|
||||
"input": 0,
|
||||
"output": 0,
|
||||
"cacheRead": 0,
|
||||
"cacheWrite": 0
|
||||
},
|
||||
"contextWindow": 222222,
|
||||
"maxTokens": 8888
|
||||
},
|
||||
"Qwen3.5-27B-BlueStar-v3-Derestricted-Lite": {
|
||||
"id": "Qwen3.5-27B-BlueStar-v3-Derestricted-Lite",
|
||||
"name": "Qwen3.5-27B-BlueStar-v3-Derestricted-Lite",
|
||||
"api": "openai-completions",
|
||||
"provider": "nanogpt",
|
||||
"baseUrl": "https://nano-gpt.com/api/v1",
|
||||
"reasoning": false,
|
||||
"input": [
|
||||
"text"
|
||||
],
|
||||
"cost": {
|
||||
"input": 0,
|
||||
"output": 0,
|
||||
"cacheRead": 0,
|
||||
"cacheWrite": 0
|
||||
},
|
||||
"contextWindow": 222222,
|
||||
"maxTokens": 8888
|
||||
},
|
||||
"Qwen3.5-27B-Derestricted": {
|
||||
"id": "Qwen3.5-27B-Derestricted",
|
||||
"name": "Qwen3.5-27B-Derestricted",
|
||||
@@ -28980,6 +29099,139 @@
|
||||
"contextWindow": 222222,
|
||||
"maxTokens": 8888
|
||||
},
|
||||
"Qwen3.5-27B-earica-Derestricted": {
|
||||
"id": "Qwen3.5-27B-earica-Derestricted",
|
||||
"name": "Qwen3.5-27B-earica-Derestricted",
|
||||
"api": "openai-completions",
|
||||
"provider": "nanogpt",
|
||||
"baseUrl": "https://nano-gpt.com/api/v1",
|
||||
"reasoning": false,
|
||||
"input": [
|
||||
"text"
|
||||
],
|
||||
"cost": {
|
||||
"input": 0,
|
||||
"output": 0,
|
||||
"cacheRead": 0,
|
||||
"cacheWrite": 0
|
||||
},
|
||||
"contextWindow": 222222,
|
||||
"maxTokens": 8888
|
||||
},
|
||||
"Qwen3.5-27B-earica-Derestricted-Lite": {
|
||||
"id": "Qwen3.5-27B-earica-Derestricted-Lite",
|
||||
"name": "Qwen3.5-27B-earica-Derestricted-Lite",
|
||||
"api": "openai-completions",
|
||||
"provider": "nanogpt",
|
||||
"baseUrl": "https://nano-gpt.com/api/v1",
|
||||
"reasoning": false,
|
||||
"input": [
|
||||
"text"
|
||||
],
|
||||
"cost": {
|
||||
"input": 0,
|
||||
"output": 0,
|
||||
"cacheRead": 0,
|
||||
"cacheWrite": 0
|
||||
},
|
||||
"contextWindow": 222222,
|
||||
"maxTokens": 8888
|
||||
},
|
||||
"Qwen3.5-27B-Infracelestial": {
|
||||
"id": "Qwen3.5-27B-Infracelestial",
|
||||
"name": "Qwen3.5-27B-Infracelestial",
|
||||
"api": "openai-completions",
|
||||
"provider": "nanogpt",
|
||||
"baseUrl": "https://nano-gpt.com/api/v1",
|
||||
"reasoning": false,
|
||||
"input": [
|
||||
"text"
|
||||
],
|
||||
"cost": {
|
||||
"input": 0,
|
||||
"output": 0,
|
||||
"cacheRead": 0,
|
||||
"cacheWrite": 0
|
||||
},
|
||||
"contextWindow": 222222,
|
||||
"maxTokens": 8888
|
||||
},
|
||||
"Qwen3.5-27B-Marvin-DPO-V2-Derestricted": {
|
||||
"id": "Qwen3.5-27B-Marvin-DPO-V2-Derestricted",
|
||||
"name": "Qwen3.5-27B-Marvin-DPO-V2-Derestricted",
|
||||
"api": "openai-completions",
|
||||
"provider": "nanogpt",
|
||||
"baseUrl": "https://nano-gpt.com/api/v1",
|
||||
"reasoning": false,
|
||||
"input": [
|
||||
"text"
|
||||
],
|
||||
"cost": {
|
||||
"input": 0,
|
||||
"output": 0,
|
||||
"cacheRead": 0,
|
||||
"cacheWrite": 0
|
||||
},
|
||||
"contextWindow": 222222,
|
||||
"maxTokens": 8888
|
||||
},
|
||||
"Qwen3.5-27B-Marvin-DPO-V2-Derestricted-Lite": {
|
||||
"id": "Qwen3.5-27B-Marvin-DPO-V2-Derestricted-Lite",
|
||||
"name": "Qwen3.5-27B-Marvin-DPO-V2-Derestricted-Lite",
|
||||
"api": "openai-completions",
|
||||
"provider": "nanogpt",
|
||||
"baseUrl": "https://nano-gpt.com/api/v1",
|
||||
"reasoning": false,
|
||||
"input": [
|
||||
"text"
|
||||
],
|
||||
"cost": {
|
||||
"input": 0,
|
||||
"output": 0,
|
||||
"cacheRead": 0,
|
||||
"cacheWrite": 0
|
||||
},
|
||||
"contextWindow": 222222,
|
||||
"maxTokens": 8888
|
||||
},
|
||||
"Qwen3.5-27B-Marvin-V2-Derestricted": {
|
||||
"id": "Qwen3.5-27B-Marvin-V2-Derestricted",
|
||||
"name": "Qwen3.5-27B-Marvin-V2-Derestricted",
|
||||
"api": "openai-completions",
|
||||
"provider": "nanogpt",
|
||||
"baseUrl": "https://nano-gpt.com/api/v1",
|
||||
"reasoning": false,
|
||||
"input": [
|
||||
"text"
|
||||
],
|
||||
"cost": {
|
||||
"input": 0,
|
||||
"output": 0,
|
||||
"cacheRead": 0,
|
||||
"cacheWrite": 0
|
||||
},
|
||||
"contextWindow": 222222,
|
||||
"maxTokens": 8888
|
||||
},
|
||||
"Qwen3.5-27B-Marvin-V2-Derestricted-Lite": {
|
||||
"id": "Qwen3.5-27B-Marvin-V2-Derestricted-Lite",
|
||||
"name": "Qwen3.5-27B-Marvin-V2-Derestricted-Lite",
|
||||
"api": "openai-completions",
|
||||
"provider": "nanogpt",
|
||||
"baseUrl": "https://nano-gpt.com/api/v1",
|
||||
"reasoning": false,
|
||||
"input": [
|
||||
"text"
|
||||
],
|
||||
"cost": {
|
||||
"input": 0,
|
||||
"output": 0,
|
||||
"cacheRead": 0,
|
||||
"cacheWrite": 0
|
||||
},
|
||||
"contextWindow": 222222,
|
||||
"maxTokens": 8888
|
||||
},
|
||||
"Qwen3.5-27B-Musica-v1": {
|
||||
"id": "Qwen3.5-27B-Musica-v1",
|
||||
"name": "Qwen3.5-27B-Musica-v1",
|
||||
@@ -28999,6 +29251,44 @@
|
||||
"contextWindow": 222222,
|
||||
"maxTokens": 8888
|
||||
},
|
||||
"Qwen3.5-27B-NaNovel-Derestricted": {
|
||||
"id": "Qwen3.5-27B-NaNovel-Derestricted",
|
||||
"name": "Qwen3.5-27B-NaNovel-Derestricted",
|
||||
"api": "openai-completions",
|
||||
"provider": "nanogpt",
|
||||
"baseUrl": "https://nano-gpt.com/api/v1",
|
||||
"reasoning": false,
|
||||
"input": [
|
||||
"text"
|
||||
],
|
||||
"cost": {
|
||||
"input": 0,
|
||||
"output": 0,
|
||||
"cacheRead": 0,
|
||||
"cacheWrite": 0
|
||||
},
|
||||
"contextWindow": 222222,
|
||||
"maxTokens": 8888
|
||||
},
|
||||
"Qwen3.5-27B-NaNovel-Derestricted-Lite": {
|
||||
"id": "Qwen3.5-27B-NaNovel-Derestricted-Lite",
|
||||
"name": "Qwen3.5-27B-NaNovel-Derestricted-Lite",
|
||||
"api": "openai-completions",
|
||||
"provider": "nanogpt",
|
||||
"baseUrl": "https://nano-gpt.com/api/v1",
|
||||
"reasoning": false,
|
||||
"input": [
|
||||
"text"
|
||||
],
|
||||
"cost": {
|
||||
"input": 0,
|
||||
"output": 0,
|
||||
"cacheRead": 0,
|
||||
"cacheWrite": 0
|
||||
},
|
||||
"contextWindow": 222222,
|
||||
"maxTokens": 8888
|
||||
},
|
||||
"Qwen3.5-27B-Omega-Evolution-v2.0-Derestricted": {
|
||||
"id": "Qwen3.5-27B-Omega-Evolution-v2.0-Derestricted",
|
||||
"name": "Qwen3.5-27B-Omega-Evolution-v2.0-Derestricted",
|
||||
@@ -29037,6 +29327,63 @@
|
||||
"contextWindow": 222222,
|
||||
"maxTokens": 8888
|
||||
},
|
||||
"Qwen3.5-27B-Queen-Derestricted": {
|
||||
"id": "Qwen3.5-27B-Queen-Derestricted",
|
||||
"name": "Qwen3.5-27B-Queen-Derestricted",
|
||||
"api": "openai-completions",
|
||||
"provider": "nanogpt",
|
||||
"baseUrl": "https://nano-gpt.com/api/v1",
|
||||
"reasoning": false,
|
||||
"input": [
|
||||
"text"
|
||||
],
|
||||
"cost": {
|
||||
"input": 0,
|
||||
"output": 0,
|
||||
"cacheRead": 0,
|
||||
"cacheWrite": 0
|
||||
},
|
||||
"contextWindow": 222222,
|
||||
"maxTokens": 8888
|
||||
},
|
||||
"Qwen3.5-27B-Queen-Derestricted-Lite": {
|
||||
"id": "Qwen3.5-27B-Queen-Derestricted-Lite",
|
||||
"name": "Qwen3.5-27B-Queen-Derestricted-Lite",
|
||||
"api": "openai-completions",
|
||||
"provider": "nanogpt",
|
||||
"baseUrl": "https://nano-gpt.com/api/v1",
|
||||
"reasoning": false,
|
||||
"input": [
|
||||
"text"
|
||||
],
|
||||
"cost": {
|
||||
"input": 0,
|
||||
"output": 0,
|
||||
"cacheRead": 0,
|
||||
"cacheWrite": 0
|
||||
},
|
||||
"contextWindow": 222222,
|
||||
"maxTokens": 8888
|
||||
},
|
||||
"Qwen3.5-27B-RpRMax-v1": {
|
||||
"id": "Qwen3.5-27B-RpRMax-v1",
|
||||
"name": "Qwen3.5-27B-RpRMax-v1",
|
||||
"api": "openai-completions",
|
||||
"provider": "nanogpt",
|
||||
"baseUrl": "https://nano-gpt.com/api/v1",
|
||||
"reasoning": false,
|
||||
"input": [
|
||||
"text"
|
||||
],
|
||||
"cost": {
|
||||
"input": 0,
|
||||
"output": 0,
|
||||
"cacheRead": 0,
|
||||
"cacheWrite": 0
|
||||
},
|
||||
"contextWindow": 222222,
|
||||
"maxTokens": 8888
|
||||
},
|
||||
"Qwen3.5-27B-Vivid-Durian": {
|
||||
"id": "Qwen3.5-27B-Vivid-Durian",
|
||||
"name": "Qwen3.5-27B-Vivid-Durian",
|
||||
@@ -31292,6 +31639,25 @@
|
||||
"contextWindow": 222222,
|
||||
"maxTokens": 8888
|
||||
},
|
||||
"x-ai/grok-4.3": {
|
||||
"id": "x-ai/grok-4.3",
|
||||
"name": "x-ai/grok-4.3",
|
||||
"api": "openai-completions",
|
||||
"provider": "nanogpt",
|
||||
"baseUrl": "https://nano-gpt.com/api/v1",
|
||||
"reasoning": false,
|
||||
"input": [
|
||||
"text"
|
||||
],
|
||||
"cost": {
|
||||
"input": 0,
|
||||
"output": 0,
|
||||
"cacheRead": 0,
|
||||
"cacheWrite": 0
|
||||
},
|
||||
"contextWindow": 222222,
|
||||
"maxTokens": 8888
|
||||
},
|
||||
"x-ai/grok-code-fast-1": {
|
||||
"id": "x-ai/grok-code-fast-1",
|
||||
"name": "Grok Code Fast 1",
|
||||
@@ -34863,6 +35229,7 @@
|
||||
},
|
||||
"contextWindow": 272000,
|
||||
"maxTokens": 128000,
|
||||
"preferWebsockets": true,
|
||||
"priority": 6,
|
||||
"thinking": {
|
||||
"mode": "effort",
|
||||
@@ -42087,9 +42454,9 @@
|
||||
"text"
|
||||
],
|
||||
"cost": {
|
||||
"input": 0.14,
|
||||
"input": 0.12,
|
||||
"output": 0.7999999999999999,
|
||||
"cacheRead": 0.09,
|
||||
"cacheRead": 0.07,
|
||||
"cacheWrite": 0
|
||||
},
|
||||
"contextWindow": 262144,
|
||||
@@ -43265,8 +43632,8 @@
|
||||
"image"
|
||||
],
|
||||
"cost": {
|
||||
"input": 2,
|
||||
"output": 6,
|
||||
"input": 1.25,
|
||||
"output": 2.5,
|
||||
"cacheRead": 0.19999999999999998,
|
||||
"cacheWrite": 0
|
||||
},
|
||||
@@ -43303,6 +43670,31 @@
|
||||
"maxLevel": "high"
|
||||
}
|
||||
},
|
||||
"x-ai/grok-4.3": {
|
||||
"id": "x-ai/grok-4.3",
|
||||
"name": "xAI: Grok 4.3",
|
||||
"api": "openai-completions",
|
||||
"provider": "openrouter",
|
||||
"baseUrl": "https://openrouter.ai/api/v1",
|
||||
"reasoning": true,
|
||||
"input": [
|
||||
"text",
|
||||
"image"
|
||||
],
|
||||
"cost": {
|
||||
"input": 1.25,
|
||||
"output": 2.5,
|
||||
"cacheRead": 0.19999999999999998,
|
||||
"cacheWrite": 0
|
||||
},
|
||||
"contextWindow": 1000000,
|
||||
"maxTokens": 8888,
|
||||
"thinking": {
|
||||
"mode": "effort",
|
||||
"minLevel": "minimal",
|
||||
"maxLevel": "high"
|
||||
}
|
||||
},
|
||||
"x-ai/grok-code-fast-1": {
|
||||
"id": "x-ai/grok-code-fast-1",
|
||||
"name": "Grok Code Fast 1",
|
||||
@@ -44033,6 +44425,25 @@
|
||||
"contextWindow": 262144,
|
||||
"maxTokens": 8192
|
||||
},
|
||||
"hf:moonshotai/Kimi-K2.6": {
|
||||
"id": "hf:moonshotai/Kimi-K2.6",
|
||||
"name": "moonshotai/Kimi-K2.6",
|
||||
"api": "openai-completions",
|
||||
"provider": "synthetic",
|
||||
"baseUrl": "https://api.synthetic.new/openai/v1",
|
||||
"reasoning": false,
|
||||
"input": [
|
||||
"text"
|
||||
],
|
||||
"cost": {
|
||||
"input": 0,
|
||||
"output": 0,
|
||||
"cacheRead": 0,
|
||||
"cacheWrite": 0
|
||||
},
|
||||
"contextWindow": 262144,
|
||||
"maxTokens": 8192
|
||||
},
|
||||
"hf:nvidia/Kimi-K2.5-NVFP4": {
|
||||
"id": "hf:nvidia/Kimi-K2.5-NVFP4",
|
||||
"name": "nvidia/Kimi-K2.5-NVFP4",
|
||||
@@ -45320,6 +45731,28 @@
|
||||
"supportsUsageInStreaming": false
|
||||
}
|
||||
},
|
||||
"grok-4-3": {
|
||||
"id": "grok-4-3",
|
||||
"name": "grok-4-3",
|
||||
"api": "openai-completions",
|
||||
"provider": "venice",
|
||||
"baseUrl": "https://api.venice.ai/api/v1",
|
||||
"reasoning": false,
|
||||
"input": [
|
||||
"text"
|
||||
],
|
||||
"cost": {
|
||||
"input": 0,
|
||||
"output": 0,
|
||||
"cacheRead": 0,
|
||||
"cacheWrite": 0
|
||||
},
|
||||
"contextWindow": 1000000,
|
||||
"maxTokens": 8888,
|
||||
"compat": {
|
||||
"supportsUsageInStreaming": false
|
||||
}
|
||||
},
|
||||
"grok-41-fast": {
|
||||
"id": "grok-41-fast",
|
||||
"name": "Grok 4.1 Fast",
|
||||
@@ -50028,6 +50461,31 @@
|
||||
"maxLevel": "xhigh"
|
||||
}
|
||||
},
|
||||
"xai/grok-4.3": {
|
||||
"id": "xai/grok-4.3",
|
||||
"name": "Grok 4.3",
|
||||
"api": "anthropic-messages",
|
||||
"provider": "vercel-ai-gateway",
|
||||
"baseUrl": "https://ai-gateway.vercel.sh",
|
||||
"reasoning": true,
|
||||
"input": [
|
||||
"text",
|
||||
"image"
|
||||
],
|
||||
"cost": {
|
||||
"input": 1.25,
|
||||
"output": 2.5,
|
||||
"cacheRead": 0.19999999999999998,
|
||||
"cacheWrite": 0
|
||||
},
|
||||
"contextWindow": 1000000,
|
||||
"maxTokens": 1000000,
|
||||
"thinking": {
|
||||
"mode": "budget",
|
||||
"minLevel": "minimal",
|
||||
"maxLevel": "xhigh"
|
||||
}
|
||||
},
|
||||
"xai/grok-code-fast-1": {
|
||||
"id": "xai/grok-code-fast-1",
|
||||
"name": "Grok Code Fast 1",
|
||||
@@ -50986,6 +51444,31 @@
|
||||
"maxLevel": "high"
|
||||
}
|
||||
},
|
||||
"grok-4.3": {
|
||||
"id": "grok-4.3",
|
||||
"name": "Grok 4.3",
|
||||
"api": "openai-completions",
|
||||
"provider": "xai",
|
||||
"baseUrl": "https://api.x.ai/v1",
|
||||
"reasoning": true,
|
||||
"input": [
|
||||
"text",
|
||||
"image"
|
||||
],
|
||||
"cost": {
|
||||
"input": 1.25,
|
||||
"output": 2.5,
|
||||
"cacheRead": 0.2,
|
||||
"cacheWrite": 0
|
||||
},
|
||||
"contextWindow": 1000000,
|
||||
"maxTokens": 30000,
|
||||
"thinking": {
|
||||
"mode": "effort",
|
||||
"minLevel": "minimal",
|
||||
"maxLevel": "high"
|
||||
}
|
||||
},
|
||||
"grok-beta": {
|
||||
"id": "grok-beta",
|
||||
"name": "Grok Beta",
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
# Changelog
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [14.5.13] - 2026-05-01
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
- Removed the built-in `python` tool in favor of `eval`, so tool allowlists and tool-call handlers referencing `python` need to migrate
|
||||
@@ -19,11 +22,18 @@
|
||||
|
||||
### Changed
|
||||
|
||||
- Changed AGENTS.md discovery to respect `.gitignore` files during project context collection so ignored context files are no longer loaded
|
||||
- Changed eval tool initialization to skip Python kernel preflight when the JavaScript backend is enabled, avoiding unnecessary startup checks
|
||||
- Changed model registry refresh flow to defer rebuilding the canonical model index until refresh operations complete, reducing refresh churn
|
||||
- Changed execution/tool discovery flow so `exec` maps to `eval` when any `eval` backend is enabled, while `bash` stays independently available
|
||||
- Changed `eval` dispatch to automatically fall back to JavaScript when Python is unavailable and JavaScript backend is enabled
|
||||
- Parallelized plugin root preloading with other startup initialization in `runRootCommand` to reduce startup latency
|
||||
- Parallelized session bootstrap work in `createAgentSession`, including AGENTS.md scanning, context discovery, prompt template loading, slash command loading, and skill discovery, to reduce time to first available session
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed eval startup messaging to report `eval` as unavailable when Python is unreachable and JavaScript backend is disabled
|
||||
|
||||
## [14.5.12] - 2026-04-30
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
@@ -391,7 +391,7 @@ A `ToolFactory` is `(session: ToolSession) => Tool | null | Promise<Tool | null>
|
||||
|
||||
1. Normalizes requested tool names (`toolNames`) and always injects `exit_plan_mode`.
|
||||
2. Resolves eval backend allowance via `PI_PY` override (`getEvalBackendsFromEnv()`) or `eval.py` / `eval.js` settings.
|
||||
3. Performs Python kernel preflight/warmup when applicable (`checkPythonKernelAvailability`, `warmPythonEnvironment`).
|
||||
3. Performs Python kernel preflight when applicable (`checkPythonKernelAvailability`).
|
||||
4. Computes effective gating (`isToolAllowed`) from settings and runtime state:
|
||||
- feature toggles (`find.enabled`, `grep.enabled`, etc.)
|
||||
- recursion guard for `task` (`task.maxRecursionDepth` vs `session.taskDepth`)
|
||||
@@ -777,7 +777,7 @@ This subsystem is split into two layers:
|
||||
- Entry points:
|
||||
- `executePython(code, options)`
|
||||
- `executePythonWithKernel(kernel, code, options)`
|
||||
- warmup/session utilities (`warmPythonEnvironment`, `disposeAllKernelSessions`).
|
||||
- session utilities (`disposeAllKernelSessions`, `disposeKernelSessionsByOwner`).
|
||||
- Manages kernel session lifecycle in `kernelSessions: Map<string, KernelSession>` with:
|
||||
- bounded session count (`MAX_KERNEL_SESSIONS`), LRU eviction (`evictOldestSession`)
|
||||
- idle cleanup timer (`cleanupIdleSessions`)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"type": "module",
|
||||
"name": "@oh-my-pi/pi-coding-agent",
|
||||
"version": "14.5.12",
|
||||
"version": "14.5.13",
|
||||
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
||||
"homepage": "https://github.com/can1357/oh-my-pi",
|
||||
"author": "Can Boluk",
|
||||
|
||||
@@ -175,6 +175,15 @@ export class ConfigFile<T> implements IConfigFile<T> {
|
||||
return result;
|
||||
}
|
||||
|
||||
getMtimeMs(): number | null {
|
||||
try {
|
||||
return fs.statSync(this.path()).mtimeMs;
|
||||
} catch (err) {
|
||||
if (isEnoent(err)) return null;
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
withValidation(name: string, validate: (value: T) => void): this {
|
||||
const prev = this.#auxValidate;
|
||||
this.#auxValidate = (value: T) => {
|
||||
|
||||
@@ -61,7 +61,25 @@ const TRAILING_CANONICAL_MARKERS = [
|
||||
"int8",
|
||||
"int4",
|
||||
] as const;
|
||||
const TRAILING_MARKER_SUFFIXES: readonly string[] = (() => {
|
||||
const suffixes: string[] = [];
|
||||
for (const marker of TRAILING_CANONICAL_MARKERS) {
|
||||
const lower = marker.toLowerCase();
|
||||
suffixes.push(`-${lower}`, `:${lower}`);
|
||||
}
|
||||
return suffixes;
|
||||
})();
|
||||
const WRAPPER_PREFIXES = ["duo-chat-"] as const;
|
||||
|
||||
let __referenceDataCache: CanonicalReferenceData | undefined;
|
||||
const EMPTY_COMPILED_EQUIVALENCE: CompiledEquivalenceConfig = {
|
||||
overrides: new Map<string, string>(),
|
||||
exclude: new Set<string>(),
|
||||
};
|
||||
const __resolutionCache: WeakMap<
|
||||
CompiledEquivalenceConfig,
|
||||
WeakMap<Model<Api>, ResolvedCanonicalModel>
|
||||
> = new WeakMap();
|
||||
const FAMILY_EXTRACTION_PATTERNS = [
|
||||
/(?:^|[/:._-])((?:claude|gemini|gpt|grok|glm|qwen|minimax|kimi|deepseek|llama|gemma|nova|mistral|ministral|pixtral|codestral|devstral|magistral|ernie|doubao|seed|aion|olmo|molmo|nemotron|palmyra|command|codex|coder|o[1345])[-a-z0-9.]+)(?::|$)/i,
|
||||
/(?:^|[/:._-])((?:claude|gemini|gpt|grok|glm|qwen|minimax|kimi|deepseek|llama|gemma|nova|mistral|ministral|pixtral|codestral|devstral|magistral|ernie|doubao|seed|aion|olmo|molmo|nemotron|palmyra|command|codex|coder|o[1345])[-a-z0-9.]+(?:[-_/][a-z0-9.]+)*)(?::|$)/i,
|
||||
@@ -79,6 +97,9 @@ function shouldReplaceReference(existing: Model<Api> | undefined, candidate: Mod
|
||||
}
|
||||
|
||||
function createCanonicalReferenceData(): CanonicalReferenceData {
|
||||
if (__referenceDataCache) {
|
||||
return __referenceDataCache;
|
||||
}
|
||||
const references = new Map<string, Model<Api>>();
|
||||
for (const provider of getBundledProviders()) {
|
||||
for (const model of getBundledModels(provider as Parameters<typeof getBundledModels>[0])) {
|
||||
@@ -89,10 +110,12 @@ function createCanonicalReferenceData(): CanonicalReferenceData {
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
references,
|
||||
officialIds: new Set(references.keys()),
|
||||
const officialIds = new Set(references.keys());
|
||||
__referenceDataCache = {
|
||||
references: Object.freeze(references) as Map<string, Model<Api>>,
|
||||
officialIds: Object.freeze(officialIds) as Set<string>,
|
||||
};
|
||||
return __referenceDataCache;
|
||||
}
|
||||
|
||||
function normalizeSelectorKey(selector: string): string {
|
||||
@@ -135,10 +158,12 @@ function buildExclusionSet(exclusions: readonly string[] | undefined): Set<strin
|
||||
}
|
||||
|
||||
function compileEquivalenceConfig(config: ModelEquivalenceConfig | undefined): CompiledEquivalenceConfig {
|
||||
return {
|
||||
overrides: buildOverrideMap(config?.overrides),
|
||||
exclude: buildExclusionSet(config?.exclude),
|
||||
};
|
||||
const overrides = buildOverrideMap(config?.overrides);
|
||||
const exclude = buildExclusionSet(config?.exclude);
|
||||
if (overrides.size === 0 && exclude.size === 0) {
|
||||
return EMPTY_COMPILED_EQUIVALENCE;
|
||||
}
|
||||
return { overrides, exclude };
|
||||
}
|
||||
|
||||
function addCanonicalCandidate(candidates: Set<string>, candidate: string): void {
|
||||
@@ -149,12 +174,10 @@ function addCanonicalCandidate(candidates: Set<string>, candidate: string): void
|
||||
}
|
||||
|
||||
function stripTrailingMarker(candidate: string): string | undefined {
|
||||
for (const marker of TRAILING_CANONICAL_MARKERS) {
|
||||
for (const separator of ["-", ":"] as const) {
|
||||
const suffix = `${separator}${marker}`;
|
||||
if (candidate.toLowerCase().endsWith(suffix)) {
|
||||
return candidate.slice(0, -suffix.length);
|
||||
}
|
||||
const lower = candidate.toLowerCase();
|
||||
for (const suffix of TRAILING_MARKER_SUFFIXES) {
|
||||
if (lower.endsWith(suffix)) {
|
||||
return candidate.slice(0, -suffix.length);
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
@@ -450,8 +473,8 @@ function getHeuristicCanonicalCandidates(modelId: string): string[] {
|
||||
const queue = [modelId];
|
||||
const visited = new Set<string>();
|
||||
|
||||
while (queue.length > 0) {
|
||||
const candidate = queue.shift();
|
||||
for (let qi = 0; qi < queue.length; qi += 1) {
|
||||
const candidate = queue[qi];
|
||||
if (!candidate) {
|
||||
continue;
|
||||
}
|
||||
@@ -644,8 +667,18 @@ export function buildCanonicalModelIndex(
|
||||
const byId = new Map<string, CanonicalModelRecord>();
|
||||
const bySelector = new Map<string, string>();
|
||||
|
||||
let modelCache = __resolutionCache.get(compiledEquivalence);
|
||||
if (!modelCache) {
|
||||
modelCache = new WeakMap<Model<Api>, ResolvedCanonicalModel>();
|
||||
__resolutionCache.set(compiledEquivalence, modelCache);
|
||||
}
|
||||
|
||||
for (const model of models) {
|
||||
const canonical = resolveCanonicalIdForModel(model, compiledEquivalence, referenceData);
|
||||
let canonical = modelCache.get(model);
|
||||
if (!canonical) {
|
||||
canonical = resolveCanonicalIdForModel(model, compiledEquivalence, referenceData);
|
||||
modelCache.set(model, canonical);
|
||||
}
|
||||
const selector = formatCanonicalVariantSelector(model);
|
||||
const variant: CanonicalModelVariant = {
|
||||
canonicalId: canonical.id,
|
||||
|
||||
@@ -797,6 +797,7 @@ export class ModelRegistry {
|
||||
#equivalenceConfig: ModelEquivalenceConfig | undefined;
|
||||
#configError: ConfigError | undefined = undefined;
|
||||
#modelsConfigFile: ConfigFile<ModelsConfig>;
|
||||
#lastStaticLoadMtime: number | null = null;
|
||||
#registeredProviderSources: Set<string> = new Set();
|
||||
#providerDiscoveryStates: Map<string, ProviderDiscoveryState> = new Map();
|
||||
#cacheDbPath?: string;
|
||||
@@ -810,6 +811,8 @@ export class ModelRegistry {
|
||||
#runtimeProviderOverrides: Map<string, ProviderOverride> = new Map();
|
||||
#runtimeProvidersBySource: Map<string, Set<string>> = new Map();
|
||||
#runtimeProviderSourceByName: Map<string, string> = new Map();
|
||||
#rebuildPending: boolean = false;
|
||||
#rebuildSuspended: number = 0;
|
||||
|
||||
/**
|
||||
* @param authStorage - Auth storage for API key resolution
|
||||
@@ -836,9 +839,14 @@ export class ModelRegistry {
|
||||
* Reload models from disk (built-in + custom from models.json).
|
||||
*/
|
||||
async refresh(strategy: ModelRefreshStrategy = "online-if-uncached"): Promise<void> {
|
||||
this.#reloadStaticModels();
|
||||
this.#suppressedSelectors.clear();
|
||||
await this.#refreshRuntimeDiscoveries(strategy);
|
||||
this.#suspendRebuild();
|
||||
try {
|
||||
this.#reloadStaticModels();
|
||||
this.#suppressedSelectors.clear();
|
||||
await this.#refreshRuntimeDiscoveries(strategy);
|
||||
} finally {
|
||||
this.#resumeRebuild();
|
||||
}
|
||||
}
|
||||
|
||||
refreshInBackground(strategy: ModelRefreshStrategy = "online-if-uncached"): void {
|
||||
@@ -860,16 +868,26 @@ export class ModelRegistry {
|
||||
}
|
||||
|
||||
async refreshProvider(providerId: string, strategy: ModelRefreshStrategy = "online"): Promise<void> {
|
||||
this.#reloadStaticModels();
|
||||
for (const selector of this.#suppressedSelectors.keys()) {
|
||||
if (selector.startsWith(`${providerId}/`)) {
|
||||
this.#suppressedSelectors.delete(selector);
|
||||
this.#suspendRebuild();
|
||||
try {
|
||||
this.#reloadStaticModels();
|
||||
for (const selector of this.#suppressedSelectors.keys()) {
|
||||
if (selector.startsWith(`${providerId}/`)) {
|
||||
this.#suppressedSelectors.delete(selector);
|
||||
}
|
||||
}
|
||||
await this.#refreshRuntimeDiscoveries(strategy, new Set([providerId]));
|
||||
} finally {
|
||||
this.#resumeRebuild();
|
||||
}
|
||||
await this.#refreshRuntimeDiscoveries(strategy, new Set([providerId]));
|
||||
}
|
||||
|
||||
#reloadStaticModels(): void {
|
||||
const currentMtime = this.#modelsConfigFile.getMtimeMs();
|
||||
if (currentMtime !== null && currentMtime === this.#lastStaticLoadMtime) {
|
||||
// models.json unchanged since last load; reload + canonical rebuild would be redundant.
|
||||
return;
|
||||
}
|
||||
this.#modelsConfigFile.invalidate();
|
||||
this.#customProviderApiKeys.clear();
|
||||
this.#keylessProviders.clear();
|
||||
@@ -924,6 +942,7 @@ export class ModelRegistry {
|
||||
const withModelOverrides = this.#applyModelOverrides(combined, this.#modelOverrides);
|
||||
this.#models = this.#applyRuntimeProviderOverrides(withModelOverrides);
|
||||
this.#rebuildCanonicalIndex();
|
||||
this.#lastStaticLoadMtime = this.#modelsConfigFile.getMtimeMs();
|
||||
}
|
||||
|
||||
/** Load built-in models, applying provider-level overrides only.
|
||||
@@ -947,14 +966,19 @@ export class ModelRegistry {
|
||||
|
||||
#mergeResolvedModels(baseModels: Model<Api>[], replacementModels: Model<Api>[]): Model<Api>[] {
|
||||
const merged = [...baseModels];
|
||||
const indexByKey = new Map<string, number>();
|
||||
for (let i = 0; i < merged.length; i += 1) {
|
||||
const m = merged[i];
|
||||
indexByKey.set(`${m.provider}\u0000${m.id}`, i);
|
||||
}
|
||||
for (const replacementModel of replacementModels) {
|
||||
const existingIndex = merged.findIndex(
|
||||
m => m.provider === replacementModel.provider && m.id === replacementModel.id,
|
||||
);
|
||||
if (existingIndex >= 0) {
|
||||
const key = `${replacementModel.provider}\u0000${replacementModel.id}`;
|
||||
const existingIndex = indexByKey.get(key);
|
||||
if (existingIndex !== undefined) {
|
||||
merged[existingIndex] = replacementModel;
|
||||
} else {
|
||||
merged.push(replacementModel);
|
||||
indexByKey.set(key, merged.length - 1);
|
||||
}
|
||||
}
|
||||
return merged;
|
||||
@@ -963,9 +987,15 @@ export class ModelRegistry {
|
||||
/** Merge custom models with built-in, replacing by provider+id match */
|
||||
#mergeCustomModels(builtInModels: Model<Api>[], customModels: CustomModelOverlay[]): Model<Api>[] {
|
||||
const merged = [...builtInModels];
|
||||
const indexByKey = new Map<string, number>();
|
||||
for (let i = 0; i < merged.length; i += 1) {
|
||||
const m = merged[i];
|
||||
indexByKey.set(`${m.provider}\u0000${m.id}`, i);
|
||||
}
|
||||
for (const customModel of customModels) {
|
||||
const existingIndex = merged.findIndex(m => m.provider === customModel.provider && m.id === customModel.id);
|
||||
if (existingIndex >= 0) {
|
||||
const key = `${customModel.provider}\u0000${customModel.id}`;
|
||||
const existingIndex = indexByKey.get(key);
|
||||
if (existingIndex !== undefined) {
|
||||
const existingModel = merged[existingIndex];
|
||||
merged[existingIndex] = enrichModelThinking({
|
||||
...existingModel,
|
||||
@@ -990,6 +1020,7 @@ export class ModelRegistry {
|
||||
} as Model<Api>);
|
||||
} else {
|
||||
merged.push(finalizeCustomModel(customModel, { useDefaults: true }));
|
||||
indexByKey.set(key, merged.length - 1);
|
||||
}
|
||||
}
|
||||
return merged;
|
||||
@@ -1756,7 +1787,26 @@ export class ModelRegistry {
|
||||
}
|
||||
|
||||
#rebuildCanonicalIndex(): void {
|
||||
if (this.#rebuildSuspended > 0) {
|
||||
this.#rebuildPending = true;
|
||||
return;
|
||||
}
|
||||
this.#canonicalIndex = buildCanonicalModelIndex(this.#models, this.#equivalenceConfig);
|
||||
this.#rebuildPending = false;
|
||||
}
|
||||
|
||||
#suspendRebuild(): void {
|
||||
this.#rebuildSuspended += 1;
|
||||
}
|
||||
|
||||
#resumeRebuild(): void {
|
||||
if (this.#rebuildSuspended > 0) {
|
||||
this.#rebuildSuspended -= 1;
|
||||
}
|
||||
if (this.#rebuildSuspended === 0 && this.#rebuildPending) {
|
||||
this.#rebuildPending = false;
|
||||
this.#canonicalIndex = buildCanonicalModelIndex(this.#models, this.#equivalenceConfig);
|
||||
}
|
||||
}
|
||||
|
||||
#parseModels(config: ModelsConfig): CustomModelOverlay[] {
|
||||
@@ -2016,6 +2066,7 @@ export class ModelRegistry {
|
||||
this.#runtimeProviderSourceByName.delete(providerName);
|
||||
this.#clearRuntimeProviderState(providerName);
|
||||
}
|
||||
this.#lastStaticLoadMtime = null;
|
||||
this.#reloadStaticModels();
|
||||
this.#rebuildCanonicalIndex();
|
||||
}
|
||||
@@ -2094,6 +2145,7 @@ export class ModelRegistry {
|
||||
this.#runtimeProviderSourceByName.set(providerName, sourceId);
|
||||
}
|
||||
if (sourceHandoff) {
|
||||
this.#lastStaticLoadMtime = null;
|
||||
this.#reloadStaticModels();
|
||||
}
|
||||
|
||||
|
||||
@@ -116,6 +116,24 @@ function cloneModelWithRequestedId(model: Model<Api>, requestedId: string): Mode
|
||||
};
|
||||
}
|
||||
|
||||
const providerModelIndexCache = new WeakMap<readonly Model<Api>[], Map<string, Model<Api> | null>>();
|
||||
|
||||
function getProviderModelIndex(availableModels: readonly Model<Api>[]): Map<string, Model<Api> | null> {
|
||||
let index = providerModelIndexCache.get(availableModels);
|
||||
if (index) return index;
|
||||
index = new Map<string, Model<Api> | null>();
|
||||
for (const m of availableModels) {
|
||||
const key = `${m.provider.toLowerCase()}\u0000${m.id.toLowerCase()}`;
|
||||
if (index.has(key)) {
|
||||
index.set(key, null); // ambiguous sentinel; do not overwrite back
|
||||
} else {
|
||||
index.set(key, m);
|
||||
}
|
||||
}
|
||||
providerModelIndexCache.set(availableModels, index);
|
||||
return index;
|
||||
}
|
||||
|
||||
export function resolveProviderModelReference(
|
||||
provider: string,
|
||||
modelId: string,
|
||||
@@ -127,14 +145,13 @@ export function resolveProviderModelReference(
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const exactMatches = availableModels.filter(
|
||||
model => model.provider.toLowerCase() === normalizedProvider && model.id.toLowerCase() === normalizedModelId,
|
||||
);
|
||||
if (exactMatches.length === 1) {
|
||||
return exactMatches[0];
|
||||
const index = getProviderModelIndex(availableModels);
|
||||
const exact = index.get(`${normalizedProvider}\u0000${normalizedModelId}`);
|
||||
if (exact === null) {
|
||||
return undefined; // ambiguous
|
||||
}
|
||||
if (exactMatches.length > 1) {
|
||||
return undefined;
|
||||
if (exact !== undefined) {
|
||||
return exact;
|
||||
}
|
||||
|
||||
if (normalizedProvider !== "openrouter") {
|
||||
@@ -142,16 +159,13 @@ export function resolveProviderModelReference(
|
||||
}
|
||||
|
||||
for (const fallbackId of getOpenRouterFallbackModelIds(modelId).slice(1)) {
|
||||
const baseMatches = availableModels.filter(
|
||||
model =>
|
||||
model.provider.toLowerCase() === normalizedProvider && model.id.toLowerCase() === fallbackId.toLowerCase(),
|
||||
);
|
||||
if (baseMatches.length === 1) {
|
||||
return cloneModelWithRequestedId(baseMatches[0], modelId);
|
||||
}
|
||||
if (baseMatches.length > 1) {
|
||||
const fallback = index.get(`${normalizedProvider}\u0000${fallbackId.toLowerCase()}`);
|
||||
if (fallback === null) {
|
||||
return undefined;
|
||||
}
|
||||
if (fallback !== undefined) {
|
||||
return cloneModelWithRequestedId(fallback, modelId);
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
|
||||
@@ -398,21 +398,22 @@ export class Settings {
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
async #load(): Promise<Settings> {
|
||||
// Project settings load (loadCapability scans cwd) is independent of the
|
||||
// persist chain (storage open → legacy migration → global config.yml read),
|
||||
// so kick it off first and await after the persist chain completes. The
|
||||
// persist steps remain sequential: migration may write config.yml, which
|
||||
// #loadYaml then reads; migration's db fallback needs #storage opened.
|
||||
const projectPromise = this.#loadProjectSettings();
|
||||
|
||||
if (this.#persist) {
|
||||
// Open storage
|
||||
this.#storage = await AgentStorage.open(getAgentDbPath(this.#agentDir));
|
||||
|
||||
// Migrate from legacy formats if needed
|
||||
await this.#migrateFromLegacy();
|
||||
|
||||
// Load global settings from config.yml
|
||||
this.#global = await this.#loadYaml(this.#configPath!);
|
||||
}
|
||||
|
||||
// Load project settings
|
||||
this.#project = await this.#loadProjectSettings();
|
||||
this.#project = await projectPromise;
|
||||
|
||||
// Build merged view
|
||||
// Build merged view (global → project → overrides; project wins over global)
|
||||
this.#rebuildMerged();
|
||||
this.#fireAllHooks();
|
||||
return this;
|
||||
|
||||
@@ -1,18 +1,14 @@
|
||||
import type { ToolSession } from "../tools";
|
||||
import type { EvalDisplayOutput, EvalLanguage } from "./types";
|
||||
|
||||
/** Per-call options shared across warm and execute. */
|
||||
export interface ExecutorBackendCallOptions {
|
||||
/** Per-cell execute() options. */
|
||||
export interface ExecutorBackendExecOptions {
|
||||
cwd: string;
|
||||
sessionId: string;
|
||||
sessionFile: string | undefined;
|
||||
kernelOwnerId: string | undefined;
|
||||
signal?: AbortSignal;
|
||||
session: ToolSession;
|
||||
}
|
||||
|
||||
/** Per-cell execute() options. */
|
||||
export interface ExecutorBackendExecOptions extends ExecutorBackendCallOptions {
|
||||
deadlineMs: number;
|
||||
reset: boolean;
|
||||
artifactPath: string | undefined;
|
||||
@@ -40,10 +36,8 @@ export interface ExecutorBackend {
|
||||
readonly label: string;
|
||||
/** Source language identifier passed to the syntax highlighter (e.g. "python", "javascript"). */
|
||||
readonly highlightLang: string;
|
||||
/** Cheap availability check (no full warmup). Used by fallback resolution. */
|
||||
/** Cheap availability check. Used by fallback resolution. */
|
||||
isAvailable(session: ToolSession): Promise<boolean>;
|
||||
/** Optional pre-warm performed once per session (no-op on backends that don't need it). */
|
||||
warm?(opts: ExecutorBackendCallOptions): Promise<{ ok: boolean; reason?: string }>;
|
||||
/** Execute one cell. Caller invokes once per cell and aggregates results. */
|
||||
execute(code: string, opts: ExecutorBackendExecOptions): Promise<ExecutorBackendResult>;
|
||||
}
|
||||
|
||||
@@ -453,25 +453,6 @@ async function ensureKernelAvailable(
|
||||
}
|
||||
}
|
||||
|
||||
export async function warmPythonEnvironment(
|
||||
cwd: string,
|
||||
sessionId?: string,
|
||||
_useSharedGateway?: boolean,
|
||||
_sessionFile?: string,
|
||||
kernelOwnerId?: string,
|
||||
signal?: AbortSignal,
|
||||
): Promise<{ ok: boolean; reason?: string }> {
|
||||
const resolvedSessionId = sessionId ?? `session:${cwd}`;
|
||||
try {
|
||||
await logger.time("warmPython:ensureKernelAvailable", ensureKernelAvailable, cwd, { signal });
|
||||
} catch (err: unknown) {
|
||||
const reason = err instanceof Error ? err.message : String(err);
|
||||
return { ok: false, reason };
|
||||
}
|
||||
attachKernelOwner(resolvedSessionId, kernelOwnerId);
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
function isResourceExhaustionError(error: unknown): boolean {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
return (
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
import type { ToolSession } from "../../tools";
|
||||
import type {
|
||||
ExecutorBackend,
|
||||
ExecutorBackendCallOptions,
|
||||
ExecutorBackendExecOptions,
|
||||
ExecutorBackendResult,
|
||||
} from "../backend";
|
||||
import { executePython, type PythonExecutorOptions, warmPythonEnvironment } from "./executor";
|
||||
import type { ExecutorBackend, ExecutorBackendExecOptions, ExecutorBackendResult } from "../backend";
|
||||
import { executePython, type PythonExecutorOptions } from "./executor";
|
||||
import { checkPythonKernelAvailability } from "./kernel";
|
||||
|
||||
const PYTHON_SESSION_PREFIX = "python:";
|
||||
@@ -29,19 +24,6 @@ export default {
|
||||
return availability.ok;
|
||||
},
|
||||
|
||||
async warm(opts: ExecutorBackendCallOptions): Promise<{ ok: boolean; reason?: string }> {
|
||||
const useSharedGateway = readSetting<boolean>(opts.session, "python.sharedGateway");
|
||||
const result = await warmPythonEnvironment(
|
||||
opts.cwd,
|
||||
namespaceSessionId(opts.sessionId),
|
||||
useSharedGateway,
|
||||
opts.sessionFile,
|
||||
opts.kernelOwnerId,
|
||||
opts.signal,
|
||||
);
|
||||
return { ok: result.ok, reason: result.reason };
|
||||
},
|
||||
|
||||
async execute(code: string, opts: ExecutorBackendExecOptions): Promise<ExecutorBackendResult> {
|
||||
const useSharedGateway = readSetting<boolean>(opts.session, "python.sharedGateway");
|
||||
const kernelMode = readSetting<PythonExecutorOptions["kernelMode"]>(opts.session, "python.kernelMode");
|
||||
|
||||
@@ -242,6 +242,11 @@ async function getChangelogForDisplay(parsed: Args): Promise<string | undefined>
|
||||
}
|
||||
|
||||
const lastVersion = settings.get("lastChangelogVersion");
|
||||
if (lastVersion === VERSION) {
|
||||
// Steady state: user already saw the current version's changelog. Skip the file read + parse.
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const changelogPath = getChangelogPath();
|
||||
const entries = await parseChangelog(changelogPath);
|
||||
|
||||
@@ -627,7 +632,7 @@ export async function runRootCommand(parsed: Args, rawArgs: string[]): Promise<v
|
||||
pluginPreloadPromise.catch(() => {});
|
||||
|
||||
const cwd = getProjectDir();
|
||||
await logger.time("settings:init", Settings.init, { cwd });
|
||||
const settingsInstance = await logger.time("settings:init", Settings.init, { cwd });
|
||||
if (parsedArgs.mode === "rpc") {
|
||||
applyRpcDefaultSettingOverrides();
|
||||
}
|
||||
@@ -659,7 +664,6 @@ export async function runRootCommand(parsed: Args, rawArgs: string[]): Promise<v
|
||||
|
||||
// Initialize discovery system with settings for provider persistence
|
||||
logger.time("initializeWithSettings", initializeWithSettings, settings);
|
||||
modelRegistry.refreshInBackground();
|
||||
|
||||
// Apply model role overrides from CLI args or env vars (ephemeral, not persisted)
|
||||
const smolModel = parsedArgs.smol ?? $env.PI_SMOL_MODEL;
|
||||
@@ -763,6 +767,7 @@ export async function runRootCommand(parsed: Args, rawArgs: string[]): Promise<v
|
||||
sessionOptions.authStorage = authStorage;
|
||||
sessionOptions.modelRegistry = modelRegistry;
|
||||
sessionOptions.hasUI = isInteractive;
|
||||
sessionOptions.settings = settingsInstance;
|
||||
|
||||
// Handle CLI --api-key as runtime override (not persisted)
|
||||
if (parsedArgs.apiKey) {
|
||||
@@ -782,6 +787,10 @@ export async function runRootCommand(parsed: Args, rawArgs: string[]): Promise<v
|
||||
createAgentSession,
|
||||
sessionOptions,
|
||||
);
|
||||
// Kick off background model discovery only after createAgentSession finishes its parallel
|
||||
// discovery arms; running these concurrently contends for the event loop and stretches
|
||||
// every parallel arm by ~30ms.
|
||||
modelRegistry.refreshInBackground();
|
||||
if (parsedArgs.apiKey && !sessionOptions.model && session.model) {
|
||||
authStorage.setRuntimeApiKey(session.model.provider, parsedArgs.apiKey);
|
||||
}
|
||||
|
||||
@@ -206,9 +206,6 @@ export interface CreateAgentSessionOptions {
|
||||
enableLsp?: boolean;
|
||||
/** Skip Python kernel availability check and prelude warmup */
|
||||
skipPythonPreflight?: boolean;
|
||||
/** Force Python prelude warmup even when test env would normally skip it */
|
||||
forcePythonWarmup?: boolean;
|
||||
|
||||
/** Tool names explicitly requested (enables disabled-by-default tools) */
|
||||
toolNames?: string[];
|
||||
|
||||
@@ -956,7 +953,6 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
||||
return !requestedToolNames || requestedToolNames.includes("edit");
|
||||
},
|
||||
skipPythonPreflight: options.skipPythonPreflight,
|
||||
forcePythonWarmup: options.forcePythonWarmup,
|
||||
contextFiles,
|
||||
skills,
|
||||
eventBus,
|
||||
|
||||
@@ -2,10 +2,9 @@
|
||||
* System prompt construction and project context loading
|
||||
*/
|
||||
|
||||
import * as fs from "node:fs";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import type { AgentTool } from "@oh-my-pi/pi-agent-core";
|
||||
import { FileType, glob } from "@oh-my-pi/pi-natives";
|
||||
import { $env, getGpuCachePath, getProjectDir, hasFsCode, isEnoent, logger, prompt } from "@oh-my-pi/pi-utils";
|
||||
import { $ } from "bun";
|
||||
import { contextFileCapability } from "./capability/context-file";
|
||||
@@ -96,68 +95,31 @@ export interface AgentsMdSearch {
|
||||
files: string[];
|
||||
}
|
||||
|
||||
function normalizePath(value: string): string {
|
||||
return value.replace(/\\/g, "/");
|
||||
}
|
||||
|
||||
function shouldSkipAgentsDir(name: string): boolean {
|
||||
if (AGENTS_MD_EXCLUDED_DIRS.has(name)) return true;
|
||||
return name.startsWith(".");
|
||||
}
|
||||
|
||||
async function collectAgentsMdFiles(
|
||||
root: string,
|
||||
dir: string,
|
||||
depth: number,
|
||||
limit: number,
|
||||
discovered: Set<string>,
|
||||
): Promise<void> {
|
||||
if (depth > AGENTS_MD_MAX_DEPTH || discovered.size >= limit) {
|
||||
return;
|
||||
}
|
||||
|
||||
let entries: fs.Dirent[];
|
||||
try {
|
||||
entries = await fs.promises.readdir(dir, { withFileTypes: true });
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
if (depth >= AGENTS_MD_MIN_DEPTH) {
|
||||
const hasAgentsMd = entries.some(entry => entry.isFile() && entry.name === "AGENTS.md");
|
||||
if (hasAgentsMd) {
|
||||
const relPath = normalizePath(path.relative(root, path.join(dir, "AGENTS.md")));
|
||||
if (relPath.length > 0) {
|
||||
discovered.add(relPath);
|
||||
}
|
||||
if (discovered.size >= limit) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (depth === AGENTS_MD_MAX_DEPTH) {
|
||||
return;
|
||||
}
|
||||
|
||||
const childDirs = entries
|
||||
.filter(entry => entry.isDirectory() && !shouldSkipAgentsDir(entry.name))
|
||||
.map(entry => entry.name)
|
||||
.sort();
|
||||
|
||||
await Promise.all(
|
||||
childDirs.map(async child => {
|
||||
if (discovered.size >= limit) return;
|
||||
await collectAgentsMdFiles(root, path.join(dir, child), depth + 1, limit, discovered);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async function listAgentsMdFiles(root: string, limit: number): Promise<string[]> {
|
||||
try {
|
||||
const discovered = new Set<string>();
|
||||
await collectAgentsMdFiles(root, root, 0, limit, discovered);
|
||||
return Array.from(discovered).sort().slice(0, limit);
|
||||
const result = await glob({
|
||||
pattern: "**/AGENTS.md",
|
||||
path: root,
|
||||
fileType: FileType.File,
|
||||
recursive: true,
|
||||
hidden: false,
|
||||
gitignore: true,
|
||||
maxResults: limit * 4,
|
||||
cache: true,
|
||||
});
|
||||
const files: string[] = [];
|
||||
for (const m of result.matches) {
|
||||
const rel = m.path.replace(/\\/g, "/");
|
||||
if (!rel?.endsWith("AGENTS.md")) continue;
|
||||
const segments = rel.split("/");
|
||||
const depth = segments.length - 1;
|
||||
if (depth < AGENTS_MD_MIN_DEPTH || depth > AGENTS_MD_MAX_DEPTH) continue;
|
||||
const dirSegments = segments.slice(0, -1);
|
||||
if (dirSegments.some(seg => AGENTS_MD_EXCLUDED_DIRS.has(seg) || seg.startsWith("."))) continue;
|
||||
files.push(rel);
|
||||
if (files.length >= limit) break;
|
||||
}
|
||||
return Array.from(new Set(files)).sort().slice(0, limit);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -348,8 +348,6 @@ export class EvalTool implements AgentTool<typeof evalSchema> {
|
||||
});
|
||||
const sessionId = sessionFile ? `session:${sessionFile}:cwd:${session.cwd}` : `cwd:${session.cwd}`;
|
||||
|
||||
const warmedBackends = new Set<EvalLanguage>();
|
||||
|
||||
for (let i = 0; i < cells.length; i++) {
|
||||
const cell = cells[i];
|
||||
const backend = cell.resolved.backend;
|
||||
@@ -360,23 +358,6 @@ export class EvalTool implements AgentTool<typeof evalSchema> {
|
||||
? AbortSignal.any([signal, timeoutSignal, sessionAbortController.signal])
|
||||
: AbortSignal.any([timeoutSignal, sessionAbortController.signal]);
|
||||
|
||||
if (!warmedBackends.has(backend.id) && backend.warm) {
|
||||
const warmup = await backend.warm({
|
||||
cwd: session.cwd,
|
||||
sessionId,
|
||||
sessionFile: sessionFile ?? undefined,
|
||||
kernelOwnerId,
|
||||
signal: combinedSignal,
|
||||
session,
|
||||
});
|
||||
if (!warmup.ok) {
|
||||
if (combinedSignal.aborted) throw new ToolAbortError();
|
||||
throw new ToolError(warmup.reason ?? `${backend.label} prelude helpers unavailable`);
|
||||
}
|
||||
session.assertEvalExecutionAllowed?.();
|
||||
}
|
||||
warmedBackends.add(backend.id);
|
||||
|
||||
const cellResult = cellResults[i];
|
||||
cellResult.status = "running";
|
||||
cellResult.output = "";
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import type { AgentTool } from "@oh-my-pi/pi-agent-core";
|
||||
import type { ToolChoice } from "@oh-my-pi/pi-ai";
|
||||
import { $env, $flag, isBunTestRuntime, logger } from "@oh-my-pi/pi-utils";
|
||||
import { $env, $flag, logger } from "@oh-my-pi/pi-utils";
|
||||
import type { AsyncJobManager } from "../async";
|
||||
import type { PromptTemplate } from "../config/prompt-templates";
|
||||
import type { Settings } from "../config/settings";
|
||||
import { EditTool } from "../edit";
|
||||
import { warmPythonEnvironment } from "../eval/py/executor";
|
||||
import { checkPythonKernelAvailability } from "../eval/py/kernel";
|
||||
import type { Skill } from "../extensibility/skills";
|
||||
import type { InternalUrlRouter } from "../internal-urls";
|
||||
@@ -108,8 +107,6 @@ export interface ToolSession {
|
||||
hasUI: boolean;
|
||||
/** Skip Python kernel availability check and warmup */
|
||||
skipPythonPreflight?: boolean;
|
||||
/** Force Python prelude warmup even when test env would normally skip it */
|
||||
forcePythonWarmup?: boolean;
|
||||
/** Pre-loaded context files (AGENTS.md, etc) */
|
||||
contextFiles?: ContextFileEntry[];
|
||||
/** Pre-loaded skills */
|
||||
@@ -297,54 +294,23 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
|
||||
const allowPython = backends.python;
|
||||
const allowJs = backends.js;
|
||||
const skipPythonPreflight = session.skipPythonPreflight === true;
|
||||
// Eval tool is enabled if EITHER backend is reachable. We only need to know
|
||||
// whether python is reachable when JS is disabled — otherwise allowEval is
|
||||
// already true and the python-availability check can be deferred to first
|
||||
// invocation of the python backend (already handled inside the executor).
|
||||
let pythonAvailable = true;
|
||||
const shouldCheckPython =
|
||||
!skipPythonPreflight && allowPython && (requestedTools === undefined || requestedTools.includes("eval"));
|
||||
const isTestEnv = isBunTestRuntime();
|
||||
const forcePythonWarmup = session.forcePythonWarmup === true;
|
||||
const skipPythonWarm = (isTestEnv && !forcePythonWarmup) || $flag("PI_PYTHON_SKIP_CHECK");
|
||||
if (shouldCheckPython) {
|
||||
if (
|
||||
!skipPythonPreflight &&
|
||||
allowPython &&
|
||||
!allowJs &&
|
||||
(requestedTools === undefined || requestedTools.includes("eval"))
|
||||
) {
|
||||
const availability = await logger.time("createTools:pythonCheck", checkPythonKernelAvailability, session.cwd);
|
||||
pythonAvailable = availability.ok;
|
||||
if (!availability.ok) {
|
||||
logger.warn("Python kernel unavailable; eval will dispatch to JavaScript backend", {
|
||||
logger.warn("Python kernel unavailable and JS backend disabled; eval will be unavailable", {
|
||||
reason: availability.reason,
|
||||
});
|
||||
} else if (!skipPythonWarm) {
|
||||
const sessionFile = session.getSessionFile?.() ?? undefined;
|
||||
const kernelOwnerId = session.getEvalKernelOwnerId?.() ?? undefined;
|
||||
const warmSessionId = sessionFile ? `session:${sessionFile}:cwd:${session.cwd}` : `cwd:${session.cwd}`;
|
||||
const warmupAbortController = new AbortController();
|
||||
try {
|
||||
session.assertEvalExecutionAllowed?.();
|
||||
|
||||
const warmupExecution = session.trackEvalExecution
|
||||
? logger.time(
|
||||
"createTools:warmPython",
|
||||
warmPythonEnvironment,
|
||||
session.cwd,
|
||||
warmSessionId,
|
||||
session.settings.get("python.sharedGateway"),
|
||||
sessionFile,
|
||||
kernelOwnerId,
|
||||
warmupAbortController.signal,
|
||||
)
|
||||
: logger.time(
|
||||
"createTools:warmPython",
|
||||
warmPythonEnvironment,
|
||||
session.cwd,
|
||||
warmSessionId,
|
||||
session.settings.get("python.sharedGateway"),
|
||||
sessionFile,
|
||||
kernelOwnerId,
|
||||
);
|
||||
await (session.trackEvalExecution?.(warmupExecution, warmupAbortController) ?? warmupExecution);
|
||||
session.assertEvalExecutionAllowed?.();
|
||||
} catch (err) {
|
||||
logger.warn("Failed to warm Python environment", {
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,21 +10,19 @@ interface PackageJsonInfo {
|
||||
}
|
||||
|
||||
async function resolvePackageRunner(cwd: string): Promise<string> {
|
||||
if ((await isFile(path.join(cwd, "bun.lock"))) || (await isFile(path.join(cwd, "bun.lockb")))) {
|
||||
return "bun run";
|
||||
}
|
||||
if (await isFile(path.join(cwd, "pnpm-lock.yaml"))) {
|
||||
return "pnpm run";
|
||||
}
|
||||
if (await isFile(path.join(cwd, "yarn.lock"))) {
|
||||
return "yarn";
|
||||
}
|
||||
if ((await isFile(path.join(cwd, "package-lock.json"))) || (await isFile(path.join(cwd, "npm-shrinkwrap.json")))) {
|
||||
return "npm run";
|
||||
}
|
||||
if ($which("bun")) {
|
||||
return "bun run";
|
||||
}
|
||||
const [bunLock, bunLockb, pnpmLock, yarnLock, npmLock, npmShrink] = await Promise.all([
|
||||
isFile(path.join(cwd, "bun.lock")),
|
||||
isFile(path.join(cwd, "bun.lockb")),
|
||||
isFile(path.join(cwd, "pnpm-lock.yaml")),
|
||||
isFile(path.join(cwd, "yarn.lock")),
|
||||
isFile(path.join(cwd, "package-lock.json")),
|
||||
isFile(path.join(cwd, "npm-shrinkwrap.json")),
|
||||
]);
|
||||
if (bunLock || bunLockb) return "bun run";
|
||||
if (pnpmLock) return "pnpm run";
|
||||
if (yarnLock) return "yarn";
|
||||
if (npmLock || npmShrink) return "npm run";
|
||||
if ($which("bun")) return "bun run";
|
||||
return "npm run";
|
||||
}
|
||||
|
||||
@@ -86,18 +84,23 @@ async function readPackageJson(filePath: string): Promise<PackageJsonInfo | null
|
||||
async function findWorkspacePackageJsons(cwd: string, patterns: string[]): Promise<string[]> {
|
||||
const includePatterns = patterns.filter(pattern => !pattern.startsWith("!")).map(normalizeWorkspacePattern);
|
||||
const excludePatterns = patterns.filter(pattern => pattern.startsWith("!")).map(normalizeWorkspacePattern);
|
||||
const excluded = new Set<string>();
|
||||
for (const pattern of excludePatterns) {
|
||||
for await (const entry of new Bun.Glob(pattern.slice(1)).scan({ cwd, onlyFiles: true })) {
|
||||
excluded.add(path.normalize(String(entry)));
|
||||
}
|
||||
}
|
||||
const files = new Set<string>();
|
||||
for (const pattern of includePatterns) {
|
||||
|
||||
const collect = async (pattern: string): Promise<string[]> => {
|
||||
const out: string[] = [];
|
||||
for await (const entry of new Bun.Glob(pattern).scan({ cwd, onlyFiles: true })) {
|
||||
const normalized = path.normalize(String(entry));
|
||||
if (normalized !== "package.json" && !excluded.has(normalized)) files.add(normalized);
|
||||
out.push(path.normalize(String(entry)));
|
||||
}
|
||||
return out;
|
||||
};
|
||||
|
||||
const [excludedLists, includedLists] = await Promise.all([
|
||||
Promise.all(excludePatterns.map(pattern => collect(pattern.slice(1)))),
|
||||
Promise.all(includePatterns.map(pattern => collect(pattern))),
|
||||
]);
|
||||
const excluded = new Set<string>(excludedLists.flat());
|
||||
const files = new Set<string>();
|
||||
for (const entry of includedLists.flat()) {
|
||||
if (entry !== "package.json" && !excluded.has(entry)) files.add(entry);
|
||||
}
|
||||
return [...files].sort((left, right) => left.localeCompare(right));
|
||||
}
|
||||
@@ -132,10 +135,10 @@ async function readPackageTasks(cwd: string): Promise<RunnerTask[] | null> {
|
||||
);
|
||||
}
|
||||
|
||||
for (const packageJsonPath of workspacePackageJsons) {
|
||||
const pkg = await readPackageJson(path.join(cwd, packageJsonPath));
|
||||
if (!pkg || pkg.scripts.length === 0) continue;
|
||||
const packageDir = path.dirname(packageJsonPath);
|
||||
const pkgs = await Promise.all(workspacePackageJsons.map(p => readPackageJson(path.join(cwd, p))));
|
||||
pkgs.forEach((pkg, index) => {
|
||||
if (!pkg || pkg.scripts.length === 0) return;
|
||||
const packageDir = path.dirname(workspacePackageJsons[index]);
|
||||
tasks.push(
|
||||
...tasksForPackage({
|
||||
pkg,
|
||||
@@ -143,7 +146,7 @@ async function readPackageTasks(cwd: string): Promise<RunnerTask[] | null> {
|
||||
namespaced: true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return tasks.length > 0 ? tasks : null;
|
||||
}
|
||||
@@ -153,8 +156,7 @@ export const pkgRunner: TaskRunner = {
|
||||
label: "Pkg",
|
||||
async detect(cwd: string): Promise<DetectedRunner | null> {
|
||||
try {
|
||||
const commandPrefix = await resolvePackageRunner(cwd);
|
||||
const tasks = await readPackageTasks(cwd);
|
||||
const [commandPrefix, tasks] = await Promise.all([resolvePackageRunner(cwd), readPackageTasks(cwd)]);
|
||||
if (!tasks || tasks.length === 0) return null;
|
||||
return { id: "pkg", label: "Pkg", commandPrefix, tasks };
|
||||
} catch (err) {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { untilAborted } from "@oh-my-pi/pi-utils";
|
||||
import type { StreamInfo } from "markit-ai";
|
||||
import { Markit } from "markit-ai";
|
||||
import type { Markit, StreamInfo } from "markit-ai";
|
||||
import { ToolAbortError } from "../tools/tool-errors";
|
||||
|
||||
export interface MarkitConversionResult {
|
||||
@@ -9,7 +8,15 @@ export interface MarkitConversionResult {
|
||||
error?: string;
|
||||
}
|
||||
|
||||
const markit = new Markit();
|
||||
let markit: () => Markit | Promise<Markit> = async () => {
|
||||
const promise = import("markit-ai").then(({ Markit }) => {
|
||||
const instance = new Markit();
|
||||
markit = () => instance;
|
||||
return instance;
|
||||
});
|
||||
markit = () => promise;
|
||||
return promise;
|
||||
};
|
||||
|
||||
function normalizeExtension(extension: string): string {
|
||||
const trimmed = extension.trim().toLowerCase();
|
||||
@@ -24,9 +31,10 @@ function normalizeError(error: unknown): string {
|
||||
return "Conversion failed";
|
||||
}
|
||||
|
||||
async function runMarkitConversion<T>(task: () => Promise<T>, signal?: AbortSignal): Promise<T> {
|
||||
async function runMarkitConversion<T>(task: (markit: Markit) => Promise<T>, signal?: AbortSignal): Promise<T> {
|
||||
try {
|
||||
return signal ? await untilAborted(signal, task) : await task();
|
||||
const instance = await markit();
|
||||
return signal ? await untilAborted(signal, () => task(instance)) : await task(instance);
|
||||
} catch (error) {
|
||||
if (error instanceof ToolAbortError) {
|
||||
throw error;
|
||||
@@ -48,7 +56,7 @@ function finalizeConversion(markdown?: string): MarkitConversionResult {
|
||||
|
||||
export async function convertFileWithMarkit(filePath: string, signal?: AbortSignal): Promise<MarkitConversionResult> {
|
||||
try {
|
||||
const result = await runMarkitConversion(() => markit.convertFile(filePath), signal);
|
||||
const result = await runMarkitConversion(markit => markit.convertFile(filePath), signal);
|
||||
return finalizeConversion(result.markdown);
|
||||
} catch (error) {
|
||||
if (error instanceof ToolAbortError) {
|
||||
@@ -70,7 +78,7 @@ export async function convertBufferWithMarkit(
|
||||
};
|
||||
|
||||
try {
|
||||
const result = await runMarkitConversion(() => markit.convert(Buffer.from(buffer), streamInfo), signal);
|
||||
const result = await runMarkitConversion(markit => markit.convert(Buffer.from(buffer), streamInfo), signal);
|
||||
return finalizeConversion(result.markdown);
|
||||
} catch (error) {
|
||||
if (error instanceof ToolAbortError) {
|
||||
|
||||
@@ -95,18 +95,15 @@ const createSession = async (
|
||||
slashCommands: [],
|
||||
enableMCP: false,
|
||||
enableLsp: false,
|
||||
forcePythonWarmup: true,
|
||||
toolNames: ["eval"],
|
||||
})
|
||||
).session;
|
||||
|
||||
const stubPythonWarmup = () => vi.spyOn(pythonExecutor, "warmPythonEnvironment").mockResolvedValue({ ok: true });
|
||||
|
||||
const createWarmupKernel = () => {
|
||||
const createMockKernel = () => {
|
||||
let alive = true;
|
||||
return {
|
||||
execute: vi.fn(async () => {
|
||||
if (!alive) throw new Error("Expected warmup kernel to be restarted after shutdown");
|
||||
if (!alive) throw new Error("Expected mock kernel to be restarted after shutdown");
|
||||
return OK_EXECUTION;
|
||||
}),
|
||||
ping: vi.fn(async () => alive),
|
||||
@@ -132,7 +129,7 @@ describe("AgentSession python cleanup", () => {
|
||||
it("does not dispose unrelated Python owners when createAgentSession fails before session construction", async () => {
|
||||
const { tempDir, cwd } = createTempProject();
|
||||
tempDirs.push(tempDir);
|
||||
const unrelatedKernel = createWarmupKernel();
|
||||
const unrelatedKernel = createMockKernel();
|
||||
const unrelatedCwd = path.join(tempDir, "unrelated-before");
|
||||
const throwingExtension: ExtensionFactory = () => {
|
||||
throw new Error("Extension init failed");
|
||||
@@ -164,7 +161,6 @@ describe("AgentSession python cleanup", () => {
|
||||
slashCommands: [],
|
||||
enableMCP: false,
|
||||
enableLsp: false,
|
||||
forcePythonWarmup: true,
|
||||
toolNames: ["eval"],
|
||||
}),
|
||||
).rejects.toThrow("Extension init failed");
|
||||
@@ -172,7 +168,7 @@ describe("AgentSession python cleanup", () => {
|
||||
expect(startSpy).toHaveBeenCalledTimes(1);
|
||||
expect(unrelatedKernel.shutdown).not.toHaveBeenCalled();
|
||||
|
||||
const replacementKernel = createWarmupKernel();
|
||||
const replacementKernel = createMockKernel();
|
||||
startSpy.mockResolvedValueOnce(replacementKernel as unknown as PythonKernelInstance);
|
||||
await pythonExecutor.executePython("print('fresh warmup before')", {
|
||||
cwd,
|
||||
@@ -198,7 +194,7 @@ describe("AgentSession python cleanup", () => {
|
||||
it("does not dispose unrelated Python owners when createAgentSession fails after session construction", async () => {
|
||||
const { tempDir, cwd } = createTempProject();
|
||||
tempDirs.push(tempDir);
|
||||
const unrelatedKernel = createWarmupKernel();
|
||||
const unrelatedKernel = createMockKernel();
|
||||
const unrelatedCwd = path.join(tempDir, "unrelated-after");
|
||||
vi.spyOn(pythonKernel, "checkPythonKernelAvailability").mockResolvedValue({ ok: true });
|
||||
const startSpy = vi
|
||||
@@ -229,7 +225,6 @@ describe("AgentSession python cleanup", () => {
|
||||
slashCommands: [],
|
||||
enableMCP: false,
|
||||
enableLsp: false,
|
||||
forcePythonWarmup: true,
|
||||
toolNames: ["eval"],
|
||||
}),
|
||||
).rejects.toThrow("Memory startup failed");
|
||||
@@ -237,7 +232,7 @@ describe("AgentSession python cleanup", () => {
|
||||
expect(startSpy).toHaveBeenCalledTimes(1);
|
||||
expect(unrelatedKernel.shutdown).not.toHaveBeenCalled();
|
||||
|
||||
const replacementKernel = createWarmupKernel();
|
||||
const replacementKernel = createMockKernel();
|
||||
startSpy.mockResolvedValueOnce(replacementKernel as unknown as PythonKernelInstance);
|
||||
await pythonExecutor.executePython("print('fresh warmup after')", {
|
||||
cwd,
|
||||
@@ -263,8 +258,6 @@ describe("AgentSession python cleanup", () => {
|
||||
it("waits for active SDK session Python work before releasing a shared retained kernel", async () => {
|
||||
const { tempDir, cwd } = createTempProject();
|
||||
tempDirs.push(tempDir);
|
||||
stubPythonWarmup();
|
||||
|
||||
const kernel = new FakeKernel();
|
||||
const blockedExecution = Promise.withResolvers<typeof OK_EXECUTION>();
|
||||
const blockedExecutionStarted = Promise.withResolvers<void>();
|
||||
@@ -336,128 +329,9 @@ describe("AgentSession python cleanup", () => {
|
||||
|
||||
expect(kernel.shutdownCalls).toBe(1);
|
||||
});
|
||||
|
||||
it("aborts tracked eval warmup during session dispose before executePython starts", async () => {
|
||||
const { tempDir, cwd } = createTempProject();
|
||||
tempDirs.push(tempDir);
|
||||
const blockedWarmupStarted = Promise.withResolvers<void>();
|
||||
const executeSpy = vi.spyOn(pythonExecutor, "executePython").mockResolvedValue({
|
||||
output: "tool ok",
|
||||
exitCode: 0,
|
||||
cancelled: false,
|
||||
truncated: false,
|
||||
totalLines: 1,
|
||||
totalBytes: 7,
|
||||
outputLines: 1,
|
||||
outputBytes: 7,
|
||||
displayOutputs: [],
|
||||
stdinRequested: false,
|
||||
});
|
||||
let warmupCallCount = 0;
|
||||
const warmupSpy = vi
|
||||
.spyOn(pythonExecutor, "warmPythonEnvironment")
|
||||
.mockImplementation(async (_cwd, _sessionId, _useSharedGateway, _sessionFile, _kernelOwnerId, signal) => {
|
||||
warmupCallCount += 1;
|
||||
if (warmupCallCount === 1) {
|
||||
return { ok: true };
|
||||
}
|
||||
blockedWarmupStarted.resolve();
|
||||
return await new Promise<{ ok: boolean; reason?: string }>(resolve => {
|
||||
const onAbort = () => resolve({ ok: false, reason: "Warmup aborted" });
|
||||
if (signal?.aborted) {
|
||||
onAbort();
|
||||
return;
|
||||
}
|
||||
signal?.addEventListener("abort", onAbort, { once: true });
|
||||
});
|
||||
});
|
||||
vi.spyOn(pythonKernel, "checkPythonKernelAvailability").mockResolvedValue({ ok: true });
|
||||
|
||||
const session = await createSession(tempDir, cwd);
|
||||
const EvalTool = session.getToolByName("eval");
|
||||
expect(EvalTool).toBeDefined();
|
||||
let toolExecutionSettled = false;
|
||||
const toolExecution = EvalTool!
|
||||
.execute("call-id", { input: "```py\nprint('tool')\n```" }, undefined, undefined, undefined)
|
||||
.finally(() => {
|
||||
toolExecutionSettled = true;
|
||||
});
|
||||
await blockedWarmupStarted.promise;
|
||||
|
||||
let disposed = false;
|
||||
const disposeSession = session.dispose().then(() => {
|
||||
disposed = true;
|
||||
});
|
||||
await Bun.sleep(0);
|
||||
|
||||
expect(disposed).toBe(false);
|
||||
expect(toolExecutionSettled).toBe(false);
|
||||
expect(warmupSpy).toHaveBeenCalledTimes(2);
|
||||
expect(executeSpy).not.toHaveBeenCalled();
|
||||
|
||||
await expect(toolExecution).rejects.toThrow("Operation aborted");
|
||||
await disposeSession;
|
||||
|
||||
expect(disposed).toBe(true);
|
||||
expect(toolExecutionSettled).toBe(true);
|
||||
expect(executeSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("rejects eval starts when warmup finishes after dispose begins", async () => {
|
||||
const { tempDir, cwd } = createTempProject();
|
||||
tempDirs.push(tempDir);
|
||||
const blockedWarmupStarted = Promise.withResolvers<void>();
|
||||
const releaseWarmup = Promise.withResolvers<void>();
|
||||
const executeSpy = vi.spyOn(pythonExecutor, "executePython").mockResolvedValue({
|
||||
output: "late",
|
||||
exitCode: 0,
|
||||
cancelled: false,
|
||||
truncated: false,
|
||||
totalLines: 1,
|
||||
totalBytes: 4,
|
||||
outputLines: 1,
|
||||
outputBytes: 4,
|
||||
displayOutputs: [],
|
||||
stdinRequested: false,
|
||||
});
|
||||
let warmupCallCount = 0;
|
||||
const warmupSpy = vi.spyOn(pythonExecutor, "warmPythonEnvironment").mockImplementation(async () => {
|
||||
warmupCallCount += 1;
|
||||
if (warmupCallCount === 1) {
|
||||
return { ok: true };
|
||||
}
|
||||
blockedWarmupStarted.resolve();
|
||||
await releaseWarmup.promise;
|
||||
return { ok: true };
|
||||
});
|
||||
vi.spyOn(pythonKernel, "checkPythonKernelAvailability").mockResolvedValue({ ok: true });
|
||||
|
||||
const session = await createSession(tempDir, cwd);
|
||||
const EvalTool = session.getToolByName("eval");
|
||||
expect(EvalTool).toBeDefined();
|
||||
const toolExecution = EvalTool!.execute(
|
||||
"call-id",
|
||||
{ input: "```py\nprint('tool after warmup')\n```" },
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
);
|
||||
await blockedWarmupStarted.promise;
|
||||
const disposeSession = session.dispose();
|
||||
releaseWarmup.resolve();
|
||||
await expect(toolExecution).rejects.toThrow(
|
||||
"Python execution is unavailable while session disposal is in progress",
|
||||
);
|
||||
await disposeSession;
|
||||
expect(warmupSpy).toHaveBeenCalledTimes(2);
|
||||
expect(executeSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
it("aborts tracked eval execution during session dispose after warmup completes", async () => {
|
||||
const { tempDir, cwd } = createTempProject();
|
||||
tempDirs.push(tempDir);
|
||||
const warmupSpy = vi.spyOn(pythonExecutor, "warmPythonEnvironment").mockImplementation(async () => {
|
||||
return { ok: true };
|
||||
});
|
||||
const blockedExecuteStarted = Promise.withResolvers<void>();
|
||||
const executeSpy = vi.spyOn(pythonExecutor, "executePython").mockImplementation(async (_code, options) => {
|
||||
const signal = options?.signal;
|
||||
@@ -507,14 +381,12 @@ describe("AgentSession python cleanup", () => {
|
||||
|
||||
expect(disposed).toBe(false);
|
||||
expect(toolExecutionSettled).toBe(false);
|
||||
expect(warmupSpy).toHaveBeenCalledTimes(2);
|
||||
expect(executeSpy).toHaveBeenCalledTimes(1);
|
||||
|
||||
const [toolResult] = await Promise.all([toolExecution, disposeSession]);
|
||||
|
||||
expect(disposed).toBe(true);
|
||||
expect(toolExecutionSettled).toBe(true);
|
||||
expect(warmupSpy).toHaveBeenCalledTimes(2);
|
||||
expect(executeSpy).toHaveBeenCalledTimes(1);
|
||||
expect(toolResult.details?.isError).toBe(true);
|
||||
expect(toolResult.content).toContainEqual(
|
||||
@@ -525,8 +397,6 @@ describe("AgentSession python cleanup", () => {
|
||||
it("detaches retained kernel ownership even when dispose times out waiting for Python work", async () => {
|
||||
const { tempDir, cwd } = createTempProject();
|
||||
tempDirs.push(tempDir);
|
||||
stubPythonWarmup();
|
||||
|
||||
const kernel = new FakeKernel();
|
||||
const blockedExecution = Promise.withResolvers<typeof OK_EXECUTION>();
|
||||
const blockedExecutionStarted = Promise.withResolvers<void>();
|
||||
@@ -583,7 +453,6 @@ describe("AgentSession python cleanup", () => {
|
||||
it("rejects direct session Python starts once dispose begins", async () => {
|
||||
const { tempDir, cwd } = createTempProject();
|
||||
tempDirs.push(tempDir);
|
||||
stubPythonWarmup();
|
||||
const executeSpy = vi.spyOn(pythonExecutor, "executePython").mockResolvedValue({
|
||||
output: "late",
|
||||
exitCode: 0,
|
||||
@@ -705,7 +574,6 @@ describe("AgentSession python cleanup", () => {
|
||||
it("rejects eval starts once dispose begins", async () => {
|
||||
const { tempDir, cwd } = createTempProject();
|
||||
tempDirs.push(tempDir);
|
||||
const warmupSpy = stubPythonWarmup();
|
||||
const executeSpy = vi.spyOn(pythonExecutor, "executePython").mockResolvedValue({
|
||||
output: "late",
|
||||
exitCode: 0,
|
||||
@@ -724,23 +592,15 @@ describe("AgentSession python cleanup", () => {
|
||||
expect(EvalTool).toBeDefined();
|
||||
const disposeSession = session.dispose();
|
||||
await expect(
|
||||
EvalTool!.execute(
|
||||
"call-id",
|
||||
{ input: "```py\nprint('late')\n```" },
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
),
|
||||
EvalTool!.execute("call-id", { input: "```py\nprint('late')\n```" }, undefined, undefined, undefined),
|
||||
).rejects.toThrow("Python execution is unavailable while session disposal is in progress");
|
||||
await disposeSession;
|
||||
expect(warmupSpy).toHaveBeenCalledTimes(1);
|
||||
expect(executeSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("rejects eval starts that reach async preflight after dispose begins", async () => {
|
||||
const { tempDir, cwd } = createTempProject();
|
||||
tempDirs.push(tempDir);
|
||||
const warmupSpy = stubPythonWarmup();
|
||||
const executeSpy = vi.spyOn(pythonExecutor, "executePython").mockResolvedValue({
|
||||
output: "late",
|
||||
exitCode: 0,
|
||||
@@ -777,15 +637,12 @@ describe("AgentSession python cleanup", () => {
|
||||
releaseArtifact.resolve();
|
||||
await expect(execution).rejects.toThrow("Python execution is unavailable while session disposal is in progress");
|
||||
await disposeSession;
|
||||
expect(warmupSpy).toHaveBeenCalledTimes(1);
|
||||
expect(executeSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("aborts every active Python execution owned by the session during dispose", async () => {
|
||||
const { tempDir, cwd } = createTempProject();
|
||||
tempDirs.push(tempDir);
|
||||
stubPythonWarmup();
|
||||
|
||||
const kernel = new FakeKernel();
|
||||
const blockedExecution = Promise.withResolvers<typeof OK_EXECUTION>();
|
||||
const blockedExecutionStarted = Promise.withResolvers<void>();
|
||||
|
||||
@@ -120,13 +120,13 @@ describe("executeJs", () => {
|
||||
"const uuid = crypto.randomUUID();",
|
||||
"const digest = await webcrypto.subtle.digest('SHA-256', new TextEncoder().encode('ok'));",
|
||||
"const base = process.cwd();",
|
||||
"fs.mkdirSync(`${base}/nested`, { recursive: true });",
|
||||
"fs.writeFileSync(`${base}/nested/value.txt`, 'hello');",
|
||||
"await fs.promises.copyFile(`${base}/nested/value.txt`, `${base}/nested/copy.txt`);",
|
||||
"const text = fs.readFileSync(`${base}/nested/copy.txt`, 'utf8');",
|
||||
"const bytes = await fs.promises.readFile(`${base}/nested/copy.txt`);",
|
||||
"const stat = fs.statSync(`${base}/nested/copy.txt`);",
|
||||
"const entries = fs.readdirSync(`${base}/nested`);",
|
||||
"fs.mkdirSync(base + '/nested', { recursive: true });",
|
||||
"fs.writeFileSync(base + '/nested/value.txt', 'hello');",
|
||||
"await fs.promises.copyFile(base + '/nested/value.txt', base + '/nested/copy.txt');",
|
||||
"const text = fs.readFileSync(base + '/nested/copy.txt', 'utf8');",
|
||||
"const bytes = await fs.promises.readFile(base + '/nested/copy.txt');",
|
||||
"const stat = fs.statSync(base + '/nested/copy.txt');",
|
||||
"const entries = fs.readdirSync(base + '/nested');",
|
||||
"const start = performance.now();",
|
||||
"return {",
|
||||
" uuid: typeof uuid,",
|
||||
@@ -198,77 +198,6 @@ describe("executeJs", () => {
|
||||
expect(directoryResult.output).toContain("Directory paths are not supported");
|
||||
});
|
||||
|
||||
it("supports parse/stringify helpers and row utilities", async () => {
|
||||
const result = await executeJs(
|
||||
[
|
||||
'const csv = \'name,role,note\\nalice,admin,"hello, world"\\nbob,user,"line1\\nline2"\';',
|
||||
"const parsed = await parse.csv(csv);",
|
||||
"const tsv = stringify.tsv(rows.pick(parsed, ['name', 'role']));",
|
||||
"const roundtrip = await parse.tsv(tsv);",
|
||||
"const grouped = rows.groupBy(parsed, 'role');",
|
||||
"const counted = rows.countBy(parsed, 'role');",
|
||||
"const sorted = rows.sortBy(parsed, 'name', { reverse: true });",
|
||||
"return { parsed, roundtrip, groupedSizes: Object.fromEntries(Object.entries(grouped).map(([key, value]) => [key, value.length])), counted, sorted: rows.column(sorted, 'name') };",
|
||||
].join("\n"),
|
||||
{
|
||||
sessionId,
|
||||
session,
|
||||
sessionFile,
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.displayOutputs).toEqual([
|
||||
{
|
||||
type: "json",
|
||||
data: {
|
||||
parsed: [
|
||||
{ name: "alice", role: "admin", note: "hello, world" },
|
||||
{ name: "bob", role: "user", note: "line1\nline2" },
|
||||
],
|
||||
roundtrip: [
|
||||
{ name: "alice", role: "admin" },
|
||||
{ name: "bob", role: "user" },
|
||||
],
|
||||
groupedSizes: { admin: 1, user: 1 },
|
||||
counted: { admin: 1, user: 1 },
|
||||
sorted: ["bob", "alice"],
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("round-trips text and binary writes", async () => {
|
||||
const result = await executeJs(
|
||||
[
|
||||
"await write('plain.txt', 'hello');",
|
||||
"await write('bin.dat', new Uint8Array([0, 1, 2, 255]));",
|
||||
"await write.json('state.json', { ok: true });",
|
||||
"await write.csv('table.csv', [{ name: 'alice', role: 'admin' }]);",
|
||||
"return {",
|
||||
" text: await read.text('plain.txt'),",
|
||||
" bytes: Array.from(await read.bytes('bin.dat')),",
|
||||
" json: await read.json('state.json'),",
|
||||
" csv: await read.csv('table.csv'),",
|
||||
"};",
|
||||
].join("\n"),
|
||||
{
|
||||
sessionId,
|
||||
session,
|
||||
sessionFile,
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(getStatusEvents(result)).toHaveLength(8);
|
||||
expect(getJsonData(result)).toEqual({
|
||||
text: "hello",
|
||||
bytes: [0, 1, 2, 255],
|
||||
json: { ok: true },
|
||||
csv: [{ name: "alice", role: "admin" }],
|
||||
});
|
||||
});
|
||||
|
||||
it("routes output() through tool.read and keeps tool.* results normalized", async () => {
|
||||
const execute = vi.fn(async (_toolCallId: string, args: unknown): Promise<AgentToolResult> => {
|
||||
const record = args as { path: string };
|
||||
|
||||
@@ -3,7 +3,6 @@ import {
|
||||
disposeAllKernelSessions,
|
||||
disposeKernelSessionsByOwner,
|
||||
executePython,
|
||||
warmPythonEnvironment,
|
||||
} from "@oh-my-pi/pi-coding-agent/eval/py/executor";
|
||||
import * as gatewayCoordinator from "@oh-my-pi/pi-coding-agent/eval/py/gateway-coordinator";
|
||||
import type {
|
||||
@@ -13,7 +12,6 @@ import type {
|
||||
} from "@oh-my-pi/pi-coding-agent/eval/py/kernel";
|
||||
import * as pythonKernel from "@oh-my-pi/pi-coding-agent/eval/py/kernel";
|
||||
import { PythonKernel } from "@oh-my-pi/pi-coding-agent/eval/py/kernel";
|
||||
import { TempDir } from "@oh-my-pi/pi-utils";
|
||||
|
||||
const OK_RESULT: KernelExecuteResult = {
|
||||
status: "ok",
|
||||
@@ -775,80 +773,6 @@ describe("python executor owner cleanup", () => {
|
||||
expect(globalKernel.shutdown).toHaveBeenCalledTimes(1);
|
||||
await globalCleanup;
|
||||
});
|
||||
|
||||
it("attaches cached warmup sessions to newly provided owners", async () => {
|
||||
using tempDir = TempDir.createSync("@python-owner-warmup-");
|
||||
const kernel = {
|
||||
execute: vi.fn(async () => OK_RESULT),
|
||||
ping: vi.fn(async () => true),
|
||||
isAlive: () => true,
|
||||
shutdown: vi.fn(async (): Promise<KernelShutdownResult> => ({ confirmed: true })),
|
||||
};
|
||||
vi.spyOn(pythonKernel, "checkPythonKernelAvailability").mockResolvedValue({ ok: true });
|
||||
const startSpy = vi.spyOn(PythonKernel, "start").mockResolvedValue(kernel as unknown as PythonKernelInstance);
|
||||
|
||||
const firstWarmup = await warmPythonEnvironment(tempDir.path(), "warm-session", true, undefined, "owner-a");
|
||||
expect(firstWarmup.ok).toBe(true);
|
||||
|
||||
const cachedWarmup = await warmPythonEnvironment(tempDir.path(), "warm-session", true, undefined, "owner-b");
|
||||
expect(cachedWarmup.ok).toBe(true);
|
||||
expect(startSpy).toHaveBeenCalledTimes(1);
|
||||
|
||||
await disposeKernelSessionsByOwner("owner-a");
|
||||
expect(kernel.shutdown).not.toHaveBeenCalled();
|
||||
|
||||
await executePython("1 + 1", {
|
||||
cwd: tempDir.path(),
|
||||
sessionId: "warm-session",
|
||||
kernelMode: "session",
|
||||
kernelOwnerId: "owner-b",
|
||||
});
|
||||
|
||||
expect(startSpy).toHaveBeenCalledTimes(1);
|
||||
expect(kernel.execute).toHaveBeenCalledTimes(1);
|
||||
|
||||
await disposeKernelSessionsByOwner("owner-b");
|
||||
expect(kernel.shutdown).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("keeps cache-hit ownerless warmups provisional until an explicit owner takes over", async () => {
|
||||
using tempDir = TempDir.createSync("@python-owner-fallback-");
|
||||
const kernel = {
|
||||
execute: vi.fn(async () => OK_RESULT),
|
||||
ping: vi.fn(async () => true),
|
||||
isAlive: () => true,
|
||||
shutdown: vi.fn(async (): Promise<KernelShutdownResult> => ({ confirmed: true })),
|
||||
};
|
||||
vi.spyOn(pythonKernel, "checkPythonKernelAvailability").mockResolvedValue({ ok: true });
|
||||
const startSpy = vi.spyOn(PythonKernel, "start").mockResolvedValue(kernel as unknown as PythonKernelInstance);
|
||||
|
||||
const firstWarmup = await warmPythonEnvironment(tempDir.path(), "warm-fallback-session", true);
|
||||
expect(firstWarmup.ok).toBe(true);
|
||||
|
||||
const cachedWarmup = await warmPythonEnvironment(tempDir.path(), "warm-fallback-session", true);
|
||||
expect(cachedWarmup.ok).toBe(true);
|
||||
expect(startSpy).toHaveBeenCalledTimes(1);
|
||||
|
||||
await executePython("1 + 1", {
|
||||
cwd: tempDir.path(),
|
||||
sessionId: "warm-fallback-session",
|
||||
kernelMode: "session",
|
||||
kernelOwnerId: "owner-a",
|
||||
});
|
||||
|
||||
expect(kernel.execute).toHaveBeenCalledTimes(1);
|
||||
|
||||
const postTakeoverWarmup = await warmPythonEnvironment(tempDir.path(), "warm-fallback-session", true);
|
||||
expect(postTakeoverWarmup.ok).toBe(true);
|
||||
expect(startSpy).toHaveBeenCalledTimes(1);
|
||||
|
||||
await disposeKernelSessionsByOwner("owner-a");
|
||||
expect(kernel.shutdown).toHaveBeenCalledTimes(1);
|
||||
|
||||
await disposeKernelSessionsByOwner("warm-fallback-session");
|
||||
expect(kernel.shutdown).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("leaves per-call kernels out of owner-scoped retained cleanup and keeps global cleanup intact", async () => {
|
||||
const perCallKernel = new FakeKernel();
|
||||
const retainedKernel = new FakeKernel();
|
||||
|
||||
@@ -2,8 +2,7 @@ import { describe, expect, it } from "bun:test";
|
||||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
import { Settings } from "@oh-my-pi/pi-coding-agent/config/settings";
|
||||
import { warmPythonEnvironment } from "@oh-my-pi/pi-coding-agent/eval/py/executor";
|
||||
import { EvalTool, getEvalToolDescription } from "@oh-my-pi/pi-coding-agent/tools/eval";
|
||||
import { EvalTool } from "@oh-my-pi/pi-coding-agent/tools/eval";
|
||||
import { $which, getProjectDir } from "@oh-my-pi/pi-utils";
|
||||
|
||||
const resolvePythonPath = (): string | null => {
|
||||
@@ -40,7 +39,22 @@ const shouldRun = Boolean(pythonPath) && hasKernelDeps;
|
||||
|
||||
describe.skipIf(!shouldRun)("PYTHON_PRELUDE integration", () => {
|
||||
it("exposes prelude helpers via eval python backend", async () => {
|
||||
const helpers = ["env", "read", "write", "append", "find", "glob", "grep", "rgrep", "sed", "tree", "stat", "diff", "run", "output"];
|
||||
const helpers = [
|
||||
"env",
|
||||
"read",
|
||||
"write",
|
||||
"append",
|
||||
"find",
|
||||
"glob",
|
||||
"grep",
|
||||
"rgrep",
|
||||
"sed",
|
||||
"tree",
|
||||
"stat",
|
||||
"diff",
|
||||
"run",
|
||||
"output",
|
||||
];
|
||||
|
||||
const session = {
|
||||
cwd: getProjectDir(),
|
||||
@@ -74,12 +88,4 @@ ${code}
|
||||
expect(tool.description).toContain("read");
|
||||
expect(tool.description).not.toContain("Documentation unavailable");
|
||||
});
|
||||
|
||||
it("renders prelude docs in eval tool description", async () => {
|
||||
const result = await warmPythonEnvironment(getProjectDir());
|
||||
expect(result.ok).toBe(true);
|
||||
const description = getEvalToolDescription();
|
||||
expect(description).toContain("read");
|
||||
expect(description).not.toContain("Documentation unavailable");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# Changelog
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [14.5.13] - 2026-05-01
|
||||
### Changed
|
||||
|
||||
- Stopped overriding `CARGO_TARGET_DIR` with an internal `target/napi-build/...` directory during native builds, so Cargo now uses the default or caller-provided target directory
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@oh-my-pi/pi-natives",
|
||||
"version": "14.5.12",
|
||||
"version": "14.5.13",
|
||||
"description": "Native Rust bindings for grep, clipboard, image processing, syntax highlighting, PTY, and shell operations via N-API",
|
||||
"homepage": "https://github.com/can1357/oh-my-pi",
|
||||
"author": "Can Boluk",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"type": "module",
|
||||
"name": "@oh-my-pi/omp-stats",
|
||||
"version": "14.5.12",
|
||||
"version": "14.5.13",
|
||||
"description": "Local observability dashboard for pi AI usage statistics",
|
||||
"homepage": "https://github.com/can1357/oh-my-pi",
|
||||
"author": "Can Boluk",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"type": "module",
|
||||
"name": "@oh-my-pi/swarm-extension",
|
||||
"version": "14.5.12",
|
||||
"version": "14.5.13",
|
||||
"description": "Swarm orchestration extension for omp",
|
||||
"homepage": "https://github.com/can1357/oh-my-pi",
|
||||
"author": "Derek Rynd",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"type": "module",
|
||||
"name": "@oh-my-pi/pi-tui",
|
||||
"version": "14.5.12",
|
||||
"version": "14.5.13",
|
||||
"description": "Terminal User Interface library with differential rendering for efficient text-based applications",
|
||||
"homepage": "https://github.com/can1357/oh-my-pi",
|
||||
"author": "Can Boluk",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"type": "module",
|
||||
"name": "@oh-my-pi/pi-utils",
|
||||
"version": "14.5.12",
|
||||
"version": "14.5.13",
|
||||
"description": "Shared utilities for pi packages",
|
||||
"homepage": "https://github.com/can1357/oh-my-pi",
|
||||
"author": "Can Boluk",
|
||||
|
||||
@@ -8,23 +8,33 @@ function stripHtmlComments(content: string): string {
|
||||
|
||||
/** Convert kebab-case to camelCase (e.g. "thinking-level" -> "thinkingLevel") */
|
||||
function kebabToCamel(key: string): string {
|
||||
if (!key.includes("-")) return key;
|
||||
return key.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
||||
}
|
||||
|
||||
/** Recursively normalize object keys from kebab-case to camelCase */
|
||||
function normalizeKeys<T>(obj: T): T {
|
||||
if (obj === null || typeof obj !== "object") {
|
||||
return obj;
|
||||
}
|
||||
if (obj === null || typeof obj !== "object") return obj;
|
||||
if (Array.isArray(obj)) {
|
||||
return obj.map(normalizeKeys) as T;
|
||||
let changed = false;
|
||||
const out: unknown[] = new Array(obj.length);
|
||||
for (let i = 0; i < obj.length; i++) {
|
||||
const v = obj[i];
|
||||
const nv = normalizeKeys(v);
|
||||
out[i] = nv;
|
||||
if (nv !== v) changed = true;
|
||||
}
|
||||
return (changed ? (out as unknown) : obj) as T;
|
||||
}
|
||||
let changed = false;
|
||||
const result: Record<string, unknown> = {};
|
||||
for (const [key, value] of Object.entries(obj as Record<string, unknown>)) {
|
||||
const normalizedKey = kebabToCamel(key);
|
||||
result[normalizedKey] = normalizeKeys(value);
|
||||
const nk = key.includes("-") ? kebabToCamel(key) : key;
|
||||
const nv = normalizeKeys(value);
|
||||
result[nk] = nv;
|
||||
if (nk !== key || nv !== value) changed = true;
|
||||
}
|
||||
return result as T;
|
||||
return (changed ? result : obj) as T;
|
||||
}
|
||||
|
||||
export class FrontmatterError extends Error {
|
||||
|
||||
@@ -418,8 +418,17 @@ function disambiguateClosingBraces(template: string): string {
|
||||
return template.replace(/\}\}(\}+)/g, "}}{{!---}}$1");
|
||||
}
|
||||
|
||||
const compiledTemplateCache = new Map<string, (context: TemplateContext) => string>();
|
||||
|
||||
export function compile(template: string): (context: TemplateContext) => string {
|
||||
return handlebars.compile(disambiguateClosingBraces(template), { noEscape: true, strict: false });
|
||||
const disambiguated = disambiguateClosingBraces(template);
|
||||
const cached = compiledTemplateCache.get(disambiguated);
|
||||
if (cached) return cached;
|
||||
const compiled = handlebars.compile(disambiguated, { noEscape: true, strict: false }) as (
|
||||
context: TemplateContext,
|
||||
) => string;
|
||||
compiledTemplateCache.set(disambiguated, compiled);
|
||||
return compiled;
|
||||
}
|
||||
|
||||
export function render(template: string, context: TemplateContext = {}): string {
|
||||
|
||||
Reference in New Issue
Block a user