diff --git a/.vscodeignore b/.vscodeignore index 7d3e5c7..5760dd8 100644 --- a/.vscodeignore +++ b/.vscodeignore @@ -9,3 +9,4 @@ vsc-extension-quickstart.md **/*.map **/*.ts **/.vscode-test.* +backup \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 1e6e444..d9e505f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.1.3", "license": "SEE LICENSE IN LICENSE", "devDependencies": { + "@modelcontextprotocol/sdk": "^1.15.0", "@types/mocha": "^10.0.10", "@types/node": "20.x", "@types/vscode": "^1.94.0", @@ -18,7 +19,8 @@ "@vscode/test-electron": "^2.5.2", "@vscode/vsce": "^3.5.0", "eslint": "^9.25.1", - "typescript": "^5.8.3" + "typescript": "^5.8.3", + "zod": "^3.25.76" }, "engines": { "vscode": "^1.94.0" @@ -556,6 +558,30 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.15.0.tgz", + "integrity": "sha512-67hnl/ROKdb03Vuu0YOr+baKTvf1/5YBHBm9KnZdjdAh8hjt4FRCPD5ucwxGB237sBpzlqQsLy1PFu7z/ekZ9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.6", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.0.1", + "express-rate-limit": "^7.5.0", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.23.8", + "zod-to-json-schema": "^3.24.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1513,6 +1539,43 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/acorn": { "version": "8.14.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", @@ -1771,6 +1834,27 @@ "node": ">= 6" } }, + "node_modules/body-parser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", + "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.0", + "http-errors": "^2.0.0", + "iconv-lite": "^0.6.3", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.0", + "type-is": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/boolbase": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", @@ -1868,6 +1952,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/c8": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/c8/-/c8-9.1.0.tgz", @@ -2223,6 +2317,50 @@ "dev": true, "license": "MIT" }, + "node_modules/content-disposition": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", + "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-disposition/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -2230,6 +2368,26 @@ "dev": true, "license": "MIT" }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, "node_modules/core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", @@ -2237,6 +2395,20 @@ "dev": true, "license": "MIT" }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -2393,6 +2565,16 @@ "node": ">=0.4.0" } }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/detect-libc": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", @@ -2513,6 +2695,13 @@ "url": "https://bevry.me/fund" } }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true, + "license": "MIT" + }, "node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", @@ -2520,6 +2709,16 @@ "dev": true, "license": "MIT" }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/encoding-sniffer": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz", @@ -2633,6 +2832,13 @@ "node": ">=6" } }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true, + "license": "MIT" + }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -2887,6 +3093,39 @@ "node": ">=0.10.0" } }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.3.tgz", + "integrity": "sha512-nVpZkTMM9rF6AQ9gPJpFsNAMt48wIzB5TQgiTLdHiuO8XEDhUgZEhqKlZWXbIzo9VmJ/HvysHqEaVeD5v9TPvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/expand-template": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", @@ -2897,6 +3136,88 @@ "node": ">=6" } }, + "node_modules/express": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", + "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.0", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.1.tgz", + "integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/express/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -2996,6 +3317,24 @@ "node": ">=8" } }, + "node_modules/finalhandler": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", + "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -3077,6 +3416,26 @@ "node": ">= 6" } }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/fs-constants": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", @@ -3410,6 +3769,33 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-errors/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/http-proxy-agent": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", @@ -3550,6 +3936,16 @@ "dev": true, "optional": true }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", @@ -3668,6 +4064,13 @@ "node": ">=8" } }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "dev": true, + "license": "MIT" + }, "node_modules/is-unicode-supported": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", @@ -4152,6 +4555,29 @@ "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", "dev": true }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -4495,6 +4921,16 @@ "dev": true, "license": "MIT" }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/node-abi": { "version": "3.75.0", "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.75.0.tgz", @@ -4576,6 +5012,16 @@ "url": "https://github.com/fb55/nth-check?sponsor=1" } }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/object-inspect": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", @@ -4588,6 +5034,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -4927,6 +5386,16 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -4974,6 +5443,16 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/path-to-regexp": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", + "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + } + }, "node_modules/path-type": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-6.0.0.tgz", @@ -5011,6 +5490,16 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pkce-challenge": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.0.tgz", + "integrity": "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, "node_modules/pluralize": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", @@ -5064,6 +5553,20 @@ "dev": true, "license": "MIT" }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/pump": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", @@ -5140,6 +5643,32 @@ "safe-buffer": "^5.1.0" } }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", + "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.6.3", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/rc": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", @@ -5294,6 +5823,23 @@ "node": ">=0.10.0" } }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/run-applescript": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.0.0.tgz", @@ -5383,6 +5929,52 @@ "node": ">=10" } }, + "node_modules/send": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", + "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "mime-types": "^3.0.1", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/send/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/send/node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/serialize-javascript": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", @@ -5393,6 +5985,22 @@ "randombytes": "^2.1.0" } }, + "node_modules/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/setimmediate": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", @@ -5400,6 +6008,13 @@ "dev": true, "license": "MIT" }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true, + "license": "ISC" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -5622,6 +6237,16 @@ "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", "dev": true }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/stdin-discarder": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz", @@ -6063,6 +6688,16 @@ "node": ">=8.0" } }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, "node_modules/ts-api-utils": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", @@ -6129,6 +6764,44 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/type-is/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/type-is/node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/typed-rest-client": { "version": "1.8.11", "resolved": "https://registry.npmjs.org/typed-rest-client/-/typed-rest-client-1.8.11.tgz", @@ -6203,6 +6876,16 @@ "node": ">= 10.0.0" } }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -6260,6 +6943,16 @@ "spdx-expression-parse": "^3.0.0" } }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/version-range": { "version": "4.14.0", "resolved": "https://registry.npmjs.org/version-range/-/version-range-4.14.0.tgz", @@ -6587,6 +7280,26 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.24.6", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.6.tgz", + "integrity": "sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==", + "dev": true, + "license": "ISC", + "peerDependencies": { + "zod": "^3.24.1" + } } } } diff --git a/package.json b/package.json index 2616e03..41a0ea7 100644 --- a/package.json +++ b/package.json @@ -180,6 +180,11 @@ ], "default": "think", "description": "Thinking mode intensity level. Higher levels provide more detailed reasoning but consume more tokens." + }, + "claudeCodeChat.permissions.yoloMode": { + "type": "boolean", + "default": false, + "description": "Enable Yolo Mode to skip all permission checks. Use with caution as Claude can execute any command without asking." } } } @@ -202,6 +207,8 @@ "@vscode/test-electron": "^2.5.2", "@vscode/vsce": "^3.5.0", "eslint": "^9.25.1", - "typescript": "^5.8.3" + "typescript": "^5.8.3", + "@modelcontextprotocol/sdk": "^1.15.0", + "zod": "^3.25.76" } } diff --git a/src/extension.ts b/src/extension.ts index 2dacc0a..792752d 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -94,6 +94,9 @@ class ClaudeChatProvider { private _backupRepoPath: string | undefined; private _commits: Array<{ id: string, sha: string, message: string, timestamp: string }> = []; private _conversationsPath: string | undefined; + private _permissionRequestsPath: string | undefined; + private _permissionWatcher: vscode.FileSystemWatcher | undefined; + private _pendingPermissionResolvers: Map void> | undefined; private _currentConversation: Array<{ timestamp: string, messageType: string, data: any }> = []; private _conversationStartTime: string | undefined; private _conversationIndex: Array<{ @@ -117,6 +120,8 @@ class ClaudeChatProvider { // Initialize backup repository and conversations this._initializeBackupRepo(); this._initializeConversations(); + this._initializeMCPConfig(); + this._initializePermissions(); // Load conversation index from workspace state this._conversationIndex = this._context.workspaceState.get('claude.conversationIndex', []); @@ -211,6 +216,9 @@ class ClaudeChatProvider { // Send platform information to webview this._sendPlatformInfo(); + + // Send current settings to webview + this._sendCurrentSettings(); } private _handleWebviewMessage(message: any) { @@ -266,6 +274,18 @@ class ClaudeChatProvider { case 'createImageFile': this._createImageFile(message.imageData, message.imageType); return; + case 'permissionResponse': + this._handlePermissionResponse(message.id, message.approved, message.alwaysAllow); + return; + case 'getPermissions': + this._sendPermissions(); + return; + case 'removePermission': + this._removePermission(message.toolName, message.command); + return; + case 'addPermission': + this._addPermission(message.toolName, message.command); + return; } } @@ -393,10 +413,26 @@ class ClaudeChatProvider { // Build command arguments with session management const args = [ '-p', - '--output-format', 'stream-json', '--verbose', - '--dangerously-skip-permissions' + '--output-format', 'stream-json', '--verbose' ]; + // Get configuration + const config = vscode.workspace.getConfiguration('claudeCodeChat'); + const yoloMode = config.get('permissions.yoloMode', false); + + if (yoloMode) { + // Yolo mode: skip all permissions regardless of MCP config + args.push('--dangerously-skip-permissions'); + } else { + // Add MCP configuration for permissions + const mcpConfigPath = this.getMCPConfigPath(); + if (mcpConfigPath) { + args.push('--mcp-config', mcpConfigPath); + args.push('--allowedTools', 'mcp__permissions__approval_prompt'); + args.push('--permission-prompt-tool', 'mcp__permissions__approval_prompt'); + } + } + // Add model selection if not using default if (this._selectedModel && this._selectedModel !== 'default') { args.push('--model', this._selectedModel); @@ -412,9 +448,6 @@ class ClaudeChatProvider { } console.log('Claude command args:', args); - - // Get configuration - const config = vscode.workspace.getConfiguration('claudeCodeChat'); const wslEnabled = config.get('wsl.enabled', false); const wslDistro = config.get('wsl.distro', 'Ubuntu'); const nodePath = config.get('wsl.nodePath', '/usr/bin/node'); @@ -970,6 +1003,460 @@ class ClaudeChatProvider { } } + private async _initializeMCPConfig(): Promise { + try { + const storagePath = this._context.storageUri?.fsPath; + if (!storagePath) {return;} + + // Create MCP config directory + const mcpConfigDir = path.join(storagePath, 'mcp'); + try { + await vscode.workspace.fs.stat(vscode.Uri.file(mcpConfigDir)); + } catch { + await vscode.workspace.fs.createDirectory(vscode.Uri.file(mcpConfigDir)); + console.log(`Created MCP config directory at: ${mcpConfigDir}`); + } + + // Create mcp-servers.json with correct path to compiled MCP permissions server + const mcpConfigPath = path.join(mcpConfigDir, 'mcp-servers.json'); + const mcpPermissionsPath = path.join(this._extensionUri.fsPath, 'out', 'permissions', 'mcp-permissions.js'); + const permissionRequestsPath = path.join(storagePath, 'permission-requests'); + + const mcpConfig = { + mcpServers: { + permissions: { + command: 'node', + args: [mcpPermissionsPath], + env: { + CLAUDE_PERMISSIONS_PATH: permissionRequestsPath + } + } + } + }; + + const configContent = new TextEncoder().encode(JSON.stringify(mcpConfig, null, 2)); + await vscode.workspace.fs.writeFile(vscode.Uri.file(mcpConfigPath), configContent); + + console.log(`Created MCP config at: ${mcpConfigPath}`); + } catch (error: any) { + console.error('Failed to initialize MCP config:', error.message); + } + } + + private async _initializePermissions(): Promise { + try { + const storagePath = this._context.storageUri?.fsPath; + if (!storagePath) {return;} + + // Create permission requests directory + this._permissionRequestsPath = path.join(storagePath, 'permission-requests'); + try { + await vscode.workspace.fs.stat(vscode.Uri.file(this._permissionRequestsPath)); + } catch { + await vscode.workspace.fs.createDirectory(vscode.Uri.file(this._permissionRequestsPath)); + console.log(`Created permission requests directory at: ${this._permissionRequestsPath}`); + } + + // Set up file watcher for *.request files + this._permissionWatcher = vscode.workspace.createFileSystemWatcher( + new vscode.RelativePattern(this._permissionRequestsPath, '*.request') + ); + + this._permissionWatcher.onDidCreate(async (uri) => { + // Only handle file scheme URIs, ignore vscode-userdata scheme + if (uri.scheme === 'file') { + await this._handlePermissionRequest(uri); + } + }); + + this._disposables.push(this._permissionWatcher); + + } catch (error: any) { + console.error('Failed to initialize permissions:', error.message); + } + } + + private async _handlePermissionRequest(requestUri: vscode.Uri): Promise { + try { + // Read the request file + const content = await vscode.workspace.fs.readFile(requestUri); + const request = JSON.parse(new TextDecoder().decode(content)); + + // Show permission dialog + const approved = await this._showPermissionDialog(request); + + // Write response file + const responseFile = requestUri.fsPath.replace('.request', '.response'); + const response = { + id: request.id, + approved: approved, + timestamp: new Date().toISOString() + }; + + const responseContent = new TextEncoder().encode(JSON.stringify(response)); + await vscode.workspace.fs.writeFile(vscode.Uri.file(responseFile), responseContent); + + // Clean up request file + await vscode.workspace.fs.delete(requestUri); + + } catch (error: any) { + console.error('Failed to handle permission request:', error.message); + } + } + + private async _showPermissionDialog(request: any): Promise { + const toolName = request.tool || 'Unknown Tool'; + + // Generate pattern for Bash commands + let pattern = undefined; + if (toolName === 'Bash' && request.input?.command) { + pattern = this.getCommandPattern(request.input.command); + } + + // Send permission request to the UI + this._postMessage({ + type: 'permissionRequest', + data: { + id: request.id, + tool: toolName, + input: request.input, + pattern: pattern + } + }); + + // Wait for response from UI + return new Promise((resolve) => { + // Store the resolver so we can call it when we get the response + this._pendingPermissionResolvers = this._pendingPermissionResolvers || new Map(); + this._pendingPermissionResolvers.set(request.id, resolve); + }); + } + + private _handlePermissionResponse(id: string, approved: boolean, alwaysAllow?: boolean): void { + if (this._pendingPermissionResolvers && this._pendingPermissionResolvers.has(id)) { + const resolver = this._pendingPermissionResolvers.get(id); + if (resolver) { + resolver(approved); + this._pendingPermissionResolvers.delete(id); + + // Handle always allow setting + if (alwaysAllow && approved) { + void this._saveAlwaysAllowPermission(id); + } + } + } + } + + private async _saveAlwaysAllowPermission(requestId: string): Promise { + try { + // Read the original request to get tool name and input + const storagePath = this._context.storageUri?.fsPath; + if (!storagePath) return; + + const requestFileUri = vscode.Uri.file(path.join(storagePath, 'permission-requests', `${requestId}.request`)); + + let requestContent: Uint8Array; + try { + requestContent = await vscode.workspace.fs.readFile(requestFileUri); + } catch { + return; // Request file doesn't exist + } + + const request = JSON.parse(new TextDecoder().decode(requestContent)); + + // Load existing workspace permissions + const permissionsUri = vscode.Uri.file(path.join(storagePath, 'permission-requests', 'permissions.json')); + let permissions: any = { alwaysAllow: {} }; + + try { + const content = await vscode.workspace.fs.readFile(permissionsUri); + permissions = JSON.parse(new TextDecoder().decode(content)); + } catch { + // File doesn't exist yet, use default permissions + } + + // Add the new permission + const toolName = request.tool; + if (toolName === 'Bash' && request.input?.command) { + // For Bash, store the command pattern + if (!permissions.alwaysAllow[toolName]) { + permissions.alwaysAllow[toolName] = []; + } + if (Array.isArray(permissions.alwaysAllow[toolName])) { + const command = request.input.command.trim(); + const pattern = this.getCommandPattern(command); + if (!permissions.alwaysAllow[toolName].includes(pattern)) { + permissions.alwaysAllow[toolName].push(pattern); + } + } + } else { + // For other tools, allow all instances + permissions.alwaysAllow[toolName] = true; + } + + // Ensure permissions directory exists + const permissionsDir = vscode.Uri.file(path.dirname(permissionsUri.fsPath)); + try { + await vscode.workspace.fs.stat(permissionsDir); + } catch { + await vscode.workspace.fs.createDirectory(permissionsDir); + } + + // Save the permissions + const permissionsContent = new TextEncoder().encode(JSON.stringify(permissions, null, 2)); + await vscode.workspace.fs.writeFile(permissionsUri, permissionsContent); + + console.log(`Saved always-allow permission for ${toolName}`); + } catch (error) { + console.error('Error saving always-allow permission:', error); + } + } + + private getCommandPattern(command: string): string { + const parts = command.trim().split(/\s+/); + if (parts.length === 0) return command; + + const baseCmd = parts[0]; + const subCmd = parts.length > 1 ? parts[1] : ''; + + // Common patterns that should use wildcards + const patterns = [ + // Package managers + ['npm', 'install', 'npm install *'], + ['npm', 'i', 'npm i *'], + ['npm', 'add', 'npm add *'], + ['npm', 'remove', 'npm remove *'], + ['npm', 'uninstall', 'npm uninstall *'], + ['npm', 'update', 'npm update *'], + ['npm', 'run', 'npm run *'], + ['yarn', 'add', 'yarn add *'], + ['yarn', 'remove', 'yarn remove *'], + ['yarn', 'install', 'yarn install *'], + ['pnpm', 'install', 'pnpm install *'], + ['pnpm', 'add', 'pnpm add *'], + ['pnpm', 'remove', 'pnpm remove *'], + + // Git commands + ['git', 'add', 'git add *'], + ['git', 'commit', 'git commit *'], + ['git', 'push', 'git push *'], + ['git', 'pull', 'git pull *'], + ['git', 'checkout', 'git checkout *'], + ['git', 'branch', 'git branch *'], + ['git', 'merge', 'git merge *'], + ['git', 'clone', 'git clone *'], + ['git', 'reset', 'git reset *'], + ['git', 'rebase', 'git rebase *'], + ['git', 'tag', 'git tag *'], + + // Docker commands + ['docker', 'run', 'docker run *'], + ['docker', 'build', 'docker build *'], + ['docker', 'exec', 'docker exec *'], + ['docker', 'logs', 'docker logs *'], + ['docker', 'stop', 'docker stop *'], + ['docker', 'start', 'docker start *'], + ['docker', 'rm', 'docker rm *'], + ['docker', 'rmi', 'docker rmi *'], + ['docker', 'pull', 'docker pull *'], + ['docker', 'push', 'docker push *'], + + // Build tools + ['make', '', 'make *'], + ['cargo', 'build', 'cargo build *'], + ['cargo', 'run', 'cargo run *'], + ['cargo', 'test', 'cargo test *'], + ['cargo', 'install', 'cargo install *'], + ['mvn', 'compile', 'mvn compile *'], + ['mvn', 'test', 'mvn test *'], + ['mvn', 'package', 'mvn package *'], + ['gradle', 'build', 'gradle build *'], + ['gradle', 'test', 'gradle test *'], + + // System commands + ['curl', '', 'curl *'], + ['wget', '', 'wget *'], + ['ssh', '', 'ssh *'], + ['scp', '', 'scp *'], + ['rsync', '', 'rsync *'], + ['tar', '', 'tar *'], + ['zip', '', 'zip *'], + ['unzip', '', 'unzip *'], + + // Development tools + ['node', '', 'node *'], + ['python', '', 'python *'], + ['python3', '', 'python3 *'], + ['pip', 'install', 'pip install *'], + ['pip3', 'install', 'pip3 install *'], + ['composer', 'install', 'composer install *'], + ['composer', 'require', 'composer require *'], + ['bundle', 'install', 'bundle install *'], + ['gem', 'install', 'gem install *'], + ]; + + // Find matching pattern + for (const [cmd, sub, pattern] of patterns) { + if (baseCmd === cmd && (sub === '' || subCmd === sub)) { + return pattern; + } + } + + // Default: return exact command + return command; + } + + private async _sendPermissions(): Promise { + try { + const storagePath = this._context.storageUri?.fsPath; + if (!storagePath) { + this._postMessage({ + type: 'permissionsData', + data: { alwaysAllow: {} } + }); + return; + } + + const permissionsUri = vscode.Uri.file(path.join(storagePath, 'permission-requests', 'permissions.json')); + let permissions: any = { alwaysAllow: {} }; + + try { + const content = await vscode.workspace.fs.readFile(permissionsUri); + permissions = JSON.parse(new TextDecoder().decode(content)); + } catch { + // File doesn't exist or can't be read, use default permissions + } + + this._postMessage({ + type: 'permissionsData', + data: permissions + }); + } catch (error) { + console.error('Error sending permissions:', error); + this._postMessage({ + type: 'permissionsData', + data: { alwaysAllow: {} } + }); + } + } + + private async _removePermission(toolName: string, command: string | null): Promise { + try { + const storagePath = this._context.storageUri?.fsPath; + if (!storagePath) return; + + const permissionsUri = vscode.Uri.file(path.join(storagePath, 'permission-requests', 'permissions.json')); + let permissions: any = { alwaysAllow: {} }; + + try { + const content = await vscode.workspace.fs.readFile(permissionsUri); + permissions = JSON.parse(new TextDecoder().decode(content)); + } catch { + // File doesn't exist or can't be read, nothing to remove + return; + } + + // Remove the permission + if (command === null) { + // Remove entire tool permission + delete permissions.alwaysAllow[toolName]; + } else { + // Remove specific command from tool permissions + if (Array.isArray(permissions.alwaysAllow[toolName])) { + permissions.alwaysAllow[toolName] = permissions.alwaysAllow[toolName].filter( + (cmd: string) => cmd !== command + ); + // If no commands left, remove the tool entirely + if (permissions.alwaysAllow[toolName].length === 0) { + delete permissions.alwaysAllow[toolName]; + } + } + } + + // Save updated permissions + const permissionsContent = new TextEncoder().encode(JSON.stringify(permissions, null, 2)); + await vscode.workspace.fs.writeFile(permissionsUri, permissionsContent); + + // Send updated permissions to UI + this._sendPermissions(); + + console.log(`Removed permission for ${toolName}${command ? ` command: ${command}` : ''}`); + } catch (error) { + console.error('Error removing permission:', error); + } + } + + private async _addPermission(toolName: string, command: string | null): Promise { + try { + const storagePath = this._context.storageUri?.fsPath; + if (!storagePath) return; + + const permissionsUri = vscode.Uri.file(path.join(storagePath, 'permission-requests', 'permissions.json')); + let permissions: any = { alwaysAllow: {} }; + + try { + const content = await vscode.workspace.fs.readFile(permissionsUri); + permissions = JSON.parse(new TextDecoder().decode(content)); + } catch { + // File doesn't exist, use default permissions + } + + // Add the new permission + if (command === null || command === '') { + // Allow all commands for this tool + permissions.alwaysAllow[toolName] = true; + } else { + // Add specific command pattern + if (!permissions.alwaysAllow[toolName]) { + permissions.alwaysAllow[toolName] = []; + } + + // Convert to array if it's currently set to true + if (permissions.alwaysAllow[toolName] === true) { + permissions.alwaysAllow[toolName] = []; + } + + if (Array.isArray(permissions.alwaysAllow[toolName])) { + // For Bash commands, convert to pattern using existing logic + let commandToAdd = command; + if (toolName === 'Bash') { + commandToAdd = this.getCommandPattern(command); + } + + // Add if not already present + if (!permissions.alwaysAllow[toolName].includes(commandToAdd)) { + permissions.alwaysAllow[toolName].push(commandToAdd); + } + } + } + + // Ensure permissions directory exists + const permissionsDir = vscode.Uri.file(path.dirname(permissionsUri.fsPath)); + try { + await vscode.workspace.fs.stat(permissionsDir); + } catch { + await vscode.workspace.fs.createDirectory(permissionsDir); + } + + // Save updated permissions + const permissionsContent = new TextEncoder().encode(JSON.stringify(permissions, null, 2)); + await vscode.workspace.fs.writeFile(permissionsUri, permissionsContent); + + // Send updated permissions to UI + this._sendPermissions(); + + console.log(`Added permission for ${toolName}${command ? ` command: ${command}` : ' (all commands)'}`); + } catch (error) { + console.error('Error adding permission:', error); + } + } + + public getMCPConfigPath(): string | undefined { + const storagePath = this._context.storageUri?.fsPath; + if (!storagePath) {return undefined;} + return path.join(storagePath, 'mcp', 'mcp-servers.json'); + } + private _sendAndSaveMessage(message: { type: string, data: any }): void { // Initialize conversation if this is the first message if (this._currentConversation.length === 0) { @@ -1286,7 +1773,8 @@ class ClaudeChatProvider { 'wsl.enabled': config.get('wsl.enabled', false), 'wsl.distro': config.get('wsl.distro', 'Ubuntu'), 'wsl.nodePath': config.get('wsl.nodePath', '/usr/bin/node'), - 'wsl.claudePath': config.get('wsl.claudePath', '/usr/local/bin/claude') + 'wsl.claudePath': config.get('wsl.claudePath', '/usr/local/bin/claude'), + 'permissions.yoloMode': config.get('permissions.yoloMode', false) }; this._postMessage({ diff --git a/src/permissions/mcp-permissions.ts b/src/permissions/mcp-permissions.ts new file mode 100644 index 0000000..6445a42 --- /dev/null +++ b/src/permissions/mcp-permissions.ts @@ -0,0 +1,212 @@ +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { z } from "zod"; +import * as fs from "fs"; +import * as path from "path"; + +const server = new McpServer({ + name: "Claude Code Permissions MCP Server", + version: "0.0.1", +}); + +// Get permissions directory from environment +const PERMISSIONS_PATH = process.env.CLAUDE_PERMISSIONS_PATH; +if (!PERMISSIONS_PATH) { + console.error("CLAUDE_PERMISSIONS_PATH environment variable not set"); + process.exit(1); +} + + +interface WorkspacePermissions { + alwaysAllow: { + [toolName: string]: boolean | string[]; // true for all, or array of allowed commands/patterns + }; +} + +function getWorkspacePermissionsPath(): string | null { + if (!PERMISSIONS_PATH) return null; + return path.join(PERMISSIONS_PATH, 'permissions.json'); +} + +function loadWorkspacePermissions(): WorkspacePermissions { + const permissionsPath = getWorkspacePermissionsPath(); + if (!permissionsPath || !fs.existsSync(permissionsPath)) { + return { alwaysAllow: {} }; + } + + try { + const content = fs.readFileSync(permissionsPath, 'utf8'); + return JSON.parse(content); + } catch (error) { + console.error(`Error loading workspace permissions: ${error}`); + return { alwaysAllow: {} }; + } +} + + +function isAlwaysAllowed(toolName: string, input: any): boolean { + const permissions = loadWorkspacePermissions(); + const toolPermission = permissions.alwaysAllow[toolName]; + + if (!toolPermission) return false; + + // If it's true, always allow + if (toolPermission === true) return true; + + // If it's an array, check for specific commands (mainly for Bash) + if (Array.isArray(toolPermission)) { + if (toolName === 'Bash' && input.command) { + const command = input.command.trim(); + return toolPermission.some(allowedCmd => { + // Support exact match or pattern matching + if (allowedCmd.includes('*')) { + // Handle patterns like "npm i *" to match both "npm i" and "npm i something" + const baseCommand = allowedCmd.replace(' *', ''); + if (command === baseCommand) { + return true; // Exact match for base command + } + // Pattern match for command with arguments + const pattern = allowedCmd.replace(/\*/g, '.*'); + return new RegExp(`^${pattern}$`).test(command); + } + return command.startsWith(allowedCmd); + }); + } + } + + return false; +} + +function generateRequestId(): string { + return `req_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`; +} + +async function requestPermission(tool_name: string, input: any): Promise<{approved: boolean, reason?: string}> { + if (!PERMISSIONS_PATH) { + console.error("Permissions path not available"); + return { approved: false, reason: "Permissions path not configured" }; + } + + // Check if this tool/command is always allowed for this workspace + if (isAlwaysAllowed(tool_name, input)) { + console.error(`Tool ${tool_name} is always allowed for this workspace`); + return { approved: true }; + } + + const requestId = generateRequestId(); + const requestFile = path.join(PERMISSIONS_PATH, `${requestId}.request`); + const responseFile = path.join(PERMISSIONS_PATH, `${requestId}.response`); + + // Write request file + const request = { + id: requestId, + tool: tool_name, + input: input, + timestamp: new Date().toISOString() + }; + + try { + fs.writeFileSync(requestFile, JSON.stringify(request, null, 2)); + + // Use fs.watch to wait for response file + return new Promise<{approved: boolean, reason?: string}>((resolve) => { + const timeout = setTimeout(() => { + watcher.close(); + // Clean up request file on timeout + if (fs.existsSync(requestFile)) { + fs.unlinkSync(requestFile); + } + console.error(`Permission request ${requestId} timed out`); + resolve({ approved: false, reason: "Permission request timed out" }); + }, 3600000); // 1 hour timeout + + const watcher = fs.watch(PERMISSIONS_PATH, (eventType, filename) => { + if (eventType === 'rename' && filename === path.basename(responseFile)) { + // Check if file exists (rename event can be for creation or deletion) + if (fs.existsSync(responseFile)) { + try { + const responseContent = fs.readFileSync(responseFile, 'utf8'); + const response = JSON.parse(responseContent); + + // Clean up response file + fs.unlinkSync(responseFile); + + // Clear timeout and close watcher + clearTimeout(timeout); + watcher.close(); + + resolve({ + approved: response.approved, + reason: response.approved ? undefined : "User rejected the request" + }); + } catch (error) { + console.error(`Error reading response file: ${error}`); + // Continue watching in case of read error + } + } + } + }); + + // Handle watcher errors + watcher.on('error', (error) => { + console.error(`File watcher error: ${error}`); + clearTimeout(timeout); + watcher.close(); + resolve({ approved: false, reason: "File watcher error" }); + }); + }); + + } catch (error) { + console.error(`Error requesting permission: ${error}`); + return { approved: false, reason: `Error processing permission request: ${error}` }; + } +} + +server.tool( + "approval_prompt", + 'Request user permission to execute a tool via VS Code dialog', + { + tool_name: z.string().describe("The name of the tool requesting permission"), + input: z.object({}).passthrough().describe("The input for the tool"), + tool_use_id: z.string().optional().describe("The unique tool use request ID"), + }, + async ({ tool_name, input }) => { + console.error(`Requesting permission for tool: ${tool_name}`); + + const permissionResult = await requestPermission(tool_name, input); + + const behavior = permissionResult.approved ? "allow" : "deny"; + console.error(`Permission ${behavior}ed for tool: ${tool_name}`); + + return { + content: [ + { + type: "text", + text: behavior === "allow" ? + JSON.stringify({ + behavior: behavior, + updatedInput: input, + }) + : + JSON.stringify({ + behavior: behavior, + message: permissionResult.reason || "Permission denied", + }) + , + }, + ], + }; + } +); + +async function main() { + const transport = new StdioServerTransport(); + await server.connect(transport); + console.error(`Permissions MCP Server running on stdio`); + console.error(`Using permissions directory: ${PERMISSIONS_PATH}`); +} + +main().catch((error) => { + console.error("Fatal error in main():", error); + process.exit(1); +}); \ No newline at end of file diff --git a/src/permissions/mcp-servers.json b/src/permissions/mcp-servers.json new file mode 100644 index 0000000..ac4a40d --- /dev/null +++ b/src/permissions/mcp-servers.json @@ -0,0 +1,10 @@ +{ + "mcpServers": { + "permissions": { + "command": "node", + "args": [ + "./out/permissions/mcp-permissions.js" + ] + } + } +} \ No newline at end of file diff --git a/src/ui-styles.ts b/src/ui-styles.ts index e137898..f62171d 100644 --- a/src/ui-styles.ts +++ b/src/ui-styles.ts @@ -83,6 +83,436 @@ const styles = ` opacity: 1; } + /* Permission Request */ + .permission-request { + margin: 4px 12px 20px 12px; + background-color: var(--vscode-inputValidation-warningBackground); + border: 1px solid var(--vscode-inputValidation-warningBorder); + border-radius: 8px; + padding: 16px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + animation: slideUp 0.3s ease; + } + + .permission-header { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 12px; + font-weight: 600; + color: var(--vscode-foreground); + } + + .permission-header .icon { + font-size: 16px; + } + + .permission-content { + font-size: 13px; + line-height: 1.4; + color: var(--vscode-descriptionForeground); + } + + + + .permission-tool { + font-family: var(--vscode-editor-font-family); + background-color: var(--vscode-editor-background); + border: 1px solid var(--vscode-panel-border); + border-radius: 4px; + padding: 8px 10px; + margin: 8px 0; + font-size: 12px; + color: var(--vscode-editor-foreground); + } + + .permission-buttons { + margin-top: 2px; + display: flex; + gap: 8px; + justify-content: flex-end; + flex-wrap: wrap; + } + + .permission-buttons .btn { + font-size: 12px; + padding: 6px 12px; + min-width: 70px; + text-align: center; + display: inline-flex; + align-items: center; + justify-content: center; + height: 28px; + border-radius: 4px; + border: 1px solid; + cursor: pointer; + transition: all 0.2s ease; + white-space: nowrap; + box-sizing: border-box; + } + + .permission-buttons .btn.allow { + background-color: var(--vscode-button-background); + color: var(--vscode-button-foreground); + border-color: var(--vscode-button-background); + } + + .permission-buttons .btn.allow:hover { + background-color: var(--vscode-button-hoverBackground); + } + + .permission-buttons .btn.deny { + background-color: transparent; + color: var(--vscode-foreground); + border-color: var(--vscode-panel-border); + } + + .permission-buttons .btn.deny:hover { + background-color: var(--vscode-list-hoverBackground); + border-color: var(--vscode-focusBorder); + } + + .permission-buttons .btn.always-allow { + background-color: rgba(0, 122, 204, 0.1); + color: var(--vscode-charts-blue); + border-color: rgba(0, 122, 204, 0.3); + font-weight: 500; + min-width: auto; + padding: 6px 14px; + height: 28px; + } + + .permission-buttons .btn.always-allow:hover { + background-color: rgba(0, 122, 204, 0.2); + border-color: rgba(0, 122, 204, 0.5); + transform: translateY(-1px); + } + + .permission-buttons .btn.always-allow code { + background-color: rgba(0, 0, 0, 0.2); + padding: 2px 4px; + border-radius: 3px; + font-family: var(--vscode-editor-font-family); + font-size: 11px; + color: var(--vscode-editor-foreground); + margin-left: 4px; + display: inline; + line-height: 1; + vertical-align: baseline; + } + + .permission-decision { + font-size: 13px; + font-weight: 600; + padding: 8px 12px; + text-align: center; + border-radius: 4px; + margin-top: 8px; + } + + .permission-decision.allowed { + background-color: rgba(0, 122, 204, 0.15); + color: var(--vscode-charts-blue); + border: 1px solid rgba(0, 122, 204, 0.3); + } + + .permission-decision.denied { + background-color: rgba(231, 76, 60, 0.15); + color: #e74c3c; + border: 1px solid rgba(231, 76, 60, 0.3); + } + + .permission-decided { + opacity: 0.7; + pointer-events: none; + } + + .permission-decided .permission-buttons { + display: none; + } + + .permission-decided.allowed { + border-color: var(--vscode-inputValidation-infoBackground); + background-color: rgba(0, 122, 204, 0.1); + } + + .permission-decided.denied { + border-color: var(--vscode-inputValidation-errorBorder); + background-color: var(--vscode-inputValidation-errorBackground); + } + + /* Permissions Management */ + .permissions-list { + max-height: 300px; + overflow-y: auto; + border: 1px solid var(--vscode-panel-border); + border-radius: 6px; + background-color: var(--vscode-input-background); + margin-top: 8px; + } + + .permission-item { + display: flex; + justify-content: space-between; + align-items: center; + padding-left: 6px; + padding-right: 6px; + border-bottom: 1px solid var(--vscode-panel-border); + transition: background-color 0.2s ease; + min-height: 32px; + } + + .permission-item:hover { + background-color: var(--vscode-list-hoverBackground); + } + + .permission-item:last-child { + border-bottom: none; + } + + .permission-info { + display: flex; + align-items: center; + gap: 8px; + flex-grow: 1; + min-width: 0; + } + + .permission-tool { + background-color: var(--vscode-badge-background); + color: var(--vscode-badge-foreground); + padding: 3px 6px; + border-radius: 3px; + font-size: 10px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; + flex-shrink: 0; + height: 18px; + display: inline-flex; + align-items: center; + line-height: 1; + } + + .permission-command { + font-size: 12px; + color: var(--vscode-foreground); + flex-grow: 1; + } + + .permission-command code { + background-color: var(--vscode-textCodeBlock-background); + padding: 3px 6px; + border-radius: 3px; + font-family: var(--vscode-editor-font-family); + color: var(--vscode-textLink-foreground); + font-size: 11px; + height: 18px; + display: inline-flex; + align-items: center; + line-height: 1; + } + + .permission-desc { + color: var(--vscode-descriptionForeground); + font-size: 11px; + font-style: italic; + flex-grow: 1; + height: 18px; + display: inline-flex; + align-items: center; + line-height: 1; + } + + .permission-remove-btn { + background-color: transparent; + color: var(--vscode-descriptionForeground); + border: none; + padding: 4px 8px; + border-radius: 3px; + cursor: pointer; + font-size: 10px; + transition: all 0.2s ease; + font-weight: 500; + flex-shrink: 0; + opacity: 0.7; + } + + .permission-remove-btn:hover { + background-color: rgba(231, 76, 60, 0.1); + color: var(--vscode-errorForeground); + opacity: 1; + } + + .permissions-empty { + padding: 16px; + text-align: center; + color: var(--vscode-descriptionForeground); + font-style: italic; + font-size: 13px; + } + + .permissions-empty::before { + content: "🔒"; + display: block; + font-size: 16px; + margin-bottom: 8px; + opacity: 0.5; + } + + /* Add Permission Form */ + .permissions-add-section { + margin-top: 6px; + } + + .permissions-show-add-btn { + background-color: transparent; + color: var(--vscode-descriptionForeground); + border: 1px solid var(--vscode-panel-border); + border-radius: 3px; + padding: 6px 8px; + font-size: 11px; + cursor: pointer; + transition: all 0.2s ease; + font-weight: 400; + opacity: 0.7; + } + + .permissions-show-add-btn:hover { + background-color: var(--vscode-list-hoverBackground); + opacity: 1; + } + + .permissions-add-form { + margin-top: 8px; + padding: 12px; + border: 1px solid var(--vscode-panel-border); + border-radius: 6px; + background-color: var(--vscode-input-background); + animation: slideDown 0.2s ease; + } + + @keyframes slideDown { + from { + opacity: 0; + transform: translateY(-10px); + } + to { + opacity: 1; + transform: translateY(0); + } + } + + .permissions-form-row { + display: flex; + gap: 8px; + align-items: center; + margin-bottom: 8px; + } + + .permissions-tool-select { + background-color: var(--vscode-input-background); + color: var(--vscode-input-foreground); + border: 1px solid var(--vscode-panel-border); + border-radius: 3px; + padding: 4px 8px; + font-size: 12px; + min-width: 100px; + height: 24px; + flex-shrink: 0; + } + + .permissions-command-input { + background-color: var(--vscode-input-background); + color: var(--vscode-input-foreground); + border: 1px solid var(--vscode-panel-border); + border-radius: 3px; + padding: 4px 8px; + font-size: 12px; + flex-grow: 1; + height: 24px; + font-family: var(--vscode-editor-font-family); + } + + .permissions-command-input::placeholder { + color: var(--vscode-input-placeholderForeground); + } + + .permissions-add-btn { + background-color: var(--vscode-button-background); + color: var(--vscode-button-foreground); + border: none; + border-radius: 3px; + padding: 4px 12px; + font-size: 12px; + cursor: pointer; + transition: background-color 0.2s ease; + height: 24px; + font-weight: 500; + flex-shrink: 0; + } + + .permissions-add-btn:hover { + background-color: var(--vscode-button-hoverBackground); + } + + .permissions-add-btn:disabled { + background-color: var(--vscode-button-secondaryBackground); + color: var(--vscode-button-secondaryForeground); + cursor: not-allowed; + opacity: 0.5; + } + + .permissions-cancel-btn { + background-color: transparent; + color: var(--vscode-foreground); + border: 1px solid var(--vscode-panel-border); + border-radius: 3px; + padding: 4px 12px; + font-size: 12px; + cursor: pointer; + transition: all 0.2s ease; + height: 24px; + font-weight: 500; + } + + .permissions-cancel-btn:hover { + background-color: var(--vscode-list-hoverBackground); + border-color: var(--vscode-focusBorder); + } + + .permissions-form-hint { + font-size: 11px; + color: var(--vscode-descriptionForeground); + font-style: italic; + line-height: 1.3; + } + + .yolo-mode-section { + display: flex; + align-items: center; + gap: 6px; + margin-top: 12px; + opacity: 0.8; + transition: opacity 0.2s ease; + } + + .yolo-mode-section:hover { + opacity: 1; + } + + .yolo-mode-section input[type="checkbox"] { + transform: scale(0.8); + margin: 0; + } + + .yolo-mode-section label { + font-size: 10px; + color: var(--vscode-descriptionForeground); + cursor: pointer; + font-weight: 400; + } + /* WSL Alert */ .wsl-alert { margin: 8px 12px; @@ -923,13 +1353,52 @@ const styles = ` gap: 8px; } - .beta-warning { - font-size: 11px; - color: var(--vscode-descriptionForeground); + .yolo-warning { + font-size: 12px; + color: var(--vscode-inputValidation-errorForeground); text-align: center; - font-style: italic; - background-color: var(--vscode-panel-background); - padding: 4px + font-weight: 500; + background-color: var(--vscode-inputValidation-errorBackground); + border: 1px solid var(--vscode-inputValidation-errorBorder); + padding: 8px 12px; + margin: 4px 12px; + border-radius: 4px; + animation: slideDown 0.3s ease; + } + + .yolo-suggestion { + margin-top: 12px; + padding: 12px; + background-color: rgba(0, 122, 204, 0.1); + border: 1px solid rgba(0, 122, 204, 0.3); + border-radius: 6px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + } + + .yolo-suggestion-text { + font-size: 12px; + color: var(--vscode-foreground); + flex-grow: 1; + } + + .yolo-suggestion-btn { + background-color: var(--vscode-button-background); + color: var(--vscode-button-foreground); + border: none; + border-radius: 4px; + padding: 6px 12px; + font-size: 11px; + cursor: pointer; + transition: background-color 0.2s ease; + font-weight: 500; + flex-shrink: 0; + } + + .yolo-suggestion-btn:hover { + background-color: var(--vscode-button-hoverBackground); } .file-picker-modal { diff --git a/src/ui.ts b/src/ui.ts index 6cdfe24..bf38f15 100644 --- a/src/ui.ts +++ b/src/ui.ts @@ -131,8 +131,8 @@ const html = ` -
- In Beta. All Claude Code tools are allowed. Use at your own risk. + @@ -248,6 +248,53 @@ const html = `
+

Permissions

+
+

+ Manage commands and tools that are automatically allowed without asking for permission. +

+
+
+
+
+ Loading permissions... +
+
+
+ + +
+ + +
+
+
+

MCP Configuration (coming soon)

@@ -592,6 +639,20 @@ const html = ` } messageDiv.appendChild(contentDiv); + + // Check if this is a permission-related error and add yolo mode button + if (type === 'error' && isPermissionError(content)) { + const yoloSuggestion = document.createElement('div'); + yoloSuggestion.className = 'yolo-suggestion'; + yoloSuggestion.innerHTML = \` +

+ 💡 This looks like a permission issue. You can enable Yolo Mode to skip all permission checks. +
+ + \`; + messageDiv.appendChild(yoloSuggestion); + } + messagesDiv.appendChild(messageDiv); messagesDiv.scrollTop = messagesDiv.scrollHeight; } @@ -776,6 +837,20 @@ const html = ` } messageDiv.appendChild(contentDiv); + + // Check if this is a permission-related error and add yolo mode button + if (data.isError && isPermissionError(content)) { + const yoloSuggestion = document.createElement('div'); + yoloSuggestion.className = 'yolo-suggestion'; + yoloSuggestion.innerHTML = \` +
+ 💡 This looks like a permission issue. You can enable Yolo Mode to skip all permission checks. +
+ + \`; + messageDiv.appendChild(yoloSuggestion); + } + messagesDiv.appendChild(messageDiv); messagesDiv.scrollTop = messagesDiv.scrollHeight; } @@ -1422,6 +1497,54 @@ const html = ` function showToolsModal() { document.getElementById('toolsModal').style.display = 'flex'; } + + function updateYoloWarning() { + const yoloModeCheckbox = document.getElementById('yolo-mode'); + const warning = document.getElementById('yoloWarning'); + + if (!yoloModeCheckbox || !warning) { + return; // Elements not ready yet + } + + const yoloMode = yoloModeCheckbox.checked; + warning.style.display = yoloMode ? 'block' : 'none'; + } + + function isPermissionError(content) { + const permissionErrorPatterns = [ + 'Error: MCP config file not found', + 'Error: MCP tool', + 'Claude requested permissions to use', + 'permission denied', + 'Permission denied', + 'permission request', + 'Permission request', + 'EACCES', + 'permission error', + 'Permission error' + ]; + + return permissionErrorPatterns.some(pattern => + content.toLowerCase().includes(pattern.toLowerCase()) + ); + } + + function enableYoloMode() { + // Update the checkbox + const yoloModeCheckbox = document.getElementById('yolo-mode'); + if (yoloModeCheckbox) { + yoloModeCheckbox.checked = true; + + // Trigger the settings update + updateSettings(); + + // Show confirmation message + addMessage('✅ Yolo Mode enabled! All permission checks will be bypassed for future commands.', 'system'); + + // Update the warning banner + updateYoloWarning(); + } + } function hideToolsModal() { document.getElementById('toolsModal').style.display = 'none'; @@ -1960,9 +2083,86 @@ const html = ` // Display notification about checking the terminal addMessage(message.data, 'system'); break; + case 'permissionRequest': + addPermissionRequestMessage(message.data); + break; } }); + // Permission request functions + function addPermissionRequestMessage(data) { + const messageDiv = document.createElement('div'); + messageDiv.className = 'message permission-request'; + + const toolName = data.tool || 'Unknown Tool'; + + // Create always allow button text with command styling for Bash + let alwaysAllowText = \`Always allow \${toolName}\`; + let alwaysAllowTooltip = ''; + if (toolName === 'Bash' && data.pattern) { + const pattern = data.pattern; + // Remove the asterisk for display - show "npm i" instead of "npm i *" + const displayPattern = pattern.replace(' *', ''); + const truncatedPattern = displayPattern.length > 30 ? displayPattern.substring(0, 30) + '...' : displayPattern; + alwaysAllowText = \`Always allow \${truncatedPattern}\`; + alwaysAllowTooltip = displayPattern.length > 30 ? \`title="\${displayPattern}"\` : ''; + } + + messageDiv.innerHTML = \` +
+ 🔐 + Permission Required +
+
+

Allow \${toolName} to execute the tool call above?

+
+ + + +
+
+ \`; + + messagesDiv.appendChild(messageDiv); + messagesDiv.scrollTop = messagesDiv.scrollHeight; + } + + function respondToPermission(id, approved, alwaysAllow = false) { + // Send response back to extension + vscode.postMessage({ + type: 'permissionResponse', + id: id, + approved: approved, + alwaysAllow: alwaysAllow + }); + + // Update the UI to show the decision + const permissionMsg = document.querySelector(\`.permission-request:has([onclick*="\${id}"])\`); + if (permissionMsg) { + const buttons = permissionMsg.querySelector('.permission-buttons'); + const permissionContent = permissionMsg.querySelector('.permission-content'); + let decision = approved ? 'You allowed this' : 'You denied this'; + + if (alwaysAllow && approved) { + decision = 'You allowed this and set it to always allow'; + } + + const emoji = approved ? '✅' : '❌'; + const decisionClass = approved ? 'allowed' : 'denied'; + + // Hide buttons + buttons.style.display = 'none'; + + // Add decision div to permission-content + const decisionDiv = document.createElement('div'); + decisionDiv.className = \`permission-decision \${decisionClass}\`; + decisionDiv.innerHTML = \`\${emoji} \${decision}\`; + permissionContent.appendChild(decisionDiv); + + permissionMsg.classList.add('permission-decided', decisionClass); + } + } + // Session management functions function newSession() { vscode.postMessage({ @@ -2357,6 +2557,10 @@ const html = ` vscode.postMessage({ type: 'getSettings' }); + // Request current permissions + vscode.postMessage({ + type: 'getPermissions' + }); settingsModal.style.display = 'flex'; } else { hideSettingsModal(); @@ -2374,6 +2578,7 @@ const html = ` const wslDistro = document.getElementById('wsl-distro').value; const wslNodePath = document.getElementById('wsl-node-path').value; const wslClaudePath = document.getElementById('wsl-claude-path').value; + const yoloMode = document.getElementById('yolo-mode').checked; // Update WSL options visibility document.getElementById('wslOptions').style.display = wslEnabled ? 'block' : 'none'; @@ -2385,11 +2590,139 @@ const html = ` 'wsl.enabled': wslEnabled, 'wsl.distro': wslDistro || 'Ubuntu', 'wsl.nodePath': wslNodePath || '/usr/bin/node', - 'wsl.claudePath': wslClaudePath || '/usr/local/bin/claude' + 'wsl.claudePath': wslClaudePath || '/usr/local/bin/claude', + 'permissions.yoloMode': yoloMode } }); } + // Permissions management functions + function renderPermissions(permissions) { + const permissionsList = document.getElementById('permissionsList'); + + if (!permissions || !permissions.alwaysAllow || Object.keys(permissions.alwaysAllow).length === 0) { + permissionsList.innerHTML = \` +
+ No always-allow permissions set +
+ \`; + return; + } + + let html = ''; + + for (const [toolName, permission] of Object.entries(permissions.alwaysAllow)) { + if (permission === true) { + // Tool is always allowed + html += \` +
+
+ \${toolName} + All +
+ +
+ \`; + } else if (Array.isArray(permission)) { + // Tool has specific commands/patterns + for (const command of permission) { + const displayCommand = command.replace(' *', ''); // Remove asterisk for display + html += \` +
+
+ \${toolName} + \${displayCommand} +
+ +
+ \`; + } + } + } + + permissionsList.innerHTML = html; + } + + function removePermission(toolName, command) { + vscode.postMessage({ + type: 'removePermission', + toolName: toolName, + command: command + }); + } + + function showAddPermissionForm() { + document.getElementById('showAddPermissionBtn').style.display = 'none'; + document.getElementById('addPermissionForm').style.display = 'block'; + + // Focus on the tool select dropdown + setTimeout(() => { + document.getElementById('addPermissionTool').focus(); + }, 100); + } + + function hideAddPermissionForm() { + document.getElementById('showAddPermissionBtn').style.display = 'flex'; + document.getElementById('addPermissionForm').style.display = 'none'; + + // Clear form inputs + document.getElementById('addPermissionTool').value = ''; + document.getElementById('addPermissionCommand').value = ''; + document.getElementById('addPermissionCommand').style.display = 'none'; + } + + function toggleCommandInput() { + const toolSelect = document.getElementById('addPermissionTool'); + const commandInput = document.getElementById('addPermissionCommand'); + const hintDiv = document.getElementById('permissionsFormHint'); + + if (toolSelect.value === 'Bash') { + commandInput.style.display = 'block'; + hintDiv.textContent = 'Use patterns like "npm i *" or "git add *" for specific commands.'; + } else if (toolSelect.value === '') { + commandInput.style.display = 'none'; + commandInput.value = ''; + hintDiv.textContent = 'Select a tool to add always-allow permission.'; + } else { + commandInput.style.display = 'none'; + commandInput.value = ''; + hintDiv.textContent = 'This will allow all ' + toolSelect.value + ' commands without asking for permission.'; + } + } + + function addPermission() { + const toolSelect = document.getElementById('addPermissionTool'); + const commandInput = document.getElementById('addPermissionCommand'); + const addBtn = document.getElementById('addPermissionBtn'); + + const toolName = toolSelect.value.trim(); + const command = commandInput.value.trim(); + + if (!toolName) { + return; + } + + // Disable button during processing + addBtn.disabled = true; + addBtn.textContent = 'Adding...'; + + vscode.postMessage({ + type: 'addPermission', + toolName: toolName, + command: command || null + }); + + // Clear form and hide it + toolSelect.value = ''; + commandInput.value = ''; + hideAddPermissionForm(); + + // Re-enable button + setTimeout(() => { + addBtn.disabled = false; + addBtn.textContent = 'Add'; + }, 500); + } // Close settings modal when clicking outside document.getElementById('settingsModal').addEventListener('click', (e) => { @@ -2446,6 +2779,10 @@ const html = ` document.getElementById('wsl-distro').value = message.data['wsl.distro'] || 'Ubuntu'; document.getElementById('wsl-node-path').value = message.data['wsl.nodePath'] || '/usr/bin/node'; document.getElementById('wsl-claude-path').value = message.data['wsl.claudePath'] || '/usr/local/bin/claude'; + document.getElementById('yolo-mode').checked = message.data['permissions.yoloMode'] || false; + + // Update yolo warning visibility + updateYoloWarning(); // Show/hide WSL options document.getElementById('wslOptions').style.display = message.data['wsl.enabled'] ? 'block' : 'none'; @@ -2460,6 +2797,11 @@ const html = ` }, 1000); } } + + if (message.type === 'permissionsData') { + // Update permissions UI + renderPermissions(message.data); + } });