Browse Source

use treesitter to parse bash commands and catch commands that go outside of cwd (#1443)

Dax 7 months ago
parent
commit
18888351e9

+ 21 - 16
bun.lock

@@ -48,7 +48,9 @@
         "hono-openapi": "0.4.8",
         "hono-openapi": "0.4.8",
         "isomorphic-git": "1.32.1",
         "isomorphic-git": "1.32.1",
         "open": "10.1.2",
         "open": "10.1.2",
-        "remeda": "2.22.3",
+        "remeda": "catalog:",
+        "tree-sitter": "0.22.4",
+        "tree-sitter-bash": "0.23.3",
         "turndown": "7.2.0",
         "turndown": "7.2.0",
         "vscode-jsonrpc": "8.2.1",
         "vscode-jsonrpc": "8.2.1",
         "xdg-basedir": "5.1.0",
         "xdg-basedir": "5.1.0",
@@ -92,7 +94,7 @@
         "ts-node": "^10.5.0",
         "ts-node": "^10.5.0",
         "tsc-multi": "https://github.com/stainless-api/tsc-multi/releases/download/v1.1.8/tsc-multi.tgz",
         "tsc-multi": "https://github.com/stainless-api/tsc-multi/releases/download/v1.1.8/tsc-multi.tgz",
         "tsconfig-paths": "^4.0.0",
         "tsconfig-paths": "^4.0.0",
-        "typescript": "5.8.3",
+        "typescript": "catalog:",
         "typescript-eslint": "8.31.1",
         "typescript-eslint": "8.31.1",
       },
       },
     },
     },
@@ -135,8 +137,9 @@
   ],
   ],
   "catalog": {
   "catalog": {
     "@types/node": "22.13.9",
     "@types/node": "22.13.9",
-    "ai": "5.0.0-beta.21",
+    "ai": "5.0.0-beta.33",
     "hono": "4.7.10",
     "hono": "4.7.10",
+    "remeda": "2.26.0",
     "typescript": "5.8.2",
     "typescript": "5.8.2",
     "zod": "3.25.49",
     "zod": "3.25.49",
   },
   },
@@ -155,11 +158,11 @@
 
 
     "@ai-sdk/anthropic": ["@ai-sdk/[email protected]", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "@ai-sdk/provider-utils": "2.2.8" }, "peerDependencies": { "zod": "^3.0.0" } }, "sha512-YSzjlko7JvuiyQFmI9RN1tNZdEiZxc+6xld/0tq/VkJaHpEzGAb1yiNxxvmYVcjvfu/PcvCxAAYXmTYQQ63IHQ=="],
     "@ai-sdk/anthropic": ["@ai-sdk/[email protected]", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "@ai-sdk/provider-utils": "2.2.8" }, "peerDependencies": { "zod": "^3.0.0" } }, "sha512-YSzjlko7JvuiyQFmI9RN1tNZdEiZxc+6xld/0tq/VkJaHpEzGAb1yiNxxvmYVcjvfu/PcvCxAAYXmTYQQ63IHQ=="],
 
 
-    "@ai-sdk/gateway": ["@ai-sdk/[email protected]", "", { "dependencies": { "@ai-sdk/provider": "2.0.0-beta.1", "@ai-sdk/provider-utils": "3.0.0-beta.3" }, "peerDependencies": { "zod": "^3.25.49 || ^4" } }, "sha512-D2SqYRT/42JTiRxUuiWtn5cYQFscpb9Z14UNvJx7lnurBUXx57zy7TbLH0h7O+WbCluTQN5G6146JpUZ/SRyzw=="],
+    "@ai-sdk/gateway": ["@ai-sdk/[email protected].18", "", { "dependencies": { "@ai-sdk/provider": "2.0.0-beta.2", "@ai-sdk/provider-utils": "3.0.0-beta.9" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-1K5L7mY04ZwpngkDPLaiBiCivVj1h7gDiCZjAIgXtVp0S2zQ+1efnM/K/o2Pig6rUbt559rDLLalwZUgvn0vig=="],
 
 
-    "@ai-sdk/provider": ["@ai-sdk/[email protected].1", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-Z8SPncMtS3RsoXITmT7NVwrAq6M44dmw0DoUOYJqNNtCu8iMWuxB8Nxsoqpa0uEEy9R1V1ZThJAXTYgjTUxl3w=="],
+    "@ai-sdk/provider": ["@ai-sdk/[email protected].2", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-vqhtZA7R24q1XnmfmIb1fZSmHMIaJH1BVQ+0kFnNJgqWsc+V8i+yfetZ37gUc4fXATFmBuS/6O7+RPoHsZ2Fqg=="],
 
 
-    "@ai-sdk/provider-utils": ["@ai-sdk/[email protected].3", "", { "dependencies": { "@ai-sdk/provider": "2.0.0-beta.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.3", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.25.49 || ^4" } }, "sha512-4gZ392GxjzMF7TnReF2eTKhOSyiSS3ydRVq4I7jxkeV5sdEuMoH3gzfItmlctsqGxlMU1/+zKPwl5yYz9O2dzg=="],
+    "@ai-sdk/provider-utils": ["@ai-sdk/[email protected].9", "", { "dependencies": { "@ai-sdk/provider": "2.0.0-beta.2", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.3", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-RJMeoqFA9mGo1XOE20bpVv4/ikVbZMHo00vmF4RweN7GHS+nEXU3SHFgtcp7NBG3j8W15b9MAitOBycRMYxecg=="],
 
 
     "@ampproject/remapping": ["@ampproject/[email protected]", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="],
     "@ampproject/remapping": ["@ampproject/[email protected]", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="],
 
 
@@ -783,7 +786,7 @@
 
 
     "aggregate-error": ["[email protected]", "", { "dependencies": { "clean-stack": "^2.0.0", "indent-string": "^4.0.0" } }, "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA=="],
     "aggregate-error": ["[email protected]", "", { "dependencies": { "clean-stack": "^2.0.0", "indent-string": "^4.0.0" } }, "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA=="],
 
 
-    "ai": ["[email protected].21", "", { "dependencies": { "@ai-sdk/gateway": "1.0.0-beta.8", "@ai-sdk/provider": "2.0.0-beta.1", "@ai-sdk/provider-utils": "3.0.0-beta.3", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.49 || ^4" }, "bin": { "ai": "dist/bin/ai.min.js" } }, "sha512-ZmgUoEIXb2G2HLtK1U3UB+hSDa3qrVIeAfgXf3SIE9r5Vqj6xHG1pN/7fHIZDSgb1TCaypG0ANVB0O9WmnMfiw=="],
+    "ai": ["[email protected].33", "", { "dependencies": { "@ai-sdk/gateway": "1.0.0-beta.18", "@ai-sdk/provider": "2.0.0-beta.2", "@ai-sdk/provider-utils": "3.0.0-beta.9", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-TKDOYDRhS6kSmfbTj3lLFmS8kBx8OOHsIfhYKJBKnAPwlbkI3/byZRBty8tfKBrwsUAbSro3GB7rFeSthft37Q=="],
 
 
     "ajv": ["[email protected]", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="],
     "ajv": ["[email protected]", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="],
 
 
@@ -1747,6 +1750,8 @@
 
 
     "node-fetch-native": ["[email protected]", "", {}, "sha512-8Mc2HhqPdlIfedsuZoc3yioPuzp6b+L5jRCRY1QzuWZh2EGJVQrGppC6V6cF0bLdbW0+O2YpqCA25aF/1lvipQ=="],
     "node-fetch-native": ["[email protected]", "", {}, "sha512-8Mc2HhqPdlIfedsuZoc3yioPuzp6b+L5jRCRY1QzuWZh2EGJVQrGppC6V6cF0bLdbW0+O2YpqCA25aF/1lvipQ=="],
 
 
+    "node-gyp-build": ["[email protected]", "", { "bin": { "node-gyp-build": "bin.js", "node-gyp-build-optional": "optional.js", "node-gyp-build-test": "build-test.js" } }, "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ=="],
+
     "node-int64": ["[email protected]", "", {}, "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw=="],
     "node-int64": ["[email protected]", "", {}, "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw=="],
 
 
     "node-mock-http": ["[email protected]", "", {}, "sha512-0gJJgENizp4ghds/Ywu2FCmcRsgBTmRQzYPZm61wy+Em2sBarSka0OhQS5huLBg6od1zkNpnWMCZloQDFVvOMQ=="],
     "node-mock-http": ["[email protected]", "", {}, "sha512-0gJJgENizp4ghds/Ywu2FCmcRsgBTmRQzYPZm61wy+Em2sBarSka0OhQS5huLBg6od1zkNpnWMCZloQDFVvOMQ=="],
@@ -2161,6 +2166,10 @@
 
 
     "tr46": ["[email protected]", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="],
     "tr46": ["[email protected]", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="],
 
 
+    "tree-sitter": ["[email protected]", "", { "dependencies": { "node-addon-api": "^8.3.0", "node-gyp-build": "^4.8.4" } }, "sha512-usbHZP9/oxNsUY65MQUsduGRqDHQOou1cagUSwjhoSYAmSahjQDAVsh9s+SlZkn8X8+O1FULRGwHu7AFP3kjzg=="],
+
+    "tree-sitter-bash": ["[email protected]", "", { "dependencies": { "node-addon-api": "^8.2.1", "node-gyp-build": "^4.8.2" }, "peerDependencies": { "tree-sitter": "^0.21.1" }, "optionalPeers": ["tree-sitter"] }, "sha512-36cg/GQ2YmIbeiBeqeuh4fBJ6i4kgVouDaqTxqih5ysPag+zHufyIaxMOFeM8CeplwAK/Luj1o5XHqgdAfoCZg=="],
+
     "trim-lines": ["[email protected]", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="],
     "trim-lines": ["[email protected]", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="],
 
 
     "trough": ["[email protected]", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="],
     "trough": ["[email protected]", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="],
@@ -2195,7 +2204,7 @@
 
 
     "typed-array-buffer": ["[email protected]", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-typed-array": "^1.1.14" } }, "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw=="],
     "typed-array-buffer": ["[email protected]", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-typed-array": "^1.1.14" } }, "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw=="],
 
 
-    "typescript": ["[email protected].3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
+    "typescript": ["[email protected].2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ=="],
 
 
     "typescript-eslint": ["[email protected]", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.31.1", "@typescript-eslint/parser": "8.31.1", "@typescript-eslint/utils": "8.31.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-j6DsEotD/fH39qKzXTQRwYYWlt7D+0HmfpOK+DVhwJOFLcdmn92hq3mBb7HlKJHbjjI/gTOqEcc9d6JfpFf/VA=="],
     "typescript-eslint": ["[email protected]", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.31.1", "@typescript-eslint/parser": "8.31.1", "@typescript-eslint/utils": "8.31.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-j6DsEotD/fH39qKzXTQRwYYWlt7D+0HmfpOK+DVhwJOFLcdmn92hq3mBb7HlKJHbjjI/gTOqEcc9d6JfpFf/VA=="],
 
 
@@ -2437,12 +2446,8 @@
 
 
     "@opencode/function/@types/node": ["@types/[email protected]", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="],
     "@opencode/function/@types/node": ["@types/[email protected]", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="],
 
 
-    "@opencode/function/typescript": ["[email protected]", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ=="],
-
     "@opencode/web/@types/node": ["@types/[email protected]", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="],
     "@opencode/web/@types/node": ["@types/[email protected]", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw=="],
 
 
-    "@opencode/web/typescript": ["[email protected]", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ=="],
-
     "@oslojs/jwt/@oslojs/encoding": ["@oslojs/[email protected]", "", {}, "sha512-hkjo6MuIK/kQR5CrGNdAPZhS01ZCXuWDRJ187zh6qqF2+yMHZpD9fAYpX8q2bOO6Ryhl3XpCT6kUX76N8hhm4Q=="],
     "@oslojs/jwt/@oslojs/encoding": ["@oslojs/[email protected]", "", {}, "sha512-hkjo6MuIK/kQR5CrGNdAPZhS01ZCXuWDRJ187zh6qqF2+yMHZpD9fAYpX8q2bOO6Ryhl3XpCT6kUX76N8hhm4Q=="],
 
 
     "@poppinss/dumper/@sindresorhus/is": ["@sindresorhus/[email protected]", "", {}, "sha512-d9xRovfKNz1SKieM0qJdO+PQonjnnIfSNWfHYnBSJ9hkjm0ZPw6HlxscDXYstp3z+7V2GOFHc+J0CYrYTjqCJw=="],
     "@poppinss/dumper/@sindresorhus/is": ["@sindresorhus/[email protected]", "", {}, "sha512-d9xRovfKNz1SKieM0qJdO+PQonjnnIfSNWfHYnBSJ9hkjm0ZPw6HlxscDXYstp3z+7V2GOFHc+J0CYrYTjqCJw=="],
@@ -2563,10 +2568,6 @@
 
 
     "miniflare/zod": ["[email protected]", "", {}, "sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug=="],
     "miniflare/zod": ["[email protected]", "", {}, "sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug=="],
 
 
-    "opencode/remeda": ["[email protected]", "", { "dependencies": { "type-fest": "^4.40.1" } }, "sha512-Ka6965m9Zu9OLsysWxVf3jdJKmp6+PKzDv7HWHinEevf0JOJ9y02YpjiC/sKxRpCqGhVyvm1U+0YIj+E6DMgKw=="],
-
-    "opencode/typescript": ["[email protected]", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ=="],
-
     "opencontrol/@modelcontextprotocol/sdk": ["@modelcontextprotocol/[email protected]", "", { "dependencies": { "content-type": "^1.0.5", "cors": "^2.8.5", "eventsource": "^3.0.2", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "pkce-challenge": "^4.1.0", "raw-body": "^3.0.0", "zod": "^3.23.8", "zod-to-json-schema": "^3.24.1" } }, "sha512-oxzMzYCkZHMntzuyerehK3fV6A2Kwh5BD6CGEJSVDU2QNEhfLOptf2X7esQgaHZXHZY0oHmMsOtIDLP71UJXgA=="],
     "opencontrol/@modelcontextprotocol/sdk": ["@modelcontextprotocol/[email protected]", "", { "dependencies": { "content-type": "^1.0.5", "cors": "^2.8.5", "eventsource": "^3.0.2", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "pkce-challenge": "^4.1.0", "raw-body": "^3.0.0", "zod": "^3.23.8", "zod-to-json-schema": "^3.24.1" } }, "sha512-oxzMzYCkZHMntzuyerehK3fV6A2Kwh5BD6CGEJSVDU2QNEhfLOptf2X7esQgaHZXHZY0oHmMsOtIDLP71UJXgA=="],
 
 
     "opencontrol/hono": ["[email protected]", "", {}, "sha512-Pst8FuGqz3L7tFF+u9Pu70eI0xa5S3LPUmrNd5Jm8nTHze9FxLTK9Kaj5g/k4UcwuJSXTP65SyHOPLrffpcAJg=="],
     "opencontrol/hono": ["[email protected]", "", {}, "sha512-Pst8FuGqz3L7tFF+u9Pu70eI0xa5S3LPUmrNd5Jm8nTHze9FxLTK9Kaj5g/k4UcwuJSXTP65SyHOPLrffpcAJg=="],
@@ -2617,6 +2618,10 @@
 
 
     "to-buffer/isarray": ["[email protected]", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="],
     "to-buffer/isarray": ["[email protected]", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="],
 
 
+    "tree-sitter/node-addon-api": ["[email protected]", "", {}, "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A=="],
+
+    "tree-sitter-bash/node-addon-api": ["[email protected]", "", {}, "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A=="],
+
     "ts-node/diff": ["[email protected]", "", {}, "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A=="],
     "ts-node/diff": ["[email protected]", "", {}, "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A=="],
 
 
     "tsc-multi/yargs": ["[email protected]", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="],
     "tsc-multi/yargs": ["[email protected]", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="],

+ 3 - 2
package.json

@@ -16,10 +16,11 @@
     ],
     ],
     "catalog": {
     "catalog": {
       "@types/node": "22.13.9",
       "@types/node": "22.13.9",
-      "ai": "5.0.0-beta.21",
+      "ai": "5.0.0-beta.33",
       "hono": "4.7.10",
       "hono": "4.7.10",
       "typescript": "5.8.2",
       "typescript": "5.8.2",
-      "zod": "3.25.49"
+      "zod": "3.25.49",
+      "remeda": "2.26.0"
     }
     }
   },
   },
   "devDependencies": {
   "devDependencies": {

+ 3 - 1
packages/opencode/package.json

@@ -46,8 +46,10 @@
     "hono-openapi": "0.4.8",
     "hono-openapi": "0.4.8",
     "isomorphic-git": "1.32.1",
     "isomorphic-git": "1.32.1",
     "open": "10.1.2",
     "open": "10.1.2",
-    "remeda": "2.22.3",
+    "remeda": "catalog:",
     "turndown": "7.2.0",
     "turndown": "7.2.0",
+    "tree-sitter": "0.22.4",
+    "tree-sitter-bash": "0.23.3",
     "vscode-jsonrpc": "8.2.1",
     "vscode-jsonrpc": "8.2.1",
     "xdg-basedir": "5.1.0",
     "xdg-basedir": "5.1.0",
     "yargs": "18.0.0",
     "yargs": "18.0.0",

+ 9 - 0
packages/opencode/src/config/config.ts

@@ -187,6 +187,9 @@ export namespace Config {
   })
   })
   export type Layout = z.infer<typeof Layout>
   export type Layout = z.infer<typeof Layout>
 
 
+  export const Permission = z.union([z.literal("ask"), z.literal("allow")])
+  export type Permission = z.infer<typeof Permission>
+
   export const Info = z
   export const Info = z
     .object({
     .object({
       $schema: z.string().optional().describe("JSON schema reference for configuration validation"),
       $schema: z.string().optional().describe("JSON schema reference for configuration validation"),
@@ -250,6 +253,12 @@ export namespace Config {
       mcp: z.record(z.string(), Mcp).optional().describe("MCP (Model Context Protocol) server configurations"),
       mcp: z.record(z.string(), Mcp).optional().describe("MCP (Model Context Protocol) server configurations"),
       instructions: z.array(z.string()).optional().describe("Additional instruction files or patterns to include"),
       instructions: z.array(z.string()).optional().describe("Additional instruction files or patterns to include"),
       layout: Layout.optional().describe("@deprecated Always uses stretch layout."),
       layout: Layout.optional().describe("@deprecated Always uses stretch layout."),
+      permission: z
+        .object({
+          edit: Permission.optional(),
+          bash: z.union([Permission, z.record(z.string(), Permission)]).optional(),
+        })
+        .optional(),
       experimental: z
       experimental: z
         .object({
         .object({
           hook: z
           hook: z

+ 9 - 2
packages/opencode/src/session/index.ts

@@ -290,6 +290,9 @@ export namespace Session {
   export function abort(sessionID: string) {
   export function abort(sessionID: string) {
     const controller = state().pending.get(sessionID)
     const controller = state().pending.get(sessionID)
     if (!controller) return false
     if (!controller) return false
+    log.info("aborting", {
+      sessionID,
+    })
     controller.abort()
     controller.abort()
     state().pending.delete(sessionID)
     state().pending.delete(sessionID)
     return true
     return true
@@ -765,7 +768,11 @@ export namespace Session {
     }
     }
 
 
     const stream = streamText({
     const stream = streamText({
-      onError() {},
+      onError(e) {
+        log.error("streamText error", {
+          error: e,
+        })
+      },
       async prepareStep({ messages }) {
       async prepareStep({ messages }) {
         const queue = (state().queued.get(input.sessionID) ?? []).filter((x) => !x.processed)
         const queue = (state().queued.get(input.sessionID) ?? []).filter((x) => !x.processed)
         if (queue.length) {
         if (queue.length) {
@@ -1030,7 +1037,7 @@ export namespace Session {
                 }
                 }
                 break
                 break
 
 
-              case "text":
+              case "text-delta":
                 if (currentText) {
                 if (currentText) {
                   currentText.text += value.text
                   currentText.text += value.text
                   if (currentText.text) await updatePart(currentText)
                   if (currentText.text) await updatePart(currentText)

+ 0 - 2
packages/opencode/src/session/prompt/beast.txt

@@ -1,5 +1,3 @@
-# Beast Mode 3.1
-
 You are opencode, an agent - please keep going until the user’s query is completely resolved, before ending your turn and yielding back to the user.
 You are opencode, an agent - please keep going until the user’s query is completely resolved, before ending your turn and yielding back to the user.
 
 
 Your thinking should be thorough and so it's fine if it's very long. However, avoid unnecessary repetition and verbosity. You should be concise, but thorough.
 Your thinking should be thorough and so it's fine if it's very long. However, avoid unnecessary repetition and verbosity. You should be concise, but thorough.

+ 82 - 1
packages/opencode/src/tool/bash.ts

@@ -2,11 +2,21 @@ import { z } from "zod"
 import { Tool } from "./tool"
 import { Tool } from "./tool"
 import DESCRIPTION from "./bash.txt"
 import DESCRIPTION from "./bash.txt"
 import { App } from "../app/app"
 import { App } from "../app/app"
+import path from "path"
+
+import Parser from "tree-sitter"
+import Bash from "tree-sitter-bash"
+import { Config } from "../config/config"
+import { Filesystem } from "../util/filesystem"
+import { Permission } from "../permission"
 
 
 const MAX_OUTPUT_LENGTH = 30000
 const MAX_OUTPUT_LENGTH = 30000
 const DEFAULT_TIMEOUT = 1 * 60 * 1000
 const DEFAULT_TIMEOUT = 1 * 60 * 1000
 const MAX_TIMEOUT = 10 * 60 * 1000
 const MAX_TIMEOUT = 10 * 60 * 1000
 
 
+const parser = new Parser()
+parser.setLanguage(Bash.language as any)
+
 export const BashTool = Tool.define("bash", {
 export const BashTool = Tool.define("bash", {
   description: DESCRIPTION,
   description: DESCRIPTION,
   parameters: z.object({
   parameters: z.object({
@@ -20,10 +30,81 @@ export const BashTool = Tool.define("bash", {
   }),
   }),
   async execute(params, ctx) {
   async execute(params, ctx) {
     const timeout = Math.min(params.timeout ?? DEFAULT_TIMEOUT, MAX_TIMEOUT)
     const timeout = Math.min(params.timeout ?? DEFAULT_TIMEOUT, MAX_TIMEOUT)
+    const tree = parser.parse(params.command)
+    const cfg = await Config.get()
+    const app = App.info()
+    const permissions = (() => {
+      const value = cfg.permission?.bash
+      if (!value)
+        return {
+          "*": "allow",
+        }
+      if (typeof value === "string")
+        return {
+          "*": value,
+        }
+      return value
+    })()
+
+    let needsAsk = false
+    for (const node of tree.rootNode.descendantsOfType("command")) {
+      const command = []
+      for (let i = 0; i < node.childCount; i++) {
+        const child = node.child(i)
+        if (!child) continue
+        if (
+          child.type !== "command_name" &&
+          child.type !== "word" &&
+          child.type !== "string" &&
+          child.type !== "raw_string" &&
+          child.type !== "concatenation"
+        ) {
+          continue
+        }
+        command.push(child.text)
+      }
+
+      // not an exhaustive list, but covers most common cases
+      if (["cd", "rm", "cp", "mv", "mkdir", "touch", "chmod", "chown"].includes(command[0])) {
+        for (const arg of command.slice(1)) {
+          if (arg.startsWith("-")) continue
+          const resolved = path.resolve(app.path.cwd, arg)
+          if (!Filesystem.contains(app.path.cwd, resolved)) {
+            throw new Error(
+              `This command references paths outside of ${app.path.cwd} so it is not allowed to be executed.`,
+            )
+          }
+        }
+      }
+
+      // always allow cd if it passes above check
+      if (!needsAsk && command[0] !== "cd") {
+        const ask = (() => {
+          for (const [pattern, value] of Object.entries(permissions)) {
+            if (new Bun.Glob(pattern).match(node.text)) {
+              return value
+            }
+          }
+          return "ask"
+        })()
+        if (ask === "ask") needsAsk = true
+      }
+    }
+
+    if (needsAsk) {
+      await Permission.ask({
+        id: "basj",
+        sessionID: ctx.sessionID,
+        title: params.command,
+        metadata: {
+          command: params.command,
+        },
+      })
+    }
 
 
     const process = Bun.spawn({
     const process = Bun.spawn({
       cmd: ["bash", "-c", params.command],
       cmd: ["bash", "-c", params.command],
-      cwd: App.info().path.cwd,
+      cwd: app.path.cwd,
       maxBuffer: MAX_OUTPUT_LENGTH,
       maxBuffer: MAX_OUTPUT_LENGTH,
       signal: ctx.abort,
       signal: ctx.abort,
       timeout: timeout,
       timeout: timeout,

+ 18 - 10
packages/opencode/src/tool/edit.ts

@@ -2,6 +2,7 @@
 // https://github.com/cline/cline/blob/main/evals/diff-edits/diff-apply/diff-06-23-25.ts
 // https://github.com/cline/cline/blob/main/evals/diff-edits/diff-apply/diff-06-23-25.ts
 // https://github.com/google-gemini/gemini-cli/blob/main/packages/core/src/utils/editCorrector.ts
 // https://github.com/google-gemini/gemini-cli/blob/main/packages/core/src/utils/editCorrector.ts
 // https://github.com/cline/cline/blob/main/evals/diff-edits/diff-apply/diff-06-26-25.ts
 // https://github.com/cline/cline/blob/main/evals/diff-edits/diff-apply/diff-06-26-25.ts
+
 import { z } from "zod"
 import { z } from "zod"
 import * as path from "path"
 import * as path from "path"
 import { Tool } from "./tool"
 import { Tool } from "./tool"
@@ -13,6 +14,8 @@ import { App } from "../app/app"
 import { File } from "../file"
 import { File } from "../file"
 import { Bus } from "../bus"
 import { Bus } from "../bus"
 import { FileTime } from "../file/time"
 import { FileTime } from "../file/time"
+import { Config } from "../config/config"
+import { Filesystem } from "../util/filesystem"
 
 
 export const EditTool = Tool.define("edit", {
 export const EditTool = Tool.define("edit", {
   description: DESCRIPTION,
   description: DESCRIPTION,
@@ -33,17 +36,22 @@ export const EditTool = Tool.define("edit", {
 
 
     const app = App.info()
     const app = App.info()
     const filepath = path.isAbsolute(params.filePath) ? params.filePath : path.join(app.path.cwd, params.filePath)
     const filepath = path.isAbsolute(params.filePath) ? params.filePath : path.join(app.path.cwd, params.filePath)
+    if (!Filesystem.contains(app.path.cwd, filepath)) {
+      throw new Error(`File ${filepath} is not in the current working directory`)
+    }
 
 
-    await Permission.ask({
-      id: "edit",
-      sessionID: ctx.sessionID,
-      title: "Edit this file: " + filepath,
-      metadata: {
-        filePath: filepath,
-        oldString: params.oldString,
-        newString: params.newString,
-      },
-    })
+    const cfg = await Config.get()
+    if (cfg.permission?.edit === "ask")
+      await Permission.ask({
+        id: "edit",
+        sessionID: ctx.sessionID,
+        title: "Edit this file: " + filepath,
+        metadata: {
+          filePath: filepath,
+          oldString: params.oldString,
+          newString: params.newString,
+        },
+      })
 
 
     let contentOld = ""
     let contentOld = ""
     let contentNew = ""
     let contentNew = ""

+ 18 - 13
packages/opencode/src/tool/read.ts

@@ -6,6 +6,7 @@ import { LSP } from "../lsp"
 import { FileTime } from "../file/time"
 import { FileTime } from "../file/time"
 import DESCRIPTION from "./read.txt"
 import DESCRIPTION from "./read.txt"
 import { App } from "../app/app"
 import { App } from "../app/app"
+import { Filesystem } from "../util/filesystem"
 
 
 const DEFAULT_READ_LIMIT = 2000
 const DEFAULT_READ_LIMIT = 2000
 const MAX_LINE_LENGTH = 2000
 const MAX_LINE_LENGTH = 2000
@@ -18,15 +19,19 @@ export const ReadTool = Tool.define("read", {
     limit: z.coerce.number().describe("The number of lines to read (defaults to 2000)").optional(),
     limit: z.coerce.number().describe("The number of lines to read (defaults to 2000)").optional(),
   }),
   }),
   async execute(params, ctx) {
   async execute(params, ctx) {
-    let filePath = params.filePath
-    if (!path.isAbsolute(filePath)) {
-      filePath = path.join(process.cwd(), filePath)
+    let filepath = params.filePath
+    if (!path.isAbsolute(filepath)) {
+      filepath = path.join(process.cwd(), filepath)
+    }
+    const app = App.info()
+    if (!Filesystem.contains(app.path.cwd, filepath)) {
+      throw new Error(`File ${filepath} is not in the current working directory`)
     }
     }
 
 
-    const file = Bun.file(filePath)
+    const file = Bun.file(filepath)
     if (!(await file.exists())) {
     if (!(await file.exists())) {
-      const dir = path.dirname(filePath)
-      const base = path.basename(filePath)
+      const dir = path.dirname(filepath)
+      const base = path.basename(filepath)
 
 
       const dirEntries = fs.readdirSync(dir)
       const dirEntries = fs.readdirSync(dir)
       const suggestions = dirEntries
       const suggestions = dirEntries
@@ -38,18 +43,18 @@ export const ReadTool = Tool.define("read", {
         .slice(0, 3)
         .slice(0, 3)
 
 
       if (suggestions.length > 0) {
       if (suggestions.length > 0) {
-        throw new Error(`File not found: ${filePath}\n\nDid you mean one of these?\n${suggestions.join("\n")}`)
+        throw new Error(`File not found: ${filepath}\n\nDid you mean one of these?\n${suggestions.join("\n")}`)
       }
       }
 
 
-      throw new Error(`File not found: ${filePath}`)
+      throw new Error(`File not found: ${filepath}`)
     }
     }
 
 
     const limit = params.limit ?? DEFAULT_READ_LIMIT
     const limit = params.limit ?? DEFAULT_READ_LIMIT
     const offset = params.offset || 0
     const offset = params.offset || 0
-    const isImage = isImageFile(filePath)
+    const isImage = isImageFile(filepath)
     if (isImage) throw new Error(`This is an image file of type: ${isImage}\nUse a different tool to process images`)
     if (isImage) throw new Error(`This is an image file of type: ${isImage}\nUse a different tool to process images`)
     const isBinary = await isBinaryFile(file)
     const isBinary = await isBinaryFile(file)
-    if (isBinary) throw new Error(`Cannot read binary file: ${filePath}`)
+    if (isBinary) throw new Error(`Cannot read binary file: ${filepath}`)
     const lines = await file.text().then((text) => text.split("\n"))
     const lines = await file.text().then((text) => text.split("\n"))
     const raw = lines.slice(offset, offset + limit).map((line) => {
     const raw = lines.slice(offset, offset + limit).map((line) => {
       return line.length > MAX_LINE_LENGTH ? line.substring(0, MAX_LINE_LENGTH) + "..." : line
       return line.length > MAX_LINE_LENGTH ? line.substring(0, MAX_LINE_LENGTH) + "..." : line
@@ -68,11 +73,11 @@ export const ReadTool = Tool.define("read", {
     output += "\n</file>"
     output += "\n</file>"
 
 
     // just warms the lsp client
     // just warms the lsp client
-    LSP.touchFile(filePath, false)
-    FileTime.read(ctx.sessionID, filePath)
+    LSP.touchFile(filepath, false)
+    FileTime.read(ctx.sessionID, filepath)
 
 
     return {
     return {
-      title: path.relative(App.info().path.root, filePath),
+      title: path.relative(App.info().path.root, filepath),
       output,
       output,
       metadata: {
       metadata: {
         preview,
         preview,

+ 17 - 10
packages/opencode/src/tool/write.ts

@@ -8,6 +8,8 @@ import { App } from "../app/app"
 import { Bus } from "../bus"
 import { Bus } from "../bus"
 import { File } from "../file"
 import { File } from "../file"
 import { FileTime } from "../file/time"
 import { FileTime } from "../file/time"
+import { Config } from "../config/config"
+import { Filesystem } from "../util/filesystem"
 
 
 export const WriteTool = Tool.define("write", {
 export const WriteTool = Tool.define("write", {
   description: DESCRIPTION,
   description: DESCRIPTION,
@@ -18,21 +20,26 @@ export const WriteTool = Tool.define("write", {
   async execute(params, ctx) {
   async execute(params, ctx) {
     const app = App.info()
     const app = App.info()
     const filepath = path.isAbsolute(params.filePath) ? params.filePath : path.join(app.path.cwd, params.filePath)
     const filepath = path.isAbsolute(params.filePath) ? params.filePath : path.join(app.path.cwd, params.filePath)
+    if (!Filesystem.contains(app.path.cwd, filepath)) {
+      throw new Error(`File ${filepath} is not in the current working directory`)
+    }
 
 
     const file = Bun.file(filepath)
     const file = Bun.file(filepath)
     const exists = await file.exists()
     const exists = await file.exists()
     if (exists) await FileTime.assert(ctx.sessionID, filepath)
     if (exists) await FileTime.assert(ctx.sessionID, filepath)
 
 
-    await Permission.ask({
-      id: "write",
-      sessionID: ctx.sessionID,
-      title: exists ? "Overwrite this file: " + filepath : "Create new file: " + filepath,
-      metadata: {
-        filePath: filepath,
-        content: params.content,
-        exists,
-      },
-    })
+    const cfg = await Config.get()
+    if (cfg.permission?.edit === "ask")
+      await Permission.ask({
+        id: "write",
+        sessionID: ctx.sessionID,
+        title: exists ? "Overwrite this file: " + filepath : "Create new file: " + filepath,
+        metadata: {
+          filePath: filepath,
+          content: params.content,
+          exists,
+        },
+      })
 
 
     await Bun.write(filepath, params.content)
     await Bun.write(filepath, params.content)
     await Bus.publish(File.Event.Edited, {
     await Bus.publish(File.Event.Edited, {

+ 1 - 1
packages/opencode/src/util/filesystem.ts

@@ -9,7 +9,7 @@ export namespace Filesystem {
   }
   }
 
 
   export function contains(parent: string, child: string) {
   export function contains(parent: string, child: string) {
-    return relative(parent, child).startsWith("..")
+    return !relative(parent, child).startsWith("..")
   }
   }
 
 
   export async function findUp(target: string, start: string, stop?: string) {
   export async function findUp(target: string, start: string, stop?: string) {

+ 44 - 0
packages/opencode/test/tool/bash.test.ts

@@ -0,0 +1,44 @@
+import { describe, expect, test } from "bun:test"
+import { App } from "../../src/app/app"
+import path from "path"
+import { BashTool } from "../../src/tool/bash"
+import { Log } from "../../src/util/log"
+
+const ctx = {
+  sessionID: "test",
+  messageID: "",
+  abort: AbortSignal.any([]),
+  metadata: () => {},
+}
+
+const bash = await BashTool.init()
+const projectRoot = path.join(__dirname, "../..")
+Log.init({ print: false })
+
+describe("tool.bash", () => {
+  test("basic", async () => {
+    await App.provide({ cwd: projectRoot }, async () => {
+      await bash.execute(
+        {
+          command: "cd foo/bar && ls",
+          description: "List files in foo/bar",
+        },
+        ctx,
+      )
+    })
+  })
+
+  test("cd ../ should fail", async () => {
+    await App.provide({ cwd: projectRoot }, async () => {
+      expect(
+        bash.execute(
+          {
+            command: "cd ../",
+            description: "Try to cd to parent directory",
+          },
+          ctx,
+        ),
+      ).rejects.toThrow()
+    })
+  })
+})

+ 1 - 1
packages/sdk/package.json

@@ -43,7 +43,7 @@
     "ts-node": "^10.5.0",
     "ts-node": "^10.5.0",
     "tsc-multi": "https://github.com/stainless-api/tsc-multi/releases/download/v1.1.8/tsc-multi.tgz",
     "tsc-multi": "https://github.com/stainless-api/tsc-multi/releases/download/v1.1.8/tsc-multi.tgz",
     "tsconfig-paths": "^4.0.0",
     "tsconfig-paths": "^4.0.0",
-    "typescript": "5.8.3",
+    "typescript": "catalog:",
     "typescript-eslint": "8.31.1"
     "typescript-eslint": "8.31.1"
   },
   },
   "imports": {
   "imports": {