Merge pull request #45 from andrepimenta/feature/permissions

Feature/permissions
This commit is contained in:
Andre Pimenta
2025-07-09 13:17:31 +01:00
committed by GitHub
8 changed files with 2259 additions and 17 deletions

View File

@@ -9,3 +9,4 @@ vsc-extension-quickstart.md
**/*.map
**/*.ts
**/.vscode-test.*
backup

715
package-lock.json generated
View File

@@ -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"
}
}
}
}

View File

@@ -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"
}
}

View File

@@ -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<string, (approved: boolean) => 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<boolean>('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<boolean>('wsl.enabled', false);
const wslDistro = config.get<string>('wsl.distro', 'Ubuntu');
const nodePath = config.get<string>('wsl.nodePath', '/usr/bin/node');
@@ -970,6 +1003,460 @@ class ClaudeChatProvider {
}
}
private async _initializeMCPConfig(): Promise<void> {
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<void> {
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<void> {
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<boolean> {
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<void> {
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<void> {
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<void> {
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<void> {
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<boolean>('wsl.enabled', false),
'wsl.distro': config.get<string>('wsl.distro', 'Ubuntu'),
'wsl.nodePath': config.get<string>('wsl.nodePath', '/usr/bin/node'),
'wsl.claudePath': config.get<string>('wsl.claudePath', '/usr/local/bin/claude')
'wsl.claudePath': config.get<string>('wsl.claudePath', '/usr/local/bin/claude'),
'permissions.yoloMode': config.get<boolean>('permissions.yoloMode', false)
};
this._postMessage({

View File

@@ -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);
});

View File

@@ -0,0 +1,10 @@
{
"mcpServers": {
"permissions": {
"command": "node",
"args": [
"./out/permissions/mcp-permissions.js"
]
}
}
}

View File

@@ -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 {

348
src/ui.ts
View File

@@ -131,8 +131,8 @@ const html = `<!DOCTYPE html>
</button>
</div>
<div class="beta-warning">
In Beta. All Claude Code tools are allowed. Use at your own risk.
<div id="yoloWarning" class="yolo-warning" style="display: none;">
⚠️ Yolo Mode Active: Claude Code can execute any tool without asking.
</div>
<!-- File picker modal -->
@@ -248,6 +248,53 @@ const html = `<!DOCTYPE html>
</div>
</div>
<h3 style="margin-top: 24px; margin-bottom: 16px; font-size: 14px; font-weight: 600;">Permissions</h3>
<div>
<p style="font-size: 11px; color: var(--vscode-descriptionForeground); margin: 0;">
Manage commands and tools that are automatically allowed without asking for permission.
</p>
</div>
<div class="settings-group">
<div id="permissionsList" class="permissions-list">
<div class="permissions-loading" style="text-align: center; padding: 20px; color: var(--vscode-descriptionForeground);">
Loading permissions...
</div>
</div>
<div class="permissions-add-section">
<div id="addPermissionForm" class="permissions-add-form" style="display: none;">
<div class="permissions-form-row">
<select id="addPermissionTool" class="permissions-tool-select" onchange="toggleCommandInput()">
<option value="">Select tool...</option>
<option value="Bash">Bash</option>
<option value="Read">Read</option>
<option value="Edit">Edit</option>
<option value="Write">Write</option>
<option value="MultiEdit">MultiEdit</option>
<option value="Glob">Glob</option>
<option value="Grep">Grep</option>
<option value="LS">LS</option>
<option value="WebSearch">WebSearch</option>
<option value="WebFetch">WebFetch</option>
</select>
<div style="flex-grow: 1; display: flex;">
<input type="text" id="addPermissionCommand" class="permissions-command-input" placeholder="Command pattern (e.g., npm i *)" style="display: none;" />
</div>
<button id="addPermissionBtn" class="permissions-add-btn" onclick="addPermission()">Add</button>
</div>
<div id="permissionsFormHint" class="permissions-form-hint">
Select a tool to add always-allow permission.
</div>
</div>
<button id="showAddPermissionBtn" class="permissions-show-add-btn" onclick="showAddPermissionForm()">
+ Add permission
</button>
<div class="yolo-mode-section">
<input type="checkbox" id="yolo-mode" onchange="updateSettings(); updateYoloWarning();">
<label for="yolo-mode">Enable Yolo Mode (Skip All Permissions)</label>
</div>
</div>
</div>
<h3 style="margin-top: 24px; margin-bottom: 16px; font-size: 14px; font-weight: 600;">MCP Configuration (coming soon)</h3>
<div>
<p style="font-size: 11px; color: var(--vscode-descriptionForeground); margin: 0;">
@@ -592,6 +639,20 @@ const html = `<!DOCTYPE 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 = \`
<div class="yolo-suggestion-text">
<span>💡 This looks like a permission issue. You can enable Yolo Mode to skip all permission checks.</span>
</div>
<button class="yolo-suggestion-btn" onclick="enableYoloMode()">Enable Yolo Mode</button>
\`;
messageDiv.appendChild(yoloSuggestion);
}
messagesDiv.appendChild(messageDiv);
messagesDiv.scrollTop = messagesDiv.scrollHeight;
}
@@ -776,6 +837,20 @@ const html = `<!DOCTYPE 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 = \`
<div class="yolo-suggestion-text">
<span>💡 This looks like a permission issue. You can enable Yolo Mode to skip all permission checks.</span>
</div>
<button class="yolo-suggestion-btn" onclick="enableYoloMode()">Enable Yolo Mode</button>
\`;
messageDiv.appendChild(yoloSuggestion);
}
messagesDiv.appendChild(messageDiv);
messagesDiv.scrollTop = messagesDiv.scrollHeight;
}
@@ -1422,6 +1497,54 @@ const html = `<!DOCTYPE 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 = `<!DOCTYPE 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 <code>\${truncatedPattern}</code>\`;
alwaysAllowTooltip = displayPattern.length > 30 ? \`title="\${displayPattern}"\` : '';
}
messageDiv.innerHTML = \`
<div class="permission-header">
<span class="icon">🔐</span>
<span>Permission Required</span>
</div>
<div class="permission-content">
<p>Allow <strong>\${toolName}</strong> to execute the tool call above?</p>
<div class="permission-buttons">
<button class="btn deny" onclick="respondToPermission('\${data.id}', false)">Deny</button>
<button class="btn always-allow" onclick="respondToPermission('\${data.id}', true, true)" \${alwaysAllowTooltip}>\${alwaysAllowText}</button>
<button class="btn allow" onclick="respondToPermission('\${data.id}', true)">Allow</button>
</div>
</div>
\`;
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 = `<!DOCTYPE html>
vscode.postMessage({
type: 'getSettings'
});
// Request current permissions
vscode.postMessage({
type: 'getPermissions'
});
settingsModal.style.display = 'flex';
} else {
hideSettingsModal();
@@ -2374,6 +2578,7 @@ const html = `<!DOCTYPE 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 = `<!DOCTYPE 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 = \`
<div class="permissions-empty">
No always-allow permissions set
</div>
\`;
return;
}
let html = '';
for (const [toolName, permission] of Object.entries(permissions.alwaysAllow)) {
if (permission === true) {
// Tool is always allowed
html += \`
<div class="permission-item">
<div class="permission-info">
<span class="permission-tool">\${toolName}</span>
<span class="permission-desc">All</span>
</div>
<button class="permission-remove-btn" onclick="removePermission('\${toolName}', null)">Remove</button>
</div>
\`;
} else if (Array.isArray(permission)) {
// Tool has specific commands/patterns
for (const command of permission) {
const displayCommand = command.replace(' *', ''); // Remove asterisk for display
html += \`
<div class="permission-item">
<div class="permission-info">
<span class="permission-tool">\${toolName}</span>
<span class="permission-command"><code>\${displayCommand}</code></span>
</div>
<button class="permission-remove-btn" onclick="removePermission('\${toolName}', '\${escapeHtml(command)}')">Remove</button>
</div>
\`;
}
}
}
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 = `<!DOCTYPE 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 = `<!DOCTYPE html>
}, 1000);
}
}
if (message.type === 'permissionsData') {
// Update permissions UI
renderPermissions(message.data);
}
});
</script>